awesome-everything RU
↑ Back to the climb

Networking & Protocols

Key schedule, SNI, ALPN, and extensions

Crux How HKDF derives every traffic key from the shared secret, why SNI leaks which hostname you visit, and how ALPN, HelloRetryRequest, and OCSP stapling work inside the handshake.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min

You understand how ECDHE produces a shared secret. But that secret is not used directly as an encryption key — it feeds a key derivation tree that produces separate keys for handshake traffic, application traffic, resumption, and export. That tree is the HKDF key schedule, and every key TLS 1.3 uses flows from it.

HKDF key schedule (RFC 8446 §7.1)

TLS 1.3 derives every key from a two-phase HKDF tree:

Phase 1 — Early Secret.

early_secret = HKDF-Extract(salt=0, IKM=PSK_or_zero)

If no PSK: IKM is all zeros. From the early secret, Derive-Secret(early_secret, "derived", "") produces the salt for phase 2.

Phase 2 — Handshake Secret.

handshake_secret = HKDF-Extract(salt=derived_from_early, IKM=ECDHE_shared_secret)

From the handshake secret:

  • Client handshake traffic key: HKDF-Expand-Label(hs_secret, "c hs traffic", transcript, hash_len)
  • Server handshake traffic key: HKDF-Expand-Label(hs_secret, "s hs traffic", transcript, hash_len)

Phase 3 — Master Secret.

master_secret = HKDF-Extract(salt=derived_from_handshake, IKM=zero)

From the master secret:

  • Client application traffic key: HKDF-Expand-Label(master, "c ap traffic", transcript, hash_len)
  • Server application traffic key: HKDF-Expand-Label(master, "s ap traffic", transcript, hash_len)
  • Resumption PSK: Derive-Secret(master, "res master", transcript)

The labels ("c hs traffic", "s ap traffic", etc.) are domain separators: compromising one key derivation path cannot help an attacker on another path.

HKDF key schedule at a glance
Hash for TLS_AES_128_GCM_SHA256
SHA-256
Hash for TLS_AES_256_GCM_SHA384
SHA-384
Handshake traffic keys protect
EncryptedExtensions → Finished
Application traffic keys protect
All data after Finished
Key update primitive
HKDF-Expand-Label(current, 'traffic upd', '', hash_len)
Cipher suite controls
AEAD cipher + hash for schedule; key exchange is separate

Cipher suite separation in TLS 1.3

TLS 1.2 packed three choices into one cipher suite string: key exchange, bulk cipher, and MAC. TLS 1.3 separated them:

  • Named-group negotiation — picks the ECDHE curve (x25519, secp256r1, …)
  • Signature-algorithm negotiation — picks how the certificate signature is verified (ecdsa_secp256r1_sha256, ed25519, …)
  • Cipher suite — picks only the AEAD cipher and hash for the key schedule: TLS_AES_128_GCM_SHA256, TLS_CHACHA20_POLY1305_SHA256, TLS_AES_256_GCM_SHA384

TLS 1.2 had hundreds of valid combinations. TLS 1.3 has five standard suites.

SNI: server name indication

Many hostnames share one IP address (virtual hosting, CDNs). Without help the server cannot pick the right certificate before reading the request — the request is encrypted. SNI solves this by having the client send the hostname in plain text inside ClientHello. The server uses it to select the right certificate before responding.

The privacy cost: anyone on the wire learns which hostname you visited. This is the motivation for Encrypted ClientHello (ECH), covered in the next lesson.

ALPN: application-layer protocol negotiation

After TLS is established, client and server must agree on the protocol above: HTTP/1.1, HTTP/2, or HTTP/3. ALPN puts a list in ClientHello (typically ["h2", "http/1.1"]) and the server picks one in ServerHello. Because both messages are protected by the transcript hash, a middlebox cannot strip h2 to force HTTP/1.1 — tampering breaks Finished.

HelloRetryRequest

If the client’s key_share only contains a curve the server does not support, the server sends HelloRetryRequest (encoded as a special ServerHello with the magic_retry_request_random value) containing a cookie and the named group it wants. The client sends a fresh ClientHello with the correct key_share plus the cookie. This adds one RTT but is rare: modern clients offer x25519 and P-256 simultaneously to pre-empt it. The cookie also enables stateless servers (load balancers without connection affinity) to complete HRR without persisting state between the two ClientHellos.

OCSP stapling and CRLite

Traditional OCSP: the client makes a live request to the CA’s OCSP responder to check revocation — leaking browsing history and adding latency. OCSP stapling moves this to the server: the server periodically fetches a signed OCSP response and attaches it to the TLS handshake via the status_request extension. No extra client round-trip, no privacy leak.

Post-2024 reality: Let’s Encrypt stopped serving OCSP on 2025-05-07. Firefox 137 shipped CRLite by default — a compact, signed bloom-filter cascade covering all WebPKI revocations from CT logs, refreshed every 12 hours. New infrastructure should plan around 90-day (soon 47-day) cert lifetimes rather than runtime OCSP.

Certificate transparency (CT)

Every publicly trusted certificate must appear in at least two CT logs — append-only, publicly auditable, signed Merkle trees. The client checks the certificate carries Signed Certificate Timestamps (SCTs). Chrome and Safari refuse connections with missing or invalid SCTs. CT made the CA ecosystem auditable: a rogue CA cannot issue a certificate for example.com without leaving a public tamper-evident record, and detection tools (crt.sh) alert domain owners within minutes.

Quiz

Why does the HKDF key schedule use domain-separated labels for each key?

Quiz

Why is OCSP stapling being phased out across the WebPKI in 2024–2026?

Order the steps

Order the TLS 1.3 key derivation hierarchy from input to output:

  1. 1 Input: ECDHE shared secret
  2. 2 HKDF-Extract produces handshake secret
  3. 3 Derive-Secret produces handshake traffic secrets (client and server)
  4. 4 HKDF-Expand-Label produces handshake traffic keys and IVs
  5. 5 Records under handshake traffic key protect EncryptedExtensions → Finished
  6. 6 After Finished: master secret derived, application traffic keys produced
Why this works

Key update for long-lived connections. TLS 1.3 supports rolling application traffic keys mid-connection without renegotiation: either side sends KeyUpdate, derives a new application traffic secret via HKDF-Expand-Label(current, "traffic upd", "", hash_len), and starts using the new key. The peer responds with its own KeyUpdate. Modern AEAD ciphers tolerate enormous data volumes before key rotation is security-critical, but the mechanism exists for ultra-long-lived sessions (persistent WebSocket connections, service-mesh links open for hours).

Recall before you leave
  1. 01
    How does ALPN prevent downgrade attacks on the HTTP version?
  2. 02
    What information does SNI reveal, and why does ECH matter?
  3. 03
    When does a TLS 1.3 server send HelloRetryRequest, and what is the client required to include in its retry?
Recap

The HKDF key schedule derives a tree of keys from the ECDHE shared secret — early secret, handshake secret, master secret — each separated by domain labels so compromising one path cannot help an attacker on another. TLS 1.3 also separated cipher suite negotiation into three independent axes (named group, signature algorithm, AEAD cipher), collapsing hundreds of TLS 1.2 combinations to five standard suites. SNI sends the target hostname in plain text so virtual hosting works; ALPN negotiates the application protocol above TLS inside the tamper-proof transcript. HelloRetryRequest handles the edge case where the client’s first key_share guess is wrong. OCSP stapling reduced per-connection revocation latency; CRLite (Firefox 137+) replaces it with a locally refreshed bloom-filter cascade.

Connected lessons
appears again in47
Continue the climb ↑0-RTT defenses, ECH, hybrid PQ, and production TLS
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.