HMAC (Hash-based Message Authentication Code) verifies both data integrity and authenticity. It's essential for API security, webhook validation, and signed requests.
Basic HMAC
import hmac
import hashlib
key = b"secret_key"
message = b"Hello, World!"
# Create HMAC
signature = hmac.new(key, message, hashlib.sha256).hexdigest()
print(signature)
# '4b393abcf...'
# Verify HMAC
def verify(key: bytes, message: bytes, signature: str) -> bool:
expected = hmac.new(key, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
print(verify(key, message, signature)) # TrueWhy compare_digest?
Prevent timing attacks:
import hmac
# DON'T: vulnerable to timing attack
if expected_sig == provided_sig: # Bad!
pass
# DO: constant-time comparison
if hmac.compare_digest(expected_sig, provided_sig):
passWebhook Signature Verification
Validate webhooks from services like GitHub, Stripe, Slack:
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
"""Verify webhook signature."""
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
# Signature might have prefix like "sha256="
if signature.startswith('sha256='):
signature = signature[7:]
return hmac.compare_digest(expected, signature)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Hub-Signature-256', '')
if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
abort(403)
# Process webhook...
return 'OK'API Request Signing
Sign requests for authentication:
import hmac
import hashlib
import time
class APIClient:
def __init__(self, api_key: str, api_secret: str):
self.api_key = api_key
self.api_secret = api_secret.encode()
def sign_request(self, method: str, path: str,
body: str = '') -> dict:
"""Generate signed request headers."""
timestamp = str(int(time.time()))
# Build string to sign
string_to_sign = f"{method}\n{path}\n{timestamp}\n{body}"
signature = hmac.new(
self.api_secret,
string_to_sign.encode(),
hashlib.sha256
).hexdigest()
return {
'X-API-Key': self.api_key,
'X-Timestamp': timestamp,
'X-Signature': signature,
}
# Usage
client = APIClient('my_api_key', 'my_api_secret')
headers = client.sign_request('POST', '/api/orders', '{"item": "book"}')Server-Side Signature Verification
import hmac
import hashlib
import time
def verify_request(method: str, path: str, body: str,
api_key: str, timestamp: str,
signature: str, secrets: dict) -> bool:
"""Verify signed API request."""
# Check timestamp (prevent replay attacks)
request_time = int(timestamp)
current_time = int(time.time())
if abs(current_time - request_time) > 300: # 5 minute window
return False
# Get secret for this API key
api_secret = secrets.get(api_key)
if not api_secret:
return False
# Recreate signature
string_to_sign = f"{method}\n{path}\n{timestamp}\n{body}"
expected = hmac.new(
api_secret.encode(),
string_to_sign.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Signed URLs
Generate time-limited signed URLs:
import hmac
import hashlib
import time
import base64
from urllib.parse import urlencode, parse_qs, urlparse
def sign_url(url: str, secret: str, expires_in: int = 3600) -> str:
"""Generate signed URL with expiration."""
expires = int(time.time()) + expires_in
# Add expiration to URL
separator = '&' if '?' in url else '?'
url_with_expiry = f"{url}{separator}expires={expires}"
# Generate signature
signature = hmac.new(
secret.encode(),
url_with_expiry.encode(),
hashlib.sha256
).hexdigest()
return f"{url_with_expiry}&signature={signature}"
def verify_signed_url(url: str, secret: str) -> bool:
"""Verify signed URL."""
# Parse URL
parsed = urlparse(url)
params = parse_qs(parsed.query)
signature = params.get('signature', [''])[0]
expires = params.get('expires', ['0'])[0]
# Check expiration
if int(expires) < time.time():
return False
# Remove signature from URL to verify
url_without_sig = url.replace(f"&signature={signature}", "")
expected = hmac.new(
secret.encode(),
url_without_sig.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# Usage
secret = "my_secret_key"
signed = sign_url("https://api.example.com/download/file.pdf", secret)
print(verify_signed_url(signed, secret)) # TrueHMAC-Based Tokens
import hmac
import hashlib
import json
import base64
import time
def create_token(payload: dict, secret: str) -> str:
"""Create HMAC-signed token."""
payload['iat'] = int(time.time())
# Encode payload
payload_json = json.dumps(payload, sort_keys=True)
payload_b64 = base64.urlsafe_b64encode(payload_json.encode()).decode()
# Create signature
signature = hmac.new(
secret.encode(),
payload_b64.encode(),
hashlib.sha256
).hexdigest()
return f"{payload_b64}.{signature}"
def verify_token(token: str, secret: str,
max_age: int = 3600) -> dict | None:
"""Verify and decode HMAC-signed token."""
try:
payload_b64, signature = token.rsplit('.', 1)
except ValueError:
return None
# Verify signature
expected = hmac.new(
secret.encode(),
payload_b64.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
return None
# Decode payload
payload_json = base64.urlsafe_b64decode(payload_b64).decode()
payload = json.loads(payload_json)
# Check age
if time.time() - payload.get('iat', 0) > max_age:
return None
return payload
# Usage
token = create_token({'user_id': 123, 'role': 'admin'}, 'secret')
payload = verify_token(token, 'secret')Different Hash Algorithms
import hmac
import hashlib
key = b"secret"
message = b"data"
# SHA-256 (recommended)
hmac.new(key, message, hashlib.sha256).hexdigest()
# SHA-512
hmac.new(key, message, hashlib.sha512).hexdigest()
# SHA-1 (legacy, avoid for new code)
hmac.new(key, message, hashlib.sha1).hexdigest()
# MD5 (broken, don't use)
# hmac.new(key, message, hashlib.md5).hexdigest()Incremental HMAC
For large messages:
import hmac
import hashlib
key = b"secret_key"
# Create HMAC object
h = hmac.new(key, digestmod=hashlib.sha256)
# Update incrementally
h.update(b"chunk 1")
h.update(b"chunk 2")
h.update(b"chunk 3")
signature = h.hexdigest()Key Derivation
Derive multiple keys from a single secret:
import hmac
import hashlib
def derive_key(master_key: bytes, purpose: str) -> bytes:
"""Derive purpose-specific key from master key."""
return hmac.new(
master_key,
purpose.encode(),
hashlib.sha256
).digest()
master = b"master_secret_key"
encryption_key = derive_key(master, "encryption")
signing_key = derive_key(master, "signing")Best Practices
- Use SHA-256 or better: Avoid MD5 and SHA-1
- Constant-time comparison: Always use
hmac.compare_digest() - Include timestamp: Prevent replay attacks
- Sufficient key length: At least 256 bits
- Key rotation: Plan for key rotation from the start
- Don't expose keys: Keep secrets out of URLs and logs
HMAC is fundamental to API security. Master it to build secure integrations.
React to this post: