Self Signed SSL Client Certificates

29 May 2015 in Tech

For my current project at DataSift I've been working on identifying requests via an SSL client certificate. There's not much out there on how to do it, so I wanted to document it in case I have to do it again in the future.

Setting the scene

Once this goes live, there will be two companies involved, Acme Corp (the client) and FooBar Ltd (the receiver). Each company will have a valid SSL certificate signed by different certificate authorities. For development purposes, both certificate authorities will be self signed, but in production they will likely be signed by Verisign or another similar company.

The purposes of this is to verify that any data sent from Acme Corp to FooBar Ltd is actually from Acme Corp, and not a third party pretending to be them.

Before generating any keys, we'll set up a folder structure to represent it

bash
mkdir -p ssl/acme ssl/foobar
cd ssl

Creating our certificate authorities

The first thing we have to do is create a certificate authority for each company. Files ending in .key are private keys, and files ending in .crt are public keys. If you want to generate certificates without passphrases, remove -des3 from the command. When it asks you for a Common Name for AcmeCorp enter "AcmeCorp" as that's the client name. When it asks for a Common name for Foobar enter "foobar.dev" as that's the domain that we'll be listening on.

bash
# Generate for Acme Corp
openssl genrsa -des3 -out acme/ca.key 4096
openssl req -new -x509 -days 365 -key acme/ca.key -out acme/ca.crt
# Generate for FooBar Ltd
openssl genrsa -des3 -out foobar/ca.key 4096
openssl req -new -x509 -days 365 -key foobar/ca.key -out foobar/ca.crt

Creating server certificates

Once we have a certificate authority, we need to generate some certificates to sign. To start with, we generate private certificates. If you want to generate certificates without passphrases, remove -des3 from the command.

bash
# Generate Acme private certificate
openssl genrsa -des3 -out acme/server.key 4096
# Generate FooBar certificate
openssl genrsa -des3 -out foobar/server.key 4096

Once we have a private certificate, we need to generate a csr (Certificate Signing Request) and have our CA certificate sign the request to create a public certificate. When it asks you for a Common Name for AcmeCorp enter "AcmeCorp" as that's the client name. When it asks for a Common name for Foobar enter "foobar.dev" as that's the domain that we'll be listening on.

bash
# Generate Acme certificate signing request
openssl req -new -key acme/server.key -out acme/server.csr
# Create public certificate by signing with our Acme CA
openssl x509 -req -days 365 -in acme/server.csr -CA acme/ca.crt -CAkey acme/ca.key -set_serial 01 -out acme/server.crt
# Generate FooBar certificate signing request
openssl req -new -key foobar/server.key -out foobar/server.csr
# Create public certificate by signing with our FooBar CA
openssl x509 -req -days 365 -in foobar/server.csr -CA foobar/ca.crt -CAkey foobar/ca.key -set_serial 01 -out foobar/server.crt

Combining certificates

As we're using self signed certificates, we need to send both our site certificate and our CA certificate when identifying ourselves. This is as simple as concatenating the files together. We also do the same for our private keys

bash
cat acme/ca.crt acme/server.crt > acme/all.crt
cat acme/ca.key acme/server.key > acme/all.key
cat foobar/ca.crt foobar/server.crt > foobar/all.crt
cat foobar/ca.key foobar/server.key > foobar/all.key

Configuring Apache + Hosts

Once we have all the certificates we need, we can test that we can access the site over SSL. Create /etc/apache2/sites-enabled/foobar.conf with the following contents:

<VirtualHost *:443> ServerName foobar.dev DocumentRoot /var/www/site SSLEngine On SSLCertificateFile /path/to/ssl/foobar/all.crt SSLCertificateKeyFile /path/to/ssl/foobar/all.key </VirtualHost>

This sets up a site to listen on port 443 with SSL enabled. Create /var/www/site/index.php with the following contents:

php
<?php
print_r($_SERVER);

Finally, add the following line to your hosts file:

127.0.0.1 foobar.dev

Testing the SSL connection

We can test the certificate by trying to connect with curl.

bash
curl https://foobar.dev

We should see something like this:

curl performs SSL certificate verification by default, using a "bundle" of Certificate Authority (CA) public keys (CA certs). If the default bundle file isn't adequate, you can specify an alternate file using the --cacert option. If this HTTPS server uses a certificate signed by a CA represented in the bundle, the certificate verification probably failed due to a problem with the certificate (it might be expired, or the name might not match the domain name in the URL). If you'd like to turn off curl's verification of the certificate, use the -k (or --insecure) option.

We could run the command again with --insecure, but instead let's register FooBar's CA authority with cURL so it can verify the certificate. To do this, we use the --cacert option.

bash
curl --cacert foobar/ca.crt https://foobar.dev

This time, we should see lots of debug information about the server

Enable SSL Environment Passthrough

To enable SSL client certificates, add the following to your Apache2 config:

SSLVerifyClient optional SSLVerifyDepth 1 SSLOptions +StdEnvVars

This optionally allows us to send an SSL Client Certificate (SSLVerifyClient), says that we should verify the certificate authority that signed the client certificate but not the parent of that CA (SSLVerifyDepth) and that we should pass SSL environment variables through to the server (StdEnvVars)

Next, restart apache with /etc/init.d/apache2 restart.

If we make our curl request again, we'll notice there's a lot more information available, starting with SSL_SERVER_.

Sending an SSL Client Certificate

Now that Apache is set up, we can send our client certificate via cURL.

bash
curl --cert acme/all.crt --key acme/all.key --cacert foobar/ca.crt https://foobar.dev

We'll receive the error "unknown ca":

bash
curl: (35) error:14094418:SSL routines:SSL3_READ_BYTES:tlsv1 alert unknown ca

To fix this, we need to tell FooBar's Apache about Acme's Certificate Authority. We do this by adding the following to our VirtualHost:

SSLCACertificateFile /path/to/ssl/acme/ca.crt

Again, restart apache with /etc/init.d/apache2 restart.

If we make our request again we should see the server info again, but this time there should be values that start with SSL_CLIENT_. To make sending a certificate mandatory, edit your VirtualHost and change SSLVerifyClient optional to SSLVerifyClient require and restart apache.

If you send a request after making this change without a client certificate, you'll get the following error:

bash
curl: (35) error:14094410:SSL routines:SSL3_READ_BYTES:sslv3 alert handshake failure

Verifying an identity

Now that we can connect, we need to verify the user's identity. There's two ways to do this:

Firstly, you can check the user's certificate serial number. This is the same so long as the certificate is valid. If a new certificate is generated, the serial number will change. This is the most secure option.

Example code:

php
if ($_SERVER['SSL_CLIENT_M_SERIAL'] !== 'D1D11C749D9C2D16') {
echo "You're not supposed to be here";
exit;
}

It's worth noting that this code will only run if the client certificate is signed by a CA that you trust, but it is not the certificate that you're expecting. If it's not a CA that you trust it will fail with a handshake failure.

The other option for verifying an identity is using the SSL_CLIENT_S_DN_Email. One email address can be used on multiple certificates, so can you no longer be sure that who you're trusting is the person that originally gave you a client certificate. However, as you need to trust the Certificate Authority that signed the certificate before it's accepted this isn't as risky as it could be.

php
if ($_SERVER['SSL_CLIENT_S_DN_Email'] !== '[email protected]') {
echo "You're not supposed to be here";
exit;
}

Of these two options, I'd choose to identify people based on serial numbers rather than email addresses