HMAC: The Complete Guide to Message Authentication Codes

Understand HMAC (Hash-based Message Authentication Code) — how it works internally, why it is more than hash(key + message), how to implement it securely, and when to use HMAC-SHA256 vs HMAC-SHA512.

· 17 min read · Message Authentication

Every time Stripe sends a webhook to your server, every time GitHub triggers a CI pipeline, and every time a browser presents a session cookie — HMAC is likely working behind the scenes to verify that the message is authentic and hasn’t been tampered with. HMAC (Hash-based Message Authentication Code) is the industry-standard mechanism for proving both data integrity and authenticity using a shared secret key.

Unlike digital signatures (RSA, ECDSA), which use a private/public key pair, HMAC is symmetric: both the sender and the receiver hold the same secret key. This makes HMAC fast, simple to implement, and ideal for scenarios where both parties trust each other — API authentication, webhook verification, session tokens, and message integrity checks.

This guide explains how HMAC works internally (the double-hash construction that makes it secure against length extension attacks), compares HMAC with digital signatures and other MAC algorithms, shows you how to implement it correctly in Python, Node.js, Go, and the browser, and covers the security pitfalls that break real-world deployments.

Want to generate and verify HMAC signatures right now? Try the HMAC Generator tool — it supports SHA-256, SHA-384, and SHA-512, all computed in your browser.

What Is HMAC?

HMAC is a specific construction for building a Message Authentication Code (MAC) from a cryptographic hash function and a shared secret key. A MAC is a short piece of data that authenticates a message — proving it came from the claimed sender and hasn’t been altered.

The formal definition, from RFC 2104 (1997):

HMAC = H(K ⊕ opad || H(K ⊕ ipad || message))

Where:

  • H is a cryptographic hash function (e.g., SHA-256)
  • K is the secret key (padded to the hash block size if shorter)
  • opad is the outer padding constant (0x5c repeated)
  • ipad is the inner padding constant (0x36 repeated)
  • || denotes concatenation

Don’t worry if the formula looks intimidating — we’ll break it down step by step.

Why Not Just hash(key + message)?

A natural first attempt at building a MAC is to concatenate the key and message, then hash: H(key || message). This seems secure, but it’s vulnerable to a length extension attack.

Many hash functions (MD5, SHA-1, SHA-256) use the Merkle-Damgård construction, which processes the message in fixed-size blocks. Given H(key || message) and the length of the message, an attacker can append additional data and compute a valid hash for H(key || message || padding || extra_data) — without knowing the key. This means they can forge a valid MAC for a modified message.

HMAC’s double-hash construction completely prevents this attack. The outer hash “locks” the output so that no further extension is possible, even if the underlying hash function is vulnerable.

How HMAC Works: Step by Step

Let’s walk through the HMAC construction with SHA-256 (block size = 64 bytes):

Step 1: Key Preparation

If the secret key K is longer than the hash block size (64 bytes for SHA-256), it is first hashed: K = H(K). If K is shorter than the block size, it is padded with zero bytes to exactly 64 bytes.

Step 2: Inner Hash

  1. XOR the prepared key with ipad (0x36 repeated 64 times)
  2. Append the message to this XORed key
  3. Hash the result: inner_hash = H(K ⊕ ipad || message)

Step 3: Outer Hash

  1. XOR the prepared key with opad (0x5c repeated 64 times)
  2. Append the inner hash to this XORed key
  3. Hash the result: HMAC = H(K ⊕ opad || inner_hash)

The final output is the HMAC value — a fixed-length digest (32 bytes for HMAC-SHA256, 64 bytes for HMAC-SHA512).

Key insight: You never need to implement this yourself. Every standard crypto library provides an HMAC function. But understanding the construction explains why HMAC is secure even when the underlying hash has weaknesses — the outer hash acts as a firewall.

HMAC Security Properties

HMAC provides three critical security guarantees:

1. Data Integrity

Any change to the message — even a single bit — produces a completely different HMAC value. The receiver will detect the tampering immediately.

2. Message Authentication

Only someone who holds the shared secret key can produce a valid HMAC for a given message. If the HMAC verifies, the receiver knows the message came from someone with the key.

3. Resistance to Length Extension

Unlike naive H(key || message) constructions, HMAC’s double-hash structure prevents attackers from extending the message and computing a valid MAC without knowing the key.

What HMAC Does NOT Provide

  • Non-repudiation — Since both sender and receiver hold the same key, either party could have generated the MAC. You can’t prove who sent it. Digital signatures (ECDSA, Ed25519) provide non-repudiation; HMAC does not.
  • Confidentiality — HMAC doesn’t encrypt the message. The plaintext is sent alongside the MAC. For authenticated encryption, use AES-GCM or ChaCha20-Poly1305.

HMAC vs Digital Signatures vs Other MACs

Property HMAC (SHA-256) Digital Signature (ECDSA/Ed25519) CMAC (AES) GMAC (AES-GCM)
Key type Symmetric (shared secret) Asymmetric (private/public) Symmetric (block cipher) Symmetric (block cipher)
Speed Very fast Slow (signing); fast (verification for RSA) Fast Fast (hardware-accelerated)
Output size 32 bytes (SHA-256) 64–72 bytes (ECDSA P-256) 16 bytes (AES block) 16 bytes
Non-repudiation ❌ No ✅ Yes ❌ No ❌ No
Requires hash function ✅ Yes ❌ No (uses elliptic curves) ❌ No (uses block cipher) ❌ No (uses block cipher)
Common use Webhooks, API auth, JWT HS256 TLS certificates, code signing, SSH IPsec, disk encryption Network protocols (TLS AEAD)
Quantum resistance Partially (Grover’s halves security) ❌ Broken by Shor’s Partially (Grover’s) Partially (Grover’s)

When to use HMAC:

  • You control both ends of the connection (internal APIs, service-to-service auth)
  • You need fast, simple message authentication
  • Non-repudiation is not required
  • Webhook verification (Stripe, GitHub, Slack)
  • JWT signing with HS256

When to use a digital signature instead:

  • You need non-repudiation (the signer can’t deny having signed)
  • The verifier doesn’t have the key (public verification, like TLS certificates)
  • You’re doing code signing, document signing, or blockchain transactions

HMAC Hash Function Choices

HMAC can work with any cryptographic hash function. Here’s how the common choices compare:

Algorithm Output size Security level Performance Recommendation
HMAC-SHA256 32 bytes 128 bits Fast (software); very fast (hardware SHA-NI) ✅ Recommended default for almost all applications
HMAC-SHA384 48 bytes 192 bits Fast Good for TLS cipher suites (SHA-384 is common in TLS 1.3)
HMAC-SHA512 64 bytes 256 bits Slower in software; fast with SHA-512 hardware Use for high-security applications or when processing large payloads
HMAC-SHA1 20 bytes 80 bits Fast ⚠️ Legacy — HMAC construction protects against SHA-1 collision attacks, but avoid for new designs (compliance flags)
HMAC-MD5 16 bytes 64 bits Very fast ❌ Do not use — MD5 is broken; use only for backward compatibility

Important nuance: HMAC’s construction is provably secure even if the underlying hash function has collision vulnerabilities — as long as the hash function’s compression function behaves like a pseudorandom function (PRF). This is why HMAC-SHA1 is theoretically still secure for HMAC use, even though SHA-1 itself is broken for digital signatures. However, compliance frameworks (PCI-DSS, NIST) often flag SHA-1 regardless of context, so use SHA-256 for new designs.

Code Examples

Browser — Web Crypto API (HMAC-SHA256)

// Import the secret key
const encoder = new TextEncoder();
const keyData = encoder.encode('my-secret-key');
const key = await crypto.subtle.importKey(
  'raw', keyData,
  { name: 'HMAC', hash: 'SHA-256' },
  false,
  ['sign', 'verify']
);

// Sign a message
const message = encoder.encode('Hello, world!');
const signature = await crypto.subtle.sign('HMAC', key, message);
console.log('HMAC:', Array.from(new Uint8Array(signature))
  .map(b => b.toString(16).padStart(2, '0')).join(''));

// Verify the signature
const isValid = await crypto.subtle.verify('HMAC', key, signature, message);
console.log('Valid:', isValid);

Python — hmac + hashlib

import hmac
import hashlib

secret = b'my-secret-key'
message = b'Hello, world!'

# Generate HMAC-SHA256
signature = hmac.new(secret, message, hashlib.sha256).hexdigest()
print(f'HMAC: {signature}')

# Verify (constant-time comparison!)
expected = hmac.new(secret, message, hashlib.sha256).hexdigest()
is_valid = hmac.compare_digest(signature, expected)
print(f'Valid: {is_valid}')

Critical: Always use hmac.compare_digest() for verification. Standard string comparison (==) is vulnerable to timing attacks — it returns early on the first mismatched character, allowing an attacker to guess the HMAC byte by byte by measuring response times.

Node.js — crypto module

const crypto = require('crypto');

const secret = 'my-secret-key';
const message = 'Hello, world!';

// Generate HMAC-SHA256
const signature = crypto
  .createHmac('sha256', secret)
  .update(message)
  .digest('hex');
console.log(`HMAC: ${signature}`);

// Verify (constant-time comparison!)
const expected = crypto
  .createHmac('sha256', secret)
  .update(message)
  .digest('hex');

const sigBuffer = Buffer.from(signature, 'hex');
const expBuffer = Buffer.from(expected, 'hex');
const isValid = crypto.timingSafeEqual(sigBuffer, expBuffer);
console.log(`Valid: ${isValid}`);

Go — crypto/hmac

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
)

func main() {
    secret := []byte("my-secret-key")
    message := []byte("Hello, world!")

    // Generate HMAC-SHA256
    mac := hmac.New(sha256.New, secret)
    mac.Write(message)
    signature := hex.EncodeToString(mac.Sum(nil))
    fmt.Printf("HMAC: %s\n", signature)

    // Verify (constant-time comparison)
    expectedMAC := hmac.New(sha256.New, secret)
    expectedMAC.Write(message)
    expected := expectedMAC.Sum(nil)

    received, _ := hex.DecodeString(signature)
    isValid := hmac.Equal(received, expected)
    fmt.Printf("Valid: %v\n", isValid)
}

OpenSSL CLI

# Generate HMAC-SHA256
echo -n "Hello, world!" | openssl dgst -sha256 -hmac "my-secret-key"

# Generate HMAC-SHA512
echo -n "Hello, world!" | openssl dgst -sha512 -hmac "my-secret-key"

# Generate HMAC-SHA256 from a file
openssl dgst -sha256 -hmac "my-secret-key" document.pdf

Real-World Applications

Webhook Verification

Services like Stripe, GitHub, Slack, and Shopify send webhooks to your server when events occur. Each request includes an HMAC signature in a header (e.g., X-Hub-Signature-256 for GitHub, Stripe-Signature for Stripe). Your server:

  1. Receives the raw request body
  2. Computes HMAC-SHA256 using the shared secret and the raw body
  3. Compares the computed HMAC with the header value using constant-time comparison
  4. If they match, the webhook is authentic — process it

Common pitfall: You must HMAC the raw request body bytes, not a re-serialized version of the parsed JSON. Even a single whitespace difference will produce a different HMAC.

API Request Signing

AWS (Signature Version 4), Google Cloud, and Azure all use HMAC to authenticateates API requests. The client signs a “canonical request” string that includes the HTTP method, path, query parameters, headers, and body — binding the signature to the specific request context.

JWT Signing (HS256)

JSON Web Tokens (JWT) can be signed with HMAC-SHA256 (the “HS256” algorithm). Both the issuer and verifier share the same secret key. The JWT’s signature is HMAC-SHA256(base64url(header) + "." + base64url(payload), secret). Our JWT Signature Verifier can decode and verify HS256-signed tokens.

Session Cookies and Tokens

Web frameworks sign session cookies with HMAC to prevent client-side tampering. When a user presents a cookie, the server recomputes the HMAC and checks it matches — if the user modified any cookie data, the HMAC won’t verify.

Message Queue Integrity

Systems like Kafka and RabbitMQ can use HMAC to verify that messages haven’t been altered in transit between producers and consumers, especially in multi-tenant environments.

Security Best Practices

1. Use Constant-Time Comparison for Verification

This is the most common implementation vulnerability. Standard string comparison (==, ===, strcmp) returns as soon as it finds a mismatch. An attacker can measure the server’s response time to determine how many leading bytes of the HMAC are correct, then guess the full HMAC one byte at a time.

Fix: Always use your language’s built-in constant-time comparison:

  • Python: hmac.compare_digest(a, b)
  • Node.js: crypto.timingSafeEqual(bufA, bufB)
  • Go: hmac.Equal(a, b)
  • Java: MessageDigest.isEqual(a, b)

2. Prevent Replay Attacks

A valid HMAC signature is valid forever — as long as the message and key don’t change. An attacker who captures a legitimate request can replay it later.

Fix: Include a timestamp in the signed payload and reject requests older than a reasonable window (e.g., 5 minutes):

HMAC(secret, timestamp + "|" + request_body)

For higher-security scenarios (financial transactions), use nonce-based replay prevention: the server issues a one-time token that the client includes in the signature, and the server invalidates it after first use.

3. Bind the Signature to Context

Signing only the message body isn’t enough. An attacker could intercept a valid request to /api/user/preferences and replay it to /api/admin/create if the bodies happen to be compatible.

Fix: Sign a canonical string that includes the HTTP method, path, and host — not just the body:

HMAC(secret, method + path + host + timestamp + body)

This is the approach used by AWS Signature Version 4.

4. Key Management

Your HMAC security is only as strong as your secret key:

  • Generate strong keys: Use a cryptographically secure random number generator. Keys should be at least 32 bytes (256 bits) for HMAC-SHA256. Example: openssl rand -hex 32
  • Never hardcode secrets: Don’t embed keys in source code. Use environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager)
  • Rotate keys regularly: Implement dual-key rotation — support both the old and new key simultaneously during a transition window, then retire the old key
  • Scan for leaks: Use tools like GitGuardian or truffleHog to detect accidentally committed secrets in your repository
  • Scope keys per client: If you have multiple API consumers, give each one a unique key. This limits the blast radius if one key is compromised and enables per-client key rotation

5. Choose the Right Algorithm

  • HMAC-SHA256 is the recommended default for virtually all applications
  • HMAC-SHA512 is appropriate for high-security contexts or large payloads
  • Avoid HMAC-SHA1 for new designs (compliance concerns, even if theoretically secure in HMAC construction)
  • Never use HMAC-MD5 except for legacy backward compatibility

Troubleshooting Checklist

If your HMAC signatures don’t match between sender and receiver, check these common issues:

Problem Likely cause Fix
Signature mismatch Encoding mismatch (UTF-8 vs ASCII vs Latin-1) Ensure both sides encode the key and message as the same byte sequence (UTF-8)
Different HMAC values Different hash algorithms Confirm both sides use the same algorithm (e.g., SHA-256, not SHA-256 on one side and SHA-512 on the other)
Unexpected output format Hex vs Base64 vs raw bytes Standardize the output encoding — .hexdigest() vs .digest() vs base64
Webhook verification fails Hashing parsed JSON instead of raw body Always HMAC the raw request body bytes; do not re-serialize
Key not working Hex string vs raw bytes confusion If the key is provided as a hex string, decode it to raw bytes before using it
Timing attack vulnerability Using == for comparison Use constant-time comparison (compare_digest, timingSafeEqual)

Frequently Asked Questions

What is HMAC in simple terms?

Think of HMAC as a digital wax seal on an envelope. The message is the letter inside, the secret key is a unique signet ring, and the HMAC is the wax seal. Only someone with the same signet ring (the secret key) can create a matching seal. If the letter is opened or altered, the seal breaks, and the recipient knows something is wrong.

Is HMAC the same as hashing?

No. A plain hash (SHA-256 of a file) only proves integrity — that the data hasn’t changed. HMAC proves both integrity and authenticity — that the data came from someone who holds the shared secret key. Without the key, an attacker can’t forge a valid HMAC, even though they can compute the hash.

Is HMAC the same as JWT?

No. JWT (JSON Web Token) is a token format that carries a payload of claims (user ID, roles, expiration). HMAC is a cryptographic primitive that can be used to sign a JWT. When you see “HS256” as a JWT algorithm, it means the JWT is signed with HMAC-SHA256. JWTs can also use asymmetric algorithms (RS256, ES256) for digital signatures. Our JWT Signature Verifier supports both.

Can HMAC be decrypted?

No. HMAC is a one-way function — it produces a fixed-length digest from a key and message, but you can’t reverse it to recover the key or the message. You can only verify an HMAC by recomputing it with the same key and message and comparing the results.

Is HMAC-SHA256 quantum-safe?

Partially. Grover’s algorithm can speed up brute-force attacks against symmetric cryptography, effectively halving the security level. HMAC-SHA256 provides 128-bit security classically, which drops to 64-bit against a quantum computer — potentially breakable. HMAC-SHA512 drops from 256-bit to 128-bit, which remains secure even against quantum attacks. For long-term quantum resistance, consider pairing HMAC with post-quantum key exchange (ML-KEM). See our Post-Quantum Cryptography guide for details.

What’s the difference between HMAC and CMAC?

HMAC uses a hash function (SHA-256, SHA-512) as its building block; CMAC uses a block cipher (AES-128, AES-256). Both produce message authentication codes, but they rely on different cryptographic primitives. HMAC is more widely used for web APIs and webhooks; CMAC is common in IPsec and some hardware-constrained environments where AES hardware acceleration is available but hash acceleration is not.

How long should an HMAC key be?

RFC 2104 recommends that the key length be at least the output size of the hash function. For HMAC-SHA256, use at least a 32-byte (256-bit) key. For HMAC-SHA512, use at least 64-byte (512-bit) keys. Keys longer than the hash block size (64 bytes for SHA-256) are first hashed down, so there’s no benefit to using keys longer than the block size.

Why do I need constant-time comparison?

Standard string comparison (==) stops at the first mismatched character and returns immediately. An attacker who can measure the server’s response time with microsecond precision can determine how many leading bytes of their guessed HMAC are correct, then brute-force the rest one byte at a time. This is called a timing attack. Constant-time comparison functions always take the same amount of time regardless of where the mismatch occurs, defeating this attack.

  • HMAC Generator — Generate and verify HMAC signatures with SHA-256, SHA-384, and SHA-512. Supports hex and Base64 output, all computed in your browser.

  • JWT Signature Verifier — Decode and verify JSON Web Tokens signed with HS256 (HMAC-SHA256), RS256, ES256, and other algorithms.

  • Digital Signature Verify — Create and verify RSA or ECDSA digital signatures. Compare with HMAC to understand symmetric vs asymmetric authentication.

  • SHA Hash Generator — Compute SHA-256, SHA-384, and SHA-512 hashes. Useful for understanding the hash functions that HMAC is built on.

Further Reading

Related Tools