-
-
Save Oskang09/f7796554e4e91cbb1f583f6f2b6e83f3 to your computer and use it in GitHub Desktop.
using System; | |
using System.Collections.Generic; | |
using System.Globalization; | |
using System.IO; | |
using System.Linq; | |
using System.Numerics; | |
using System.Runtime.Intrinsics.Arm; | |
using System.Security.Cryptography; | |
using System.Security.Cryptography.X509Certificates; | |
using System.Security.Cryptography.Xml; | |
using System.Text; | |
using System.Threading.Tasks.Dataflow; | |
using System.Xml; | |
using System.Xml.Linq; | |
using System.Xml.XPath; | |
using DotNetEnv; | |
using invoicehub.model; | |
using LinqKit; | |
using Microsoft.AspNetCore.SignalR.Protocol; | |
namespace invoicehub.util | |
{ | |
public static class UblSignature | |
{ | |
private const string SignatureID = "signature"; | |
private const string SignaturePropertiesID = "id-xades-signed-props"; | |
public static XmlElement SignWithXAdES(X509Certificate2 signingCertificate, XmlDocument xmlDocument) | |
{ | |
var signedXml = new XadesSignedXml(xmlDocument); | |
signedXml.Signature.Id = SignatureID; | |
signedXml.SigningKey = signingCertificate.GetRSAPrivateKey(); | |
var keyInfo = new KeyInfo(); | |
keyInfo.AddClause(new KeyInfoX509Data(signingCertificate)); | |
signedXml.KeyInfo = keyInfo; | |
var transformReference = new Reference { Id = "id-doc-signed-data", Uri = "" }; | |
transformReference.AddTransform(CreateXPathTransform( | |
"not(//ancestor-or-self::ext:UBLExtensions)", | |
"ext", | |
"urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" | |
)); | |
transformReference.AddTransform(CreateXPathTransform( | |
"not(//ancestor-or-self::cac:Signature)", | |
"cac", | |
"urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" | |
)); | |
transformReference.AddTransform(new XmlDsigC14NTransform()); | |
var (xadesObject, signedProperties) = GetXadesObject(xmlDocument, signingCertificate); | |
var signedReference = new Reference | |
{ | |
Uri = $"#{SignaturePropertiesID}", | |
Type = XadesSignedXml.XmlDsigSignatureProperties, | |
}; | |
signedXml.AddReference(transformReference); | |
signedXml.AddReference(signedReference); | |
signedXml.AddObject(xadesObject); | |
signedXml.ComputeSignature(); | |
var signed = signedXml.GetXml(); | |
XmlNamespaceManager nsManager = new XmlNamespaceManager(xmlDocument.NameTable); | |
nsManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#"); | |
var propsDigestNode = signed.SelectSingleNode("/ds:SignedInfo/ds:Reference[@URI='#id-xades-signed-props']/ds:DigestValue", nsManager); | |
using (SHA256 sha256 = SHA256.Create()) | |
{ | |
AddDsigPrefix(signedProperties); | |
var bytes = Encoding.UTF8.GetBytes(signedProperties.OuterXml); | |
byte[] digestBytes = sha256.ComputeHash(bytes); | |
propsDigestNode.InnerText = Convert.ToBase64String(digestBytes); | |
} | |
var docDigestNode = signed.SelectSingleNode("/ds:SignedInfo/ds:Reference[@Id='id-doc-signed-data' and @URI='']/ds:DigestValue", nsManager); | |
var signatureNode = signed.SelectSingleNode("/ds:SignatureValue", nsManager); | |
using (SHA256 sha256 = SHA256.Create()) | |
{ | |
var docDigestBytes = Convert.FromBase64String(docDigestNode.InnerText); | |
var signatureBytes = signingCertificate.GetRSAPrivateKey().SignHash(docDigestBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); | |
signatureNode.InnerText = Convert.ToBase64String(signatureBytes); | |
} | |
XNamespace nsSig = "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-2"; | |
XNamespace nsSac = "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2"; | |
XNamespace nsSbc = "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2"; | |
XNamespace nsCbc = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"; | |
var udsWrapper = new XElement(nsSig + "UBLDocumentSignatures", | |
new XAttribute(XNamespace.Xmlns + "sig", nsSig), | |
new XAttribute(XNamespace.Xmlns + "sac", nsSac), | |
new XAttribute(XNamespace.Xmlns + "sbc", nsSbc), | |
new XAttribute(XNamespace.Xmlns + "cbc", nsCbc), | |
new XElement(nsSac + "SignatureInformation", | |
new XElement(nsCbc + "ID", "urn:oasis:names:specification:ubl:signature:1"), | |
new XElement(nsSbc + "ReferencedSignatureID", "urn:oasis:names:specification:ubl:signature:Invoice"), | |
XElement.Parse(signed.OuterXml) | |
) | |
); | |
var signDoc = new XmlDocument(); | |
using (var udsReader = udsWrapper.CreateReader()) | |
{ | |
signDoc.Load(udsWrapper.CreateReader()); | |
} | |
return signDoc.DocumentElement; | |
} | |
private static (DataObject, XmlElement) GetXadesObject(XmlDocument document, X509Certificate2 signingCertificate) | |
{ | |
// <Object> | |
var objectNode = document.CreateElement("Object", SignedXml.XmlDsigNamespaceUrl); | |
// <Object><QualifyingProperties> | |
var qualifyingPropertiesNode = document.CreateElement(XadesSignedXml.XadesPrefix, "QualifyingProperties", XadesSignedXml.XadesNamespaceUrl); | |
qualifyingPropertiesNode.SetAttribute("Target", $"{SignatureID}"); | |
objectNode.AppendChild(qualifyingPropertiesNode); | |
// <Object><QualifyingProperties><SignedProperties> | |
var signedPropertiesNode = document.CreateElement(XadesSignedXml.XadesPrefix, "SignedProperties", XadesSignedXml.XadesNamespaceUrl); | |
signedPropertiesNode.SetAttribute("Id", SignaturePropertiesID); | |
qualifyingPropertiesNode.AppendChild(signedPropertiesNode); | |
// <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties> | |
var signedSignaturePropertiesNode = document.CreateElement(XadesSignedXml.XadesPrefix, "SignedSignatureProperties", XadesSignedXml.XadesNamespaceUrl); | |
signedPropertiesNode.AppendChild(signedSignaturePropertiesNode); | |
// <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties> </SigningTime> | |
var signingTime = document.CreateElement(XadesSignedXml.XadesPrefix, "SigningTime", XadesSignedXml.XadesNamespaceUrl); | |
signingTime.InnerText = $"{DateTime.UtcNow:s}Z"; | |
signedSignaturePropertiesNode.AppendChild(signingTime); | |
// <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate> | |
var signingCertificateNode = document.CreateElement(XadesSignedXml.XadesPrefix, "SigningCertificate", XadesSignedXml.XadesNamespaceUrl); | |
signedSignaturePropertiesNode.AppendChild(signingCertificateNode); | |
// <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert> | |
var certNode = document.CreateElement(XadesSignedXml.XadesPrefix, "Cert", XadesSignedXml.XadesNamespaceUrl); | |
signingCertificateNode.AppendChild(certNode); | |
// <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><CertDigest> | |
var certDigestNode = document.CreateElement(XadesSignedXml.XadesPrefix, "CertDigest", XadesSignedXml.XadesNamespaceUrl); | |
certNode.AppendChild(certDigestNode); | |
// <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><CertDigest> </DigestMethod> | |
var digestMethod = document.CreateElement("DigestMethod", SignedXml.XmlDsigNamespaceUrl); | |
var digestMethodAlgorithmAtribute = document.CreateAttribute("Algorithm"); | |
digestMethodAlgorithmAtribute.InnerText = SignedXml.XmlDsigSHA256Url; | |
digestMethod.Attributes.Append(digestMethodAlgorithmAtribute); | |
AddDsigNamespaceAttr(digestMethod); | |
certDigestNode.AppendChild(digestMethod); | |
// <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><CertDigest> </DigestMethod> | |
var digestValue = document.CreateElement("DigestValue", SignedXml.XmlDsigNamespaceUrl); | |
digestValue.InnerText = Convert.ToBase64String(signingCertificate.GetCertHash(HashAlgorithmName.SHA256)); | |
AddDsigNamespaceAttr(digestValue); | |
certDigestNode.AppendChild(digestValue); | |
// <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><IssuerSerial> | |
var issuerSerialNode = document.CreateElement(XadesSignedXml.XadesPrefix, "IssuerSerial", XadesSignedXml.XadesNamespaceUrl); | |
certNode.AppendChild(issuerSerialNode); | |
// <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><IssuerSerial> </X509IssuerName> | |
var x509IssuerName = document.CreateElement("X509IssuerName", SignedXml.XmlDsigNamespaceUrl); | |
x509IssuerName.InnerText = signingCertificate.Issuer; | |
AddDsigNamespaceAttr(x509IssuerName); | |
issuerSerialNode.AppendChild(x509IssuerName); | |
// <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><IssuerSerial> </X509SerialNumber> | |
var x509SerialNumber = document.CreateElement("X509SerialNumber", SignedXml.XmlDsigNamespaceUrl); | |
x509SerialNumber.InnerText = ToDecimalString(signingCertificate.SerialNumber); | |
AddDsigNamespaceAttr(x509SerialNumber); | |
issuerSerialNode.AppendChild(x509SerialNumber); | |
var dataObject = new DataObject { Data = qualifyingPropertiesNode.SelectNodes(".") }; | |
return (dataObject, signedPropertiesNode); | |
} | |
private static string ToDecimalString(string serialNumber) | |
{ | |
BigInteger bi; | |
if (BigInteger.TryParse(serialNumber, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out bi)) | |
{ | |
return bi.ToString(CultureInfo.InvariantCulture); | |
} | |
else | |
{ | |
return serialNumber; | |
} | |
} | |
private static XmlDsigXPathTransform CreateXPathTransform(string value, string prefix, string uri) | |
{ | |
XmlDsigXPathTransform transform = new XmlDsigXPathTransform { Algorithm = "http://www.w3.org/TR/1999/REC-xpath-19991116" }; | |
XmlDocument doc = new XmlDocument { PreserveWhitespace = true }; | |
XmlElement element = doc.CreateElement("XPath"); | |
element.InnerText = value; | |
element.SetAttribute("xmlns:" + prefix, uri); | |
transform.LoadInnerXml(element.SelectNodes(".")); | |
return transform; | |
} | |
private static void AddDsigPrefix(XmlNode node) | |
{ | |
if (string.IsNullOrEmpty(node.Prefix)) | |
{ | |
node.Prefix = "ds"; | |
} | |
foreach (XmlNode child in node.ChildNodes) | |
{ | |
AddDsigPrefix(child); | |
} | |
} | |
private static void AddDsigNamespaceAttr(XmlElement element) | |
{ | |
var dsAttr = element.OwnerDocument.CreateAttribute("xmlns:ds"); | |
dsAttr.Value = "http://www.w3.org/2000/09/xmldsig#"; | |
element.Attributes.Append(dsAttr); | |
} | |
} | |
public class XadesSignedXml : SignedXml | |
{ | |
public const string XadesPrefix = "xades"; | |
public const string XmlDsigSignatureProperties = "http://www.w3.org/2000/09/xmldsig#SignatureProperties"; | |
public const string XadesNamespaceUrl = "http://uri.etsi.org/01903/v1.3.2#"; | |
public XmlElement PropertiesNode { get; set; } | |
private readonly List<DataObject> _dataObjects = new List<DataObject>(); | |
public XadesSignedXml(XmlDocument document) : base(document) { } | |
public override XmlElement GetIdElement(XmlDocument document, string idValue) | |
{ | |
if (string.IsNullOrEmpty(idValue)) | |
return null; | |
var xmlElement = base.GetIdElement(document, idValue); | |
if (xmlElement != null) | |
return xmlElement; | |
if (_dataObjects.Count == 0) | |
return null; | |
foreach (var dataObject in _dataObjects) | |
{ | |
var nodeWithSameID = FindNodeWithPrefix(dataObject.Data, "Id", idValue); | |
if (nodeWithSameID != null) | |
return nodeWithSameID; | |
} | |
return null; | |
} | |
public new void AddObject(DataObject dataObject) | |
{ | |
base.AddObject(dataObject); | |
_dataObjects.Add(dataObject); | |
} | |
private XmlElement FindNodeWithPrefix(XmlNodeList nodeList, string idKey, string idValue) | |
{ | |
foreach (XmlNode node in nodeList) | |
{ | |
var attribute = node.Attributes[idKey]; | |
if (attribute != null && attribute.Value == idValue) | |
return (XmlElement)node; | |
if (!node.HasChildNodes) continue; | |
var foundNode = FindNodeWithPrefix(node.ChildNodes, idKey, idValue); | |
if (foundNode != null) return foundNode; | |
} | |
return null; | |
} | |
} | |
} |
UPDATE COMMIT(3,4):
- Modify XaDES Object returning function ( required signedProperties to perform signing for the issues DS320 )
- Adding Dsig Namespace Attribute to SignedProperties fields. DigestMethod, DigestValue, X509IssuerName, X509SerialNumber.
- Re-calculate hash with SignedProperties from (1) and replace the signature calculate by
SignedXml
WARN: This doesn't set the docDigest
which let SignedXml
to handle that, so when passing document to SignWithXAdES
function, the document must without Signature
block else you have to calculate the docDigest
at your own.
Note: If you're using UBLSharp you can refer as per below, invoice
was the InvoiceType
class from UBLSharp.
using (var reader = invoice.ToXDocument().CreateReader())
{
var document = new XmlDocument()
{
PreserveWhitespace = true
};
document.Load(reader);
var certificate = X509Certificate2.CreateFromPem(serviceAccount.Certificate);
certificate = certificate.CopyWithPrivateKey(DataMapper.CERTIFICATE_PRIVATE_KEY);
invoice.UBLExtensions = new List<UBLExtensionType>()
{
new ()
{
ExtensionURI = "urn:oasis:names:specification:ubl:dsig:enveloped:xades",
ExtensionContent = UblSignature.SignWithXAdES(certificate, document),
}
};
}
invoice.Signature = new List<SignatureType>{
new SignatureType {
ID = "urn:oasis:names:specification:ubl:signature:Invoice",
SignatureMethod = "urn:oasis:names:specification:ubl:dsig:enveloped:xades",
}
};
hi, your method is works, do you have example how to calc own docDigest instead of let signedXml to handle? because after the latest LHDN updated now got error DS333
For quick fix , create a method to recalculate the signature manually , then override the value at the end of SignWithXAdES method.
Thanks to https://www.facebook.com/groups/developerkaki/permalink/2216468748699026/?mibextid=K35XfP&rdid=WqCabbBliA0qYyIQ&share_url=https%3A%2F%2Fwww.facebook.com%2Fshare%2Fp%2FK87TtkhuszScN8EK%2F%3Fmibextid%3DK35XfP
For quick fix , create a method to recalculate the signature manually , then override the value at the end of SignWithXAdES method. Thanks to https://www.facebook.com/groups/developerkaki/permalink/2216468748699026/?mibextid=K35XfP&rdid=WqCabbBliA0qYyIQ&share_url=https%3A%2F%2Fwww.facebook.com%2Fshare%2Fp%2FK87TtkhuszScN8EK%2F%3Fmibextid%3DK35XfP
yes correct, issue solved, thanks alot
UPDATE COMMIT(5):
As per previous comment from @nikihaitou & @namelessinhell , will have to re-calculate the signature and append back , the main changes as per below. Mainly for fixing issues DS333.
var docDigestNode = signed.SelectSingleNode("/ds:SignedInfo/ds:Reference[@Id='id-doc-signed-data' and @URI='']/ds:DigestValue", nsManager);
var signatureNode = signed.SelectSingleNode("/ds:SignatureValue", nsManager);
using (SHA256 sha256 = SHA256.Create())
{
var docDigestBytes = Convert.FromBase64String(docDigestNode.InnerText);
var signatureBytes = signingCertificate.GetRSAPrivateKey().SignHash(docDigestBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
signatureNode.InnerText = Convert.ToBase64String(signatureBytes);
}
UPDATE:
Just got the certificate and was able to submit document with valid status.
{
"uuid": "M1NW5KPXTFNBH7PT1KZHZH3J10",
"submissionUid": "TTK0N9Q36G00W7GA1KZHZH3J10",
"longId": "Q3RBV9B2G4T5T4P01KZHZH3J10djj7Dx1721811664",
"typeName": "Invoice",
"typeVersionName": "Version 2",
"issuerTin": "#hidden",
"issuerName": "#hidden",
"receiverId": "#hidden",
"receiverName": "#hidden",
"dateTimeReceived": "2024-07-24T09:01:04Z",
"dateTimeValidated": "2024-07-24T09:01:05Z",
"totalExcludingTax": 6.3,
"totalDiscount": 0.0,
"totalNetAmount": 6.3,
"totalPayableAmount": 6.37,
"status": "Valid",
"createdByUserId": "C25044560030:68b189a9-4bd0-4230-8f04-8575c63f77dd",
"documentStatusReason": null,
"cancelDateTime": null,
"rejectRequestDateTime": null,
"validationResults": null,
"internalId": "INV240724090103998564",
"dateTimeIssued": "2024-07-23T11:50:00Z"
}
UPDATE COMMIT(2):