HTTP Public Key Pinning

HTTPS sites are encrypted with a public/private key pair, being vouched for in a certificate by having that certificate signed by a trusted CA. However, it might be possible that somebody generates a certificate for your domain and has it signed by a fraudulent or compromised CA. Protection against this is provided by use of HKPK. But what keys should be "pinned" and why? I've found information about this online to be lacking, so I did some research and here's the results.

What the header looks like

The HPKP header is called Public-Key-Pins and lists base64 encoded hashes of the allowable public keys (pin-sha256), a time window (in seconds) during which the public keys are pinned (max-age), an optional flag to specify whether or not subdomains should be included in the pinning process (includeSubdomains) and an optional URL that accepts problem reports when a visiting browser detects a violation of the pins (report-uri). An example header would be:

Public-Key-Pins: pin-sha256="W4699GoENqakvlu1BsXHtgpHPGWLa+W3qAbv9Xr2aos="; pin-sha256="scQZPmZJuJTIThrWyW6hnYPHU3W+wUj18pb+1ZV86h4="; max-age=5184000

In this example, two keys are pinned for 60 days from the moment the header is presented to the visitor's browser.

At least 2 keys

Since HPKP marks only certain keys as trusted, any other keys are effectively blocked, even if they are valid. However, there might be a valid reason to replace a certificate, such as a private key being compromised or a change in some metadata contained in the certificate. If you had only pinned your current certificate trust chain, that means replacing the key would be impossible until the pinning window has expired for all users of the site, which could be months or years. For that reason, you must always pin at least two keys, one of which represents the trust chain you are currently using, the other of which represents a backup trust chain. This is actually mandated in the standard (and for very good reason) and can even be gleaned from the fact that the Public-Key-Pins header name is plural. Note that you can also have more than two keys pinned, which can be useful in certain circumstances, but you should always have a backup trust chain. That backup should, and this is very important, currently not be used to serve any content. Its sole purpose is to be there to provide access to your site in case your primary trust chain can no longer be used.

CRT or CSR? What happens when I renew?

Public key hashes can be generated from certificates or from the corresponding CSR. Since both contain the same public key, they will result in the same hash. The OpenSSL commands to retrieve the hashes are slightly different depending on whether you use a certificate or the signing request as input. To obtain the hash from a CSR, use the command:

openssl req -pubkey -in example.csr | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

This performs the following operations in order: 1) parse the CSR and extract the public key as text, 2) convert the public key to binary format, 3) calculates the SHA-256 hash of the binary public key, 4) encodes that has in base64

To obtain the hash from an existing certificate, replace the "req" command with "x509" so the command line becomes:

openssl x509 -pubkey -in example.crt | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

If you try these out, you'll see that the hashes obtained from the certificate and for the signing requests are indeed the same. This also means that, as long as you keep using the same CSR when renewing your certificates, the hash will not change. It depends only on the public key.

Leaf, intermediate or root certificate?

HPKP doesn't necessarily just pin a single certificate. You could pin your own active public key, which means your own (leaf) certificate is trusted, as well as any other certificates that are signed from the same CSR, regardless of which CA has signed them. This is basically what you do with your backup CSR. It is also the most secure option, as it means that it (and your backup key) are the only keys that can possibly be used for accessing your site. Aside from the best security, it also makes it possible to switch to another CA without having to update your key pins.

Pinning a certain public key implies trust in the entire chain. Pinning a public key means that any certificates signed by that key's corresponding private key are also trusted.

So, instead of pinning your own certificate, you could also trust your CA's intermediate certificate which they used to sign your request. This means you could use any CSR, as long as it is signed by the same CA, which can be quite useful. Unfortunately, it is also (slightly) less secure, as it would enable any other customer of that same CA to impersonate your site, but only if they manage to trick your CA into signing their fraudulent certificate. That's the very problem HPKP was designed to prevent, yet at a smaller scale (i.e. it is limited to a specific single CA that has to be compromised, rather than there being a problem when any CA is compromised).

On the same note, you could trust the CA's root certificate (which they used to sign their own intermediate certificate, which in turn was used to sign your request). The slight advantage of this is that you would be able to switch to another class of certificate as long as it's with the same CA. For instance, you could go from having a DV to an EV certificate, which you might also do by simply pinning your leaf key. The downside is that, as with pinning an intermediate certificate, you would trust other customers of the CA. In fact, you wouldn't just trust the CA's other customers in the same class, but you would effectively trust all of them. Hence, pinning your CA's root certificate has no significant advantages, but bigger disadvantages and in short cannot be recommended.

My personal preference is to pin only the leaf key and the backup key (which is also a leaf key), maybe adding in the CA intermediate key in advance when I foresee any changes.

Other options

I haven't yet described the other two elements that may optionally be present in the Public-Key-Pins header: includeSubdomains and report-uri.

includeSubdomains

As is obvious from the name, if the includeSubdomains flag is present, the specified keys are pinned not just for the current hostname, but for any other subdomains in your domain as well. This can be a good idea in many cases. If you use several additional keys (possibly self-signed certificates) for example, with a webmail service or an admin panel such as phpMyAdmin, you would have to pin those as well. In that case, I would simply recommend ommitting the includeSubdomains flag.

report-uri

Some browsers (presently only browsers based on Chromium 46 or later) allow you to specify an URL where they can report irregularities. If somebody visits your site using a browser that supports HPKP and is presented with a public key that doesn't match one of the pinned keys, that is an indication of tampering. If the browser detects this and also supports reporting, it will submit a report in JSON format to whatever endpoint you have specified. A report would look like:

{
	"date-time": date-time,
	"hostname": hostname,
	"port": port,
	"effective-expiration-date": expiration-date,
	"include-subdomains": include-subdomains,
	"noted-hostname": noted-hostname,
	"served-certificate-chain": [
		pem1, ... pemN
	],
	"validated-certificate-chain": [
		pem1, ... pemN
	],
	"known-pins": [
		known-pin1, ... known-pinN
	]
}

There is a caveat: the reporting endpoint cannot be on the same site if it is also served using HTTPS. That is because that very site cannot be reached precisely because the public key is not trusted. It would be possible to serve the reporting endpoint over plain old regular unencrypted HTTP as that is not subject to public key pinning. However, that is still not possible if the site has been served with HSTS. So, instead, you may want to put your reporting endpoint on another domain or forego it entirely. You could make use of the free service at report-uri.io.

Browser support

HPKP is supported by Mozilla Firefox since version 35, Google Chrome since version 46, and Opera since version 33 (as it is based on Chromium 46). Internet Explorer, Edge and Safari at the time of writing, do not support public key pinning. For Edge, the feature is under consideration.

Further reference

If you want to read more, check out any of the following:

Comments

No comments, yet...

Post a comment