Python · Course navigation

Message authentication with HMAC

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

On this page
Before you start

Hashing and bytes.

  • Attach a keyed tag to a message so that tampering is detected by anyone who holds the key.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

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.

Code example

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

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
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

Behind the scenes

How Python processes this code

Python runs this source through its interpreter. It creates objects for values and resolves names as each statement runs.

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