TLS 1.3 handshake
Every detail of the TLS 1.3 handshake: key schedule, certificate verification, 0-RTT, replay protection, and the practical security posture in 2026.
Why TLS 1.3 was a redesign
TLS 1.2 was a patchwork of two decades of fixes layered on SSL 3.0. The protocol carried obsolete cipher suites (RSA key exchange, CBC mode, RC4), allowed downgrades to weaker versions, and took 2 RTTs to complete. Every weakness produced a named attack: BEAST, CRIME, BREACH, Lucky 13, POODLE, FREAK, Logjam, DROWN.
TLS 1.3 (RFC 8446, August 2018) is a ground-up redesign. The wire format is the same shape so middleboxes do not flip out, but every cipher, every round trip, every option was reconsidered.
Result: faster, safer, easier to configure correctly.
The full 1-RTT handshake
ClientHello
The client sends:
legacy_version: TLS 1.2 for backwards compat. Real version is in extension.random: 32 bytes of randomness.legacy_session_id: empty or random, ignored.cipher_suites: AEAD suites only (TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256, two more rare).legacy_compression_methods: null only.extensions:supported_versions: TLS 1.3 (0x0304).supported_groups: X25519, P-256, P-384, FFDHE groups.signature_algorithms: ECDSA, RSA-PSS, EdDSA.key_share: client's ephemeral public key for one (or more) of the supported_groups.server_name(SNI): which hostname.application_layer_protocol_negotiation(ALPN): h2, http/1.1, h3.psk_key_exchange_modesandpre_shared_key: for resumption.
The key_share is a guess. If the server prefers a different group, it sends HelloRetryRequest and the client resends. This adds 1 RTT, so clients try to guess right.
ServerHello and the rest
The server picks a cipher suite and a key_share group, computes the shared secret, and derives handshake traffic keys. From here on, everything is encrypted.
In one flight:
ServerHello: server's random and key_share. This is the last unencrypted message.EncryptedExtensions: extensions that need to be encrypted (ALPN choice, max_fragment_length).Certificate: server's cert chain.CertificateVerify: signature over the entire transcript so far, using the cert's private key. Proves the server holds the key.Finished: HMAC over the transcript, using a key derived from the handshake secret. Confirms the handshake is intact.
Optionally the server can include CertificateRequest for mutual TLS.
Client Finished
Client verifies certificate chain, verifies CertificateVerify signature, derives keys, sends its own Finished. Application data can piggyback on this packet.
Total: 1 RTT from ClientHello to the first encrypted application data.
The key schedule
TLS 1.3 uses HKDF (RFC 5869) to derive a hierarchy of keys from initial secrets.
Early Secret = HKDF-Extract(0, PSK or 0)
Handshake Secret = HKDF-Extract(Derived, DH shared secret)
Master Secret = HKDF-Extract(Derived, 0)
From each, traffic keys (client and server) are derived for each phase:
- Early traffic keys: 0-RTT data, derived from Early Secret.
- Handshake traffic keys: encrypted handshake messages, derived from Handshake Secret.
- Application traffic keys: post-handshake data, derived from Master Secret.
Each key is bound to the transcript hash up to that point, which prevents tampering.
What got removed and why
TLS 1.3 explicitly forbids:
- RSA key exchange: no forward secrecy. If the server's key is stolen later, all past sessions can be decrypted.
- Static DH: same problem.
- DHE with custom groups: caller might pick a weak group (Logjam). Now only well-known groups.
- MD5, SHA-1 signatures: cryptographically broken.
- RC4: biased keystream.
- 3DES: too slow, sweet32 attack.
- CBC mode: padding oracle attacks (Lucky 13, BEAST). AEAD only now.
- Compression: CRIME attack reveals secret data via compression ratio.
- Renegotiation: complex, security holes (CVE-2009-3555). Replaced by KeyUpdate.
- Custom extensions for export-grade crypto: FREAK, Logjam.
The result: ~5 cipher suites instead of 300+. Easier to audit, easier to configure.
Cipher suite naming
TLS 1.3 names a cipher suite by its AEAD and hash: TLS_AES_128_GCM_SHA256. No more "RSA_WITH_AES_128_CBC_SHA"; key exchange and signature are negotiated separately via supported_groups and signature_algorithms.
Certificate validation
Server sends a chain of certificates. The client must:
- Parse and validate signatures in the chain.
- Find an anchor (root certificate) in its trust store.
- Check each cert is within its validity window.
- Check revocation: CRL, OCSP, or stapled OCSP.
- Verify the leaf's Subject Alternative Names match the SNI.
- Verify any extended key usage flags (must include serverAuth).
Revocation is the messy part. CRLs are huge and rarely fetched. OCSP requires an extra request to the CA on every connection. OCSP stapling lets the server attach a signed OCSP response in the handshake, avoiding the side channel. OCSP Must-Staple is a cert extension that requires stapling.
SNI and Encrypted Client Hello
SNI is sent in cleartext in ClientHello. This leaks the hostname to any observer, even though the rest of the connection is encrypted.
ECH (Encrypted Client Hello, draft RFC) encrypts the entire inner ClientHello, including SNI, using a public key fetched via DNS (HTTPS record). The outer SNI carries a generic name like cloudflare-ech.com. This hides which site you are visiting from passive observers.
Adoption: Cloudflare deployed, Firefox supports, Chrome experimental.
Session resumption and 0-RTT
After a successful handshake, the server can issue a NewSessionTicket to the client. The ticket contains an encrypted blob the server can later decrypt to recover the resumption master secret. The client stores the ticket.
On the next connection:
- Client sends ClientHello with
pre_shared_keyextension carrying the ticket. - If the server accepts, both derive new keys from the resumption secret without a fresh DH exchange.
This is 1 RTT, same as cold start, but skips the expensive cert verify and signature.
With early_data, the client can include application data in the first flight, encrypted under early traffic keys derived from the resumption secret. This is 0 RTT.
The 0-RTT replay problem
Early data can be captured by an attacker and replayed against the server. The server has no fresh nonce to detect the replay. If your endpoint changes state on first invocation, replay is a real attack.
Mitigations:
- Only allow 0-RTT for safe, idempotent operations (GET, OPTIONS, HEAD).
- Server can track recently-seen ClientHello hashes to detect duplicates within a window.
- Application layer should be replay-tolerant.
Reality: most CDNs only enable 0-RTT for GET requests, often with a CDN-side anti-replay cache. Cloudflare publishes the details.
Downgrade protection
A man-in-the-middle could try to force a downgrade to TLS 1.2 (where attacks exist). TLS 1.3 puts a downgrade sentinel in the server random: if the server is willing to do 1.3 but the negotiated version is 1.2, the random ends in specific bytes that a 1.3-capable client will check and abort.
TLS over QUIC
QUIC integrates TLS 1.3 directly (RFC 9001). The handshake uses the same messages but runs over QUIC streams, not TCP. The cryptographic keys are also used for QUIC packet protection.
This integration saves a round trip versus TCP+TLS because the TLS handshake and the QUIC handshake share their RTTs.
Performance tuning
- Use ECDSA certs (P-256) instead of RSA-2048: smaller cert, faster signature.
- Enable session resumption: drop cold-start cost on returning visitors.
- Enable OCSP stapling: avoid client-side revocation fetches.
- Use HTTP/3 (which is TLS over QUIC): better on lossy mobile networks.
- Prefer X25519 over P-256: slightly faster, simpler implementation.
TLS configuration mistakes I have seen
- Disabling forward secrecy by leaving TLS 1.2 RSA key exchange enabled.
- Using a cert signed by a private CA that browsers do not trust.
- Forgetting to renew before expiry (Let's Encrypt cert expiry is the #1 cause of "site is down" tickets).
- Wildcard cert covering one level only (
*.example.comdoes not covera.b.example.com). - Missing SAN entries (CN alone is no longer trusted by browsers).
- HSTS misconfiguration: preload list inclusion is permanent and unforgiving.
Debugging
openssl s_client -connect host:443 -tls1_3: see the handshake.curl --tlsv1.3 -v https://...: see the negotiated version.- SSL Labs (ssllabs.com): grade your TLS config.
dig HTTPS example.com: see HTTPS record with ECH and ALPN hints.- Wireshark with the right TLS keylog file: decrypt and read.
Numbers to memorize
- TLS 1.3 cold start: 1 RTT.
- TLS 1.3 resume: 0 RTT with early_data, 1 RTT without.
- TLS 1.2 cold start: 2 RTT.
- AEAD record overhead: 16 bytes auth tag + a few bytes nonce.
- Default cert chain depth: 2-3 (leaf, intermediate, root).
- Let's Encrypt cert validity: 90 days.
Learn more
- Paper
- Paper
- Paper
- ArticleCloudflare: A detailed look at TLS 1.3Cloudflare
- DocsHigh Performance Browser Networking: TLSIlya Grigorik
- Docs