Skip to content
Crypto2026-10-04Begin3 min read

HKDF: How TLS 1.3 Grows One Shared Secret Into a Whole Key Tree

The problem: one secret, one tree of keys

When a TLS handshake completes, both sides hold a shared secret (the X25519 output, or the two secrets concatenated in the ML-KEM hybrid group). But a TLS session needs far more than one key:

  • Client and server handshake-stage write keys and IVs (protecting the Finished messages)
  • Client and server application-data write keys and IVs
  • Per-stage Finished verification keys
  • The exporter interface (key material for upper layers)
  • Resumption PSKs and ticket keys

And these keys must be mutually independent — leaking an application-data key must not endanger handshake integrity, and old session keys must not help attack new ones. Growing one secret into a tree, safely, is HKDF's job.

Two steps: Extract converges, Expand branches

HKDF (RFC 5869) is built on HMAC, in two phases:

Extract: PRK = HMAC(salt, IKM) — mix the input keying material (whose distribution may be uneven) with a salt, converging it into a pseudorandom key of uniform cryptographic quality. The salt provides domain separation: different purposes, different salts, different PRKs even from identical IKM.

Expand: OKM = HKDF-Expand(PRK, info, L) — derive output keying material of any length, labeled by info. The info field says exactly what this key "is, for whom, for what".

Why not just hash(secret || label)? Three hard reasons: length-extension-style structural weaknesses (depending on the hash's construction), no safe way to produce long outputs, and the decisive one — no built-in domain separation. The info mechanism guarantees "client write key" and "server write key" can never collide into the same key.

The TLS 1.3 three-layer schedule

TLS 1.3 uses HKDF as a scheduling tree:

Early Secret  ←  PSK (resumption path)
   ↓ Derive-Secret
Handshake Secret  ←  ECDHE/ML-KEM shared secret injected
   ↓ "c hs traffic" / "s hs traffic"   → handshake-stage keys
Master Secret
   ↓ "c ap traffic" / "s ap traffic"   → application-data keys
   ↓ "exp master"                      → exporter
   ↓ "res master"                      → resumption PSK

Each layer is one HKDF-Extract (injecting a new secret) plus multiple labeled Expands. The elegant property: each handshake stage only unlocks its own layer's keys — if handshake keys are compromised, application-layer keys still stand independently; meanwhile the Finished messages, keyed by handshake keys, cover the entire transcript so a man in the middle cannot tamper with negotiation.

Fast KDF vs. slow KDF: two things sharing one name

A common confusion: HKDF and PBKDF2/argon2 are all called "key derivation", yet they are opposites in purpose —

HKDFPBKDF2 / bcrypt / argon2
InputAlready high-entropy secrets (DH outputs)Low-entropy human passwords
Design goalFast (session setup must not stall)Slow (punish brute force)
Brute-force resistanceNot its jobIts core job

Deriving from a password with HKDF, or deriving TLS session keys with argon2, are both category errors — the first has no defense at all, the second adds hundreds of milliseconds for nothing.

Implementer's note

HKDF is a first-class citizen in browser WebCrypto (crypto.subtle.deriveBits with the HKDF algorithm) — something I felt directly while building this site's tools: the HMAC primitive (the HMAC tool) and HKDF differ by only two thin wrappers — extract is one HMAC call, expand is a counter-based chain of them. The other side of the contrast is just as real: the AES tool must take the slow PBKDF2 path when deriving keys from passphrases (browser-measured at only tens of thousands of iterations per second — a feature, not a performance bug). The two derivation regimes coexist in one tool site, turning this division-of-labor table into something touchable.