Skip to content

Instantly share code, notes, and snippets.

@Oskang09
Last active July 24, 2024 10:17
Show Gist options
  • Save Oskang09/f7796554e4e91cbb1f583f6f2b6e83f3 to your computer and use it in GitHub Desktop.
Save Oskang09/f7796554e4e91cbb1f583f6f2b6e83f3 to your computer and use it in GitHub Desktop.
Just sharing for the digital signature.... i will keep updated when i'm free...
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;
}
}
}
@Oskang09
Copy link
Author

Oskang09 commented Jul 2, 2024

UPDATE COMMIT(2):

  1. Added 2 XPathTransform ( since mention by LHDN is a must )
  2. Fix Target "Signature" Properties ( having extra "#" based on previous version )
  3. Update Signature Schema using "http://uri.etsi.org/01903/v1.3.2#SignedProperties" instead of xmldsig one.

@Oskang09
Copy link
Author

Oskang09 commented Jul 3, 2024

UPDATE COMMIT(3,4):

  1. Modify XaDES Object returning function ( required signedProperties to perform signing for the issues DS320 )
  2. Adding Dsig Namespace Attribute to SignedProperties fields. DigestMethod, DigestValue, X509IssuerName, X509SerialNumber.
  3. 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",
    }
};

@nikihaitou
Copy link

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

@namelessinhell
Copy link

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

image
image

@nikihaitou
Copy link

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

image image

yes correct, issue solved, thanks alot

@Oskang09
Copy link
Author

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);
}

@Oskang09
Copy link
Author

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"
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment