/* $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;
}
}
}