TLS 1.3 Handshake

1-RTT by default, 0-RTT when you're brave, all encrypted post-ServerHello

TLS 1.3 (RFC 8446) cut the handshake from two round trips down to one. The client speculates on which key share the server will accept; the server responds with ClientHello + ServerHello + finished + the application's first response, all in a single round trip. With 0-RTT (PSK resumption), the client can even send application data inside the first packet — before any server confirmation — accepting the cost of replay risk for the latency win.

Everything after ServerHello is encrypted: the certificate, the extensions, the Finished. This closes a long-standing TLS 1.2 weakness where the server's certificate chain travelled in cleartext, leaking which site you were visiting even on encrypted connections. Combined with ECH (Encrypted Client Hello, in progress), the entire handshake becomes opaque to network observers.

The 1-RTT Handshake

Six message types in three flights. The client guesses a curve (X25519 or P-256), sends a key share, and the server picks one or sends HelloRetryRequest if it doesn't like the guess.

Client Server ClientHello + key_share, supported_versions, signature_algorithms, ALPN, SNI ServerHello (cleartext) + key_share ⇒ both sides derive handshake secret {EncryptedExtensions} ALPN, SNI ack, etc. {Certificate} {CertificateVerify} signature over transcript hash ⇒ proves cert ownership {Finished} {Finished} + [Application Data] 1-RTT to first byte of app data sent = encrypted with handshake secret [] = encrypted with application secret

Key Numbers

1 RTT
to first byte of application data (TLS 1.3 default)
0 RTT
with PSK resumption (replay-vulnerable)
2 RTT
TLS 1.2 baseline (and the prior bar)
5
supported AEAD ciphers in TLS 1.3 (3 standardized)
256 bits
handshake secret length (with SHA-256 PRF)
RFC 8446
TLS 1.3 spec, published Aug 2018
~99%
browser TLS 1.3 deployment by 2024

ClientHello: The Ambitious First Flight

The client speculates that one of its offered key_share groups is acceptable to the server. If so, the server can derive a shared secret immediately. If not, the server sends HelloRetryRequest specifying which group it wants and the client retries — a 2-RTT fallback.

{`ClientHello
  legacy_version            = 0x0303              // TLS 1.2 for compat
  random                    = 32 random bytes
  legacy_session_id         = 32 random bytes (cookie for middlebox)
  cipher_suites             = [TLS_AES_128_GCM_SHA256,
                               TLS_AES_256_GCM_SHA384,
                               TLS_CHACHA20_POLY1305_SHA256]
  legacy_compression_methods= [null]
  extensions:
    supported_versions      = [TLS 1.3]            // signals real version
    supported_groups        = [x25519, secp256r1, secp384r1, ...]
    key_share               = [x25519: <32 bytes>, secp256r1: <65 bytes>]
                              // speculative ECDHE pubkeys
    signature_algorithms    = [ed25519, ecdsa_secp256r1_sha256, rsa_pss_rsae_sha256, ...]
    server_name             = "example.com"        // SNI
    application_layer_protocol_negotiation = [h2, http/1.1]
    psk_key_exchange_modes  = [psk_dhe_ke]         // for resumption
    pre_shared_key          =   // optional, enables 0-RTT`}

ServerHello + Encrypted Rest

The server picks one of the offered key_share groups, computes the ECDHE shared secret, and switches to encrypted mode for everything after ServerHello.

{`ServerHello (cleartext)
  legacy_version    = 0x0303
  random            = 32 random bytes
  cipher_suite      = TLS_AES_128_GCM_SHA256       // chosen
  key_share         = x25519: <32 bytes>            // server's pubkey
  supported_versions = TLS 1.3

# At this point both sides compute:
#   shared_secret = ECDHE(client_priv, server_pub) = ECDHE(server_priv, client_pub)
#   handshake_secret = HKDF-Extract(shared_secret, ...)
# All subsequent handshake messages are encrypted with handshake_secret-derived keys.

# Encrypted handshake messages:
{EncryptedExtensions}    // ALPN result, SNI ack
{Certificate}            // server's cert chain
{CertificateVerify}      // sig over transcript hash, proves cert ownership
{Finished}               // HMAC over transcript with handshake secret`}

The HKDF Key Schedule

TLS 1.3 derives a hierarchy of secrets from the ECDHE shared secret using HKDF (HMAC-based Key Derivation Function). Each stage's output feeds the next stage.

{`# Simplified key schedule (RFC 8446 §7.1)

ZERO   = 32 bytes of zero (HKDF salt for first extract)

early_secret      = HKDF-Extract(ZERO, PSK or ZERO)
                    |- binder_key       (used for PSK binders)
                    |- early_traffic    (0-RTT data key)
                    \\- derived_es

handshake_secret  = HKDF-Extract(derived_es, ECDHE_shared_secret)
                    |- client_handshake_traffic_secret  (CHTS)
                    |- server_handshake_traffic_secret  (SHTS)
                    \\- derived_hs

master_secret     = HKDF-Extract(derived_hs, ZERO)
                    |- client_application_traffic_secret_0 (CATS_0)
                    |- server_application_traffic_secret_0 (SATS_0)
                    |- exporter_master_secret              (for QUIC, channel binding)
                    \\- resumption_master_secret           (for next session)

# Each *_traffic_secret is then expanded into key + iv with HKDF-Expand-Label:
client_write_key = HKDF-Expand-Label(CATS_0, "key", "", 16)
client_write_iv  = HKDF-Expand-Label(CATS_0, "iv",  "", 12)`}

Key updates rotate the application traffic secret without a new handshake. KeyUpdate messages can fire periodically (every 16 MB is conventional) to limit damage from any future key compromise.

0-RTT and the Replay Risk

With 0-RTT, the client sends application data in the first packet alongside the ClientHello, encrypted with a key derived from the previous session's resumption_master_secret. Saves a round trip. Costs replay protection.

{`# 0-RTT flow (PSK resumption with early data)
[round trip 0]
ClientHello
  + pre_shared_key       = 
  + early_data           = 
{EarlyData}              =      # encrypted with early_secret
                                                    # SAME key as last session
ServerHello
  + pre_shared_key       = accepted
{EncryptedExtensions}
  + early_data           = accepted
{Finished}
{Application Data}       = 

# THE PROBLEM: 0-RTT data has no replay protection.
# An attacker who recorded the encrypted ClientHello + EarlyData can
# replay it. The server has no way to tell "is this the original or
# a replay?" because both have the same nonce and key material.

# Mitigation: only allow IDEMPOTENT requests in 0-RTT.
#   - GET /api/users         OK (read-only)
#   - POST /api/charge $100  NOT OK (replay = double-charge)

# nginx config
ssl_early_data on;
proxy_set_header Early-Data $ssl_early_data;
# Backend rejects state-changing requests when Early-Data: 1`}

Session Resumption (PSK)

After a successful handshake the server can issue a session ticket. The client stores it and presents it on the next connection as a Pre-Shared Key. Skips the certificate verification round and (with 0-RTT) the round trip itself.

{`# After the handshake, server sends:
NewSessionTicket
  ticket_lifetime    = 86400          // seconds, max 7 days per spec
  ticket_age_add     = random
  ticket_nonce       = random
  ticket             =   // encrypted resumption_master_secret
  extensions:
    early_data       = max_early_data_size: 16384

# Next connection:
ClientHello
  pre_shared_key:
    identities       = []
    binders          = [HMAC over transcript with binder_key]

# Server validates the binder, recovers the resumption secret,
# and proceeds without sending a Certificate (no CPU-heavy signing).

# Tradeoffs:
#   + Faster (no cert chain, possibly 0-RTT)
#   + Saves CPU on server (no RSA/ECDSA signature)
#   - Forward secrecy depends on PSK_DHE mode (default in TLS 1.3)
#   - Tickets are bearer tokens - protect them like passwords`}

TLS 1.3 vs TLS 1.2

AspectTLS 1.2TLS 1.3
Round trips2 (or 1 with False Start)1, optionally 0
Cipher suites~300 in IANA registry, many weak5 total, 3 mandatory, all AEAD
Key exchangeRSA, DHE, ECDHEECDHE only (forward secrecy mandatory)
Static RSAAllowedRemoved
RenegotiationYes (CVE-prone)Removed (replaced by KeyUpdate)
Certificate visibilityCleartextEncrypted (post-ServerHello)
CompressionOptional (CRIME attack)Removed
MAC-then-encryptYes (some ciphers)Encrypt-then-MAC only (AEAD)

Tradeoffs

0-RTT vs replay safety

0-RTT saves an RTT but exposes idempotency assumptions. Get them wrong and an attacker can replay GET /pay/transfer requests forever. Most servers default to off; turn on only for safe routes.

Speculation cost

Sending a key_share for X25519 costs ~32 bytes per ClientHello whether or not the server accepts it. If the server prefers a different group, HelloRetryRequest forces a 2-RTT path. Pick supported_groups wisely.

Tickets vs forward secrecy

Long-lived tickets let one server-key compromise decrypt months of traffic. Short lifetimes (~1 day) plus PSK_DHE mode give resumption with rolling forward secrecy.

Middlebox compat

Lots of TLS 1.3 design (legacy_version, ChangeCipherSpec, session_id) exists purely to fool middleboxes that crashed on real TLS 1.3. The handshake is more complex than it needs to be.

FAQ

What's the relationship between TLS 1.3 and QUIC?

QUIC bundles TLS 1.3 inline. The QUIC handshake is the TLS 1.3 handshake without the TLS record layer (QUIC handles framing). Same key schedule, same messages, just delivered over UDP instead of TCP.

What is ECH?

Encrypted Client Hello. The SNI (server name) field is currently visible in ClientHello, leaking which site you're contacting. ECH encrypts the inner ClientHello using a public key from a DNS HTTPS record. Browser support is shipping; server adoption is partial.

How does TLS 1.3 prevent downgrade attacks?

The server's random includes a sentinel pattern when downgrading would be safe. A genuine TLS 1.2 server lacks the sentinel; a TLS 1.3 server pretending to be TLS 1.2 includes it. The client checks and aborts on mismatch.

Why are there only five cipher suites?

TLS 1.2 had ~300 combinations of (key exchange, auth, cipher, MAC), most weak or redundant. TLS 1.3 separated key exchange (ECDHE only) from authentication (signature_algorithms) from the AEAD cipher, leaving just the AEAD choice. Three of the five are mandatory for compliance.

Can I use ed25519 in TLS 1.3?

Yes — signature_algorithms can include ed25519. Most CAs still issue ECDSA P-256 or RSA certs, but ed25519 leaf certificates are increasingly supported. Big perf win on the signing side.

What's the cost of TLS 1.3 vs unencrypted HTTP?

~1ms CPU and ~250 ns/byte for AES-GCM on modern hardware. The handshake adds a single RTT. For internal RPC at high throughput, TLS overhead is in the noise. For mobile clients on high-latency networks, 1-RTT is a meaningful saving over TLS 1.2.