1

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);
    }
}
t.probst
  • 11
  • 4
  • See following sample code. Notes at top says you can't get the private key : https://wiki.cac.washington.edu/display/infra/Certificates+and+Private+Key+Permissions – jdweng Aug 30 '23 at 17:48
  • @jdweng That doesn't appear to be true, I am able to inspect the PrivateKey property when I set a breakpoint. – t.probst Aug 30 '23 at 18:12
  • Can you verify the certificate with OpenSSL? Is signature valid? – jdweng Aug 30 '23 at 21:38
  • The original certificate that I import is valid. I am able to use it to encrypt and decrypt. It's once I use the imported data to make a copy of the certificate and private key that the key doesn't match the certificate. I'm doing something wrong in the way I handle the private key data. – t.probst Aug 31 '23 at 11:19
  • I think the algorithm is wrong or padding. See : https://learn.microsoft.com/en-us/windows/win32/api/ncrypt/nf-ncrypt-ncryptcreatepersistedkey You are using RSA which is not necessarily SHA256. The are lots of different RSA options. – jdweng Aug 31 '23 at 12:27
  • @jdweng there is only one algorithm identifier for RSA, the private key is an RSA key. – t.probst Aug 31 '23 at 12:42
  • Search for RSA on this page : https://en.wikipedia.org/wiki/Transport_Layer_Security – jdweng Aug 31 '23 at 12:43
  • @jdweng I don't think that page is directly relevant to my issue. The original key was created using "openssl rsa ..." so the algorithm should be regular RSA. In the documentation for NCryptCreatePersistedKey, the only CNG Algorithm Identifier for the `pszAlgId` parameter is "RSA". – t.probst Aug 31 '23 at 13:24
  • You are verifying with HashAlgorithmName.SHA256. That is not default RSA mode. – jdweng Aug 31 '23 at 14:25
  • @jdweng I see, I didn't realize you were referring to the signature verification function. I have used that same function with HashAlgorithmName.SHA256 to verify the certificate and key match when they were imported using a different method(code in the other question I linked in the 1st paragraph. So I think that's right. – t.probst Aug 31 '23 at 14:36
  • Read the documentation : https://www.openssl.org/docs/manmaster/man1/openssl-rsa.html – jdweng Aug 31 '23 at 15:48

0 Answers0