HMAC is a cryptographic construction that combines a secret key with a hash function to produce a message authentication code. Unlike an ordinary hash, only a party that knows the key can reproduce the correct value. A valid HMAC therefore indicates that the message was not modified and that it was created by someone holding the shared secret.
Python’s hmac module implements the algorithm standardized in RFC 2104. It is widely used to validate webhooks, authenticate messages between services, protect distributed configuration, sign compact cookies, and verify payloads received from queues or APIs. This guide covers generation, safe verification, canonical messages, replay attacks, and key rotation.
Regular hashes versus HMAC
A digest such as SHA-256 has no secret. Anyone who changes a message can calculate a new digest. A trusted checksum can detect file corruption, but a digest alone does not authenticate the sender.
HMAC incorporates a key through a standardized construction. Do not invent alternatives such as sha256(secret + message). Custom combinations can introduce length-extension issues, ambiguous fields, or unsafe key handling.
Review the Python hashlib guide for general hashes and the password hashing guide for credential storage. HMAC solves a different problem: message authentication.
Creating HMAC-SHA256
import hmac
import hashlib
key = b"randomly-generated-server-secret"
message = b"order=123&amount=49.90"
mac = hmac.new(key, message, digestmod=hashlib.sha256)
print(mac.hexdigest())
The key and message must be bytes-like objects. Encode text explicitly:
key = "server secret".encode("utf-8")
message = "action=confirm".encode("utf-8")
digestmod is required. SHA-256 is a common interoperable choice. HMAC requires a fixed-size digest, so extendable-output functions such as SHAKE-128 and SHAKE-256 cannot be used.
The hmac.digest() shortcut
When the complete message already fits in memory, hmac.digest() provides a compact call and may use an optimized implementation.
signature = hmac.digest(key, message, "sha256")
print(signature.hex())
For streams and large files, create an HMAC object and update it incrementally.
Incremental processing
from pathlib import Path
import hmac
import hashlib
def hmac_file(path: Path, key: bytes) -> str:
mac = hmac.new(key, digestmod=hashlib.sha256)
with path.open("rb") as file:
while chunk := file.read(1024 * 1024):
mac.update(chunk)
return mac.hexdigest()
This avoids loading a large file into memory and follows the approach described in reading giant files without freezing Python.
Verification with compare_digest()
Do not compare secret authentication tags with ==. A typical comparison may stop at the first mismatch, creating small timing differences. Repeated measurements can sometimes reveal information.
def verify_signature(
key: bytes,
message: bytes,
supplied_hex: str,
) -> bool:
expected = hmac.new(
key,
message,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, supplied_hex)
The two values should have the same type. Text comparisons must use ASCII-only strings, such as hexadecimal digests. Length differences may still reveal length information, so validate the expected format before comparing.
Validating a webhook
A webhook normally sends a raw request body and a signature header. Calculate the HMAC over exactly the bytes specified by the provider.
def verify_webhook(body: bytes, header: str, secret: bytes) -> None:
if not header.startswith("sha256="):
raise ValueError("Invalid signature format")
supplied = header.removeprefix("sha256=")
if len(supplied) != 64:
raise ValueError("Invalid signature length")
expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, supplied):
raise PermissionError("Invalid signature")
Do not parse JSON and serialize it again before verification. Whitespace, key ordering, escapes, and line endings can change the bytes. Read the raw body, verify the signature, and only then parse the payload.
Canonical message design
When you define a signing protocol, specify fields, order, encoding, and separators. Naive concatenation can be ambiguous: ("ab", "c") and ("a", "bc") produce the same bytes if no boundary exists.
def canonical_message(timestamp: int, method: str, path: str, body: bytes) -> bytes:
prefix = f"v1\n{timestamp}\n{method.upper()}\n{path}\n".encode("utf-8")
return prefix + body
Include a version marker so the protocol can evolve without confusing new and legacy signatures.
Replay protection
A valid HMAC does not stop an attacker from capturing and resending the same valid request. Sensitive commands should authenticate a timestamp, nonce, or unique event ID.
import time
MAX_AGE = 300
if abs(time.time() - timestamp) > MAX_AGE:
raise PermissionError("Expired message")
Store accepted nonces for the permitted window. A safe order is: validate structure and limits, check timestamp, calculate HMAC, compare, record the nonce, and only then perform the action.
Key generation and storage
An HMAC key needs sufficient entropy and should come from a cryptographically secure source.
import secrets
key = secrets.token_bytes(32)
Do not derive the key from a short phrase, commit it to source control, or print it in logs. Use a secret manager, protected environment variable, or restricted file. Separate keys by purpose and environment.
Key rotation
Associate each key with a public identifier such as key_id. During rotation, sign only with the new key while temporarily accepting the previous key for messages created before the change.
KEYS = {
"2026-08": b"new-key...",
"2026-07": b"previous-key...",
}
Validate and limit the identifier. Do not blindly try hundreds of keys for attacker-controlled requests.
Tag truncation
Some protocols transmit only part of an HMAC to save space. Truncation lowers forgery resistance. Prefer the complete digest unless a reviewed standard specifies an acceptable minimum. Never accept arbitrary-length prefixes.
Signing API requests
API signing schemes often authenticate the HTTP method, canonical path, sorted query, timestamp, and a hash of the body. Client and server must normalize all elements identically. The guide to consuming REST APIs in Python complements this with timeout and response validation practices.
HMAC does not encrypt
The message remains readable. HMAC provides integrity and authenticity, not confidentiality. Use TLS for transport and authenticated encryption for secret stored data. Do not reuse one key for HMAC and encryption; derive purpose-specific keys with an appropriate KDF.
Common mistakes
Frequent errors include using ==, signing reserialized JSON, omitting a timestamp, accepting variable-length tags, using predictable keys, logging secrets, reusing a key for unrelated purposes, and executing the business action before verification.
Best practices
Use HMAC-SHA256, a random key of at least 32 bytes, a versioned canonical message, and compare_digest(). Authenticate timestamp and nonce, apply size limits before processing, rotate keys by identifier, and log only non-sensitive metadata. Prefer the provider’s official verification library when its signing format is complex.
Conclusion
Python’s hmac module provides a standard way to authenticate messages with a secret key and a hash function. The call itself is simple; security depends on the exact bytes signed, safe comparison, key management, and replay protection. When these details are specified clearly, HMAC is a robust choice for webhooks and service-to-service communication.
See the official hmac documentation and RFC 2104.







