HMAC
Chapter Forty-Nine
Syllabus topic Module 1, "Message Authentication and Hash Functions: HMAC"
Pages 298 to 305 of 678
In one line
Hash the message twice, with the key mixed in differently each time. That nesting is what makes a hash into a message authentication code, and it is what the naive H(key || message) fails to do.
In the wording a student can write in an examination: HMAC is a mechanism for message authentication using cryptographic hash functions, specified in RFC 2104 and in FIPS PUB 198-1. For a hash H with block size B, a key K and text, define K0 as K padded with zeros to B bytes, or, if K is longer than B, as H(K) padded to B bytes. Let ipad be the byte 0x36 repeated B times and opad the byte 0x5C repeated B times. Then
HMAC(K, text) = H((K0 XOR opad) || H((K0 XOR ipad) || text))
Why HMAC exists
Two reasons, and the second is the interesting one.
Reason 1: hash functions are fast and available. In the 1990s, hash functions ran considerably faster than block ciphers in software, and hash code was freely available where cipher code was export-controlled. A MAC built on a hash was therefore cheaper and easier to deploy than one built on a cipher.
Reason 2: the obvious way of doing it is broken. H(key || message) is forgeable, and H(message || key) has its own problem: a collision in H gives two messages with the same tag under any key. HMAC's nesting avoids both, with a proof: if the underlying compression function is a secure MAC on fixed-length inputs, HMAC is a secure MAC on arbitrary-length inputs. That proof is why HMAC is used rather than something simpler.
The construction, and the attack it defends against
# HMAC, and the length-extension attack that explains why it is nested.
# SHA-256 is written compactly here, in a form that can be CONTINUED from a
# known state, because that is what the attack in section 5 needs and what the
# previous chapter's module does not expose.
import hashlib
import hmac as pyhmac
K = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]
IV = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
def rr(x, n):
return ((x >> n) | (x << (32 - n))) & 0xFFFFFFFF
def blocks(h, data):
"""Compress whole 64-byte blocks into the state h."""
for off in range(0, len(data), 64):
w = [int.from_bytes(data[off + 4 * i:off + 4 * i + 4], "big") for i in range(16)]
for t in range(16, 64):
s0 = rr(w[t - 15], 7) ^ rr(w[t - 15], 18) ^ (w[t - 15] >> 3)
s1 = rr(w[t - 2], 17) ^ rr(w[t - 2], 19) ^ (w[t - 2] >> 10)
w.append((w[t - 16] + s0 + w[t - 7] + s1) & 0xFFFFFFFF)
a, b, c, d, e, f, g, hh = h
for t in range(64):
t1 = (hh + (rr(e, 6) ^ rr(e, 11) ^ rr(e, 25))
+ ((e & f) ^ (~e & 0xFFFFFFFF & g)) + K[t] + w[t]) & 0xFFFFFFFF
t2 = ((rr(a, 2) ^ rr(a, 13) ^ rr(a, 22))
+ ((a & b) ^ (a & c) ^ (b & c))) & 0xFFFFFFFF
a, b, c, d, e, f, g, hh = ((t1 + t2) & 0xFFFFFFFF, a, b, c,
(d + t1) & 0xFFFFFFFF, e, f, g)
h = [(x + y) & 0xFFFFFFFF for x, y in zip(h, [a, b, c, d, e, f, g, hh])]
return h
def padding(total_len):
"""FIPS 180-4's padding for a message of total_len bytes."""
return b"\x80" + b"\x00" * ((-total_len - 9) % 64) + (total_len * 8).to_bytes(8, "big")
def sha256(data, state=None, already=0):
"""The digest; with state and already set, CONTINUES from that state."""
h = list(state) if state else list(IV)
h = blocks(h, data + padding(already + len(data)))
return b"".join(x.to_bytes(4, "big") for x in h)
print("the compact SHA-256 agrees with the standard library:",
sha256(b"abc").hex() == hashlib.sha256(b"abc").hexdigest())
print()
def hmac(key, message, block=64):
"""FIPS 198-1 section 4: H((K0 XOR opad) || H((K0 XOR ipad) || text))."""
if len(key) > block:
key = sha256(key)
k0 = key + b"\x00" * (block - len(key))
ipad = bytes(b ^ 0x36 for b in k0)
opad = bytes(b ^ 0x5C for b in k0)
return sha256(opad + sha256(ipad + message))
print("RFC 4231's test vectors for HMAC-SHA-256:")
for n, (k, m, want) in enumerate((
(b"\x0b" * 20, b"Hi There",
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"),
(b"Jefe", b"what do ya want for nothing?",
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"),
(b"\xaa" * 131, b"Test Using Larger Than Block-Size Key - Hash Key First",
"60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54")), 1):
got = hmac(k, m).hex()
print(" case %d: %s matches the RFC: %s" % (n, got[:32] + "...", got == want))
print(" and against the standard library on a fourth input:",
hmac(b"key", b"message") == pyhmac.new(b"key", b"message", hashlib.sha256).digest())
print()
print("the two pads are constants that differ in half their bits:")
print(" ipad = 0x36 = %s" % format(0x36, "08b"))
print(" opad = 0x5c = %s" % format(0x5C, "08b"))
print(" their XOR = %s, which is %d bits of 8"
% (format(0x36 ^ 0x5C, "08b"), bin(0x36 ^ 0x5C).count("1")))
print(" so the inner and outer keys behave as two independent keys from one.")
print()
print("a key longer than the block is HASHED; a shorter one is ZERO PADDED:")
for k in (b"short", b"x" * 64, b"x" * 131):
k0 = sha256(k) if len(k) > 64 else k
print(" key of %3d bytes -> %3d bytes, then zero padded to 64"
% (len(k), len(k0)))
print(" Note that two different keys can give the same K0: a key and the same")
print(" key with zeros appended. Use a key of exactly the digest length.")
print()
print("WHY THE NESTING. The obvious construction, H(key || message), is FORGEABLE.")
print("A Merkle and Damgard hash's digest IS its internal state, so an attacker")
print("who has the tag and knows the key's LENGTH can continue the computation.")
SECRET = b"sixteen byte key"
message = b"amount=500&to=8817"
naive = sha256(SECRET + message)
print(" the naive tag H(key || message) =", naive.hex()[:40] + "...")
state = [int.from_bytes(naive[4 * i:4 * i + 4], "big") for i in range(8)]
glue = padding(len(SECRET) + len(message))
extra = b"&to=9999"
forged = sha256(extra, state=state, already=len(SECRET) + len(message) + len(glue))
print(" the attacker appends %r and, WITHOUT the key, computes" % extra.decode())
print(" forged tag =", forged.hex()[:40] + "...")
print(" the true tag for that message =",
sha256(SECRET + message + glue + extra).hex()[:40] + "...")
print(" identical :",
forged == sha256(SECRET + message + glue + extra))
print(" and the message the server will accept is")
print(" amount=500&to=8817 || padding || &to=9999")
print(" which most parsers read as to=9999.")
print()
print("HMAC is not forgeable this way, because the outer hash is applied to a")
print("FIXED-LENGTH inner digest, so there is no state for an attacker to extend.")
print()
print("and a tag must be compared in CONSTANT TIME:")
print(" a comparison that stops at the first wrong byte leaks how many bytes")
print(" were right, and an attacker who can time it recovers the tag byte by")
print(" byte, needing 256 tries per byte instead of 2 to the power 256.")
print(" Python's hmac.compare_digest exists for this; == does not do it.")HMAC
the compact SHA-256 agrees with the standard library: True
RFC 4231's test vectors for HMAC-SHA-256:
case 1: b0344c61d8db38535ca8afceaf0bf12b... matches the RFC: True
case 2: 5bdcc146bf60754e6a042426089575c7... matches the RFC: True
case 3: 60e431591ee0b67f0d8a26aacbf5b77f... matches the RFC: True
and against the standard library on a fourth input: True
the two pads are constants that differ in half their bits:
ipad = 0x36 = 00110110
opad = 0x5c = 01011100
their XOR = 01101010, which is 4 bits of 8
so the inner and outer keys behave as two independent keys from one.
a key longer than the block is HASHED; a shorter one is ZERO PADDED:
key of 5 bytes -> 5 bytes, then zero padded to 64
key of 64 bytes -> 64 bytes, then zero padded to 64
key of 131 bytes -> 32 bytes, then zero padded to 64
Note that two different keys can give the same K0: a key and the same
key with zeros appended. Use a key of exactly the digest length.
WHY THE NESTING. The obvious construction, H(key || message), is FORGEABLE.
A Merkle and Damgard hash's digest IS its internal state, so an attacker
who has the tag and knows the key's LENGTH can continue the computation.
the naive tag H(key || message) = a00f22d1eacfb4313a7fb15d76010065cb56790a...
the attacker appends '&to=9999' and, WITHOUT the key, computes
forged tag = 68dd1e8ed235322604c51f5cf7ecd5320c3cf2c9...
the true tag for that message = 68dd1e8ed235322604c51f5cf7ecd5320c3cf2c9...
identical : True
and the message the server will accept is
amount=500&to=8817 || padding || &to=9999
which most parsers read as to=9999.
HMAC is not forgeable this way, because the outer hash is applied to a
FIXED-LENGTH inner digest, so there is no state for an attacker to extend.
and a tag must be compared in CONSTANT TIME:
a comparison that stops at the first wrong byte leaks how many bytes
were right, and an attacker who can time it recovers the tag byte by
byte, needing 256 tries per byte instead of 2 to the power 256.
Python's hmac.compare_digest exists for this; == does not do it.HMAC
Read six things out of that run.
HMAC
The compact SHA-256 agrees with the standard library, checked on the first line before anything is built on it.
All three RFC 4231 vectors match, including the 131-byte key, which is longer than the 64-byte block and so must be hashed first. That case is the one implementations get wrong.
The two pads differ in four of eight bits, so K0 XOR ipad and K0 XOR opad differ in half their bits. That is the point of using two constants: the inner and outer computations behave as though they used two independent keys, derived from one.
A key longer than the block is hashed and a shorter one zero-padded, and the run notes the consequence: a key and the same key with zeros appended give the same K0, so they give the same tags. RFC 2104 notes it, and the practical rule is to use a key of exactly the digest length.
And the length-extension forgery succeeds. Given H(secret || message) and the length of the secret, the attacker set the hash's state to the tag, continued the computation with &to=9999, and produced a tag that matches the true tag for the extended message exactly. No key. The message the server sees is amount=500&to=8817, then the padding bytes, then &to=9999, and most parsers take the last value.
HMAC
HMAC is not forgeable that way, because the outer hash is applied to the inner digest, which is a fixed-length value. There is no state in the output that corresponds to an extendable message, so there is nothing to continue.
Why the nesting works, in the shape an examiner wants
The inner hash produces a fixed-length digest of the key-prefixed message. Whatever length-extension property the hash has applies to that inner computation, and the attacker never sees its output.
The outer hash is applied to a fixed-length input: B bytes of keyed pad plus the inner digest. So the outer computation is a single-block or two-block hash of a fixed size, and length extension is meaningless on it: there is no message length to extend.
And the two keyed pads are effectively two keys. So an attacker cannot use a relation found in the inner computation against the outer one.
One efficiency note worth knowing: K0 XOR ipad and K0 XOR opad are each one block long and depend only on the key, so their compression can be precomputed once and reused for every message. HMAC therefore costs the hash of the message plus about one extra block, not two full hashes.
Two rules about using it
Truncation is allowed and is bounded. FIPS 198-1 permits the output to be truncated to t bytes. A shorter tag is cheaper and weaker: an attacker's chance of guessing a tag is 1 in 2 to the power 8t. The standard requires t to be at least 4 bytes and recommends at least half the digest length.
The comparison must be constant time. A comparison that returns as soon as two bytes differ tells an attacker how many leading bytes were right. With that, a tag is recovered byte by byte, at 256 tries per byte rather than 2 to the power 256 for the whole tag. A 32-byte tag falls in about 8,192 queries instead of never. The run's last lines say so, and the library function for it exists precisely because the obvious comparison is unsafe.
Worked example: where HMAC goes in a protocol
The requirement. A college API accepts amount and to parameters and must reject any request not issued by the college's own system.
The wrong design. tag = SHA256(secret + parameters), tag sent alongside. This is the design the run forges. An attacker who has seen one legitimate request appends &to=9999 and computes a valid tag.
The second wrong design. tag = SHA256(parameters + secret). No length extension, but a collision in SHA-256 gives two parameter strings with the same tag under every key, so the day the hash falls the scheme falls with it, for all keys ever used.
HMAC
The right design. tag = HMAC-SHA256(secret, canonical_parameters), where canonical_parameters is a form that cannot be parsed two ways: parameters sorted, lengths included, separators that cannot appear in a value. The canonicalisation matters as much as the HMAC, because the forgery above worked partly by making a string that two parsers read differently.
And three more things. Include a timestamp inside the authenticated data and reject stale requests, or replay is still possible. Compare the tag in constant time. And use a key of 32 bytes, exactly the digest length.
The step that carries the marks. Naming all four: HMAC, canonicalisation, a timestamp inside the coverage, and a constant-time comparison. Three of the four are not about the MAC at all, and a scheme that gets only the MAC right is still broken.
Distinctions that carry marks
The first two columns are the two naive constructions: hash the key followed by the message, and hash the message followed by the key. A table cell cannot carry the concatenation symbol inside a code span, so they are named in words.
| Key then message, hashed once | Message then key, hashed once | HMAC | |
|---|---|---|---|
| Length extension forgery | yes, performed in this chapter | no | no |
| A hash collision breaks it | yes | yes, for every key | no |
| Has a security proof | no | no | yes |
| Specified in a standard | no | no | FIPS 198-1, RFC 2104 |
| HMAC | CMAC | |
|---|---|---|
| Built on | a hash function | a block cipher |
| Standard | FIPS 198-1, RFC 2104 | SP 800-38B |
| Keys used | two derived from one, by ipad and opad | two derived from one, for the final block |
| Choose it when | a hash is already present, or in software | a block cipher is already present, as in hardware |
| ipad | opad | |
|---|---|---|
| Byte | 0x36 | 0x5C |
| Binary | 00110110 | 01011100 |
| Used in | the inner hash | the outer hash |
| They differ in | four of eight bits, so the two derived keys differ in half their bits |
What beginners get wrong here
Writing H(key || message) as HMAC. That is the construction HMAC exists to replace, and this chapter forges it.
Getting the pads the wrong way round. 0x36 is ipad and goes in the inner hash; 0x5C is opad and goes in the outer one.
Forgetting that a long key is hashed first. A key longer than the block is replaced by its own digest. RFC 4231's third test case checks exactly this.
Comparing tags with ==. It leaks timing and the tag falls byte by byte in a few thousand queries.
Thinking HMAC needs a collision-resistant hash. It does not, which is why HMAC-SHA-1 remains acceptable after SHA-1's collision.
HMAC
Quick revision
HMAC(K, text) = H((K0 XOR opad) || H((K0 XOR ipad) || text)), from FIPS 198-1 and RFC 2104.K0is the key zero-padded to the block size, orH(K)padded if the key is longer than the block.ipadis0x36repeated, for the inner hash;opadis0x5C, for the outer. They differ in four of eight bits, so the two derived keys differ in half their bits.H(key || message)is forgeable by length extension, performed in this chapter: the tag is the hash's state, so an attacker who knows the key's length extends the message and computes a valid tag without the key.H(message || key)is not extendable but falls to a hash collision, for every key.- HMAC's nesting defeats both, and it has a proof: a secure fixed-length compression function gives a secure arbitrary-length MAC.
- The keyed pads depend only on the key, so their compression is precomputed: HMAC costs one hash plus about a block.
- Truncation to
tbytes is allowed, at least 4 and preferably half the digest; guessing costs 1 in 2 to the power 8t. - Compare tags in constant time. A byte-by-byte comparison recovers a 32-byte tag in about 8,192 queries.
- HMAC does not need collision resistance, which is why HMAC-SHA-1 is still acceptable.
Test yourself
1. Write the HMAC construction and define each part. HMAC(K, text) = H((K0 XOR opad) || H((K0 XOR ipad) || text)). H is a cryptographic hash function with block size B. K0 is the key padded with zeros to B bytes, or H(K) padded to B bytes if the key is longer than B. ipad is the byte 0x36 repeated B times and opad is 0x5C repeated B times.
2. Why is H(key || message) not a secure MAC? Because an iterated hash's digest is its internal chaining state. An attacker who has the tag and knows the length of the key can set the state to that tag and continue the computation with data of their choosing, producing a valid tag for the original message followed by the hash's padding and their own addition, without ever knowing the key. The chapter performs this forgery and the forged tag matches the true one exactly.
3. Why is H(message || key) also unsatisfactory? Because a collision in the hash gives two messages with the same tag under every possible key: if two messages hash identically then appending the same key to both yields identical inputs to the final compression. So a single published collision would break the scheme for all users and all keys at once.
HMAC
4. What are ipad and opad, and why are there two of them? ipad is the byte 0x36 repeated to the block size and opad is 0x5C repeated likewise. Two are used so that the inner and outer hashes are keyed differently: the two bytes differ in four of their eight bits, so K0 XOR ipad and K0 XOR opad differ in about half their bits and behave as two independent keys derived from one.
5. What happens to a key longer than the hash's block size, and what subtlety follows? It is replaced by its own hash and then zero-padded to the block size. The subtlety is that zero padding means a key and the same key with trailing zero bytes appended produce the same K0, and therefore the same tags, so they are not distinct keys. The practical rule is to use a key of exactly the digest length.
6. How is HMAC made efficient? The two keyed pads, K0 XOR ipad and K0 XOR opad, are each exactly one block long and depend only on the key, so the compression of each can be computed once and reused for every message under that key. HMAC therefore costs the hash of the message plus roughly one extra block, rather than two complete hashes.
7. Why must an HMAC tag be compared in constant time? Because a comparison that stops at the first differing byte takes a time that depends on how many leading bytes matched. An attacker who can measure that recovers the tag one byte at a time, needing about 256 attempts per byte instead of searching the whole tag space, so a 32-byte tag falls in a few thousand queries. A comparison that always examines every byte removes the signal.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.