Message authentication with HMAC

Attach a keyed tag to a message so that tampering is detected by anyone who holds the key.

Python
import hashlib
import hmac

key = b"shared-secret-key"


def sign(message):
    return hmac.new(key, message, hashlib.sha256).hexdigest()


def verify(message, tag):
    return hmac.compare_digest(sign(message), tag)


message = b"amount=100&to=alice"
tag = sign(message)
print(verify(message, tag))
print(verify(b"amount=900&to=alice", tag))
print(verify(message, "0" * 64))
print(len(tag))
Output
True
False
False
64

Explanation

A message authentication code (MAC) is a short tag computed from a message and a secret key. A receiver who has the same key recomputes the tag. If it matches, the message is intact and came from someone who knows the key. Without the key, an attacker cannot produce a valid tag for a changed message.

HMAC builds a MAC from a hash function. It is defined in RFC 2104 and has the form HMAC(K, m) = H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ m)). HMAC is not the same as hashing key + message, because that simple construction is vulnerable to length-extension attacks with hashes such as SHA-256; HMAC is not.

A MAC gives integrity and authenticity between parties that share a key, but not non-repudiation: either party could have created a valid tag, so it cannot prove who sent it. Digital signatures, which use a private and a public key, provide that. The program signs a payment request, verifies it, and shows that a changed message or a guessed tag is rejected. Always compare tags with hmac.compare_digest. Keys must be random, secret, and long enough, for example 32 random bytes; the string used here is only a readable stand-in.

Explore the dry run
Guided walkthroughReady
1import hashlib
2import hmac
3
4key = b"shared-secret-key"
5
6
7def sign(message):
8 return hmac.new(key, message, hashlib.sha256).hexdigest()
9
10
11def verify(message, tag):
12 return hmac.compare_digest(sign(message), tag)
13
14
15message = b"amount=100&to=alice"
16tag = sign(message)
17print(verify(message, tag))
18print(verify(b"amount=900&to=alice", tag))
19print(verify(message, "0" * 64))
20print(len(tag))

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
HMAC-SHA256 mixes the secret key into the hash, producing a 64-hex-character tag.—
—
The receiver recomputes the tag with the shared key and gets the same value.—
True
A changed message yields a different tag, so the original tag no longer matches.—
True
False
A guessed tag is rejected because the attacker does not know the key.—
True
False
False
SHA-256 produces a 32-byte tag, which is 64 hexadecimal characters.—
True
False
False
64

Cost and common mistakes

Computing a tag is O(n) in the message length. Verification costs the same.

  • Do not build a MAC as hash(key + message). Use HMAC, which is designed to avoid length-extension attacks.
  • Compare tags with hmac.compare_digest, not ==, to avoid timing side channels.
  • A MAC does not hide the message. It protects integrity and authenticity, not confidentiality.

Try it yourself

What does a MAC prove that a plain SHA-256 hash of the message does not?

Reveal answer and reasoning

That the message was produced by someone who knows the secret key. Anyone can compute a plain hash, so an attacker could replace both message and hash.

Further reading