TLS 1.3 Handshake
One round trip to a secure channel — how TLS 1.3 got there by deleting options, what each handshake message actually does, and why 0-RTT is the one part that trades security for latency.
TLS 1.3 (RFC 8446, 2018) took five years and 28 drafts, and its central design move was subtraction. TLS 1.2 accumulated two decades of options — RSA key transport, static DH, CBC modes, compression, renegotiation, MD5/SHA-1 signatures, custom DH groups — and nearly every major TLS attack exploited one of them or the negotiation between them. TLS 1.3 removed all of it, cut the handshake to one round trip, and made forward secrecy and AEAD mandatory rather than optional.
What was deleted, and which attack each deletion closes #
| Removed | Attack it enabled |
|---|---|
| RSA key transport | Bleichenbacher oracles — DROWN, ROBOT, Marvin |
| CBC ciphersuites | padding oracles — BEAST, Lucky 13, POODLE |
| RC4 | keystream biases |
| Compression | CRIME, BREACH |
| Renegotiation | triple-handshake, renegotiation injection |
| Custom/arbitrary DH groups | Logjam, small-subgroup confinement |
| MD5/SHA-1 in signatures | SLOTH, collision forgeries |
| Static DH / non-PFS suites | retroactive decryption of recorded traffic |
What remains is a short list: ECDHE or DHE key agreement with named groups only, five AEAD ciphersuites, and signatures over the transcript. Forward secrecy is not negotiable — there is no non-PFS mode left to downgrade into.
The 1-RTT handshake #
sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello<br/>+ key_share (X25519 guess)<br/>+ supported_versions, signature_algorithms
Note over S: derives shared secret immediately
S->>C: ServerHello + key_share<br/>-- encrypted from here --<br/>EncryptedExtensions<br/>Certificate<br/>CertificateVerify<br/>Finished
Note over C: verifies cert + transcript signature
C->>S: Finished
Note over C,S: application data flows
The move that buys the round trip: the client guesses the group and sends its ECDHE key share in the very first flight. If it guesses right (X25519 essentially always), the server completes key agreement on receipt and can encrypt everything after ServerHello. A wrong guess costs a HelloRetryRequest and one extra round trip — the fallback, not the norm.
The messages, and what each is for:
- ClientHello — random, offered ciphersuites,
key_share(the actual ECDHE public value),supported_versions(this is where "TLS 1.3" is declared — the legacy version field still says 1.2 for middlebox compatibility),signature_algorithms. - ServerHello — chosen suite and the server's
key_share. After this, both sides derive handshake traffic keys and everything else is encrypted, including the certificate. TLS 1.2 sent certificates in the clear; TLS 1.3 does not, which removes a passive-observer identity leak (though SNI in the ClientHello still leaks the hostname unless ECH is in use). - EncryptedExtensions — the rest of the negotiated parameters, now confidential.
- Certificate + CertificateVerify — the chain, plus a signature over the entire handshake transcript hash with the certificate's private key. This is what proves possession of the key and simultaneously binds the whole negotiation.
- Finished — HMAC over the transcript under a derived
finished_key, from each side.
The key schedule #
Everything derives from HKDF through chained extract/expand stages:
flowchart TD Z0["0"] --> E1["Extract"] PSK["PSK (or 0)"] --> E1 --> ES["Early Secret"] ES --> DS1["Derive-Secret"] --> E2["Extract"] DH["ECDHE output"] --> E2 --> HS["Handshake Secret"] HS --> CHTS["client_handshake_traffic_secret"] HS --> SHTS["server_handshake_traffic_secret"] HS --> DS2["Derive-Secret"] --> E3["Extract"] Z1["0"] --> E3 --> MS["Master Secret"] MS --> CATS["client_application_traffic_secret"] MS --> SATS["server_application_traffic_secret"] MS --> RMS["resumption_master_secret"] MS --> EMS["exporter_master_secret"]
Every Derive-Secret call mixes in a label and the transcript hash at that point, so each key is bound to the exact handshake state that produced it. Record keys and nonces come from the traffic secrets, and the per-record nonce is the 96-bit sequence number XORed with a static IV — counter-derived, never random, which is exactly the GCM nonce discipline the protocol needs. Traffic secrets can be ratcheted forward with KeyUpdate for long-lived connections.
Session resumption and 0-RTT #
After a handshake the server may issue NewSessionTickets carrying a PSK. A resumed handshake uses that PSK instead of a certificate, and the client may attach early data to its first flight — encrypted under a key derived from the PSK alone, before any fresh ECDHE. Zero round trips to first byte.
Resumption with the psk_dhe_ke mode restores full forward secrecy by mixing fresh ECDHE into the resumed handshake; prefer it over pure psk_ke.
Operational notes #
- Named groups only. X25519 is the default; P-256 for FIPS. No custom parameters means no Logjam.
- Five ciphersuites, all AEAD:
TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256, and two CCM variants. Clients pick GCM or ChaCha20 by hardware. - Middlebox compatibility mode — the dummy
ChangeCipherSpecand legacy version fields exist purely because deployed middleboxes broke on a handshake that looked new. A large fraction of the RFC's final drafts went into making TLS 1.3 look enough like 1.2 on the wire to pass. - Encrypted Client Hello (ECH) closes the remaining SNI leak by encrypting the inner ClientHello to a public key published in DNS; it is a separate, later specification.
- Post-quantum: deployments have converged on the hybrid
X25519MLKEM768group — classical and ML-KEM combined, so the handshake stays secure if either component holds. Enabled by default in major browsers as of 2026.