HMAC (Hash-based Message Authentication Code) combines a hash function with a secret key to produce a signature that proves two things at once: the message hasn't been altered, and it was signed by someone who holds the key. It's defined in RFC 2104 and works with any underlying hash function, SHA-1, SHA-256, SHA-384, or SHA-512 are the common choices today.

A plain hash answers "did this data change?" — anyone can compute SHA-256(message) and check it matches. HMAC answers a stronger question: "did someone who knows the secret produce this?" Without the key, an attacker can't forge a valid signature even if they know exactly which hash function is in use.

The construction: two hash passes, not one

HMAC doesn't just hash the key and message together. It nests two hash operations, mixing the key into each with a different padding constant:

HMAC(key, message) = H( (key' XOR opad) || H( (key' XOR ipad) || message ) )

ipad = 0x36 repeated to the hash's block size
opad = 0x5c repeated to the hash's block size
key' = key, hashed down or zero-padded to exactly one block

The inner hash binds the key to the message; the outer hash binds the key to that result. This two-pass structure is what makes HMAC resistant to length-extension attacks, a real weakness in the naive approach of hashing a secret and message concatenated together (see the FAQ below).

Plain hash vs HMAC

Plain hashHMAC
InputsMessage onlyMessage + secret key
Anyone can compute itYesOnly with the key
ProvesData integrityIntegrity + authenticity
Typical useChecksums, deduplicationWebhook signatures, API request signing, JWT HS256/384/512

This site's Hash Generator computes the plain version; the HMAC Generator adds the secret-key input and shows how the same message produces a completely different signature once a key is involved.

Where HMAC shows up

Webhook verification: Stripe, GitHub, and most webhook providers sign the request body with HMAC-SHA256 and send the signature in a header (Stripe-Signature, X-Hub-Signature-256). The receiving server recomputes the HMAC with its copy of the shared secret and compares, no request is trusted unless the signature matches.

Important: Never compare signatures with ===, ==, or strcmp. Those bail out at the first mismatched byte, so the comparison takes measurably longer the more leading bytes match, an attacker who can time enough requests can recover a valid signature one byte at a time. Use a constant-time function instead: crypto.timingSafeEqual() in Node, hmac.compare_digest() in Python, hash_equals() in PHP, hmac.Equal() in Go. Node's version throws if the two inputs aren't the same byte length, so check that first, the digest's length is fixed by the algorithm and isn't a secret worth protecting.

AWS SigV4 chains HMAC-SHA256 through several derivation steps to sign API requests without ever putting the raw secret key on the wire. JWTs signed with the HS256, HS384, or HS512 algorithm are literally the token's header and payload run through HMAC with the API's shared secret, that's what makes a tampered JWT fail verification.

Frequently asked questions

No. HMAC produces a fixed-size signature, not ciphertext, and there is no way to recover the original message from it, the same one-way property a plain hash has.

What HMAC adds over a plain hash is a secret key: only someone who knows the key can produce or verify a valid signature. It proves authenticity and integrity, not confidentiality, the message itself still travels in the clear unless you encrypt it separately.