Reputation: 3478
I'm trying to reload a certificate private key with different export policies to fix this issue. I reused the code from this answer to export the private key, and then import it and set the export policy to AllowPlainTextExport. With that I should be able to reconstruct the original certificate with reimported private key and export its parameters if necessary. Here is the code that I have now:
using Microsoft.Win32.SafeHandles;
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
namespace TestRsaCngExportImport
{
class Program
{
internal const string NcryptPkcs8PrivateKeyBlob = "PKCS8_PRIVATEKEY";
private const int NcryptDoNotFinalizeFlag = 0x00000400;
public const string MicrosoftSoftwareKeyStorageProvider = "Microsoft Software Key Storage Provider";
private static readonly byte[] pkcs12TripleDesOidBytes = Encoding.ASCII.GetBytes("1.2.840.113549.1.12.1.3\0");
static void Main(string[] args)
{
var certificate = CreateCertificate();
FixPrivateKey(certificate);
}
public static void FixPrivateKey(X509Certificate2 certificate)
{
var cngKey = (RSACng)RSACertificateExtensions.GetRSAPrivateKey(certificate);
var exported = ExportPkcs8KeyBlob(cngKey.Key.Handle, "", 1);
var importedKeyName = ImportPkcs8KeyBlob(exported, "", 1);
// Attempt #1
CspParameters parameters = new CspParameters();
parameters.KeyContainerName = importedKeyName;
var rsaKey = new RSACryptoServiceProvider(parameters);
certificate.PrivateKey = rsaKey; // public key doesn't match the private key
// Attempt #2
var rsaCngKey = new RSACng(CngKey.Open(importedKeyName));
certificate.PrivateKey = rsaCngKey; // Only asymmetric keys that implement ICspAsymmetricAlgorithm are supported.
// Attempt #3
certificate.PrivateKey = null;
X509Certificate2 certWithKey = certificate.CopyWithPrivateKey(rsaKey); // The provided key does not match the public key for this certificate.
}
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 unsafe static string ImportPkcs8KeyBlob(byte[] exported, string password, int kdfCount)
{
var pbeParams = new NativeMethods.NCrypt.PbeParams();
var pbeParamsPtr = &pbeParams;
var salt = new byte[NativeMethods.NCrypt.PbeParams.RgbSaltSize];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
rng.GetBytes(salt);
pbeParams.Params.cbSalt = salt.Length;
Marshal.Copy(salt, 0, (IntPtr)pbeParams.rgbSalt, salt.Length);
pbeParams.Params.iIterations = kdfCount;
var keyName = Guid.NewGuid().ToString("D");
fixed (char* passwordPtr = password)
fixed (char* keyNamePtr = keyName)
fixed (byte* oidPtr = pkcs12TripleDesOidBytes)
{
NativeMethods.NCrypt.NCryptOpenStorageProvider(out var safeNCryptProviderHandle, MicrosoftSoftwareKeyStorageProvider, 0);
NativeMethods.NCrypt.NCryptBuffer* buffers = stackalloc NativeMethods.NCrypt.NCryptBuffer[4];
buffers[0] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsSecret,
cbBuffer = checked(2 * (password.Length + 1)),
pvBuffer = (IntPtr)passwordPtr,
};
if (buffers[0].pvBuffer == IntPtr.Zero)
{
buffers[0].cbBuffer = 0;
}
buffers[1] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsAlgOid,
cbBuffer = pkcs12TripleDesOidBytes.Length,
pvBuffer = (IntPtr)oidPtr,
};
buffers[2] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsAlgParam,
cbBuffer = sizeof(NativeMethods.NCrypt.PbeParams),
pvBuffer = (IntPtr)pbeParamsPtr,
};
buffers[3] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsKeyName,
cbBuffer = checked(2 * (keyName.Length + 1)),
pvBuffer = (IntPtr)keyNamePtr,
};
var desc2 = new NativeMethods.NCrypt.NCryptBufferDesc
{
cBuffers = 4,
pBuffers = (IntPtr)buffers,
ulVersion = 0,
};
var result = NativeMethods.NCrypt.NCryptImportKey(safeNCryptProviderHandle, IntPtr.Zero, NcryptPkcs8PrivateKeyBlob, ref desc2, out var safeNCryptKeyHandle, exported, exported.Length, NcryptDoNotFinalizeFlag);
if (result != 0)
throw new Win32Exception(result);
var exportPolicyBytes = BitConverter.GetBytes(
(int)(CngExportPolicies.AllowExport |
CngExportPolicies.AllowPlaintextExport |
CngExportPolicies.AllowArchiving |
CngExportPolicies.AllowPlaintextArchiving));
NativeMethods.NCrypt.NCryptSetProperty(safeNCryptKeyHandle, "Export Policy", exportPolicyBytes, exportPolicyBytes.Length, CngPropertyOptions.Persist);
NativeMethods.NCrypt.NCryptFinalizeKey(safeNCryptKeyHandle, 0);
return keyName;
}
}
private static unsafe byte[] ExportPkcs8KeyBlob(SafeNCryptKeyHandle keyHandle, string password, int kdfCount)
{
var pbeParams = new NativeMethods.NCrypt.PbeParams();
var pbeParamsPtr = &pbeParams;
var salt = new byte[NativeMethods.NCrypt.PbeParams.RgbSaltSize];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
rng.GetBytes(salt);
pbeParams.Params.cbSalt = salt.Length;
Marshal.Copy(salt, 0, (IntPtr)pbeParams.rgbSalt, salt.Length);
pbeParams.Params.iIterations = kdfCount;
fixed (char* stringPtr = password)
fixed (byte* oidPtr = pkcs12TripleDesOidBytes)
{
NativeMethods.NCrypt.NCryptBuffer* buffers =
stackalloc NativeMethods.NCrypt.NCryptBuffer[3];
buffers[0] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsSecret,
cbBuffer = checked(2 * (password.Length + 1)),
pvBuffer = (IntPtr)stringPtr,
};
if (buffers[0].pvBuffer == IntPtr.Zero)
{
buffers[0].cbBuffer = 0;
}
buffers[1] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsAlgOid,
cbBuffer = pkcs12TripleDesOidBytes.Length,
pvBuffer = (IntPtr)oidPtr,
};
buffers[2] = new NativeMethods.NCrypt.NCryptBuffer
{
BufferType = NativeMethods.NCrypt.BufferType.PkcsAlgParam,
cbBuffer = sizeof(NativeMethods.NCrypt.PbeParams),
pvBuffer = (IntPtr)pbeParamsPtr,
};
var desc = new NativeMethods.NCrypt.NCryptBufferDesc
{
cBuffers = 3,
pBuffers = (IntPtr)buffers,
ulVersion = 0,
};
int result = NativeMethods.NCrypt.NCryptExportKey(keyHandle, IntPtr.Zero, NcryptPkcs8PrivateKeyBlob, ref desc, null, 0, out int bytesNeeded, 0);
if (result != 0)
throw new Win32Exception(result);
byte[] exported = new byte[bytesNeeded];
result = NativeMethods.NCrypt.NCryptExportKey(keyHandle, IntPtr.Zero, NcryptPkcs8PrivateKeyBlob, ref desc, exported, exported.Length, out bytesNeeded, 0);
if (result != 0)
throw new Win32Exception(result);
if (bytesNeeded != exported.Length)
Array.Resize(ref exported, bytesNeeded);
return exported;
}
}
private static class NativeMethods
{
internal static class NCrypt
{
public const string NCryptLibraryName = "ncrypt.dll";
[DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
internal static extern int NCryptCreatePersistedKey(SafeNCryptProviderHandle hProvider, [Out] out SafeNCryptKeyHandle phKey, string pszAlgId, string pszKeyName, int dwLegacyKeySpec, CngKeyCreationOptions dwFlags);
[DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
internal static extern int NCryptOpenStorageProvider([Out] out SafeNCryptProviderHandle phProvider, [MarshalAs(UnmanagedType.LPWStr)] string pszProviderName, int dwFlags);
[DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
internal static extern int NCryptExportKey(SafeNCryptKeyHandle hKey, IntPtr hExportKey, string pszBlobType, ref NCryptBufferDesc pParameterList, byte[] pbOutput, int cbOutput, [Out] out int pcbResult, int dwFlags);
[DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
internal static extern int NCryptImportKey(SafeNCryptProviderHandle hProvider, IntPtr hImportKey, string pszBlobType, ref NCryptBufferDesc pParameterList, [Out] out SafeNCryptKeyHandle phKey, [MarshalAs(UnmanagedType.LPArray)] byte[] pbData, int cbData, int dwFlags);
[DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
internal static extern int NCryptSetProperty(SafeNCryptHandle hObject, string pszProperty, [MarshalAs(UnmanagedType.LPArray)] byte[] pbInput, int cbInput, CngPropertyOptions dwFlags);
[DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
internal static extern int NCryptSetProperty(SafeNCryptHandle hObject, string pszProperty, string pbInput, int cbInput, CngPropertyOptions dwFlags);
[DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
internal static extern int NCryptSetProperty(SafeNCryptHandle hObject, string pszProperty, IntPtr pbInput, int cbInput, CngPropertyOptions dwFlags);
[DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
internal static extern int NCryptFinalizeKey(SafeNCryptKeyHandle hKey, int dwFlags);
[DllImport(NCryptLibraryName, CharSet = CharSet.Unicode)]
internal static extern int NCryptExportKey(SafeNCryptKeyHandle hKey, IntPtr hExportKey, string pszBlobType, IntPtr pParameterList, byte[] pbOutput, int cbOutput, [Out] out int pcbResult, int dwFlags);
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct PbeParams
{
internal const int RgbSaltSize = 8;
internal CryptPkcs12PbeParams Params;
internal fixed byte rgbSalt[RgbSaltSize];
}
[StructLayout(LayoutKind.Sequential)]
internal struct CryptPkcs12PbeParams
{
internal int iIterations;
internal int cbSalt;
}
[StructLayout(LayoutKind.Sequential)]
internal struct NCryptBufferDesc
{
public int ulVersion;
public int cBuffers;
public IntPtr pBuffers;
}
[StructLayout(LayoutKind.Sequential)]
internal struct NCryptBuffer
{
public int cbBuffer;
public BufferType BufferType;
public IntPtr pvBuffer;
}
internal enum BufferType
{
PkcsAlgOid = 41,
PkcsAlgParam = 42,
PkcsAlgId = 43,
PkcsKeyName = 45,
PkcsSecret = 46,
}
}
}
}
}
The certificate gets exported and then imported. However, the imported private key cannot be reassigned to the original certificate. I'm getting either "The provided key does not match the public key for this certificate" or "Only asymmetric keys that implement ICspAsymmetricAlgorithm are supported". Is there anything I'm doing wrong?
Upvotes: 0
Views: 1855
Reputation: 33238
// Attempt #1
CspParameters parameters = new CspParameters();
parameters.KeyContainerName = importedKeyName;
var rsaKey = new RSACryptoServiceProvider(parameters);
certificate.PrivateKey = rsaKey; // public key doesn't match the private key
CAPI (the library behind CspParameters) can't understand keys in CNG at all on Windows 7 or 8.1; it (theoretically) has support for it on 10, but you definitely have to tell it that the key lives in CNG (CspParameters.ProviderName).
The code here made a new CAPI key in "Microsoft RSA and AES Enhanced Cryptographic Service Provider" with ProviderType 24 that just happened to have the same local key name as your CNG key.
You didn't specify the flag UseExistingOnly, and the key didn't exist, so it made a new one... and that's why the public key didn't match what's in the certificate.
// Attempt #2
var rsaCngKey = new RSACng(CngKey.Open(importedKeyName));
certificate.PrivateKey = rsaCngKey; // Only asymmetric keys that implement ICspAsymmetricAlgorithm are supported.
The PrivateKey
property only supports CAPI, either in get or set. The set is really dangerous to ever use, because it doesn't modify the cert object, it modifies the state of the cert in the Windows Certificate Store system... which means it also affects any other now or future objects operating on the same (Windows) cert.
// Attempt #3
certificate.PrivateKey = null;
X509Certificate2 certWithKey = certificate.CopyWithPrivateKey(rsaKey); // The provided key does not match the public key for this certificate.
This is the same new random key created from Attempt 1.
If you remove Attempt 1, then merge 2 and 3, you should end up with
var rsaCngKey = new RSACng(CngKey.Open(importedKeyName));
X509Certificate2 certWithKey = certificate.CopyWithPrivateKey(rsaCngKey);
And that should work. (If you've already imported the cert into the cert store, you can just add certWithKey
into the cert store, which will have the same "everyone suddenly knows about this" updating change as cert.set_PrivateKey
, except it's way more obvious that you asked the cert store to take a change)
Upvotes: 1