Friday, January 15, 2016

Generating certificates with Subject Alternative Name (SAN) using OpenSSL

SSL's Subject Alternative Name (SAN) allows you to specify multiple names on a single certificate.  In my case, this allows me to validate a certificate whether I'm communicating with the service by name or IP address.

I'm guessing most people would just tell their certificate broker they want a SAN and what the SAN should contain.  I'm running my own CA, so the nuts and bolts of doing this became more important.  Like most things, the "doing" is's the "knowing what to do" that was a little painful.

For my purposes, I need to be generate certificates via a script with no human interaction.  Since I need to know my host's name and IP address, I grab them from the OS:

my_ip=$(ip a show dev eth0 | awk '/inet / { split($2, a, /\//); print a[1]; }' | head -n1)

OpenSSL config files allow me to read these environment variables, which makes automation much simpler, as you'll see below.
First, I'll assume you already have a certificate authority with the requisite keys.  First, generate a key named "my.key":

openssl genrsa -rand /dev/urand -out my.key 2048

We need to create a config file for the next two commands.  You can specify these things on the command line, but it was easier for me to put them in a file.  My config, which I named "my.cnf", looks something like this:

[ req ]
prompt             = no
req_extensions     = v3_req
default_bits       = 2048
distinguished_name = req_distinguished_name
string_mask        = utf8only

[ req_distinguished_name ]
countryName            = US
stateOrProvinceName    = Colorado
localityName           = Rocky Mountains
organizationName       = Snow
organizationalUnitName = Adventure
emailAddress           =
commonName             = $ENV::MY_HN

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage         = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName   = @alt_names

[ alt_names ]
IP0  = $ENV::MY_IP

Note that you can specify multiple IP addresses and DNS names in the SAN, assuming you calter the file and the commands below accordingly.

Now, the first part of the magic.  Generate a certificate signing request named "my.csr" (note that we pass in the env variables the config file is expecting to read):

MY_HN="$my_hn" MY_IP="$my_ip" openssl req -new -key my.key -out my.csr -config my.cnf

What I've written so far is pretty well documented on several blogs like mine.  This next part isn't. When you sign the CSR, you must specify the same extensions with the "-extension" and "-extfile" options.  We can use the same config file, as long as we pass in the variables like we did when generating the CSR.

The command below will generate "my.pem".  Note that we pass in the same variables, use the same config, and use my certificate authority's public key (myca.pem) and private key (myca.key):
MY_HN="$my_hn" MY_IP="$my_ip" openssl x509 -req -in my.csr -CA myca.pem -CAkey myca.key -CAcreateserial -extensions v3_req -extfile my.cnf -out my.pem -days 365

To test it, import "myca.pem" into your browser's trusted CA list.  When you install this certificate (my.pem) on a web server,  you'll be able to browse to that site via name or IP address without complaints or warning.  Inspect the certificate and you'll see something like this:

If you see the Subject Alternative Name in the Subject field, you didn't add the extensions when you signed the certificate.  That's what I was struggling with initially.  Here's what it looks like when it's wrong:

No comments: