Crypto on the Command Line, Part 6: HMAC
An HMAC (hash-based message authentication code) is a hash mixed with a shared key: anyone can hash a message, and only a holder of the key can compute
its tag. This post runs the HMAC scene of the
Security Primitives
tool with
openssl, and the tags are the ones the tool shows:
- what the name means, and where HMAC is used;
- making a tag and checking it, and checking a webhook the same way;
- building the same tag by hand, from two SHA-256 runs;
- a tag for a message Alice never wrote, made by Bob;
- an HMAC over the AES ciphertext from Part 2 , which catches Mallory's change.
HMAC: hash-based message authentication code
The name says what the tag is made from and what it is for:
- Message authentication code (MAC): a short tag sent beside a message, which proves two things to
whoever holds the key:
- the message was not changed on the way;
- it was made by a holder of the key.
- Hash-based: the tag is made by a hash function, here SHA-256, with the key mixed in. HMAC-SHA256 is HMAC on SHA-256; HMAC-SHA1 and HMAC-SHA512 are the same recipe on other hashes.
- A MAC hides nothing: the message travels in the clear, as beside a signature.
- The tag is not a key: it is 32 bytes computed from the message and the key, like a digest, and it holds neither.
HMAC: a symmetric primitive
HMAC is symmetric, like AES: the same key makes the tag and checks it, and both ends hold that key. No public key and no certificate takes part, so it is not public-key cryptography and not part of a PKI.
- The shared key has to reach both ends first, in one of three ways:
- Public-key cryptography only delivers the key. Once both ends hold it, they use it two ways:
- AES with it hides the message (secrecy);
- HMAC with it shows the message was not changed, and who made it (integrity and authenticity).
- One key per job: TLS turns the one agreed secret into separate keys for encryption and for the MAC,
in each direction. The last section of this post does the same with
purple.keyandmac.key.
Where HMAC is used
HMAC is used wherever two parties already share a secret and one must know that a message came from the other, unchanged:
- Webhooks: GitHub, Stripe and Slack put an HMAC of the request body in a header, made with a secret shared when the webhook was set up; the receiving server computes it again before acting on the request.
- API requests: AWS signs every API call (Signature Version 4) with an HMAC chain keyed by the secret access key, so the key itself never crosses the wire.
- Tokens and cookies: an
HS256JWT and the session cookie of Flask, Django or Rails carry an HMAC tag, so the server knows a cookie came back exactly as it issued it. - One-time codes: an authenticator app's six digits are an HMAC of the current 30-second interval, keyed by the secret in the QR code (TOTP).
- Building keys: HKDF, which turns a shared secret into keys ( Part 7 ), and PBKDF2, which turns a password into a key, are both built from HMAC.
- TLS: TLS 1.2 tagged each record with HMAC; TLS 1.3 uses HMAC to derive its keys and to check the
handshake's
Finishedmessages.
The tools on Ubuntu 26.04
The commands are shown as a terminal session:
$starts a command: type what follows it;>marks a line that continues the command above it, for a command too long for one line;- a line with neither is output. A command with no lines under it prints nothing.
openssl (3.5 on 26.04) and xxd; if either is missing:
1$ sudo apt update
2$ sudo apt install openssl xxd
3$ mkdir -p ~/crypto-lab && cd ~/crypto-lab
Making a tag
Alice and Bob share the key purple; a real key would be 32 random bytes, as in Part 2. -hmac gives the key
to openssl dgst:
1$ printf 'pay carol 10' > msg.txt
2$ openssl dgst -sha256 -hmac purple msg.txt
3HMAC-SHA2-256(msg.txt)= a1c2c39998d9cd956ccae43506157ae2400ec7c9714f8d24e01835f71571df85
- The tag is 32 bytes, as a SHA-256 digest is.
- Alice sends the message and the tag. Mallory can change the message, but she cannot compute the tag for the new one: she lacks the key.
openssl mac -digest SHA256 -macopt key:purple -in msg.txt HMACis the newer spelling and prints the same tag in capitals.
Checking a tag
To check is to compute again. Bob runs the same command with his copy of the key and compares the result with the tag that came with the message; equal tags mean the message is the one Alice tagged.
Checking needs the same key as making, which is what makes an HMAC different from a signature ( Part 5 ): there, checking takes only the public key.
Checking a webhook
A webhook is the same check on a web server. Here the shared secret is whsec-4f1a, the body is a small
JSON document, and the sender puts the tag in a header such as GitHub's X-Hub-Signature-256: sha256=…:
1$ printf '{"action":"opened","number":7}' > body.json
2$ openssl dgst -sha256 -hmac 'whsec-4f1a' body.json
3HMAC-SHA2-256(body.json)= 85b856d04494e6b9f7f537f05f3fff110e7da945606692c95617d832628271a2
- The receiving server runs the same computation on the body it received and compares the result with the header; anything else is dropped.
- The tag covers the body's exact bytes: the server must check the body as it arrived, before parsing it and writing it out again.
- The comparison should take the same time whether the first byte differs or the last (in Python,
hmac.compare_digest), so an attacker cannot find the tag one byte at a time by timing the answers.
What a tag proves to the receiver
Bob computes the tag of the message he received with his copy of the key, and compares it with the tag that came with it. Equal tags tell him two things:
- The message was not changed on the way: a change to any bit gives a different tag.
- A holder of the key made it: only Alice and Bob hold the key, and Bob knows he did not make this tag, so Alice did.
Mallory cannot get past this the way she got past a plain digest in Part 1 : she can change the message, but without the key she cannot compute its new tag.
A tag is not a signature. Both prove the message unchanged and say who made it, but a tag proves it only to the other holder of the key:
| Digest | HMAC tag | Signature | |
|---|---|---|---|
| Made with | nothing | the shared key | the sender's private key |
| Checked with | nothing | the same shared key | the sender's public key |
| Who can check | anyone | only the key's holders | anyone |
| Who can make one | anyone | every holder, the checker included | only the sender |
| Proves the message unchanged | only if the digest arrives safely | yes | yes |
| Proves who made it | no | only to the other holder | to anyone |
- A tag convinces nobody else. Bob shows a judge Alice's message and its tag; Bob holds the key, so he could have made the tag himself, as A tag either end could have made shows.
- A signature convinces anyone: only the private key could have made it, and anyone can check it with the public key. So Alice cannot deny a signature she made (non-repudiation); she can deny a tag.
- HMAC is chosen where both ends already share a key: it is much faster than a signature and its tag is shorter, and a webhook, a cookie or a TLS record only has to convince its one receiver.
Building the tag by hand
HMAC is SHA-256 run twice, with the key mixed into each run. With 64-byte blocks (SHA-256's), the recipe is:
- pad the key with zero bytes to 64;
- XOR it with
36bytes (the inner pad) and with5cbytes (the outer pad); - inner hash: SHA-256 of the inner pad followed by the message;
- outer hash: SHA-256 of the outer pad followed by the inner hash. That is the tag.
The key purple padded to 64 bytes, in hex:
1$ key=$(printf purple | xxd -p)
2$ while [ ${#key} -lt 128 ]; do key+=00; done
3$ echo $key
4707572706c6500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
The two pads, one byte at a time:
1$ ipad=; opad=
2$ for i in $(seq 0 2 126); do
3> b=0x${key:i:2}
4> ipad+=$(printf %02x $(( b ^ 0x36 )))
5> opad+=$(printf %02x $(( b ^ 0x5c )))
6> done
7$ echo $ipad
8464344465a5336363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636
9$ echo $opad
102c292e2c30395c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c
The inner hash, then the outer one; xxd -r -p turns the hex back into bytes:
1$ { echo $ipad | xxd -r -p; cat msg.txt; } | openssl dgst -sha256 -binary > inner.bin
2$ { echo $opad | xxd -r -p; cat inner.bin; } | sha256sum
3a1c2c39998d9cd956ccae43506157ae2400ec7c9714f8d24e01835f71571df85 -
The tag openssl dgst -hmac printed.
- Why not one hash of the key and the message: SHA-256's digest is its internal state at the end, so anyone holding the digest of key‖message can carry on hashing and get the digest of key‖message‖more, without the key (a length extension). The outer hash closes that.
- A key longer than 64 bytes is first hashed to 32, then padded the same way.
A tag either end could have made
Bob holds the same key, so he can tag a message Alice never wrote:
1$ printf 'pay bob 99' | openssl dgst -sha256 -hmac purple
2SHA2-256(stdin)= 44505002f445e5ad495407c0a053620c200cbfe3b5a1187c8078a82598f17ecd
- The tag is correct, so a judge shown it cannot tell whether Alice or Bob made it.
- An
HS256JWT is an HMAC tag: every service that checks one could also make one.
Catching a changed ciphertext
In
Part 2
Mallory changed pay carol 10 to pay carol 90 by
flipping one bit of the IV, without the key. An HMAC over the IV and the ciphertext catches it. Alice
encrypts as in Part 2, then tags what she sends with a second key, mac.key:
1$ openssl rand -hex 32 > purple.key
2$ openssl rand -hex 32 > mac.key
3$ openssl rand -hex 16 > msg.iv
4$ openssl enc -aes-256-cbc -K $(cat purple.key) -iv $(cat msg.iv) -in msg.txt -out msg.enc
5$ { xxd -r -p msg.iv; cat msg.enc; } | openssl dgst -sha256 -hmac $(cat mac.key) -r | cut -c1-64 > msg.tag
6$ cat msg.tag
7a00c0a972efe89052bfd9bf353470226e9c40f7ed4fa908d20f78b6a86c430fb
Mallory flips the bit, and Bob computes the tag of the IV and ciphertext he received before decrypting:
1$ iv=$(cat msg.iv)
2$ iv2=${iv:0:20}$(printf %02x $(( 0x${iv:20:2} ^ 0x08 )))${iv:22}
3$ { echo $iv2 | xxd -r -p; cat msg.enc; } | openssl dgst -sha256 -hmac $(cat mac.key) -r | cut -c1-64
47b0cc05473a920c0c0c14550f1228e4da63783228981d14b269c173757471dec
- The tags differ, so Bob discards the message without decrypting it.
- This order is encrypt-then-MAC: the tag covers exactly the bytes on the wire.
- Two keys, one for each job, is the rule for this construction.
- AES-GCM builds a tag into the cipher, so TLS needs no separate HMAC.