Message Authentication with hmac

· 5 min read · Updated March 15, 2026 · intermediate
python security hmac cryptography authentication

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 the secrets module
  • 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_digest for verification
  • Consider key rotation: Plan for how to rotate keys if they are compromised

See Also