I'm running into the following and after feeling like I've exhausted various avenues of research on Google and Stack Overflow I decided to just ask my own question about it.
I'm trying to generate a personal certificate (using BouncyCastle) based on a CA certificate that I already have and own. After generating the certificate, placing it in the 'My' store, I then attempt to update my IIS website's SSL binding to use this new certificate.
What I'm noticing is that the updates to the IIS website (using ServerManager
) are not throwing exception, yet when I go to the IIS Manager console I notice the website's binding has no SSL certificate selected. When I attempt to select the certificate that I created (shows up fine as a viable option) I get the following error message:
A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520)
As a test I exported my generated certificate (with the private key) and reinstalled it via the wizard and then once again tried setting up the binding (via IIS Manager) which worked.
Because of this behavior I assumed it was an issue with how I was generating or adding the certificate to the store. I was hoping someone may have some idea of what the issue I'm having may be. The following are the relevant functions (I believe) used in creating the certificate, adding it to the store, and updating the website's binding programmatically:
Main function the generates that get the CA certificate private key, generates the personal self-signed certificate, and updates the sites binding:
public static bool GenerateServerCertificate(
X509Certificate2 CACert,
bool addToStore,
DateTime validUntil)
{
try
{
if (CACert.PrivateKey == null)
{
throw new CryptoException("Authority certificate has no private key");
}
var key = DotNetUtilities.GetKeyPair(CACert.PrivateKey).Private;
byte[] certHash = GenerateCertificateBasedOnCAPrivateKey(
addToStore,
key,
validUntil);
using (ServerManager manager = new ServerManager())
{
Site site = manager.Sites.Where(q => q.Name == "My Site").FirstOrDefault();
if (site == null)
{
return false;
}
foreach (Binding binding in site.Bindings)
{
if (binding.Protocol == "https")
{
binding.CertificateHash = certHash;
binding.CertificateStoreName = "MY";
}
}
manager.CommitChanges();
}
}
catch(Exception ex)
{
LOG.Error("Error generating certitifcate", ex);
return false;
}
return true;
}
Generating the certificate based on the CA private key:
public static byte[] GenerateCertificateBasedOnCAPrivateKey(
bool addToStore,
AsymmetricKeyParameter issuerPrivKey,
DateTime validUntil,
int keyStrength = 2048)
{
string subjectName = $"CN={CertSubjectName}";
// Generating Random Numbers
CryptoApiRandomGenerator randomGenerator = new CryptoApiRandomGenerator();
SecureRandom random = new SecureRandom(randomGenerator);
ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerPrivKey, random);
// The Certificate Generator
X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator();
certificateGenerator.AddExtension(
X509Extensions.ExtendedKeyUsage,
true,
new ExtendedKeyUsage((new List<DerObjectIdentifier> { new DerObjectIdentifier("1.3.6.1.5.5.7.3.1") })));
// Serial Number
BigInteger serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
certificateGenerator.SetSerialNumber(serialNumber);
// Issuer and Subject Name
X509Name subjectDN = new X509Name(subjectName);
X509Name issuerDN = new X509Name(CACertificateName);
certificateGenerator.SetIssuerDN(issuerDN);
certificateGenerator.SetSubjectDN(subjectDN);
// Valid For
DateTime notBefore = DateTime.UtcNow.Date;
DateTime notAfter = validUntil > notBefore ? validUntil : notBefore.AddYears(1);
certificateGenerator.SetNotBefore(notBefore);
certificateGenerator.SetNotAfter(notAfter);
// Subject Public Key
AsymmetricCipherKeyPair subjectKeyPair;
var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
var keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(keyGenerationParameters);
subjectKeyPair = keyPairGenerator.GenerateKeyPair();
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
// Generating the Certificate
Org.BouncyCastle.X509.X509Certificate certificate = certificateGenerator.Generate(signatureFactory);
// correcponding private key
PrivateKeyInfo info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private);
// merge into X509Certificate2
X509Certificate2 x509 = new X509Certificate2(certificate.GetEncoded());
Asn1Sequence seq = (Asn1Sequence)Asn1Object.FromByteArray(info.ParsePrivateKey().GetDerEncoded());
if (seq.Count != 9)
{
throw new PemException("Malformed sequence in RSA private key");
}
RsaPrivateKeyStructure rsa = RsaPrivateKeyStructure.GetInstance(seq);
RsaPrivateCrtKeyParameters rsaparams = new RsaPrivateCrtKeyParameters(
rsa.Modulus,
rsa.PublicExponent,
rsa.PrivateExponent,
rsa.Prime1,
rsa.Prime2,
rsa.Exponent1,
rsa.Exponent2,
rsa.Coefficient);
x509.PrivateKey = DotNetUtilities.ToRSA(rsaparams);
if (addToStore)
{
// Add certificate to the Personal store
AddCertToStore(x509, StoreName.My, StoreLocation.LocalMachine, "Certificate Friendly Name");
}
return x509.GetCertHash();
}
Adding the certificate to the store:
private static void AddCertToStore(X509Certificate2 cert, StoreName storeName, StoreLocation storeLocation, string friendlyName)
{
X509Store store = new X509Store(storeName, storeLocation);
try
{
store.Open(OpenFlags.ReadWrite);
store.Add(cert);
if (!string.IsNullOrWhiteSpace(friendlyName)) {
var certs = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, cert.Subject, true);
if (certs.Count > 0)
{
certs[0].FriendlyName = friendlyName;
}
}
}
finally
{
store.Close();
}
}
Just a final note, I have tried a few things from what I've seen on various sites in regards to that error (doesn't seem very clear what the issue is):
Any ideas what my issue could be?
Update:
I was able to get some results by doing the following:
Export my certificate to a file programmatically by doing File.WriteAllBytes(filePath, cert.Export(X509ContentType.Pkcs12, password));
Then I import this certificate file to the store by doing:
var cert = new X509Certificate2(certFilePath, certPassword, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
// My original AddCertToStore function
AddCertToStore(cert, StoreName.My, StoreLocation.LocalMachine, "Friendly Name");
Finally, I set the binding as I was doing earlier:
using (ServerManager manager = new ServerManager())
{
Site site = manager.Sites.Where(q => q.Name == "My Site").FirstOrDefault();
if (site == null)
{
return false;
}
foreach (Binding binding in site.Bindings)
{
if (binding.Protocol == "https")
{
binding.CertificateHash = certHash;
binding.CertificateStoreName = "MY";
}
}
manager.CommitChanges();
}
Doing it this way works, but I don't see why I would have export the certificate to a file, THEN load it into a X509Certificate2 object, add to the store, and finally set up the binding.
The ToRSA
method most likely creates an ephemeral RSA key, so when the references are all gone the key gets deleted. Exporting the ephemeral structure into a PFX then re-importing it with PersistKeySet is one way to turn it into a persisted key. Others exist, but that one is one of the less convoluted ones.
You don't actually have to write it to a file, though.
byte[] pkcs12Blob = cert.Export(X509ContentType.Pkcs12, password);
ver certWithPersistedKey = new X509Certificate2(pkcs12Blob, password, allTheFlagsYouAlreadySet);
There are also other subtleties going on, like setting the PrivateKey property has different behaviors for a cert instance that was loaded from a store and one which was loaded from bytes... the PFX/PKCS#12 export/import works around all of those.
For us, it was related to an invalid certificate. We went to IIS >> Server Certificates and exported the certificate from there.
The certificate was correctly bound to IIS site after that.
User contributions licensed under CC BY-SA 3.0