Python Basics: Checking Message Tags with hmac
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 1Save 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.pyOutput:
Tag bytes: 32
Original: True
Altered: False
Empty tag: FalseThe 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.pyOutput:
Matches one-shot: True
Empty update unchanged: True
Hex characters: 64Each 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.pyOutput:
Prefix unchanged: True
Branches differ: Truecopy 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_dirIf rmdir reports that anything remains, inspect the directory rather than deleting unknown files.
References
Acknowledgements
This post was created with assistance from AI.