One Secret Is Not One Key: The Discipline of Key Derivation with HKDF
A raw DH or ML-KEM shared secret is key material, not a key. How HKDF extract-then-expand, key separation, and the TLS 1.3 key schedule get it right.
Permalink1. One Secret Is Not One Key
You have just finished a Diffie-Hellman exchange -- or, in 2026, an ML-KEM encapsulation -- and you are holding a shared secret that no eavesdropper can compute [6]. The obvious next move is to use it as your AES-256 key. That move is wrong, and it is wrong twice over. Those two reasons are the entire reason key derivation exists.
Reason one: the secret may not be uniform. A Diffie-Hellman output is a structured group element -- an integer of a particular form living in a particular subgroup, not a uniformly random string of bytes. A block cipher and a MAC both expect their key to be indistinguishable from random bits, and a raw group element simply is not that. RFC 5869 says it plainly: a Diffie-Hellman value "is NOT a uniformly random or pseudorandom string," so the extraction step "SHOULD NOT be skipped" [1]. TLS 1.3 carries the same warning into its own design, never keying anything directly from the raw (EC)DHE value [2].
Reason two: one protocol needs many keys. A single connection almost never wants one key. TLS wants a client write key and a server write key, an initialization vector for each direction, an exporter secret, a resumption secret, and fresh keys after every update [2]. If you carve all of those out of one secret naively -- or worse, reuse the same key for encryption and authentication -- a weakness in one use leaks into the others. The keys must be independent: learning one must tell an attacker nothing about the rest.
A KDF is a function that turns input keying material (IKM) -- a shared secret, a master key, or other high-entropy input -- into one or more output keys that are indistinguishable from random and independent of one another. It is the disciplined replacement for "just hash the secret."
Put the two reasons together and you get the sentence this entire article defends.
One secret is not one key. A raw shared secret is material: possibly non-uniform, and dangerous to use for more than one purpose. Key derivation is the discipline of turning that one secret into a hierarchy of keys that are each uniform and mutually independent.
Diagram source
flowchart LR
S["One shared secret (DH or ML-KEM)"] --> KDF["Key derivation"]
KDF --> K1["Client write key"]
KDF --> K2["Server write key"]
KDF --> K3["Client IV"]
KDF --> K4["Server IV"]
KDF --> K5["Exporter secret"]
KDF --> K6["Resumption secret"] Everything that follows falls out of that picture as three rules. Rule one: do not skip Extract on a non-uniform secret. Rule two: do not reuse a derived key across contexts -- give each its own separation. Rule three: do not bind too little context, or two different situations will collapse to the same key.
Almost every famous break in this space -- the Triple Handshake attack [4], the Selfie reflection [5], and the plain mistake of using a raw secret as a key [1] -- is the violation of exactly one of these three rules. We will name each one in the catalog at the end.
One scope boundary before we start. This article is about the high-entropy branch: secrets with real randomness in them, such as key-exchange outputs and master keys. Low-entropy inputs -- passwords and PINs -- live on a different branch (Part 12 of this series, on password hashing) for a reason we will make precise in the theory: Extract can concentrate the entropy already present, but it cannot invent entropy that was never there [1].
If the fix is a named discipline, where did it come from -- and why did the field take roughly fourteen years to get it right?
2. Key Derivation Before It Had a Name
Long before anyone wrote "KDF" on a whiteboard, every protocol still faced the same task: one secret in, many keys out. And for about fourteen years, they all reached for the same tool -- hash the secret together with a counter and a label -- without ever quite agreeing on what that tool was supposed to guarantee.
Start with SSL 3.0 around 1996, from Netscape's Alan Freier, Philip Karlton, and Paul Kocher. Its key material came from a bespoke construction that nested MD5 and SHA-1 hashes over the master secret and the two hello nonces [7]. There was no separation of concerns, no name for the two jobs it was quietly doing at once; it produced bytes, and the bytes looked random enough. TLS 1.0 openly descended from it: the RFC states it is "based on the SSL 3.0 Protocol Specification as published by Netscape" [8].
Diagram source
timeline
title Key derivation from 1996 to 2026
1996 : SSL 3.0 bespoke MD5 and SHA-1 key block
1999 : TLS 1.0 PRF, P_MD5 XOR P_SHA-1, feedback mode
2009 : NIST SP 800-108 expand-only PRF modes
2010 : HKDF extract-then-expand, RFC 5869 and CRYPTO 2010
2018 : TLS 1.3 staged key schedule
2020 : NIST SP 800-56C two-step converges on HKDF
2024 : ML-KEM standardized, PQ hybrids ship
2026 : NIST revises SP 800-56C for KEM secrets and KMAC In January 1999, Tim Dierks and Christopher Allen gave the pattern its first careful specification: the TLS 1.0 pseudorandom function. It split the secret into two halves and computed P_MD5(S1, label + seed) XOR-ed with P_SHA-1(S2, label + seed), where each P_hash is an HMAC-driven feedback-mode expander (A(i) = HMAC(secret, A(i-1))) [8]. The design hedged its bets across two hash functions so the result stayed secure if either one held.
It was the first mass-deployed protocol KDF -- and it was expand-only in spirit: it stretched a secret into blocks with a label and a feedback chain, with no explicit extraction step and no clean argument for what happens when the input is a structured Diffie-Hellman value rather than an already-random key.
A PRF is a keyed function whose outputs are indistinguishable from those of a truly random function, to anyone who does not hold the key. HMAC is the workhorse PRF of this whole story; Part 11 of this series covers it in depth. A KDF's expansion step is essentially a PRF chained in feedback mode, driven by a counter.
The same shape appeared in the public-key world. The ANSI X9.63 and IEEE 1363 working groups specified KDF1- and KDF2-style functions that produced keying material by hashing an (elliptic-curve) Diffie-Hellman shared secret together with a counter and some shared information [9]. RFC 5869 later named IEEE 1363a-2004 -- alongside NIST SP 800-56A and SP 800-108 -- among the schemes that "do not explicitly differentiate between the 'extract' and 'expand' stages, often resulting in design shortcomings" [1]. The ANS X9.63 standard itself sits behind a paywall, but SEC 1 v2.0 (section 3.6.1) documents the identical construction openly as ANSI-X9.63-KDF: Ki = Hash(Z || Counter || SharedInfo) over the shared secret Z [9]. Treat ANSI X9.63 as reported prior art of that "hash the secret with a counter and shared info" shape, not a scheme RFC 5869 names by title (the RFC names IEEE 1363a-2004, not X9.63).
Then NIST codified its own line: a concatenation KDF in SP 800-56A [10], and, in 2009, Lily Chen's SP 800-108 with its counter, feedback, and double-pipeline modes for deriving keys from an already-uniform key [11].
Look across all of them -- SSL 3.0, the TLS PRF, X9.63, IEEE 1363, SP 800-56A, SP 800-108 -- and one shape repeats: hash the secret with a counter and a label. So does one blind spot: no explicit Extract step, and no honest argument for a non-uniform input, because every design implicitly treated the hash as if it were a perfectly random function.
The two-hash hedge did not survive. TLS 1.2 replacedP_MD5 XOR P_SHA-1 with a single SHA-256 PRF, declaring that "this PRF with the SHA-256 hash function is used for all cipher suites" [12]. The hedge added complexity without giving the modularity a real Extract/Expand split would later provide.
These constructions shipped, and they protected real traffic for years without a headline disaster. So what, mechanically, was missing? Why could nobody prove they were sound?
3. Why "Just Hash the Secret" Cannot Be Proven Safe
Zoom in on the mechanics of the antipattern everyone kept reinventing: take the secret, hash it with a counter and a label, use the bytes. Two silent failures hide inside it, and one of them is the kind of bug you can never catch in a test -- only in a proof.
Failure one: a non-uniform input with no proof. Suppose your secret is a Diffie-Hellman value . It is unpredictable to an attacker, yes, but it is not uniform: it is an element of a specific group, with algebraic structure an adversary knows about. When you feed it straight into a hash and declare the output "random," you are making an assumption you cannot back up. There is no standard-model argument that H(g^{xy}) is indistinguishable from a random string. The only way the old designs "work" is if you pretend the hash is a perfect random oracle. Krawczyk put the indictment precisely: in practice most KDFs, including widely standardized ones, "follow ad-hoc approaches that treat cryptographic hash functions as perfectly random functions" [13].
Min-entropy measures a source's worst-case unpredictability. If its most likely value occurs with probability , its min-entropy is . This -- not the friendlier Shannon entropy -- is the quantity that governs extraction, because security must hold against an attacker who guesses the single likeliest value first.
Failure two: no context, so silent reuse. The homemade construction has nowhere to say what this key is for. With no distinct label bound into the derivation, the same secret hashed the same way yields the same bytes, and nothing stops a program from using those bytes for two different purposes. That is not a dramatic bug you can see; it is a latent precondition, the quiet setup behind attacks we will name later.
A randomness extractor is a function that takes a source with enough min-entropy but a non-uniform distribution and produces output statistically close to uniform. It is the formal object the "Extract" half of a KDF is supposed to be -- and precisely what "hash it and hope" fails to provide any guarantee of being.
There is also a concrete, testable hazard in the naive H(secret || info) form, separate from the proof problem.
So why not fix it with a better hash, or more rounds? Because the real problem is not the hash function -- it is that a single step is being asked to do two different jobs with two different security requirements, and being proven for neither. Concentrating dispersed entropy into a uniform key is one job. Stamping out many independent context-separated keys is another.
The first of those two jobs even carries a measurable cost: concentrating entropy by purely statistical means is lossy -- the theory section later makes that tax precise -- which is exactly why the eventual fix used a computational extractor, keyed and analyzed under a computational assumption rather than a purely information-theoretic one [13].
If hashing-with-a-counter cannot be proven sound for a non-uniform secret, the field did not need a different hash. It needed a different mental model. In 2010, one paper supplied it.
4. Krawczyk's Split: Two Jobs, Two Proofs
The insight that reorganized the field is almost embarrassing to state once you see it: the two jobs everyone had been doing at once are actually two different jobs, and they need two different proofs. In 2010 Hugo Krawczyk named them, separated them, and proved each -- and in doing so converted fourteen years of ad-hoc practice into a deployable primitive.
The first job is Extract. Treat it as a keyed computational randomness extractor: it takes the possibly non-uniform input keying material -- a Diffie-Hellman element, or a hybrid secret whose classical half is non-uniform -- and concentrates whatever entropy is dispersed inside it into one uniform pseudorandom key. Its defining limitation is the honest one from the previous section: it concentrates entropy, it cannot create it.
The second job is Expand. Treat it as a variable-length PRF: given that one uniform key, it stamps out arbitrarily many output keys, each independent of the others because each is bound to a distinct context string. The independence is not a hope; it is the PRF property, applied to distinct inputs.
The two-stage structure at the heart of HKDF. Extract concentrates a non-uniform secret's entropy into a single uniform pseudorandom key; Expand then derives many independent keys from that one key, each bound to a distinct context label. Splitting the derivation in two is what lets each half be defined, and proven, on its own terms.
An extractor whose output need only be computationally indistinguishable from uniform, rather than statistically close to it. Keyed by the salt and analyzed under HMAC's pseudorandomness, it avoids the Leftover Hash Lemma's entropy tax -- at the price of resting on a computational assumption instead of an information-theoretic one.
The engineering move that made this practical was refusing to invent two new gadgets. Both stages are instantiated with the same primitive: HMAC. One construction, two roles. Extract is PRK = HMAC(salt, IKM); Expand is HMAC chained over its previous output (feedback mode, counter-stepped). Because HMAC was already everywhere, HKDF cost implementers essentially nothing new to adopt (Part 11 of this series treats HMAC as its own subject).
Diagram source
flowchart TD
IKM["IKM -- the raw shared secret"] --> EX["HKDF-Extract, a computational extractor"]
SALT["salt (optional, extract-time)"] --> EX
EX --> PRK["PRK -- one uniform pseudorandom key"]
PRK --> EXP["HKDF-Expand, a variable-length PRF"]
INFO["info -- a distinct context per key"] --> EXP
EXP --> OKM["OKM -- many independent subkeys"] Two artifacts landed in 2010, weeks apart, and it is worth keeping them distinct. The interface is RFC 5869, "HMAC-based Extract-and-Expand Key Derivation Function," by Krawczyk and Pasi Eronen, published that May, whose stated goal was "to discourage the proliferation of multiple KDF mechanisms" [1]. The theory is the CRYPTO 2010 paper, which supplied what the field had never had.
"...the first general and rigorous definition of KDFs and their security which we base on the notion of computational extractors." -- Hugo Krawczyk, "Cryptographic Extraction and Key Derivation: The HKDF Scheme," CRYPTO 2010 [13]
That is the whole reframing in one line. A KDF is not a fancy hash; it is an extractor followed by a PRF, and each has a definition and a proof. The paper analyzes the HMAC construction "based on the extraction and pseudorandom properties of HMAC," and is careful about "minimizing these assumptions as much as possible for each usage scenario" [13].
Here is the corroboration that it was a real discovery and not one team's preference: NIST arrived at the same structure independently. SP 800-56C Rev. 2, by Elaine Barker, Lily Chen, and Richard Davis (2020), standardizes a two-step "extraction-then-expansion" key-derivation method -- the identical shape -- for key establishment [15], while SP 800-108 remains the expand-only sibling for inputs that are already uniform [11].
Two lineages, one design. When the IETF and academic line and the NIST line converge on the same two-step structure from different starting points, that is the signature of having found something true rather than merely convenient.
A design and a proof are worth nothing until you can actually call the thing. What does the construction look like in code, and what, exactly, are salt and info for?
5. Extract, Then Expand: The Deployable Primitive
Here is the whole thing, worked. Two function calls you could implement this afternoon, and two knobs that do all the work.
Extract is one line. The salt becomes the HMAC key, and the secret becomes the message:
If you have no salt, RFC 5869 says use a string of HashLen zero bytes [1]. The output is one uniform pseudorandom key.
The uniform, pseudorandom key that Extract produces from the input keying material. It is HashLen bytes long (32 for SHA-256) and is the sole keying input to every subsequent Expand call. Everything downstream depends only on the PRK, never on the raw secret again.
Expand is an iterated (feedback-mode) PRF: it chains HMAC over its own previous output block, the context string, and a single incrementing counter byte, then takes the first L bytes:
Diagram source
flowchart TD
PRK["PRK"] --> T1["T1 = HMAC of PRK over info and byte 0x01"]
T1 --> T2["T2 = HMAC of PRK over T1, info, byte 0x02"]
T2 --> T3["T3 = HMAC of PRK over T2, info, byte 0x03"]
T1 --> OKM["OKM = first L bytes of T1 then T2 then T3"]
T2 --> OKM
T3 --> OKM That single incrementing byte is why the output has a hard ceiling: [1]. Ask for more and the counter would overflow a byte. The counter starts at 1, not 0, so the largest block index is 255 -- hence the factor of 255 rather than 256.
The pseudorandom bytes Expand produces, which the caller slices into the actual working keys. A single derivation is capped at bytes; longer needs cannot be met by asking for a bigger L.
None of this needs to stay abstract. The code below is HKDF built from nothing but HMAC-SHA-256, run on RFC 5869's own first test vector, so you can check the output against the standard.
// HKDF (RFC 5869) built from HMAC-SHA-256, run on the RFC's own Test Vector 1.
const toHex = (buf) =>
Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
const hexToBytes = (h) => new Uint8Array(h.match(/../g).map((x) => parseInt(x, 16)));
async function hmac(keyBytes, msgBytes) {
const key = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
return new Uint8Array(await crypto.subtle.sign('HMAC', key, msgBytes));
}
// Extract: concentrate the (non-uniform) IKM into one uniform PRK.
async function hkdfExtract(salt, ikm) { return hmac(salt, ikm); }
// Expand: T(i) = HMAC(PRK, T(i-1) || info || byte(i)); OKM = first L bytes.
async function hkdfExpand(prk, info, L) {
const N = Math.ceil(L / 32); // SHA-256 output is 32 bytes
let t = new Uint8Array(0);
let okm = new Uint8Array(0);
for (let i = 1; i <= N; i++) { // L must stay <= 255 * 32 = 8160 bytes
const block = new Uint8Array(t.length + info.length + 1);
block.set(t, 0);
block.set(info, t.length);
block.set([i], t.length + info.length);
t = await hmac(prk, block);
const merged = new Uint8Array(okm.length + t.length);
merged.set(okm, 0); merged.set(t, okm.length);
okm = merged;
}
return okm.slice(0, L);
}
(async () => {
const ikm = hexToBytes('0b'.repeat(22));
const salt = hexToBytes('000102030405060708090a0b0c');
const info = hexToBytes('f0f1f2f3f4f5f6f7f8f9');
const prk = await hkdfExtract(salt, ikm);
const okm = await hkdfExpand(prk, info, 42);
console.log('PRK =', toHex(prk)); // expect 077709362c2e32df0ddc3f0dc47bba63...
console.log('OKM =', toHex(okm)); // expect 3cb25f25faacd57a90434f64d0362f2a...
})(); Press Run to execute.
What that code prints (RFC 5869 Test Vector 1)
PRK = 077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5 and OKM = 3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865 -- byte for byte the values published in RFC 5869 Appendix A [1]. If your implementation prints these two lines, your Extract and Expand are correct.
Now the two knobs. Salt is an extract-time input; RFC 5869 says a salt "adds significantly to the strength of HKDF," and although it is optional it is recommended when you have one [1]. Info is an expand-time input, and it is the load-bearing one: distinct info strings give you independent keys for free, because RFC 5869 designed info precisely so that it "may prevent the derivation of the same keying material for different contexts" [1].
A non-secret value used as the HMAC key during Extract. It is optional but recommended; RFC 5869 states it "adds significantly to the strength of HKDF." Unlike a nonce, a salt is fixed and reusable -- the same salt across many sessions is fine.
Binding a distinct context label into each derivation so that keys for different purposes are cryptographically independent, even though they descend from one secret. In HKDF this is exactly the job of the info string.
Run the separation yourself. From one PRK, two different info strings yield unrelated keys, and -- the part worth internalizing -- the same info yields the same key, which is the misuse made visible.
// Domain separation: one PRK, different info -> independent keys; same info -> identical.
const toHex = (buf) =>
Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
const hexToBytes = (h) => new Uint8Array(h.match(/../g).map((x) => parseInt(x, 16)));
const bytes = (s) => new TextEncoder().encode(s);
async function hmac(k, m) {
const key = await crypto.subtle.importKey(
'raw', k, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
return new Uint8Array(await crypto.subtle.sign('HMAC', key, m));
}
async function hkdfExpand(prk, info, L) {
const N = Math.ceil(L / 32);
let t = new Uint8Array(0), okm = new Uint8Array(0);
for (let i = 1; i <= N; i++) {
const block = new Uint8Array(t.length + info.length + 1);
block.set(t, 0); block.set(info, t.length); block.set([i], t.length + info.length);
t = await hmac(prk, block);
const merged = new Uint8Array(okm.length + t.length);
merged.set(okm, 0); merged.set(t, okm.length); okm = merged;
}
return okm.slice(0, L);
}
(async () => {
// Pretend we already ran Extract and hold this PRK.
const prk = hexToBytes('077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5');
const encKey = await hkdfExpand(prk, bytes('app enc key'), 32);
const macKey = await hkdfExpand(prk, bytes('app mac key'), 32);
const encKey2 = await hkdfExpand(prk, bytes('app enc key'), 32); // same info as encKey
console.log('enc =', toHex(encKey));
console.log('mac =', toHex(macKey));
console.log('distinct info -> independent keys? ', toHex(encKey) !== toHex(macKey));
console.log('same info -> identical keys? ', toHex(encKey) === toHex(encKey2));
})(); Press Run to execute.
People constantly confuse salt with a nonce. They are different animals, and so is info. Keeping the three straight prevents a whole class of bugs.
| Input | What it is | When it acts | Reuse rule | Cost of getting it wrong |
|---|---|---|---|---|
salt | Extractor strengthener (the HMAC key in Extract) | Extract time | Fixed, reusable across sessions | Weaker extraction, but not catastrophic |
info | Domain / key-separation label | Expand time | Distinct per derived key | Two keys collide -> reuse across contexts |
| nonce | Per-message uniqueness for the AEAD | Encryption time | Never repeat under one key | Catastrophic cipher failure -- and not a KDF input at all |
Part 2 of this series covers salt and nonce generation; the one thing to carry here is that a salt is safe to hardcode per protocol, while a nonce never is.
When may you skip Extract? Only when the input is already a uniform key -- an existing symmetric master key, or a PRK you extracted earlier. RFC 5869 has a section literally titled "To Skip or not to Skip," and the rule is exactly the entropy question: uniform input, Extract is wasted work; non-uniform input such as a Diffie-Hellman value, Extract "SHOULD NOT be skipped" [1].
One more property, easy to miss now and load-bearing later: because Extract is HMAC(salt, IKM), the salt and the secret both flow through HMAC, and HMAC can be argued secure even when the fresh secret rides in on the message (IKM) side while the salt keys HMAC -- the swap, or dual-PRF, direction. That "either input can carry the security" behavior -- the dual-PRF intuition -- is what lets a post-quantum secret be bolted onto a classical one without a redesign. Hold that thought.
One Extract-and-Expand pair is a primitive. A real protocol needs a whole hierarchy -- many keys, chained and context-bound, across an entire handshake. What does that look like at production scale?
6. How HKDF Is Wired Today: The TLS 1.3 Key Schedule
The primitive froze in 2010 and has not been superseded. Everything called "state of the art" since is about wiring -- how you chain one Extract-and-Expand pair into an entire hierarchy without a separation bug. The flagship wiring is a diagram every TLS implementer has stared at: the TLS 1.3 key schedule, defined in RFC 8446 section 7.1 by Eric Rescorla in 2018, building on the OPTLS design of Krawczyk and Wee [2] [16].
TLS 1.3 first defines two thin wrappers over HKDF. HKDF-Expand-Label is just HKDF-Expand with the label prefixed by the string tls13 , and Derive-Secret is HKDF-Expand-Label with the transcript hash of the handshake messages as its context [2]. Then it chains a sequence of HKDF-Extract calls, each using the previous secret as the salt and a new secret as the IKM:
Diagram source
flowchart TD
Z0["0"] --> E1["HKDF-Extract"]
PSK["PSK"] --> E1
E1 --> ES["Early Secret"]
ES --> B1["Derive-Secret: c e traffic (binds ClientHello)"]
ES --> D1["Derive-Secret: derived"]
D1 --> E2["HKDF-Extract"]
DHE["(EC)DHE shared secret"] --> E2
E2 --> HS["Handshake Secret"]
HS --> B2["Derive-Secret: s hs traffic (binds CH to SH)"]
HS --> D2["Derive-Secret: derived"]
D2 --> E3["HKDF-Extract"]
Z1["0"] --> E3
E3 --> MS["Master Secret"]
MS --> B3["Derive-Secret: s ap traffic (binds CH to server Finished)"]
MS --> B4["Derive-Secret: res master (binds CH to client Finished)"] Read what that structure buys. The chain Extracts on the non-uniform (EC)DHE value, so rule one is satisfied. Derive-Secret's label separates every key -- c hs traffic is not s hs traffic, so client and server never share a key -- satisfying rule two. And every secret binds a transcript hash, so two handshakes with different Hello randoms can never derive the same traffic keys, satisfying rule three [2].
Key updates ratchet forward with an HKDF-Expand-Label using the label traffic upd [2]. The final working keys come from HKDF-Expand-Label(Secret, "key", ...) and HKDF-Expand-Label(Secret, "iv", ...), and those feed the AEAD cipher -- AES-GCM or ChaCha20-Poly1305, the subject of Part 7 [2].
The RFC states the article's whole thesis in its own words, describing the shape of its own diagram.
"...the secrets shown down the left side of the diagram are just raw entropy without context, whereas the secrets down the right side include Handshake Context and therefore can be used to derive working keys without additional context." -- RFC 8446, section 7.1 [2]
Left side, raw entropy; right side, entropy plus bound context. That is Extract and Expand, drawn as a protocol.
The same staged-Extract, labeled-Expand pattern reappears everywhere HKDF is deployed: HPKE (RFC 9180), MLS, the Noise framework behind WireGuard [17], and Signal, whose X3DH keys HKDF with a zero-filled salt and an application-identifier context [18] and whose Double Ratchet builds forward-secure KDF chains from it [19].The post-quantum front changed the input, not the primitive
The genuinely new development of 2024 to 2026 is not a KDF. It is what gets fed into HKDF-Extract. To resist "harvest now, decrypt later," protocols now run a classical and a post-quantum key exchange side by side and combine the two shared secrets.
The recommended hybrid group [20], X25519MLKEM768, concatenates the ML-KEM-768 secret from FIPS 203 [6] and then the classical X25519 secret, and hands the pair to the existing TLS 1.3 HKDF-Extract as its (EC)DHE input [3]. Order matters for interop: the bytes are ML-KEM || X25519, and the group name deliberately lists the two components in the reverse order for historical reasons [21].
Diagram source
flowchart LR
M["ML-KEM-768 shared secret"] --> CAT["Concatenate: ML-KEM then X25519"]
X["X25519 shared secret"] --> CAT
CAT --> IKM["Hybrid IKM, a longer secret"]
IKM --> EX["The same TLS 1.3 HKDF-Extract"]
EX --> HS["Handshake Secret, schedule unchanged"] No new KDF. No new schedule. The hybrid secret is just a longer IKM, and the security goal is "secure if either component is secure" -- the dual-PRF payoff foreshadowed in the previous section [3]. This is the entire reason extract-then-expand aged so well: a brand-new kind of secret slotted in without touching the primitive.
IANA assignedX25519MLKEM768 the codepoint 4588, that is 0x11EC, marked Recommended, and retired the earlier X25519Kyber768Draft00 at 0x6399 as obsolete [20].
How much of this is actually running? Time-stamp it, because it moves fast. Cloudflare reported that in early 2024 "nearly two percent of all TLS 1.3 connections" were post-quantum-secured, and expected "double-digit adoption by the end of 2024" [22]; Google documented Chrome's own migration to ML-KEM on the web [23]. Treat the two-percent figure as a dated snapshot, not a steady state.
The standards body is now catching up to the deployment. In January 2026 NIST announced it will revise SP 800-56C Rev. 2 to let the shared secret incorporate a secret from an approved KEM, to allow flexible formatting of hybrid shared secrets, and to approve KMAC "for the randomness extraction step (in addition to the expansion step)" [24].
TLS 1.3 is one superb design point. But HKDF is not the only KDF, and it is not always the right one. Given your input and your constraints, which do you actually reach for?
7. Choosing a KDF: The Design Space and a Decision Matrix
HKDF is not a monopoly. It is one answer to two orthogonal questions: does your input need extraction? and what is your compliance and library situation? Answer those two and the choice mostly makes itself. The methods below do not really race one another; they occupy different cells of an input-by-environment grid.
The extract-then-expand family handles non-uniform, high-entropy secrets: HKDF [1] and the NIST SP 800-56C two-step method [15]. The expand-only family assumes the input is already uniform and skips extraction: SP 800-108's counter, feedback, and double-pipeline modes over HMAC or KMAC [11], and libsodium's crypto_kdf over BLAKE2b [25]. A single-primitive school argues you never need a separate Extract step if the primitive earns separation structurally: KMAC and cSHAKE from a Keccak sponge [26], and BLAKE3's derive_key, which takes a mandatory hardcoded context string [27]. And SP 800-56C's one-step method is the "one call" middle ground [15].
| Method | Core primitive | Explicit Extract? | Non-uniform input? | Separation input | FIPS-approved | Best suited for |
|---|---|---|---|---|---|---|
| HKDF (RFC 5869) | HMAC | Yes | Yes | info | HMAC approved, cited in NIST guidance | DH/KEM secrets, protocol schedules |
| SP 800-56C two-step | HMAC or KMAC | Yes | Yes | FixedInfo | Yes | FIPS key establishment |
| SP 800-56C one-step | Hash / HMAC / KMAC | Folded into one call | Yes, with care | FixedInfo | Yes | FIPS, one-call ECDH |
| SP 800-108 (expand-only) | HMAC or KMAC | No | No, extract first | Label / Context | Yes | Subkeys from a uniform key |
| KMAC-as-KDF (SP 800-185) | Keccak sponge | Implicit (sponge) | Yes | customization string | Yes (SHA-3) | SHA-3 stacks, built-in separation |
BLAKE3 derive_key | BLAKE3 tree hash | Implicit (tree) | Yes | context | No | High-throughput, non-FIPS |
libsodium crypto_kdf | BLAKE2b | No | No, extract first | 8-byte context | No | Ergonomic subkeys from a master key |
The load-bearing row is "non-uniform input." If your secret is a raw Diffie-Hellman value, or a hybrid secret whose classical half is non-uniform, you need a real extractor -- HKDF, the 56C two-step, or a sponge or tree that doubles as one -- not a bare expander such as SP 800-108 or crypto_kdf, which assume you already hold a uniform key. A stand-alone ML-KEM secret is already uniform, so extraction there is a harmless safe default rather than a necessity. Everything else in the table is a secondary tiebreak.
crypto_kdf can derive up to subkeys from a single master key and context, each between 128 and 512 bits, using an 8-byte string context plus a 64-bit subkey_id for separation [25]. It is delightful for subkeys -- and exactly wrong for a raw DH secret, because it never extracts.
So the decision rules compress to three lines. A uniform key and you need subkeys, reach for expand-only (HKDF-Expand, SP 800-108, KMAC, or crypto_kdf). A non-uniform high-entropy secret such as a Diffie-Hellman output, or a hybrid whose classical half is non-uniform, reach for extract-then-expand (HKDF or the 56C two-step). A low-entropy password, reach for a password hash (Argon2id), never a KDF from this table. Then the tiebreaks: need FIPS, use SP 800-56C or 800-108; on a SHA-3 stack, use KMAC; outside FIPS and chasing raw throughput on many long keys, use BLAKE3.
That last branch -- passwords -- deserves care, because it is where the worst mistakes happen and where terminology misleads people.
Honesty about performance: these constructions do not meaningfully compete on speed. Qualitatively, the KDF's cost is dominated by the surrounding public-key operation -- computing an X25519 or ML-KEM shared secret is orders of magnitude more work than the handful of HMAC compressions HKDF adds on top, as public cycle-count benchmarks for these primitives make plain [28]. Selection is driven by input assumptions, compliance, and library availability, not throughput.
Every method here, however clever, obeys the same wall: you can concentrate entropy but you can never create it. That conservation law -- and what it forbids -- is the theory underneath all of it.
8. You Cannot Create Entropy
The single most important result in this field is not an algorithm. It is a conservation law.
"The extract step in HKDF can concentrate existing entropy but cannot amplify entropy." -- RFC 5869, section 4 [1]
Read literally, that sentence is the border of the entire subject. If the source IKM carries bits of min-entropy, then no KDF -- however clever, however many rounds, however long its output -- yields more than about bits of key security [1]. A source with 20 bits of entropy cannot become a 128-bit key by any extractor that has ever been or will ever be designed.
This is exactly the wall between high-entropy KDFs and password hashing, and it explains why they are different branches. A password hash does not break this bound either. It cannot add entropy. What it adds is cost per guess: a salt to defeat precomputation, and deliberate slowness or memory-hardness to make each of the attacker's guesses expensive. The entropy of a weak password is still low after hashing; the attacker just pays more per attempt. Confusing "make guessing expensive" with "add entropy" is the conceptual error behind running HKDF on a password.
The conservation law also explains the shape of the 2010 fix. Purely statistical extraction is lossy: the classical Leftover Hash Lemma says that to land within statistical distance of uniform you pay roughly bits of the source as overhead [29]. A computational extractor -- HMAC keyed by the salt -- sidesteps that tax, delivering full-length pseudorandom keys under a computational assumption instead of an information-theoretic one [13]. You trade a little theoretical purity for not throwing away entropy you cannot spare.
So much for the lower bound. What is the upper bound -- what can the best construction actually promise? At the primitive level, HKDF is a secure KDF if and only if HMAC is simultaneously a good computational extractor (for Extract) and a good variable-length PRF (for Expand) [13]. A real gap in that story was closed only recently: the 2023 dual-PRF analysis proved HMAC is a PRF for keys of arbitrary length, in the standard model, with good multi-user bounds [30].
And at the schedule level -- the level that actually matters for a deployed protocol -- Fischlin and Gunther proved, in a multi-stage key-exchange model, that the TLS 1.3 handshake modes "establish session keys with their desired security properties under standard cryptographic assumptions" [31]. That is the current ceiling on what we can prove about a whole deployed key schedule, not just one KDF call.
A function that stays a secure PRF whether it is keyed through its normal key input or through its message input. HMAC is relied on as a dual-PRF wherever a fresh secret arrives in the message slot -- as in the post-quantum hybrids of the previous section. The 2023 analysis shows this holds only for key sets satisfying a "feasibility" condition, which the authors argue real applications meet [30].
The honesty this section owes: the theory is settled at the primitive level but has live gaps at the edges. Some HKDF arguments are cleanest with a salt and standard-model assumptions, yet salt-less HKDF -- the common case, since TLS uses all-zero salts at several stages -- leans on stronger, more idealized assumptions [1]. The community shorthand for the danger of over-modeling is blunt.
What would the ideal KDF be? A random oracle keyed by the secret: a black box that, for each distinct pair of secret and context, returns fresh independent uniform bits, with security equal to the secret's min-entropy and zero leakage across contexts. True random oracles do not exist, so that ideal is not reachable in the standard model. But extract-then-expand, with a distinct info per key, is the closest deployable approximation of it -- Extract gives "uniform from non-uniform" up to the entropy bound, and Expand-with-context gives "independent per query" up to PRF security.
No KDF yields more key security than the min-entropy already in its input. That one conservation law is the entire border between HKDF and password hashing, and no amount of stretching moves it. Independence is not free either: if two derivations share a secret and bind the same or empty context, their outputs are identical -- which is not a theoretical nicety but exactly how the Triple Handshake and Selfie breaks happened.
The primitive-level theory is essentially finished. So where is the real frontier? Not in the KDF box -- in everything wired around it.
9. Open Problems at the Wiring Layer
Here is the tell that tells you where the frontier is: every named break in this space was a schedule-level bug, not a primitive-level one. The KDF itself is proven. The genuinely open, hard problems all live in how we wire it and how we prove the wiring.
How safe is plain concatenation as a post-quantum combiner? Deployed hybrids concatenate the classical and post-quantum shared secrets and feed the result to the existing HKDF-Extract [3]. Is that generic construction always as good as a purpose-built combiner? Generic KEM-combiner theory gives black-box results -- the combined KEM is secure "as long as at least one of the ingredient KEMs is" [32]. The dedicated route, X-Wing, earns a tighter proof and better efficiency, but only because it fixes X25519, ML-KEM-768, and SHA3-256 concretely; its guarantees "may not apply in the general case" [33]. What is still missing is a clean, general characterization of when concatenation is safe -- covering KEM binding, ciphertext collisions, and how those interact with the rest of the schedule -- versus when a dedicated combiner is required. This is not academic: it guards a growing double-digit share of internet traffic during the migration.
X-Wing's efficiency and tight proof are specific to its exact primitive choices, so they should not be read as a general statement about concatenation. Its author list is deliberately not asserted here: the source page renders none, and prior search results were inconsistent [33].The pending NIST revision is announced, not published. The January 2026 planning note says NIST will revise SP 800-56C to admit a KEM secret, allow flexible hybrid formatting, and approve KMAC as an extraction step [24]. Until the document actually lands, the exact approved formatting for hybrid secrets and the KMAC-as-extractor parameters are unsettled.
Nobody has machine-checked an entire modern key schedule. We can prove a great deal about one HKDF call; proving the whole hierarchy of a real protocol -- every stage, every label, resumption, 0-RTT, key updates, external PSKs -- is far harder, and symbolic and computational proofs cover different threat models. Symbolic analysis with Tamarin modeled all of the TLS 1.3 handshake modes and still surfaced an unexpected authentication behavior [34]; computational multi-stage proofs fix a specific model and version [31]. TLS 1.3 is among the most-analyzed protocols in history, and yet each proof pins one model and one version. The open goal is whole-schedule, machine-checked proofs that track the derivation graph -- labels, transcript binding, key separation -- for the newer hierarchies too: MLS, Signal's Double Ratchet, and the post-quantum hybrids.
Could a KDF API make misuse unrepresentable? Every real break here is a usage error the type system permitted: a skipped Extract, a missing or duplicated info, one Expand output split across two keys. Labeled wrappers push toward "you cannot derive a key without naming its purpose" -- TLS's HKDF-Expand-Label, HPKE's LabeledExtract and LabeledExpand with a suite identifier [35], BLAKE3's mandatory context [27]. But these are conventions, not enforcement: they encourage separation without preventing reuse. The open design problem is an interface -- in the type or effect system -- where forgetting the context, reusing an output, or truncating instead of re-deriving simply does not compile.
Is HMAC really a dual-PRF everywhere we now lean on it? The dual-PRF guarantee holds only for feasible key sets [30]. TLS 1.3, KEMTLS, MLS, and Noise all assume the swap direction, and the new post-quantum wirings feed novel, structured lattice secrets into the message slot. The open problem is a per-deployment or automated check that each new hybrid schedule's secrets land in a feasible set -- so the assumption is verified rather than assumed.
These are open at the research edge. But you have a protocol to ship this week. What does the whole discipline boil down to in practice?
10. A Practical Guide: What to Actually Type
Strip the theory to rules you can apply without reading a single proof. If you keep one diagram from this article, make it this one.
Diagram source
flowchart TD
START["Your input keying material"] --> Q1{"Low-entropy password or PIN?"}
Q1 -->|Yes| PW["Password hash: Argon2id (Part 12), not HKDF"]
Q1 -->|No| Q2{"Already a uniform key?"}
Q2 -->|Yes| EXPAND["Expand-only: HKDF-Expand, SP 800-108, KMAC, or crypto_kdf. Distinct label per subkey"]
Q2 -->|No, a raw Diffie-Hellman value| EXTRACT["Extract-then-expand: HKDF or SP 800-56C two-step. Do not skip Extract, salt if you have one"] That tree is the whole decision. A low-entropy password goes to Argon2id, never to HKDF, because Extract cannot manufacture the entropy a password lacks [1]. A non-uniform high-entropy secret -- a Diffie-Hellman output, or a hybrid whose classical half is non-uniform -- goes to extract-then-expand, and you must not skip Extract [1]. An already-uniform key goes to expand-only, with a distinct label for every subkey.
Once you are in the HKDF branch, a short checklist keeps you out of trouble:
- Give every derived key its own
infoor label. This is rule two, made operational. Two keys, two distinct context strings. - Use a real salt when you have one, otherwise a fixed protocol-labeled salt. The salt strengthens extraction; the one input you must never omit is the context [1].
- Respect the output ceiling. A single derivation caps at bytes [1]. Need more keys? Do more derivations with distinct labels -- never truncate one long output into two keys, and never split one Expand output across two purposes.
- Adopt a labeling convention. TLS's
Derive-Secretand HPKE'sLabeledExtractandLabeledExpandare reusable templates even outside those protocols [2] [35]. They bake domain separation into every call.
A practical recipe for the info string is to give it structure: a protocol identifier, a version, the purpose, and, where authentication depends on it, the role or identity. "myapp v1 client write" is a better context than "key1", because it cannot accidentally collide with another purpose and it says what it is.
Including a hash of the messages exchanged so far as context in a derivation, so that a derived key is valid only for one exact conversation. TLS 1.3's Derive-Secret binds a transcript hash at every stage -- which is precisely what makes the Triple Handshake attack impossible against it.
You can make the correct behavior the only behavior. The wrapper below refuses to derive a key without a purpose label -- a small, self-imposed version of the misuse-resistant API from the open problems. Run it: the unlabeled call is blocked, and the two labeled calls produce independent keys.
// A Derive-Secret-style wrapper: no purpose label, no key.
const toHex = (buf) =>
Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, '0')).join('');
const hexToBytes = (h) => new Uint8Array(h.match(/../g).map((x) => parseInt(x, 16)));
const bytes = (s) => new TextEncoder().encode(s);
async function hmac(k, m) {
const key = await crypto.subtle.importKey(
'raw', k, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
return new Uint8Array(await crypto.subtle.sign('HMAC', key, m));
}
async function hkdfExpand(prk, info, L) {
const N = Math.ceil(L / 32);
let t = new Uint8Array(0), okm = new Uint8Array(0);
for (let i = 1; i <= N; i++) {
const block = new Uint8Array(t.length + info.length + 1);
block.set(t, 0); block.set(info, t.length); block.set([i], t.length + info.length);
t = await hmac(prk, block);
const merged = new Uint8Array(okm.length + t.length);
merged.set(okm, 0); merged.set(t, okm.length); okm = merged;
}
return okm.slice(0, L);
}
// Every derived key MUST name its purpose. The info encodes protocol, version, purpose.
async function deriveKey(prk, purpose, lengthBytes) {
if (!purpose || purpose.length === 0) {
throw new Error('refusing to derive a key with no purpose label');
}
return hkdfExpand(prk, bytes('myapp v1 ' + purpose), lengthBytes);
}
(async () => {
const prk = hexToBytes('077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5');
try {
await deriveKey(prk, '', 32); // forgot the purpose -> blocked
} catch (e) {
console.log('blocked:', e.message);
}
const clientWrite = await deriveKey(prk, 'client write', 32);
const serverWrite = await deriveKey(prk, 'server write', 32);
console.log('client write =', toHex(clientWrite));
console.log('server write =', toHex(serverWrite));
})(); Press Run to execute.
You rarely need to implement any of this by hand. HKDF ships in OpenSSL (via EVP_KDF) [36] and BoringSSL (via the standalone HKDF, HKDF_extract, and HKDF_expand functions in hkdf.h) [37], in Python's cryptography [38], in RustCrypto's hkdf [39], and in Go's standard library. Go promoted HKDF into the standard library as crypto/hkdf in Go 1.24; before that it lived in golang.org/x/crypto/hkdf [40]. The move is a small signal of how settled the primitive is -- stable enough to be standard-library furniture. For the expand-only and FIPS paths, reach for the SP 800-108 and 56C key-based derivation functions in your platform's crypto library.
Every rule above exists because someone shipped its opposite. Here is the catalog of what goes wrong -- and the three-rule lens that explains all of it.
11. The Antipattern Catalog, Root-Caused
Almost every notable, well-documented break in this space is the violation of one of the three rules from the very first section. Once you see the pattern, you cannot unsee it.
| Break | Year | Root cause | Rule broken | Fix | Source |
|---|---|---|---|---|---|
| Triple Handshake | 2014 | Master secret not bound to the session transcript | Rule 3: too little context | Extended Master Secret | [4] [41] |
| Selfie | 2019 | Derived keys not bound to who is speaking | Rule 3: identity not bound | Bind role and identity | [5] |
| Raw secret as key | any | Skipped Extract on a non-uniform secret | Rule 1: do not skip Extract | Run Extract first | [1] |
| Homemade `H(secret | info)` | any | No proof, plus length extension | Rule 1, and key the hash | |
| One key for encrypt and MAC | any | One key serving two purposes | Rule 2: separate by context | Two distinct info strings | [1] |
Take the two famous ones in turn. In the Triple Handshake attack, an active attacker set up two TLS sessions so that both derived the same master secret, because that secret was not bound to the sessions' certificates and parameters -- breaking the authentication that resumption and renegotiation depend on [4]. Root cause: too little context in the derived secret. The fix, the Extended Master Secret, bound the master secret to a hash of the full handshake, and TLS 1.3 later generalized that into per-stage transcript binding [41].
In Selfie, a party using an external pre-shared key could be tricked into completing a handshake with itself -- a reflection -- because the derived keys were not bound to who was speaking [5]. Root cause: identity and role not bound. It is the same rule-three failure as Triple Handshake, wearing the costume of identity rather than transcript.
And the third break needs no research paper, because it is the antipattern from the opening: use the raw Diffie-Hellman or ML-KEM output directly as a key. For a Diffie-Hellman value that is skipping Extract, which RFC 5869 names and forbids in one clause; a uniform ML-KEM secret is still single-use, so using it directly collapses every purpose onto one key [1].
The rest of the misuse list is the same handful of errors: a homemade H(secret || info) that has no proof and leaks by length extension; reusing one derived key for both encryption and its MAC, or splitting one Expand output across two purposes; a missing or duplicate info that silently collides two keys; running HKDF on a password; confusing a fixed salt with a per-message nonce; and truncating a longer output rather than re-deriving with the right length and label [1].
Frequently asked questions
Can I use my Diffie-Hellman or ML-KEM shared secret directly as an AES key?
No. A Diffie-Hellman output is non-uniform; an ML-KEM output is already uniform but single-use; neither is a finished key. Run HKDF-Extract to get a uniform PRK, then Expand a distinct key for each use with its own info string. In the deployed X25519MLKEM768 hybrid the concatenated secret still needs Extract, because its classical X25519 half is non-uniform [1].
Isn't hashing the secret with a context label a perfectly good KDF?
No. A homemade H(secret || context) has no security proof for a non-uniform input, and with a Merkle-Damgard hash such as SHA-256 it invites length-extension attacks. Use HMAC-based HKDF instead; Part 10 covers the hashing details [1].
HKDF, PBKDF2, Argon2 -- aren't these all KDFs? Why not use one everywhere?
They are one family split by input entropy, and the error is directional. Use HKDF for high-entropy secrets such as DH and KEM outputs; use Argon2id for passwords. Never run HKDF on a password -- Extract cannot amplify entropy -- and never run a slow, memory-hard password hash on a high-entropy DH or KEM secret [1].
Do I really need a salt? I don't always have one.
The salt and info are both optional in RFC 5869 but recommended. A salt strengthens extraction, and a fixed protocol-labeled salt is acceptable when you have no fresh one. The context input, info, is the one you must never omit -- it is what separates your keys [1].
Can I reuse one derived key for both encryption and its MAC?
No. Derive two keys with two distinct info strings. Never split one Expand output across two purposes, and never truncate a longer output into two keys -- re-derive with distinct labels instead [1].
Does post-quantum migration change how I derive keys?
So return to the moment we started in: the handshake is done, the shared secret is in your hand, and the tempting one-liner is to use it as a key. You now know why not, twice over, and you know the discipline that replaces the temptation. Extract concentrates the entropy that is there -- never inventing what is not. Expand, with a distinct context for every key, stamps out independence. Bind the transcript and the identities, and two situations can never collapse into one key.
The TLS 1.3 schedule is not a special trick; it is simply what obeying all three rules at once looks like, drawn as a protocol -- and post-quantum migration proved the design right by changing the input without touching the primitive.
One secret is not one key. Extract concentrates entropy and can never amplify it; Expand plus a distinct context stamps out independent keys; and binding the transcript keeps two conversations from ever sharing a key. Get those three right and the named breaks never reach you. Reach for the raw secret, and you have already made the one mistake the whole discipline exists to prevent.
Part 14 turns from deriving keys to spending them: the authenticated encryption those traffic keys feed. But the habit you carry out of this one is smaller and sharper than any schedule -- never again reach for the raw secret.
Study guide
Key terms
- Key material
- A raw shared secret, possibly non-uniform and single-use, that must be run through a KDF before it becomes a usable key.
- Extract
- The KDF stage that concentrates a non-uniform secret's entropy into one uniform pseudorandom key. It cannot amplify entropy.
- Expand
- The KDF stage that derives many independent keys from the PRK, each bound to a distinct info context.
- PRK
- Pseudorandom Key: the uniform HashLen-byte output of Extract, and the sole keying input to Expand.
- Domain separation
- Binding a distinct context label into each derivation so that keys for different purposes are independent.
- Transcript binding
- Including a hash of the handshake messages as context so a key is valid for only one exact conversation.
- Dual-PRF
- A PRF that stays secure when keyed through its message input, relied on when a fresh secret is fed into HMAC's message slot.
Comprehension questions
Why is a raw Diffie-Hellman output not safe to use directly as an AES key?
It is non-uniform, a structured group element rather than random bytes, and single-purpose. It must be extracted into a uniform PRK, then expanded into a distinct key per use.
What is the one thing Extract can never do?
Create entropy. It concentrates the min-entropy already present but cannot amplify it, which is exactly the boundary with password hashing.
What are the three rules whose violation explains almost every break?
Do not skip Extract on a non-uniform secret; do not reuse a derived key across contexts; do not bind too little context.
Which rule did the Triple Handshake attack break, and how was it fixed?
Rule three, too little context: the master secret was not bound to the transcript. The Extended Master Secret bound it to a session hash, and TLS 1.3 made per-stage transcript binding structural.
How did post-quantum migration change key derivation?
Only the input changed. The classical and ML-KEM shared secrets are concatenated and fed into the same, unchanged HKDF-Extract.
References
- (2010). RFC 5869: HMAC-based Extract-and-Expand Key Derivation Function (HKDF). https://www.rfc-editor.org/rfc/rfc5869.txt - The canonical HKDF specification: extract-then-expand, salt and info roles, the output-length cap, and the entropy-concentration boundary. ↩
- (2018). RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3. https://www.rfc-editor.org/rfc/rfc8446.txt - TLS 1.3: the staged HKDF-Extract key schedule, Derive-Secret and HKDF-Expand-Label, and per-stage transcript binding. ↩
- (2025). Hybrid key exchange in TLS 1.3 (draft-ietf-tls-hybrid-design-16, work in progress). https://datatracker.ietf.org/doc/draft-ietf-tls-hybrid-design/ - The post-quantum hybrid wiring: concatenate the classical and PQ secrets into the existing HKDF-Extract. ↩
- (2014). Triple Handshakes and Cookie Cutters: Breaking and Fixing Authentication over TLS (IEEE S&P 2014). https://doi.org/10.1109/SP.2014.15 - Triple Handshake: an active attacker forces two TLS sessions to derive the same master secret. ↩
- (2019). Selfie: reflections on TLS 1.3 with PSK. https://eprint.iacr.org/2019/347 - The Selfie reflection attack on TLS 1.3 external-PSK: a role and identity separation failure. ↩
- (2024). FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard (ML-KEM). https://csrc.nist.gov/pubs/fips/203/final - ML-KEM: the post-quantum shared secret that gets fed into the KDF. ↩
- (2011). RFC 6101: The Secure Sockets Layer (SSL) Protocol Version 3.0. https://www.rfc-editor.org/rfc/rfc6101.txt - SSL 3.0: the Generation 0 bespoke MD5 and SHA-1 key block. ↩
- (1999). RFC 2246: The TLS Protocol Version 1.0. https://www.rfc-editor.org/rfc/rfc2246.txt - TLS 1.0: the first mass-deployed protocol KDF (P_MD5 XOR P_SHA-1 feedback-mode PRF) and its SSL 3.0 lineage. ↩
- (2009). SEC 1: Elliptic Curve Cryptography, Version 2.0 (Section 3.6.1, ANSI-X9.63-KDF). https://www.secg.org/sec1-v2.pdf - SEC 1 v2.0: the open documentation of the ANSI-X9.63-KDF hash-with-counter construction. ↩
- (2018). NIST SP 800-56A Rev. 3: Recommendation for Pair-Wise Key-Establishment Schemes Using Discrete Logarithm Cryptography. https://csrc.nist.gov/pubs/sp/800/56/a/r3/final - NIST SP 800-56A: the concatenation KDF for discrete-log key establishment. ↩
- (2024). NIST SP 800-108 Rev. 1: Recommendation for Key Derivation Using Pseudorandom Functions. https://csrc.nist.gov/pubs/sp/800/108/r1/upd1/final - SP 800-108: the expand-only PRF KDF family (counter, feedback, double-pipeline) for already-uniform keys. ↩
- (2008). RFC 5246: The Transport Layer Security (TLS) Protocol Version 1.2. https://www.rfc-editor.org/rfc/rfc5246.txt - TLS 1.2, which replaced the dual-hash PRF with a single SHA-256 PRF. ↩
- (2010). Cryptographic Extraction and Key Derivation: The HKDF Scheme (CRYPTO 2010). https://eprint.iacr.org/2010/264 - The CRYPTO 2010 paper by Krawczyk: the first rigorous KDF definition, framing Extract as a computational extractor and Expand as a PRF. ↩
- (1996). Keying Hash Functions for Message Authentication (CRYPTO 1996). https://cseweb.ucsd.edu/~mihir/papers/kmd5.pdf - Bellare, Canetti, and Krawczyk: the HMAC construction that keys the hash to prevent length extension. ↩
- (2020). NIST SP 800-56C Rev. 2: Recommendation for Key-Derivation Methods in Key-Establishment Schemes. https://csrc.nist.gov/pubs/sp/800/56/c/r2/final - The NIST two-step extraction-then-expansion KDF, converging independently on the HKDF structure. ↩
- (2015). The OPTLS Protocol and TLS 1.3. https://eprint.iacr.org/2015/978 - OPTLS, the HKDF-based design ancestor of the TLS 1.3 key schedule. ↩
- (2018). The Noise Protocol Framework. https://noiseprotocol.org/noise.html - The Noise framework (the basis for WireGuard): mixing DH outputs into a chaining key. ↩
- (2016). The X3DH Key Agreement Protocol. https://signal.org/docs/specifications/x3dh/ - Signal X3DH: HKDF with a zero-filled salt and F concatenated with KM for domain separation. ↩
- (2016). The Double Ratchet Algorithm. https://signal.org/docs/specifications/doubleratchet/ - The Signal Double Ratchet forward-secure KDF chains built on HKDF. ↩
- (2026). Transport Layer Security (TLS) Parameters: Supported Groups registry. https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml - The IANA registry pinning the X25519MLKEM768 (4588) and obsolete X25519Kyber768Draft00 codepoints. ↩
- (2026). Post-quantum hybrid ECDHE-MLKEM Key Agreement for TLSv1.3 (draft-ietf-tls-ecdhe-mlkem-05, work in progress). https://datatracker.ietf.org/doc/draft-ietf-tls-ecdhe-mlkem/ - The X25519MLKEM768 hybrid: the ML-KEM then X25519 byte order and the reversed group-name ordering. ↩
- (2024). The state of the post-quantum Internet. https://blog.cloudflare.com/pq-2024/ - The Cloudflare dated snapshot of roughly two percent post-quantum TLS adoption in early 2024. ↩
- (2024). A new path for Kyber on the web. https://security.googleblog.com/2024/09/a-new-path-for-kyber-on-web.html - Deployment context for the Chrome Kyber-to-ML-KEM migration. ↩
- (2026). NIST to Revise Key Establishment Recommendations. https://csrc.nist.gov/News/2026/nist-to-revise-key-establishment-recommendations - The NIST January 2026 note to revise SP 800-56C for KEM secrets and KMAC extraction. ↩
- (2024). Key derivation with crypto_kdf (libsodium documentation). https://doc.libsodium.org/key_derivation - The libsodium crypto_kdf over BLAKE2b: ergonomic subkey derivation from a master key. ↩
- (2016). NIST SP 800-185: SHA-3 Derived Functions: cSHAKE, KMAC, TupleHash and ParallelHash. https://csrc.nist.gov/pubs/sp/800/185/final - KMAC and cSHAKE: a SHA-3 single-primitive KDF option with customization-string separation. ↩
- (2020). BLAKE3: one function, fast everywhere. https://github.com/BLAKE3-team/BLAKE3 - BLAKE3 derive_key with a mandatory hardcoded context, and its not-a-password-hash guidance. ↩
- (2026). eBACS: ECRYPT Benchmarking of Cryptographic Systems -- Diffie-Hellman secret-sharing measurements. https://bench.cr.yp.to/results-dh.html - eBACS public cycle-count benchmarks showing the KDF cost is dominated by the key exchange. ↩
- (2004). Randomness Extraction and Key Derivation Using the CBC, Cascade and HMAC Modes (CRYPTO 2004). https://cs.nyu.edu/~dodis/ps/hmac.pdf - Dodis, Gennaro, Hastad, Krawczyk, and Rabin: the Leftover Hash Lemma statistical-extraction tax. ↩
- (2023). When Messages are Keys: Is HMAC a dual-PRF? (CRYPTO 2023). https://eprint.iacr.org/2023/861 - The 2023 dual-PRF analysis: HMAC swap-PRF security is conditional on key-set feasibility. ↩
- (2020). A Cryptographic Analysis of the TLS 1.3 Handshake Protocol. https://eprint.iacr.org/2020/1044 - The Fischlin and Gunther multi-stage security proof of the whole TLS 1.3 handshake. ↩
- (2018). KEM Combiners. https://eprint.iacr.org/2018/024 - KEM combiner theory: the combined KEM is secure if at least one ingredient is. ↩
- (2024). X-Wing: a hybrid KEM based on X25519 and ML-KEM-768. https://eprint.iacr.org/2024/039 - X-Wing: a dedicated hybrid KEM with a tighter proof tied to specific primitive choices. ↩
- (2017). A Symbolic Analysis of the TLS 1.3 Handshake Protocol (Tamarin, ACM CCS 2017). https://dl.acm.org/doi/10.1145/3133956.3134063 - The Tamarin symbolic analysis covering every TLS 1.3 handshake mode. ↩
- (2022). RFC 9180: Hybrid Public Key Encryption. https://www.rfc-editor.org/rfc/rfc9180.txt - HPKE: KEM plus KDF plus AEAD over HKDF, with LabeledExtract and LabeledExpand domain separation. ↩
- (2026). OpenSSL Manual: EVP_KDF-HKDF -- The HKDF EVP_KDF implementations. https://docs.openssl.org/master/man7/EVP_KDF-HKDF/ - The OpenSSL HKDF implementation via EVP_KDF. ↩
- (2026). BoringSSL Reference: hkdf.h -- HKDF, HKDF_extract, and HKDF_expand. https://commondatastorage.googleapis.com/chromium-boringssl-docs/hkdf.h.html - The BoringSSL HKDF, HKDF_extract, and HKDF_expand functions. ↩
- (2026). pyca/cryptography: Key Derivation Functions (HKDF). https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/ - The HKDF implementation in the Python cryptography library. ↩
- (2026). RustCrypto: hkdf -- Pure Rust implementation of HKDF (RFC 5869). https://docs.rs/hkdf/latest/hkdf/ - The RustCrypto pure-Rust HKDF implementation of RFC 5869. ↩
- (2025). Go 1.24 Release Notes: new crypto/hkdf, crypto/pbkdf2, and crypto/sha3 packages. https://go.dev/doc/go1.24 - The Go 1.24 release notes documenting the new crypto/hkdf standard-library package. ↩
- (2015). RFC 7627: TLS Session Hash and Extended Master Secret Extension. https://www.rfc-editor.org/rfc/rfc7627.txt - Extended Master Secret and session-hash binding: the deployed fix for the Triple Handshake attack. ↩