What does HMAC verification do?
HMAC verification recomputes the HMAC of a message using the same secret and algorithm, then compares it to a provided tag. If they match, the message has not been modified and was signed by someone holding the secret. This is how webhook receivers, JWT validators, and signed-URL gateways confirm a request is genuine before acting on it.
Is the comparison really constant-time?
No — and we are upfront about it. JavaScript engines intern strings, branch prediction adapts, JIT optimisations vary by call shape, and garbage collection introduces non-uniform pauses. Our compare runs a fixed-iteration XOR loop over the full string length, which is the closest you can get in browser JS, but it is not a substitute for a true constant-time compare. For production verification, do it server-side using crypto.timingSafeEqual (Node), hmac.compare_digest (Python), subtle.ConstantTimeCompare (Go), or the equivalent in your runtime.
Why does my GitHub webhook fail to verify?
Three usual culprits. (1) GitHub signs the raw request body bytes — if your framework parses JSON before exposing the body, the bytes you HMAC are different from the bytes GitHub signed. (2) The X-Hub-Signature-256 header is prefixed with "sha256=" which you must strip before comparison. (3) Encoding mismatches — GitHub uses lowercase hex; do not mix in Base64 or uppercase. Paste the raw body and the hex tag (without the prefix) into this tool to confirm your secret matches.
Hex or Base64 — which should I paste?
Match the format of the system that produced the tag. GitHub, Stripe, and most webhook providers use lowercase hex. JWT HS256 uses Base64URL (which we accept as Base64 — replace - with + and _ with /, and pad with = if needed). AWS SigV4 uses lowercase hex. The dropdown must match the format you paste, or the tag will never validate.
What is the security risk of timing attacks?
A naive string equality (a === b) returns false at the first differing character, so an attacker who can measure response time can probe the tag one byte at a time — this is how the 2010 Boston Symes timing attack on Google Keyczar worked. Constant-time compare always touches every byte regardless of where the mismatch is, so timing leaks no information about which bytes match. The attack is hardest over the public internet (jitter masks microsecond differences) but realistic on local networks and microservices.
Are my secrets uploaded to your server?
No. Verification runs entirely in your browser — the secret, message, and provided tag never touch any server. Open DevTools → Network and click Verify; you will see zero requests fire. The Web Crypto SubtleCrypto API runs in your browser native code, so the computation is fast and private.
Can I verify a JWT here?
Partially. Split the JWT on dots into header.payload.signature. Set message to "header.payload" (the two Base64URL parts joined by a dot, kept as Base64URL — do not decode). Set the provided tag to the third part (the signature). Pick HMAC-SHA256 + Base64. Convert any "-" to "+" and "_" to "/" and pad with "=" to make standard Base64. For full JWT validation including expiry and issuer claims, use a dedicated library like jose.
What if Valid shows but my server rejects it?
Most often a body-encoding mismatch. Webhook providers sign raw bytes (often UTF-8 JSON with no extra whitespace), but your framework may pretty-print, sort keys, or re-encode before re-signing. Always sign and verify the raw request body verbatim. Other causes: clock-skew on time-bound signatures (Stripe attaches a timestamp), header parsing dropping leading "sha256=" prefix, or a stale secret after rotation.