cat posts/2015-09-22-the-tls-handshake-visually.md
The TLS handshake, visually
I explain the TLS handshake to people more often than almost anything else, usually badly, on a whiteboard. Most developers who configure certificates have no picture of what the certificate is for, and for years I didn’t either. This week seemed like a good time to clean up the whiteboard version, because Let’s Encrypt issued its first certificate on the 14th, for helloworld.letsencrypt.org. It isn’t trusted by browsers yet. The cross-signature from IdenTrust that would fix that isn’t in place, so if you visit that page today you’ll most likely get a warning. General availability is planned for the week of November 16th, if the schedule holds. Still, a free, automated CA backed by Mozilla, the EFF and others is the most hopeful thing to happen to HTTPS in years, and I’ve already got a list of side projects I’m going to move over.
I’m going to describe TLS 1.2 with an ECDHE key exchange and an RSA certificate, which is what you get from most well-configured servers today. If you want the math underneath, I went through Diffie-Hellman and RSA with small numbers in an older post. Here I’ll treat them as black boxes.
The messages
ClientHello. The client says which TLS versions and cipher suites it supports, sends 32 bytes of fresh randomness (the client random), and lists some extensions. The important ones are SNI, which names the host it wants so one IP address can serve many certificates, and the elliptic curves it supports. All of this is in the clear. Anyone on the network can see which site you asked for, even though they won’t see the page.
ServerHello. The server picks a version and a cipher suite from the client’s list, say ECDHE-RSA-AES128-GCM-SHA256, and sends its own 32 random bytes.
Certificate. The server’s certificate, plus the intermediates needed to chain it back to a root the browser trusts. The certificate says, in effect, “the holder of the private key matching this public key is entitled to call itself example.com”, signed by a CA.
ServerKeyExchange. This is the part most people don’t know is there. The server generates a brand-new elliptic curve key pair just for this connection, and sends the public half. Then it signs that public key, along with both random values, using the private key from its certificate. This signature is the whole reason the certificate exists. It proves that the ephemeral key came from someone who holds example.com’s private key, and the randoms prove it was made for this connection and isn’t a replay.
ClientKeyExchange. The client checks the certificate chain, the hostname, the dates, and the signature on the server’s ephemeral key. If all that passes, it generates its own ephemeral key pair and sends the public half. Now both sides can do the Diffie-Hellman computation and arrive at the same shared secret (the “pre-master secret”), which never went over the wire.
ChangeCipherSpec and Finished. Each side says “from now on I’m encrypting”, then sends a Finished message, which is a MAC over a hash of every handshake message so far. If an attacker had tampered with anything, say removed the strong cipher suites from the ClientHello, the two sides will have hashed different transcripts and the Finished check fails.
From shared secret to session keys
The pre-master secret isn’t used directly. Both sides feed it, along with the two random values, through TLS’s pseudo-random function (an HMAC-based construction, SHA-256 for this cipher suite):
The output is a 48-byte master secret, and then a second pass expands it into a “key block” that gets sliced up into the keys each side uses. Each direction has its own key, so the client’s traffic and the server’s are encrypted separately. With AES-GCM there’s no separate MAC key, because GCM does authentication itself, and the “IVs” are a few bytes of fixed nonce material that get combined with a counter for each record.
When the connection closes, both sides throw away the ephemeral keys. That’s forward secrecy. Even someone who records the traffic and later steals the server’s certificate key can’t decrypt it, because the certificate key only ever signed things. It was never used to encrypt the secret. In the older RSA key exchange, which plenty of servers still prefer, the client encrypts the pre-master secret with the server’s RSA public key, and then the certificate key really does unlock everything, retroactively. This is the change I nagged everyone about after PRISM.
Where it goes wrong
Each step above is a place where real attacks have happened, and this year has had some good ones.
FREAK in March and Logjam in May were both downgrade attacks on the ServerKeyExchange step. Logjam is the neat one. In the older finite-field Diffie-Hellman suites, the server’s signature covers its DH parameters but not the cipher suite it chose, so an attacker in the middle could ask a server that still supported the 1990s “export” suites for 512-bit DH, pass that along to a client expecting normal DH, and then break the 512-bit discrete log fast enough to forge the Finished messages. The fix was mostly for servers to stop supporting export crypto, which they should have done a decade ago, and for browsers to refuse small DH groups.
Certificate validation is the other big one, and it’s mostly a client problem. The handshake only protects you if the client actually checks that the chain leads to a root it trusts, for the right hostname. Every root in your trust store is allowed to vouch for every site. That’s why Superfish was so bad: it added its own root to our laptops, and after that it could sign anything. And there’s a long history of mobile apps and libraries that just turn validation off because it was failing in testing. If you’ve ever written ServicePointManager.ServerCertificateValidationCallback = delegate { return true; }; in .NET to “fix” a dev environment, I have too, and I hope we both took it out before shipping.
Revocation is the weak spot that nobody has really fixed. If a certificate’s key is stolen, the CA can revoke it, but browsers mostly check revocation with a soft fail. If the OCSP check times out, they let you through anyway, and an attacker who can block your traffic can usually block the OCSP check too. Short-lived certificates are one answer, which is another reason the automation in Let’s Encrypt matters. If renewing is a cron job, you can renew often.
Two round trips
The last thing worth knowing is the cost. A full handshake is two round trips before the first byte of the request, on top of TCP’s own handshake. On a 100ms mobile connection that’s 300ms before anything happens. Session resumption (session IDs or session tickets) lets a returning client skip the certificate and key exchange and do it in one round trip, and False Start in Chrome and Firefox lets the client send data right after its Finished without waiting for the server’s. TLS 1.3, which is still being drafted at the IETF, is aiming for one round trip for a full handshake and is cutting a lot of the old options, including the RSA key exchange. I’ll believe it when I see it deployed, but it looks good on paper.
If you want to see all of this on a real connection, openssl s_client -connect example.com:443 -servername example.com -msg prints each handshake message as it goes past, and SSL Labs will tell you which of the bad options your own server still allows. I’ll be checking ours again once our certificates come from Let’s Encrypt.