HTTP Public Key Pinning: How to do it right
[Thanks to Felix aka @nexusnode for inspiring this post. Also, see his blog post [1] for more details]
One of the underutilized security measures I mentioned recently was "HTTP Public Key Pinning", or HPKP. First again, what is HPKP:
HPKP adds a special header to the HTTP response. This header lists hashes of public keys which may be used with a particular site. If an imposter manages to convince a certificate authority to hand her a certificate for your domain name, then the browser can reject the certificate based on the hash it learned from the valid site.
Why is this so important? Did you get rid of SSLv3? How many public breaches can you point to that are due to someone leaving SSLv3 (not v2..) enabled? I am not talking about lab experiments. I am talking about people losing customer data as a result. On the other hand, here are some news reports of unauthorized individuals obtaining certificates from valid certificate authorities [3]. The new "Lets Encrypt" project may make this a bit easier. Anybody able to upload files to your web server may be able to obtain an SSL certificate.
The header looks like (the base 64 encoded hashes are abbreviated to fit them in one line):
Public-Key-Pins: pin-sha256='ABCE...1234='; pin-sha='ECBA...5321=='; max-age=86400;
First of all, you need AT LEAST two hashes. The idea is that you create two key pairs. One of the public keys you send to the certificate authority (CA) as part of a certificate signing request (CSR) to have it signed. The second key pair you keep in a safe place. But you do add hashes for both keys to the pinning header. This way, should the current "live" key get compromised, you can use the backup key, and browsers will already know it is valid.
Browsers will actually ignore the header if they only find one key listed. This is an important measure to prevent self-inflicted DoS conditions. In addition, the HPKP header is only considered if it is received over HTTPS.
To test your pin, all around SSL testing site https://ssllabs.com is helpful as usual. It will calculate the pin for each certificate it finds. Personally, I am using a simple shell script to create the hash from the CSR:
openssl req -in test.csr -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | base64 (based on the one Felix has on his site)
just replace test.csr with your CSR filename. The script extracts the public key, then converts it to "DER" (binary) format, calculates a sha256 digest and finally encodes that digest in BASE64. You can use the certificate as well if you didn't keep the CSR around.
There are also a couple of additional helpful parameters:
- includeSubDomains : This will extend the key pin to any subdomains of yours.
- report-uri: If you would like to be notified whenever a browser runs into a bad certificate, you can ask the browser to post a report to this URI. The report is a JSON snippet that will include details like the certificate that was found and any pins that resulted in its rejection. You can use report-uri.io If you don't want to create your own system to catch the reports.
If you are afraid of false positives, you can also use the "Public-Key-Pins-Report-Only" header. This will result in a report, but the site will not be blocked.
So what should you do:
- Start out with the "Pubic-Key-Pins-Report-Only" header to get comfortable with key pinning
- Create a spare key spare and keep it offline (three copies... three different locations... I like DVDs, but it doesn't hurt to print them just in case)
- Watch your reports. Any false positives?
- After a couple months, pull the switch and remove the "-Report-Only" part.
[1] https://tools.ietf.org/html/rfc7469
[2] https://www.felixrr.pro/archives/425/http-public-key-pinning-hpkp
[3] just search Google news for "certificate authority issued ssl certificate unauthorized" and a few nice stories should come up.
Comments