# 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.

*Published: 2026-07-11*
*Canonical: https://paragmali.com/blog/one-secret-is-not-one-key-the-discipline-of-key-derivation-w*
*© Parag Mali. All rights reserved.*

---
<TLDR>
A raw shared secret is not a key -- it is key *material*. A Diffie-Hellman output is non-uniform and an ML-KEM output is single-use, and neither is safe to key a cipher with directly, so protocols run the secret through a key derivation function that does two separable jobs: **Extract** concentrates its entropy into one uniform pseudorandom key (it cannot *amplify* entropy -- that is where password hashing lives), and **Expand** stretches that key into many independent subkeys, each bound to a distinct context label [@rfc5869]. HKDF (RFC 5869) is the deployed instantiation, HMAC playing both roles [@rfc5869]; the TLS 1.3 key schedule is the flagship hierarchy built from it [@rfc8446], and post-quantum migration changed only what gets fed in, not the primitive [@draft-hybrid]. Get three rules right -- do not skip Extract, separate every key by context, bind the transcript -- and the named breaks such as Triple Handshake [@doi-sp2014] and Selfie [@eprint-2019-347] never happen to you.
</TLDR>

## 1. 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 [@fips203]. 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" [@rfc5869]. TLS 1.3 carries the same warning into its own design, never keying anything directly from the raw (EC)DHE value [@rfc8446].

An ML-KEM shared secret is the other case. FIPS 203 derives it from an internal hash (G, SHA3-512), so the secret is already a uniform 256-bit key rather than a structured value, and it is not entropy extraction that makes it safe [@fips203]. What ships in practice, though, is a *hybrid*: the input keying material is the concatenation of the ML-KEM secret and an X25519 secret, and because that concatenation still carries the non-uniform Diffie-Hellman half, the whole block goes through HKDF-Extract as one input [@draft-hybrid]. The rule you follow does not change -- run the shared secret through Extract -- but for ML-KEM the reason is the company it keeps, not its own distribution.

**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 [@rfc8446]. 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.

<Definition term="Key Derivation Function (KDF)">
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."
</Definition>

Put the two reasons together and you get the sentence this entire article defends.

> **Key idea:** 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.

<Mermaid caption="One shared secret must become many independent keys -- the problem every key derivation function solves.">
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"]
</Mermaid>

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 [@doi-sp2014], the Selfie reflection [@eprint-2019-347], and the plain mistake of using a raw secret as a key [@rfc5869] -- is the violation of exactly one of these three rules. We will name each one in the catalog at the end.

> **Note:** Never key AES, ChaCha20, or HMAC directly from a Diffie-Hellman or KEM output. It is raw material -- non-uniform, single-use, or both -- not a finished key. Treat it as input to a KDF, and derive every working key with its own context.

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](/blog/the-one-job-of-a-password-hash-why-fast-is-the-enemy-and-mem/)) 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 [@rfc5869].

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 [@rfc6101]. 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" [@rfc2246].

<Mermaid caption="Thirty years of key derivation. The shape is convergence on extract-then-expand, not a chain of catastrophic breaks.">
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
</Mermaid>

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))`) [@rfc2246]. 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.

<Definition term="Pseudorandom Function (PRF)">
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.
</Definition>

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 [@sec1-v2]. 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" [@rfc5869].<Sidenote>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` [@sec1-v2]. 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).</Sidenote>

Then NIST codified its own line: a concatenation KDF in SP 800-56A [@sp800-56a], 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 [@sp800-108-r1].

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.

<Sidenote>The two-hash hedge did not survive. TLS 1.2 replaced `P_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" [@rfc5246]. The hedge added complexity without giving the modularity a real Extract/Expand split would later provide.</Sidenote>

<Aside label="Convergence, not catastrophe">
It is tempting to read a history of cryptography as a sequence of disasters, each fixed by the next design. This one is not that. The pre-HKDF KDFs were not catastrophically broken; they were *unproven and muddled*. Two lineages -- the IETF and academic line that became HKDF, and the NIST SP 800-56 and 800-108 line -- were separately groping toward the same structure. The interesting question is not "when did they break" but "why could nobody prove they were sound," and the answer is what finally split the field's one confused step into two honest ones.
</Aside>

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 $g^{xy}$. 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" [@eprint-2010-264].

<Definition term="Min-entropy">
Min-entropy measures a source's worst-case unpredictability. If its most likely value occurs with probability $p$, its min-entropy is $-\log_2 p$. 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.
</Definition>

**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.

<Definition term="Randomness extractor">
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.
</Definition>

There is also a concrete, testable hazard in the naive `H(secret || info)` form, separate from the proof problem.

> **Note:** With a Merkle-Damgard hash (MD5, SHA-1, SHA-256), knowing `H(secret || info)` and the length of the secret lets an attacker compute `H(secret || info || padding || suffix)` for a suffix of their choosing -- without ever learning the secret. That is exactly why HMAC keys the hash instead of using raw `H` [@bck96-hmac], and why homemade concatenation KDFs are dangerous. Sponge and tree hashes (SHA-3, BLAKE3) are not vulnerable to this. Part 10 of this series covers the mechanism in full.

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 [@eprint-2010-264].

> **Note:** Concentrating entropy and separating keys are two different jobs, and the old designs did both at once in a single step that was proven for neither -- surviving only by pretending the hash was a perfect random oracle. Name the two jobs, prove each, and the confusion dissolves.

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.

<Definition term="Extract-then-Expand">
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.
</Definition>

<Definition term="Computational randomness extractor">
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.
</Definition>

The engineering move that made this practical was refusing to invent two new gadgets. Both stages are instantiated with the same primitive: [HMAC](/blog/the-tag-verified-the-cipher-held-the-forgery-went-through-a-/). 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).

<Mermaid caption="The extract-then-expand data flow. Two stages, two security notions, one shared HMAC primitive.">
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"]
</Mermaid>

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" [@rfc5869]. The *theory* is the CRYPTO 2010 paper, which supplied what the field had never had.

<PullQuote>
"...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 [@eprint-2010-264]
</PullQuote>

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" [@eprint-2010-264].

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 [@sp800-56c-r2], while SP 800-108 remains the expand-only sibling for inputs that are already uniform [@sp800-108-r1].

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.

> **Note:** A KDF is *two* functions with *two* security notions: an extractor that concentrates entropy into one uniform key (and provably cannot create entropy), and a PRF that stamps out arbitrarily many keys, each independent because each names a distinct context. Once you separate them -- and build both from the same HMAC -- everything the old designs groped toward becomes provable. Nothing was "broken"; the field finally named the two jobs.

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:

$$\mathrm{PRK} = \mathrm{HMAC\text{-}Hash}(\text{salt},\ \mathrm{IKM})$$

If you have no salt, RFC 5869 says use a string of `HashLen` zero bytes [@rfc5869]. The output is one uniform pseudorandom key.

<Definition term="Pseudorandom Key (PRK)">
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.
</Definition>

**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:

$$T(i) = \mathrm{HMAC\text{-}Hash}(\mathrm{PRK},\ T(i-1)\ \Vert\ \text{info}\ \Vert\ \mathrm{byte}(i)), \quad T(0)=\varepsilon$$

<Mermaid caption="HKDF-Expand chains HMAC blocks in feedback mode, counter-stepped. Each block feeds the next, and the output keying material is the concatenation, truncated to L bytes.">
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
</Mermaid>

That single incrementing byte is why the output has a hard ceiling: $L \le 255 \cdot \mathrm{HashLen}$ [@rfc5869]. Ask for more and the counter would overflow a byte.<MarginNote>The counter starts at 1, not 0, so the largest block index is 255 -- hence the factor of 255 rather than 256.</MarginNote>

<Definition term="Output Keying Material (OKM)">
The pseudorandom bytes Expand produces, which the caller slices into the actual working keys. A single derivation is capped at $255 \cdot \mathrm{HashLen}$ bytes; longer needs cannot be met by asking for a bigger `L`.
</Definition>

<Sidenote>That cap is 8,160 bytes with SHA-256 and 16,320 bytes with SHA-512 -- comfortably more than any single key, but a real limit if you try to run HKDF as a general keystream generator. It is not one. Derive keys with it; use a stream cipher for streams.</Sidenote>

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.

<RunnableCode lang="js" title="HKDF Extract and Expand from scratch (RFC 5869 Test Vector 1)">{`
// 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...
})();
`}</RunnableCode>

<Spoiler kind="solution" label="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 [@rfc5869]. If your implementation prints these two lines, your Extract and Expand are correct.
</Spoiler>

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 [@rfc5869]. **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" [@rfc5869].

<Definition term="Salt (key derivation)">
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.
</Definition>

<Definition term="Domain separation">
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.
</Definition>

> **Note:** `info` is not decoration. It is the entire mechanism of key separation. Two Expand calls with the same PRK and the same `info` return identical bytes; two calls with different `info` return independent keys. Every key-separation bug in this article is, at bottom, a missing or duplicated `info`.

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.

<RunnableCode lang="js" title="Domain separation: distinct info gives independent keys, same info collides">{`
// 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));
})();
`}</RunnableCode>

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](/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/); 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" [@rfc5869].

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 [@rfc8446] [@eprint-2015-978].

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 [@rfc8446]. Then it chains a sequence of `HKDF-Extract` calls, each using the previous secret as the salt and a new secret as the IKM:

<Mermaid caption="The TLS 1.3 key schedule: a staged HKDF-Extract chain (Early to Handshake to Master), with one representative Derive-Secret branch per stage. Every derived secret binds a transcript hash.">
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)"]
</Mermaid>

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 [@rfc8446].

The schedule folds in a pre-shared key, then the (EC)DHE secret, then a final zero. Along the way it stamps out early-data secrets, handshake traffic secrets, application traffic secrets, an exporter secret, a resumption secret, and the PSK binder keys (`ext binder` and `res binder`) that "prevent the substitution of one type of PSK for the other" [@rfc8446].

Key updates ratchet forward with an `HKDF-Expand-Label` using the label `traffic upd` [@rfc8446]. 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](/blog/the-aead-decision-matrix-seven-ciphers-three-edges-one-choic/), the subject of Part 7 [@rfc8446].

The RFC states the article's whole thesis in its own words, describing the shape of its own diagram.

<PullQuote>
"...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 [@rfc8446]
</PullQuote>

Left side, raw entropy; right side, entropy plus bound context. That is Extract and Expand, drawn as a protocol.

<Sidenote>The same staged-Extract, labeled-Expand pattern reappears everywhere HKDF is deployed: HPKE (RFC 9180), MLS, the Noise framework behind WireGuard [@noise], and Signal, whose X3DH keys HKDF with a zero-filled salt and an application-identifier context [@signal-x3dh] and whose Double Ratchet builds forward-secure KDF chains from it [@signal-doubleratchet].</Sidenote>

### 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 [@iana-tls-groups], `X25519MLKEM768`, concatenates the ML-KEM-768 secret from FIPS 203 [@fips203] and then the classical X25519 secret, and hands the pair to the *existing* TLS 1.3 HKDF-Extract as its (EC)DHE input [@draft-hybrid]. 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 [@draft-ecdhe-mlkem].

<Mermaid caption="Post-quantum hybrid wiring: two shared secrets are concatenated (ML-KEM first, then X25519) into a longer IKM and fed to the same, unchanged, TLS 1.3 HKDF-Extract.">
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"]
</Mermaid>

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 [@draft-hybrid]. This is the entire reason extract-then-expand aged so well: a brand-new *kind* of secret slotted in without touching the primitive.

> **Note:** HKDF has not been replaced, improved, or deprecated since 2010. Every advance labeled "state of the art" -- the TLS 1.3 schedule, HPKE's labeled derivation, the post-quantum hybrids -- is a new way to *wire* the same two functions. When a primitive absorbs a shift as large as post-quantum migration with zero changes to itself, that is the sign it was designed at the right level of abstraction.

<Sidenote>IANA assigned `X25519MLKEM768` the codepoint 4588, that is 0x11EC, marked Recommended, and retired the earlier `X25519Kyber768Draft00` at 0x6399 as obsolete [@iana-tls-groups].</Sidenote>

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" [@cloudflare-pq]; Google documented Chrome's own migration to ML-KEM on the web [@google-blog]. 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)" [@nist-2026-news].

<Aside label="Why the standard lags the deployment">
FIPS-validated systems cannot ship a construction the standard does not yet permit, so the standard is currently *behind* deployed TLS. Browsers concatenated a KEM secret into HKDF-Extract in 2024; NIST announced the matching revision to SP 800-56C in 2026 [@nist-2026-news]. That gap is not dysfunction -- it is the normal order of operations. Engineering ships the wiring, the standard blesses it once the analysis settles, and validation follows the standard.
</Aside>

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 [@rfc5869] and the NIST SP 800-56C two-step method [@sp800-56c-r2]. 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 [@sp800-108-r1], and libsodium's `crypto_kdf` over BLAKE2b [@libsodium]. 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 [@sp800-185], and BLAKE3's `derive_key`, which takes a mandatory hardcoded `context` string [@blake3]. And SP 800-56C's one-step method is the "one call" middle ground [@sp800-56c-r2].

| 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.

<Sidenote>libsodium's `crypto_kdf` can derive up to $2^{64}$ 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 [@libsodium]. It is delightful for subkeys -- and exactly wrong for a raw DH secret, because it never extracts.</Sidenote>

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.

<Aside label="Password hashing is key derivation too">
Password hashing is genuinely a kind of key derivation -- PBKDF2 literally stands for Password-Based Key Derivation Function. The distinction is not "KDF versus not-KDF"; it is *input entropy*. HKDF and Argon2id are adjacent branches of one family, split by whether the input has real entropy. The error is therefore *directional*: never run HKDF on a password, because Extract "cannot amplify entropy" [@rfc5869], and never run a deliberately slow, memory-hard password hash on a high-entropy DH or KEM secret. Part 12 of this series covers the low-entropy branch; here it only marks the boundary.
</Aside>

> **Note:** You do not have to take my word on the boundary. BLAKE3's own documentation says it "is not a password hashing algorithm" and points you at Argon2 [@blake3]; libsodium keeps `crypto_kdf` and its password-hashing API strictly separate. Part 10 of this series covers BLAKE3 as a hash in its own right.

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 [@ebacs]. 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.

<PullQuote>
"The extract step in HKDF can concentrate existing entropy but cannot amplify entropy." -- RFC 5869, section 4 [@rfc5869]
</PullQuote>

Read literally, that sentence is the border of the entire subject. If the source `IKM` carries $m$ bits of min-entropy, then no KDF -- however clever, however many rounds, however long its output -- yields more than about $m$ bits of key security [@rfc5869]. 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 $\varepsilon$ of uniform you pay roughly $2\log_2(1/\varepsilon)$ bits of the source as overhead [@crypto-2004-dghkr]. 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 [@eprint-2010-264]. 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) [@eprint-2010-264]. 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](/blog/secure-against-whom-the-security-definitions-every-protocol-/), with good multi-user bounds [@eprint-2023-861].

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" [@eprint-2020-1044]. That is the current ceiling on what we can prove about a whole deployed key schedule, not just one KDF call.

<Definition term="Dual-PRF">
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 [@eprint-2023-861].
</Definition>

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 [@rfc5869]. The community shorthand for the danger of over-modeling is blunt.

<Aside label="HKDF is not a random oracle">
It is tempting to reason about HKDF as if it were a perfect random function -- the very habit that made the old designs unprovable. HKDF is *not* a random oracle, and treating it as one can prove things that are not true of the real construction. Its guarantees are the extractor and PRF properties of HMAC, no more. The dual-PRF result sharpens the same caution: the swap-key guarantee is *conditional*, holding only for feasible key sets, so each new post-quantum wiring must re-check that its secrets land in a feasible set [@eprint-2023-861]. Part 1 of this series covers the PRF and random-oracle assumptions these arguments rest on.
</Aside>

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.

> **Key idea:** 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 [@draft-hybrid]. 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" [@eprint-2018-024]. 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" [@eprint-2024-039]. 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.

<Sidenote>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 [@eprint-2024-039].</Sidenote>

**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 [@nist-2026-news]. Until the document actually lands, the exact approved formatting for hybrid secrets and the KMAC-as-extractor parameters are unsettled.

> **Note:** Treat the 2026 SP 800-56C revision as a planning note, not a specification. NIST has stated its intent to bless KEM secrets and KMAC extraction, but the parameters are not final [@nist-2026-news]. Design against the current standard and the IETF drafts, and be ready to adjust the formatting details when the revised document is published.

**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 [@acm-tamarin]; computational multi-stage proofs fix a specific model and version [@eprint-2020-1044]. 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 [@rfc9180], BLAKE3's mandatory `context` [@blake3]. 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 [@eprint-2023-861]. 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.

<Mermaid caption="The KDF selection decision tree. Two questions -- is the input a password, and is it already uniform -- determine the whole choice.">
flowchart TD
    START["Your input keying material"] --> Q1&#123;"Low-entropy password or PIN?"&#125;
    Q1 -->|Yes| PW["Password hash: Argon2id (Part 12), not HKDF"]
    Q1 -->|No| Q2&#123;"Already a uniform key?"&#125;
    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"]
</Mermaid>

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 [@rfc5869]. 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 [@rfc5869]. 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 `info` or 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 [@rfc5869].
- **Respect the output ceiling.** A single derivation caps at $255 \cdot \mathrm{HashLen}$ bytes [@rfc5869]. 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-Secret` and HPKE's `LabeledExtract` and `LabeledExpand` are reusable templates even outside those protocols [@rfc8446] [@rfc9180]. 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.

<Definition term="Transcript binding">
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.
</Definition>

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.

<RunnableCode lang="js" title="A Derive-Secret-style wrapper that refuses to derive without a purpose">{`
// 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));
})();
`}</RunnableCode>

You rarely need to implement any of this by hand. HKDF ships in OpenSSL (via `EVP_KDF`) [@openssl-hkdf] and BoringSSL (via the standalone `HKDF`, `HKDF_extract`, and `HKDF_expand` functions in `hkdf.h`) [@boringssl-hkdf], in Python's `cryptography` [@pyca-hkdf], in RustCrypto's `hkdf` [@rustcrypto-hkdf], and in Go's standard library.<Sidenote>Go promoted HKDF into the standard library as `crypto/hkdf` in Go 1.24; before that it lived in `golang.org/x/crypto/hkdf` [@go124-notes]. The move is a small signal of how settled the primitive is -- stable enough to be standard-library furniture.</Sidenote> 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.

> **Note:** The single habit that prevents most KDF bugs: give every derived key an explicit, unique purpose string, and never reuse or split one. If your code derives a key without naming what it is for, that missing name *is* the bug. And keep the salt-versus-nonce line straight -- a salt is fixed and reusable, a nonce is per-message and must never repeat (Part 2).

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 | [@doi-sp2014] [@rfc7627] |
| Selfie | 2019 | Derived keys not bound to who is speaking | Rule 3: identity not bound | Bind role and identity | [@eprint-2019-347] |
| Raw secret as key | any | Skipped Extract on a non-uniform secret | Rule 1: do not skip Extract | Run Extract first | [@rfc5869] |
| Homemade `H(secret || info)` | any | No proof, plus length extension | Rule 1, and key the hash | Use HKDF over HMAC | [@rfc5869] |
| One key for encrypt and MAC | any | One key serving two purposes | Rule 2: separate by context | Two distinct `info` strings | [@rfc5869] |

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 [@doi-sp2014]. 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 [@rfc7627].

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 [@eprint-2019-347]. 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 [@rfc5869].

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](/blog/the-fingerprint-two-files-shared-a-field-guide-to-cryptograp/); 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 [@rfc5869].

> **Note:** If you remember a single prohibition from this article, make it this: never key a cipher or a MAC directly from a raw Diffie-Hellman or ML-KEM secret. A Diffie-Hellman value is non-uniform, so skipping its Extract step is rule one -- the root of the whole misuse catalog, and RFC 5869 spells it out, the extract step "SHOULD NOT be skipped" [@rfc5869]. An ML-KEM secret is already uniform but single-use, so it still must be expanded into a distinct key per context before it goes near a cipher.

<FAQ title="Frequently asked questions">
<FAQItem question="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 [@rfc5869].
</FAQItem>
<FAQItem question="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 [@rfc5869].
</FAQItem>
<FAQItem question="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 [@rfc5869].
</FAQItem>
<FAQItem question="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 [@rfc5869].
</FAQItem>
<FAQItem question="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 [@rfc5869].
</FAQItem>
<FAQItem question="Does post-quantum migration change how I derive keys?">
No. An ML-KEM shared secret is just another high-entropy input. Deployed hybrids concatenate the classical and post-quantum secrets and feed the pair into the same, unchanged HKDF-Extract [@draft-hybrid]. The primitive did not change; only what you feed it did [@fips203].
</FAQItem>
</FAQ>

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.

> **Key idea:** 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.

<StudyGuide slug="key-derivation-hkdf-key-hierarchies" keyTerms={[
  { term: "Key material", definition: "A raw shared secret, possibly non-uniform and single-use, that must be run through a KDF before it becomes a usable key." },
  { term: "Extract", definition: "The KDF stage that concentrates a non-uniform secret's entropy into one uniform pseudorandom key. It cannot amplify entropy." },
  { term: "Expand", definition: "The KDF stage that derives many independent keys from the PRK, each bound to a distinct info context." },
  { term: "PRK", definition: "Pseudorandom Key: the uniform HashLen-byte output of Extract, and the sole keying input to Expand." },
  { term: "Domain separation", definition: "Binding a distinct context label into each derivation so that keys for different purposes are independent." },
  { term: "Transcript binding", definition: "Including a hash of the handshake messages as context so a key is valid for only one exact conversation." },
  { term: "Dual-PRF", definition: "A PRF that stays secure when keyed through its message input, relied on when a fresh secret is fed into HMAC's message slot." }
]} questions={[
  { q: "Why is a raw Diffie-Hellman output not safe to use directly as an AES key?", a: "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." },
  { q: "What is the one thing Extract can never do?", a: "Create entropy. It concentrates the min-entropy already present but cannot amplify it, which is exactly the boundary with password hashing." },
  { q: "What are the three rules whose violation explains almost every break?", a: "Do not skip Extract on a non-uniform secret; do not reuse a derived key across contexts; do not bind too little context." },
  { q: "Which rule did the Triple Handshake attack break, and how was it fixed?", a: "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." },
  { q: "How did post-quantum migration change key derivation?", a: "Only the input changed. The classical and ML-KEM shared secrets are concatenated and fed into the same, unchanged HKDF-Extract." }
]} />
