skip to content

Key Derivation

One key should never serve two purposes: crypto/hkdf expands a master secret into per-use keys bound to a label, and crypto/pbkdf2 stretches a passphrase into one.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

How do you use `hkdf.Key` in Go to turn one master secret into a separate key per purpose?

level: juniorimportance: must knowfreq 45%

answer

  1. one secret in, many keys out
  2. five arguments, one of them varies
  3. the fifth argument names the purpose
  4. info is the domain separator
  5. deterministic, so nothing extra is stored

basics

~10 s

Call hkdf.Key once per purpose with a different info string each time, for example "cookie-encryption-v1" versus "url-signing-v1". Same secret, same salt, different info, and you get independent keys you never have to store.

solid answer

~40 s

`crypto/hkdf`, in the standard library since Go 1.24, exposes `hkdf.Key(h func() Hash, secret, salt []byte, info string, keyLength int) ([]byte, error)`. I call it once per purpose with the same master secret and salt but a distinct `info` label — `"acme/session-cookie/v1"`, `"acme/download-url/v1"` — and each call returns an independent key of the length I asked for. The `info` string is the domain separator: it is the argument that makes the outputs unrelated, so one stored secret can back a dozen features. Because the function is deterministic, the library derives on demand at startup instead of storing a key per feature; only the master secret is ever at rest. `keyLength` is capped at the hash's `Size()` times 255, and that cap is essentially the only thing the error return reports.

code

go · 12 lines
go
type Deriver struct {
	master []byte
	salt   []byte
}

func (d *Deriver) SessionCookieKey() ([]byte, error) {
	return hkdf.Key(sha256.New, d.master, d.salt, "acme/session-cookie/v1", 32)
}

func (d *Deriver) DownloadURLKey() ([]byte, error) {
	return hkdf.Key(sha256.New, d.master, d.salt, "acme/download-url/v1", 32)
}

go deeper

for a junior

Be ready to name the five arguments of hkdf.Key in order and say which one distinguishes two purposes. Knowing that the output is deterministic, so derived keys are never stored, is the other half of the answer.

for a middle

Explain why one master secret with distinct info labels is safe, and what the salt contributes that the info string does not. Expect a follow-up on what a nil salt turns into inside the package.

for a senior

Show that you treat info strings as an operational contract — versioned, unique per purpose, reviewed — and that you know which arguments crypto/hkdf will silently accept as empty.

for a principal

Own the API shape: whether callers pass raw info strings or call named methods, and what re-deriving every key in production would cost if that namespace ever has to change.

## The problem a KDF solves A real service ends up needing several independent secret keys: one to authenticate session cookies, one to sign download URLs, one to encrypt a column, one per tenant. Provisioning four secrets means four things to generate, distribute, rotate, back up and eventually leak. Using one secret everywhere is worse: a single mistake in one feature compromises all of them, and rotation becomes all-or-nothing. A key derivation function collapses that to one stored secret. You keep a single high-entropy master secret and derive a per-purpose key from it on demand. The derived keys are computationally unrelated: knowing the cookie key tells an attacker nothing about the URL-signing key. ## The Go call `crypto/hkdf` joined the standard library in Go 1.24. Its one-call form is: ```go func Key[Hash hash.Hash](h func() Hash, secret, salt []byte, info string, keyLength int) ([]byte, error) ``` Five arguments: - **`h`** — a hash *constructor*, almost always `sha256.New`. Note you pass the function, not a hash value; the package calls it to build the instances it needs internally. It is a type parameter, so the compiler infers `Hash` from the argument and you never write it out. - **`secret`** — the master secret. HKDF assumes this is already high-entropy: a value from a secure random source, a shared secret from a key agreement, or a key that came out of an earlier derivation. It is emphatically not the place for a human-typed passphrase. - **`salt`** — a non-secret, ideally random value. It strengthens the internal extract step. It may be `nil`, in which case the package substitutes a run of zero bytes as long as the hash's output. - **`info`** — the per-purpose label, and the argument that does the work you actually care about. Two calls that differ only here return unrelated keys. - **`keyLength`** — how many bytes you want back. The error return is narrow: `keyLength` may not exceed the hash's `Size()` times 255, which is 8160 bytes with SHA-256. A `nil` salt, an empty `info` string and a short `secret` are all accepted without complaint, so any validation beyond the length bound has to live in code you write. ## Determinism is the point Given the same hash, secret, salt, info and length, `hkdf.Key` returns the same bytes every time, on every machine, forever. Three consequences follow, and they are what an interviewer is usually probing for: 1. **You do not store derived keys.** The service derives them at startup or per request and keeps only the master secret in its secret store. That shrinks the number of protected items to one. 2. **The info string is part of the key's identity.** Change the label and you have a different key, so anything already protected by the old key stops verifying or decrypting. Labels are therefore a contract, not a comment, and they normally carry an explicit version suffix from day one. 3. **Identical inputs across environments give identical keys.** If staging and production share a master secret and pass the same info string, they derive the same key. That is a determinism property, not a bug — but it is a common way a cross-environment leak happens. ## What a wrapper looks like The idiomatic shape for a library other teams import is a small type holding the master secret and salt, exposing one method per purpose: ```go type Deriver struct { master []byte salt []byte } func (d *Deriver) SessionCookieKey() ([]byte, error) { return hkdf.Key(sha256.New, d.master, d.salt, "acme/session-cookie/v1", 32) } ``` Naming the purposes as methods rather than taking a `purpose string` parameter is deliberate. A raw string parameter means callers invent their own labels, and two callers that happen to pass the same string — or the empty string — get the same key with no error and no warning. ## Length and truncation One detail worth knowing: HKDF's output for a shorter length is a *prefix* of its output for a longer one. Asking for 16 bytes and asking for 32 with otherwise identical arguments does not give you two independent keys; the first is the first half of the second. Length is not a separation mechanism. The `info` string is. ## History Before Go 1.24 the same algorithm lived in `golang.org/x/crypto/hkdf`, with an older interface that handed you an `io.Reader` to read key material from. The standard-library version returns the bytes directly and returns an error rather than deferring failure to a read. New code should use `crypto/hkdf`.

  • Does the salt you pass to `hkdf.Key` have to be secret?
    No. HKDF's salt is a non-secret value that strengthens the internal extract step; RFC 5869 allows it to be public and even omitted. You may pass `nil`, and `crypto/hkdf` substitutes a zero-filled salt as long as the hash output. Because it is not secret you can keep it in config beside the code — but it must be identical on every machine that has to derive the same key.
  • Do you have to store the keys once you have derived them?
    No. HKDF is deterministic, so the same hash, secret, salt, info and length always produce the same bytes. The library derives on demand and only the master secret needs protecting, which is the main operational reason to use a KDF at all. It is also why the info string becomes a permanent contract: change it and you get a different key.
  • What does `hkdf.Key` actually return an error for?
    Essentially one thing: a `keyLength` greater than the hash's `Size()` times 255, which is 8160 bytes with SHA-256. A `nil` salt, an empty `info` string and a two-byte secret are all accepted and return a key with a nil error, so any stricter validation has to be in the wrapper you write around it.

One master key stamped through differently labelled stencils. The stencil decides which key comes out, and you can always re-cut the same key from the same stencil, so there is nothing extra to lock away.

saying these in an interview costs you the question

  • Thinks the info string has to be kept secret
  • Stores every derived key instead of re-deriving it
  • Passes the same info string for every purpose
  • Believes hkdf.Key strengthens a low-entropy passphrase
  • Separates purposes by varying the salt instead of the info label
open as a page

In Go, why use crypto/pbkdf2 rather than hkdf.Key to derive a key from a user passphrase?

level: middleimportance: must knowfreq 52%

basics

~20 s

HKDF has no work factor: hkdf.Key runs only a couple of hash operations, so guessing a weak passphrase stays cheap. pbkdf2.Key takes an explicit iteration count that makes every guess as costly as you choose.

open as a page

In crypto/hkdf, when would you call Extract and Expand yourself instead of hkdf.Key?

level: middleimportance: should knowfreq 38%

basics

~20 s

hkdf.Key is Extract followed by Expand in one call. Call the halves yourself when one secret feeds many keys: Extract once to get a pseudorandom key, then Expand it repeatedly with different info strings, skipping the repeated extraction.

open as a page

Your hkdf.Key wrapper let an empty info string through and two features now derive the same key — how do you catch this?

level: seniorimportance: should knowfreq 30%

basics

~20 s

crypto/hkdf validates only the requested key length, so an empty info string derives silently and every purpose that omits it shares one key. Audit the info string at every call site, and make the wrapper reject empty and unregistered labels.

open as a page

You own a Go library wrapping hkdf.Key for ten teams — how do you shape the info-string namespace you can never change?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Treat info strings as a versioned public namespace — owner, purpose, algorithm, version — allocated centrally, and export named accessors rather than a raw string parameter. Changing a label later means re-deriving everything that key protects.

open as a page