HPKP as defined in RFC7469, 'tells a web client to associate a specific cryptographic public key with a certain web server'.
Used in combination with HTTP Strict Transport Security, HPKP can be a strong defense against man in the middle (MITM) attacks by enforcing certificate chain validation.
In this event, the threat agent is a rogue or a compromised certificate authority such as Diginotar's fraudulent issuance of google.com certificates.
HPKP relies in trust on first use. On initial connection, the web client cannot validate the certificate chain . This means Eve can potentially intercept the initial connection and perform an MITM.
Major websites (such as Google and Facebook) have pre-shared keys hardcoded into Chrome and Firefox as a mitigation. However this is not a practical solution for the rest of the Internet. In practice, future connection requests to the valid web host should then return an error to the client.
As of August 2016, browser support for HPKP is limited to Firefox, Chromium and Opera.
If incorrectly configured, HPKP can cause a website to become inaccessible. For testing the Public-Key-Pins-Report-Only directive can be used or alternatively the website owner can set a low value for max-age.
The standards document requires at least two pins to enforce HPKP. It is at the website owner's discretion on which pins are appropriate for deployment.
For this exercise I elected to rely on Github's HPKP policy. The reasoning here is that Github is one of the largest websites that supports HPKP and their setup seems robust enough for production use.
The directives as used in the above policy are described here:
This is a base64 string that corresponds to the pin for a public key. I intend to pin two root keys, one intermediate key and two offline backup keys.
For the root keys, I selected (Identrust) DST Root CA X3 root certificate and the newer ISRG Root X1 by Let's Encrypt and for the intermediate certificate I chose Let's Encrypt Authority X3.
Using the retrieved information, the next step is to generate the base64 encoded hashes as shown below:
$ openssl x509 -in certificate.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64 $ curl https://letsencrypt.org/certs/isrgrootx1.pem | openssl x509 -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64 $ curl https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem | openssl x509 -pubkey | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
The two offline keys are generated and stored offline as backup keys in the event the Let's Encrypt CA becomes unavailable.
Generation of the private key and the required base64 hash is shown below.
Replace foo.com with a name of your choice.
$ openssl req -nodes -sha256 -newkey rsa:4096 -keyout "foo.com.rsa.key" -out "foo.com.rsa.csr" $ openssl req -pubkey < foo.com.csr | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
This value in seconds is equivalent to two months. A shorter duration offers less protection to users while a higher duration presents a risk where pin validation might fail.
- For testing remember to use a smaller value such as 60. A mistake could potentially break access to a domain.
This optional directive is self-descriptive and is used to apply the HPKP policy to all subdomains.
This is an optional directive through which pin validation failures are to be reported. The value should obviously be to a different domain.
A real time reporting tool is included with report-uri.io for reporting pin validation errors.
Using the directives described above, the result is should be similar to the snippet below.
Header set Public-Key-Pins "pin-sha256=\"Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys="; pin-sha256=\"YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=\"; pin-sha256=\"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=\"; pin-sha256=\"BnCc92LNTmq+v0mGoN13pbuR/d8WcOBYtZCCMBKwrmo=\"; pin-sha256=\"MBCWEpJFZIJKp3Tjy4WTIL2AcL91yV4Zo2PfsHyiaVQ=\"; report-uri=\"https://55343e1861289e8df8aa157da3c58796.report-uri.io/r/default/hpkp/enforce\"; max-age=5184000; includeSubdomains;"
In Apache, the snippet is to be added to virtual hosts located by default in /etc/httpd/conf.d/ssl.conf. Restarting the web server should now enforce public key pinning.
Using Firefox, one can verify that HPKP is enabled using the network tool bundled with 'Web developer' Tools.
At the date of posting, this website has successfully deployed HTTP Public Key Pinning.
Chrome has announced their intent to depreciate HPKP in 2018. With this in mind, public key pinning is effectively disabled on this site by setting the max-age field to 0.
- report-uri.io - A great utility that offers tools to configure and deploy a HPKP policy
- Mozilla Developer Network article on Public Key Pinning
- Github's HPKP policy
- HTTP Public Key Pinning Extension HPKP for Apache, NGINX and Lighttpd
- Scott Helme's post on HPKP
- Activating HTTP Public Key Pinning (HPKP) on Let's Encrypt