0

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?

Keith Robertson
  • 791
  • 7
  • 13
  • I think this all boils down to you reopened the key for `rsaCsp` (just use `csp`), and you lost the machinekey bit. So your "reopening" of the key made a new one of the default size (1024-bit). – bartonjs Jan 10 '20 at 23:45
  • @bartonjs. Unfortunately, this is a necessary step as explained in the comment. The CSP associated with the imported cert cannot use SHA256, so the key needs to be opened with a newer provider. I've seen examples which set CspParameters.ProviderName explicitly, but in practice I found this wasn't necessary. AFAIK this does not make a new key. For one thing, this produces the same signature every execution; if the key were regenerated every run, every run would produce a different signature. – Keith Robertson Jan 13 '20 at 21:51
  • fair enough, but you need to set the MachineKey flag in the CspParameters. Your current code opens or creates a key with the same name in the user scope. – bartonjs Jan 13 '20 at 22:20
  • Indeed you are correct! Thank you! I thought if there was an additional file it would get cleaned up when the CSPs are disposed, but no it isn't. So I added `Flags = CspProviderFlags.UseMachineKeyStore`. Any advice on the crux of the question, how to use RSA CNG for signing in a way that's compatible with this CAPI signature method? – Keith Robertson Jan 15 '20 at 22:48
  • aside from missing the MachineKey flag, everything else looked right. If it’s still not working then it’s be good to get an update of what state you’re in. – bartonjs Jan 15 '20 at 22:50
  • Same state. The CNG signature is different from the CAPI signature for the same key. And it's incompatible: the CNG signature does not verify using a CAPI CSP. I need advice on how to create the signature with CNG in a way that verifies with the CAPI CSPs so that I can update our application to CNG and have the digital signatures verify by existing consumers. – Keith Robertson Jan 17 '20 at 15:41
  • Your usage is correct. If the signature doesn’t verify then the key is incorrect. You can verify that by calling ExportParameters(false) and comparing the Modulus (and Exponent) value. Until they’re exporting the same you have two different keys. – bartonjs Jan 17 '20 at 15:44
  • I'll check that. Thx. – Keith Robertson Jan 17 '20 at 15:54
  • To clarify, you need to compare the Modulus from rsaCsp and rsaCng, not Modulus and Exponent from the same key :). (Exponent is 99.99% of the time `new byte[] { 0x01, 0x00, 0x01 }`) – bartonjs Jan 17 '20 at 16:39
  • @bartonjs. FWIW, I got that.:-) Params are the same (now!); I even compared private key info, creating the cert key as Exportable, and those are the same. But now the signatures are also the same! So I reversed changes until they became incompatible again. Turns out you were right all along! Not specifying Machine store did make a different key of 1024 bits. Don't know how I thought I'd seen same signature each run, but now I see it changes when I don't specify the store. It works when I specify CSP to use Machine store. Thanks!! Please create answer and I'll gladly mark as THE answer! – Keith Robertson Jan 17 '20 at 18:44

1 Answers1

1

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,
};
bartonjs
  • 30,352
  • 2
  • 71
  • 111
  • Thank you! I now have included `Flags = cspInfo.MachineKeyStore ? CspProviderFlags.UseMachineKeyStore | CspProviderFlags.UseExistingKey : CspProviderFlags.UseExistingKey` where `var cspInfo = ((RSACryptoServiceProvider)cert.PrivateKey).CspKeyContainerInfo;` – Keith Robertson Jan 28 '20 at 14:10