HMAC Internals — How HMAC Works, Key Management, and When to Use It Over Plain Hashing

Every API request to Stripe, every JWT token signed with HS256, every webhook callback from GitHub — they all rely on HMAC (Hash-based Message Authentication Code). HMAC adds a secret key to a hash function, producing a code that proves the message was created by someone who knows the key and has not been modified. This guide explains the algorithm internals (RFC 2104), key management best practices, and practical implementation patterns.

Try it now: Use the HMAC Generator to compute HMAC-SHA256, HMAC-SHA1, and HMAC-MD5 digests with any key, right in your browser.

1. What HMAC Solves That Hashing Doesn't

A plain cryptographic hash like SHA-256 guarantees integrity — the same input always produces the same output. But anyone can compute the hash. If you send message + SHA-256(message) to a server, an attacker who modifies the message can also recompute the hash. You need something stronger.

HMAC adds authentication on top of integrity. It requires a secret key that only the sender and receiver share:

PropertyPlain HashHMAC
Integrity (detect modification)YesYes
Authentication (prove who sent it)No — anyone can hashYes — requires secret key
KeylessYesNo — requires shared key
Use caseFile checksums, content addressingAPI signatures, JWT, webhook verification

2. How the HMAC Algorithm Works (RFC 2104)

HMAC was defined by H. Krawczyk, M. Bellare, and R. Canetti in RFC 2104 (1997). The construction is elegantly simple — it hashes the message twice, each time mixing in the key via XOR:

HMAC(K, m) = H((K' ⊕ opad) ∥ H((K' ⊕ ipad) ∥ m))

Step-by-Step Construction

  1. Prepare the key (K'): If the key is shorter than the hash block size B (64 bytes for MD5/SHA-1/SHA-256, 128 bytes for SHA-384/SHA-512), pad with zeros on the right. If longer, hash it first: K' = H(K) padded to B bytes.
  2. Inner hash: XOR K' with the ipad (0x36 repeated B times), concatenate the message, and hash: H((K' ⊕ ipad) ∥ m).
  3. Outer hash: XOR K' with the opad (0x5C repeated B times), concatenate the inner hash result, and hash again: H((K' ⊕ opad) ∥ inner_hash).

Why Two Hashes?

The double-hash construction prevents length extension attacks. With a plain H(key ∥ message), an attacker who knows the hash output can compute H(key ∥ message ∥ attacker_data) without knowing the key, because Merkle-Damgard hash functions like SHA-256 allow the hash state to be resumed. HMAC's outer hash makes this attack impossible — the attacker would need to invert the outer hash to extend it.

Security proof: Bellare (2006) proved that HMAC is a secure PRF (pseudorandom function) as long as the underlying hash function is a PRF, even if the hash has structural weaknesses. This is why HMAC-MD5 remains safe in practice despite MD5 collision attacks.

3. HMAC-SHA256 vs HMAC-MD5 vs HMAC-SHA1 — Security Comparison

AlgorithmHashOutput LengthBlock SizeSecurity StatusRecommendation
HMAC-MD5MD5128 bits64 BNot broken in HMAC mode (RFC 6151)Deprecated — avoid in new systems
HMAC-SHA1SHA-1160 bits64 BNot broken in HMAC modeLegacy only — OAuth 1.0a still uses it
HMAC-SHA256SHA-256256 bits64 BSecure — FIPS 180-4Recommended default
HMAC-SHA384SHA-384384 bits128 BSecure — FIPS 180-4High-security applications
HMAC-SHA512SHA-512512 bits128 BSecure — FIPS 180-4Maximum security margin
Important nuance: While HMAC-MD5 and HMAC-SHA1 are not broken in the HMAC construction (collision attacks don't apply), they are deprecated for new systems. SHA-256 is the industry standard minimum. Use SHA-512 if you want extra margin for post-quantum scenarios.

4. Key Length and Key Management

How Long Should an HMAC Key Be?

HMAC VariantRecommended Key LengthMinimumRationale
HMAC-SHA256256 bits (32 bytes)128 bitsKeys shorter than hash output reduce security; longer keys are hashed first (no extra benefit)
HMAC-SHA384384 bits (48 bytes)192 bitsMatch hash output length
HMAC-SHA512512 bits (64 bytes)256 bitsMatch hash output length

Key Management Rules

  1. Generate keys with a CSPRNG — never use passwords, timestamps, or predictable values as keys.
  2. One key per service — compromising a webhook key should not expose your JWT signing key.
  3. Rotate keys periodically — most API providers allow multiple active keys during rotation.
  4. Store keys securely — use environment variables or a secrets manager (Vault, AWS KMS), never hardcode in source or commit to git.
  5. Transmit keys out-of-band — webhook secrets should be provisioned via a dashboard, not sent over email or chat.

5. Practical Use Cases with Code Examples

5.1 API Request Signing (Stripe-Style)

Stripe, GitHub, and Slack all sign webhook payloads with HMAC-SHA256. The pattern is the same:

// Server-side verification (Node.js)
const crypto = require('crypto');

function verifyWebhook(payload, signatureHeader, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  // CRITICAL: Use timing-safe comparison!
  const expectedBuf = Buffer.from(expected, 'hex');
  const actualBuf = Buffer.from(signatureHeader, 'hex');
  return crypto.timingSafeEqual(expectedBuf, actualBuf);
}

// Usage
const sig = req.headers['x-signature-sha256'];
if (!verifyWebhook(req.rawBody, sig, process.env.WEBHOOK_SECRET)) {
  return res.status(401).send('Invalid signature');
}
Never use === to compare HMACs: String equality comparison is vulnerable to timing attacks — an attacker can determine the correct HMAC character by character by measuring response time. Always use crypto.timingSafeEqual() (Node.js), hmac.compare_digest() (Python), or hash_equals() (PHP).

5.2 JWT HS256 Token Verification

A JWT signed with HS256 is simply Base64url(header) + "." + Base64url(payload) authenticated with HMAC-SHA256:

# Python — verify a JWT HS256 token
import hmac
import base64

def verify_jwt_hs256(token, secret):
    # Split token into header.payload and signature
    parts = token.split('.')
    if len(parts) != 3:
        return False

    signing_input = f"{parts[0]}.{parts[1]}".encode('utf-8')
    signature = base64.urlsafe_b64decode(parts[2] + '==')  # fix padding

    expected = hmac.new(
        secret.encode('utf-8'),
        signing_input,
        'sha256'
    ).digest()

    return hmac.compare_digest(signature, expected)
Related tool: For a full JWT inspection (header, claims, expiry check), use the JWT Decoder & Debugger.

5.3 AWS Signature Version 4

AWS Sig V4 is a multi-round HMAC derivation chain that produces a final signing key and signature:

# AWS Sig V4 key derivation
import hmac, hashlib

def get_aws_sigv4_key(secret_key, date, region, service):
    k_date = hmac.new(
        ('AWS4' + secret_key).encode('utf-8'),
        date.encode('utf-8'),
        hashlib.sha256
    ).digest()

    k_region = hmac.new(k_date, region.encode('utf-8'), hashlib.sha256).digest()
    k_service = hmac.new(k_region, service.encode('utf-8'), hashlib.sha256).digest()
    k_signing = hmac.new(k_service, b'aws4_request', hashlib.sha256).digest()

    return k_signing  # Used to sign the actual request

5.4 TOTP (Time-Based One-Time Password)

TOTP (RFC 6238) is HMAC-SHA1 applied to a time-based counter, then truncated to 6 digits:

# TOTP generation
import hmac, hashlib, struct, time

def totp(secret_base32, digits=6, period=30):
    import base64
    key = base64.b32decode(secret_base32)
    counter = int(time.time()) // period
    msg = struct.pack('>Q', counter)  # 8-byte big-endian

    h = hmac.new(key, msg, hashlib.sha1).digest()
    offset = h[-1] & 0x0F
    code = struct.unpack('>I', h[offset:offset+4])[0] & 0x7FFFFFFF
    return str(code % (10 ** digits)).zfill(digits)

6. HMAC vs CMAC vs HKDF

FunctionBased OnPurposeStandard
HMACHash functionMessage authenticationRFC 2104 / FIPS 198-1
CMACBlock cipher (AES)Message authentication (cipher-based)NIST SP 800-38B
HKDFHMACKey derivation from a master secretRFC 5869
KMACKeccak/SHA-3Message authentication (SHA-3 family)NIST SP 800-185
When to use CMAC instead: In smart card and payment systems (EMV ARQC/ARPC, GlobalPlatform SCP03), AES-CMAC is preferred over HMAC because the hardware already has an AES co-processor. Compute CMAC with the Crypto Checksum Verifier.

7. Common Bugs and Debugging Checklist

SymptomCauseFix
HMAC mismatch on serverKey encoding differs (UTF-8 vs hex vs Base64)Verify key format — decode hex/Base64 keys before using
HMAC works locally but fails in productionMessage includes trailing newline or BOMStrip \n and \r\n from both sides; use raw request body
Webhook verification always failsJSON body was parsed and re-serialized (changed whitespace)Use the raw request body, not JSON.stringify()
HMAC-SHA256 produces different resultsURL encoding vs raw bytes in messageUse the exact same byte sequence on both sides; check for + vs %20
Timing attack suspectedUsing === instead of constant-time comparisonReplace with timingSafeEqual / compare_digest
AWS Sig V4 failsWrong date format or region stringDate must be YYYYMMDD; region must match the endpoint

8. RFC Test Vectors

RFC 4231 defines standard test vectors for HMAC-SHA2. Use these to verify your implementation:

# RFC 4231 Test Case 2
Key:     "Jefe" (4 bytes)
Data:    "what do ya want for nothing?" (28 bytes)
Results:
  HMAC-SHA256: 5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
  HMAC-SHA384: af45d2e376484031617f78d2b58a6b1b9c7ef464f5a01b47e42ec3736322445e
               8e2240ca5e69e2c78b3239ecfafb72e2
  HMAC-SHA512: 164b7a7bfcf619e8148e915db6e1e4b0fa0e4e5f3a0a3f2d0e0f1e2d3c4b5a69
               8e7f6e5d4c3b2a190887e6f5d4c3b2a1 (truncated for display)
Verify your implementation: Open the HMAC Generator, enter key Jefe and message what do ya want for nothing?, then compare the HMAC-SHA256 output against the test vector above.

9. Quick Reference

ScenarioAlgorithmKey Recommendation
New API / webhook signingHMAC-SHA256256-bit random key (32 bytes, hex-encoded)
JWT token signingHMAC-SHA256 (HS256)≥256 bits; consider RS256/ES256 for multi-tenant
AWS API requestsHMAC-SHA256 (Sig V4)AWS secret access key (40 chars Base64)
OAuth 1.0aHMAC-SHA1Consumer secret + token secret
TOTP authenticator appsHMAC-SHA1 (common) or HMAC-SHA256Base32-encoded shared secret
Legacy / compatibilityHMAC-MD5Acceptable if already deployed; migrate to SHA-256