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.