0

I am trying to use .Net 7 to programmatically add an X509 certificate and private key to the Windows certificate store.

The intended use of the certificate is for encrypting a CODESYS project file. The certificate has to be added to the current user store to be used in CODESYS.

I want to programmatically enforce strong private key protection. I am having trouble finding and understanding information about how to do this properly, but have managed to get it to work using CNG, by creating a new RSACng key from the existing certificate's private key, setting the UIPolicy in the CngKeyCreationParameters, and re-associating the new key with the certificate:

public static int InstallCertificateCurrentUser(byte[] certData)
{
    using (X509Store store = new X509Store("My", StoreLocation.CurrentUser))
    {
        store.Open(OpenFlags.ReadWrite);

        using (X509Certificate2 certificate = new X509Certificate2(certData, "", X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.UserKeySet | X509KeyStorageFlags.UserProtected)) /* ephemeralkeyset flag causes an operation not supported exception */
        {
            RSACng rsaCNG = new RSACng();
            rsaCNG.FromXmlString(certificate.GetRSAPrivateKey().ToXmlString(true));
            var keyData = rsaCNG.Key.Export(CngKeyBlobFormat.GenericPrivateBlob);
  
            var keyParams = new CngKeyCreationParameters
            {
                ExportPolicy = CngExportPolicies.AllowExport,
                KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey,
                UIPolicy = new CngUIPolicy(CngUIProtectionLevels.ForceHighProtection)
            };

            keyParams.Parameters.Add(new CngProperty(CngKeyBlobFormat.GenericPrivateBlob.Format, keyData, CngPropertyOptions.None));

            using (var key = CngKey.Create(CngAlgorithm.Rsa, "testKey", keyParams))
            {
                rsaCNG = new RSACng(key);

                using (X509Certificate2 certOnly = new X509Certificate2(certificate.Export(X509ContentType.Cert), "", X509KeyStorageFlags.Exportable))
                {
                    X509Certificate2 certWithKey = certOnly.CopyWithPrivateKey(rsaCNG);
                    store.Add(certWithKey);
                }
            }
        }
    }
}

While this works for setting strong protection on the private key when the certificate is added to the windows key store, the certificate no longer works with CODESYS. After encrypting and closing the file, an error occurs when trying to open it again.

CODESYS documentation is very hard to find so I can't be completely sure as to the cause. Through experimentation, I have narrowed it down to the Bag Attributes that .Net adds to the certificate and key.

I export the certificate and key from the certificate store and then get the certificate and key using OpenSSL:

openssl pkcs12 -in certAndKey.pfx -nocerts -out privateKey.key

openssl pkcs12 -in certAndKey.pfx -clcerts -nokeys -out ceert.crt

(sensitive data here has been replaced with '<x>')

Bag Attributes
    localKeyID: 01 00 00 00 
subject=C = <x>, ST = <x>, O = <x>, OU = <x>, CN = <x>
issuer=C = <x>, ST = <x>, L = <x>, O = <x>, OU = <x>, CN = <x>
-----BEGIN CERTIFICATE-----
<encoded certificate>
-----END CERTIFICATE-----
Bag Attributes
    localKeyID: 01 00 00 00 
    friendlyName: testKey
    Microsoft CSP Name: Microsoft Software Key Storage Provider
Key Attributes: <No Attributes>
-----BEGIN ENCRYPTED PRIVATE KEY-----
<encoded private key>
-----END ENCRYPTED PRIVATE KEY-----

If I edit the certificate file, completely removing the bag attriubutes, or edit the localKeyID of the key file to match the certificate's thumbprint, then recombine them with OpenSSL and import the resulting file into the certificate store, decryption of the encrypted CODESYS project works again.

OpenSSL command used to recombine the certificate and key:

openssl pkcs12 -export -out newCertificateAndKey.pfx -inkey privateKey.key -in certificate.crt

But now strong private key protection is no longer enforced. The goal is to avoid having to rely on the user dealing with the certificate at all, they just have to run a utility.

Things I have tried:

  • I have tried to use Pkcs12StoreBuilder() to create the certificate, because I think I should be able to nest a Pkcs9LocalKeyId object within it to properly set the localKeyID bag attribute. Ignoring the fact that I haven't figured out how to properly do that yet, I can't find a way to enforce strong key protection through this method.

  • I have found a some other stackoverflow posts hinting that I might be able to set the bag attributes using the BouncyCastle cryptography API, but I can't find good documentation on how to do it. I don't think I would be able to enforce strong key protection with bouncy castle, so I think I'd end up having the same issue with bag attributes when converting the BouncyCastle object to a .Net one.

  • I have tried to export the certificate and key from .Net as PEM files and edit the files to remove the bag attributes or set the key's localKeyID to the thumbprint of the certificate. The issue is that my changes aren't persisted when I recombine the certificate and key in .Net to add them to the certificate store.

The problem seems to be specifically with the RSACertificateExtensions.CopyWithPrivateKey(X509Certificate2, RSA) method that associates the key with the certificate. This is the step that adds the bag attributes, and I have not been able to find a way to accomplish enforcing strong key protection without using it.

What I'm trying to do right now and think has the best chance of working:

I found the source code for CopyWithPrivateKey. It doesn't seem to deal with bag attributes, so I think that must be done in the call to CertificateExtensionsCommon.CopyWithPersistedCngKey(X509Certificate2, CngKey). My searches for this turn up next to nothing, I've only found one mention of it here. I'm not sure if this is the right thing to look at, the file seems to have been from a different library and doesn't exist on Github anymore. I'm not sure if anything is being done to bag attributes here or not. It's using Crypt32 and I have looked at documentation for wincrypt.h but I'm still working on understanding it.

I think it's possible to do what I want using PInvoke and Crypt32. I just can't find good examples to help me understand what's going on.

I think this is a very niche problem and I've hit a wall. Crossing my fingers someone can give me some advice or guidance.

EDIT: I have figured out most of the process for doing this using PInvoke. I can create the certificate and private key, enforce strong key protection, and add the certificate and key to the store. The problem I'm still having is that I'm doing something wrong with the private key data, so while the private key is associated with the certificate, it doesn't match it anymore. I have posted another question about this.

t.probst
  • 11
  • 4
  • Asking ChatGPT 4 it uses Bouncy Castle to create a password protected PFX in memory (PKCS#12), and then uses `return new X509Certificate2(stream.ToArray(), password, X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable)` which can then be added to the `My` store. – Maarten Bodewes Aug 18 '23 at 08:16
  • 1
    @MaartenBodewes This doesn't work for my needs. Strong private key protection won't be enforced this way. BouncyCastle can't set Windows certificate store key protection. I tried asking ChatGPT about how to do this and it indicated it can only be done through CNG or by creating a custom CSP or KSP. It was only able to provide a rough outline of the method for doing it with CNG which didn't include any of the declarations for PInvoke. Unfortunately that's the part I'm having the most difficulty with and I have already come across the code ChatGPT provided. – t.probst Aug 18 '23 at 12:48
  • "bag attributes" only apply while it's in PFX/PKCS12 form. Once it's in the cert store, those attributes don't matter. Importing the PFX with KeyStorageFlags.UserProtected should be all you need... what's not working with that that has made you write the rest of this code? – bartonjs Aug 28 '23 at 17:45
  • @bartonjs Importing the PFX with UserProtected enables private key protection, but leaves the choice of medium or high level to the user. If medium the user only receives a notification when the key is accessed instead of having to input a password. As for the bag attributes, that's the only difference I could find between the certificate I created using CNG and the same certificate created with OpenSSL, it may be related to CODESYS but our support package has run out and not yet been renewed. – t.probst Aug 29 '23 at 18:38

0 Answers0