When you need to verify that a message hasn't been tampered with AND came from a trusted source, plain hashing isn't enough. That's where HMAC comes in.
The Problem with Plain Hashes
Suppose you send a message with its hash:
Message: {"amount": 100, "recipient": "Alice"}
Hash: sha256(message) = a1b2c3...
An attacker could:
- Intercept the message
- Modify it to
{"amount": 10000, "recipient": "Mallory"} - Compute the new hash
- Send both to the recipient
The hash verifies integrity but not authenticity—anyone can compute it.
What is HMAC?
HMAC (Hash-based Message Authentication Code) combines a cryptographic hash with a secret key. Only parties who know the key can create or verify the HMAC.
HMAC(key, message) = hash(key XOR opad || hash(key XOR ipad || message))
Or more simply:
HMAC = Hash(Secret Key + Message) (with proper padding)
The secret key makes all the difference:
Message: {"amount": 100, "recipient": "Alice"}
Secret Key: "my-secret-key"
HMAC: HMAC-SHA256(key, message) = f4e5d6...
Without the key, an attacker can't forge a valid HMAC.
How HMAC Works
HMAC uses two passes of the hash function:
- Inner hash: Combine key with inner padding (ipad), hash with message
- Outer hash: Combine key with outer padding (opad), hash with inner result
ipad = 0x36 repeated (block size times)
opad = 0x5c repeated (block size times)
inner_hash = H((key XOR ipad) || message)
HMAC = H((key XOR opad) || inner_hash)
This construction:
- Prevents length extension attacks
- Provides better security properties than simple
hash(key + message) - Is proven secure if the underlying hash function is secure
HMAC vs Hash Comparison
| Property | Plain Hash | HMAC |
|---|---|---|
| Input | Message only | Key + Message |
| Verifies integrity | ✓ | ✓ |
| Verifies authenticity | ✗ | ✓ |
| Anyone can compute | ✓ | ✗ (need key) |
| Prevents tampering | ✗ | ✓ |
Common HMAC Algorithms
HMAC can use any cryptographic hash:
| Algorithm | Output Size | Use Case |
|---|---|---|
| HMAC-MD5 | 128 bits | Legacy only |
| HMAC-SHA1 | 160 bits | Legacy, some webhooks |
| HMAC-SHA256 | 256 bits | Recommended |
| HMAC-SHA512 | 512 bits | Extra security |
Recommendation: Use HMAC-SHA256 for new projects.
HMAC in API Authentication
Many APIs use HMAC for request signing:
1. Client prepares request
POST /api/transfer
Body: {"amount": 100, "to": "Alice"}
Timestamp: 1705312800
2. Client creates signature
const message = timestamp + method + path + body;
const signature = hmacSha256(secretKey, message);
3. Client sends with headers
POST /api/transfer HTTP/1.1
X-Timestamp: 1705312800
X-Signature: a1b2c3d4e5f6...
Content-Type: application/json
{"amount": 100, "to": "Alice"}
4. Server verifies
const expectedSig = hmacSha256(secretKey, message);
if (signature !== expectedSig) {
return 401 Unauthorized;
}
This pattern is used by AWS, Stripe, GitHub webhooks, and many more.
Implementing HMAC Signatures
JavaScript (Node.js)
const crypto = require('crypto');
function createHmacSignature(secret, message) {
return crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
}
function verifyHmacSignature(secret, message, signature) {
const expected = createHmacSignature(secret, message);
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Python
import hmac
import hashlib
def create_hmac_signature(secret, message):
return hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
def verify_hmac_signature(secret, message, signature):
expected = create_hmac_signature(secret, message)
return hmac.compare_digest(signature, expected)
C# / .NET
using System.Security.Cryptography;
using System.Text;
string CreateHmacSignature(string secret, string message)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
return Convert.ToHexString(hash).ToLower();
}
Timing Attacks and Constant-Time Comparison
Never use regular string comparison for signatures!
// VULNERABLE - timing attack possible
if (signature === expectedSignature) { ... }
// SAFE - constant time comparison
if (crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { ... }
Why? Regular comparison returns early when it finds a mismatch. An attacker can measure response times to gradually discover the correct signature, byte by byte.
Constant-time comparison always takes the same time regardless of where differences occur.
HMAC Best Practices
1. Use strong, random keys
// Generate a secure key
const key = crypto.randomBytes(32).toString('hex');
At minimum, use 256 bits (32 bytes) for HMAC-SHA256.
2. Include timestamp in signed message
Prevents replay attacks:
const message = timestamp + request;
// Reject if timestamp is too old
if (Date.now() - timestamp > 300000) reject();
3. Sign all relevant request parts
Include method, path, headers, and body in the signature:
const message = `${method}:${path}:${headers}:${body}`;
4. Use constant-time comparison
Always use timingSafeEqual() or hmac.compare_digest().
5. Keep keys secret
- Store securely (environment variables, secret managers)
- Rotate periodically
- Use different keys for different purposes
6. Include version in signature
Allows algorithm upgrades:
X-Signature-Version: hmac-sha256-v1
Common HMAC Mistakes
1. Using plain hash instead of HMAC
// WRONG - no authentication
const hash = sha256(message);
// RIGHT
const hmac = hmacSha256(secret, message);
2. Weak or hardcoded keys
// WRONG
const key = "password123";
// RIGHT
const key = process.env.HMAC_SECRET; // Random 256-bit key
3. Not including all data
// WRONG - body can be modified
const sig = hmac(key, timestamp);
// RIGHT
const sig = hmac(key, timestamp + method + path + body);
4. String comparison for verification
# WRONG - timing attack
if signature == expected: ...
# RIGHT
if hmac.compare_digest(signature, expected): ...
Real-World Examples
GitHub Webhooks
X-Hub-Signature-256: sha256=a1b2c3...
Verify:
expected = 'sha256=' + hmac_sha256(secret, body)
Stripe Webhooks
Stripe-Signature: t=1705312800,v1=a1b2c3...
Verify:
payload = timestamp + '.' + body
expected = hmac_sha256(secret, payload)
AWS Signature V4
Uses HMAC-SHA256 in a multi-step process:
- Create canonical request
- Create string to sign
- Calculate signing key (derived via HMAC chain)
- Calculate signature
Summary
HMAC provides authentication that plain hashes can't:
- Hash: Verifies integrity (message wasn't corrupted)
- HMAC: Verifies integrity AND authenticity (message came from trusted source)
Key points:
- Use HMAC-SHA256 for new projects
- Keep keys secret and strong (256+ bits)
- Always use constant-time comparison
- Include timestamp to prevent replay attacks
- Sign all relevant request data
Need to generate HMAC signatures? Try our HMAC Generator!