Search code examples
xmlx509certificatesigningx509certificate2certificate-store

Signing xml document with a installed X.509 certificate


I am having trouble signing an xml docuemnt with an installed certificate. I have tried with the certificated installed in the LocalMachine, CurrentUser, and Initialize instance of X509Certificate2 using the certificate file (.pfx). Each has its own issues; my preference is to use the certificate that is installed in the LocalMachine store. Below I have outlined the three methods and the concerns with each:

StoreLocation LocalMachine - prefered method

var certStore = new X509Store(StoreLocation.LocalMachine);
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, certificateThumbPrint, true);
var myCert = certCollection[0];

// I can get the correct certificate but the following line throws "Invalid provider type specified." error
var SigningKey = myCert.PrivateKey;

StoreLocation CurrentUser

var certStore = new X509Store(StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, certificateThumbPrint, true);
var myCert = certCollection[0];

// I can get the correct certificate but the following line throws "Keyset does not exist" error
var SigningKey = myCert.PrivateKey;

I can only get the PrivateKey if I change the permissions to the following folder %ALLUSERSPROFILE%\Application Data\Microsoft\Crypto\RSA\MachineKeys. This doesn't look the right way to implement the signing.

Using the certificate file

var certificateFile = @"C:\CertificateFolder\AuthorizedCertificate.pfx";
var myCert = new X509Certificate2(certificateFile, password, X509KeyStorageFlags.UserKeySet);

This method works however I'd have to provide certificate file and the password which is not desireable.

How can I get the first method (LocalMachine) to work? What's the recommended/best practice way to do this?

For reference the following code is used to sign the xml document

private void SignXml(XmlDocument xmlDoc, X509Certificate2 cert)
{
    // Create a SignedXml object.
    SignedXml signedXml = new SignedXml(xmlDoc);

    // Add the key to the SignedXml document.
    signedXml.SigningKey = cert.PrivateKey;

    // Create a reference to be signed.
    Reference reference = new Reference();
    reference.Uri = "";

    // Add an enveloped transformation to the reference.
    var env = new XmlDsigEnvelopedSignatureTransform();
    reference.AddTransform(env);

    // Include the public key of the certificate in the assertion.
    signedXml.KeyInfo = new KeyInfo();
    signedXml.KeyInfo.AddClause(new KeyInfoX509Data(cert, X509IncludeOption.WholeChain));

    // 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.
    xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlDigitalSignature, true));
}

Solution

  • LocalMachine store version

    Replace

    var SigningKey = myCert.PrivateKey;
    

    with

    var SigningKey = myCert.GetRSAPrivateKey();
    

    (s/RSA/DSA/ as appropriate)

    The PrivateKey property can only return keys that are castable to RSACryptoServiceProvider or DSACryptoServiceProvider. "Invalid provider type specified" means that your private key is stored in CNG, not CAPI.

    This will only work if you have .NET 4.6.2 or higher installed, because that's when certain limitations (regarding non-RSACryptoServiceProvider RSA) within SignedXml and its helper classes was fixed.

    (Alternative: Upgrade to Windows 10, where the OS added a CAPI to CNG bridge to solve this problem)

    CurrentUser store version

    This version fails because when you imported the certificate from PFX you imported it with MachineKeySet (or you didn't specify UserKeySet and it was previously exported from the machine key store). The copy of the certificate in the user store says that it's private key lives in the machine store. And, for some reason, you don't have access to it. ("For some reason" because normally that would suggest you couldn't have added it...)

    PFX version

    This works because the PFX says that the key should be stored in a CAPI CSP (PFXes carry a lot of metadata), allowing the certificate PrivateKey property to function.