Code, Code, Revolution!
Signing of XML documents make up the foundation for SAML security by providing proof that a message hasn’t been tampered with by a third party. If you’ve ever worked with certificates and signing XML in .NET 2+ you know that it’s not a big deal. The X509Certificate2 class make loading public and private keys from certificates on disk or certificate store a breeze. While you’re probably looking forward to .NET 4 just as much as I am, sometimes your customers have old platforms that just can’t be upgraded to .NET 2.0+ by recompiling it. So if you’re stuck with .NET 1.1 you will run into problems handling certificates. The solution for .NET 1.1, and other languages as well of course, is a library from Microsoft called CAPICOM. While CAPICOM isn’t a .NET library it’s easily made available in .NET using Interop.
Download CAPICOM SDK from Microsoft
Build the interop-dll by starting up “Visual Studio 2003 Command Prompt” and run the following command in the same directory as the CAPICOM dll:
tlbimp capicom.dll /out:Interop.CAPICOM_NET1.dll
The interop dll is included in my example below
CAPICOM isn’t exactly the X509Certificate2 class for .NET 1, there’s a bit of work to get your signing and verifying on the road. I’ve built a help class which will load your certificates and provide the RSA-class you need to use SignXml, it’s available, as usual, at the end of this post. The demo project containing my helper class is built using VS 2008 but the helper class itself compiles in .NET 1.1. In the CapicomLibrary project folder there’s a bat file that compiles the library into .NET1.1 code (given that you have .NET 1.1 framework installed of course). The reason I’ve chosen to use .NET 2 for my demo project is so that I can use X509Certificate2 to verify/visualize that my code is actually working as intended. Certificates are also included in the demo for testing purposes. Find out more about creating certs using Makecert.exe here.
I hope this post will save you the time wasted (more or less) searching for a valid and compatible solution to sign and verify XML using .NET 1.1.
The Code
using System; using System.Text; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Runtime.InteropServices; using System.Security.Cryptography.Xml; using System.Xml; using Interop.CAPICOM_NET1; using System.IO; namespace CapicomLibrary { public class CertHelper { /// <summary> /// Returns a RSA object containing the private key /// from a certificate in the local machine certificate store /// </summary> /// <param name="certSubjectName">Subject name of the cert to load</param> /// <param name="certStoreName">Name of the store to load the cert from.</param> /// <returns>Returns null if the certificate wasn't found</returns> public static RSA LoadPrivateKeyFromStore(string certSubjectName, string certStoreName) { Certificate capiCert = LoadCertificateFromStore(certSubjectName, certStoreName); RSA rsaObject = null; // Release com objects if (capiCert != null) { rsaObject = CreatePrivateRSAFromCert(capiCert); Marshal.ReleaseComObject(capiCert); } return rsaObject; } /// <summary> /// Load a RSA object containing the private key /// from a pfx certificate. /// </summary> /// <param name="filePath">Path to the pfx certificate</param> /// <param name="password">Password needed to read the private key</param> /// <returns></returns> public static RSA LoadPrivateKeyFromPFXFile(string filePath, string password) { Certificate c = new Certificate(); c.Load(filePath, password, CAPICOM_KEY_STORAGE_FLAG.CAPICOM_KEY_STORAGE_EXPORTABLE, CAPICOM_KEY_LOCATION.CAPICOM_LOCAL_MACHINE_KEY); RSA rsaObject = CreatePrivateRSAFromCert(c); if (c != null) Marshal.ReleaseComObject(c); return rsaObject; } /// <summary> /// Load a public key from the computers' certificate store. /// </summary> /// <param name="store">Store to load the certificate from</param> /// <param name="serialNr">Cert serialnumer to load</param> /// <returns>Returns null if the certificate wasn't found</returns> public static RSA LoadPublicKeyFromStore(string certSubjectName, string certStoreName) { Certificate capiCert = LoadCertificateFromStore(certSubjectName, certStoreName); // Create the RSA object if the cert isn't null if (capiCert != null) { X509Certificate cert = ConvertCertificateToX509Certificate(capiCert); // Release the certificate object Marshal.ReleaseComObject(capiCert); return CreatePublicRSAFromCert(cert); } return null; } /// <summary> /// Load a X509Certificate from the computers certificate store. /// </summary> /// <param name="certSubjectName">Subject name of the certificate to retrieve</param> /// <param name="certStoreName">The name of the store where the certificate is stored.</param> /// <returns></returns> public static X509Certificate ConvertCertificateToX509Certificate(Certificate capiCert) { X509Certificate cert = null; // do we have any certs? if (capiCert != null) { // get a certificate context from that cert ICertContext iCertCntxt = (ICertContext)capiCert; // now get a pointer to the context int certcntxt = iCertCntxt.CertContext; // turn the int pointer into a managed IntPtr IntPtr hCertCntxt = new IntPtr(certcntxt); if (hCertCntxt != IntPtr.Zero) { // create an X509Certificate from the cert context cert = new X509Certificate(hCertCntxt); } // free the certificate context iCertCntxt.FreeContext(certcntxt); } return cert; } /// <summary> /// Get a certificate from the certificate store. The returned certificate should /// be released using Marshal.ReleaseComObject(certificate); when it's not needed anymore /// </summary> /// <param name="certSubjectName">Subject name of the certificate to retrieve</param> /// <param name="certStoreName">The name of the store where the certificate is stored.</param> /// <returns>Returns null if the certificate wasn't found</returns> public static Certificate LoadCertificateFromStore(string certSubjectName, string certStoreName) { StoreClass store = new StoreClass(); store.Open(CAPICOM_STORE_LOCATION.CAPICOM_LOCAL_MACHINE_STORE, certStoreName, CAPICOM_STORE_OPEN_MODE.CAPICOM_STORE_OPEN_EXISTING_ONLY | CAPICOM_STORE_OPEN_MODE.CAPICOM_STORE_OPEN_READ_ONLY); Certificates oCerts = (Certificates)store.Certificates; oCerts = (Certificates)oCerts.Find(CAPICOM_CERTIFICATE_FIND_TYPE.CAPICOM_CERTIFICATE_FIND_SUBJECT_NAME, certSubjectName, false); Certificate cert = null; if (oCerts.Count > 0) { cert = (Certificate)oCerts[1]; } // Release com objects if (store != null) Marshal.ReleaseComObject(store); if (oCerts != null) Marshal.ReleaseComObject(oCerts); return cert; } /// <summary> /// Load public key from a .cer certificate /// </summary> /// <param name="filePath"></param> /// <returns></returns> public static RSA LoadPublicKeyFromFile(string filePath) { // Load the certificate from disk FileStream fileStream = File.OpenRead(filePath); byte[] certData = new byte[fileStream.Length]; fileStream.Read(certData, 0, (int)fileStream.Length); fileStream.Close(); X509Certificate xCert = new X509Certificate(certData); return CreatePublicRSAFromCert(xCert); } /// <summary> /// Create a RSA object from a X509Certificate /// </summary> /// <param name="cert">Certificate to create the RSA from</param> /// <returns></returns> private static RSA CreatePublicRSAFromCert(X509Certificate cert) { // Create a RSA object from the public key RSA rsa = new RSACryptoServiceProvider(); RSAParameters rp = new RSAParameters(); // Extract the modulus from the public key, starts at index 7 and is 128 bytes rp.Modulus = BlockCopy(cert.GetPublicKey(), 7, 128); //Extract the exponent which starts at index 137 and is 3 bytes rp.Exponent = BlockCopy(cert.GetPublicKey(), 137, 3); // Import the parameters to complete the RSA object rsa.ImportParameters(rp); return rsa; } /// <summary> /// Create a RSA object from a CAPICOM certificate /// </summary> /// <param name="cert">Certificate to create the RSA from</param> /// <returns></returns> private static RSA CreatePrivateRSAFromCert(Certificate cert) { CspParameters cspParams = new CspParameters(); cspParams.Flags = CspProviderFlags.UseMachineKeyStore; cspParams.ProviderName = cert.PrivateKey.ProviderName; cspParams.ProviderType = (int)cert.PrivateKey.ProviderType; cspParams.KeyContainerName = cert.PrivateKey.ContainerName; cspParams.KeyNumber = (int)cert.PrivateKey.KeySpec; RSACryptoServiceProvider rsaKey = new RSACryptoServiceProvider(cspParams); return rsaKey; } /// <summary> /// Read a sequence of bytes out of an array into a new array /// </summary> /// <param name="source">Array to extract bytes from</param> /// <param name="startAt">Position to start extracting from</param> /// <param name="size">The amount of bytes to extract</param> /// <returns></returns> private static byte[] BlockCopy(byte[] source, int startAt, int size) { if ((source == null) || (source.Length < (startAt + size))) return null; byte[] ret = new byte[size]; Buffer.BlockCopy(source, startAt, ret, 0, size); return ret; } } }
Using the class
(remember that there’s .NET 2.0 code here also)
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Security.Cryptography.Xml; using System.Xml; using CapicomLibrary; using System.Security.Cryptography; using System.Xml.Linq; using System.Security.Cryptography.X509Certificates; namespace CapicomTest { class Program { private static readonly string CERT_STORE_PATH = @"C:\Documents and Settings\bjornsallarp\My Documents\Visual Studio 2008\Projects\CapicomTest\CertStore\demo\"; public Program() { // Load our private and public key from disk using CAPICOM RSA capiSignKey = CertHelper.LoadPrivateKeyFromPFXFile(CERT_STORE_PATH + "DemoCert.pfx", "demo"); RSA capiPubKey = CertHelper.LoadPublicKeyFromFile(CERT_STORE_PATH + "PubKey.cer"); // Load our private and public key from disk using X509Certificate2 X509Certificate2 dotNet2PubKey = new X509Certificate2(CERT_STORE_PATH + "PubKey.cer"); RSACryptoServiceProvider rsaDotNet2PubKey = new RSACryptoServiceProvider(); rsaDotNet2PubKey.FromXmlString(dotNet2PubKey.PublicKey.Key.ToXmlString(false)); X509Certificate2 dotNet2PrivKey = new X509Certificate2(CERT_STORE_PATH + "DemoCert.pfx", "demo", X509KeyStorageFlags.Exportable); RSACryptoServiceProvider rsaDotNet2PrivKey = new RSACryptoServiceProvider(); rsaDotNet2PrivKey.FromXmlString(dotNet2PrivKey.PrivateKey.ToXmlString(true)); // Sign and verify the xml keys loaded with CAPICOM XmlDocument xmlDoc = CreateSomeXml(); SignXml(xmlDoc, capiSignKey); Console.WriteLine("Verification using CAPICOM/CAPICOM: " + VerifyXml(xmlDoc, capiPubKey)); // Sign the xml with key loaded by CAPICOM and verify using key loaded with X509Certificate2 xmlDoc = CreateSomeXml(); SignXml(xmlDoc, capiSignKey); Console.WriteLine("Verification using CAPICOM/X509Certificate2: " + VerifyXml(xmlDoc, rsaDotNet2PubKey)); // Sign the xml with key loaded by X509Certificate2 and verify using key loaded with CAPICOM xmlDoc = CreateSomeXml(); SignXml(xmlDoc, rsaDotNet2PrivKey); Console.WriteLine("Verification using X509Certificate2/CAPICOM: " + VerifyXml(xmlDoc, capiPubKey)); // It can be loaded from certstore like this aswell given that it's imported in the Trusted Root capiPubKey = CertHelper.LoadPublicKeyFromStore("DemoCert", "ROOT"); capiSignKey = CertHelper.LoadPrivateKeyFromStore("DemoCert", "ROOT"); } private XmlDocument CreateSomeXml() { // Create a simple XML structure XDocument xDoc = new XDocument(); xDoc.Add(new XElement("People", new XElement("Björn", new XAttribute("Age", "26") ), new XElement("Hampus", new XAttribute("Age", "26") ), new XElement("Anna", new XAttribute("Age", "27") ) )); XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xDoc.ToString()); return xmlDoc; } // Verify the signature of an XML file against an asymetric // algorithm and return the result. public static bool VerifyXml(XmlDocument xmlDocument, RSA Key) { // Create a new SignedXml object and pass it // the XML document class. SignedXml signedXml = new SignedXml(xmlDocument); // Find the "Signature" node and create a new // XmlNodeList object. XmlNodeList nodeList = xmlDocument.GetElementsByTagName("Signature"); // Load the signature node. signedXml.LoadXml((XmlElement)nodeList[0]); // Check the signature and return the result. return signedXml.CheckSignature(Key); } /// <summary> /// Sign an XML file. This document cannot be verified unless /// the verifying code has the key with which it was signed. /// </summary> /// <param name="Doc"></param> /// <param name="Key"></param> public static void SignXml(XmlDocument Doc, RSA Key) { // Check arguments. if (Doc == null) throw new ArgumentException("Doc"); if (Key == null) throw new ArgumentException("Key"); // Create a SignedXml object. SignedXml signedXml = new SignedXml(Doc); // Add the key to the SignedXml document. signedXml.SigningKey = Key; // Create a reference to be signed. Reference reference = new Reference(); reference.Uri = ""; // Add an enveloped transformation to the reference. XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform(); reference.AddTransform(env); // Add the reference to the SignedXml object. signedXml.AddReference(reference); // Compute the signature. signedXml.ComputeSignature(); // Get the XML representation of the signature and save // it to an XmlElement object. XmlElement xmlDigitalSignature = signedXml.GetXml(); // Append the element to the XML document. Doc.DocumentElement.AppendChild(Doc.ImportNode(xmlDigitalSignature, true)); } static void Main(string[] args) { new Program(); } } }
Download my XML signing/verifying example using CAPICOM
With this blog I try to provide useful tips and solutions for programming .NET, Objective-C and more. My name is Björn Sållarp, and I love writing code.
IK
June 28th, 2010 at 11:04 pm
The code in CreatePublicRSAFromCert that extracts the modulus and the exponent from the public key seems suspiciously simple, compared to the GetCertPublicKey function here http://www.jensign.com/JavaScience/dotnet/VerifySig/source/VerifySig.txt
Is it valid for any cert or just for certain type, with particular key length etc.?
Thanks.