munotes®

HMAC

Get access to whole semester resourcesSemester Pass

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.")
munotes.in298

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.
munotes.in299

HMAC

Read six things out of that run.

munotes.in300

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.

munotes.in301

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.

munotes.in302

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 onceMessage then key, hashed onceHMAC
Length extension forgeryyes, performed in this chapternono
A hash collision breaks ityesyes, for every keyno
Has a security proofnonoyes
Specified in a standardnonoFIPS 198-1, RFC 2104
HMACCMAC
Built ona hash functiona block cipher
StandardFIPS 198-1, RFC 2104SP 800-38B
Keys usedtwo derived from one, by ipad and opadtwo derived from one, for the final block
Choose it whena hash is already present, or in softwarea block cipher is already present, as in hardware
ipadopad
Byte0x360x5C
Binary0011011001011100
Used inthe inner hashthe outer hash
They differ infour 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.

munotes.in303

HMAC

Quick revision

  • HMAC(K, text) = H((K0 XOR opad) || H((K0 XOR ipad) || text)), from FIPS 198-1 and RFC 2104.
  • K0 is the key zero-padded to the block size, or H(K) padded if the key is longer than the block.
  • ipad is 0x36 repeated, for the inner hash; opad is 0x5C, 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 t bytes 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.

munotes.in304

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.

munotes.in305

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.

Issue
Done!