Intermediate

Using HMAC for Message Authentication

Learn how to use HMAC for API request signing, webhook verification, and secure message authentication.

What is HMAC?

HMAC (Hash-based Message Authentication Code) is a mechanism for verifying both the integrity and authenticity of a message. It combines a cryptographic hash function with a secret key to produce a signature that proves:

  • -Integrity: The message hasn't been modified
  • -Authenticity: The message came from someone who knows the secret key
Key Insight

Unlike regular hashing, HMAC requires a secret key. Without the key, an attacker cannot create a valid HMAC even if they know the message and the hash algorithm. This makes HMAC perfect for API authentication.

How HMAC Works

HMAC formula:
HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message))
Where:
  • H = hash function (SHA-256, SHA-512, etc.)
  • ⊕ = XOR operation
  • || = concatenation
  • opad = outer padding (0x5c repeated)
  • ipad = inner padding (0x36 repeated)

Simplified Explanation

HMAC essentially hashes the message twice with the key mixed in using XOR operations. This prevents length extension attacks and ensures the key is properly integrated into the hash.

You don't need to implement this yourself - use built-in HMAC functions in your programming language.

Common Use Cases

1. API Request Signing

Sign API requests to prove they came from an authorized client. The server verifies the signature before processing.

Used by: AWS, Stripe, Twilio, GitHub webhooks

2. Webhook Verification

Services send webhooks with HMAC signatures so you can verify the webhook actually came from them.

Used by: Stripe, GitHub, Shopify, PayPal

3. Session Tokens

Create tamper-proof session tokens by including an HMAC of the session data.

Used by: JWT (JSON Web Tokens), cookie-based sessions

4. Message Integrity

Ensure messages between systems haven't been tampered with during transmission.

Used by: IPsec, TLS, secure messaging protocols

Implementation Examples

Python:
import hmac
import hashlib

secret_key = b"your-secret-key"
message = b"Hello, World!"

# Generate HMAC
signature = hmac.new(secret_key, message, hashlib.sha256).hexdigest()
print(f"HMAC: {signature}")

# Verify HMAC
def verify_hmac(message, signature, key):
    expected = hmac.new(key, message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

# Always use compare_digest to prevent timing attacks
is_valid = verify_hmac(message, signature, secret_key)
print(f"Valid: {is_valid}")
Node.js:
const crypto = require('crypto');

const secretKey = 'your-secret-key';
const message = 'Hello, World!';

// Generate HMAC
const signature = crypto
  .createHmac('sha256', secretKey)
  .update(message)
  .digest('hex');

console.log(`HMAC: ${signature}`);

// Verify HMAC
function verifyHmac(message, signature, key) {
  const expected = crypto
    .createHmac('sha256', key)
    .update(message)
    .digest('hex');
  
  // Use timingSafeEqual to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

const isValid = verifyHmac(message, signature, secretKey);
console.log(`Valid: ${isValid}`);
PHP:
$secretKey = 'your-secret-key';
$message = 'Hello, World!';

// Generate HMAC
$signature = hash_hmac('sha256', $message, $secretKey);
echo "HMAC: $signature\n";

// Verify HMAC
function verifyHmac($message, $signature, $key) {
    $expected = hash_hmac('sha256', $message, $key);
    // Use hash_equals to prevent timing attacks
    return hash_equals($expected, $signature);
}

$isValid = verifyHmac($message, $signature, $secretKey);
echo "Valid: " . ($isValid ? 'true' : 'false');

API Request Signing Example

Here's how to sign API requests to prevent tampering and replay attacks:

Client-side (Python):
import hmac
import hashlib
import time
import requests

API_KEY = "your-api-key"
SECRET_KEY = "your-secret-key"

def sign_request(method, path, body=""):
    timestamp = str(int(time.time()))
    
    # Create signature string
    message = f"{method}\n{path}\n{timestamp}\n{body}"
    
    # Generate HMAC
    signature = hmac.new(
        SECRET_KEY.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()
    
    return {
        'X-API-Key': API_KEY,
        'X-Timestamp': timestamp,
        'X-Signature': signature
    }

# Make signed request
headers = sign_request('POST', '/api/users', '{"name":"Alice"}')
response = requests.post(
    'https://api.example.com/api/users',
    headers=headers,
    json={"name": "Alice"}
)
Server-side verification (Node.js):
const crypto = require('crypto');

function verifyRequest(req) {
  const apiKey = req.headers['x-api-key'];
  const timestamp = req.headers['x-timestamp'];
  const signature = req.headers['x-signature'];
  
  // Check timestamp (prevent replay attacks)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) { // 5 minutes
    return false; // Request too old
  }
  
  // Look up secret key for this API key
  const secretKey = getSecretKeyForApiKey(apiKey);
  if (!secretKey) return false;
  
  // Reconstruct message
  const body = JSON.stringify(req.body);
  const message = `${req.method}\n${req.path}\n${timestamp}\n${body}`;
  
  // Verify signature
  const expected = crypto
    .createHmac('sha256', secretKey)
    .update(message)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

// Middleware
app.use((req, res, next) => {
  if (!verifyRequest(req)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  next();
});

Webhook Verification Example

Verify webhooks from services like Stripe or GitHub:

Stripe webhook verification (Node.js):
const crypto = require('crypto');

function verifyStripeWebhook(payload, signature, secret) {
  // Stripe sends signature in format: t=timestamp,v1=signature
  const elements = signature.split(',');
  const timestamp = elements.find(e => e.startsWith('t=')).split('=')[1];
  const sig = elements.find(e => e.startsWith('v1=')).split('=')[1];
  
  // Create signed payload
  const signedPayload = `${timestamp}.${payload}`;
  
  // Compute expected signature
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  
  // Verify
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(sig)
  );
}

// Express route
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['stripe-signature'];
  const payload = req.body.toString();
  
  if (!verifyStripeWebhook(payload, signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process webhook
  const event = JSON.parse(payload);
  console.log('Verified webhook:', event.type);
  res.json({received: true});
});

Security Best Practices

✓ Use Strong Keys

Generate random keys with at least 256 bits of entropy. Don't use passwords or predictable values.

✓ Use Constant-Time Comparison

Always use hmac.compare_digest() (Python), crypto.timingSafeEqual() (Node.js), or hash_equals() (PHP) to prevent timing attacks.

✓ Include Timestamps

Add timestamps to prevent replay attacks. Reject requests older than 5-15 minutes.

✓ Use SHA-256 or Better

HMAC-SHA-256 is the standard. Avoid HMAC-MD5 and HMAC-SHA-1.

✓ Keep Keys Secret

Store keys in environment variables or secret management systems. Never commit them to version control.

✓ Use HTTPS

HMAC protects integrity, not confidentiality. Always use HTTPS to encrypt the entire request.

Common Mistakes

✗ Using == for Comparison

Never use signature == expected. This is vulnerable to timing attacks. Use constant-time comparison functions.

✗ Weak Keys

Don't use short keys, passwords, or predictable values. Generate cryptographically random keys.

✗ No Replay Protection

Without timestamps or nonces, attackers can replay valid requests. Always include replay protection.

✗ Signing Only Part of the Request

Sign the entire request (method, path, headers, body). Attackers can modify unsigned parts.

Try It Yourself

Experiment with HMAC using our Hash Calculator:

Exercise: Generate and Verify HMAC
  1. 1. Go to the Hash Calculator
  2. 2. Scroll to the HMAC section
  3. 3. Enter a message: Hello, World!
  4. 4. Enter a key: my-secret-key
  5. 5. Copy the HMAC signature
  6. 6. Change the message slightly and notice the signature changes completely
  7. 7. Change it back - the signature matches again

Official Resources

Implementation Documentation

Related Guides