TLS Handshake
The TLS handshake is the exchange that turns a plain TCP connection into an encrypted, authenticated one. In TLS 1.3 it costs a single round trip: the client and the server each send one public key share, both derive the same secret independently, and the server signs a hash of the conversation to prove it is who its certificate claims.
Two people stand on opposite rims of a canyon and want a shared colour of paint. Each starts from the same public yellow, stirs in a private colour nobody else knows, and shouts the mixture across. Each then adds their own private colour to what they caught. Both end up holding the identical shade, and anyone listening in the valley heard two mixtures they cannot unmix. That illustrates Diffie–Hellman key agreement; authentication and the later key schedule are still required. The interesting question — the one this lesson is actually about — is how you know who was standing on the other rim.
How the TLS Handshake Works
Start with what the wire carries. The client picks a private number a and sends only A = g^a mod p. The server picks a private b and sends only B = g^b mod p. The client computes B^a, the server computes A^b, and because both equal g^(ab) they are the same number, call it S. Neither a, nor b, nor S was ever transmitted. Someone recording every byte holds A and B and needs to solve a discrete logarithm to get anywhere, which is computationally infeasible with suitable real parameters and current classical attacks. The tiny teaching parameters are easy to break.
From S both sides run HKDF over the transcript so far to get handshake keys, and subsequent handshake payloads are encrypted. Record headers remain visible, and compatibility ChangeCipherSpec records are an exception. That includes the certificate, which is why a packet capture of TLS 1.3 shows you far less than a capture of TLS 1.2 did.
Now the part that carries the actual weight. The server sends its Certificate: a document saying "the holder of this public key runs example.com", signed by a certificate authority your browser already trusts. A certificate is public. Every visitor to the site has a copy. Presenting one proves nothing at all.
So the server also sends CertificateVerify: a signature, made with the private key the certificate names, over a context-bound encoding of the transcript hash for this connection so far. That hash includes both key shares. It is unique to this conversation, so the signature cannot be lifted from another one, and it cannot be produced by anyone who does not hold the private key. This is the message that binds the certificate to the party at the far end. Everything else in the handshake is bookkeeping around it.
Finally each side sends Finished, an HMAC over the transcript hash using a derived Finished key, not S directly. If anyone had edited a byte of the earlier, unencrypted hellos — say, to strip a modern cipher suite and force a weak one — the two sides would have hashed different transcripts and the HMACs would not match. This authenticates the negotiated transcript; TLS also has dedicated version-downgrade protections.
TLS Handshake Step by Step
The default run is the full handshake to example.com:443, with TCP already connected. The Diffie–Hellman numbers are toy-sized so you can check them: p = 23, g = 5.
- Nothing exists yet. TCP is up, so every byte so far has been readable by anyone on the path. Neither side holds a key.
- The client generates its half. Private
a = 6, publicA = 5^6 mod 23 = 8. Watch the two rows in the key panel: the private one carries a padlock and will never appear on a packet. - ClientHello crosses in the clear, carrying
key_share A = 8and the hostname in SNI. Anyone watching now knowsA. For real parameters recoveringafromAis computationally hard; these tiny numbers are only a teaching example. - The server generates its half. Private
b = 15, publicB = 5^15 mod 23 = 19. Both privates are fresh for this connection and discarded at the end of it. - The server derives
S.A^b mod 23 = 8^15 mod 23 = 2. It can encrypt from this moment, having received no secret to do so. - ServerHello carries
key_share B = 19. It is the last unencrypted handshake message in this run; record headers and compatibility records are not hidden. - The client derives
S.B^a mod 23 = 19^6 mod 23 = 2. Same number, different exponent, never transmitted. The shaded band appears across the diagram: everything below this line is encrypted. - Certificate. The canvas labels the identity
CN = example.com; real hostname validation uses subjectAltName, not CN. The certificate chain reaches a trusted CA and carries the server’s public key. Encrypted, but only for privacy — the contents are public information. - CertificateVerify. A signature over the hash of the transcript, made with the private key matching the certificate.
- Finished from the server: an HMAC over the transcript hash with the derived server Finished key.
- Three certificate checks. Does the chain reach a trusted CA? Does the name match the host you asked for? Is it in date? All three pass. This is a real certificate for
example.com, and it is still not enough. - The signature check.
verify(cert.pub, sig, H(transcript))returns true. The certificate said who owns the key; this says the far end is holding it. A copied certificate dies here. - The transcript check. The client recomputes the Finished HMAC over its own view of the handshake. It matches, so nobody edited anything.
- The client's Finished goes back, and the handshake is complete in both directions.
- Application keys. A second HKDF derivation produces separate keys for the data, so the handshake keys never encrypt a request.
GET / HTTP/1.1, encrypted, one round trip after TCP connect.- Seven displayed transfers, zero key secrets on the wire, one handshake signature. The count includes the application request and omits EncryptedExtensions; it is not a count of real TLS handshake messages. Nothing secret was transmitted, and the signature is what bound the key to
example.comrather than to whoever happened to answer. The panel on the right holds everything an eavesdropper saw:A = 8,B = 19, addresses, timing, packet sizes, the SNI hostname. Not the request.
You can watch a real one with openssl s_client -connect example.com:443 -tls1_3 -msg, which prints each handshake message as it arrives; everything after ServerHello shows up as an encrypted record with the inner message type decoded beside it.
Why the Signature, Not the Certificate, Makes It Safe
Select the On-path attacker scenario. Someone controls a router between you and the server — a hostile hotspot, a compromised ISP box. Her position is the whole of her advantage; nothing about her is clever.
She cannot passively relay, because a relayed handshake produces a key she has no part in. So she substitutes: she keeps the client's A = 8, sends the server her own M = 11, and answers the client with M in place of the server's B. Now she runs two handshakes. She and the client share M^a = 9. She and the server share M^b = 10. Both are cryptographically perfect. The client and the server share nothing.
Beat 11 of that run is the one to sit with. Every derivation has succeeded, both sides believe they have a secure channel, and no mechanism so far has noticed her. The paint trick works exactly as advertised — it just never promised anything about who is standing across the canyon.
Then she forwards the server's certificate. Unmodified, byte for byte, because certificates are public and copying one breaks no rule. Chain, name, dates: all three pass. She has a genuine certificate for example.com in a connection she controls.
And she is stopped one message later, at CertificateVerify. The signature she can replay covers H(A=11 · B=19), the transcript she exchanged with the server. The client will hash H(A=8 · B=11). Different bytes, so the signature does not verify, and producing a correct one would need the server's private key, which never left the server. The client sends decrypt_error — TLS 1.3's alert for a handshake signature that fails to verify — and tears the connection down. A browser may report a handshake failure; a certificate-warning page is not evidence of this particular failure.
Delete CertificateVerify from the protocol and that entire run succeeds in silence. That is what one signature buys.
What Session Resumption Skips
Select Session resumption. The browser has connected to this server before and kept the ticket it was handed: a public id, plus a secret key only these two hold. The ClientHello offers the id and attaches a binder — a MAC over the hello itself, keyed with that secret. The id proves nothing, because it travels in the clear and anyone watching could copy it; the MAC is the part only the real client can compute. The server looks up the key the id names, checks the binder, and the whole certificate flight simply never happens: no Certificate, no CertificateVerify, no signature to verify. The panel of certificate checks does not appear at all.
Five displayed transfers instead of seven, and no certificate or CertificateVerify verification in this resumption model. The round-trip count is unchanged — the network round-trip requirement stays the same, though reduced computation can affect elapsed time — but it removes certificate authentication work. This preset still performs an ephemeral public-key exchange, so the CPU savings depend on the implementation. On a server terminating tens of thousands of connections a second, that is the difference that matters.
Note that the client still generates a fresh a and still runs a key exchange. This preset chooses PSK plus ephemeral (EC)DHE. TLS 1.3 also permits PSK-only mode, which lacks that forward-secrecy property. The binder authenticates the offer; the ephemeral exchange protects later application traffic against subsequent PSK compromise. Both feed the key schedule — the ticket's secret half and S together — so an attacker who replays a recorded ClientHello, binder and all, still derives nothing readable. A resumption that leaned on the ticket alone would be decryptable by anyone who ever obtained it.
Edge Cases
- The handshake runs on top of an existing TCP connection. The three-way TCP handshake has already cost a round trip before ClientHello is sent. "One round trip" always means one after connect. Over a fresh connection to a distant server, a page load pays both.
- 0-RTT early data. A resuming client may attach application data to its very first flight, cutting the wait to zero. The catch is that this data has no replay protection: an attacker who records it can send it again. Applications must decide whether replay is acceptable and apply replay defenses; idempotence alone does not establish safety for every effect of a request.
- HelloRetryRequest. If the client guesses a group the server will not use, the server asks for a new key share and the handshake costs two round trips instead of one. Guessing right is why clients offer X25519 by default.
- Client certificates. The server can ask the client to authenticate the same way, with its own Certificate and CertificateVerify. Rare on the public web, common inside service meshes.
- An expired, untrusted, or wrong-name certificate fails this model’s chain, name, or date check. A misissued certificate with an accepted chain and matching identity may pass those checks. Those three checks and the signature check answer different questions, and both have to pass.
- The failure modes are distinguishable in the logs. A chain, name, or date problem surfaces as a certificate error before any signature is examined —
unable to get local issuer certificate, a hostname mismatch, an expiry. A CertificateVerify that does not verify raisesdecrypt_error, which despite the name means the signature was wrong. A Finished mismatch raisesdecrypt_errortoo, but only after the certificate has been accepted, which is the shape of a tampered handshake rather than a misconfigured server. - Certificate revocation is the messy one. A stolen key can be revoked, but checking revocation reliably is hard enough that revocation mechanisms and failure policies vary by browser; short lifetimes reduce exposure but do not replace all revocation checks.
Common Mistakes
- Thinking the certificate encrypts the connection. Its public key authenticates the handshake rather than directly encrypting application data. The keys come from the Diffie–Hellman exchange; the certificate's key is used once, to sign. Encryption and identity are separate mechanisms that happen to travel together.
- Thinking a valid certificate proves you are talking to its owner. Certificates are public documents. The attacker scenario passes every certificate check while running the connection. The proof is the signature over this connection's transcript.
- Believing that a secure key exchange implies a secure connection. Diffie–Hellman gives you a shared key with somebody. Unauthenticated, it will happily give you one with an attacker.
- Assuming TLS hides where you are going. In this example without ECH, the destination IP, packet sizes, timing, and SNI hostname in ClientHello are visible. TLS protects the contents of the conversation, not the fact that it happened.
- Expecting resumption to cut a round trip. It reduces certificate-authentication work without removing a network round trip, unless 0-RTT is also used. Computation savings can still reduce total elapsed time. If your handshake latency has not moved after enabling session tickets, that is the expected result.
- Reading a TLS 1.2 diagram as though it were TLS 1.3. A full TLS 1.2 handshake typically needed two round trips, sent the certificate in the clear, and offered an RSA key-transport mode with no forward secrecy. TLS 1.3 removed RSA key transport. In this ephemeral exchange, later disclosure of the server’s signing key does not reveal recorded session keys once ephemeral secrets are erased; PSK-only and 0-RTT have different forward-secrecy properties.
A Note on Simplification
The Diffie–Hellman numbers on the canvas are toys. p = 23 and g = 5 exist so you can verify 19^6 mod 23 = 2 on paper; real TLS 1.3 uses X25519 or NIST P-256, which use elliptic-curve group operations rather than this finite-field exponentiation. TLS 1.3 also defines finite-field groups.
The lesson runs one cipher suite with no negotiation, and collapses the HKDF key schedule to a single step. The real schedule is a tree of derived secrets — early, handshake, and application traffic secrets, each with separate keys for each direction, plus an exporter secret and a resumption secret — all chained so that each stage depends on the transcript up to that point. EncryptedExtensions, which always follows ServerHello, is not drawn.
Left out entirely: 0-RTT early data, HelloRetryRequest, client certificates, the details of chain building and revocation, and Encrypted Client Hello, standardized in RFC 9849 to encrypt ClientHello content including the private SNI, which this example leaves readable. RFC 8446 is the specification, and it is unusually readable for an RFC.
Keep exploring
A few lessons that share the same technique or lead into the next idea.
Premium players need paid access. Every Explain doc is free.
Found a mistake or something unclear? Leave feedback.