Always make sure you only have ONE localhost SSL certificate in your store

by Landon | Jun 14, 2017 | Leave a comment

Stay in web development long enough, and eventually you’re going to need to do development work over an SSL connection. Because development work in SSL is typically done on localhost (like most development), you will need a certificate that certifies that localhost is secure. If you don’t, your browser will try to prevent you from navigating to your own locally hosted web server (which is silly since traffic never really leaves your machine).

Typically, the easiest way around this problem is to generate a “Self-signed certificate,” in which you’re basically vouching for yourself in order to satisfy browser SSL cert requirements. If you happen to be using IIS, generating a self-signed cert takes all of about 30 seconds, and storing it in Window’s trusted certificate authority repositories takes just as long.

Up until a few weeks ago, Google Chrome would take these certificates without complaint. But, with a recent update, they began requiring SANs (Subject Alternative Names) to be listed in the certificate. I happen to be using IIS 7, which doesn’t place a SAN by default. So that meant that the usual IIS certificate generation procedure became instantly worthless.

If you happen to be running on Windows 8.1 or later, powershell 4 comes with a cmdlet that allows you to generate a self-signed cert with SANs. I unfortunately am running Windows 7, however, so that cmdlet was unfortunately worthless.

So, I turned to openssl. Here are the steps I took from a linux bash in order to create a valid self-signed certificate with SAN’s.

Step 1: ¬†Write (or copy/paste) an openssl configuration file. … I’ve put mine at the bottom of this post for your reference.

I found this configuration here and made a copy, altering some things I didn’t really understand, but muddling through it anyway. Hopefully that’s a good resource for you.

Step 2: Run this command (This command will generate an ssl certificate and a key that can be folded into a pfx that’s good for 10 years … please note the -config option pulls in the configuration we just created)

openssl req -config localhost.conf -new -x509 -sha256 -newkey rsa:2048 -nodes -keyout localhost.key.pem -days 3652 -out localhost.cert.pem

Step 3: Check your work, if you like.

openssl x509 -in localhost.cert.pem -text -noout

Step 4: Now that you have a certificate and a private key, if you need this certificate to be stored in Windows, you’ll need to convert it to a .pfx certificate. Run this to do that.

openssl pkcs12 -inkey localhost.key.pem -in localhost.cert.pem -export -out localhost.true.pfx

Step 5: Add your certificate to your local trusted root certificate store, and ensure that your web server can serve it with HTTPS requests. (I’m afraid you’ll have to google-fu that one…)

Now, the kicker….

After I had done all these steps, I would occasionally and erratically be served protocol errors when loading localhost on Chrome. Half the time it loaded fine… half the time it didn’t. This frustrated me to no end.

It turns out that I had two certificates in my trusted root store for localhost (or at least I think I did). I had previously made a self-signed certificate straight out of IIS 7, and it was still in IIS, capable of being served on HTTPS requests, though I didn’t have it bound to any ports. After banging my head against my desk repeatedly (in a figurative sense), I finally tried getting rid of the old certificate, simultaneously clearing it out of the trusted root certificate authority store.

Haven’t had a problem since. The moral of the story? Always make sure you only have ONE localhost SSL certificate in your store.

# Self Signed (note the addition of -x509):
#     openssl req -config example-com.conf -new -x509 -sha256 -newkey rsa:2048 -nodes -keyout example-com.key.pem -days 365 -out example-com.cert.pem
# Signing Request (note the lack of -x509):
#     openssl req -config example-com.conf -new -newkey rsa:2048 -nodes -keyout example-com.key.pem -days 365 -out example-com.req.pem
# Print it:
#     openssl x509 -in example-com.cert.pem -text -noout
#     openssl req -in example-com.req.pem -text -noout

[ req ]
default_bits        = 2048
default_keyfile     = server-key.pem
distinguished_name  = subject
req_extensions      = req_ext
x509_extensions     = x509_ext
string_mask         = utf8only

# The Subject DN can be formed using X501 or RFC 4514 (see RFC 4519 for a description).
#   It's sort of a mashup. For example, RFC 4514 does not provide emailAddress.
[ subject ]
countryName         = US

stateOrProvinceName = MT

localityName        = Montana

organizationName    = Montana Interactive LLC

# Use a friendly name here because it's presented to the user. The server's DNS
#   names are placed in Subject Alternate Names. Plus, DNS names here is deprecated
#   by both IETF and CA/Browser Forums. If you place a DNS name here, then you 
#   must include the DNS name in the SAN too (otherwise, Chrome and others that
#   strictly follow the CA/Browser Baseline Requirements will fail).
commonName          = Development Computer

emailAddress            = XXXXXXXXXXXXXX

# Section x509_ext is used when generating a self-signed certificate. I.e., openssl req -x509 ...
[ x509_ext ]

subjectKeyIdentifier        = hash
authorityKeyIdentifier  = keyid,issuer

#  If RSA Key Transport bothers you, then remove keyEncipherment. TLS 1.3 is removing RSA
#  Key Transport in favor of exchanges with Forward Secrecy, like DHE and ECDHE.
basicConstraints        = CA:FALSE
keyUsage            = digitalSignature, keyEncipherment
subjectAltName          = @alternate_names
nsComment           = "OpenSSL Generated Certificate"

# RFC 5280, Section 4.2.1.12 makes EKU optional
# CA/Browser Baseline Requirements, Appendix (B)(3)(G) makes me confused
# extendedKeyUsage  = serverAuth, clientAuth

# Section req_ext is used when generating a certificate signing request. I.e., openssl req ...
[ req_ext ]

subjectKeyIdentifier        = hash

basicConstraints        = CA:FALSE
keyUsage            = digitalSignature, keyEncipherment
subjectAltName          = @alternate_names
nsComment           = "OpenSSL Generated Certificate"

# RFC 5280, Section 4.2.1.12 makes EKU optional
# CA/Browser Baseline Requirements, Appendix (B)(3)(G) makes me confused
# extendedKeyUsage  = serverAuth, clientAuth

[ alternate_names ]

DNS.1       = localhost
DNS.2       = localhost.localdomain
DNS.3       = 127.0.0.1

# IPv6 localhost
DNS.4     = ::1
DNS.5     = fe80::1