Bouncy Castle: Add a Subject Alternative Name when creating a Certificate
Bouncy Castle provides a way to assign a Subject Alternative Name while generating a certificate. Like most of the APIs, it can take a little getting used to.
This article shows a few ways not to generate SANs as well as some 'correct' code that helped me generate Alternate DNS Names for my test certificates
References:
- Raw information about Subject Alt Name [www.alvestrand.no]
- Add a SubjectAlternativeName [bouncycastle.org]
- Bouncy Castle GeneralName object [boredwookie.net]
- Adding a SAN with BC [forums.oracle.com]
- Creaet a Cert with a SAN [www.experts-exchange.com]
- Code examples of GeneralName [massapi.com]
Background
I had a devil of a time trying to figure out how to add a DNS Name Subject Alternative Name using Bouncy Castle. Like most things the solution to my problem ended up being deceptively simple. While going along I learned that you can have several different types of SAN:
- DNSName
- DirectoryEntry
- Email address
- Other (scroll down to see the options)
Overview
Here's a quick high-level overview of what goes into creating a certificate with a SAN:
- Everything in this article about generating a certificate (keypair, cert generator, etc...)
- A GeneralName object set to the appropriate type (DNS Name)
- A GeneralNames object which holds the GeneralName object
- Use the AddExtension(Oid, Critical, Asn1Encodable) on the cert generator to add the Alt Name
Working Code Sample
I'll start off with the code that worked for me. That way you can skip all the other crazy stuff I tried and save time:
GeneralName altName = new GeneralName(GeneralName.DnsName, "fred.flintstone.com");
GeneralNames subjectAltName = new GeneralNames(altName);
cGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false, subjectAltName);
Explanation:
- The first line sets up the GeneralName object as a DNS Name
- Line 2 encapsulates this in a GeneralNames object
- Line 3 invokes the AddExtension method on the cert generator I had previously setup and:
- Sets the OID to 2.5.29.17 (Subject Alt Name) using the X509Extensions class
- Sets the criticality to False (I don't know what this means)
- Passes in the GeneralNames object with the Alternate DNS Name
(I'm looking for a code highlighting plugin for concrete5, if anyone knows of one...)
Here is what the working solution looks like:
public static byte[] Generate(string certCN, string signerCN, int bitStrength, String hType, String cType, DateTime validFrom, DateTime validTo) { // Create a keypair var kpGenerator = new RsaKeyPairGenerator(); kpGenerator.Init(new KeyGenerationParameters(new SecureRandom(), bitStrength)); var kp = kpGenerator.GenerateKeyPair(); // Create a certificate var cGenerator = new X509V3CertificateGenerator(); var cCN = new X509Name("CN=" + certCN); var sCN = new X509Name("CN=" + signerCN); var serial = BigInteger.ProbablePrime(120, new Random()); cGenerator.SetSerialNumber(serial); cGenerator.SetSubjectDN(cCN); cGenerator.SetIssuerDN(sCN); cGenerator.SetNotBefore(validFrom); cGenerator.SetNotAfter(validTo); cGenerator.SetSignatureAlgorithm(hType); cGenerator.SetPublicKey(kp.Public); //------ Add a Subject Alternative Name ----- GeneralNames subjectAltName = new GeneralNames(new GeneralName(GeneralName.DnsName, "*.goggles.com")); //subjectAltName = new GeneralNames(new GeneralName(GeneralName.Rfc822Name, "phil@uletide.com"));
//subjectAltName = new GeneralNames(new GeneralName(GeneralName.DnsName, "*.whackamole.com")); cGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false, subjectAltName); // ---- generate the cert & return the encoded bytes ----- var cert = cGenerator.Generate(kp.Private); // Self-signed for now return cert.GetEncoded(); }
Doesn't look to bad, does it? There were a few things I had to figure out before everything fell into place:
- Figure out that I should NOT pass in an X509Name into the constructor of GeneralName
- Finding out the correct constructor took longer than it should have. I could have found it quicker if I had narrowed my search earlier on
- Find out that the GeneralName object has to be encapsulated by a GeneralNames object.
- If you don't do this you get weirdness (more on that below)
Note: I left a line commented out in the 'complete' example so you can see the format for adding an Email address
Note 2: Here is how Windows displays the correct Subject Alt Names:
Some methods that didn't work
I tried a few crazy things before arriving at the correct method described above. Here are some of the more educational methods:
Method 1: Using Issuer Alternative Name instead of Subject Alternative Name
GeneralNames issuerAltName = new GeneralNames(new GeneralName(new X509Name("CN=somedomain.tld"))); cGenerator.AddExtension(X509Extensions.IssuerAlternativeName, false, issuerAltName);
This method was doomed from the start as I wasn't even using the correct OID in the AddExtension method. I did get a nice Issuer alt name, though.
Here's what the Windows Cert Browser showed when I examined the cert (slightly different cert shown- has multiples issuer alt names):
Method 2: Use Subject Alt Name along with a Directory Entry
GeneralNames subjectAltName = new GeneralNames(new GeneralName(new X509Name("CN=somedomain.tld"))); cGenerator.AddExtension(X509Extensions.IssuerAlternativeName, false, subjectAltName);
I did get closer to my goal with this one: It was at least populating the Subject alt Name field. Unfortunately it populated it with a bunch of Directory Entries which didn't work for me. I needed DNS Names!
Method 3: Add a DNSName typed GeneralName object directly to the certificate generator
cGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false, new GeneralName(GeneralName.DnsName, "domain.com"));
I was pretty confident that this one would work as it had the correct type (DnsName) and the GeneralName object type is ASN1Encodable. Unfortunately it didn't work out quite how I expected. When viewed within windows the Subject Alt Name entry looks goofy:
Method 4: Try an Asn1Object instead of a GeneralName
byte[] sanbyte = Encoding.ASCII.GetBytes("*.google.com"); cGenerator.AddExtension(X509Extensions.SubjectAlternativeName, false, new GeneralName(GeneralName.DnsName, Asn1Object.FromByteArray(sanbyte)));
My thinking here was to bypass 'GeneralName' and see if I can't just get some arbitrary bytes encoded. That didn't work out so well, either. Clearly I wasn't supposed to do that:
(EndofStreamException was unhandled. DEF length 46 object truncated by 36)
At this point I was pretty tired. Nothing that I had tried was working and it seemed less and less likely that I would be able to get a SAN generated. I finally tried encapsulating the GeneralName object inside of a GeneralNames before using AddExtension, which did the trick (See the top of the article for that solution).