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.

Weaknesses

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.

Deployment

Caution

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.

Policy Configuration

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:

pin-sha="..."

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.

Key Generation

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.

DST Root CA X3 root certificate is sourced from here and the Let's Encrypt certificate keys are available here.

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

max-age=5184000

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.

includeSubDomains

This optional directive is self-descriptive and is used to apply the HPKP policy to all subdomains.

report-uri

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.

Testing

Using Firefox, one can verify that HPKP is enabled using the network tool bundled with 'Web developer' Tools.

Alternatively online tools by report-uri.io found here or a complete report using Qualys SSL Server Test can be used to test a domain.

Conclusion

At the date of posting, this website has successfully deployed HTTP Public Key Pinning.

Update

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.

References