All Glossary Terms
Security

What is a hmac?

HMAC stands for Hash-based Message Authentication Code — the algorithm under almost every webhook signature scheme, almost always HMAC-SHA256. It combines a secret key with a message hash to prove authenticity: anyone with the secret can compute the same digest from the same message, and nobody without the secret can forge one. HMAC defends against length-extension attacks that the naïve `SHA256(secret + body)` construction is vulnerable to. Three operational rules: use ≥32-byte random secrets, decode hex/base64 consistently before comparing, and always use a constant-time compare (`crypto.timingSafeEqual`, `hmac.compare_digest`) to prevent byte-by-byte timing leaks.

HMAC stands for Hash-based Message Authentication Code. It's the algorithm under almost every webhook signature scheme. The construction is HMAC-SHA256 in 99% of cases, occasionally HMAC-SHA1 (legacy) or HMAC-SHA512 (rare).

The math, briefly: HMAC takes a secret key and a message, applies the underlying hash function (SHA-256) twice with two derived keys, and outputs a fixed-size digest (32 bytes for SHA-256). The properties that make it useful for webhooks: anyone with the secret can compute the same digest from the same message, and nobody without the secret can produce a valid digest for any message. This means a recipient who shares the secret can verify the message wasn't forged or modified.

Why HMAC and not just SHA256(secret + body)? The naïve construction is vulnerable to length-extension attacks against MD5/SHA-1/SHA-256 — an attacker can take a known valid digest and append data, producing a new valid digest without knowing the secret. HMAC's double-hashing structure prevents this. Always use HMAC; never use raw hash(secret + message).

In webhook contexts, HMAC has three operational concerns beyond the math. Secret length: use ≥32 bytes (256 bits) of random data. Short secrets (anything under 16 bytes) reduce the brute-force search space. Generate with crypto.randomBytes(32) or equivalent — never a password, never a UUID, never anything human-readable. Encoding: the digest output is binary; webhook headers transport it as hex (Stripe, GitHub) or base64 (Shopify). Always decode to the same form before comparing. Comparison: use a constant-time compare (crypto.timingSafeEqual, hmac.compare_digest). Standard string equality returns early on the first byte mismatch, leaking byte-by-byte timing info that an attacker can exploit over many requests.

HMAC's threat model is "shared secret authenticity." Both ends hold the same secret, so both can produce valid signatures — meaning HMAC proves the message came from *someone with the secret*, not specifically from the source. If your secret leaks, you lose authenticity entirely. Rotate immediately when leaks are suspected (see signing-secret-rotation).

For richer guarantees — non-repudiation, public verifiability — you'd use asymmetric signatures (RSA, Ed25519). A few providers do (Apple, some webhook gateways), but HMAC dominates because it's faster, simpler, and good enough when the source and recipient are explicitly partnered.

Example

Compute HMAC-SHA256 in Node.js
import crypto from 'node:crypto'

const secret = process.env.WEBHOOK_SECRET // 32+ random bytes
const message = rawBody                   // exactly as sent
const digest = crypto
  .createHmac('sha256', secret)
  .update(message)
  .digest('hex')                          // → 64-char hex string

See HMAC in real traffic

WebhookWhisper captures every webhook with full headers, body, signature, and timing — so concepts like hmac stop being abstract and become something you can inspect.

Start Free