Two machines that have never spoken before need to agree on a secret. They're communicating over a network where anyone can read every packet. And they need to pull this off in a few milliseconds, before you notice the page loading slowly. That's the TLS handshake.
Every HTTPS connection starts with one. It's the reason your connections have a latency floor, and it's where most TLS misconfigurations live. I covered what HTTPS actually protects in a previous post. This one is the handshake itself, the message flow I promised would get its own post.
đź§ Three jobs before any data flows
Before a single byte of application data moves, the handshake needs to accomplish three things:
- Agree on a shared symmetric key. Both sides need an identical secret to encrypt everything that follows, and that secret can't have crossed the wire in plaintext.
- Confirm the server's identity. The client needs proof it's talking to the real server and not someone intercepting traffic. The server presents a certificate; the client verifies it. (How certificates actually work, X.509 fields, chains of trust, CAs, is a separate post that's coming.)
- Settle on the cryptography. Which cipher suite, which key exchange group, which signature algorithm. Both sides advertise what they support and negotiate a common set.
Everything else is mechanics to accomplish those three goals.
The TLS 1.3 handshake, message by message
TLS 1.2 needed two full round trips before application data could flow. TLS 1.3 cuts that to one. Here's how.
Client Server
| |
|--- ClientHello ------> |
| (supported versions, cipher suites, |
| key_share with guessed group) |
| |
| <------ ServerHello ---|
| (selected cipher, |
| server key_share) |
| |
| [ENCRYPTED FROM HERE ON] |
| |
| <--- Certificate ------------|
| <--- CertificateVerify ------|
| <--- Finished ---------------|
| |
|--- Finished ----------> |
| |
|============ Application Data ==================|
The ClientHello carries supported TLS versions, a list of cipher suites, and (this is the 1.3 trick) a key_share extension. The client guesses which elliptic curve group the server will pick and pre-sends its ephemeral public key for that group. It's a bet. Usually it's right.
The ServerHello comes back with the server's own key_share. At this point both sides have enough to compute the shared secret. So everything the server sends after ServerHello is already encrypted: its Certificate, a CertificateVerify (proving it holds the private key matching that cert), and a Finished message.
The client checks the certificate, sends its own Finished, and application data starts flowing.
The client's optimistic key_share guess is what collapses the old two round trips into one. But if the guess is wrong, say the client offered X25519 but the server only supports P-256, the server sends a HelloRetryRequest and the client has to resend with the correct group. That costs an extra round trip. Basically back to TLS 1.2 speed. So clients default to the most common group (X25519 today) and almost never guess wrong.
How both sides get the same key without sending it
This is ECDHE: Elliptic Curve Diffie-Hellman Ephemeral. Each side generates a throwaway keypair. They swap public halves in the clear inside their Hello messages. Then each combines its own private half with the other's public half, and both arrive at the same shared secret. The eavesdropper sees both public halves and still can't compute it.
If you want the underlying math, I covered it in my Diffie-Hellman post. The short version: the one-way-ness of elliptic curve multiplication makes it work.
The raw shared secret isn't used directly though. TLS 1.3 feeds it through HKDF (HMAC-based Key Derivation Function) to derive separate keys per direction and per purpose. Handshake traffic gets different keys than application traffic. So even if one derived key leaked, it wouldn't compromise the others.
⚡ Forward secrecy, and why "ephemeral" matters
The E in ECDHE. Ephemeral.
Keys are generated fresh for every connection and thrown away once the session ends. So an attacker who records your encrypted traffic today and then steals the server's long-term private key next year? Still can't decrypt those old sessions. The session keys are gone. Nobody has them anymore.
Contrast that with old-school RSA key transport in TLS 1.2 and earlier. The client generated a random premaster secret, encrypted it with the server's long-term public key, and sent it over. If an attacker later got that private key, they could decrypt every recorded session that used it. Years of traffic, all at once.
TLS 1.3 removed RSA key transport entirely. Honestly, it was overdue. The protocol now mandates forward secrecy for every connection. There's no negotiation about it. You don't get to opt out.
What TLS 1.3 threw out
The spec didn't just add a faster handshake. It killed a bunch of things that had caused real vulnerabilities:
- Static RSA key transport: no forward secrecy, gone
- Renegotiation: caused repeated authentication bugs, gone
- TLS-level compression: enabled the CRIME attack, gone
- CBC-mode ciphers: padding oracle attacks (POODLE, Lucky13), gone
- RC4 and 3DES: just old and broken, gone
- Custom/weak DH groups: enabled Logjam, gone
And the cipher suite naming got simpler. A TLS 1.3 suite like TLS_AES_128_GCM_SHA256 names only the AEAD cipher and the hash function. Key exchange and signature algorithms are negotiated separately now, through extensions. Cleaner. Less room to accidentally configure something awful.
One weird survivor: the ChangeCipherSpec message still exists, but only as a dummy for middlebox compatibility. It does nothing.
🛠️ Resumption and 0-RTT
Reconnecting to a server you've already talked to shouldn't cost a full handshake. TLS 1.3 handles this with pre-shared keys (PSKs). After the first handshake completes, the server issues a session ticket containing a PSK. On the next connection, the client includes that PSK in its ClientHello, and both sides can skip the certificate exchange.
0-RTT goes further. The client sends application data in its very first flight, literally alongside the ClientHello. No waiting for the server to respond at all.
But here's the gotcha: 0-RTT data is replayable. An attacker who captures that first flight can replay it to the server, and the server might process it again. So 0-RTT is only safe for idempotent requests. A GET that fetches your homepage? Fine. A POST that transfers money? Never.
| Mode | Round trips | Forward secrecy | Replay risk |
|---|---|---|---|
| Full handshake | 1-RTT | Yes | None |
| PSK resumption | 1-RTT | Yes (with ECDHE) | None |
| 0-RTT | 0-RTT | No (uses previous key) | Replayable |
Where it goes wrong in practice
A short list of things that break TLS in production:
- Still permitting TLS 1.0/1.1 because "legacy clients." Those protocols have known vulnerabilities.
- Server sends a misordered or incomplete certificate chain. The client can't verify and rejects the connection. (The upcoming certificates post covers chain mechanics.)
- Clock skew on the client causing spurious certificate validity failures.
- Enabling 0-RTT globally without thinking about which endpoints are idempotent.
- SNI still leaks the hostname in plaintext in the ClientHello — Encrypted Client Hello (ECH) fixes this but adoption is slow.
📌 Takeaways
- TLS 1.3 completes the handshake in one round trip by having the client speculatively send its key share upfront
- ECDHE produces a shared secret that never crosses the wire, and ephemeral keys give you forward secrecy for free
- The server's certificate and identity proof are encrypted in TLS 1.3, an improvement over 1.2 where the cert was visible to passive observers
- 0-RTT resumption is fast but replayable, only use it for idempotent requests
- TLS 1.3 forcibly removed every mode that lacked forward secrecy, which eliminated entire classes of past attacks
Keep reading
- HTTPS Explained: What the Padlock Actually Protects
- How End-to-End Encryption Really Works: The Magic of Diffie-Hellman Key Exchange
More from me
I write about backend systems, networking, and the internals of tools we use daily at arnavsharma.dev. Plenty more there if this was useful.
Top comments (0)