Search code examples
c#certificatesignaturesha256

Certificate signing produces different signature when on server


I am trying to sign some data using a certificate private key. The issue I'm finding is that the signature is different depending on if I'm executing it locally or on a server.

I'm using the following code as a test, running under the same user both locally and on the server:

using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace TestSignature
{
    class Program
    {
        static void Main(string[] args)
        {

            var key = SigningKeyFromCertificate(StoreName.My, StoreLocation.LocalMachine, X509FindType.FindByThumbprint, "thumbprint");
            var alg = CryptoConfig.MapNameToOID("SHA256");
            var data = Encoding.UTF8.GetBytes("test");
            var sig = key.SignData(data, alg);

            Console.WriteLine(Convert.ToBase64String(sig));

        }

        private static RSACryptoServiceProvider SigningKeyFromCertificate(StoreName storeName, StoreLocation storeLocation, X509FindType findType, string findValue)
        {
            X509Store store = new X509Store(storeName, storeLocation);
            store.Open(OpenFlags.ReadOnly);

            var certs = store.Certificates.Find(findType, findValue, false);
            if (certs?.Count > 0)
            {
                var cert = certs[0];
                if (cert.HasPrivateKey)
                {
                    // Force use of Enhanced RSA and AES Cryptographic Provider to allow use of SHA256.
                    var key = cert.PrivateKey as RSACryptoServiceProvider;
                    var enhanced = new RSACryptoServiceProvider().CspKeyContainerInfo;
                    var parameters = new CspParameters(enhanced.ProviderType, enhanced.ProviderName, key.CspKeyContainerInfo.UniqueKeyContainerName);
                    return new RSACryptoServiceProvider(parameters);
                }
                else
                {
                    throw new Exception($"No private key access to cert '{findValue}.'");
                }
            }
            else
            {
                throw new Exception($"Cert '{findValue}' not found!");
            }
        }
    }
}

Locally, I get the following signature:

YUjspKhLl7v3u5VQkh1PfHytMTpEtbAftxOA5v4lmph3B4ssVlZp7KedO5NW9K5L222Kz9Ik9/55NirS0cNCz/cDhEFRtD4daJ9qLRuM8oD5hCj6Jt9Vc6WeS2he+Cqfoylnv4V9plfi1xw8y7EyAf4C77BGkXOdyP5wyz2Xubo=

On the server, I get this one instead:

u1RUDwbBlUpOgNNkAjXhYEWfVLGpMOa0vEfm6PUkB4y9PYBk1lDmCAp+488ta+ipbTdSDLM9btRqsQfZ7JlIn/dIBw9t5K63Y7dcDcc7gDLE1+umLJ7EincMcdwUv3YQ0zCvzc9RrP0jKJManV1ptQNnODpMktGYAq1KmJb9aTY=

Any idea of what could be different? I would think, with the same certificate, the same code, and the same data, the signature should be the same.

(The example is written in C# 4.5.2.)


Solution

  • You have some code to reopen the CAPI key handle under PROV_RSA_AES:

    // Force use of Enhanced RSA and AES Cryptographic Provider to allow use of SHA256.
    var key = cert.PrivateKey as RSACryptoServiceProvider;
    var enhanced = new RSACryptoServiceProvider().CspKeyContainerInfo;
    
    var parameters = new CspParameters(
        enhanced.ProviderType,
        enhanced.ProviderName,
        key.CspKeyContainerInfo.UniqueKeyContainerName);
    
    return new RSACryptoServiceProvider(parameters);
    

    But key.CspKeyContainerInfo.UniqueKeyContainerName isn't the name of the key (it's the name of the file on disk where the key lives), so you're opening a brand new key (you're also generating a new ephemeral key just to ask what the default provider is). Since it's a named key it persists, and subsequent application executions resolve to the same key -- but a different "same" key on each computer.

    A more stable way of reopening the key is

    var cspParameters = new CspParameters
    {
        KeyContainerName = foo.CspKeyContainerInfo.KeyContainerName,
        Flags = CspProviderFlags.UseExistingKey,
    };
    

    (since the provider type and name aren't specified they will use the defaults, and by saying UseExistingKey you get an exception if you reference a key that doesn't exist).


    That said, the easiest fix is to stop using RSACryptoServiceProvider. .NET Framework 4.6 (and .NET Core 1.0) have a(n extension) method on X509Certificate2, GetRSAPrivateKey(), it returns an RSA (which you should avoid casting) which is usually RSACng (on Windows), but may be RSACryptoServiceProvider if only CAPI had a driver required for a HSM, and may be some other RSA in the future. Since RSACng handles SHA-2 better there's almost never a need to "reopen" the return object (even if it's RSACryptoServiceProvider, and even if the type isn't PROV_RSA_AES (24), that doesn't mean the HSM will fail to do SHA-2).