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
YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=

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
zjL4m+14ZUqqAj3No4frEbwv3oMsjQabh1LSDXF0efI=

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.

 

References:
[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

Enabling HTTP2 on Apache under Ubuntu 16.04

I reciently upgraded to Ubuntu Server 16.04 and was bummed to find out that HTTP2 support had been removed from Apache. Luckily we can easily add the plug-in and enable it. [1]

To do this, we need to build Apache ourselves and then copy out the one file we need. Let’s make sure we have the prerequisites:

apt-get install devscripts build-essential fakeroot

Now install libnghttp2 and Apache:

apt-get install libnghttp2-dev apache2

Now we build Apache from source. Check to make sure these three lines are added and not commented from /etc/apt/sources.list:

deb-src http://archive.ubuntu.com/ubuntu xenial main restricted universe
deb-src http://archive.ubuntu.com/ubuntu xenial-updates main restricted universe
deb-src http://security.ubuntu.com/ubuntu xenial-security main restricted universe multiverse

And update for good measure:

apt-get update

Now we’re ready to begin:

cd /tmp
mkdir apache2
cd apache2
apt-get source apache2
apt-get build-dep apache2
cd apache-2.4.18
fakeroot debian/rules binary

Now we can move mod_http2.so to Ubuntu’s Apache installation:

cp /tmp/apache2/apache-2.4.18/debian/apache2-bin/usr/lib/apache2/modules/mod_http2.so /usr/lib/apache2/modules/

Next we create the load file:

nano /etc/apache2/mods-available/http2.load

And paste this line:

LoadModule http2_module /usr/lib/apache2/modules/mod_http2.so

Next edit your site’s .conf file to include:

Protocols h2 http/1.1

Now we can enable http2 and restart apache:

a2enmod http2
service apache2 restart

That’s it! For cleanup, you can remove the /tmp/apache2 folder and remove the prerequisites if you no longer need them:

rm -Rf /tmp/apache2
apt-get remove devscripts build-essential fakeroot
apt-get autoremove

References:
1. https://zitseng.com/archives/10470

Updated SSLCipherSuite String

Chrome has been complaining “Your connection to website is encrypted with obsolete cryptography”.  I made a change to my SSLCipherSuite string located at /etc/apache2/mods-available/ssl.conf to fix this.
 

Chrome would like you to be using anything with a higher hash than SHA1 and using GCM instead of CBC suites.[1]  For a simple fix, we can move the one Chrome currently prefers to the top of the list:

SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDH+HIGH:EDH+HIGH:-3DES:DES-CBC3-SHA:!CAMELLIA:!MD5:!aNULL

This will be fine until Chrome (and other browsers) support AES256-GCM-SHA384. If you don’t mind a longer string and would like to future-proof now, you can change your string to:

SSLCipherSuite ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDH+HIGH:EDH+HIGH:-3DES:DES-CBC3-SHA:!CAMELLIA:!MD5:!aNULL

Make sure you have SSLHonorCipherOrder set to on:

SSLHonorCipherOrder on

References:
[1] http://security.stackexchange.com/a/83891