Justin Helgerson
Justin Helgerson

Reputation: 25551

RSA decrypt throws 'Access denied' exception

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?

Upvotes: 2

Views: 3003

Answers (1)

bartonjs
bartonjs

Reputation: 33266

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).

Upvotes: 2

Related Questions