HMAC (Hash-based Message Authentication Code) is a mechanism for verifying both the integrity and authenticity of a message. It is used extensively in API security — Stripe, GitHub, Twilio, Slack, and AWS all use HMAC to authenticate webhook payloads and API requests.
How HMAC Works
HMAC combines a secret key with a cryptographic hash function (typically SHA-256 or SHA-512):
HMAC(key, message) = Hash((key ⊕ opad) || Hash((key ⊕ ipad) || message))
In practice:
HMAC = HMAC_SHA256(secret_key, message)If the HMACs match, the message is authentic and untampered.
HMAC vs Signatures vs API Keys
| | API Key | HMAC | Asymmetric Sig (RSA/Ed25519) |
|---|---|---|---|
| Secret shared? | Yes (the key itself) | Yes (signing secret) | No (private key stays local) |
| Verifies sender | ✅ | ✅ | ✅ |
| Verifies integrity | ❌ | ✅ | ✅ |
| Key exposure risk | High (key = auth) | Medium | Low |
| Performance | Fast | Fast | Slower |
Common HMAC Use Cases
Webhook Verification (Stripe Example)
Stripe signs every webhook payload with your webhook secret:
import hmac, hashlib, time
def verify_stripe_webhook(payload: bytes, sig_header: str, secret: str) -> bool:
# sig_header looks like: "t=1614556800,v1=abc123..."
parts = dict(item.split("=", 1) for item in sig_header.split(","))
timestamp = parts["t"]
signature = parts["v1"]
# Prevent replay attacks: reject if > 5 minutes old
if abs(time.time() - int(timestamp)) > 300:
return False
signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
GitHub Webhook Verification
const crypto = require('crypto');
function verifyGitHubWebhook(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
AWS Request Signing (SigV4)
AWS uses HMAC-SHA256 to sign API requests:
StringToSign = "AWS4-HMAC-SHA256" + "\n" + timestamp + "\n" + credentialScope + "\n" + hashedCanonicalRequest
Signature = HMAC-SHA256(SigningKey, StringToSign)
Implementing HMAC in Different Languages
Node.js:
const crypto = require('crypto');
const signature = crypto
.createHmac('sha256', secretKey)
.update(messageBody)
.digest('hex');
Python:
import hmac, hashlib
signature = hmac.new(
secret_key.encode(), message.encode(), hashlib.sha256
).hexdigest()
Go:
import "crypto/hmac"; "crypto/sha256"; "encoding/hex"
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write([]byte(message))
signature := hex.EncodeToString(mac.Sum(nil))
PHP:
$signature = hash_hmac('sha256', $message, $secretKey);
Security Best Practices
1. Always Use Timing-Safe Comparison
Never use === or == to compare HMAC values — this is vulnerable to timing attacks:
// ❌ WRONG — vulnerable to timing attacks
if (computedHmac === receivedHmac) { ... }
// ✅ CORRECT
if (crypto.timingSafeEqual(Buffer.from(computedHmac), Buffer.from(receivedHmac))) { ... }
2. Include a Timestamp and Reject Replays
Add a timestamp to the signed payload and reject requests older than 5 minutes. Stripe, GitHub, and Slack all do this.
3. Use HMAC-SHA256 or Stronger
MD5 and SHA-1 based HMACs are deprecated. Use SHA-256 or SHA-512.
4. Rotate Secrets Periodically
Treat HMAC secrets like passwords. Rotate them regularly and immediately if exposed.
Generate HMAC Signatures Online
Use our free HMAC Generator to compute HMAC-SHA256 and HMAC-SHA512 signatures instantly in your browser, with your message and secret key — entirely client-side, nothing is sent to any server.
