HMAC Generator

Sign a message with a secret key — HMAC over SHA-256, SHA-512, SHA-1 or MD5 — and check a signature against it.

Signed as the UTF-8 bytes of what's pasted, spaces included, with every line break written as the one chosen. This box keeps line breaks as LF whatever was pasted, so pick CRLF for a body sent with CRLF line endings.

The key never leaves your browser: the signature is worked out here in the page, and nothing is sent anywhere.

Algorithm
Written as
Signature · SHA-256

715b29c013bf13a9338bc6fb117f3cbf8c39a96a3a2c5930c1582c2a78ed0308

30 bytes of message, 19 of key.

How webhook signatures are checked

Both services below sign exactly the way this tool does, so the value after the prefix is an HMAC you can check here.

ServiceWhat it sendsWhat the signature is made of
GitHubX-Hub-Signature-256: sha256=<hex>HMAC-SHA256 of the raw request body, keyed with the webhook secret.
StripeStripe-Signature: t=<time>,v1=<hex>HMAC-SHA256 of <time>.<raw body>, keyed with the signing secret, whsec_… and all.

GitHub: paste the raw body as the message, the secret as the key, and what follows sha256= into Check a signature. Stripe: sign t.body — the timestamp from the header, a dot, then the raw body — with the whole signing secret as the key, and compare against v1=.

Runs in your browser. Nothing you enter is sent anywhere.

How to use it

Paste the exact text to sign into Message and the key into Secret key, and the signature updates as you type. The message is signed as UTF-8, spaces included, with each line break as LF or CRLF, as picked under the box. The key is read as Text, Hex or Base64.

  1. Pick SHA-256, SHA-512, SHA-1 or MD5 under Algorithm.
  2. Pick Hex or Base64 under Written as, then copy the value next to Signature. The line under it counts the bytes of message and key.
  3. To check a signature you were sent, paste it into Check a signature, in hex or base64, whichever Written as is set to. The page answers Matches — this message, signed with this key. or Doesn't match — the signature pasted was made from a different message, key or algorithm.

How webhook signatures are checked sums up what GitHub and Stripe send and sign. A key that can’t be read in its encoding gets one sentence saying why.

Examples

A GitHub webhook body, signed with Text, SHA-256 and Hex (the page’s own sample):

Message     {"action":"opened","number":1}
Secret key  s3cr3t-do-not-share
715b29c013bf13a9338bc6fb117f3cbf8c39a96a3a2c5930c1582c2a78ed0308

The line reads 30 bytes of message, 19 of key. The same body re-serialized by a framework as {"action": "opened", "number": 1} signs to 7a95857d841f682fb194c2c9452cb7fcd28cacd3e936f4905b9afae17a144f97, so GitHub’s value no longer matches. Verify the raw bytes.

A Stripe event: the t= timestamp from Stripe-Signature, a dot and the raw body, with the whole signing secret as a Text key:

Message     1791968400.{"id":"evt_1","type":"invoice.paid"}
Secret key  whsec_3d8f0a2b7c9e41d6
8891cfd81ba9c1e31c4873d11724164684c9619d8097be544825f169e0fe6ba7

The header’s v1= value should equal it.

A Standard Webhooks (Svix) event signs the webhook-id and webhook-timestamp values and the body, joined by dots. The secret after whsec_ is a Base64 key, and the signature is Written as Base64:

Message     msg_2Lx9.1791968400.{"type":"user.created"}
Secret key  AbPR2zKGQQN2Hdr66kyjKR5po7xPFffb
wjTac8+zdN5cuvviEQqTEbluPPwEgIHjJhjcu+jxEjo=

With the prefix left on, whsec_AbPR2zKGQQN2Hdr66kyjKR5po7xPFffb fails with Base64 spells four characters to every three bytes, so a key comes in groups of four; this one has 38.

Good to know

  • HMAC is RFC 2104: H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ m)). A key longer than the hash’s block (64 bytes, or 128 for SHA-512) is hashed first, and the page says so: The key is longer than SHA-256's 64-byte block, so it's hashed to 32 bytes first, as RFC 2104 says.
  • sha256(key + message) isn’t a MAC. MD5, SHA-1, SHA-256 and SHA-512 all allow length extension: given that hash, anyone can compute the hash of the same input with bytes appended, without the key. HMAC’s nested form rules that out.
  • Prefixes aren’t part of the signature. Paste only what follows sha256=, v1= or v1,; with the prefix left on, it doesn’t match.
  • Hex ignores case, Base64 doesn’t. Spaces in a pasted signature are dropped. Signatures come out as lowercase hex or standard Base64 with +, / and =, so a base64url value with - and _ won’t match.
  • Keys must be spelled strictly. Hex takes an even number of digits with no 0x or spaces. Base64 takes the standard alphabet, padded with = to a multiple of four characters.
  • Pick CRLF for a body sent with CRLF line endings. A browser text box keeps line breaks as LF whatever was pasted. Email (RFC 5322) and multipart bodies end lines with CRLF. With the page’s key, name=Ada and role=admin, each followed by a line break, sign to b93de9bf5273c40ffafe09557897e5b1b50cc1cd63d4554562bbe218019cd257 as LF (20 bytes) and 1c21475874ab4e605df18104f064ca4a1f5c0c9e4d14f494fed1b93836afa44e as CRLF (22 bytes).
  • HMAC-SHA1 and HMAC-MD5 are dated, not broken. Hash collisions don’t give a practical HMAC forgery (RFC 6151 covers HMAC-MD5), and HOTP and TOTP (RFC 4226, RFC 6238) use HMAC-SHA-1 by default. New designs use SHA-256.
  • The message is limited to 100,000 characters, the key to 1,000 and a pasted signature to 200.

Questions

How do I generate an HMAC-SHA256?

Put the message and key in the two boxes and copy the SHA-256 signature. In Node, crypto.createHmac('sha256', key).update(message).digest('hex'); in Python, hmac.new(key, message, hashlib.sha256).hexdigest(), with both as bytes. In a shell, printf '%s' "$message" | openssl dgst -sha256 -hmac "$key" prints it after the =.

How do I verify a webhook signature?

Compute the HMAC of the raw request body (with the timestamp, where the service signs one) and compare it with the header’s value in constant time. In Express, read it with express.raw({ type: 'application/json' }), not express.json(), which parses it. Compare with Node’s crypto.timingSafeEqual, which throws when lengths differ, or Python’s hmac.compare_digest, and reject old timestamps to stop replays.

Why doesn’t my HMAC match?

Because the bytes differ somewhere: a re-serialized or trimmed body, a trailing newline, LF where the sender had CRLF, a key read as text when it’s hex or base64, or a prefix such as sha256= left on the signature. Check the byte counts under Signature, then change one thing at a time.