Message Authentication with hmac
When you need to verify that a message came from a specific sender and was not tampered with during transit, HMAC (Hash-based Message Authentication Code) is the standard solution. Python’s hmac module provides a simple but powerful interface for creating and verifying HMACs using any hash algorithm.
What Is HMAC?
HMAC combines a secret key with a message and a cryptographic hash function to produce a code that only someone with the key could generate. This provides two guarantees:
- Authentication: Only someone with the secret key could have created the HMAC
- Integrity: Any modification to the message would result in a different HMAC
Common uses include:
- Verifying API request signatures (AWS, Stripe, and many others use HMAC)
- Checking that files have not been tampered with
- Authenticating messages between server and client
- Building custom authentication protocols
Getting Started with hmac
The basic workflow involves creating an HMAC object with a key and message, then comparing the digest:
import hmac
secret_key = b"my_secret_key"
message = b"Hello, World!"
# Create HMAC using SHA-256
code = hmac.new(secret_key, message, digestmod="sha256")
print(code.hexdigest())
# a1b2c3d4e5f6... (a 64-character hex string)
To verify a message, compute the HMAC with the same key and compare:
import hmac
def verify_hmac(secret_key, message, expected_mac):
"""Verify a message using HMAC."""
computed_mac = hmac.new(secret_key, message, digestmod="sha256")
return hmac.compare_digest(computed_mac.hexdigest(), expected_mac)
# Example usage
key = b"secret_key"
message = b"Transfer $100 to account 12345"
expected = "a1b2c3d4e5f6..."
is_valid = verify_hmac(key, message, expected)
print(f"Message is valid: {is_valid}")
Notice the use of hmac.compare_digest — this prevents timing attacks by comparing the digests in constant time.
HMAC with Different Hash Algorithms
The hmac module supports any hash algorithm available in hashlib:
import hmac
key = b"my_secret_key"
message = b"Important message"
# SHA-256 (recommended for most purposes)
hmac_sha256 = hmac.new(key, message, digestmod="sha256").hexdigest()
print(f"SHA-256: {hmac_sha256}")
# SHA-512 (more secure but slower)
hmac_sha512 = hmac.new(key, message, digestmod="sha512").hexdigest()
print(f"SHA-512: {hmac_sha512}")
# MD5 (legacy, but still used for compatibility)
hmac_md5 = hmac.new(key, message, digestmod="md5").hexdigest()
print(f"MD5: {hmac_md5}")
# BLAKE2b (fast and secure)
hmac_blake2 = hmac.new(key, message, digestmod="blake2b").hexdigest()
print(f"BLAKE2b: {hmac_blake2}")
SHA-256 offers a good balance of security and performance. SHA-512 is recommended for high-security applications.
Incremental HMAC Calculation
For large messages or streaming data, you can update the HMAC incrementally:
import hmac
key = b"secret_key"
# Create the HMAC object
h = hmac.new(key, digestmod="sha256")
# Feed data in chunks
h.update(b"Hello, ")
h.update(b"World!")
h.update(b" More data.")
# Get the final digest
result = h.hexdigest()
print(f"Incremental HMAC: {result}")
This pattern is useful when processing large files or streaming data from the network.
Practical Examples
API Request Signing
Many APIs use HMAC to sign requests. Here’s a simplified example:
import hmac
import hashlib
import time
def sign_request(secret_key, method, path, timestamp, body=""):
"""Sign an API request with HMAC-SHA256."""
message = f"{timestamp}\n{method}\n{path}\n{body}"
signature = hmac.new(
secret_key.encode(),
message.encode(),
digestmod="sha256"
).hexdigest()
return signature
# Example: signing a request
secret = "your_api_secret"
method = "POST"
path = "/api/v1/transfer"
timestamp = str(int(time.time()))
body = '{"amount": 100, "to": "account123"}'
signature = sign_request(secret, method, path, timestamp, body)
print(f"Signature: {signature}")
# The request would include:
# Authorization: HMAC-SHA256 {timestamp}:{signature}
File Integrity Checking
Verify that files have not been tampered with:
import hmac
import hashlib
def create_file_signature(filename, key):
"""Create an HMAC signature for a file."""
h = hmac.new(key, digestmod="sha256")
with open(filename, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
return h.hexdigest()
def verify_file(filename, key, expected_signature):
"""Verify a file has not been tampered with."""
actual = create_file_signature(filename, key)
return hmac.compare_digest(actual, expected_signature)
# Example usage
key = b"file_verification_key"
signature = create_file_signature("important.dat", key)
print(f"File signature: {signature}")
# Later, verify the file
is_valid = verify_file("important.dat", key, signature)
print(f"File is valid: {is_valid}")
Token Generation
Generate authenticated tokens that clients cannot forge:
import hmac
import base64
import json
import time
def create_token(payload, secret_key):
"""Create an HMAC-signed token."""
# Add expiration to the payload
payload["exp"] = int(time.time()) + 3600 # 1 hour expiry
# Encode the payload
data = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode()
# Sign the token
signature = hmac.new(secret_key.encode(), data.encode(), digestmod="sha256").hexdigest()
return f"{data}.{signature}"
def verify_token(token, secret_key):
"""Verify and decode an HMAC-signed token."""
try:
data, signature = token.rsplit(".", 1)
# Verify the signature
expected = hmac.new(secret_key.encode(), data.encode(), digestmod="sha256").hexdigest()
if not hmac.compare_digest(signature, expected):
return None, "Invalid signature"
# Decode and check expiration
payload = json.loads(base64.urlsafe_b64decode(data))
if payload.get("exp", 0) < time.time():
return None, "Token expired"
return payload, None
except Exception as e:
return None, str(e)
# Example usage
secret = "my_application_secret"
payload = {"user_id": 12345, "role": "admin"}
token = create_token(payload, secret)
print(f"Token: {token[:50]}...")
# Verify the token
result, error = verify_token(token, secret)
if result:
print(f"Valid! User: {result['user_id']}, Role: {result['role']}")
else:
print(f"Invalid: {error}")
Timing-Safe Comparison
Always use hmac.compare_digest for comparing HMACs, not the == operator:
import hmac
# Wrong way - vulnerable to timing attacks
def insecure_compare(a, b):
return a == b # DON'T USE THIS
# Right way - constant-time comparison
def secure_compare(a, b):
return hmac.compare_digest(a, b) # ALWAYS USE THIS
Timing attacks exploit small differences in how long comparisons take. hmac.compare_digest takes the same amount of time regardless of where the difference is, preventing attackers from guessing the correct HMAC byte by byte.
Key Best Practices
- Use strong keys: Generate keys with at least 256 bits of entropy using
os.urandom(32)or thesecretsmodule - Use SHA-256 or stronger: Avoid MD5 and SHA-1 for new implementations
- Never expose keys: Keys should never appear in logs, URLs, or error messages
- Use constant-time comparison: Always use
hmac.compare_digestfor verification - Consider key rotation: Plan for how to rotate keys if they are compromised
See Also
- hashlib-module — The hash algorithms used by hmac
- os-module — os.urandom for key generation
- base64-module — Encoding tokens for transmission