How an HMAC works — and why it beats a plain hash
A plain hash like SHA-256 turns any input into a fixed-length fingerprint, but it proves only that data wasn't corrupted — anyone can compute it. An HMAC (hash-based message authentication code) adds a secret key to the mix: the standard construction is HMAC(K, m) = H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ m)) — the key is XOR-padded, the message is hashed once with the inner key, and that digest is hashed again with the outer key. Only someone who holds the same key can produce or verify a valid signature, which is why HMACs are used to authenticate webhooks, API requests, session tokens and JWTs (the HS256 algorithm is exactly HMAC-SHA256).
You might wonder why anyone bothers with that two-pass construction instead of simply hashing secret + message. The answer is the length-extension attack: SHA-256 (like SHA-1 and SHA-512) processes input in blocks and exposes its full internal state as the digest, so an attacker who knows SHA-256(secret ‖ message) can keep hashing from that state and forge a valid digest for secret ‖ message ‖ padding ‖ anything — without ever knowing the secret. HMAC's nested, double-keyed design closes that hole completely, which is why every serious API uses HMAC rather than an ad-hoc concatenation.
This tool feeds your UTF-8 message and key to the browser's built-in Web Crypto API (crypto.subtle.importKey + crypto.subtle.sign), so the computation is native, fast and entirely local.
Choosing the hash and the encoding
Use HMAC-SHA256 by default — it is what GitHub, Stripe, Slack, Shopify and most modern APIs specify, and it is the hash behind JWT's HS256. SHA-384/512 are equally fine if a spec calls for them. HMAC-SHA1 deserves a footnote: although SHA-1 itself is broken for collisions, the HMAC construction doesn't rely on collision resistance, so HMAC-SHA1 is not practically broken — but it survives mainly in legacy APIs (e.g. older OAuth 1.0 signatures), so use it only when the other side requires it.
The raw output of an HMAC is bytes; what you see is an encoding of those bytes. Hex (lowercase, two characters per byte) is what most webhook headers use — GitHub's sha256=… value is 64 hex characters. Base64 is one-third shorter and common in Authorization headers and AWS-style request signing. They are the same signature, just written differently — which is why the verifier above accepts either.
Test vector: key key, message The quick brown fox jumps over the lazy dog
| Algorithm | HMAC (hex) |
|---|---|
| HMAC-SHA1 | de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9 |
| HMAC-SHA256 | f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8 |
| HMAC-SHA512 | b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a |
Type those exact inputs above and you should see exactly these values — a quick sanity check that any HMAC implementation should pass.
Debug checklist: "my webhook signature doesn't match"
- Sign the raw body bytes, not parsed-then-re-serialized JSON.
JSON.parse→JSON.stringifyreorders nothing in theory but changes whitespace and unicode escaping in practice — one byte off and the HMAC is completely different. - Read the exact header the provider documents: GitHub uses
X-Hub-Signature-256(not the older SHA-1X-Hub-Signature), Stripe usesStripe-Signature, Slack usesX-Slack-Signature. - Strip the prefix and match the encoding. GitHub's value is
sha256=+ lowercase hex; some APIs send Base64. Comparing hex against Base64 will never match. - Check timestamp schemes. Stripe and Slack don't sign the body alone — they sign
timestamp + "." + body(Stripe) or"v0:" + timestamp + ":" + body(Slack). Recreate the exact signed string, including separators. - Verify the secret itself — test vs. live keys, trailing newlines from copy-paste, and whether the provider expects the secret as text or decoded from Base64.
Related tools: generate plain digests with the hash generator, decode and inspect HS256 tokens with the JWT decoder, or convert signatures between text and Base64 with the Base64 encoder/decoder.
Frequently asked questions
- What is an HMAC?
- An HMAC (hash-based message authentication code) is a keyed hash: it mixes a secret key into a cryptographic hash such as SHA-256 to produce a short signature of a message. Anyone who holds the same key can recompute the signature and confirm both that the message was not altered (integrity) and that it came from someone who knows the key (authenticity).
- What's the difference between an HMAC and a regular hash?
- A plain hash like SHA-256 depends only on the message, so anyone can compute it — it proves integrity but not who produced it. An HMAC also depends on a secret key, so only parties holding the key can create or verify a valid signature. Naively concatenating the key with the message and hashing it is not a safe substitute, because plain SHA-256 is vulnerable to length-extension attacks; the HMAC construction was designed specifically to avoid that.
- Is my secret key sent anywhere?
- No. The message and the secret key are processed entirely on your own device using the browser's built-in Web Crypto API (crypto.subtle). Nothing is uploaded, logged or stored — you can load the page, disconnect from the internet and keep computing signatures.
- Which algorithm should I choose?
- Use HMAC-SHA256 unless the service you are integrating with specifies otherwise — it is the de-facto standard for webhooks, API signing and JWTs (HS256). SHA-384 and SHA-512 are fine too. HMAC-SHA1 is still cryptographically acceptable inside the HMAC construction, but it survives mostly in legacy APIs; pick it only when the other side requires it.
- How do I verify a webhook signature?
- Take the raw, unmodified request body exactly as it arrived (the bytes, not re-serialized JSON), compute an HMAC over it with the shared secret using the algorithm the provider documents, and compare the result to the signature header — for example GitHub's X-Hub-Signature-256 (hex, prefixed with sha256=) or Stripe's Stripe-Signature (hex, with a timestamp that must also be included in the signed payload). Paste the body, secret and the expected value into this tool to check your math.
- Can an HMAC be reversed?
- No. Like any cryptographic hash, an HMAC is one-way: the original message cannot be recovered from the signature. Verification works the other way around — you recompute the HMAC with the same secret key and compare. That also means an HMAC can only be verified by someone who holds the key; without it, the signature is just an opaque string.