2

I'm switching some .NET Framework libraries over to .NET Standard. One of my libraries handles JSON Web Tokens (JWT) using a certificate store on the local machine. The library was using RSACryptoServiceProvider and it seems like that's not recommended anymore.

As a result I'm switching to using the GetPublicKey() and GetPrivateKey() extension methods, but I'm having an issue with the private key. Anytime I call Decrypt on the RSA instance of the private key I receive:

{Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: Access denied at System.Security.Cryptography.RSACng.EncryptOrDecrypt(SafeNCryptKeyHandle key, Byte[] input, AsymmetricPaddingMode paddingMode, Void* paddingInfo, EncryptOrDecryptAction encryptOrDecrypt) at System.Security.Cryptography.RSACng.EncryptOrDecrypt(Byte[] data, RSAEncryptionPadding padding, EncryptOrDecryptAction encryptOrDecrypt) at System.Security.Cryptography.RSACng.Decrypt(Byte[] data, RSAEncryptionPadding padding)

Here is a short sample of the code resulting in the exception:

public X509Certificate2 GetCert() {
    using (var certStore = new X509Store(StoreName.My, StoreLocation.LocalMachine)) {
        certStore.Open(OpenFlags.ReadOnly);
        var certMatches = certStore.Certificates.Find(X509FindType.FindByThumbprint, CertificateThumbprint, false);
        return certMatches[0];
    }
}

var cert = GetCert();
var publicKey = cert.GetRSAPublicKey();
var encryptedBytes = publicKey.Encrypt(bytes, System.Security.Cryptography.RSAEncryptionPadding.OaepSHA256);
var privateKey = cert.GetRSAPrivateKey();

// Exception on this line.  :(
var decryptedBytes = privateKey.Decrypt(encryptedBytes, System.Security.Cryptography.RSAEncryptionPadding.OaepSHA256);

This same code works using RSACryptoServiceProvider. I verified the user has access to the private key of the certificate in the store.

What's causing this access denied exception?

Justin Helgerson
  • 24,900
  • 17
  • 97
  • 124
  • 1
    Am I incorrect in thinking that .NET standard 2.0 contains `RSACryptoServiceProvider`? If so, why do you want to avoid it? – Maarten Bodewes Aug 23 '18 at 20:58
  • @MaartenBodewes It seemed like avoiding it has been the general advice around the .NET community. One example: https://stackoverflow.com/questions/41986995/implement-rsa-in-net-core. – Justin Helgerson Aug 23 '18 at 22:06
  • Meh, that's something about a property being ignored. Generally the code will use the same functions underneath. – Maarten Bodewes Aug 23 '18 at 22:46
  • I appreciate your input @MaartenBodewes. The library I use to work with JWT's expects the `RSA` object and will throw if `RSACryptoServiceProvider` is used. So I am still looking for a solution. – Justin Helgerson Aug 24 '18 at 13:45
  • @MaartenBodewes RSACryptoServiceProvider is Windows CAPI (NT4/XP era), RSACng is Windows CNG (Vista/7). Neither is managed code :) – bartonjs Aug 24 '18 at 14:27
  • 1
    @JustinHelgerson does `((RSACng)privateKey).Key.KeyUsages` say it's a Signing only key? – bartonjs Aug 24 '18 at 14:31
  • @bartonjs Yep, indeed it does. I presume that's a problem here? Unfortunately moving to a new cert isn't easy since we have a base 64 encoded version (with public key) out in the wild that we can't update. Also, I just wanted to say I see you around the .NET community a lot on GitHub and your really helpful. Definitely appreciate the openness. – Justin Helgerson Aug 24 '18 at 14:39

1 Answers1

2

The problem seems to be that when the private key was created (or imported) it got marked as a signing-only key. With a CNG key this can be (and, in this specific case, has been) verified by inspecting the KeyUsages property of the CngKey object (((RSACng)privateKey).Key.KeyUsages).

Before continuing, look at the RSACryptoServiceProvider version of your key. rsaCsp.CspParameters.KeyNumber is what we're really looking for.

switch (rsaCsp.CspParameters.KeyNumber)
{
    case 0:
       You're on the CAPI-to-CNG bridge, new to Windows 10.
       Keep going, this is the answer I answered.
       break;
    case 1:
       This is a CAPI AT_KEYEXCHANGE key.
       Things should just work...
       break;
    case 2:
       This is a CAPI AT_SIGNATURE key, I don't understand why CAPI allowed decryption.
       A different answer is required.
       break;
    default:
       throw new ArgumentOutOfRangeException();
}

Technically speaking, the key usages can't be changed after the key is "finalized" (the CNG term, not the .NET term). But if the key is exportable, you can work around the problem with something like

private static CngKey ResetKeyUsage(CngKey key)
{
    CngKeyCreationParameters keyParameters = new CngKeyCreationParameters
    {
        ExportPolicy = key.ExportPolicy,
        KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey,
    };

    if (key.IsMachineKey)
    {
        keyParameters.Parameters.Add(
            key.GetProperty("Security Descr", (CngPropertyOptions)4));

        keyParameters.KeyCreationOptions |= CngKeyCreationOptions.MachineKey;
    }

    CngKeyBlobFormat rsaPrivateBlob = new CngKeyBlobFormat("RSAPRIVATEBLOB");

    keyParameters.Parameters.Add(
        new CngProperty(
            rsaPrivateBlob.Format,
            key.Export(rsaPrivateBlob),
            CngPropertyOptions.Persist));

    CngAlgorithm alg = key.Algorithm;
    string name = key.KeyName;

    CngKey newKey = CngKey.Create(alg, name, keyParameters);
    key.Dispose();
    return newKey;
}

Note that this should really be a one-time operation, since it undoubtedly does bad things to any open key handles.

If the ExportPolicy says it's Exportable, but not PlaintextExportable, there's a more complicated rigamarole involved (and probably becomes easier to do by directly P/Invoking).

bartonjs
  • 30,352
  • 2
  • 71
  • 111
  • As an aside... I wasn't able to cast to `RSACryptoServiceProvider` in .NET Core (`cert.PrivateKey as RSACryptoServiceProvider` always returns null). So... I fired up LINQPad and was able to get the key number (`(certMatches[0].PrivateKey as RSACryptoServiceProvider).CspKeyContainerInfo.KeyNumber`) which was 2. – Justin Helgerson Aug 24 '18 at 16:50
  • Also, I should mention, before my switch to .NET Core I had to call `ExportCspBlob(true)` and import into `RSACryptoServiceProvider` for any private key usage. – Justin Helgerson Aug 24 '18 at 17:00
  • @JustinHelgerson Ah, that was an important detail. Turns out, I had AT_KEYEXCHANGE and AT_SIGNATURE backwards (updated the "preamble"). Fixing it in CAPI is... different. (Little easier, and a little harder). If you used to do export/import you can continue to do so, of course. – bartonjs Aug 24 '18 at 17:06
  • Sorry, this is a little outside of my wheelhouse. It looks like I can get by with the `GetRSAPrivateKey` method followed by exporting the parameters, calling `RSA.Create` and then importing the parameters. What did you mean by fixing it in CAPI? I honestly never entirely understood why I had to export/import to begin with (but it worked and I had to move on to other requirements). Thanks a lot for your help so far. – Justin Helgerson Aug 24 '18 at 19:39