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:

 

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:

 

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:

  1. Figure out that I should NOT pass in an X509Name into the constructor of GeneralName
    1. 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
  2. Find out that the GeneralName object has to be encapsulated by a GeneralNames object.
    1. 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:
6-SuccessWithGeneralNames.png

 

 

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):
1-DirectoryEntriesOnlyIssuerAltname.png

 

 

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!
2-DirectoryEntriesonlySAN.png

 

 

 

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:
3-WeirdnessWithOnlyAGeneralName.png

 

 

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:
4-ExceptionWithByteMethod.png
(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).