HTTP Public-Key-Pinning explained

The what, why, and how of RFC 7469

In my last post “Deploying TLS the hard way” I explained how TLS and its extensions (as well as a few HTTP extensions) work and what to watch out for when enabling TLS for your server. One of the HTTP extensions mentioned is HTTP Public-Key-Pinning (HPKP). As a short reminder, the header looks like this:

  max-age=15768000; includeSubDomains

You can see that it specifies two pin-sha256 values, that is the pins of two public keys. One is the pin of any public key in your current certificate chain and the other is the pin of any public key not in your current certificate chain. The latter is a backup in case your certificate expires or has to be revoked.

It is definitely not obvious which public keys you should pin and what a good backup pin would be. Let us answer those questions by starting with a more detailed overview of how public key pinning and TLS certificates work.

How are RSA keys represented?

Let us go back to the beginning and start by taking a closer look at RSA keys:

$ openssl genrsa 2048

The above command generates a 2048 bit RSA key and prints it to the console. Although it says -----BEGIN RSA PRIVATE KEY----- it does not only return the private key but an ASN.1 structure that also contains the public key - we thus actually generated an RSA key pair.

A common misconception when learning about keys and certificates is that the RSA key itself for a given certificate expires. RSA keys however never expire - after all they are just numbers. Only the certificate containing the public key can expire and only the certificate can be revoked. Keys “expire” or are “revoked” as soon as there are no more valid certificates using the public key, and you threw away the keys and stopped using them altogether.

What does the certificate contain?

By submitting the Certificate Signing Request (CSR) containing your public key to a Certificate Authority it will issue a valid certificate. That will again contain the public key of the RSA key pair we generated above and an expiration date. Both the public key and the expiration date will be signed by the CA so that modifications of any of the two would render the certificate invalid immediately.

For simplicity I left out a few other fields that X.509 certificates contain to properly authenticate TLS connections, for example your server’s hostname and other details.

How does public key pinning work?

The whole purpose of public key pinning is to detect when the public key of a certificate for a specific host has changed. That may happen when an attacker compromises a CA such that they are able to issue valid certificates for any domain. A foreign CA might also just be the attacker, think of state-owned CAs that you do not want to be able to MITM your site. Any attacker intercepting a connection from a visitor to your server with a forged certificate can only be prevented by detecting that the public key has changed.

After establishing a TLS session with the server, the browser will look up any stored pins for the given hostname and check whether any of those stored pins match any of the SPKI fingerprints (the output of applying SHA-256 to the public key information) in the certificate chain. The connection must be terminated immediately if pin validation fails.

A valid certificate that passed all basics checks will be accepted if the browser could not find any pins stored for the current hostname. This might happen if the site does not support public key pinning and does not send any HPKP headers at all, or if this is the first time visiting and the server has not seen the HPKP header yet in a previous visit.

What if you need to replace your certificate?

If your certificate expires or an attacker stole the private key you will have to replace (and possibly revoke) the leaf certificate. This might invalidate your pin, the constraints for obtaining a new valid certificate are the same as for an attacker that tries to impersonate you and intercept TLS sessions.

Pin validation requires checking the SPKI fingerprints of all certificates in the chain and will succeed if any of the public keys matches any of the pins. When for example StartSSL signed your certificate you have another intermediate Class 1 or 2 certificate and their root certificate in the chain. The browser trusts only the root certificate but the intermediate ones are signed by the root certificate. The intermediate certificate in turn signs the certificate deployed on your server and that is called a chain of trust.

If you pinned your leaf certificate then the only way to recover is your backup pin - whatever this points to must be included in your new certificate chain if you want to allow users that stored your pin from previous connections back on your server.

An easier solution would be available if you provided the SPKI fingerprint of StartSSL’s Class 1 intermediate certificate. To construct a new valid certificate chain you simply have to ask StartSSL to re-issue a new certificate for a new or your current key. This comes at the price of a slightly bigger attack surface as someone that stole the private key of the CA’s intermediate certificate would be able to impersonate your site and pass key pinning checks.

Another possibility is pinning StartSSL’s root certificate. Any certificate issued by StartSSL would let you construct a new valid certificate chain. Again, this slightly increases the attack vector as any compromised intermediate or root certificate would allow to impersonate your site and pass pinning checks.

What key should I pin?

Given all of the above scenarios you might ask which key would be the best to pin, and the answer is: it depends. You can pin one or all of the public keys in your certificate chain and that will work. The specification requires you to have at least two pins, so you must include the SPKI hash of another CA’s root certificate, another CA’s intermediate certificate (a different tier of your current CA would also work), or another leaf certificate. The only requirement is that this pin is not equal to the hash of any of the certificates in the current chain. The poor browser cannot tell whether you gave it a valid and useful backup pin so it will happily accept random values too.

Pinning to a small set of CAs that you are comfortable with helps you reduce the risk to yourself. Pinning just your leaf certificates is only advised if you are really certain that this is for you. It is a little like driving without a seatbelt and might work most of the time. If something goes wrong it usually goes really wrong and you want to avoid that.

Pinning only your own leaf certs also bears the risk of creating a backup key that adheres to ancient standards and could not be used anymore when you have to replace your current certificate. Assume it was three years ago, and your backup was a 1024-bit RSA key pair. You pin for a year, and your certificate expires. You go to a CA and say “Hey, re-issue my cert for Key A”, and they say “No, your key is too small/weak”. You then say “Ah, but what about my backup key?” - and that also gets rejected because it is too short. In effect, because you only pinned to keys under your control you are now bricked.