Message Authentication with HMAC
A plain hash proves that data has not changed, but anyone can recompute it and forge a new pair. A MAC (message authentication code) adds a secret key to the hash so that only someone holding the key can produce a valid code. HMAC - hash-based MAC - is the standard construction: it applies a keyed hash (SHA-256 in practice) in a specific two-pass pattern over the message and the key, and it is available in the standard library.
Sign and Verify
import hashlib
import hmac
import secrets
key = secrets.token_bytes(32) # shared secret, 256 bits
message = b"transfer 1000 USD to bob"
def sign(msg: bytes) -> bytes:
return hmac.new(key, msg, hashlib.sha256).digest()
def verify(msg: bytes, mac: bytes) -> bool:
expected = hmac.new(key, msg, hashlib.sha256).digest()
return hmac.compare_digest(expected, mac)
mac = sign(message)
print(verify(message, mac)) # True
print(verify(message + b" ", mac)) # False - one byte changed
compare_digest is mandatory: a regular == can short-circuit on the first differing byte and leak information through timing. The MAC travels with the message (header, footer, or side channel) and the verifier recomputes and compares.
Encoding for Transport
import base64
token = base64.urlsafe_b64encode(mac).decode()
# "k9xQ...=" - safe to put in a URL, header, or JSON field
mac_back = base64.urlsafe_b64decode(token.encode())
This exact pattern - payload plus base64 MAC - is what JWTs do in their header.payload.signature structure, and what webhooks use to prove that a POST came from the provider.
The cryptography Library Variant
from cryptography.hazmat.primitives import hmac as chmac
from cryptography.hazmat.primitives import hashes
h = chmac.HMAC(hashlib_sha256 := hashes.SHA256(), key)
h.update(message)
mac = h.finalize()
verifier = chmac.HMAC(hashes.SHA256(), key)
verifier.update(message)
try:
verifier.finalize(mac) # raises InvalidTag if the MAC does not match
print("authenticated")
except Exception:
print("tampered")
HMAC vs Digital Signature
| HMAC | RSA/Ed25519 signature | |
|---|---|---|
| Secret sharing | key is shared (symmetric) | only signer holds the private key |
| Third-party verification | no (they need the key) | yes (public key) |
| Speed | very fast | slower |
| Typical use | API webhooks, cache entries, DB row integrity | software updates, certificates, contracts |
Use HMAC when both sides already share a secret; use a real digital signature (next RSA chapters) when the verifier is a stranger or when you need non-repudiation.
Summary
Compute MACs with hmac.new(key, msg, sha256), transmit them in a stable encoding like base64url, and always verify with compare_digest. HMAC is the cheapest way to make "this exact byte string was approved by the key holder" true between two parties who share that key.