DevKitHub

API & Security

HMAC Generator and Webhook Signature Checker

Compute the HMAC of a message with a secret key, or check a signature you were sent. The webhook presets build the exact string Stripe, GitHub and Slack sign, and nothing you type leaves your browser.

1 line

HMAC-SHA-256

HMAC
2bbcfa9524f3218c7a34b30e6936f8b1a4516cb097f1a85a1c7d98b5977ec769
Read as
Key: 0 bytes, read as UTF-8 text. Message: 13 bytes, read as UTF-8 text. HMAC-SHA-256: 32 bytes, written as hex.
  • The key is empty. RFC 2104 allows it, so a result is shown, but a MAC with an empty key proves nothing about who made it.

This tool runs entirely in your browser. Your input is never uploaded, stored or logged.

How it works

HMAC, defined in RFC 2104, is H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ message)): the key is padded with zeros to the hash’s block size — 64 bytes for MD5, SHA-1 and SHA-256, 128 for SHA-384 and SHA-512 — XORed with two constants, and the hash is applied twice. A key longer than the block is hashed first, which is the step home-made implementations forget, so they fail only on long secrets. The result is as long as the hash: 32 bytes for HMAC-SHA-256. Everything here is computed in plain arithmetic in the page, checked against the RFC 4231 and RFC 2202 test vectors and, for SHA-384 and SHA-512, the FIPS 180-4 examples.

Because HMAC works on bytes, a mismatch almost always means the two sides disagree about bytes, and the result panel says which bytes were used. The key: a secret shown as 64 hex characters is 64 bytes if used as text but 32 if decoded as hex, and the two give unrelated MACs. GitHub, Stripe and Slack all use their secret as text — Stripe’s whsec_ prefix included — whereas services following Standard Webhooks, Svix among them, base64-decode the part after whsec_. The message: a trailing newline or a CRLF is signed like any other byte, and a browser text area turns CRLF into LF, so paste hex or base64 when exact line endings matter. The output: hex and base64 of one MAC look nothing alike, so the checker reads either, and strips a sha256= or v0= prefix. It compares in constant time, because === stops at the first differing character and so tells an attacker how much of a guess was right.

The presets follow each provider’s current documentation. Stripe signs the timestamp, a full stop and the raw body, and sends Stripe-Signature: t=…,v1=… in hex; v0 is a fake signature on test events that Stripe says to ignore, and while a secret is being rolled the header carries one v1 per active secret. GitHub signs the raw body and sends X-Hub-Signature-256: sha256=…, alongside the older SHA-1 X-Hub-Signature. Slack signs v0:, the X-Slack-Request-Timestamp, a colon and the body, and sends X-Slack-Signature: v0=…. The GitHub and Slack presets reproduce the examples in their documentation exactly. Stripe’s libraries and Slack’s own example also reject a timestamp more than five minutes from the receiver’s clock, to stop replays; this page checks the signature only, not its age.

Common problems

Every example below is run against this tool in our test suite, so what it says here is what the tool actually does.

The expected signature is 20 bytes — the length of an HMAC-SHA-1 — but HMAC-SHA-256 produces 32.

sha1=01dc10d0c83e72ed246219cdd91669667fe2ca59
Why:
GitHub sends two signature headers: X-Hub-Signature, which is HMAC-SHA-1, and X-Hub-Signature-256. The SHA-1 value can never match a SHA-256 HMAC, whatever the secret.
Fix:
Read X-Hub-Signature-256, or choose SHA-1 if that really is the header you have.

The signatures do not match.

8fde2e970f9163923fb1cb61bb945626ff2b4091d87e622ee3ad600160592325
Why:
One side signed the body with a trailing newline and the other without it. A newline added by an editor, by echo or by a logging call is a byte like any other, and it changes every bit of the MAC.
Fix:
Sign and verify the raw request bytes as received. Use printf rather than echo in a shell, and paste the body as hex or base64 here when line endings matter.

The expected signature is 63 hex digits, an odd number.

sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e1
Why:
A character was lost when the signature was copied — usually the last one, dropped by a terminal selection or a line wrap.
Fix:
Copy the header value again in full. An HMAC-SHA-256 signature is exactly 64 hex digits.

The same secret gives a different HMAC here than in my code.

Why:
The secret is written in hex or base64, and one side decoded it to bytes while the other used the characters as text. "4a656665" used as text is an 8-byte key; decoded as hex it is the 4-byte key "Jefe" from RFC 4231, and the two MACs share nothing.
Fix:
Check how the provider documents the secret and set Key encoding to match. GitHub, Stripe and Slack all use it as text.

No signatures found matching the expected signature for payload.

Why:
Stripe’s library error when the body was parsed as JSON and serialised again before verification — express.json() registered before the webhook route is the usual cause — so whitespace or key order changed and the signed bytes are gone.
Fix:
Verify against the raw body (express.raw for that one route), then parse it. The Stripe preset here signs "{t}.{body}" so you can compare byte for byte.

Checking the signature with === works, but a security review flags it.

Why:
String equality returns at the first differing character, so the response time reveals how much of a guessed signature was right, and a valid one can be built a byte at a time.
Fix:
Compare with crypto.timingSafeEqual in Node, hmac.compare_digest in Python, hmac.Equal in Go or MessageDigest.isEqual in Java.

Frequently asked questions

How do I verify a GitHub webhook signature?
Compute HMAC-SHA-256 of the raw request body with the webhook secret as the key, write it in lowercase hex, put "sha256=" in front and compare it with the X-Hub-Signature-256 header in constant time. Choose the GitHub preset, paste the body and the secret, and paste the header into Expected signature to check one here.
Why does my HMAC not match the provider’s?
Nearly always because the bytes differ: the body was re-serialised or gained a trailing newline, the secret was decoded from hex or base64 on one side only, or the signature is being compared in a different encoding. The result panel lists how the key and message were read, so each can be checked in turn.
Is HMAC-SHA1 still safe to use?
HMAC does not rely on the collision resistance that practical attacks have broken in SHA-1, and no practical attack on HMAC-SHA-1 is known, so existing uses such as GitHub’s legacy header and most TOTP apps are not broken. New designs should still use HMAC-SHA-256.
Is my secret key sent anywhere?
No. The HMAC is computed in your browser by code on this page, and there is no endpoint on this site that accepts a key or a message. A browser test checks that no network request made while the tool is used contains the secret.

Last updated