HPKP Public Key Pinning

A good way to protect your site from fraudulent certificates is to implement HTTP Public Key Pinning a.k.a HPKP. This lets your visitor’s browsers know which certificate they should be expecting from your site. If it receives a certificate it doesn’t expect, the connection will fail.

Implementing this is quite easy as we’re just adding a header to the configuration. To get started we’ll need a few things in place.

We’ll need to decide which certificates we are going to pin. You need to include at least one certificate you ARE using and at least one certificate you are NOT using. The certificate you are not using is your backup certificate. This way, if the certificate you are using and pinning gets revoked or otherwise becomes unusable (you loose the private key, for example), you have a get out of jail free card.

As mentioned, you need to pin at least one certificate you are using. You don’t have to pin your server’s certificate, you are able to pin your certificate authority’s intermediate certificate. This has the advantage of letting you revoke or regenerate your server’s certificate without having to update your HPKP header. This is the method I initially employed after switching to Let’s Encrypt. A good explanation of various pinning methods can be found over on Scott Helme’s blog. [1]

Let’s get started. First we need to make sure the headers module is enable in Apache:

# a2enmod headers

There are a couple of ways to get your certificate’s base64 encoded public key. The easiest would be to head over to SSL Labs and test your site, on that page you will find the “Pin SHA256” for each certificate your server is sending out.

You can also extract this string from a certificate, private key or certificate signing request: [2]

From a certificate:

openssl x509 -in my-certificate.crt -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

From your private key:

openssl rsa -in my-key-file.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64

From your certificate signing request:

openssl x509 -in my-certificate.crt -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

If we wanted to get the public key from Let’s Encrypt’s X3 Intermediate Authority, we can do the following:

# wget -q https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem -O - | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
writing RSA key

We now have the first of our two pins. To get the second, we can generate our own backup certificate. You can of course use any certificate you are not actively using:

# openssl req -nodes -newkey rsa:2048 -keyout example.key -out example.csr
Generating a 2048 bit RSA private key
writing new private key to 'example.key'
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
Country Name (2 letter code) [AU]:CA
State or Province Name (full name) [Some-State]:Ontario
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:.
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:www.example.com
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

We now have our backup certificate generated, we can get it’s pin from the private key:

# openssl rsa -in ./example.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64
writing RSA key

With that we have everything we need to create our header. Edit the config file for your site, e.g. /etc/apache2/sites-enabled/default-ssl.conf and add the following anywhere within virtualhost (substituting your own pins of course). I put mine right next to my Strict-Transport-Security header:

Header always set Public-Key-Pins 'pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="; pin-sha256="zjL4m+14ZUqqAj3No4frEbwv3oMsjQabh1LSDXF0efI="; max-age=5184000; includeSubdomains;'

This will set your header to pin Let’s Encrypt’s intermediate certificate and your backup certificate in a browser for two months which is “considered a balance between the two competing security concerns.” [3]

Be sure to reload apache’s configuration:

# service apache2 reload

And then head over to SSL Labs to make sure you have everything setup correctly.


[1] https://scotthelme.co.uk/guidance-on-setting-up-hpkp/
[2] https://developer.mozilla.org/en/docs/Web/Security/Public_Key_Pinning
[3] https://tools.ietf.org/html/rfc7469#section-4.1