Keith Robertson
Keith Robertson

Reputation: 841

Use RSACng to SignData that's compatible with RSACryptoServiceProvider

I'm trying to convert some older CAPI code to use CNG, specifically with the goal of hydrating certificates with ephemeral private keys. (Not supported by CAPI, as I understand it.)

We use a certificate's private key (PK) to sign data. I expected the two implementations to produce compatible output, but to my surprise they don't.

// Hydrate a cert with PK. Make PK a file-based key so we can get the container and use CAPI.
// (Note, although .NET Framework 4.8, the LangVersion is 8.0 so I can use using.)
using var cert = new X509Certificate2(testCertData, "passwd", X509KeyStorageFlags.MachineKeySet);
// Some arbitrary data to sign.
byte[] data = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 };

byte[] signatureCsp, signatureCng;
// Sign with CAPI. This is what we're doing today.
// The cert PK's crypto provider doesn't support SHA256, so we build a different one.
var csp = (RSACryptoServiceProvider)cert.PrivateKey;
var cp = new CspParameters{
    KeyContainerName = csp.CspKeyContainerInfo.KeyContainerName,
    KeyNumber = csp.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2,
};
using (var rsaCsp = new RSACryptoServiceProvider(cp)) {
    signatureCsp = rsaCsp.Sign(data);
}
// Sign with CNG. This is what I want to do.
using (var rsaCng = cert.GetRSAPrivateKey()) {
    signatureCng = rsaCng.Sign(data);
}
// The signatures are different. In fact they're different lengths, 128 and 256 bytes respectively.
Console.WriteLine(Convert.ToBase64String(signatureCsp));
Console.WriteLine(Convert.ToBase64String(signatureCng));

// But maybe they're compatible? Let's see if code which verifies the first can verify the second.
using (var provider = new RSACryptoServiceProvider(cp)) {
    var verifiedCsp = provider.Verify(data, signatureCsp);
    var verifiedCng = provider.Verify(data, signatureCng);
    Console.WriteLine("RSACryptoServiceProvider verified: {0}", verifiedCsp);
    Console.WriteLine("RSACng signature         verified: {0}", verifiedCng); // Nope. False.
}

The above uses the following extension methods, for uniformity, treating both cases as implementations of the abstract RSA base type and with the same crypto parameters.

public static byte[] Sign(this RSA rsa, byte[] buffer) {
    return rsa.SignData(buffer, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
public static bool Verify(this RSA rsa, byte[] buffer, byte[] signature) {
    return rsa.VerifyData(buffer, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}

What do I need to do to sign data with the certificate's CNG key so that the output can be verified by current consumers of signatures produced by the CAPI key?

Upvotes: 0

Views: 1646

Answers (1)

bartonjs
bartonjs

Reputation: 33088

Your usage of the RSA classes is correct, and will produce compatible results for the appropriate keys.

The problem here is that you open the PFX into the machine key store:

using var cert = new X509Certificate2(testCertData, "passwd", X509KeyStorageFlags.MachineKeySet);

But when you reopened the RSACryptoServiceProvider version of the key, you didn't set the CspProviderFlags.UseMachineKeyStore flag.

var cp = new CspParameters{
    KeyContainerName = csp.CspKeyContainerInfo.KeyContainerName,
    KeyNumber = csp.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2,
};

Since you didn't set the machine key store flag, it used the user keystore, and "opening" the key created a new key (since one did not previously exist with that name). After it was created once, successive runs would reopen the same key (assuming the PFX import got a consistent key name).

var cp = new CspParameters{
    KeyContainerName = csp.CspKeyContainerInfo.KeyContainerName,
    KeyNumber = csp.CspKeyContainerInfo.KeyNumber == KeyNumber.Exchange ? 1 : 2,
    Flags = CspProviderFlags.UseMachineKeyStore | CspProviderFlags.UseExistingKey,
};

Upvotes: 1

Related Questions