What is HMAC? - cover art

Crypto and hashing 14 min read

What is HMAC?

August 16, 2026 · 14 min read

HMAC (Hash-based Message Authentication Code) proves that a message was not tampered with and that the sender knows a shared secret key. It combines a cryptographic hash (usually SHA-256) with a secret using a nested construction defined in RFC 2104 / FIPS 198.

Stripe webhooks, GitHub delivery signatures, and AWS Signature Version 4 all use HMAC-style constructions. Unlike a bare SHA-256 digest, an attacker cannot forge a valid HMAC without the key.

Definition

Inputs: message m, secret key K, hash function H (e.g. SHA-256). Output: fixed-size tag. Both parties compute the same tag from m and K; the verifier compares tags in constant time. Changing one byte of m or using the wrong key yields a different tag with overwhelming probability.

HMAC does not encrypt the message - the body can still be JSON in plain text. It only provides authenticity and integrity for whoever holds the secret.

How HMAC works (conceptually)

HMAC pads the key to the block size of the hash, XORs with inner and outer pad constants, and runs two hash passes: H((K ⊕ opad) || H((K ⊕ ipad) || message)). This structure protects against extension attacks that naive H(key || message) schemes suffer.

import { createHmac } from "node:crypto";

const secret = process.env.WEBHOOK_SECRET;
const body = request.rawBody; // exact bytes received
const tag = createHmac("sha256", secret).update(body).digest("hex");

// Compare to header: X-Signature-Sha256=tag

HMAC-SHA256 in practice

Prefer HMAC-SHA256 for new integrations. Encode tags as hex or Base64 consistently; document header names and whether the signature covers timestamps or version prefixes. Include replay protection with monotonic nonces or clock-skew windows when headers carry timestamps.

Key rotation: support two secrets during overlap; verify with either key and retire the old one on a published date.

Verifying safely

Use constant-time comparison (crypto.timingSafeEqual in Node) on equal-length buffers. Parsing hex vs Base64 mismatches causes false negatives - normalize encoding before compare. Always HMAC the raw body bytes your framework received, not re-serialized JSON, or pretty-print differences will break signatures.

import { timingSafeEqual } from "node:crypto";

function safeEqual(a, b) {
  const ba = Buffer.from(a, "hex");
  const bb = Buffer.from(b, "hex");
  if (ba.length !== bb.length) return false;
  return timingSafeEqual(ba, bb);
}

HMAC vs plain hash

A public SHA-256 digest lets anyone verify content integrity if they already have the file - but not authenticity. HMAC requires the secret to produce or verify the tag. Digital signatures (RSA, ECDSA) use public-key math instead of shared secrets; choose HMAC for symmetric trust, signatures when many verifiers need one public key.

Do not confuse HMAC with password hashing - passwords need slow algorithms (bcrypt, Argon2), not fast HMAC.

FAQ

Is HMAC encryption?
No. HMAC authenticates messages; it does not hide content.
Can I use the same secret for all customers?
Avoid that. Per-tenant secrets limit blast radius if one integrator leaks a key.
HMAC-SHA256 vs RSA signatures?
HMAC needs a shared secret. RSA/ECDSA use public/private keys - better when many parties verify with one public key.
Why did my webhook signature fail?
Common causes: wrong secret, re-encoded JSON body, wrong header encoding (hex vs Base64), or charset changes.

Related: SHA-256 explained simply

Browse all tools