Key Derivation Functions
The two different jobs called "KDF" — stretching low-entropy passwords versus expanding high-entropy secrets — why HKDF splits extract from expand, and why context binding is what stops cross-protocol key reuse.
"KDF" names two jobs that share a word and almost nothing else. One turns a low-entropy human password into a key and must be deliberately slow. The other turns an already-strong secret — a Diffie–Hellman output, a master key — into several independent keys and must be fast. Using the wrong family for the job is a real and common bug: a password through HKDF is brute-forceable, and a DH secret through Argon2 is a pointless performance tax.
| Password-based KDF | Key-expansion KDF | |
|---|---|---|
| Input | low entropy (~20-40 bits) | high entropy (≥128 bits) |
| Threat | offline brute force | none from guessing |
| Design goal | expensive (time + memory) | cheap, provably a PRF |
| Examples | Argon2id, scrypt, bcrypt, PBKDF2 | HKDF, SP 800-108 KDFs, TLS 1.3 schedule |
| Covered in | password storage | this entry |
Why not just hash it #
The naive key = SHA256(shared_secret) fails for reasons that are structural, not stylistic:
- A Diffie–Hellman output is a group element, not a uniform bit string. Its bits are biased and it carries algebraic structure — for a finite-field DH the result is always a quadratic residue, which alone leaks a bit.
- Merkle–Damgård hashes are length-extendable, so
H(secret ‖ context)constructions are unsafe as MACs and fragile as KDFs. - You usually need several keys (send/receive, encryption/MAC), and they must be independent — recovering one must not reveal the others.
- Nothing binds the key to the context it was derived for, which is what enables cross-protocol reuse attacks.
HKDF: extract, then expand #
Krawczyk's HKDF (RFC 5869) is the standard answer, and its contribution is separating two distinct operations that ad-hoc KDFs conflate:
Extract takes non-uniform input keying material (IKM) and condenses it into a uniformly random pseudorandom key (PRK). Note the inversion: the salt is the HMAC key and the secret is the message — deliberate, so that extraction is sound even when the salt is public or absent.
Expand stretches the PRK into as many output bytes as needed, using a counter and an info string.
The three inputs each do a specific job:
| Input | Required? | Purpose |
|---|---|---|
IKM |
yes | the secret being derived from |
salt |
optional but recommended | de-correlates derivations across sessions; may be public and non-secret; strengthens the extraction proof |
info |
optional but use it | binds the output to a purpose — this is the key-separation mechanism |
What TLS 1.3 does with it #
TLS 1.3 derives its entire key hierarchy from HKDF, and the design is worth studying as the reference application:
flowchart TD PSK["PSK / 0"] --> E["Extract"] --> ES["Early Secret"] ES --> D1["Derive-Secret"] --> HS0 DH["ECDHE shared secret"] --> E2["Extract"] HS0 --> E2 --> HS["Handshake Secret"] HS --> CHTS["client_handshake_traffic_secret"] HS --> SHTS["server_handshake_traffic_secret"] HS --> E3["Extract"] --> MS["Master Secret"] MS --> CATS["client_application_traffic_secret"] MS --> SATS["server_application_traffic_secret"] MS --> RMS["resumption_master_secret"]
Two properties fall out of this structure. Every derivation runs HKDF-Expand-Label, which mixes in a label and the handshake transcript hash — so keys are bound not just to a purpose but to the exact negotiated parameters, which kills whole classes of downgrade and cross-protocol attack. And the chained extracts mean a compromise of one stage's secret doesn't hand over the others.
Choosing and using #
- Deriving several keys from a strong secret → HKDF-SHA256. If the input is already uniform (a random 32-byte master key),
HKDF-Expandalone suffices. - FIPS-constrained → NIST SP 800-108 KDFs in counter mode with HMAC or CMAC.
- From a password → never HKDF. Use Argon2id; see password storage.
- From a DH/ECDH output → HKDF-Extract with the handshake transcript as salt, then Expand per key.
- Modern all-in-one → BLAKE3 has a native
derive_keymode with a domain-separation context string, and is fast enough to make the extract/expand distinction moot for most uses.
Sources
- docsRFC 5869 — HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
- paperCryptographic Extraction and Key Derivation: The HKDF Scheme (Krawczyk, 2010)
- docsRFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3
- docsNIST SP 800-108 Rev. 1 — Recommendation for Key Derivation Using Pseudorandom Functions