Skip to content
Python Basics: Checking Message Tags with hmac

Python Basics: Checking Message Tags with hmac

October 21, 2026

A checksum detects a change only when you already trust the expected checksum. A message authentication code adds a shared secret key. Python’s hmac module lets two parties holding that key calculate and compare a tag for the same bytes.

These examples use invented messages and a deliberately public demonstration key. Never reuse this key in a real system. They illustrate the API, not a complete authentication protocol, key-management system, or password-storage design.

Prepare a scratch directory

These three standalone examples were tested on Linux with Python 3.12.14. Use Bash, Python 3.12 or newer, a text editor, and the standard mktemp, rm, and rmdir utilities. No third-party packages are required. Keep assertions enabled by running without Python’s -O option.

Create a fresh directory and keep using the same terminal:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Save each named source file here as UTF-8. Avoid naming a source file hmac.py. The examples calculate values in memory and do not access a network or write data files.

1. Check an original and an altered message

Save as check_message_tag.py:

import hmac

key = b"public-demonstration-key"
message = b"task=build;count=3"
received_tag = hmac.digest(key, message, "sha256")

def matches(payload, tag):
    expected = hmac.digest(key, payload, "sha256")
    return hmac.compare_digest(expected, tag)

assert matches(message, received_tag)
assert not matches(b"task=build;count=4", received_tag)
assert not matches(message, b"")
print("Tag bytes:", len(received_tag))
print("Original:", matches(message, received_tag))
print("Altered:", matches(b"task=build;count=4", received_tag))
print("Empty tag:", matches(message, b""))

Run it:

python3 check_message_tag.py

Output:

Tag bytes: 32
Original: True
Altered: False
Empty tag: False

The sender’s tag is simulated by received_tag. The receiver recomputes a tag from the bytes actually received. Both sides must agree on the key and algorithm. SHA-256 produces a 32-byte tag here.

Use compare_digest for tag comparisons instead of ordinary equality; it avoids content-based short circuiting. Keep both arguments in the same representation. Raw bytes and a hexadecimal string are different types. A failed check means the message should not be accepted as authenticated.

2. Feed a message in pieces

Save as update_message_tag.py:

import hmac

key = b"public-demonstration-key"
chunks = [b"task=", b"build;", b"count=3"]
incremental = hmac.new(key, digestmod="sha256")
for chunk in chunks:
    incremental.update(chunk)

whole = hmac.digest(key, b"".join(chunks), "sha256")
assert hmac.compare_digest(incremental.digest(), whole)
before = incremental.digest()
incremental.update(b"")
assert hmac.compare_digest(before, incremental.digest())
print("Matches one-shot:", hmac.compare_digest(
    incremental.digest(), whole))
print("Empty update unchanged:", hmac.compare_digest(
    before, incremental.digest()))
print("Hex characters:", len(incremental.hexdigest()))

Run it:

python3 update_message_tag.py

Output:

Matches one-shot: True
Empty update unchanged: True
Hex characters: 64

Each update extends the same message. The chunk boundaries add no separators; the code joins to exactly task=build;count=3. An empty chunk adds no bytes. This approach also works when reading a larger payload incrementally.

Our test compares the incremental result with the one-shot calculation. It deliberately does not print the tag: the relationship between the two calculations is the point of the example.

3. Branch from a shared prefix

Save as copy_message_tag.py:

import hmac

key = b"public-demonstration-key"
prefix = b"task="
base = hmac.new(key, prefix, digestmod="sha256")
build = base.copy()
build.update(b"build")
test = base.copy()
test.update(b"test")

assert hmac.compare_digest(
    build.digest(), hmac.digest(key, b"task=build", "sha256"))
assert hmac.compare_digest(
    test.digest(), hmac.digest(key, b"task=test", "sha256"))
print("Prefix unchanged:", hmac.compare_digest(
    base.digest(), hmac.digest(key, prefix, "sha256")))
print("Branches differ:", not hmac.compare_digest(
    build.digest(), test.digest()))

Run it:

python3 copy_message_tag.py

Output:

Prefix unchanged: True
Branches differ: True

copy creates an independent calculation from the current state. Extending build does not append bytes to base or test. The assertions also verify each branch against its complete message, so accidental extra separators would be caught.

HMAC does not encrypt these messages or prevent replay by itself. A real protocol must define the exact authenticated bytes and handle freshness separately. Do not invent an ad hoc serialization for security-sensitive data; follow the protocol you are implementing.

Clean up

In the same terminal, remove only these source files, return to the original directory, and remove the now-empty scratch directory:

rm -- check_message_tag.py update_message_tag.py copy_message_tag.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If rmdir reports that anything remains, inspect the directory rather than deleting unknown files.

References

Acknowledgements

This post was created with assistance from AI.

Last updated on