At long last, the Internet Engineering Task Force (IETF) has published RFC 7469, Public Key Pinning Extension for HTTP (HPKP). Thanks to my colleagues Ryan Sleevi, Adam Langley, and Chris Evans for coming up with the idea; and thanks to Ryan and Chris E. for helping me write the many drafts that preceded the final RFC. Thanks also to the many IETF participants who commented on the drafts and helped shepherd the document through to RFC status.
HPKP is an attempt to solve 1 of the big problems in the Web PKI: the fact that essentially any certification authority (CA) or intermediate issuer can issue end-entity (EE, or “leaf”) certificates for essentially any web site. For example, even though the certificate for mail.google.com is issued by “Google Internet Authority G2”, which in turn is issued by the root CA “GeoTrust Global CA”, an obscure Dutch CA can also try to issue certificates for mail.google.com. So, we’d really like some way to stop clients from having to trust such misissued certificates.
Often, people propose to solve this problem by partitioning the web: either they would like to configure their clients to trust only CAs from their own nation; or they would like for CAs of nation X to be banned from issuing certificates for organizations from nations Y and Z; or both. There are a couple problems with this. Crucially, the Web is World-Wide by nature and its many great benefits flow directly from that. Additionally, it is not always clear what nation a given organization is really ‘from’, and hence it is not always clear what CA ‘should’ have issued the organization’s certificates.
There can be no perfect set of ‘golden roots’ — you cannot construct a minimal set of issuing certificates whose operators are more certain than some other set not to mis-issue (whether on purpose or by accident). If you partition the web, you reduce its value without actually reducing the threat of mis-issuance. So we need something else.
HPKP is 1 way to do that. HPKP enables a web server to tell clients (like browsers) to expect the server to always present, in its X.509 certificate chain, at least 1 of a set of public keys; and otherwise to to reject the certificate chain. Thus, a web site operator can effectively reduce the set of issuers that can issue for their site, without partitioning the web.
To understand key pinning, first consider the classic simple case: SSH host key management. When you first connect to an SSH server with a client that has no previous knowledge of the server, you see this:
chris@goatbeast:~ $ ssh freebsd The authenticity of host 'freebsd (10.0.0.4)' can't be established. ECDSA key fingerprint is b0:79:74:0f:58:20:80:fd:c7:47:33:d6:9c:40:df:20. Are you sure you want to continue connecting (yes/no)?
My server, freebsd, is presenting its public key to the SSH client to prove its identity. The problem is, my client has no knowledge of that (server-name, public-key) pair. So it asks me to resolve the confusion. I am supposed to perform some out-of-band check that the key fingerprint is correct, and say Yes or No.
Assuming I say Yes, my client will henceforth expect this server to present that key and only that key. If my server ever presents a different key — whether due to legitimate key rotation or an actual network attack — my client will refuse to connect, and print a message like this:
No ECDSA host key is known for freebsd and you have requested strict checking. Host key verification failed.
The reason this works for SSH is that almost everyone who uses SSH is an expert user: a systems administrator, devops engineer, or software engineer. They understand the error message, know what to do in case of key verification failure, and can act on it. The community of people who use any given server is small. They can simply talk to each other: “Hey, did you rotate the keys for the server?”
But on the world-wide web, that won’t fly. Key rotation is common, we need a friction-free introduction for that first connection, and the people using browsers have no special knowledge of cryptographic authentication. Therefore, we must still rely on CAs to provide the introductions, and we still use chains of certificates to give us flexible continuity for our web servers’ cryptographic identities. And rather than pinning a single end-entity key, as in SSH, we can pin a set of keys — potentially at several places in the certificate chain. As we’ll see below, this can greatly increase reliability.
HPKP Pin Validation is essentially set intersection: given the set of public keys in the signed certificate chain, are any of them the same as any of the keys the server has asserted (“pinned”) as known-good? If so, Pin Validation succeeds; if not, the client should behave like an SSH client: drop the invalid connection. In Chrome, that looks like this:
WARNING: Public key pinning for web sites can be very dangerous. If you make a mistake, you might cause clients to pin a set of keys that validates today but which stops validating a week or a year from now, if something changes. In that case, you’ll end up denying service to your own site! People won’t be able to connect. (We call this “bricking your site”.) Unless you are very confident that you understand the Web PKI, and unless you are very confident that you can manage your site’s cryptographic identity very well, you should not use key pinning. Stick to regular, un-pinned Web PKI until you get more confident.
There are several steps you have to take to pin 1 or more of the public keys in your site’s certificate chain(s):
In the following sections I’ll describe how to do each step.
As we saw in the Certificate Viewer screenshot, a site’s certificate is at the end of a chain of (usually) at least 3 certificates: the root certificate or trust anchor, 1 or more intermediate issuer certificates, and finally the end-entity certificate. Typically, the web server must serve as part of its TLS handshake all of these certificates except the root or trust anchor — the client maintains a set of trust anchors and finds 1 that signed the top-most intermediate. In certain cases, a server can serve only its EE and the clients will discover the intermediate issuers, but this often leads to trouble. Generally, expect to have to serve a chain containing the intermediate issuer(s).
However, be aware that the chain you serve is not necessarily the chain that clients will (re)build when validating the chain! This is due to cross-signing, and the generally surprisingly complicated way in which clients build and validate certificate chains. You can partially control this by ensuring that you serve good chains with well-known intermediate issuers that chain up to a single well-known trust anchor. Even so, you must test with a variety of clients to make sure you know what chains clients will really build and validate.
Crucially, clients perform Pin Validation on the chain they build during chain validation, which is not necessarily the same as the chain you serve. So, unfortunately, you can’t always simply pin the keys in the chain you serve and be certain that Pin Validation will succeed. (Although see the next section for ways to get better coverage.)
Some sites use a distinct EE certificate for each distinct server in a cluster. Perhaps each EE is issued by the same issuers, but perhaps not. If not, your situation is likely very complex and key pinning might not work for you. (Or, it may only work with a very large pin set.)
Now that you have a grip on what certificate chain(s) clients will build and validate, it’s time to decide where in that chain to pin. For the sake of discussion, I’ll assume a simple server deployment model:
Thus, we have 2 certificate chains in production: CA → intermediary → EE1, and CA → intermediary → EE2. The servers in the 2 clusters are configured, correctly, to serve the chains intermediary → EE1 (for data center 1) and intermediary → EE2 (for DC 2).
Let’s further assume for simplicity that clients do indeed build a path through the intermediary to the same CA certificate as we expect. (Again, in reality, you cannot simply assume this.)
We can choose to pin the keys of any of the 4 certificates: CA, intermediary, EE1, and EE2. The implications of pinning at different levels vary:
By pinning at multiple levels in the certificate chain — e.g. the EEs and the intermediaries, the EEs and the root, the intermediaries and the root, or at all 3 — the site operator can trade off trusting more issuers with greater ease of avoiding bricking the site.
The RFC mandates that hosts MUST provide a backup pin: A pin that is not present in the chain that the client validates. This is for your own good: if you lose control of your private keys and need to re-key your site and get new certificates, you don’t want your site to have any down time — and certainly not to be bricked! Unless clients have already pinned your backup key, your site would be bricked until the max-age timed out.
In this example, I’ll use a backup EE certificate as a backup pin. (You could, and likely should, also use an alternate intermediary or root issuer certificate for your backup. Additionally, it is best to get your backup signed by a valid issuer, before disaster strikes, so that you really can put it into production at a moment’s notice!)
This script generates a new key and an associated certificate signing request (CSR; which is what you would send to a CA for them to sign). This is a way to generate a primary and/or backup EE key and CSR for your site. Again, the safest thing to do is to actually get your backup key in a valid certificate issued by a real issuer, so that you could put it into production immediately if necessary.
#!/bin/sh openssl genrsa -out "$1".key 2048 openssl req -new -key "$1".key -out "$1".csr
This script makes a key pin: it reads in either an X.509 certificate (in PEM format) or a certificate signing request (also in PEM format), extracts its subject public key info (SPKI) section, hashes the SPKI with SHA-256, and then base 64-encodes that:
#!/bin/sh type="x509" case "$1" in x509) type="x509" ;; req) type="req" ;; *) echo "Usage: $0 x509 certificate-pathname" echo " $0 req certificate-signing-request-pathname" exit 1 esac openssl $type -noout -in "$2" -pubkey | \ openssl asn1parse -noout -inform pem -out public.key openssl dgst -sha256 -binary public.key | openssl enc -base64
The output of this script is what you will put in your PKP headers. For example, this is an example Apache header directive that I am currently using for nonfreesoftware.org (lines folded to fit):
Header add Public-Key-Pins "max-age=500; includeSubDomains; pin-sha256=\"wBVXRiGdJMKG7vQhr9tZ9br9Md4l7cO69LF2a88Au/o=\"; pin-sha256=\"fv1+PWVvrBGKldX8uRtODY3sDbBKlsJOa48mI9s+6Mk=\"; pin-sha256=\"lT09gPUeQfbYrlxRtpsHrjDblj9Rpz+u7ajfCrg4qDM=\""
I’ve pinned my end-entity, an issuer, and a backup key. I’ve set the max-age for 500 seconds, so that I can’t brick the site for very long. And, of course, I’ve pinned only an alternate name for the site, not the canonical name (which is noncombatant.org).
Finally, check to make sure that your client has read and understood the key pins. In this screenshot, you can see that Chrome has recognized my Public-Key-Pins header: