49

My application will take a set of files and sign them. (I'm not trying to sign an assembly.) There is a .p12 file that I get the private key from.

This is the code I was trying to use, but I get a System.Security.Cryptography.CryptographicException "Invalid algorithm specified.".

X509Certificate pXCert = new X509Certificate2(@"keyStore.p12", "password");
RSACryptoServiceProvider csp = (RSACryptoServiceProvider)pXCert.PrivateKey;
string id = CryptoConfig.MapNameToOID("SHA256");
return csp.SignData(File.ReadAllBytes(filePath), id);

According to this answer it can't be done (the RSACryptoServiceProvider does not support SHA-256), but I was hoping that it might be possible using a different library, like Bouncy Castle.

I'm new to this stuff and I'm finding Bouncy Castle to be very confusing. I'm porting a Java app to C# and I have to use the same type of encryption to sign the files, so I am stuck with RSA + SHA256.

How can I do this using Bouncy Castle, OpenSSL.NET, Security.Cryptography, or another 3rd party library I haven't heard of? I'm assuming, if it can be done in Java then it can be done in C#.

UPDATE:

this is what I got from the link in poupou's anwser

X509Certificate2 cert = new X509Certificate2(KeyStoreFile, password");
RSACryptoServiceProvider rsacsp = (RSACryptoServiceProvider)cert.PrivateKey;
CspParameters cspParam = new CspParameters();
cspParam.KeyContainerName = rsacsp.CspKeyContainerInfo.KeyContainerName;
cspParam.KeyNumber = rsacsp.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2;
RSACryptoServiceProvider aescsp = new RSACryptoServiceProvider(cspParam);
aescsp.PersistKeyInCsp = false;
byte[] signed = aescsp.SignData(File.ReadAllBytes(file), "SHA256");
bool isValid = aescsp.VerifyData(File.ReadAllBytes(file), "SHA256", signed);

       

The problem is that I'm not getting the same results as I got with the original tool. As far as I can tell from reading the code the CryptoServiceProvider that does the actual signing is not using the PrivateKey from key store file. Is that Correct?

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
scott
  • 2,991
  • 5
  • 36
  • 47
  • Also see [Signing and verifying signatures with RSA C#](https://stackoverflow.com/q/8437288/608639), [how to sign bytes using my own rsa private key using rs256 algorithm?](https://stackoverflow.com/q/25909044/608639), [Signing data with private key in c#](https://stackoverflow.com/q/31828420/608639), [How can I sign a file using RSA and SHA256 with .NET?](https://stackoverflow.com/q/7444586/608639), [Signing a string with RSA private key on .NET?](https://stackoverflow.com/q/3169829/608639), etc. – jww May 30 '17 at 13:54

10 Answers10

72

RSA + SHA256 can and will work...

Your later example may not work all the time, it should use the hash algorithm's OID, rather than it's name. As per your first example, this is obtained from a call to CryptoConfig.MapNameToOID(AlgorithmName) where AlgorithmName is what you are providing (i.e. "SHA256").

First you are going to need is the certificate with the private key. I normally read mine from the LocalMachine or CurrentUser store by using a public key file (.cer) to identify the private key, and then enumerate the certificates and match on the hash...

X509Certificate2 publicCert = new X509Certificate2(@"C:\mycertificate.cer");

//Fetch private key from the local machine store
X509Certificate2 privateCert = null;
X509Store store = new X509Store(StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
foreach( X509Certificate2 cert in store.Certificates)
{
    if (cert.GetCertHashString() == publicCert.GetCertHashString())
        privateCert = cert;
}

However you get there, once you've obtained a certificate with a private key we need to reconstruct it. This may be required due to the way the certificate creates it's private key, but I'm not really sure why. Anyway, we do this by first exporting the key and then re-importing it using whatever intermediate format you like, the easiest is xml:

//Round-trip the key to XML and back, there might be a better way but this works
RSACryptoServiceProvider key = new RSACryptoServiceProvider();
key.FromXmlString(privateCert.PrivateKey.ToXmlString(true));

Once that is done we can now sign a piece of data as follows:

//Create some data to sign
byte[] data = new byte[1024];

//Sign the data
byte[] sig = key.SignData(data, CryptoConfig.MapNameToOID("SHA256"));

Lastly, the verification can be done directly with the certificate's public key without need for the reconstruction as we did with the private key:

key = (RSACryptoServiceProvider)publicCert.PublicKey.Key;
if (!key.VerifyData(data, CryptoConfig.MapNameToOID("SHA256"), sig))
    throw new CryptographicException();
spicy.dll
  • 948
  • 8
  • 23
csharptest.net
  • 62,602
  • 11
  • 71
  • 89
  • 5
    That export / import of the private key is likely switching from a CSP that does not support SHA256 (the one associated with the certificate) to another CSP that supports it (e.g. the "AES" CSP) – poupou Sep 19 '11 at 23:41
  • 4
    I serialized the key.CspKeyContainerInfo before and after the export/import to compare them and it does indeed have a different provider type, from `Microsoft Base Cryptographic Provider v1.0` to `Microsoft Enhanced RSA and AES Cryptographic Provider` – Despertar Jun 05 '14 at 02:29
  • 15
    I am yet to see a production environment with exportable private keys. This is not a good answer. – Visar Feb 23 '15 at 16:00
  • 2
    This does not work for me, i get the following exception, "An unhandled exception of type 'System.Security.Cryptography.CryptographicException' occurred in mscorlib.dll Additional information: Key not valid for use in specified state" from the line : key.FromXmlString(privateCert.PrivateKey.ToXmlString(true)); – Joseph Nov 16 '15 at 10:32
  • 1
    @Visar What is a better answer then? How can you sign something without a private key? – NickG Jan 25 '17 at 11:37
  • Signature = Encrypted(Hash). You can't sign without key. The solution is to upgrade all your certificates to add an extra attribute in the X509 to instruct Microsoft to use latest AES . This is the solution I found and what we did. This is one of the dumbest in the series of stupidities invented by Microsoft. Recreate X509 for their own specific purpose because our CrypotNG is not smart enough to understand that the cert is AES/SHA256 and also RSA 256. – Visar Jan 25 '17 at 21:45
  • @NickG: Timo's answer below is probably the closest to the real answer for this issue if you can't afford to re-create your certs. Decompose PFX and re-compose again by instructing PFX to use the their latest CryptoNG adding specific attribute for this. -CSP "Microsoft Enhanced RSA and AES Cryptographic Provider". – Visar Jan 28 '17 at 00:25
25

The use of privateKey.toXMLString(true) or privateKey.exportParameters(true) aren't usable in a secure environment, since they require your private key to be exportable, which is NOT a good practice.

A better solution is to explicitly load the "Enhanced" crypto provider as such:

// Find my openssl-generated cert from the registry
var store = new X509Store(StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindBySubjectName, "myapp.com", true);
var certificate = certificates[0];
store.Close();
// Note that this will return a Basic crypto provider, with only SHA-1 support
var privKey = (RSACryptoServiceProvider)certificate.PrivateKey;
// Force use of the Enhanced RSA and AES Cryptographic Provider with openssl-generated SHA256 keys
var enhCsp = new RSACryptoServiceProvider().CspKeyContainerInfo;
var cspparams = new CspParameters(enhCsp.ProviderType, enhCsp.ProviderName, privKey.CspKeyContainerInfo.KeyContainerName);
privKey = new RSACryptoServiceProvider(cspparams);
BKibler
  • 259
  • 3
  • 2
  • Thank you. Very clean. So you simply create an empty (correct) RSACryptoServiceProvider, take its ProviderType and ProviderName, and combine them with the private key's KeyContainerName to form a working private key. – Timo Feb 20 '15 at 10:18
  • I see that it works. I was expecting to need more data from the original private key. All that you transfer from it to the new RSACryptoServiceProvider is its KeyContainerName. Does that contain all of the private key's necessary data? – Timo Feb 20 '15 at 10:19
  • 4
    unfortunately after doing this you are not able to use the public key with verifydata that was signed with this key – Cyan Mar 10 '16 at 20:23
  • After using this solution, the public key can no longer verify a signature made with that key. Neither encrypted data with the public key cannot be decrypted with the private key. Anything special that needs to be done to the public key? – Mihai Caracostea Apr 07 '16 at 11:48
  • 1
    I agree with the concern of a secure environment, but, when using this approach, the produced result cannot be validated with the public key inside the certificate. – Marcos Junior Apr 16 '16 at 01:13
  • 2
    This seems to produce hashes that are plain incorrect. It doesn't appear to be accessing the original private key because I'm not getting the security popup. – antak Jun 13 '16 at 02:41
  • 2
    This method doesn't work and I have no idea how it has so many upvotes. The key it's using is NOT the one from the certificate so all the signatures are incorrect. – NickG Jan 30 '17 at 11:14
12

This is how I dealt with that problem:

 X509Certificate2 privateCert = new X509Certificate2("certificate.pfx", password, X509KeyStorageFlags.Exportable);

 // This instance can not sign and verify with SHA256:
 RSACryptoServiceProvider privateKey = (RSACryptoServiceProvider)privateCert.PrivateKey;

 // This one can:
 RSACryptoServiceProvider privateKey1 = new RSACryptoServiceProvider();
 privateKey1.ImportParameters(privateKey.ExportParameters(true));

 byte[] data = Encoding.UTF8.GetBytes("Data to be signed"); 

 byte[] signature = privateKey1.SignData(data, "SHA256");

 bool isValid = privateKey1.VerifyData(data, "SHA256", signature);
user3658415
  • 137
  • 1
  • 3
  • This looks to be how jose-jwt does it. https://github.com/dvsekhvalnov/jose-jwt/blob/master/UnitTests/TestSuite.cs#L2346 – Danny May 27 '16 at 23:23
  • This one worked for me just fine. The key element (forgive the pun) was to make the certificate exportable and to then create a new RSACSP using the private key. The only minor difference to this was that I created the 2nd CSP using "csp.FromXmlString(cert.PrivateKey.ToXmlString(true))". – Bertie Sep 03 '18 at 07:37
10

I settled on changing the key file to specify the appropriate Crypto Service Provider, avoiding the issue in .NET altogether.

So when I create a PFX file out of a PEM private key and a CRT public certificate, I do it as follows:

openssl pkcs12 -export -aes256 -CSP "Microsoft Enhanced RSA and AES Cryptographic Provider" -inkey priv.pem -in pub.crt -out priv.pfx

The key part being -CSP "Microsoft Enhanced RSA and AES Cryptographic Provider".

(-inkey specifies the private key file and -in specifies the public certificate to incorporate.)

You may need to tweak this for the file formats you have on hand. The command line examples on this page can help with that: https://www.sslshopper.com/ssl-converter.html

I found this solution here: http://hintdesk.com/c-how-to-fix-invalid-algorithm-specified-when-signing-with-sha256/

Timo
  • 7,992
  • 4
  • 49
  • 67
  • I went with your solution to replace the self-signed certificate using "makecert" with the one created by openSSL. With makecert windows command, even if I was specifying the -sy 24 which means CSP of 24, it was still creating a certificate with CSP = 1, here is a C# code below to check if someone has the same issue. var x509Certificate = new X509Certificate2(@"cert.pfx", "Password", X509KeyStorageFlags.Exportable); Console.WriteLine(x509Certificate.ToString(true)); Console.ReadLine(); – Kaushik Ghosh Aug 17 '16 at 16:30
  • In situations when you have a RSASHA2 cert and still complains of wrong algorithm - this is the only valid answer. – Visar Jan 28 '17 at 00:28
  • 1
    Thank you. Using the correct provider made all the difference. Powershell: $cert = New-SelfSignedCertificate -Subject "OIOSAML test" -Provider "Microsoft Enhanced RSA and AES Cryptographic Provider" -CertStoreLocation Cert:\LocalMachine\My -NotAfter (Get-Date).AddYears(3) -HashAlgorithm SHA256 – 9Rune5 Mar 20 '20 at 18:28
6

Use can use this on more recent frameworks.

    public byte[] GetSignature(byte[] inputData)
    {
        using (var rsa = this.signingCertificate.GetRSAPrivateKey())
        {
            return rsa.SignData(inputData, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        }
    }

    public bool ValidateSignature(byte[] inputData, byte[] signature)
    {
        using (var rsa = this.signingCertificate.GetRSAPublicKey())
        {
            return rsa.VerifyData(inputData, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        }
    }

The signingCertificate above is a X509Certificate2 with a private key. This method does not require you to import any existing keys and works in a secure environment.

Vinay Chandra
  • 502
  • 5
  • 18
5

When you use a certificate to get your RSACryptoServiceProvider it really matters what's the underlying CryptoAPI provider. By default, when you create a certificate with 'makecert', it's "RSA-FULL" which only supports SHA1 hashes for signature. You need the new "RSA-AES" one that supports SHA2.

So, you can create your certificate with an additional option: -sp "Microsoft Enhanced RSA and AES Cryptographic Provider" (or an equivalent -sy 24) and then your code would work without the key juggling stuff.

Vladik Branevich
  • 1,180
  • 8
  • 11
4

Here is how I signed a string without having to modify the certificate (to a Microsoft Enhanced RSA and AES Cryptographic provider).

        byte[] certificate = File.ReadAllBytes(@"C:\Users\AwesomeUser\Desktop\Test\ServerCertificate.pfx");
        X509Certificate2 cert2 = new X509Certificate2(certificate, string.Empty, X509KeyStorageFlags.Exportable);
        string stringToBeSigned = "This is a string to be signed";
        SHA256Managed shHash = new SHA256Managed();
        byte[] computedHash = shHash.ComputeHash(Encoding.Default.GetBytes(stringToBeSigned));


        var certifiedRSACryptoServiceProvider = cert2.PrivateKey as RSACryptoServiceProvider;
        RSACryptoServiceProvider defaultRSACryptoServiceProvider = new RSACryptoServiceProvider();
        defaultRSACryptoServiceProvider.ImportParameters(certifiedRSACryptoServiceProvider.ExportParameters(true));
        byte[] signedHashValue = defaultRSACryptoServiceProvider.SignData(computedHash, "SHA256");
        string signature = Convert.ToBase64String(signedHashValue);
        Console.WriteLine("Signature : {0}", signature);

        RSACryptoServiceProvider publicCertifiedRSACryptoServiceProvider = cert2.PublicKey.Key as RSACryptoServiceProvider;
        bool verify = publicCertifiedRSACryptoServiceProvider.VerifyData(computedHash, "SHA256", signedHashValue);
        Console.WriteLine("Verification result : {0}", verify);
AdmiralThrawn
  • 374
  • 5
  • 15
3

According to this blog it should work with FX 3.5 (see note below). However it's important to recall that most of .NET cryptography is based on CryptoAPI (even if CNG is being more and more exposed in recent FX releases).

The key point is that CryptoAPI algorithm support depends on the Crypto Service Provider (CSP) being used and that varies a bit between Windows versions (i.e. what's working on Windows 7 might not work on Windows 2000).

Read the comments (from the blog entry) to see a possible workaround where you specify the AES CSP (instead of the default one) when creating your RSACCryptoServiceProvider instance. That seems to work for some people, YMMV.

Note: this is confusing to many people because all the released .NET frameworks includes a managed implementation of SHA256 which cannot be used by CryptoAPI. FWIW Mono does not suffer from such issues ;-)

poupou
  • 43,413
  • 6
  • 77
  • 174
  • that work around looks like it might work. I have to do more testing before I can know for sure. – scott Sep 16 '11 at 19:33
  • So I tried the work around in the comments, but it doesn't look like its using the private key. Is that correct? – scott Sep 16 '11 at 20:53
  • That blog post was the clarification I need to explain what our existing code is doing. – mlhDev Jun 11 '15 at 19:26
1

I know this is an old thread but for those still stuck in the past and looking for an answer, the following worked for me based off @BKibler's answer. The comments stated it's not using the correct key and it's because the solution is missing a couple key settings.

// Find my openssl-generated cert from the registry
var store = new X509Store(StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(X509FindType.FindBySubjectName, "myapp.com", true);
var certificate = certificates[0];
store.Close();

// Note that this will return a Basic crypto provider, with only SHA-1 support
var privKey = (RSACryptoServiceProvider)certificate.PrivateKey;

// Force use of the Enhanced RSA and AES Cryptographic Provider with openssl-generated SHA256 keys
var enhCsp = new RSACryptoServiceProvider().CspKeyContainerInfo;

if (!Enum.TryParse<KeyNumber>(privKey.CspKeyContainerInfo.KeyNumber.ToString(), out var keyNumber))
     throw new Exception($"Unknown key number {privKey.CspKeyContainerInfo.KeyNumber}");

var cspparams = new CspParameters(enhCsp.ProviderType, enhCsp.ProviderName, privKey.CspKeyContainerInfo.KeyContainerName)
{
     KeyNumber = (int)keyNumber,
     Flags = CspProviderFlags.UseExistingKey
};

privKey = new RSACryptoServiceProvider(cspparams);

You need to set both "KeyNumber" and "Flags" so the existing (non-exportable) key is used and you can use the public key from the certificate to verify.

Ruben Helsloot
  • 12,582
  • 6
  • 26
  • 49
0

I have noticed similar issues in .NET with the wrong private key being used (or was it flat-out errors? I do not recall) when the certificate I am working with is not in the user/computer certificate store. Installing it into the stored fixed the problem for my scenario and things started working as expected - perhaps you can try that.

Sander
  • 25,685
  • 3
  • 53
  • 85
  • 1
    I'm loading the certificate from a file, I'm not using any of the windows installed certificates. – scott Sep 19 '11 at 15:58