HMAC (Hash-based Message Authentication Code)

Authentication Security Notes Jan 6, 2025 JAVASCRIPT

Definition

Imagine you’re sending a sealed letter to someone important. You want them to be absolutely certain that the letter came from you and wasn’t tampered with during delivery. In the old days, nobles used wax seals with their unique signet ring - only someone with that specific ring could create that exact seal. If the seal was broken or looked different, the recipient knew something was wrong. HMAC (Hash-based Message Authentication Code) is the digital version of this wax seal - it creates a unique “signature” for any message using a secret key that only you and the recipient know.

HMAC works by combining your message with a secret key and running them through a cryptographic hash function (like SHA-256). The result is a fixed-length string of characters that’s unique to both the message content AND the secret key. If even a single character in the message changes, the HMAC changes completely. If someone doesn’t know the secret key, they can’t generate a valid HMAC. When the recipient receives your message, they perform the same calculation with the same secret key. If their result matches the HMAC you sent, they know two things: the message definitely came from someone who knows the secret key (authentication), and the message hasn’t been modified since it was signed (integrity).

HMAC is everywhere in modern APIs, even if you don’t see it directly. When you use AWS APIs, every request is signed with HMAC-SHA256 using your secret access key. When a webhook arrives from Stripe, it includes an HMAC signature so you can verify it’s really from Stripe. When your browser establishes an HTTPS connection, HMAC is part of the security handshake. It’s one of the fundamental building blocks of secure communication - a simple concept (combine message + secret + hash) that provides powerful security guarantees.

Example

Real-World Scenario 1: AWS API Request Signing When you make any request to AWS (uploading to S3, launching EC2 instances, etc.), your request is signed with HMAC-SHA256. Your AWS SDK takes the request details (HTTP method, headers, URL, body), combines them with your Secret Access Key, and generates a signature. AWS receives the request, performs the same calculation with your key from their database, and verifies the signatures match. This proves the request came from someone with valid credentials AND the request wasn’t modified in transit.

Real-World Scenario 2: Webhook Verification When Stripe sends a webhook to your server notifying you of a payment, how do you know it’s really from Stripe and not an attacker? Stripe includes an HMAC signature in the Stripe-Signature header, calculated using your webhook secret. Your server calculates the HMAC of the webhook body using the same secret. If they match, you know the webhook is authentic. If they don’t match, you reject it. This prevents attackers from faking payment notifications.

Real-World Scenario 3: API Key Security Rather than sending API keys in plain text with every request (which could be intercepted), some APIs use HMAC-signed requests. Coinbase Pro’s API requires you to sign each request with HMAC-SHA256 using your API secret, including a timestamp. The signature proves you have the secret without transmitting it. The timestamp prevents replay attacks - old signed requests can’t be reused because the timestamp would be stale.

Real-World Scenario 4: Secure Cookie Authentication When you log into a website and get a “remember me” cookie, that cookie often contains an HMAC signature. The server creates a cookie value like user_id=12345|timestamp=1699000000|signature=abc123. The signature is an HMAC of the other fields using a server-side secret. When you return, the server verifies the signature - if someone modified the user_id to impersonate another user, the signature wouldn’t match.

Analogy

The Wax Seal Analogy: In historical times, important letters were sealed with wax and stamped with a unique signet ring. Only the sender had that specific ring, so the recipient could verify who sent it. If the seal was broken or didn’t match the expected pattern, the letter had been tampered with. HMAC is this signet ring for digital messages - a unique stamp that proves authenticity and integrity.

The Secret Recipe Verification: Imagine a baker sends a cake to a competition with a sealed card containing an HMAC. The card says “This is a carrot cake made with our secret ingredient.” The HMAC is calculated from the recipe details plus a secret only the baker and judge share. The judge can verify the sealed HMAC matches what they calculate, proving the cake really came from that baker and the description wasn’t swapped by a competitor.

The Military Challenge-Response: Military units use challenge-response codes: “What’s the password?” “Thunderbird.” But with HMAC, it’s more sophisticated: “Here’s today’s random challenge: 73829.” You combine the challenge with your secret password, hash it, and respond with the result. Only someone who knows the real password could generate the correct response for that specific challenge, and tomorrow’s challenge will have a different correct answer.

The Package Tamper Seal with Serial Number: Modern packages have tamper-evident seals with unique serial numbers. The manufacturer records each serial number and associates it with the package contents. If someone opens the package and tries to reseal it with a fake seal, the serial number won’t match the manufacturer’s records. HMAC works similarly - the “serial number” (signature) is mathematically tied to the exact package contents (message).

Code Example


// HMAC signature generation
const crypto = require('crypto');

function signRequest(message, secretKey) {
  return crypto
    .createHmac('sha256', secretKey)
    .update(message)
    .digest('hex');
}

// Example: Signing an API request
const method = 'POST';
const path = '/api/users';
const timestamp = Date.now();
const body = JSON.stringify({ name: 'John' });

// Create the signing string (canonical format)
const message = `${method}\n${path}\n${timestamp}\n${body}`;
const signature = signRequest(message, 'your-secret-key');

// Send with request
fetch('https://api.example.com/api/users', {
  method: 'POST',
  headers: {
    'X-Signature': signature,
    'X-Timestamp': timestamp,
    'Content-Type': 'application/json'
  },
  body: body
});

// Verify on server
function verifySignature(req, secretKey) {
  const receivedSig = req.headers['x-signature'];
  const timestamp = req.headers['x-timestamp'];

  // Check timestamp freshness (prevent replay attacks)
  const now = Date.now();
  const requestTime = parseInt(timestamp);
  if (now - requestTime > 300000) { // 5 minutes
    return false; // Request too old
  }

  const message = `${req.method}\n${req.path}\n${timestamp}\n${req.body}`;
  const expectedSig = signRequest(message, secretKey);

  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(receivedSig),
    Buffer.from(expectedSig)
  );
}

// Webhook verification (Stripe-style)
function verifyWebhook(payload, signatureHeader, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signatureHeader),
    Buffer.from(`sha256=${expectedSignature}`)
  );
}

Diagram

sequenceDiagram
    participant C as Client
    participant S as Server

    Note over C,S: Both share the same SECRET KEY
(established securely beforehand) Note over C: Build message to sign:
method + path + timestamp + body Note over C: Calculate HMAC:
signature = HMAC-SHA256(message, secret_key) C->>S: Request with:
X-Signature: abc123...
X-Timestamp: 1699000000
Body: {"name": "John"} Note over S: Rebuild same message from request Note over S: Recalculate HMAC using
the same secret key alt Signatures Match Note over S: Verify timestamp is recent
(prevent replay attacks) S-->>C: 200 OK - Request Authentic else Signatures Differ S-->>C: 401 Unauthorized
Invalid Signature else Timestamp Too Old S-->>C: 401 Unauthorized
Request Expired end Note over C,S: Provides INTEGRITY + AUTHENTICATION
Secret key never transmitted

Security Notes

SECURITY NOTES

CRITICAL: HMAC security depends on strong secrets and proper implementation. Vulnerable to replay attacks.

Secret Key Management:

  • Cryptographically random: Keys must be random and sufficiently long (32+ bytes)
  • Secure storage: Use secrets management systems (vault, AWS Secrets Manager)
  • Never log keys: Never log or expose secret keys
  • Key rotation: Rotate keys regularly (every 90 days recommended)
  • Separate per client: Use different keys for each client/service

Hash Algorithm Selection:

  • Use SHA-256 or better: Use SHA-256, SHA-384, or SHA-512
  • Avoid weak algorithms: Never use MD5, SHA-1, or CRC32
  • Consistent algorithm: Always use same algorithm for verification

Replay Attack Prevention:

  • Include timestamp: Add message creation timestamp; reject old requests
  • Time window: Reject messages older than 5-15 minutes
  • Nonce value: Include unique nonce for additional replay protection
  • Request ID: Prevent reuse of same request ID within time window

Signature Calculation:

  • Include all relevant data: Sign method, path, headers, body, timestamp
  • Canonical format: Use consistent format for signing (no extra whitespace)
  • Don’t partial sign: Include everything that could be tampered with
  • Timing-safe comparison: Use constant-time comparison to prevent timing attacks

Implementation Best Practices:

  • HTTPS required: Always transmit over HTTPS despite HMAC protection
  • Separate signing key: Don’t mix HMAC key with encryption key
  • Version your scheme: Support multiple algorithms for rotation
  • Error handling: Don’t leak signature validity in error messages
  • Monitor: Log and alert on signature verification failures

Standards & RFCs