Private Key Doesn't Match X509 Certificate After Converting to CNG

180 Views Asked by At

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

There are 0 best solutions below