Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 66 additions & 16 deletions xmlsignature/UBLXMLSignature.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Text;
using System.Globalization;
Expand All @@ -10,11 +10,16 @@
using System.Xml.Serialization;
using System.Xml.XPath;
using System.Security.Cryptography;
using System.Numerics;

/// <summary>
/// Author : TH MOK
/// Date : 16/07/2024
/// Description : To manually generate the UBLExtensions scratch. Cause LHDN keep changing. Easy to maintain
///
/// Update By : KKurosagi
/// Date : 22/07/2024
/// Description : Add "xmlns:ext" at invoice tag if not exist. minor code tuning to generate VALID Xml. Use BigInt to parse certificate serial number
/// </summary>
namespace AngelMay.EInvoiceLib.Utility
{
Expand All @@ -39,9 +44,9 @@ public static class UBLSignatureXML
public static XmlDocument xmlInv;

static XNamespace xmlns = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2";
static XNamespace cac = "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-2";
static XNamespace cac = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
static XNamespace cbc = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";
static XNamespace ext = "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2";
static XNamespace ext = "urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2";
static XNamespace sac = "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2";
static XNamespace sbc = "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2";
static XNamespace sig = "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-2";
Expand All @@ -59,17 +64,53 @@ public static class UBLSignatureXML
/// <param name="docXml"></param>
public static void StartGenerate(string docXml)
{
invXmlString = docXml;
//insert extensions for Ext. MUST HAVE even though the child xml didn't have extension 'ext'
XmlDocument tmpInv = new XmlDocument();
tmpInv.LoadXml(docXml);
if (tmpInv.FirstChild.Attributes["xmlns:ext"] is null)
{
System.Xml.XmlAttribute newAttribute = tmpInv.CreateAttribute("xmlns:ext");
newAttribute.Value = ext.NamespaceName;
tmpInv.FirstChild.Attributes.Append(newAttribute);
}

invXmlString = XmlDsigC14N(tmpInv.OuterXml);
doc = new XmlDocument();
doc.PreserveWhitespace = false;
ReadCertFile();
PrepareDigestData();

//create a temp Invoice xml, cause need to include all the header xmlns
XElement root = InvoiceRoot();

var mainXMl = ToXmlElement(root, doc);
//now doc contains the whole UBLExtensions struture and data
doc.AppendChild(mainXMl);

//replace <ds:DigestMethod ...></ds:DigestMethod> => <ds:DigestMethod ... />
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
addNameSpace(nsmgr);

// Find all ds:DigestMethod elements
XmlNodeList digestMethods = doc.SelectNodes("//ds:DigestMethod", nsmgr);

// Remove any child nodes to make them self-closing, but keep attributes
foreach (System.Xml.XmlNode digestMethod in digestMethods)
{
// Create a new empty element with the same name and attributes
System.Xml.XmlElement emptyElement = doc.CreateElement(digestMethod.Prefix, digestMethod.LocalName, digestMethod.NamespaceURI);
if (digestMethod.Attributes != null)
{
foreach (System.Xml.XmlAttribute attr in digestMethod.Attributes)
{
emptyElement.Attributes.Append((System.Xml.XmlAttribute)attr.CloneNode(true));
}
}

// Replace the original element with the new empty element
digestMethod.ParentNode.ReplaceChild(emptyElement, digestMethod);
}

//get the SignedProperties via xpath
var propSignXml = getSignPropertiesXML();
propSignXml = RemoveXmlDeclaration(propSignXml);
Expand All @@ -96,11 +137,16 @@ private static void InsertUBLExtIntoMainDoc()
var UBLExtensionsXML = docXML.Substring(start, end - start + "</ext:UBLExtensions>".Length);
var signatureXML = docXML.Substring(start2, end2 - start2 + "</cac:Signature>".Length);

invXmlString = xmlInv.OuterXml;
//===========================


start = invXmlString.IndexOf("<cbc:ID>");
var content = invXmlString.Insert(start, UBLExtensionsXML);
start = content.IndexOf("<cac:AccountingSupplierParty>");
content = content.Insert(start, signatureXML);
content = XmlDsigC14N(content);

//content = RunC14N(content);
xmlInv = new XmlDocument();
xmlInv.LoadXml(content);
}
Expand All @@ -111,15 +157,16 @@ private static byte[] GenerateDocHash()
string canonicalXml = XmlDsigC14N(invXmlString);
xmlInv = new XmlDocument();
xmlInv.LoadXml(canonicalXml);
var processedXml = PreprocessXml(xmlInv.OuterXml);
return Sha256Hash(RunC14N(processedXml));
var processedXml = PreprocessXml(canonicalXml);
return Sha256Hash(processedXml);
//return Encoding.UTF8.GetBytes(processedXml);

}

private static void ReadCertFile()
{
string certPath = "your cert full path";
string certPass = "your cert passsowrd";
cert = new X509Certificate2();
cert.Import(File.ReadAllBytes(certPath), certPass, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
}

Expand All @@ -135,7 +182,8 @@ static void PrepareDigestData()
_signData.SignatureValue = Convert.ToBase64String(sign);
_signData.X509Certificate = Convert.ToBase64String(cert.RawData);
_signData.X509IssuerName = cert.Issuer;
_signData.X509SerialNumber = Int64.Parse(cert.SerialNumber, NumberStyles.HexNumber).ToString();
//_signData.X509SerialNumber = Int64.Parse(cert.SerialNumber, NumberStyles.HexNumber).ToString();
_signData.X509SerialNumber = BigInteger.Parse(cert.SerialNumber, NumberStyles.HexNumber).ToString();
_signData.X509SubjectName = cert.Subject;
_signData.SigningTime = DateTime.UtcNow.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
}
Expand All @@ -148,7 +196,7 @@ static string getSignPropertiesXML()

string xpath = "/a:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties";
var element = doc.SelectSingleNode(xpath, nsmgr);

return LinearizeXml(element.OuterXml);

}
Expand Down Expand Up @@ -183,9 +231,9 @@ private static void addNameSpace(XmlNamespaceManager nsmgr)
nsmgr.AddNamespace("xades", xades.NamespaceName);
}

static XmlElement ToXmlElement(this XElement xelement, XmlDocument xmldoc)
static System.Xml.XmlElement ToXmlElement(this XElement xelement, XmlDocument xmldoc)
{
return xmldoc.ReadNode(xelement.CreateReader()) as XmlElement;
return xmldoc.ReadNode(xelement.CreateReader()) as System.Xml.XmlElement;
}
static XElement InvoiceRoot()
{
Expand Down Expand Up @@ -366,7 +414,7 @@ static XElement SigningCertificate()
new XElement(xades + "Cert",
new XElement(xades + "CertDigest",
new XElement(ds + "DigestMethod",
new XAttribute("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256"), ""),
new XAttribute("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")),
new XElement(ds + "DigestValue", _signData.CertDigest)
),
new XElement(xades + "IssuerSerial",
Expand Down Expand Up @@ -395,7 +443,7 @@ private static string RemoveSignatureElement(string xml)
XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);
nsManager.AddNamespace("cac", "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2");

XmlNode signatureNode = doc.SelectSingleNode("//cac:Signature", nsManager);
System.Xml.XmlNode signatureNode = doc.SelectSingleNode("//cac:Signature", nsManager);
if (signatureNode != null)
{
signatureNode.ParentNode.RemoveChild(signatureNode);
Expand All @@ -414,6 +462,7 @@ private static string RunC14N(string xml)

using (var ms = new System.IO.MemoryStream())
{

var stream = (Stream)transform.GetOutput(typeof(Stream));
stream.CopyTo(ms);
ms.Position = 0;
Expand Down Expand Up @@ -464,7 +513,8 @@ public static string XmlDsigC14N(string xmlString)
// Create a XmlDsigC14NTransform object
XmlDsigC14NTransform transform = new XmlDsigC14NTransform();
// Load XML data into the transform
transform.LoadInput(xmlDoc);
transform.LoadInput(xmlDoc);

// Perform canonicalization
Stream outputStream = (Stream)transform.GetOutput(typeof(Stream));

Expand Down Expand Up @@ -497,7 +547,7 @@ static byte[] SignData(byte[] hashdata, X509Certificate2 cert)
{
byte[] signedData = null;
using (RSA rsa = cert.GetRSAPrivateKey())
{
{
try
{
var sharedParameters = rsa.ExportParameters(false);
Expand Down