Cryptography · Protocol Construction

Domain Separation and Labelled Key Derivation

Why one master secret must never produce two interchangeable keys, how labels are encoded into a KDF, and which label collisions still cause cross-protocol failures.

A protocol that establishes one strong secret and then uses it for five different jobs has, without any visible mistake, created a protocol in which each job can be attacked with the others’ tools. The secret is the same string of bytes; the only thing distinguishing “the key that encrypts client data” from “the key that authenticates a finished message” is a convention in the code. If an attacker can make one context accept a value produced in the other, the convention has become the entire security argument, and conventions do not resist chosen-input attacks.

Key derivation with explicit labels exists to replace that convention with a cryptographic fact. Instead of splitting a master secret into pieces and hoping the pieces are used where intended, each derived key is the output of a pseudorandom function applied to a structured input that names its purpose. Two purposes produce two unrelated outputs even when the underlying secret is identical, and the naming is inside the cryptographic computation rather than beside it.

Why splitting is not deriving

The naive construction is to concatenate the master secret with a purpose string and hash it: $k_i = H(S ,|, \text{“enc”})$. This is a PRF only under assumptions about $H$ that are not the ones hash functions are designed for. A hash function is built to resist collisions and preimages on arbitrary inputs; it is not built so that $H(S | x)$ is unpredictable to someone who knows $H(S | y)$ for a different $x$. Length-extension makes the weakness concrete for the Merkle–Damgård constructions: given $H(S | x)$ and the length of $S | x$, an attacker can compute $H(S | x | \mathit{pad} | z)$ without knowing $S$. A protocol that derived keys by appending suffixes to a hashed secret is therefore one message away from letting an attacker extend a known key into a neighbouring one.

HMAC removes that class of problem, which is why the standard constructions are HMAC-based. HKDF, specified in RFC 5869, splits the job in two: an extract step that compresses input keying material of any length and any distribution into a fixed-size pseudorandom key, and an expand step that stretches that key into as many output bytes as needed. Extract is $\mathrm{HMAC}(salt, IKM)$; expand is an HMAC-based counter chain. Both are PRF evaluations, so an output is indistinguishable from random to anyone who does not hold the PRK, and nothing about the output reveals a usable relationship to the input.

The expand step is where the label enters, and its structure matters:

$$\mathrm{OKM} = T_1 | T_2 | \cdots | T_n, \qquad T_i = \mathrm{HMAC}(PRK,; T_{i-1} | info | i)$$

with $T_0$ empty and $i$ a single byte. Because $info$ is concatenated with a one-byte counter, and the counter runs only to 255, a single HKDF-Expand call produces at most $255 \cdot L$ bytes where $L$ is the hash output length: 8160 bytes for SHA-256, 12240 for SHA-384. That is a hard ceiling, not a guideline, and a protocol that needs more must extract a fresh PRK.

Encoding a label so it cannot be confused

Concatenating a label and a context into $info$ reintroduces the ambiguity the construction was supposed to remove. If one call site passes label "key" with context "A" and another passes label "keyA" with an empty context, both produce the byte string keyA and therefore the same key. The fix is to make the fields self-delimiting, which TLS 1.3 does with a small structure it calls HkdfLabel:

struct {
    uint16 length;                  // output length in bytes, big-endian
    opaque label<7..255>;           // 1-byte length prefix, "tls13 " + Label
    opaque context<0..255>;         // 1-byte length prefix, usually a transcript hash
} HkdfLabel;

Three properties come out of that layout. The output length is an input to the derivation, so asking for 32 bytes and asking for 48 bytes with the same label and context give outputs where neither is a prefix of the other. Without the length field, the first $T_1$ block would be identical and any caller taking a prefix of the longer output would obtain the shorter key. The label carries a one-byte length, so no label can absorb the following field. And the label itself begins with the protocol identifier tls13 , which is how the same derivation machinery in DTLS 1.3 produces different keys: it uses dtls13 instead, so a datagram connection and a stream connection established from the same pre-shared key do not share traffic secrets. That single five-byte difference is the entire separation between two protocols built from one specification.

RFC 8446’s expansion function is therefore a thin wrapper around HKDF-Expand:

import hmac, hashlib, struct

def expand_label(secret: bytes, label: str, context: bytes,
                 length: int, hash_name: str = "sha256") -> bytes:
    full_label = ("tls13 " + label).encode("ascii")
    info = (struct.pack("!H", length)
            + bytes([len(full_label)]) + full_label
            + bytes([len(context)]) + context)
    h = lambda k, m: hmac.new(k, m, hash_name).digest()
    out, block = b"", b""
    for counter in range(1, 256):
        block = h(secret, block + info + bytes([counter]))
        out += block
        if len(out) >= length:
            return out[:length]
    raise ValueError("length exceeds 255 hash blocks")

The loop termination and the 255-block ceiling are the two places where an implementation can go wrong quietly: returning fewer bytes than requested truncates a key, and silently hashing more than 255 blocks would produce output that no other implementation can reproduce.

The context field is a commitment, not a comment

The context field is where a derivation becomes bound to a particular run of the protocol. In TLS 1.3 it is normally the transcript hash at the point of derivation, which ties every traffic secret to the exact handshake that produced it; the mechanism by which that digest is accumulated is described in transcript hashing and handshake binding. An empty context is legal and is used for secrets that must be derivable before any handshake message exists, such as the early secret. Choosing empty where a transcript is available gives up the binding for no gain.

The finished_key label is a good illustration of why purpose separation is not cosmetic. It is derived from a traffic secret with an empty context, and its only use is as an HMAC key over the transcript hash. If the same traffic secret were also used directly as an AEAD key, an attacker who obtained a Finished MAC would hold a value that is a valid MAC under a key the peer also uses for record protection, and the two uses would have to be argued secure jointly. Deriving a separate 32-byte key for each use means the argument for each is independent, and a break in one does not transfer.

The same pattern appears outside TLS. NIST SP 800-108 specifies KDFs in counter, feedback, and double-pipeline modes whose input includes a Label, a Context, and the output length, all fixed-length-prefixed, precisely so that distinct applications of one key-derivation key cannot collide. SP 800-108 also requires that the label and context be agreed in advance between the parties, which is the operational counterpart of the encoding rule: a separator only separates if both sides compute it identically.

Collisions that survive correct encoding

Labelled derivation does not repair a protocol whose labels are chosen badly. Three failure patterns recur.

Label reuse across purposes. Two call sites that pass the same label and the same context obtain the same key. Nothing in HKDF detects this; the labels are the protocol’s responsibility. A useful discipline is that a label names one consumer, and that consumer’s code is the only place the label string appears, so a grep for the literal finds every use.

Ambiguous application of the derived bytes. A key derived for “the client’s handshake traffic” and used for both an AEAD and a MAC is two uses of one key even though the derivation was clean. The label describes the derivation, not the consumption; if one output feeds two algorithms, the separation has to happen one level down.

Version and role confusion. Labels that omit the protocol version let a future version inherit the old one’s keys, which is exactly the situation a version downgrade attack wants. Labels that omit the role let a client and a server derive the same key from the same secret when the direction should differ. TLS 1.3 handles direction by deriving a client_application_traffic_secret_N and a server_application_traffic_secret_N separately and expanding each with the key and iv labels, so the two directions never share an AEAD key or a nonce space. The consequences of getting that wrong are described in nonce construction and reuse limits.

A subtler case is a protocol that derives a key from a secret that is itself derived, and uses the intermediate value somewhere else. Because HKDF-Expand is a PRF, the intermediate secret is not recoverable from its outputs, but it is recoverable by anyone who can recompute the chain. Protocols that expose an intermediate secret to an application, for exporters or for resumption, must treat that exposure as granting the ability to derive everything downstream of it. That is why exporter values are derived with their own label and a context that includes the exporter label, and why the resumption secret is kept distinct from the application traffic secret rather than shared with it.

The final constraint is one of ordering rather than encoding: a label cannot commit to a value that does not exist yet. A derivation that happens before the peer’s authentication has completed can only be bound to the messages received so far, and any later message that should have influenced the key will not have. Protocols that derive early secrets therefore place the compensating check in the Finished MAC, which covers the full transcript, rather than pretending the early derivation was bound to it. Where that check sits in the message sequence, and why its position is load-bearing, is the subject of key confirmation ordering.

References

  1. RFC 5869: HMAC-based Extract-and-Expand Key Derivation Function (HKDF) — IETF
  2. NIST SP 800-108: Recommendation for Key Derivation Using Pseudorandom Functions — NIST
  3. RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3 — IETF