Create a signed certificate with crypt32.dll - c#

I want to create certificates programmatically in C#.net which are signed by a CA. I was able to create a self signed certificate with CertCreateSelfSignCertificate as described here:
http://msdn.microsoft.com/en-us/library/aa376039(VS.85).aspx
Self Signed Certificate in Windows without makecert?
I was looking through the MSDN documentation and I can't seem to find a function to generate a certificate and sign it from a request. Most functions seem to be for manipulating the certificate store. Am I barking up the wrong dll here?

I tried the unmanaged approach as well without success. In contrast to that, creating certificates with BouncyCastle is a breeze:
var keygen = new RsaKeyPairGenerator();
keygen.Init(new KeyGenerationParameters(new SecureRandom(), 2048));
var keys = keygen.GenerateKeyPair();
var certGen = new X509V3CertificateGenerator();
var dnName = new X509Name("CN=Test CA");
certGen.SetSerialNumber(BigInteger.ValueOf(1));
certGen.SetIssuerDN(dnName);
certGen.SetNotBefore(DateTime.Today);
certGen.SetNotAfter(DateTime.Today.AddYears(10));
certGen.SetSubjectDN(dnName);
certGen.SetPublicKey(keys.Public);
certGen.SetSignatureAlgorithm("SHA1WITHRSA");
var cert = certGen.Generate(keys.Private);
This is a self-signed CA certificate, but creating a signed certificate is the same. Just change the issuer and the signing private key. You can also export certificates to DER (.cer) and PKCS12 (.p12) as well.

My tendency would be to try this with capicom.dll, first. It's basically a wrapper for cryptoapi.

I found the answer to this. I loaded up makecert.exe in a debugger and found it was using this call to create a signed certificate: CryptSignAndEncodeCertificate
http://msdn.microsoft.com/en-us/library/aa380277(VS.85).aspx

Related

Pair Certificate Enrollment Request key and X509Certificate2

I'm migrating a tool that creates Certificate Enrollment Request (certmgr.msc) that prompts a security dialog to assign it a password, then send that request to the CA and later the CA sends back a Base 64 encoded certificate. Everything is fine.
Now, that i have that b64string, i can convert it to a certificate using
var bytes = Encoding.UTF8.GetBytes(b64string);
var certificate = new X509Certificate2(bytes);
the thing is that i need to install that certificate in CurrentUser/Personal store to sign transactions, but i can't because the private key is not within the installed certificate.
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
store.Close();
I think i must set the password withing the X509Certificate2 constructor, but how could i get that password? Or how could i pair the certificate request and the installed certificate?
I have some old C code that uses XEnroll i think, that does what i want in 1 line:
hr = m_pXEnroll->raw_acceptPKCS7( PKCS7 );
What i'm doing wrong? Maybe my focus is incorrect
I made this up using CertEnrollLib.
As #Crypt32 pointed, it is just as easy as IX509Enrollment.InstallResponse(...)
This is my code:
CX509Enrollment objEnroll = new CX509EnrollmentClass();
objEnroll.Initialize(X509CertificateEnrollmentContext.ContextUser);
objEnroll.InstallResponse(InstallResponseRestrictionFlags.AllowNone, pkcs7, EncodingType.XCN_CRYPT_STRING_BASE64, null);
In my case, for this code to work i have to have a Certificate Enrollment Request, that i generated using the same lib. I'll post the link only cause it's not directly related to this topic.
https://www.sysadmins.lv/retired-msft-blogs/alejacma/how-to-create-a-certificate-request-with-certenroll-and-net-csharp.aspx
I don't know if there is a more pure C# way of doing this in the framework version i'm using (4.0), but this worked for me.

Azure Key Valut and Expiring Certificate

I hope I can explain this correctly. I inherited a couple of windows applications that need a certificate installed to the local cert store in order to access an Azure Key Vault's Secret to do what the applications do. Currently everything is working correctly. The cert in Azure is set to expire on 10/31/2019.
A new certificate has been created with an expiration in September of 2020.
When I had these applications dumped on me I was give the cert to use but it has a .p12 extension. I can only export the new Azure certificate as .cer or .pfx.
When I install the newly exported cert as either .pfx or .cer the applications fail. If I install the old cert with .p12 extension they work.
Both apps use the code below to get (I think) the local cert that is current via the "Issuer" which is CN = Value. I've checked both the old and new values of "Issuer/CN =" and they are identical.
Does the cert exported in Azure need to have a .p12 extension? If so how do I do that.
If the cert in Azure exported is okay as a .pfx where might my problem(s) be?
C# code in apps that get local cert to in turn gets the necessary Azure secret to do the work:
private static X509Certificate2 ReadCertificateFromStore(string certName)
{
X509Certificate2 cert = null;
try
{
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certCollection = store.Certificates;
// Find unexpired certificates.
X509Certificate2Collection currentCerts = certCollection.Find(X509FindType.FindByTimeValid, DateTime.Now, false);
// From the collection of unexpired certificates, find the ones with the correct name.
X509Certificate2Collection signingCert = currentCerts.Find(X509FindType.FindBySubjectDistinguishedName, certName, false);
// Return the first certificate in the collection, has the right name and is current.
cert = signingCert.OfType<X509Certificate2>().OrderByDescending(c => c.NotBefore).FirstOrDefault();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return cert;
}
First .p12 as well as .pfx are extension for the PKCS#12 format.
Both apps use the code below to get (I think) the local cert that is current via the "Issuer" which is CN = Value. I've checked both the old and new values of "Issuer/CN =" and they are identical.
Based on your code that is not true
// From the collection of unexpired certificates, find the ones with the correct name.
X509Certificate2Collection signingCert =
currentCerts.Find(X509FindType.FindBySubjectDistinguishedName, certName, false);
It says FindBySubjectDistinguishedName which means that the subject of both certificates need to be exactly the same. Here is an example:
And another one with multiple elements in the subject:
You could also install both certificates and play around to figure the parameters to get the right certificate. I converted parts of your code to PowerShell:
$store =
new-object System.Security.Cryptography.X509Certificates.X509Store( `
[System.Security.Cryptography.X509Certificates.StoreName]::My, `
[System.Security.Cryptography.X509Certificates.StoreLocation]::CurrentUser);
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly);
$signingCert =
$store.Certificates.Find(
[System.Security.Cryptography.X509Certificates.X509FindType]::FindBySubjectDistinguishedName,
"CN=...", `
$false);
$signingCert
Mystery solved. In addition to installing the certificate on the machines in question you also need to register the cert (.cer portion) in Azure's App Registrations.

Creating certificate issued based on self-signed root certificate with BouncyCastle

I have been using this repo that allows creating a certificate based on self-signed Root Certificate.
My Root Certificate was given to me in the form of MyCARoot.cer and MyCARoot.pvk They were created (and already deployed) previously with makecert. So in order for me to use the code in the above link I had to combine .cer and .pvk files in .pfx using pvk2pfx.exe
My problem is that after generating the certificate and importing it to Personal store it seems to be invalid. On the Certification Path tab in MMC it says:
The issuer of the certificate could not be found
Note: the only change I made to the code are the password that were used.
The problem with that source was that it was supposed to do this:
X509Certificate issueCert = Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(issuerCertificate);
var authorityKeyIdentifier = new AuthorityKeyIdentifierStructure(issueCert);
certificateGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifier);
instead of this:
var authorityKeyIdentifierExtension =
new AuthorityKeyIdentifier(
SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuerKeyPair.Public),
new GeneralNames(new GeneralName(issuerDN)),
issuerSerialNumber);
certificateGenerator.AddExtension(
X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifierExtension);

"The credentials supplied to the package were not recognized" error when authenticating as server with certificate generated using BouncyCastle

I'm trying to create a certificate using the BouncyCastle.Crypto dll, which is then used to authenticate a SslStream as the server in a Windows Service process, which runs under the Local System account.
However when I get to the SslStream.AuthenticateAsServer(certificate) call, it throws a Win32 exception with the error message "The credentials supplied to the package were not recognized".
There are several questions on here about this error message, but none of them seem to describe, or solve, my particular problem.
In the hope that someone may be able to offer some help, I include the code I am using to create and install the certificate:
// First create a certificate using the BouncyCastle classes
BigInteger serialNumber = BigInteger.ProbablePrime(120, new Random());
AsymmetricCipherKeyPair keyPair = GenerateKeyPair();
X509V1CertificateGenerator generator = new X509V1CertificateGenerator();
generator.SetSerialNumber(serialNumber);
generator.SetIssuerDN(new X509Name("CN=My Issuer"));
generator.SetNotBefore(DateTime.Today);
generator.SetNotAfter(DateTime.Today.AddYears(100));
generator.SetSubjectDN(new X509Name("CN=My Issuer"));
generator.SetPublicKey(keyPair.Public);
generator.SetSignatureAlgorithm("SHA1WITHRSA");
Org.BouncyCastle.X509.X509Certificate cert = generator.Generate(
keyPair.Private, SecureRandom.GetInstance("SHA1PRNG"));
// Ok, now we have a BouncyCastle certificate, we need to convert it to the
// System.Security.Cryptography class, by writing it out to disk and reloading
X509Certificate2 dotNetCert;
string tempStorePassword = "Password01"; // In real life I'd use a random password
FileInfo tempStoreFile = new FileInfo(Path.GetTempFileName());
try
{
Pkcs12Store newStore = new Pkcs12Store();
X509CertificateEntry entry = new X509CertificateEntry(cert);
newStore.SetCertificateEntry(Environment.MachineName, entry);
newStore.SetKeyEntry(
Environment.MachineName,
new AsymmetricKeyEntry(keyPair.Private),
new [] { entry });
using (FileStream s = tempStoreFile.Create())
{
newStore.Save(s,
tempStorePassword.ToCharArray(),
new SecureRandom(new CryptoApiRandomGenerator()));
}
// Reload the certificate from disk
dotNetCert = new X509Certificate2(tempStoreFile.FullName, tempStorePassword);
}
finally
{
tempStoreFile.Delete();
}
// Now install it into the required certificate stores
X509Store targetStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
targetStore.Open(OpenFlags.ReadWrite);
targetStore.Add(dotNetCert);
targetStore.Close();
Ok, now I have created and installed the certificate. I then configure my Windows Service to use this certificate by supplying it with the generated certificate's thumbprint. I then use the certificate like this:
// First load the certificate
X509Certificate2 certificate = null;
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
foreach (X509Certificate2 certInStore in store.Certificates)
{
if (certInStore.Thumbprint == "...value not shown...")
{
certificate = certInStore;
break;
}
}
SslStream sslStream = new SslStream(new NetworkStream(socket, false), false);
// Now this line throws a Win32Exception
// "The credentials supplied to the package were not recognized"
sslStream.AuthenticateAsServer(certificate);
Does anyone have any idea what the problem could be here?
I don't get the problem if I install a certificate created with 'makecert', but that isn't suitable for production certificates.
I've also tried creating a separate x509v1 CA certificate and then x509v3 certificate for server authentication, but I get the same error, so I removed this in the example code for simplicity.
That particular error message rings a bell. I'll guess that either you did not store the private key with the certificate, or, the Windows service does not have access to the private key. To check this, open the Certificates MMC snap-in:
Run mmc (e.g. from the Start menu)
File menu > Add/Remove Snap-in
Select "Certificates" in left pane and then click Add
Select "Computer Account" (for LocalMachine) then click Next,
and then Finish
Navigate to the certificate and double-click in the right pane. On the General tab that comes up, you should see a little key icon at the bottom, along with the text, "You have a private key that corresponds to this certificate." If not, that's the problem. The private key was not saved.
If the private key is present, click Ok to dismiss this dialog, and then right-click on the certificate in the right pane and select on the pop-up menu: All Tasks > Manage Private Keys. In that dialog, make sure that the Windows account that the service runs under has read access to the private key. If it doesn't, that's the problem.
Edit: Oops, you wrote that the service runs as Local System, so it must be a missing private key, if it is one of these two problems. I'll leave the key access check in my answer anyway, for anybody else that hits this and is not running as Local System.
Sometime the problem happens when the application try to reach the certificate doesn't have enough privilege to access the certificate, the issue may resolve by running the application as administrator.
I've the same issue, tried everything from many posts, and google researching.
But looks like I found fix.
When I changed Identify from ApplicationPoolIdentity to LocalSystem everything start working perfectly.
May be will be helpful for someone.
For me works on Windows Server 2012 R2 (.net 4.6.1) - "All Tasks > Manage Private Keys" and set access to Everyone (setting to IS_IUSRS was not enough)
Found this solution online but I can't find the source to give the credit.
Since I ran into the "The credentials supplied to the package were not recognized" problem with AuthenticateAsClient() (for client verification), I'd like to document how I solved it. It's a different method with the same end goal. Since it might be useful for AuthenticateAsServer(), figured why not.
Here I convert a BC Certificate to a .NET certificate. Add an extra step in converting it to a .NET X509Certificate2 to store it's PrivateKey property.
Org.BouncyCastle.X509.X509Certificate bcCert;
X509Certificate dotNetCert = DotNetUtilities.ToX509Certificate(bcCert);
X509Certificate2 dotNetCert2 = new X509Certificate2(dotNetCert);
Problem showed up when adding a BouncyCastle private key to a .NET private key. The X509 certificates converted fine but not the private keys. I converted the BC private key to RSACryptoServiceProvider using the provided DotNetUtilities. Unfortunately it looks like the conversion isn't complete. So I created another RSACryptoServiceProvider which I then initialized. Then I imported the private key into the one I created.
// Apparently, using DotNetUtilities to convert the private key is a little iffy. Have to do some init up front.
RSACryptoServiceProvider tempRcsp = (RSACryptoServiceProvider)DotNetUtilities.ToRSA((RsaPrivateCrtKeyParameters)ackp.Private);
RSACryptoServiceProvider rcsp = new RSACryptoServiceProvider(new CspParameters(1, "Microsoft Strong Cryptographic Provider",
new Guid().ToString(),
new CryptoKeySecurity(), null));
rcsp.ImportCspBlob(tempRcsp.ExportCspBlob(true));
dotNetCert2.PrivateKey = rcsp;
After that, I was able to save the X509Certificate2 object directly to the key store. I didn't need the actual file so I skipped that step.
Previously, every time I have run into this issue, I have had to delete the cert out of my local machine cert store and re-import it. Then it all seems happy. I can't see how it could be a global permissions issue or invalid cert if simply re-importing it fixes the issue.
How I finally fixed it was using the winhttpcertcfg tool from the Windows Resource Kit to grant permission to the specific user that was using the cert.
The syntax would be:
"C:\Program Files (x86)\Windows Resource Kits\Tools\winhttpcertcfg" -i cert.p12 -c LOCAL_MACHINE\My -a UserWhoUsesTheCert -p passwordforp12
I had the similar issue when calling a WCF REST service from .NET application where I need to attach the client certificate; All I had to do was provide access to the certificate in cert store[mmc console] to the "NETWORKSERVICE] off course my IIS Pool was default pool which indicates its using NETWORKService user account.
the mistake that I did was, I copied the cert from another store to Local
Machine -> Personnel store where the certificate was protected with password. should import the certificate explicitly in required store.
If you running from IIS, ensure that the Application Pool has 'Load User Profile' set to true.
This was the only solution for me.
I don't recall this error but the certificate you're creating is not a valid to be used for SSL/TLS, including:
v1 (not v3) certificate;
missing extensions;
invalid CN;
...
There are several RFC that talks about this, including RFC5246 on TLS (1.2).
Finally making your own certificates is not more suitable than using the ones made by makecert (but the last one can generate the minimum set to be usable for an SSL/TLS server certificate).
I strongly suggest you to buy, from a good known Certificate Authority (CA), a SSL/TLS certificate for production. That will get you a working certificate recognized by the most browsers and tools.
Another reason for this error is that you ran the application / server under an account which password has changed, breaking its capability of accessing the certificate it wants to use in the certificate store.
This especially may not be as obvious if you use a NuGet package like LettuceEncrypt which automatically stores the LetsEncrypt in your store.
Delete the certificate from your store and reimport it.

Inserting Certificate (with privatekey) in Root, LocalMachine certificate store fails in .NET 4

I'm having problems inserting a new CA certificate with privatekey in the Root certificate store of the localmachine.
This is what happens:
//This doesn't help either.
new StorePermission (PermissionState.Unrestricted) { Flags = StorePermissionFlags.AddToStore }.Assert();
var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
privkey.PersistKeyInCsp = true;
//This shouldn't be necessary doesn't make a difference what so ever.
RSACryptoServiceProvider.UseMachineKeyStore = true;
cert.PrivateKey = privkey;
store.Open (OpenFlags.MaxAllowed);
store.Add (cert);
store.Close ();
The certificate gets inserted and it all looks dandy: (see!)
Note: is says it has a privatekey.
So you'd say one would be able to find it with FindPrivateKey
C:\Users\Administrator\Desktop>FindPrivateKey.exe Root LocalMachine -t "54 11 b1 f4 31 99 19 d3 5a f0 5f 01 95 fc aa 6f 71 12 13 eb"
FindPrivateKey failed for the following reason:
Unable to obtain private key file name
Use /? option for help
It's cute .... BUT IT'S WRONG!! (2 stupid dogs reference)
And the Certificate export dialog gives me this very fine message:
This code is run while impersonating an administrator using this snippet: click here
I'd just love to know WHY?
(tested on Windows Server 2008 R2 & Windows 7)
I'll be damned!
It works when I compile it to v3.5!!!!
What to do?
I had exactly the same problem and the solution turned out to be really simple.
All I had to do is to pass
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet
to X509Certificate2's ctor.
Now you are using the DotNetUtilities to convert the bouncycastle certificate to the .net one, but the helper method creates the .net cert with the DefaultKeySet (instead of MachineKeySet + PersistKeySet
).
And arrange the private key like this:
var cspParams = new CspParameters
{
KeyContainerName = Guid.NewGuid().ToString(),
KeyNumber = (int)KeyNumber.Exchange,
Flags = CspProviderFlags.UseMachineKeyStore
};
var rsaProvider = new RSACryptoServiceProvider(cspParams);
I hope this helps.
It seems to me you should import the key in a little other way. See http://support.microsoft.com/kb/950090 for an example.
Moreover I find not good to save private key in UseMachineKeyStore. In the most cases you need import certificate with the private key in My store of some user and import in Root only certificate without private key.
It you do need save private key on Machine key store, that you should at least protect the key for reading only for some selected users and not from Everyone. The key container is just a file in the file system (see files in the diriectory "%ALLUSERSPROFILE%\Microsoft\Crypto\Keys") which has security descriptors like other files in NTFS. To change security descriptors of the files you can use CspKeyContainerInfo.CryptoKeySecurity property and AddAccessRule, RemoveAccessRule and so on.
UPDATED: First of all sorry for the long answer.
I could divide your program code in two parts. In the first part you generate a self-signed certificate which can be used as a CA certificates and you save it as rootcert.pfx file. In the second part you import the certificate, but use RSACryptoServiceProvider filled with properties of previous created key instead of using rootcert.pfx.
I suggest to replace the second part of your code to more standard and simple code: import certificate with the private key from rootcert.pfx like it described in http://support.microsoft.com/kb/950090. It works very well.
I don't use myself the BouncyCastle, so I could not comment the first part of your code, but in general what you do in the code you could do also with respect of MakeCert.exe utility from the Windows SDK. You can do like following
MakeCert.exe -pe -ss MY -a sha1 -cy authority -len 2048 -m 120 -r -# 1
-n "CN=Some Root CA, C=NL, OU=BleedingEdge, ST=Somewhere, L=Somelane"
Then you can export certificate with or without private key with respect of Certificate Snap-In (for mmc.exe). In the example above I don't restrict CA for some special EKU, so you can use it without any restriction, but if you do need the restrictions you can just add additional parameters to MakeCert.exe. You can also use MakeCert.exe to create other certificate which are signed with the CA certificate. So you are able to make small PKI with respect of MakeCert.exe only.
It seems to me that creating of the certificate is really a separate part of your code. Your main problem is in the second part.
If you want import CA certificate you should take in consideration some important things:
You should import it in Root or AuthRoot in localMachine on every (or many) computer of your organization, but you should import the certificate without the private key. You can do this with respect of following
CertMgr.exe -add -c CA.cer -s -r localMachine AuthRoot
You should import CA certificate with private key on the computer on one computer and only for the user who will issue other certificates (who will sign new certificates with the private key of CA). One use to import the certificate in the My certificate store of CurrentUser. So the code on the computer could looks like
following:
// import PFX
X509Certificate2 cert = new X509Certificate2 (#"c:\Oleg\rootcert.pfx", "password",
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
// save certificate and private key
X509Store storeMy = new X509Store (StoreName.My, StoreLocation.CurrentUser);
storeMy.Open (OpenFlags.ReadWrite);
storeMy.Add (cert);
// get certificate without private key
// one can import certificate from rootcert.cer instead
byte[] certBlobWithoutPrivateKey = cert.Export (X509ContentType.Cert);
// save pure certificate in Root of the local machine
X509Certificate2 certWithoutPrivateKey = new X509Certificate2 (certBlobWithoutPrivateKey);
X509Store storeRoot = new X509Store (StoreName.Root, StoreLocation.LocalMachine);
storeRoot.Open (OpenFlags.ReadWrite);
storeRoot.Add (certWithoutPrivateKey);
The code will work if you do will change StoreName.My and StoreLocation.CurrentUser to another values, but I don't recommend you to do this.
In general importing of certificates in .NET code look like a little strange and not shows what will be done under the hood. Windows knows only Key Containers where private keys (to be exactly the key pair) will be saved with respect of CSP and Certificate Stores where certificates will be saved (see http://msdn.microsoft.com/en-us/library/bb204781.aspx about location of the store). To be able to save information about the key container in the certificate store Microsoft introduced so named Certificate Extended Properties. If you use in .NET properties of X509Certificate2 like Thumbprint, FriendlyName, HasPrivateKey, Archived and so on you work with the Extended Properties of the certificate. So I recommend you to import CA certificate twice. One in Root or AuthRoot without setting CERT_KEY_PROV_INFO_PROP_ID Certificate Extended Properties and one more time in My store with the setting of information about the place of Key Container with the private key (CERT_KEY_PROV_INFO_PROP_ID). Moreover you can consider to remove private key directly after the usage, import it only if you really need to use it and not hold it permanently. All this is important to have better security.
I have encountered this problem and it seems that even the user with which you are running the FindPrivateKey tool does not have access to the key and therefore you would get the "Unable to obtain private key file name" message. You could run the tool as LocalSystem process.
More information here:
http://www.itsolutionbraindumps.com/2011/02/finding-private-key-for-your.html
Dinko
new X509Certificate2(localPFXPath, inputPass, X509KeyStorageFlags.MachineKeySet & X509KeyStorageFlags.PersistKeySet); with the & instead of the | worked for me.
Usually Certificates in Root won't have private key to manage. You should import to My folder if you are associating key in the web request. I have TLS/SSl exception where I have chain of client certificates. If you store all the chain of certificates in My store then I got rid of that exception. Where the problem is with user accounts. Utility to store the certificates uses current user account and the actual application runs on system account.
The basic problem is that the .NET certificates API is just a wrapper around the C++ advapi32 certificate manager api, so you don’t get to specify all the options that get passed to this api that is actually responsible for sticking the cert into the Windows cert store and persisting the keys. The bottom line is that the “UseMachineStore” option needs to get passed to the CspProviderFlags which in turn gets passed to the CAPI.CRYPT_MACHINE_KEYSET. This is the little guy that determines whether the key gets persisted for real or not. There seem to be several different reasons why this option doesn’t get set even though you set the X509KeyStorageFlags.PersistKeySet and MachineKeySet and Exportable. All these options only live as long as the stupid key stays in the C:\ProgramData\Microsoft\Crypto\RSA\MachineKeys\ folder. If CRYPT_MACHINE_KEYSET doesn’t get set at time of import then advapi32 blows the key away as soon as the certificate handle gets disposed by GC.
Solution: Add the certificate to the Trusted Root BEFORE you import the certificate into the Personal machine store. In reading the logs from CAPI2, I actually see two calls to “X509 Objects” every time the Certificate is “Imported”. One always has the <Flags value="20" CRYPT_MACHINE_KEYSET="true"/>, (what we want) but the other does not UNLESS “Verify Chain Policy” returns no errors. So it looks like advapi32 is checking the “validity” of the cert and either returns an exception that gets swallowed by X509Certificate2 (I love how many empty catch blocks they have in that code) or advapi32 just unilaterally decides to not persist the keys for untrusted certificates. (By the way, I suspect this is a behavior change between 2008 and 20012, but I haven’t proven that.) To work around this, I added an If-check to my code to add the certificate that if the Issuer equals the Subject (it is a self-signed cert) then add the cert to the Root before adding it to My.
if (certificate.Issuer.Equals(certificate.Subject))
{
using (X509Store store = new X509Store(StoreName.Root, StoreLocation.LocalMachine)) {
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
store.Close();
}
}
using (X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine)){
store.Open(OpenFlags.ReadWrite);
store.Add(certificate);
store.Close();
}
Note: I have found that this is unneccessary if using a certificate that does not have a Subject Key Identifier already in it. Somehow when you trigger the api to actually generate the SKI instead of handing it in, it triggers the conditional to pass the magic CRYPT_MACHINE_KEYSET flag to advapi32.

Categories