Reputation: 25551
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
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