Using HTTPS helps preventing someone from snooping your username/password or hijacking your sessions. Using HSTS makes sure the connection stays on HTTPS, even if a MITM tries to redirect you to the plain HTTP version of a web site. But it is easier than you might think for a MITM to use a rogue certificate, making you believe everything is fine. HTTP Public Key Pinning (HPKP) helps the browser check that everything actually is fine.
The Certificate Authority (CA) Model
Certificates are issued by certificate authorities (CAs) who already have their root certificates installed in your OS or browser. All installed root certificates are treated equally with the same amount of trust, and are given ultimate trust – meaning if an installed root certificate signed a certificate, your computer will trust that certificate.
When your browser receives a certificate, or certificate chain, from a web site, it will check if it was signed by an installed root certificate. If it is, then it considers all good.
Do you see the issue here? Who decides which root certificates goes into your OS/browser, and which CAs are trusted? Both the governments in China and USA are represented, BTW.
Once in a while a certificate authority (CA) gets compromised and rogue certificates are issued. Even if you got your certificate from StartCom, computers will be happy with a certificate from Comodo – or the Hong Kong Post Office.
It is also incredibly easy to trick someone into installing a new root certificate. Free WiFi (yay!) is the most efficient, as you already are the MITM.
Pinning is the technique of telling your browser that only certificates with a specific public key somewhere in the certificate chain is to be trusted. Current implementations are based on Trust on First Use (TOFU), meaning your browser will have to trust the connection the first time you use a site. This is because the pinning info is sent via a HTTP header by the web server. In the future this can hopefully be retrieved via DNSSEC signed DNS records.
By sending the HTTP pinning header you are telling the browser to “only trust certificates with this public key somewhere in the chain”.
The browser will store the info it received on its first use and check against that info on later use.
What to pin?
You pin the public key of a certificate. With at least 3 certificates per site (site cert, intermediate cert, root cert) that gives you a few options on what to pin.
Keep in mind that your site cert will be replaced in the near future – within 3 years (used to be 5) – but most likely even before that due to reissues.
You have no control over what intermediate cert the CA will use on a reissue, so don’t pin that.
CAs might have multiple root certs in trust stores, and you simply can’t control which one they will use on reissues.
Rely on what you can control: Your own public key is the only thing in the certificate chain you can control. So that is what you want to pin.
HPKP doesn’t just let you, but requires you, to provide a backup public key. This is useful for when your private key gets compromised. Your backup key will then be used to generate a new certificate. The backup key should of course not ever be stored on the same infrastructure as your primary key and should never be used for anything else.
How to pin?
Technically, we’re not pinning the public key itself, but the Subject Public Key Information (SPKI) fingerprint, and we’re sending it Base64 encoded.
To get the Base64 encoded fingerprint of the SPKI for a private key, issue the following command (replace server.key with your own private key file):
openssl rsa -in server.key -outform der -pubout | openssl dgst -sha256 -binary | openssl enc -base64
The last line of the output is what you want.
You can do this for as many keys as you need, but remember that you need at least the primary key you are using in production right now and a backup key stored somewhere else.
I have the following two fingerprints for my web site:
Primary: mpA8FLefHJeVrMEzwxCaj5Vo+h7EKMufcUYnN2Z66FU= Backup: JSvx7a2q1pjkti0hGhOIPXfru36ORHjyFEawlBPuNxY=
The format of the header we want to send is:
Public-Key-Pins: pin-sha256="PRIMARY_KEY"; pin-sha256="BACKUP_KEY"; max-age=PIN_CACHE_EXPIRE_TIME; includeSubdomains; report-uri="https://example.com/hpkp-report"
max-age tells the browser for how long, in seconds, it should cache the pins. As explained in the RFC, you want this to be long enough to actually have a security impact, but short enough to let you replace the keys within a reasonable timeframe:
There is probably no ideal upper limit to the max-age directive that would satisfy all use cases. However, a value on the order of 60 days (5,184,000 seconds) may be considered a balance between the two competing security concerns.
Just like with HSTS, the
includeSubdomains is optional, and should only be defined if you are using these keys for all subdomains as well. I am not, so I leave it out.
report-uri is also optional and validation failures are reported to this URI.
So, I’m using this in my Nginx config:
add_header Public-Key-Pins 'pin-sha256="mpA8FLefHJeVrMEzwxCaj5Vo+h7EKMufcUYnN2Z66FU="; pin-sha256="JSvx7a2q1pjkti0hGhOIPXfru36ORHjyFEawlBPuNxY="; max-age=5184000';
If I was running Apache, it would look like this:
Header always set Public-Key-Pins "pin-sha256=\"mpA8FLefHJeVrMEzwxCaj5Vo+h7EKMufcUYnN2Z66FU=\"; pin-sha256=\"JSvx7a2q1pjkti0hGhOIPXfru36ORHjyFEawlBPuNxY=\"; max-age=5184000"
At least Chromium based browsers and Firefox have support for HTTP Public Key Pinning, but note the following excerpt from the Chromium Security FAQ:
Chrome does not perform pin validation when the certificate chain chains up to a private trust anchor. A key result of this policy is that private trust anchors can be used to proxy (or MITM) connections, even to pinned sites. “Data loss prevention” appliances, firewalls, content filters, and malware can use this feature to defeat the protections of key pinning.
In other words: If someone tricks you into installing their root certificate (like for free WiFi), Chrome won’t validate the certificate with the pinned info. Use FireFox instead.
I assume more browsers will support HPKP soon. In any case, no browsers will break over this.