Wiki / Concepts

HMAC

A keyed hash that proves a message came from someone holding the key and was not altered on the way. Its odd nested shape exists to kill length extension, and its verification must be constant-time or the tag leaks a byte at a time.

What each side does with the shared key
Message m, shared key K
Sender: tag = HMAC(K, m), transmits (m, tag)
m travels in the clear, the tag is not encryption
Receiver: recomputes HMAC(K, m) and compares
Compare in constant time, then accept or drop in silence
Both sides run the same function with the same key, which is what makes a MAC symmetric: it proves the message came from someone in the group that holds the key, never which member, and it is therefore not a signature. It also says nothing about when the message was sent, so a captured (m, tag) pair replays perfectly.

HMAC (Hash-based Message Authentication Code, RFC 2104 and FIPS 198-1) is a message authentication code: a short tag computed from a message and a secret key, which anyone holding the key can recompute to check that the message is authentic and unmodified. It is the default keyed primitive in everything from TLS to JWTs to firmware update manifests.

The construction, and why it is nested

HMAC(K, m) = H( (K' XOR opad) || H( (K' XOR ipad) || m ) )

K' is the key padded with zero bytes up to the hash's block size (64 bytes for SHA-1 and SHA-256, 128 for SHA-384 and SHA-512), or the hash of the key first if the key is longer than a block. ipad is the byte 0x36 repeated to the block size and opad is 0x5C repeated.

The double hashing is not decoration. Classic hashes (MD5, SHA-1, SHA-2) are Merkle-Damgard constructions, and they leak their internal state in their output. Given H(K || m) and the length of K, an attacker can resume the hash from that state and compute H(K || m || padding || suffix) for any suffix they like, without knowing K. That is the length extension attack, and it turns the obvious "prefix the key and hash" design into a forgery machine. Wrapping the inner hash in a second, keyed hash cuts that path: the attacker never sees the inner state.

Two consequences worth knowing:

  • SHA-3 and BLAKE2 are not length-extendable, so they can be keyed directly (KMAC, BLAKE2's keyed mode) and do not need the nesting.
  • HMAC's security proof rests on the compression function behaving like a PRF, not on collision resistance, which is why HMAC-MD5 was not immediately broken by MD5 collisions. It is still deprecated, and there is no reason to pick it today.

What it gives you, and what it does not

A valid tag proves integrity (the bytes did not change) and authenticity (whoever produced it held the key). It does not give you:

  • Confidentiality. The message travels in the clear unless you encrypt it separately.
  • Non-repudiation. Both parties hold the same key, so either could have produced the tag. That is the whole difference between a MAC and a signature.
  • Freshness. A recorded (message, tag) pair stays valid forever. Anything that must not be executed twice needs a counter, a nonce, or a timestamp inside the authenticated message. See Wiki: replay-attack.

And the thing that gets built wrong most often: a checksum masked with a secret is not a MAC. CRC is linear, so an attacker who can see one valid pair can compute the correction for a modified message without ever recovering the key. If the design says "CRC-32 XOR key", the design has no authentication.

Verification is where implementations fail

Comparing the received tag with the computed one must not short-circuit on the first differing byte. A naive memcmp returns faster for a wrong tag that shares a longer prefix with the right one, and with enough timed attempts an attacker recovers the correct tag byte by byte, never knowing the key. Use the constant-time comparison your language provides:

import hmac, hashlib

tag = hmac.new(key, message, hashlib.sha256).digest()
ok = hmac.compare_digest(tag, received)      # never ==, never memcmp

From the shell, the two OpenSSL generations differ and the old form is the one most tutorials still show:

# OpenSSL 3.x
openssl mac -digest SHA256 -macopt hexkey:00112233 HMAC < message.bin
# OpenSSL 1.1.1
openssl dgst -sha256 -mac hmac -macopt hexkey:00112233 message.bin

Truncating the tag is allowed: RFC 2104 permits it down to half the hash output and at least 80 bits. Truncating to 4 bytes because the packet was tight is how a protocol ends up brute-forceable online.

Where you meet it on embedded targets

  • JWT HS256 is HMAC-SHA256 over the header and payload. Two classic findings live there: an implementation that trusts the token's own alg field, and a key short enough to be recovered offline with a wordlist.
  • Wi-Fi WPA2 derives its keys with PBKDF2-HMAC-SHA1 and authenticates the handshake with a MIC computed from the KCK.
  • Industrial and space links bolt authentication on late: DNP3 Secure Authentication and the space-link security layers both add a keyed tag to a protocol that had none.
  • Firmware update manifests on small parts often use HMAC rather than a signature, because symmetric verification is cheap. The cost is that the verifying key is on the device, so one dump breaks the fleet. See Wiki: hardcoded-secrets.

Pitfalls

  • A fleet-wide shared key. HMAC is only as good as key separation. One extracted key authenticates messages to every device that shares it.
  • Authenticating the wrong bytes. A tag over the payload but not over the header, the length, or the recipient lets an attacker move a valid payload into a different context. Authenticate everything that changes the meaning.
  • Encrypt-then-MAC, not the reverse. MAC the ciphertext, verify before decrypting, and you never parse attacker-controlled plaintext. AEAD modes do this for you.
  • Nonce-based MACs are not interchangeable. GMAC and Poly1305 are one-time MACs: reuse a nonce with the same key and the key is recoverable. HMAC has no such requirement, which is exactly why it survives careless use.
  • A failed verification should be silent. Reporting "bad tag" versus "bad length" hands back an oracle.

Further reading