HMAC (Hash-based Message Authentication Code) is a construction that turns any cryptographic hash function into a message authenticator. It takes two inputs — a secret key and a message — and produces a fixed-size tag that proves both who created the message and that it hasn't been tampered with.
Unlike a plain hash (which anyone can compute), an HMAC can only be verified by someone who holds the secret key. This makes it the standard tool for API authentication, signed tokens, and webhook verification.
| Plain Hash | HMAC | Digital Sig | |
|---|---|---|---|
| Needs secret key | No | Yes (shared) | Yes (private) |
| Verifiable by anyone | Yes | Only key holders | Yes (public key) |
| Tamper detection | No | Yes | Yes |
| Non-repudiation | No | No | Yes |
| Length-extension safe | No | Yes | Yes |
| Speed | Fast | Fast | Slow |
import { createHmac } from 'crypto';
// Sign a message
const key = 'my-secret-key';
const message = 'Hello, HMAC!';
const hmac = createHmac('sha256', key)
.update(message)
.digest('hex');
console.log(hmac);
// → 64-char hex string
// Verify (timing-safe comparison)
import { timingSafeEqual } from 'crypto';
function verify(key, message, expected) {
const actual = createHmac('sha256', key)
.update(message).digest();
return timingSafeEqual(actual, Buffer.from(expected, 'hex'));
}
import hmac, hashlib
key = b'my-secret-key'
message = b'Hello, HMAC!'
# Sign a message
tag = hmac.new(key, message, hashlib.sha256).hexdigest()
print(tag)
# → 64-char hex string
# Verify (timing-safe comparison)
def verify(key, message, expected_hex):
actual = hmac.new(key, message, hashlib.sha256).digest()
expected = bytes.fromhex(expected_hex)
# hmac.compare_digest prevents timing attacks
return hmac.compare_digest(actual, expected)
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
// Sign a message
func Sign(key, message []byte) string {
mac := hmac.New(sha256.New, key)
mac.Write(message)
return hex.EncodeToString(mac.Sum(nil))
}
// Verify (timing-safe)
func Verify(key, message []byte, expected string) bool {
tag, _ := hex.DecodeString(expected)
mac := hmac.New(sha256.New, key)
mac.Write(message)
actual := mac.Sum(nil)
// hmac.Equal uses constant-time comparison
return hmac.Equal(actual, tag)
}
Never compare HMAC tags with ==. A naive string comparison short-circuits on the first mismatched byte, leaking timing information that can be used to forge tags byte-by-byte.
The security of HMAC is entirely in the key. Use a cryptographically random key of at least 32 bytes. Never reuse the same key for different purposes (e.g. signing tokens vs verifying webhooks).
HMAC proves integrity and authenticity but does not hide the message content. The message is visible to anyone who receives it — use HMAC alongside encryption (e.g. AES-GCM) if confidentiality is needed.
HMAC alone doesn't prevent replay attacks. In production (webhooks, APIs) always include a timestamp in the signed payload and reject messages older than a few minutes.