Self Signed SSL Client Certificates
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/foobarcd 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 Corpopenssl genrsa -des3 -out acme/ca.key 4096openssl req -new -x509 -days 365 -key acme/ca.key -out acme/ca.crt# Generate for FooBar Ltdopenssl genrsa -des3 -out foobar/ca.key 4096openssl 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 certificateopenssl genrsa -des3 -out acme/server.key 4096# Generate FooBar certificateopenssl 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 requestopenssl req -new -key acme/server.key -out acme/server.csr# Create public certificate by signing with our Acme CAopenssl 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 requestopenssl req -new -key foobar/server.key -out foobar/server.csr# Create public certificate by signing with our FooBar CAopenssl 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.crtcat acme/ca.key acme/server.key > acme/all.keycat foobar/ca.crt foobar/server.crt > foobar/all.crtcat 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
<?phpprint_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
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