Reputation: 3458
I have a certificate file provided by another party which I'm loading in my application and cannot export its private key parameters. It looks like the certificate is using CNG rather than CryptoAPI, so I can't access the private key directly, only with GetRSAPrivateKey() method. The method returns RSACngKey
rather than RSACryptoServiceProvider
which is a different implementation of RSA. The problem is that the returned key seems to be missing CngExportPolicies.AllowPlaintextExport
in its export policies, and so I can't export RSA parameters from this certificate. I can reproduce the problem by generating a new certificate that misses necessary export policies:
using System;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace TestRsaCngConsole
{
class Program
{
static void Main(string[] args)
{
var oldCertificate = CreateCertificate();
var oldCertificateBytes = oldCertificate.Export(X509ContentType.Pfx, "");
var newCertificate = new X509Certificate2(oldCertificateBytes, "",
X509KeyStorageFlags.Exportable |
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet);
LogCertificate(oldCertificate, "old certificate"); // this fails
LogCertificate(newCertificate, "new certificate"); // works only on Win10
Console.ReadKey();
}
private static X509Certificate2 CreateCertificate()
{
var keyParams = new CngKeyCreationParameters();
keyParams.KeyUsage = CngKeyUsages.Signing;
keyParams.Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider;
keyParams.ExportPolicy = CngExportPolicies.AllowExport; // here I don't have AllowPlaintextExport
keyParams.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(2048), CngPropertyOptions.None));
var cngKey = CngKey.Create(CngAlgorithm.Rsa, Guid.NewGuid().ToString(), keyParams);
var rsaKey = new RSACng(cngKey);
var req = new CertificateRequest("cn=mah_cert", rsaKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); // requires .net 4.7.2
var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));
return cert;
}
private static void LogCertificate(X509Certificate2 certificate, string name)
{
Console.WriteLine("----- Testing " + name + " ------");
try
{
var rsaPrivateKey = certificate.GetRSAPrivateKey();
var parameters = rsaPrivateKey.ExportParameters(true);
Console.WriteLine("Certificate private key RSA parameters were successfully exported.");
var privateKey = certificate.PrivateKey;
Console.WriteLine("Certificate private key is accessible.");
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
}
The program shows the following output when running on Windows 10:
----- Testing old certificate ------
System.Security.Cryptography.CryptographicException: The requested operation is not supported.
at System.Security.Cryptography.NCryptNative.ExportKey(SafeNCryptKeyHandle key, String format)
at System.Security.Cryptography.CngKey.Export(CngKeyBlobFormat format)
at System.Security.Cryptography.RSACng.ExportParameters(Boolean includePrivateParameters)
at TestRsaCngConsole.Program.LogCertificate(X509Certificate2 certificate, String name) in D:\Projects\TestRsaCngConsole\TestRsaCngConsole\Program.cs:line 44
----- Testing new certificate ------
Certificate private key RSA parameters were successfully exported.
Certificate private key is accessible.
So the first certificate fails to export the private key, because it is missing AllowPlaintextExport flag in its export policies. But after re-loading the old certificate with exportable flags I can export the new certificate parameters just fine. However it doesn't work on Windows Server 2012 or Windows Server 2016 and throws exceptions for both certificates:
----- Testing old certificate ------
System.Security.Cryptography.CryptographicException: The requested operation is not supported.
at System.Security.Cryptography.NCryptNative.ExportKey(SafeNCryptKeyHandle key, String format)
at System.Security.Cryptography.CngKey.Export(CngKeyBlobFormat format)
at System.Security.Cryptography.RSACng.ExportParameters(Boolean includePrivateParameters)
at TestRsaCngConsole.Program.LogCertificate(X509Certificate2 certificate, String name) in D:\Projects\TestRsaCngConsole\TestRsaCngConsole\Program.cs:line 44
----- Testing new certificate ------
System.Security.Cryptography.CryptographicException: The requested operation is not supported.
at System.Security.Cryptography.NCryptNative.ExportKey(SafeNCryptKeyHandle key, String format)
at System.Security.Cryptography.CngKey.Export(CngKeyBlobFormat format)
at System.Security.Cryptography.RSACng.ExportParameters(Boolean includePrivateParameters)
at TestRsaCngConsole.Program.LogCertificate(X509Certificate2 certificate, String name) in D:\Projects\TestRsaCngConsole\TestRsaCngConsole\Program.cs:line 44
I need to be able to fix the certificate and make it possible to export the RSA parameters, even if the cert was originally missing AllowPlaintextExport. What is so different on Windows Server and is there a way to fix the certificate?
Upvotes: 16
Views: 12147
Reputation: 492
I tested whole suggestions but they didn't work for me. Finally I figured out a different solution as follows.
static byte[] DecryptUsingBouncyCastle(byte[] encryptedData)
{
var builder = new Pkcs12StoreBuilder();
var store = builder.Build();
store.Load(File.OpenRead("file path"), "password".ToCharArray());
var alias = store.Aliases.Cast<string>();
var _key = store.GetKey(alias.First());
var cipher = new OaepEncoding(new RsaEngine(), new Sha256Digest(), new Sha1Digest(), null);
cipher.Init(false, _key.Key);
var decrypted = cipher.ProcessBlock(encryptedData, 0, encryptedData.Length);
return decrypted;
}
Upvotes: 2
Reputation: 1071
This is the solution for .NET Framework:
X509Certificate2 cert = new X509Certificate2("certificate.pfx", "password", X509KeyStorageFlags.Exportable);
Source: https://github.com/dotnet/runtime/issues/26031
Upvotes: 10
Reputation: 33098
Unfortunately, the only way to export the key in that state is to P/Invoke into NCryptExportKey to set up an encrypted export; then import that into a new key via NCryptImportKey, and then set the export policy to AllowPlaintextExport.
Starting in .NET Core 3.0 this will be easier:
using (RSA exportRewriter = RSA.Create())
{
// Only one KDF iteration is being used here since it's immediately being
// imported again. Use more if you're actually exporting encrypted keys.
exportRewriter.ImportEncryptedPkcs8PrivateKey(
"password",
rsa.ExportEncryptedPkcs8PrivateKey(
"password",
new PbeParameters(
PbeEncryptionAlgorithm.Aes128Cbc,
HashAlgorithmName.SHA256,
1)),
out _);
return exportRewriter.ExportParameters(true);
}
The .NET Core code for exporting encrypted is at https://github.com/dotnet/corefx/blob/64477348da1ff57a43deb65a4b12d32986ed00bd/src/System.Security.Cryptography.Cng/src/System/Security/Cryptography/CngKey.Export.cs#L126-L237, it's not a very nice API to have to call from C#.
Upvotes: 6