I am trying to digitally sign a XML document using SHA256.
I am trying to use Security.Cryptography.dll for this.
Here is my code -
CryptoConfig.AddAlgorithm(typeof(RSAPKCS1SHA256SignatureDescription),"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); X509Certificate2 cert = new X509Certificate2(@"location of pks file", "password"); XmlDocument doc = new XmlDocument(); doc.PreserveWhitespace = true; doc.Load(@"input.xml"); SignedXml signedXml = new SignedXml(doc); signedXml.SigningKey = cert.PrivateKey; signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; // // Add a signing reference, the uri is empty and so the whole document // is signed. Reference reference = new Reference(); reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); reference.AddTransform(new XmlDsigExcC14NTransform()); reference.Uri = ""; signedXml.AddReference(reference); // // Add the certificate as key info, because of this the certificate // with the public key will be added in the signature part. KeyInfo keyInfo = new KeyInfo(); keyInfo.AddClause(new KeyInfoX509Data(cert)); signedXml.KeyInfo = keyInfo; // Generate the signature. signedXml.ComputeSignature();
But i am getting "Invalid algorithm specified." error at signedXml.ComputeSignature();
. Can anyone tell me what I am doing wrong?
X509Certificate2
loads the private key from the pfx file into the Microsoft Enhanced Cryptographic Provider v1.0 (provider type 1
a.k.a. PROV_RSA_FULL
) which doesn't support SHA-256.
The CNG-based cryptographic providers (introduced in Vista and Server 2008) support more algorithms than the CryptoAPI-based providers, but the .NET code still seems to be working with CryptoAPI-based classes like RSACryptoServiceProvider
rather than RSACng
so we have to work around these limitations.
However, another CryptoAPI provider, Microsoft Enhanced RSA and AES Cryptographic Provider (provider type 24
a.k.a. PROV_RSA_AES
) does support SHA-256. So if we get the private key into this provider, we can sign with it.
First, you'll have to adjust your X509Certificate2
constructor to enable the key to be exported out of the provider that X509Certificate2
puts it into by adding the X509KeyStorageFlags.Exportable
flag:
X509Certificate2 cert = new X509Certificate2( @"location of pks file", "password", X509KeyStorageFlags.Exportable);
And export the private key:
var exportedKeyMaterial = cert.PrivateKey.ToXmlString( /* includePrivateParameters = */ true);
Then create a new RSACryptoServiceProvider
instance for a provider that supports SHA-256:
var key = new RSACryptoServiceProvider( new CspParameters(24 /* PROV_RSA_AES */)); key.PersistKeyInCsp = false;
And import the private key into it:
key.FromXmlString(exportedKeyMaterial);
When you've created your SignedXml
instance, tell it to use key
rather than cert.PrivateKey
:
signedXml.SigningKey = key;
And it will now work.
Here are the list of provider types and their codes on MSDN.
Here's the full adjusted code for your example:
CryptoConfig.AddAlgorithm(typeof(RSAPKCS1SHA256SignatureDescription), "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); X509Certificate2 cert = new X509Certificate2(@"location of pks file", "password", X509KeyStorageFlags.Exportable); // Export private key from cert.PrivateKey and import into a PROV_RSA_AES provider: var exportedKeyMaterial = cert.PrivateKey.ToXmlString( /* includePrivateParameters = */ true); var key = new RSACryptoServiceProvider(new CspParameters(24 /* PROV_RSA_AES */)); key.PersistKeyInCsp = false; key.FromXmlString(exportedKeyMaterial); XmlDocument doc = new XmlDocument(); doc.PreserveWhitespace = true; doc.Load(@"input.xml"); SignedXml signedXml = new SignedXml(doc); signedXml.SigningKey = key; signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; // // Add a signing reference, the uri is empty and so the whole document // is signed. Reference reference = new Reference(); reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); reference.AddTransform(new XmlDsigExcC14NTransform()); reference.Uri = ""; signedXml.AddReference(reference); // // Add the certificate as key info, because of this the certificate // with the public key will be added in the signature part. KeyInfo keyInfo = new KeyInfo(); keyInfo.AddClause(new KeyInfoX509Data(cert)); signedXml.KeyInfo = keyInfo; // Generate the signature. signedXml.ComputeSignature();
Exporting and re-importing has already been given as an answer, but there are a couple other options that you should be aware of.
The GetRSAPrivateKey (extension) method returns an RSA instance of "the best available type" for the key and platform (as opposed to the PrivateKey property which "everyone knows" returns RSACryptoServiceProvider).
In 99.99(etc)% of all RSA private keys the returned object from this method is capable of doing SHA-2 signature generation.
While that method was added in .NET 4.6(.0) the requirement of 4.6.2 exists in this case because the RSA instance returned from GetRSAPrivateKey didn't work with SignedXml. That has since been fixed (162556).
I, personally, don't like this approach because it uses the (now-legacy) PrivateKey property and RSACryptoServiceProvider class. But, it has the advantage of working on all versions of .NET Framework (though not .NET Core on non-Windows systems, since RSACryptoServiceProvider is Windows-only).
private static RSACryptoServiceProvider UpgradeCsp(RSACryptoServiceProvider currentKey) { const int PROV_RSA_AES = 24; CspKeyContainerInfo info = currentKey.CspKeyContainerInfo; // WARNING: 3rd party providers and smart card providers may not handle this upgrade. // You may wish to test that the info.ProviderName value is a known-convertible value. CspParameters cspParameters = new CspParameters(PROV_RSA_AES) { KeyContainerName = info.KeyContainerName, KeyNumber = (int)info.KeyNumber, Flags = CspProviderFlags.UseExistingKey, }; if (info.MachineKeyStore) { cspParameters.Flags |= CspProviderFlags.UseMachineKeyStore; } if (info.ProviderType == PROV_RSA_AES) { // Already a PROV_RSA_AES, copy the ProviderName in case it's 3rd party cspParameters.ProviderName = info.ProviderName; } return new RSACryptoServiceProvider(cspParameters); }
If you already have cert.PrivateKey cast as an RSACryptoServiceProvider you can send it through UpgradeCsp. Since this is opening an existing key there'll be no extra material written to disk, it uses the same permissions as the existing key, and it does not require you to do an export.
But (BEWARE!) do NOT set PersistKeyInCsp=false, because that will erase the original key when the clone is closed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With