HMAC Generator and Verifier

Sign a message with a shared key, or paste a signature you received and find out whether it matches. Key and output encodings are yours to set, because a webhook signature failing at 3am is almost always an encoding mismatch rather than a broken key.

HMAC generator and verifier

Algorithm
Key material is read as UTF-8 bytes.
0 bytes
Drop a file here to sign its raw bytes instead of the text above
Output encoding
Set a key and a message, then press Generate HMAC.
HMAC-SHA256, 256 bit tag, 64 hex characters
Key as the algorithm sees it
  • Decoded key length0 bytes
  • Hash block size64 bytes
  • Key handlingPadded with zero bytes to the block size

Your HMAC is right on this page and wrong at the API, and the key encoding is usually why

A webhook arrives, your server computes a signature over the body, and the two strings do not match. The key is correct, the algorithm is correct, and the tag still differs on every character. Three details decide the answer before the hash function ever runs: what bytes your key really is, what bytes the message really is, and how the tag was written down.

What HMAC adds that a plain hash does not

Hash a message with SHA-256 and anyone holding the message reproduces the digest. That detects accidental corruption and nothing else. An attacker who rewrites the message rewrites the digest alongside it, and you have no way to tell.

HMAC folds a secret key into the hash, so only key holders produce a matching tag. The construction comes from RFC 2104 and looks like this:

HMAC(K, m) = H( (K' XOR opad) || H( (K' XOR ipad) || m ) )

The two passes are the point. A single pass of key plus message, written as H(K || m), falls to length extension on SHA-1, SHA-256, and every other Merkle-Damgard hash. An attacker appends data to your message and computes a valid tag for the longer version without knowing the key. The nested structure closes that hole, which is why hand rolled signing schemes fail security review and HMAC does not.

Three encodings, and the mismatch that eats an afternoon

The key box above accepts text, hex, and base64 because APIs disagree about which one they hand you. The bytes differ wildly between readings of the same string.

Key as writtenRead asActual bytesLength
4a6f7368Text34 61 36 66 37 33 36 388 bytes
4a6f7368Hex4a 6f 73 684 bytes
Sm9zaA==Text53 6d 39 7a 61 41 3d 3d8 bytes
Sm9zaA==Base644a 6f 73 684 bytes

Two of those rows produce one tag. The other two produce tags with nothing in common. Read the API documentation for the wording it uses. Stripe and GitHub hand you a secret meant to be taken as literal text, AWS Signature Version 4 builds a chain of raw byte keys, and most Java examples calling Hex.decodeHex want the hex reading.

The panel on the right reports the decoded key length after every edit, so a 64 character hex secret showing 32 bytes tells you the hex mode took, and 64 bytes tells you it did not.

The message has to be the exact bytes, not the pretty version

Signature checks fail on message handling more often than on key handling, and the failures share a shape: the sender signed raw bytes, the receiver signed something a framework reshaped on the way in.

When a text payload refuses to match, drop the actual file into the box above. The file path reads raw bytes with no text interpretation at all, which settles the newline and encoding questions in one step.

Check yourself against the published test vectors

Before blaming the other side, prove your implementation on inputs with known answers. RFC 4231 case 1 uses a key of twenty 0x0b bytes and the message Hi There, and RFC 2202 covers the older digests. Press Load RFC test vector to fill the console with the matching case for whichever algorithm is selected.

AlgorithmKeyExpected tag in hex
HMAC-SHA2560x0b repeated 20 timesb0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7
HMAC-SHA5120x0b repeated 20 times87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cdedaa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854
HMAC-SHA10x0b repeated 20 timesb617318655057264e28bc0b6fb378c8ef146be00
HMAC-MD50x0b repeated 16 times9294727a3638bb1c13f48ef8158bfc9d

Match those and your reading of key and message is sound. Keep failing against a partner API afterwards and the disagreement sits in what gets signed, not in the hashing.

Compare tags in constant time, never with a string equality operator

Verify mode above reports a match or a mismatch and stops there, on purpose. It never names the character where the two diverge, because a comparison quitting at the first wrong byte gives an attacker a timing signal. Feed a server a million guesses, measure microseconds, and the tag falls out one byte at a time. Real servers use a comparison reading every byte regardless:

// PHP $expected = hash_hmac('sha256', $request->getContent(), $secret);if (!hash_equals($expected, $signature)) {abort(403);}// Node const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();const given = Buffer.from(signature, 'hex');if (expected.length !== given.length || !crypto.timingSafeEqual(expected, given)) {return res.status(403).end();}# Python expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()if not hmac.compare_digest(expected, signature):abort(403)

Note the length check ahead of timingSafeEqual in the Node version. That function throws on mismatched lengths rather than returning false, and an unhandled throw inside a webhook handler becomes a 500 instead of a clean rejection.

Which digest to sign with

AlgorithmTag sizeBlock sizeWhere it stands
HMAC-SHA256256 bit64 bytesThe default for new work. Webhook signatures, API request signing, and JWT HS256 all land here.
HMAC-SHA512512 bit128 bytesFaster than SHA-256 on 64 bit CPUs for long messages. The wider tag adds little, since 256 bits already sits past reach.
HMAC-SHA384384 bit128 bytesSHA-512 truncated. Shows up in TLS cipher suites and in JWT HS384.
HMAC-SHA224224 bit64 bytesRare outside compliance profiles asking for one specific tag width.
HMAC-SHA1160 bit64 bytesThe collision attacks on SHA-1 do not break HMAC-SHA1, and it holds up in practice. Auditors flag it anyway, so treat it as a legacy option for AWS Signature Version 2 and older OAuth 1.0a partners.
HMAC-MD5128 bit64 bytesSame story with a thinner margin. Kept here for reading old systems, never for new ones.

The collision weaknesses in MD5 and SHA-1 attack a different property than HMAC leans on, which is why no practical forgery exists against either construction. Picking them today still means explaining that distinction to every reviewer who greps the codebase, so choose SHA-256 and skip the conversation.

Key length, and what happens at the edges

HMAC takes a key of any length, then normalizes it against the hash block size before use. Two edges are worth knowing about.

The facts panel reports which branch your key falls into after each edit, including the hash step when the key runs oversized.

Where this tool stops

All hashing happens in your tab through the CryptoJS file loaded with the page. Open the network panel and press Generate HMAC to confirm no request leaves during signing.

Questions from people whose signature check keeps returning 403

The mismatches behind most failed HMAC comparisons.

My HMAC matches on this page but the API rejects it. Where do I look first?

Check the key encoding, then the exact message bytes. Take the API secret and ask whether the documentation treats it as literal text or as a hex or base64 encoding of raw bytes. A 64 character hex secret read as text gives a 64 byte key instead of a 32 byte one, and every character of the tag changes. After that, compare byte counts on the message. A trailing newline, a CRLF line ending, or a JSON body that went through a parser and back are the three usual culprits.

What is the difference between HMAC-SHA256 and plain SHA-256?

SHA-256 takes one input and anyone reproduces the digest. HMAC-SHA256 takes a message and a secret key, so only key holders produce a matching tag. That makes HMAC an authentication check rather than an integrity check. A plain hash catches a corrupted download. An HMAC catches someone rewriting the message on purpose, since the rewrite needs the key to carry a valid tag.

Should the output be hex or base64?

Whatever the other side expects, since both describe the same bytes. Hex is the common choice for webhook headers such as the GitHub and Stripe signature fields, and it doubles the tag length in characters. Base64 appears in AWS Signature Version 2, in JWT signatures as base64url, and anywhere the tag rides inside a URL. Toggle the encoding above and the underlying tag does not change at all.

How long should my HMAC key be?

At least as long as the digest output, so 32 random bytes for SHA-256. Generate it from a cryptographic random source rather than typing a passphrase, because HMAC applies no key stretching and a memorable secret falls to an offline guessing attack against one captured message. Keys longer than the hash block size, 64 bytes for SHA-256, get hashed down first, so extra length past that point adds nothing.

Is HMAC-SHA1 still safe to use?

Mechanically yes. The published SHA-1 attacks find collisions, HMAC security rests on a different property, and no practical forgery against HMAC-SHA1 exists. AWS Signature Version 2 and OAuth 1.0a still rely on it. Scanners and auditors flag SHA-1 wherever it appears, though, and defending the distinction in every review costs more than switching. Use SHA-256 for new work and keep SHA-1 for talking to systems offering nothing else.

Why does verify mode refuse to tell me which character is wrong?

Because a comparison reporting where two tags diverge leaks the correct tag. An attacker submits guesses, watches how far the check got, and recovers the signature byte by byte. Servers avoid this with hash_equals in PHP, crypto.timingSafeEqual in Node, or hmac.compare_digest in Python, all of which read every byte before answering. Verify mode here follows the same rule so the habit carries over.

Can I recover the message or the key from an HMAC?

No. The tag is a one way function of both inputs and holds far fewer bits than most messages. What stays possible is guessing: if the key is a short passphrase or the message comes from a small set of options, an attacker tries candidates offline until a tag matches. That is an attack on your entropy rather than on HMAC, and a 32 byte random key ends it.

How do I sign a file rather than a text string?

Drop it onto the message box above. The file is read as raw bytes with no text decoding, which is what a server computing the signature does. The same result comes from the command line with openssl dgst -sha256 -hmac "your-key" file.bin, and the two should agree character for character. Use the command line version for files big enough to strain a browser tab.

Does my key or message get sent anywhere?

No. The HMAC runs in the page through the CryptoJS library, and nothing in the signing path makes a network request. Watch the network panel while pressing Generate HMAC to check for yourself. The library file downloads from a CDN when the page loads, so the first visit needs a connection and the tool works offline after that.

Which webhook providers use which format?

GitHub sends a header shaped as sha256= followed by the hex tag over the raw body. Stripe sends a timestamp and a signature, and the signed string is the timestamp, a dot, and the raw body joined together, so signing the body alone never matches. Shopify sends base64 over the raw body. Slack prefixes a version string and a timestamp before the body. Read the provider documentation for the exact string being signed, since the body on its own is the exception rather than the rule.