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 aPkcs9LocalKeyId
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.