Cryptographic Protocol

Using HMAC for Message Authentication

How nested hashing with inner and outer padding creates tamper-proof API signatures and defeats length-extension exploits.

HMAC (Hash-based Message Authentication Code) is a symmetric cryptographic construction defined in RFC 2104 that verifies both data integrity and message authenticity by combining a shared secret key with an underlying hash function. Unlike naive secret concatenation (H(k || m)), HMAC employs a nested two-pass hashing scheme with inner and outer padding constants that mathematically prevents length-extension attacks and existential forgery.

In short

HMAC turns any standard hash function (like SHA-256) into a secure message authenticator using a nested formula: HMAC(k, m) = H((k ⊕ opad) || H((k ⊕ ipad) || m)). By padding and hashing the key in two separate stages, it guarantees that an attacker who intercepts the signature cannot forge new messages, alter query parameters, or reconstruct the secret key.

01. The Plain-Language Analogy

The Double-Enveloped Wax Notary Stamp

Imagine sending a contract through an untrusted courier. If you simply append your signature wax seal to the bottom of the raw document, a dishonest courier could paste extra clauses below your signature, calculate the new length, and claim you approved the extended terms.

With HMAC, you place the original contract into an inner tamper-evident envelope mixed with an inner secret stamp pattern (ipad = 0x36). You seal that envelope and calculate its checksum. Then, you place that sealed inner envelope inside a second, heavier outer envelope stamped with an outer secret stamp pattern (opad = 0x5c). The courier cannot append anything to the inner document without ripping the outer seal, and they cannot recreate either seal without physical access to your private stamp ring.

02. Step-by-Step Mechanics (RFC 2104 Pipeline)

For SHA-256, the block size B = 64 bytes (512 bits) and the output size L = 32 bytes (256 bits). The HMAC pipeline executes across five deterministic steps:

Step 1: Key Normalization to Block Size $B$

If the secret key $K$ is longer than 64 bytes, it is hashed first: $K_0 = H(K)$ (32 bytes). If $K$ is shorter than 64 bytes, it is zero-padded on the right with null bytes (0x00) until it reaches exactly 64 bytes ($K_0$).

Step 2: Inner Masking with ipad

Compute the inner padded key S_i = K_0 ⊕ ipad, where ipad is the constant byte 0x36 repeated 64 times (0x363636...). This thoroughly masks the key bits into a 64-byte initialization vector.

Step 3: First Hash Pass (Inner Digest)

Concatenate the inner masked key with the variable-length message $m$ and hash the result: H_i = H(S_i || m) The output $H_i$ is a fixed 32-byte intermediate hash.

Step 4: Outer Masking with opad

Compute the outer padded key S_o = K_0 ⊕ opad, where opad is the constant byte 0x5c repeated 64 times (0x5c5c5c...). The Hamming distance between 0x36 and 0x5c is 4 bits per byte, ensuring complete orthogonality between the two stages.

Step 5: Second Hash Pass (Final Authentication Tag)

Concatenate the outer masked key $S_o$ with the 32-byte inner hash $H_i$ and hash them together: HMAC = H(S_o || H_i) Because the message $m$ is buried inside $H_i$ and re-hashed behind $S_o$, an attacker cannot perform length-extension on the outer digest.

03. Worked Example: RFC 4231 Test Vector #2

Every standard-compliant HMAC-SHA256 library must reproduce this canonical test vector defined in RFC 4231 Section 4.3:

Secret Key ($K$): "Jefe" (4 ASCII bytes: 0x4a656665)

Padded with 60 zero bytes to form 64-byte block $K_0$.

Message ($m$): "what do ya want for nothing?" (28 bytes)
Expected Output (HMAC-SHA256):
5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843
Production Webhook Verification Pattern (Node.js):
import crypto from 'crypto';

export function verifyStripeWebhook(payload, signatureHeader, secret) {
  // Format: "t=1614555842,v1=5bdcc146bf60..."
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(kv => kv.split('='))
  );

  const timestamp = parseInt(parts.t, 10);
  const receivedSig = Buffer.from(parts.v1, 'hex');

  // Prevent replay attacks (5 minute window)
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
    throw new Error('Webhook timestamp outside tolerance');
  }

  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${payload}`)
    .digest();

  // Strict constant-time comparison prevents timing attacks (CWE-208)
  if (expectedSig.length !== receivedSig.length || !crypto.timingSafeEqual(expectedSig, receivedSig)) {
    throw new Error('HMAC signature mismatch');
  }
  return true;
}

04. Common Misconceptions Corrected

Misconception 1: "HMAC encrypts the payload"

HMAC provides message authentication and integrity, not confidentiality. The payload travels in plaintext alongside the HMAC tag. If you need confidentiality as well, use Authenticated Encryption with Associated Data (AEAD) like AES-256-GCM or ChaCha20-Poly1305.

Misconception 2: "SHA256(secret || message) is just as safe as HMAC"

This naive prefix construction is catastrophically vulnerable to length-extension attacks on Merkle-Damgård hashes (MD5, SHA-1, SHA-256). An attacker can reconstruct the internal state registers from the hash and append extra data (&role=admin) with a valid hash without knowing the secret. HMAC completely eliminates this vulnerability.

Misconception 3: "HMAC is vulnerable if the underlying hash has collisions"

Even though MD5 is completely broken for collision resistance (digital signatures and certificates), HMAC-MD5 remains surprisingly resistant to practical attacks because the inner and outer keys mask the compression inputs. However, modern standards require migrating to HMAC-SHA256 for full cryptographic defense.

05. Message Authentication Scheme Comparison

Authentication Scheme Key Architecture Length Extension Safe? Primary Use Cases
HMAC-SHA256 Symmetric (shared secret) Yes (Nested layers) Stripe/AWS API signing, JWT (HS256), IPsec
KMAC256 Symmetric (Keccak/SHA-3 cSHAKE) Yes (Native sponge) Next-gen NIST post-quantum applications
Ed25519 / ECDSA Asymmetric (Public/Private key) Yes (Hash-then-sign) SSH keys, Git commit signing, Bitcoin tx
H(key || msg) Symmetric (Naive prefix) No (Vulnerable) Legacy broken implementations (Never use)