HMAC Construction
debt(d6/e3/b3/t7)
Closest to 'specialist tool catches' (d5), +1 because semgrep patterns for hash(secret.message) exist but rolling-your-own MAC often escapes default rules and requires custom rules or code review to catch.
Closest to 'simple parameterised fix' (e3), the quick_fix is swapping hash(...) for hash_hmac('sha256', $message, $key, true) plus hash_equals() for comparison — a small localized pattern replacement, slightly more than one line.
Closest to 'localised tax' (b3), HMAC usage is typically confined to specific verification points (webhooks, tokens, cookies) rather than pervading the whole system.
Closest to 'serious trap' (t7), the misconception that HMAC provides confidentiality plus the intuitive but broken hash(secret||message) construction contradict how developers naively reason about 'signing' — the obvious approach is dangerously wrong.
Also Known As
TL;DR
Explanation
HMAC (Hash-based Message Authentication Code) is defined in RFC 2104 as HMAC(K, m) = H((K' XOR opad) || H((K' XOR ipad) || m)), where K' is the key padded or hashed to the hash function's block size, ipad is 0x36 repeated, and opad is 0x5C repeated. This nested structure serves a specific purpose: even though Merkle-Damgard hashes like SHA-256 are vulnerable to length-extension attacks (where an attacker can append data to a hash without knowing the input), the outer hash of an HMAC binds the entire computation and blocks that attack. HMAC provides authenticity (the message came from someone with the key) and integrity (the message was not altered), but not confidentiality — the message itself is not encrypted. Security depends on: (1) using a strong underlying hash (SHA-256 or SHA-3, never MD5/SHA-1 for new systems), (2) keys of at least the hash's output length (32 bytes for SHA-256), (3) constant-time tag comparison to prevent timing attacks, and (4) never reusing an HMAC key for other purposes like encryption. In PHP, use hash_hmac('sha256', $message, $key, true) for the raw binary tag and hash_equals() for verification. HMAC is the building block for many protocols: TLS record MACs (in CBC modes), JWT HS256 signatures, AWS Signature V4, HKDF key derivation, and TOTP/HOTP one-time passwords. It is distinct from AEAD constructions like AES-GCM, which provide authentication as part of encryption — HMAC is used when you need authentication of plaintext or of already-encrypted data (encrypt-then-MAC).
Common Misconception
Why It Matters
Common Mistakes
- Building a homemade MAC as hash(secret || message) — vulnerable to length-extension on SHA-2 hashes.
- Comparing HMAC tags with === or strcmp — timing side channels leak tag bytes; use hash_equals().
- Reusing the same key for HMAC and encryption — key separation is a core cryptographic hygiene rule.
- Using MD5 or SHA-1 as the underlying hash for new systems — while HMAC-MD5 is not yet fully broken, it should not be used.
- Truncating HMAC tags to too few bytes (e.g. 32 bits) — makes brute-force forgery feasible.
Avoid When
- You need message confidentiality — HMAC does not encrypt; combine with AES-GCM or use an AEAD directly.
- An AEAD cipher like AES-GCM or ChaCha20-Poly1305 already covers your needs — no separate MAC is required.
- You need public verifiability — HMAC uses a shared secret; use digital signatures (RSA/ECDSA/Ed25519) instead.
- You would derive multiple keys from one HMAC key — use HKDF, which is built on HMAC but designed for key derivation.
When To Use
- Authenticating messages between services sharing a symmetric secret (e.g. webhook signatures).
- Building JWT HS256 signatures or verifying signed URLs and session cookies.
- Encrypt-then-MAC constructions when using a non-AEAD cipher mode like AES-CBC.
- Verifying integrity and authenticity of password reset links or one-time tokens carrying user data.
Code Examples
// Homemade MAC — vulnerable to length extension:
function signMessage(string $message, string $secret): string {
return hash('sha256', $secret . $message);
}
function verifyMessage(string $message, string $tag, string $secret): bool {
$expected = hash('sha256', $secret . $message);
return $expected === $tag; // Timing attack: leaks tag byte by byte
}
// Attacker who knows H(secret || message) can compute
// H(secret || message || padding || extension) without knowing the secret.
// Proper HMAC construction:
function signMessage(string $message, string $key): string {
// Raw binary tag; use bin2hex() only if transport requires text.
return hash_hmac('sha256', $message, $key, true);
}
function verifyMessage(string $message, string $tag, string $key): bool {
$expected = hash_hmac('sha256', $message, $key, true);
return hash_equals($expected, $tag); // Constant-time comparison
}
// Key generation: 32 bytes of CSPRNG output for HMAC-SHA256.
$key = random_bytes(32);