/* $Id: SignEnvelopedSignature.cs $ * Last updated: * $Date: 2022-01-29 15:43:00 $ * $Version: 0.9.1 $ */ /******************************* LICENSE *********************************** * Copyright (C) 2022 David Ireland, DI Management Services Pty Limited. * All rights reserved. <https://di-mgt.com.au> <https://cryptosys.net> * The code in this module is licensed under the terms of the MIT license. * For a copy, see <http://opensource.org/licenses/MIT> **************************************************************************** */ using System; using System.Text; using System.Text.RegularExpressions; using System.IO; using System.Diagnostics; /* * Requires DI Management CryptoSys Libraries available from <https://cryptosys.net/>. * 1. CryptoSys PKI Pro: https://cryptosys.net/pki/ * 2. SC14N, a straightforward XML canonicalization utility: https://cryptosys.net/sc14n/ * EITHER add references to * `diCrSysPKINet.dll` (installed by default in `C:\Program Files (x86)\CryptoSysPKI\DotNet`) * `diSc14nNet.dll` (installed by default in `C:\Program Files (x86)\Sc14n\DotNet`) * OR add the C# source code files `CryptoSysPKI.cs` and `diSc14nNet.cs` * directly to your project. */ using Pki = CryptoSysPKI; using Sc14n; namespace DIManagement.SignEnvelopedSignature { class SignEnvelopedSignature { static void Main(string[] args) { string signedDoc; // Setup to display debugging info in console in Debug mode // (Should be ignored in final Release mode) TextWriterTraceListener[] listeners = new TextWriterTraceListener[] { new TextWriterTraceListener(Console.Out) }; Debug.Listeners.AddRange(listeners); // Debug to check libraries are available and where Debug.WriteLine("PKI Version=" + Pki.General.Version() + " " + Pki.General.ModuleName() + " [" + Pki.General.CompileTime() + "]"); Debug.WriteLine("SC14N Version=" + Sc14n.Gen.Version() + " " + Sc14n.Gen.ModuleName() + " [" + Sc14n.Gen.CompileTime() + "]"); // Require minimum PKI if (Pki.General.Version() < 200000) { Console.WriteLine("PKI version must be 20.0.0 or above"); return; } // Show current working directory Debug.WriteLine("CWD=" + System.IO.Directory.GetCurrentDirectory()); // Do the business... signedDoc = SignDoc("cancelacion-2022-signed.xml", "cancelacion-2022-base.xml", "emisor2021.cer", "emisor.key", password:"12345678a", noFlatten:false); // Returns either the name of the file just created or an error message beginning with "**ERROR" if (signedDoc.IndexOf("**ERROR") >= 0) { Console.WriteLine(signedDoc); } else { Console.WriteLine("Created file '{0}'", signedDoc); } } /// <summary> /// Sign an enveloped-signature XML document /// </summary> /// <param name="outputxmlFile">Name of signed output file to be created</param> /// <param name="baseXmlFile">Path to base XML document.</param> /// <param name="certFile">Filename of X.509 certificate file, or certificate-as-a-string</param> /// <param name="keyFile">Filename of matching private key (or PEM string)</param> /// <param name="password">Password for encrypted private key ("" if not encrypted)</param> /// <param name="noFlatten">Set as <c>true</c> to leave XML in original format (default=flatten to a single line)</param> /// <returns>String containing the name of the output document or an error message beginning "***ERROR:"</returns> /// <remarks> /// The base XML document is expected to be a well-formed XML document with placeholders of the form <c>@!...!@</c>. /// This code assumes specific hard-coded values are set in the Signature template. /// The Reference must have <c>URI=""</c>, the Transform algorithm must be "http://www.w3.org/2000/09/xmldsig#enveloped-signature". /// The SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1", /// the DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1", and /// the CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315". /// The placeholders <c>@!DIGVAL!@</c> and <c>@!SIGVAL!@</c> are mandatory. /// All other placeholders are optional and will be ignored if not present. /// </remarks> static string SignDoc(string outputxmlFile, string baseXmlFile, string certFile, string keyFile, string password, bool noFlatten = false) { // DEFAULT ALGORITHMS // These match the algorithms hard-coded into the Signature template // (change the template, remember to change these) const Pki.SigAlgorithm sigAlg = Pki.SigAlgorithm.Rsa_Sha1; const Sc14n.DigAlg digAlg = Sc14n.DigAlg.Sha1; const Sc14n.TranMethod tranMethod = Sc14n.TranMethod.Inclusive; string xmlStr; string strMsg; byte[] b; StringBuilder sbPriKey; string pubKey, certStr; string s, sidigval; // Read in base XML document and flatten it, if necessary xmlStr = File.ReadAllText(baseXmlFile); if (!noFlatten) xmlStr = FlattenXml(xmlStr); // Read in private key, cert as a string, and public key sbPriKey = Pki.Rsa.ReadPrivateKey(keyFile, password); if (sbPriKey.Length == 0) { strMsg = String.Format("**ERROR: Failed to read private key file '{0}'", keyFile); return strMsg; } certStr = Pki.X509.ReadStringFromFile(certFile); if (certStr.Length == 0) { strMsg = String.Format("**ERROR: Failed to read X.509 certificate in '{0}'", certFile); return strMsg; } // We will use the cert string from now on pubKey = Pki.Rsa.ReadPublicKey(certStr).ToString(); if (pubKey.Length == 0) { strMsg = String.Format("**ERROR: Could not extract public keyFile from certificate"); return strMsg; } // Check private and public keys match if (Pki.Rsa.KeyMatch(sbPriKey.ToString(), pubKey) != 0) { return ("***ERROR: private key does not match public key in certificate"); } // Set Timestamp // NOTE: if @!TIMESTAMP!@" placeholder does not exist, this has no effect, // so you can hardcode an fixed timestamp in the baseXML if you wish s = IsoTime_Local(); Debug.WriteLine("SigningTime=" + s); xmlStr = xmlStr.Replace("@!TIMESTAMP!@", s); // Fill in the KeyInfo values, if they exist. s = certStr; xmlStr = xmlStr.Replace("@!CERTIFICATE!@", s); s = Pki.X509.QueryCert(certStr, "serialNumber", Pki.X509.OutputOpts.Decimal); xmlStr = xmlStr.Replace("@!SERIALNUMBER!@", s); s = Pki.X509.QueryCert(certStr, "issuerName", Pki.X509.OutputOpts.Ldap); xmlStr = xmlStr.Replace("@!ISSUERNAME!@", s); s = Pki.Rsa.ToXMLString(pubKey, 0); xmlStr = xmlStr.Replace("@!RSAKEYVALUE!@", s); // Get XML as a byte array // Assumes base file is UTF-8 encoded b = System.Text.Encoding.UTF8.GetBytes(xmlStr); // Compute the digest value over the canonicalized data excluding the Signature element s = Sc14n.C14n.ToDigest(b, "Signature", Sc14n.Tran.OmitByTag, digAlg); Debug.WriteLine("DigestValue=" + s); // Set the DigestValue in the original string xmlStr = xmlStr.Replace("@!DIGVAL!@", s); // Now back to a temp byte array to perform C14N on SignedInfo b = System.Text.Encoding.UTF8.GetBytes(xmlStr); // Compute digest of SignedInfo sidigval = Sc14n.C14n.ToDigest(b, "SignedInfo", Tran.SubsetByTag, digAlg, tranMethod); Debug.WriteLine("SignedInfo Digest=" + sidigval); if (sidigval.Length == 0) { strMsg = String.Format("**ERROR: Failed to compute C14N digest value: '{0}'", Sc14n.Err.LastError()); return strMsg; } // Compute signature value and insert in Signature string s = Pki.Sig.SignDigest(Pki.Cnv.FromBase64(sidigval), sbPriKey.ToString(), "", sigAlg); // Clean up private key Pki.Wipe.String(sbPriKey); Debug.WriteLine(String.Format("SignatureValue=" + s)); if (s.Length == 0) { strMsg = String.Format("**ERROR: Failed to compute signature: '{0}'", Pki.General.LastError()); return strMsg; } xmlStr = xmlStr.Replace("@!SIGVAL!@", s); // Final check for uncompleted '@!..!@' int nret = xmlStr.IndexOf("@!"); if (nret >= 0) { Debug.WriteLine("WARNING: uncompleted '@!..!@' items"); } // Output XML should now be complete // Final check (Note convert string to UTF-8 bytes before passing) s = Sc14n.C14n.ToDigest(Encoding.UTF8.GetBytes(xmlStr), "", 0, 0, 0); if (s.Length == 0) { strMsg = String.Format("**ERROR: XML internal problem: '{0}'", Sc14n.Err.LastError()); return strMsg; } Debug.WriteLine("FINAL XML:"); Debug.WriteLine(xmlStr); // Write final document string to output file File.WriteAllText(outputxmlFile, xmlStr); // Return full path name of output file created FileInfo fi = new FileInfo(outputxmlFile); return fi.FullName; } static string IsoTime_Local() { // Compute current signing time in <xs:datetime> form return DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"); } /// <summary> /// Flatten XML data in a string /// </summary> /// <param name="s">String containing XML data</param> /// <returns>XML data with no whitespace between elements</returns> static string FlattenXml(string s) { s = Regex.Replace(s, @"^\s+", "", RegexOptions.Multiline); s = Regex.Replace(s, @"\s+$", "", RegexOptions.Multiline); s = s.Replace("\r\n", " ").Replace("\n", " "); s = Regex.Replace(s, @">\s+<", @"><"); return s; } } }