HMAC generator
Computes HMAC-SHA-1, SHA-256, SHA-384 and SHA-512 from a message and a key, and identifies which one a Stripe, GitHub or Shopify webhook signature came from.
Enter a key to compute the signatures.
What this tool does
HMAC turns a message and a shared secret into a short signature. Anyone holding the same secret can recompute it and see whether the message arrived unchanged and came from someone who also knows the secret. This tool computes all four common variants at once, and — given a signature you were sent — tells you which one produced it.
That second direction is usually the reason people arrive. A webhook is failing verification, and the question is not really "is this signature valid" but "which of the several plausible things am I doing differently from the sender". A sha256= style prefix in front of a pasted signature is recognised and stripped, so a webhook header value can go in exactly as it arrived.
The key is where this goes wrong
HMAC signs bytes with bytes. The message is usually obvious — the raw request body — but the key almost never is, because a secret arrives as a string and a string is not bytes until you decide how to read it:
a3f2 as text 4 bytes 61 33 66 32 a3f2 as hex 2 bytes a3 f2
Both readings are legitimate, both produce a perfectly well-formed signature, and the two signatures have nothing in common. Nothing warns you: there is no error, no length complaint, just a value that does not match the one the sender computed. That is why the key encoding here is a visible choice rather than a guess — when a signature does not match, this is the first thing to flip.
As a rough guide: a secret with a prefix like whsec_ or a run of mixed-case letters and digits is normally meant as text; a string of exactly 32 or 64 characters using only 0-9 and a-f is usually hex; and one ending in = is almost certainly Base64. But check the sender’s documentation rather than the shape, because the shape is not proof.
The message has to be the exact bytes
The other half of a mismatch is the message. HMAC is defined over bytes, so anything that changes the bytes changes the signature completely — there is no partial credit and no near miss.
- Re-serialising JSON. Parsing a body and stringifying it again may reorder keys, change spacing, or normalise numbers. Sign and verify the raw body you received, never a round-tripped copy of it.
- A trailing newline. Some tools add one when you save the body to a file; it is a byte, and it changes everything.
- Character encoding. A body containing non-ASCII text has to be read as the same encoding both sides use — UTF-8 in practice.
- Compression or middleware. If something in front of your handler decompresses or rewrites the body, verify before that happens, not after.
Many providers also do not sign the body alone. Stripe signs a timestamp and the body joined with a dot; AWS signs a canonical request built from method, path, headers and a hash of the payload. If verification fails against the plain body, the signed string is probably not the plain body — that is documented on the sender’s side and worth reading before debugging further.
Why SHA-1 is offered here and not in the hash tool
The hash tool marks SHA-1 as broken. This one lists HMAC-SHA-1 without a warning, and that is deliberate rather than an oversight.
SHA-1 is unusable for signatures and certificates because collisions can be constructed: two different documents can be made to share a digest. HMAC does not depend on that property. Its security rests on the secret key, and the construction — hashing the message twice, with the key mixed in both times — holds up even when the underlying hash is collision-weak. HMAC-SHA-1 remains sound and is still what OAuth 1.0a and older AWS signing use, so a tool that refused to compute it would simply be less useful without being safer.
For anything new, SHA-256 is the sensible default. Longer digests are not meaningfully stronger here — the security ceiling is the key, not the digest length — so SHA-384 and SHA-512 are worth choosing only when something you must interoperate with asks for them.
Comparing signatures safely
This page compares with an ordinary string comparison, which is fine here: you are holding the key yourself, so there is nothing to leak. In a server verifying an incoming request, it is not fine. An ordinary comparison stops at the first differing byte, so the time it takes reveals how much of a guess was correct, and an attacker who can send many requests can recover a valid signature one byte at a time.
Use the constant-time comparison your platform provides — crypto.timingSafeEqual in Node, hmac.compare_digest in Python, hash_equals in PHP — on the raw bytes rather than on hex strings. Along with it, compare against a signature you computed yourself rather than trusting an algorithm named in the request, and reject a message whose timestamp is far from now so an old valid signature cannot be replayed.
HMAC is not a hash, and not a signature
Three things get confused here, and the difference matters for what you can claim afterwards:
- A hash takes a message and produces a digest. Anyone can compute it, so it proves the message has not changed accidentally — not that it came from anyone in particular.
- An HMAC takes a message and a shared secret. Both parties hold the same key, so it proves the sender knew the secret. It cannot prove which party sent it, because either could have produced it.
- A signature uses a private key that only the sender holds, and a public key anyone can verify with. That is what gives non-repudiation: the sender cannot later deny it.
So HMAC is the right tool between two systems that already share a secret — webhooks, internal APIs, session tokens — and the wrong one when you need to prove to a third party who sent something. Also note it authenticates but does not conceal: the message travels in the clear, and HMAC says nothing about confidentiality.
Verifying a Stripe webhook signature
Stripe is the one recipe here that is not a paste, and it is usually the one people arrive having already tried. The Stripe-Signature header is a comma-separated list of elements rather than a signature, and the string Stripe signed is not the request body on its own. Stripe’s own documentation breaks the header across lines for readability; a real one arrives on a single line.
Stripe-Signature: t=1492774577, v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd, v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39
- Message: the value of t, then a full stop, then the raw request body exactly as it arrived. For the header above that string starts 1492774577. and the body follows immediately. Stripe calls it the signed payload, and it is the whole reason a Stripe signature never matches the body alone.
- Key: the signing secret of that one endpoint, whole, including its whsec_ prefix. It is not your API key, and it is not another endpoint’s secret — the same URL registered twice has two.
- Key encoding: Text. A signing secret is a string Stripe chose, and although it looks random it is neither hex nor Base64; reading it as either gives different bytes and a signature that matches nothing.
- Signature to check: the v1 element on its own, not the whole header. v1= pastes as it stands, because that label is stripped exactly as sha256= is — but the whole header does not, because the leading t= is taken for the label and what follows it is not a signature at all.
Three more things about this header are worth knowing before you debug anything else. Ignore every scheme that is not v1: the v0 element on test events is deliberately not a real signature, and ignoring the rest is what stops a weaker scheme being forced on you. While an endpoint secret is being rolled, both secrets are live for up to a day and the header carries one v1 element per secret, of which only one will match. And every delivery attempt is signed afresh, so a retry does not carry the first attempt’s signature — the timestamp inside the signed string is also what makes rejecting an old one a real defence rather than a gesture, since it cannot be altered without breaking the signature.
Verifying a GitHub webhook signature
GitHub is the paste this tool was built around, and the one recipe here you can check from end to end, because GitHub publishes a worked example. The signature arrives in X-Hub-Signature-256 as the label sha256= followed by a hex digest, and that label is recognised and removed here, so the header value goes in exactly as it arrived.
X-Hub-Signature-256: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
- Message: the raw request body, byte for byte. GitHub's published example signs the body Hello, World! with nothing after it — no trailing newline.
- Key: the secret you typed into the webhook's settings, exactly as you typed it. GitHub's published example uses It's a Secret to Everybody.
- Key encoding: Text. A GitHub secret is a string you chose, so it is never hex — even a secret that happens to be nothing but hex digits is read as the characters you typed.
- Signature to check: the whole header value, sha256= and all. Hex is compared without regard to case, so it does not matter which way your logs print it.
Those three values together are the quickest sanity check there is: paste them in and this page reports a match on HMAC-SHA-256 in hex, which tells you the calculator and your reading of the recipe are both right before you try either on a real delivery. GitHub also sends an older X-Hub-Signature header, which is HMAC-SHA-1 over the same body and is kept only for legacy purposes; because this page computes all four digests at once, a signature from either header is identified without your having to say which. The one thing it cannot rescue is a body that arrived as anything other than the bytes GitHub sent — payloads can carry characters outside ASCII, and both sides have to read them as UTF-8.
Verifying a Shopify webhook signature
Shopify writes its digest in Base64 rather than hex, and that is the only difference that matters here: the same 32 bytes become 44 characters ending in a single equals sign, and that character is padding rather than a label, so nothing is stripped from the front of it.
X-Shopify-Hmac-SHA256: dXEH6g6yUJ/CESIczphLijdXC211hsIsRvQ3nIsEPhc=
- Message: the raw request body, byte for byte as delivered. Shopify’s own warning is about body-parser middleware — verify first and parse afterwards, because a parser that has already turned the body into an object has thrown the bytes away.
- Key: the client secret of the app the webhook belongs to. Not its access token, and not the API key sitting beside it in the same panel.
- Key encoding: Text. As with the other two, the secret is a string rather than an encoding of bytes.
- Signature to check: the whole header value. It pastes as it stands, and because both hex and Base64 are tried against every digest, the encoding this page reports back is itself the answer to which spelling the sender used.
The value in the block above is there to show the shape: it is the same 32 bytes as the GitHub example, written in Base64 instead of hex. That is worth seeing side by side, because it is the whole content of the difference between the two headers — one digest, two spellings — and it is why this page names the encoding alongside the algorithm instead of only telling you that something matched.
Frequently asked questions
- My signature does not match. What do I check first?
- The key encoding, then the message bytes. A secret that looks like hex is often meant as hex rather than as text, and the two produce completely different signatures with no error either way. After that, make sure you are signing the raw body exactly as received rather than a re-serialised copy of it.
- Why is SHA-1 offered here when the hash tool says it is broken?
- Because HMAC does not rely on collision resistance, which is the property SHA-1 lost. Its security comes from the key. HMAC-SHA-1 is still sound and still in use by OAuth 1.0a and older AWS signing, though SHA-256 is the right default for anything new.
- Is a longer digest more secure?
- Not meaningfully. The strength of an HMAC is bounded by the secret, not by the digest length, so SHA-512 is not four times better than SHA-256. Pick the one the other side expects.
- What is the difference between HMAC and a digital signature?
- HMAC uses one secret that both sides know, so it proves the sender knew the secret but not which side sent it. A digital signature uses a private key only the sender holds, so a third party can verify it and the sender cannot deny it. If you need that last property, HMAC is the wrong tool.
- Should I compare signatures with ===?
- Not in a server. An ordinary comparison returns as soon as bytes differ, and that timing leaks how much of a guessed signature was right. Use your platform’s constant-time comparison on the raw bytes. On this page it does not matter, because you already hold the key.
- Is my key sent anywhere?
- No. Everything is computed in your browser with the Web Crypto API; the message and the key never leave your device.
- How do I verify a Stripe webhook signature?
- Join three things into one string — the timestamp from the header’s t element, a full stop, and the raw request body — and sign that with the endpoint’s signing secret read as text, then compare the result with the header’s v1 element. The section above walks it through field by field. What catches most people is the first step: the body on its own is not what Stripe signed.
- How do I verify a GitHub webhook signature?
- Sign the raw request body with the webhook's secret read as text, using SHA-256, and compare the hex digest with the X-Hub-Signature-256 header. The sha256= label can stay on when you paste it here. GitHub publishes a worked example — the body Hello, World! under the secret It's a Secret to Everybody — which is the quickest way to check your reading of the recipe before trying it on a real delivery.
- How do I verify a Shopify webhook signature?
- Sign the raw request body with the client secret of the app the webhook belongs to, read as text, using SHA-256, and compare the Base64 digest with the X-Shopify-Hmac-SHA256 header. The whole header value pastes as it is — the trailing equals sign is Base64 padding. When it does not match, the usual cause is middleware that parsed the body before your handler ever saw it.
- How do I verify a webhook signature from any other provider?
- Four questions settle it, and the sender’s documentation has all four: which header carries the signature, what string is actually signed, how the secret is meant to be read, and whether the digest is hex or Base64. Fill those into the fields above. If it still does not match, the answer is almost always the second one — a great many senders sign the body with something else joined to it rather than the body alone.
Related tools
- TOTP / 2FA code debugger
Generate and debug 2FA codes, with the full derivation.
- JWT decoder / verifier
Decode and verify JSON Web Tokens — signature and claims.
- Hash generator
MD5, SHA-1, SHA-256, SHA-384 and SHA-512 at once.
- Certificate & PEM decoder
Read an X.509 certificate or CSR without OpenSSL.