TLS 1.3 Handshake

Every HTTPS connection you have ever debugged starts here. TLS 1.3 cut the handshake from two round trips to one, deleted RSA key transport outright, and added a 0-RTT mode that trades replay safety for latency. This module walks the real BoringSSL state machine, the HKDF key schedule, X.509 chain validation and the record layer that carries every byte afterwards.

Launch Simulator →

What you'll explore

  1. 01

    One Round Trip, Not Two

    TLS 1.2 spent a full round trip agreeing on parameters before either side could talk about keys: ClientHello, ServerHello, then a second exchange for the key agreement, then Finished. TLS 1.3 collapses that by making a guess. The client sends its ClientHello already carrying a key share for the group it expects the server to pick, so if the guess is right the server can reply with its own share, the certificate, and Finished in one flight — application data flows after a single round trip. If the guess is wrong the server sends a HelloRetryRequest and you pay the extra trip after all, which is why a client's group-preference list is a real latency decision and not a cosmetic one. In BoringSSL none of this lives in a linear function: SSL_do_handshake() is a pump that repeatedly re-enters a state machine, returning to the caller whenever it needs more bytes from the network. That shape is why a TLS error surfaces at a state name rather than a line number, and why 'where in the handshake did it fail' is the first question worth asking.

  2. 02

    The Keys Nobody Ever Sends

    In TLS 1.2's RSA mode the client picked the premaster secret and encrypted it to the server's public key — the secret physically travelled over the wire. Anyone who recorded the session and later obtained that one private key could decrypt it, forever. TLS 1.3 removes that mode entirely: every key exchange is ephemeral Diffie-Hellman over a named group, both sides contribute a share, and the shared secret is never transmitted. Compromising the server's long-term key afterwards lets an attacker impersonate the server but not read yesterday's traffic; that property is forward secrecy. What the certificate's key does now is sign the transcript, proving the server participated, rather than decrypt a secret. The derived secret is then fed through a HKDF-based key schedule — tls13_advance_key_schedule() extracts and expands at each stage — producing separate handshake and application traffic keys rather than one blob. Note the cipher suite no longer names the key exchange at all: TLS_AES_128_GCM_SHA256 says AEAD and hash, and the group is negotiated independently.

  3. 03

    Trust Is a Local Decision

    The server sends a chain; your side decides whether it terminates in something you already trust. Validation walks upward from the leaf, finding each certificate's issuer and checking signature, validity window and constraints, until it reaches a certificate in the local trust store — the trust anchor. A server may include the root in what it sends (RFC 8446 says a certificate specifying a trust anchor MAY be omitted, and both BoringSSL and OpenSSL will transmit one if it is in the configured chain), but an anchor is never trusted because it arrived: the trust decision reads only your local store, which is exactly why a chain that validates on your laptop can fail inside a minimal container with an empty CA bundle, and why stripping a served root is a legitimate handshake-size optimisation rather than a correctness change. The depth limit is the most misread knob here: it counts only the intermediate CAs, excluding both the leaf and the anchor. Exceed it and OpenSSL stops extending the chain and reports X509_V_ERR_CERT_CHAIN_TOO_LONG. Revocation is the other half: asking an OCSP responder online puts a third party on your connection's critical path and tells them which site you are visiting, so TLS instead lets the server staple a signed, timestamped OCSP response it fetched itself.

  4. 04

    Resumption, and the Price of Zero

    After a handshake completes the server may hand the client one or more session tickets, each a self-contained blob plus a pre-shared key derived from the finished session. A later connection can offer that ticket instead of repeating the full certificate exchange, which removes the certificate flight and the signature verification but still costs one round trip. 0-RTT goes further: the client attaches application data to its very first flight, encrypted under a key derived from the ticket alone. Nothing in that data is bound to anything fresh from the server — the server has not spoken yet — so an attacker who captures the flight can send it again later and nothing in the data itself distinguishes the copy from the original. A server can bolt on mitigations — single-use tickets, or recording which ClientHellos it has already seen within a freshness window — but each one costs shared state the stateless ticket design was meant to avoid. The exposure is intrinsic to sending data before the peer has contributed randomness, not an implementation flaw to be patched. It is safe for idempotent requests and unsafe for anything that transfers money, mutates state, or is counted. 0-RTT can also be rejected for many independent reasons, and BoringSSL records which one, so 'why did my early data not apply' has an actual answer.

  5. 05

    After the Handshake: Every Byte in a Record

    The handshake is the short part. Everything afterwards is framed into records, each sealed with the negotiated AEAD under a per-record nonce derived from a sequence number, then handed to TCP. TLS 1.3 tightened this layer in ways that matter operationally: the plaintext content type moves inside the encrypted body so an observer sees only 'application data', padding is available to blur lengths, and compression is gone because compressing attacker-influenced plaintext alongside secrets is what made CRIME possible. A record's plaintext is capped at 16 KB and the ciphertext adds header plus AEAD tag, so each record has fixed overhead — send one byte per record and you pay it every time. Because integrity is per-record and the sequence number is implicit, records cannot be dropped, reordered or replayed without the AEAD failing, which is why a TLS connection answers a corrupted record by tearing down rather than retrying. This is also the layer where head-of-line blocking lives: a partially-received record cannot be decrypted at all.

Validated against BoringSSL (TLS 1.3 protocol) + OpenSSL (X.509 chain verification) boringssl-eada60b7 + openssl-3.5.7 — ssl/ssl_lib.cc (BoringSSL handshake entry) and friends. Every simulated behavior cites a real source function, and a server-side correlation engine computes how each layer's state cascades into the next. See the methodology →

This module is part of zerohop Pro — it unlocks alongside the full kata library, progress tracking, and weak-area analysis.