Creating certificates using BouncyCastle

by fkollmann 7/16/2009 10:55:57 AM

For one of our projects we required some code to automatically…

  • generate a self-signed certificate,
  • ensure that it’s trusted, and
  • install it into the IIS webapp https:443.

To do this the following steps are performed – the following code has been simplified but shows the way:

0) Notes

  • BouncyCastle has been used to generate the key.
  • RandomGenerators is a helper class of us to generate some random data. It’s no big deal to implement them yourselves.
  • ServerManager is a type of "C:\Windows\System32\inetsrv\Microsoft.Web.Administration.dll"

1) Find existing certificate

var keyStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);

keyStore.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite);

var certs = keyStore.Certificates.Find(
    X509FindType.FindBySubjectDistinguishedName,
    "CN=" + Environment.MachineName,
    true
    );

// find a valid key
X509Certificate2 cert = null;

foreach (var c in certs)
{
    // a private key is required
    if (!c.HasPrivateKey)
    {
        continue;
    }

    return cert;
}

2) Generate new certificate using BouncyCastle (if non was found)

var kpgen = new RsaKeyPairGenerator();

kpgen.Init(new KeyGenerationParameters(
    new SecureRandom(new CryptoApiRandomGenerator()),
    1024
    ));

var kp = kpgen.GenerateKeyPair();

var gen = new X509V3CertificateGenerator();

var certName = new X509Name("CN=" + Environment.MachineName);
var serialNo = BigInteger.ProbablePrime(120, RandomGenerators.CreateRandom());

gen.SetSerialNumber(serialNo);
gen.SetSubjectDN(certName);
gen.SetIssuerDN(certName);
gen.SetNotAfter(DateTime.Now.AddYears(1));
gen.SetNotBefore(DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0)));
gen.SetSignatureAlgorithm("MD5WithRSA");
gen.SetPublicKey(kp.Public);

gen.AddExtension(
    X509Extensions.AuthorityKeyIdentifier.Id,
    false,
    new AuthorityKeyIdentifier(
        SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(kp.Public),
        new GeneralNames(new GeneralName(certName)),
        serialNo
    ));

gen.AddExtension(
    X509Extensions.ExtendedKeyUsage.Id,
    false,
    new ExtendedKeyUsage(new ArrayList()
    {
        new DerObjectIdentifier("1.3.6.1.5.5.7.3.1")
    }));

var newCert = gen.Generate(kp.Private);

3) Import key to Windows keystore

private static X509Certificate2 ConvertToWindows(X509Certificate newCert, AsymmetricCipherKeyPair kp)
{
    var tempStorePwd = RandomGenerators.GetRandomString(50, 75);
    var tempStoreFile = new FileInfo(Path.GetTempFileName());

    try
    {
        // store key
        {
            var newStore = new Pkcs12Store();

            var certEntry = new X509CertificateEntry(newCert);

            newStore.SetCertificateEntry(
                Environment.MachineName,
                certEntry
                );

            newStore.SetKeyEntry(
                Environment.MachineName,
                new AsymmetricKeyEntry(kp.Private),
                new[] { certEntry }
                );

            using (var s = tempStoreFile.Create())
            {
                newStore.Save(
                    s,
                    tempStorePwd.ToCharArray(),
                    new SecureRandom(new CryptoApiRandomGenerator())
                    );
            }
        }

        // reload key
        return new X509Certificate2(tempStoreFile.FullName, tempStorePwd);
    }
    finally
    {
        tempStoreFile.Delete();
    }
}

4) Ensure the certificate is trusted

if (!cert.Verify())
{
    Console.WriteLine("Enforcing trust on certificate...");

    var keyStore = new X509Store(StoreName.AuthRoot, StoreLocation.LocalMachine);

    keyStore.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadWrite);

    try
    {
        keyStore.Add(cert);
    }
    finally
    {
        keyStore.Close();
    }
}

5) Bind the certificate to the IIS webapp

using (var sm = new ServerManager())
{
    foreach (var site in sm.Sites)
    {
        foreach (var bnd in site.Bindings)
        {
            if ((bnd.EndPoint.Port == 443) &&
                (bnd.Protocol == "https"))
            {
                site.Bindings.Remove(bnd);
                site.Bindings.Add(bnd.BindingInformation, cert.GetCertHash(), "MY");

                break;
            }
        }
    }

    // commit changes
    sm.CommitChanges();
}

 

UPDATE: Changes certificate serial number length form 64 bit to 120 bit to match Microsoft’s makecert default.
UPDATE2: There is a known issue: The certificate created here cannot be added to a binding after being created (via API and IIS7 Manager). The workaround is to always create a new certificate.