skip to content

How do you compute an HMAC-SHA256 tag in Go, and what must hmac.New's first argument be?

level: middleimportance: should knowfreq 50%

answer

  1. a keyed hash, not a plain digest
  2. the first argument is not a hash
  3. no parentheses after sha256.New
  4. the result is an ordinary hash.Hash
  5. key length is not constrained

basics

~20 s

Call hmac.New(sha256.New, key) - the first argument is a function that returns a fresh hash.Hash, written without parentheses. The result is an ordinary hash.Hash: write the message into it and call Sum(nil) for the 32-byte tag.

solid answer

~40 s

`hmac.New` has the signature `func New(h func() hash.Hash, key []byte) hash.Hash`. The first argument is a **constructor**, so you pass `sha256.New`, not `sha256.New()` - HMAC needs to build two independent instances of the underlying hash internally, and it needs to rebuild them on `Reset`. The second argument is the shared secret; any length is accepted, because HMAC hashes a key longer than the block size down and zero-pads a shorter one. What comes back is a plain `hash.Hash`, so the rest is identical to unkeyed hashing: `mac.Write(body)` (or `io.Copy(mac, r)`), then `mac.Sum(nil)` for a 32-byte tag. Calling `Reset()` returns it to the initial keyed state for the next message - the key is retained.

code

go · 5 lines
go
func tag(key, payload []byte) []byte {
	mac := hmac.New(sha256.New, key) // sha256.New, not sha256.New()
	mac.Write(payload)
	return mac.Sum(nil) // 32 bytes for SHA-256
}

go deeper

for a junior

Memorise the three lines: mac := hmac.New(sha256.New, key), mac.Write(payload), tag := mac.Sum(nil). Pass sha256.New with no parentheses - it is the function itself, not a call.

for a middle

Explain why the parameter is a constructor: HMAC runs the underlying hash twice over key-derived pads, so it must build fresh instances, including on Reset. Also know that any key length is accepted and the tag is always 32 bytes.

for a senior

Show you know what the tag covers - exactly the bytes written, in order - so the canonical byte form of a payload must be pinned down before anyone computes a tag, or two services will never agree.

for a principal

Own the key's lifecycle rather than the call: where a signing key lives, how it rotates without downtime, and whether each consumer gets its own key are the decisions this three-line snippet hides from view.

## A keyed hash, exposed as the same interface An ordinary SHA-256 digest is a function of the message alone: anyone who has the message can compute it. An HMAC tag is a function of the message **and a secret key**, so only holders of the key can produce it. Go puts that in `crypto/hmac`, and deliberately gives it the same shape as every other hash. ```go func New(h func() hash.Hash, key []byte) hash.Hash ``` ### The first argument is a function The single most common compile error here is writing `hmac.New(sha256.New(), key)`. The parameter type is `func() hash.Hash` - a **constructor**, not an instance. You write `sha256.New`, the function value itself. Why a constructor? HMAC is defined roughly as `H(outerKeyPad || H(innerKeyPad || message))`. That requires two separate runs of the underlying hash, each starting from a clean state, and it requires rebuilding the inner state whenever `Reset` is called. Handing over a factory lets `hmac` create as many fresh instances as it needs. It also removes an entire failure mode: you cannot accidentally pass in a hash that already has bytes written to it. The same factory shape means any `hash.Hash` implementation can be plugged in - `sha256.New`, `sha512.New`, `sha512.New384` - without `crypto/hmac` knowing anything about them. ### The key `key []byte` has **no length constraint**, which surprises people who expect "must be 32 bytes". HMAC normalises the key to the underlying hash's block size (64 bytes for SHA-256): a longer key is hashed first and the digest used instead, a shorter one is zero-padded up. So a 16-byte key and a 100-byte key are both legal. That said, the key should be uniformly random bytes from a cryptographic source, not a memorable string, and it should be scoped to one purpose. Deriving per-purpose keys from one master secret is its own topic; the point at the API level is that `hmac.New` will not tell you your key was weak. ### The result is just a hash.Hash ```go mac := hmac.New(sha256.New, key) mac.Write(body) tag := mac.Sum(nil) // 32 bytes ``` Because the return type is `hash.Hash`, everything you know already applies: - It embeds `io.Writer`, so `io.Copy(mac, r)` tags a stream, and `fmt.Fprintf(mac, ...)` works. - `Sum(b)` appends the tag to `b` and does **not** change the state, exactly as for an unkeyed hash. - `Size()` is the underlying hash's output size - 32 for SHA-256 - so the tag is 32 bytes, regardless of how long the key was. - `Reset()` returns the value to its initial **keyed** state, ready for a new message with the same key. It does not forget the key, and it does not need to be re-created per message. - It is not safe for concurrent use. One `mac` per request, or one per goroutine. ### What the tag actually covers Exactly the bytes written, in exactly that order - nothing more. If a payload can be serialised two ways (different JSON key ordering, optional whitespace, a header included or not), then the tag is over one specific byte sequence and the other side must reproduce that same sequence. Fixing the canonical byte form is a design decision you make before you write the first `Write` call, and getting it wrong shows up as tags that never line up between two services. ### Why not just hash the key and the message together `sha256.Sum256(append(key, body...))` looks equivalent and is not. SHA-256 is a Merkle-Damgard construction, which means someone who knows a digest and the length of its input can continue hashing from that state and produce a valid digest for a longer message - without ever seeing the key. Concatenation also blurs the boundary between key and message, so different key/message splits can produce the same input bytes. HMAC's two-pass keyed construction exists to close both holes, and it is three characters more typing. ### The shape to memorise Constructor, key, write, sum: ```go mac := hmac.New(sha256.New, key) mac.Write(payload) tag := mac.Sum(nil) ``` Everything else - streaming, prefixed output, reuse - follows from the fact that `mac` is nothing more exotic than a `hash.Hash` that happens to know a secret. Note that the constructor's type must be literally `func() hash.Hash`. A constructor that returns a concrete type - `crypto/sha3`'s `New256() *sha3.SHA3`, for instance - is not assignable to it, because Go function types are invariant; wrap it instead: `hmac.New(func() hash.Hash { return sha3.New256() }, key)`.

  • Why does hmac.New take func() hash.Hash instead of an already-constructed hash.Hash?
    Because HMAC runs the underlying hash twice - an inner pass over the key-derived pad plus the message, and an outer pass over the result - so it needs two independent instances, and it needs fresh ones again on `Reset`. A factory supplies them. It also guarantees `crypto/hmac` starts from a clean hash rather than one a caller has already written into.
  • Does the key have to be a particular length, and what is the tag length?
    The key can be any length: HMAC hashes a key longer than the block size (64 bytes for SHA-256) down to a digest, and zero-pads a shorter one up. The tag length is the underlying hash's output size, so HMAC-SHA256 always yields 32 bytes no matter what the key looked like.
  • What does Reset do on the value returned by hmac.New?
    It returns the value to its initial keyed state, ready to tag a new message with the same key - it does not clear or forget the key. That makes it cheap to reuse one `mac` for many messages in a single goroutine, though sharing one across goroutines is unsafe, as with any `hash.Hash`.
  • Why isn't sha256.Sum256 over the key followed by the message an acceptable substitute?
    That is an unkeyed digest of concatenated bytes. SHA-256's Merkle-Damgard structure lets someone who knows the digest and the input length extend the message and compute a valid digest without the key, and concatenation leaves the key/message boundary ambiguous. HMAC's two-pass keyed construction is specified to avoid exactly these.

A plain digest is a photo of a document - anyone can take one. An HMAC tag is a wax seal: it is made over the same document, but only whoever holds the stamp can produce it.

saying these in an interview costs you the question

  • Writes hmac.New(sha256.New(), key) and cannot explain the compile error
  • Believes HMAC is just SHA-256 over key plus message concatenated
  • Insists the key must be exactly 32 bytes
  • Expects hmac.New to return a special type rather than a hash.Hash
  • Thinks Reset clears the key
  • Assumes the tag length grows with the key length