Add a generated certificate to the store and update an IIS site binding

5

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):

  • This works on a different box (my personal development machine) but I hit these snags on a server machine (running Windows Server 2012 R2)
  • The IIS Help dialog informs me the machine is running IIS 8.5
  • Verified the validity generated certificate and the CA certificate with CertUtil.exe
  • Verified the generated certificate and the CA certificate had a private key that could be found
  • Verified administrators (and eventually even my logged in account) had access to where the private key file for both the CA certificate and the generated certificate.

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.

c#
ssl
iis
bouncycastle
x509certificate2
asked on Stack Overflow Nov 30, 2016 by Fizz • edited Nov 30, 2016 by Fizz

2 Answers

2

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.

answered on Stack Overflow Dec 1, 2016 by bartonjs
0

For us, it was related to an invalid certificate. We went to IIS >> Server Certificates and exported the certificate from there.

enter image description here

The certificate was correctly bound to IIS site after that.

answered on Stack Overflow Nov 1, 2019 by Raghav

User contributions licensed under CC BY-SA 3.0