I'm trying to use CNG through PInvoke to work with x509 certificates. My goal is to import a certificate with its private key, set the flag NCRYPT_UI_FORCE_HIGH_PROTECTION_FLAG
(forces a password to be input when the key is accessed), and add the certificate and key back to the store (more details in a different question I posted)
A password is required when the certificate is added to the store, as expected, and the store shows the certificate as having a private key. After fetching the new certificate from the store, HasPrivateKey is true and I can see the key if I set a breakpoint, but the private key can't be found when trying to export, and trying to verify a signature fails.
This is my code for importing data from a certificate, creating a certificate context from it, and importing the key data into a new CNG key before adding it to the store.
//certData comes from calling Export(X509ContentType.Pfx) on an X509Certificate2
public static int InstallCertificate(byte[] certData)
{
using (X509Certificate2 certificate = new X509Certificate2(certData, "", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.UserKeySet))
{
//Create a certificate context with the imported certificate data
byte[] certBytes = certificate.RawData;
const int X509_ASN_ENCODING = 0x1;
IntPtr certContext = CertCreateCertificateContext(X509_ASN_ENCODING, certBytes, certBytes.Length);
if (certContext == IntPtr.Zero)
{
var err = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
try
{
int result;
IntPtr hProvider;
const string MS_KEY_STORAGE_PROVIDER = "Microsoft Software Key Storage Provider";
openResult = NCryptOpenStorageProvider(out hProvider, MS_KEY_STORAGE_PROVIDER, 0);
if (openResult != 0)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
IntPtr hKey = IntPtr.Zero;
byte[] keyBlob = certificate.GetRSAPrivateKey().ExportPkcs8PrivateKey();
//get certificate name to use for the key
StringBuilder subjectName = new StringBuilder(1024);
if (CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, IntPtr.Zero, subjectName, (uint)subjectName.Capacity) > 0)
{
subjectName.ToString();
}
int createResult = NCryptCreatePersistedKey(hProvider, out hKey, "RSA", subjectName.ToString(), 0, 0);
if (createResult == -2146893809) //key already exists in the key container
{
int openResult = NCryptOpenKey(hProvider, out hKey, subjectName.ToString(), 0, 0);
if (openResult != 0)
{
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
Console.WriteLine("PRIVATE KEY FOUND");
}
else if (createResult != 0)
{
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
else
{
//Make key exportable and enforce strong protection
uint exportPolicy = NCRYPT_ALLOW_EXPORT_FLAG;
int exportSetResult = NCryptSetProperty(
hKey,
"Export Policy",
BitConverter.GetBytes(exportPolicy),
sizeof(uint),
0);
NCRYPT_UI_POLICY uiPolicy = new NCRYPT_UI_POLICY
{
dwVersion = 1,
dwFlags = NCRYPT_UI_FORCE_HIGH_PROTECTION_FLAG,
pszCreationTitle = Marshal.StringToHGlobalUni("Key Creation"),
pszFriendlyName = Marshal.StringToHGlobalUni("My Key"),
pszDescription = Marshal.StringToHGlobalUni("My Key Description")
};
byte[] uiPolicyBytes = StructureToByteArray(uiPolicy);
int protectionSetResult = NCryptSetProperty(
hKey,
NCRYPT_UI_POLICY_PROPERTY,
uiPolicyBytes,
(uint)uiPolicyBytes.Length,
0);
//Get the key data in DER format to import into the CNG key
AsymmetricAlgorithm pk = certificate.PrivateKey;
AsymmetricCipherKeyPair pkPair = DotNetUtilities.GetKeyPair(pk);
PrivateKeyInfo pkInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(pkPair.Private);
byte[] derEncodedPrivateKey = pkInfo.ToAsn1Object().GetDerEncoded();
int importResult = NCryptImportKey(hProvider, hKey, "PKCS8_PRIVATEKEY", IntPtr.Zero, out _, derEncodedPrivateKey, derEncodedPrivateKey.Length, 0);
if (importResult != 0)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
int finalizeResult = NCryptFinalizeKey(hKey, 0);
// Cleanup any allocated unmanaged memory
Marshal.FreeHGlobal(uiPolicy.pszCreationTitle);
Marshal.FreeHGlobal(uiPolicy.pszFriendlyName);
Marshal.FreeHGlobal(uiPolicy.pszDescription);
}
//associate the CNG key with the certificate context
CRYPT_KEY_PROV_INFO provInfo = new CRYPT_KEY_PROV_INFO
{
pwszContainerName = subjectName.ToString(),
pwszProvName = "Microsoft Software Key Storage Provider",
dwProvType = 0,
dwFlags = unchecked((int)CERT_NCRYPT_KEY_SPEC),
cProvParam = 0,
rgProvParam = IntPtr.Zero,
dwKeySpec = 1
};
bool contextSet = CertSetCertificateContextProperty(certContext, CERT_KEY_PROV_INFO_PROP_ID, 0, ref provInfo);
if (!contextSet)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
//Open the store and add the certificate
IntPtr hStore = CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, IntPtr.Zero, CERT_SYSTEM_STORE_CURRENT_USER, "MY");
if (hStore == IntPtr.Zero)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
bool addCert = CertAddCertificateContextToStore(hStore, certContext, CERT_STORE_ADD_REPLACE_EXISTING, IntPtr.Zero);
if (!addCert)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
//Fetch the certificate from the store and check if the key matches
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certs = store.Certificates.Find(X509FindType.FindBySubjectName, subjectName.ToString(), false);
X509Certificate2 cert = null;
if (certs.Count > 0)
{
cert = certs[0];
}
store.Close();
if (cert != null && cert.HasPrivateKey)
{
var x = cert.GetRSAPrivateKey();
if (IsPrivateKeyMatching(cert, x))
{
Console.WriteLine("Private key matches the certificate.");
}
else
{
Console.WriteLine("Private key does NOT match the certificate.");
}
}
}
NCryptFreeObject(hKey);
NCryptFreeObject(hProvider);
}
finally
{
CertFreeCertificateContext(certContext);
}
}
return 1;
}
This is my code for checking if the key matches:
public static bool IsPrivateKeyMatching(X509Certificate2 certificate, RSA privateKey)
{
byte[] dataToSign = Encoding.UTF8.GetBytes("SampleData"); // Any random data will work
byte[] signature;
// Create a signature using the private key
using (var rsa = privateKey)
{
signature = rsa.SignData(dataToSign, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
// Verify the signature using the certificate's public key
using (var rsa = certificate.GetRSAPublicKey())
{
return rsa.VerifyData(dataToSign, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}