RSA 解密抛出 'Access denied' 异常

RSA decrypt throws 'Access denied' exception

我正在将一些 .NET Framework 库切换到 .NET Standard。我的一个库使用本地计算机上的证书存储来处理 JSON Web 令牌 (JWT)。图书馆使用的是 RSACryptoServiceProvider,现在好像是

因此,我转而使用 GetPublicKey()GetPrivateKey() 扩展方法,但私钥有问题。每当我在收到的私钥的 RSA 实例上调用 Decrypt 时:

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

这是导致异常的代码的简短示例:

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

同样的代码可以使用 RSACryptoServiceProvider。我验证了用户可以访问商店中证书的私钥。

是什么导致此访问被拒绝异常?

问题似乎是当创建(或导入)私钥时,它被标记为仅签名密钥。通过检查 CngKey 对象 (((RSACng)privateKey).Key.KeyUsages) 的 KeyUsages 属性,可以使用 CNG 密钥(并且,在这种特定情况下,已经)验证这一点。

在继续之前,请查看您的密钥的 RSACryptoServiceProvider 版本。 rsaCsp.CspParameters.KeyNumber 才是我们真正要找的。

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();
}

从技术上讲,在密钥为 "finalized"(CNG 术语,而非 .NET 术语)后,密钥用法无法更改。但是如果密钥是可导出的,您可以使用

之类的方法解决该问题
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;
}

请注意,这实际上应该是一次性操作,因为它无疑会对任何打开的密钥句柄造成不良影响。

如果 ExportPolicy 说它是可导出的,而不是 PlaintextExportable,则涉及更复杂的复杂规则(直接 P/Invoking 可能更容易做到)。