HTTP Public Key Pinning (HPKP) is one of a few recent developments in website security that are focused around SSL encryption. It is designed to help protect against man-in-the-middle attacks.
When added to a website, an SSL certificate has two main purposes:
- It encrypts all of the data sent between the user and the server, which is important if you are collecting any sort of personal information
- It verifies to the user that the website you are visiting is authentic, i.e. not a spoof or fake website that is trying to steal your data
When you order an SSL certificate from a Certificate Authority (CA) such as Thawte or Globalsign, depending on the grade of certificate that you purchase, they will put a certain amount of effort into verifying your identity. They will then issue an SSL certificate to you that contains their stamp of approval. When you add this to your website, your browser will effectively look at the SSL certificate, recognise the CA, and give the user a nice green padlock to indicate that the site they are visiting is kosher.
This does present a potential security threat however, in that if a CA gets hacked and someone starts issuing loads of fake certificates under their name, all of the browsers in the world will implicitly trust these certificates, giving the hacker effectively free reign to do all sorts of terrible things to the poor web users of this world. Believe it or not, this has happened on more than one occasion!
HPKP is designed to tackle this security risk.
At a high level, what it does is store a list of the hashes of all valid SSL certificates for a particular site in the user's web browser.
If the user then visits a version of your site that is using a certificate that isn't in the list you get a security error and your browser will prevent you from going any further.
To set it up, you need to include an HPKP HTTP header in your response, which includes the hash of a number of SSL certificates.
Public-Key-Pins: pin-sha256="JFB3oerc+5OiOMo7DM3yPlRAgLiLbVfU+siRbVUqJmo=";pin-sha256="klO13nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=";pin-sha256="grX4Ta4HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=";max-age=7884000
We'll decode this header later on in the article for you.
The browser will then look at this response header and, if it is valid, it will be stored by the browser for a certain amount of time (the duration of which is also defined in the header), at which point it will expire and will be updated the next time the user visits the website.
Once the browser has stored this list of valid SSL certificates, you will not be able to change it until the header expires. This means that if you swap out your SSL certificate and your new certificate isn't covered by your old HPKP header, any users who have visited the site previously will start seeing an SSL error and will be denied access until the HPKP header that is cached in their browser expires.
This makes the whole process of implementing HPKP very risky, so you need to make sure you do it right. If you do it wrong, you could very easily end up bricking your entire website.
At this point it's worth mentioning that you can pin any certificate in your chain with HPKP, all the way up to your CA's root certificate. As such, if you like you can include the intermediate or root certificate of your CA rather than your own certificate. This reduces the risk of bricking your site as it means you can very quickly get a new SSL certificate from your CA, without having to update your HPKP header. However it also increases the attack surface of the site, as if your CA gets compromised, your HPKP header will automatically validate any certificates issued by that CA, until the issue is picked up by the CA and they revoke the root certificate.
As additional protection against bricking your site, the standard requires you to implement at least one backup pins (or hashes). These are basically hashes of certificates that do not appear in your certificate chain, the idea being that you can include the public key hash of another two or more key pairs as fallback options in case you need to replace your current certificate. One of these key pairs can then be used to generated a new SSL certificate that will pass your HPKP header check.
You can also use one of these backup pins to include the hash of an intermediate or root certificate from an alternative CA, giving you the option to buy a new certificate from an alternative CA if your current CA gets compromised.
Cache Time
A big factor in HPKP's ability to brick a site is the length of time that HPKP headers are cached in users' browsers.
If you define a cache time of 6 months and you don't have any useful backup pins, when you come to replace your SSL certificate you will quickly realise that you will be in quite a difficult predicament; any user who had visited your site in the last 6 months will be presented with a security warning and will be prevented from accessing your site for anything up to a further 6 months. Scary!
As such, you need to be careful when setting the max-age attribute in your HPKP header. We would always recommend starting with a very low value; around 60 seconds. This gives you enough time to test your configuration without destroying your site.
In this scenario, if you deploy a dud HPKP header, only users who visited your site in the last minute would see an error, and even then only for a few seconds.
Once you are happy that everything is stable, you can gradually increase the max-age over a period of a week or two. The security industry generally recommend a value of 6 months, however you may want to keep it to 3 months, just for that extra bit of flexibility. It's also worth mentioning that some browsers (including Chrome) will only cache an HPKP record for a maximum of 60 days.
What does a header look like?
OK, so back to decoding the example header that I included earlier in the article. Here is the header again:
Public-Key-Pins: pin-sha256="JFB3oerc+5OiOMo7DM3yPlRAgLiLbVfU+siRbVUqJmo=";pin-sha256="klO13nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=";pin-sha256="grX4Ta4HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME=";max-age=7884000
So let's break this down into it's constituent parts:
Public-Key-Pins: #this is the header name definition
pin-sha256="JFB3oerc+5OiOMo7DM3yPlRAgLiLbVfU+siRbVUqJmo="; #this is the SHA256 hash of the current SSL certificate on the site
pin-sha256="klO13nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY="; #this is the SHA256 hash of the first backup CSR
pin-sha256="grX4Ta4HpZx6tSHkmCrvpApTQGo67CYDnvprLg5yRME="; #this is the SHA256 hash of the second backup CSR
max-age=7884000 #this the cache expiry time in seconds
Implementing HPKP
Bearing all of this in mind, there are a few different approaches to implementing HPKP, I'm going to focus on three approaches here:
Approach 1 - Current certificate + two or more backup CSRs
This is probably the most secure approach to implementing HPKP, however in this instance you need to be absolutely sure that the private keys associated with your backup CSRs are stored securely somewhere, as when your certificate needs to be renewed or replaced you'll need this private key to install your new SSL certificate on the server.
You need to be very careful about how / where you store your private keys. Ideally copy each key onto a separate, encrypted USB drive, then keep these drives under lock and key in separate physical locations.
To build up an HPKP header in this way, you first need to create a hash of your current certificate. I would recommend doing this using one of the very useful tools provided by report-uri.io - https://report-uri.io/home/tools.
Take the pem formatted version of your certificate and load it into this tool: https://report-uri.io/home/pubkey_hash. Then make a note of this hash.
From there, use openssl to generate a couple of 2048 bit private keys on your local machine:
openssl genrsa -out yourdomain.com.1.key 2048
openssl genrsa -out yourdomain.com.2.key 2048
Now you can use openssl again to generate some CSRs from these private keys:
openssl req -new -sha256 -key yourdomain.com.1.key -out yourdomain.com.1.csr
openssl req -new -sha256 -key yourdomain.com.2.key -out yourdomain.com.2.csr
openssl will guide you through the process, asking you for a number of values for country, common name etc.
Now you can load the contents of these new CSRs into the hash tool that I provided before in order to create your two backup pins.
Approach 2 - Current certificate + CA root certificate + Alternative CA root certificate
This solution is a bit less onerous, in that you don't have to mess about creating backup CSRs and storing private keys. As such, it also reduces the risk of you bricking your site.
All you need to do is store the hash of the root certificate for your current CA, plus the hash of a root certificate from an alternative CA.
When you do need to replace your certificate, you just need to make sure you source your certificate from one of the CAs in your HPKP header.
The drawback of this approach is that increases your attack surface area. If one of the two CA's in your HPKP get compromised and a hacker obtains a fake certificate for your website, this fake certificate will validate against your HPKP header.
However, this approach does still provide better security than not having an HPKP header at all, because by introducing an HPKP header in this context, you are limiting your exposure to only the CA's listed in the header.
Also, if a CA does get compromised, they will revoke their intermediate and root certificates as soon as they realise, invalidating both your certificate chain and that particular hash in your HPKP key.
report-uri.io actually provide a tool that will generate hashes for your entire certificate chain, which is useful in this respect. You can find this tool here: https://report-uri.io/home/pkp_hash.
Enter your website into this tool to generate the hashes for your current certificate, the associated intermediate and any other CA root certificates. Ignore the CA intermediate certificates and make a note of the hash of your current certificate and root certificate hashes.
Now you need to include the hash of a root certificate from an alternative CA. To do this, find the root certificate on your chosen CA's website. As an example, Comodo's certificates can be found here - https://support.comodo.com/index.php?/Knowledgebase/List/Index/71.
Then take the PEM version of this certificate and use the report-uri.io hash tool (https://report-uri.io/home/pubkey_hash) to generate a hash for this cert.
Approach 3 - Current certificate + CA root certificate + Alternative CA root certificate + backup CSRs
This solution is basically a combination of the first two options. It offers the flexibility of using two different CAs, combined with some additional backup CSRs just in case.
With this scenario, even if both of your CAs became compromised, you would still have a backup CSR to fall back on, allowing you to source a certificate from a third CA.
To build up a header for this approach, simply generate the various hashes as described in approach 1 and 2, then build them together into one header.
Recommended Approach
All of these approaches offer a decent way of implementing HPKP. The approach that you choose will ultimately depend on the sensitivity of the site that you are looking to protect and how confident you are that you will be keep your backup CSRs and private keys safe. If you are able to keep your backups safe then Approach 1 is the most secure, however this increases the risk of bricking your site if for some reason your backups aren't available.
Here at Indulge we tend to roll with approach 3. It offers a decent level of security with a couple of get out clauses in case we need them, which is important in order for us to sleep at night!
Renewing or Replacing A Certificate
When the time comes to replace your certificate, what is the best way to approach this?
If you have backup CSRs available in your HPKP header, then you just need to submit one of these CSRs to your chosen CA and get them to generate a certificate for you.
When you install this certificate on your site, the hash of this certificate will already be included in the HPKP header and so your site will continue to function.
You should then generate a new backup CSR and update your HPKP record, so that you always have at least 2 backup CSRs available.
If you have hashes of your CA's root certificates in your HPKP header, then you should review and update these if necessary (for example if you have moved to a new CA).
In an emergency situation, if don't have any backup CSRs and your root certificate hashes are invalid (for example if you have changed CA), you can generate a new CSR from the private key of your current certificate. This will produce a new certificate with a hash that matches your old one. If your private key has been compromised though and you need to revoke your old certificate, you could be in big trouble! This is why backup CSRs are so important.
Disclaimer
If you do go ahead and implement HPKP, it is your responsibility to ensure that it is implemented correctly. Neither Indulge or myself will be held responsible for you bricking your site with an incorrect implementation of HPKP.
More Info
Some useful resources on HPKP:
- Scott Helm's fantastic article on the subject: https://scotthelme.co.uk/guidance-on-setting-up-hpkp/
- Report URI tools for managing HPKP: https://report-uri.io/home/tools
- Wikipedia article on HPKP: https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning
- A story of a company that bricked their site with HPKP: https://www.smashingmagazine.com/be-afraid-of-public-key-pinning/