Skip to content
All posts

What is HMAC (and why SHA-256 alone isn't enough)?

HMAC is how you prove a message wasn't tampered with. It's SHA-256 plus a clever wrapping you should never try to reinvent.

DDDev DeskDeveloper Tools EditorPublished April 25, 20265 min readintermediate

# The problem

You want to send a message M from A to B and prove on arrival that:

1. It really came from A (who shares a secret with B)

2. Nobody modified a byte in transit

Simple-sounding. Naive solutions all fail.

# Why you can't just prepend a secret

"Just send SHA-256(secret + M) alongside the message."

For Merkle-Damgård hashes (SHA-1, SHA-2 family), this is vulnerable to a length-extension attack. An attacker who sees the hash can append their own bytes to M and compute a valid hash for M + attacker_bytes without knowing the secret.

HMAC fixes this.

# What HMAC is

HMAC wraps a hash function with two extra passes and a pair of constants. The construction:


HMAC(K, M) = H((K ⊕ opad) || H((K ⊕ ipad) || M))

Where:

  • H is your hash function (SHA-256, SHA-384, etc.)
  • K is the secret key (padded/hashed to the block size)
  • ipad = 0x36 repeated
  • opad = 0x5C repeated
  • || is concatenation, is XOR

You don't need to understand the internals to use it. But recognize that HMAC is not just "hash with a secret" — the two-layer structure is what defeats length extension.

# Where you'll meet HMAC

  • Webhooks (Stripe, GitHub, Slack): the provider HMAC-signs the body with a secret you share, you verify before processing.
  • AWS Signature V4: every API request to AWS is HMAC-signed.
  • JWT HS256: the signature part of a JWT with algorithm HS256 is HMAC-SHA256.
  • Session cookies: many frameworks sign cookies with HMAC so the browser can't forge them.

# How to verify a webhook


import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, headerSignature, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  return timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(headerSignature, "hex"),
  );
}

<div class="callout callout-warning" role="note"><div class="callout-title">Warning</div><div class="callout-body"><p>Use a <strong>constant-time comparison</strong> (like <code>timingSafeEqual</code>), not <code>===</code>. String equality leaks the first position that differs via timing, enabling attackers to brute-force the signature byte by byte.</p></div></div>

# HMAC vs signatures

  • HMAC uses a shared secret. Both sides can generate and verify. Fast.
  • Digital signatures (RSA, Ed25519, ECDSA) use public/private keys. Only the holder of the private key can sign; anyone with the public key can verify. Slower, but doesn't require sharing the secret.

Use HMAC when you control both ends. Use signatures when you need third parties to verify without trusting each other.

# Generate and verify HMAC in your browser

Paste a message and a secret into our HMAC Generator — it produces SHA-1, SHA-256, SHA-384 and SHA-512 variants, in hex and Base64.

Frequently asked questions

Is HMAC the same as just hashing secret + message?

No, and it matters. Naive `SHA-256(secret + message)` is vulnerable to length-extension attacks on Merkle-Damgård hashes like SHA-2. HMAC's double-hash construction prevents that.

Can I use HMAC with SHA-1?

HMAC-SHA1 is still considered secure for authentication (HMAC doesn't need collision resistance like signing does). But SHA-256 is just as fast and forward-safe; no reason to pick SHA-1 on new code.

How long should my HMAC key be?

At least the output size of the hash (32 bytes for HMAC-SHA256). Longer doesn't help; shorter is acceptable but wasteful.

New posts, once a week.

Practical developer guides. No spam. Unsubscribe any time.

Tools mentioned

Keep reading