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))  # True

Why 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):
    pass

Webhook 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))  # True

HMAC-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

  1. Use SHA-256 or better: Avoid MD5 and SHA-1
  2. Constant-time comparison: Always use hmac.compare_digest()
  3. Include timestamp: Prevent replay attacks
  4. Sufficient key length: At least 256 bits
  5. Key rotation: Plan for key rotation from the start
  6. 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: