Security2026-04-05

What Is HMAC? How It Works and When to Use It

HMAC (Hash-based Message Authentication Code) proves both data integrity and authenticity. Learn how HMAC works, how it differs from plain hashing, and how to implement it for webhooks and APIs.

hmacauthenticationsecuritycryptographywebhooksapi-security

What Is HMAC? How It Works and When to Use It

If you've ever verified a GitHub webhook, signed an AWS API request, or looked inside a JWT token, you've used HMAC — whether you knew it or not. HMAC is one of the most widely deployed authentication mechanisms in software systems, yet it's often confused with encryption or plain hashing.

This guide explains exactly what HMAC is, why it exists, how the algorithm works internally, and how to implement it correctly.

What Is HMAC?

HMAC stands for Hash-based Message Authentication Code. It's a mechanism for verifying two things simultaneously:

  1. Integrity — the message has not been altered in transit
  2. Authenticity — the message was created by someone who knows the secret key

HMAC is defined in RFC 2104 (1997) and uses a cryptographic hash function (typically SHA-256 or SHA-512) combined with a secret key to produce a fixed-size authentication tag.

The critical distinction: HMAC is not encryption. HMAC does not hide the content of a message — it only proves the message is authentic and unmodified. The message itself is still sent in plaintext (or over TLS). HMAC is a signature, not a cipher.

Why HMAC Exists: The Problem with Plain Hashes

To understand why HMAC was invented, consider what happens if you try to use a plain hash for authentication.

Suppose you want to prove to a server that a message came from you and wasn't modified. You could send the message alongside its SHA-256 hash:

message = "transfer $100 to Alice"
hash = SHA256("transfer $100 to Alice")
     = a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3

The problem: An attacker who intercepts the message can also compute SHA-256 — it's a public algorithm with no secret. They can modify the message to "transfer $1000 to Bob", compute a new SHA-256 hash, and send both along. The server can't tell the difference.

A hash alone proves integrity only if the attacker can't also compute hashes — which they can. You need a secret that only you and the server know. That's what HMAC adds.

With HMAC:

message = "transfer $100 to Alice"
hmac = HMAC-SHA256(secret_key, "transfer $100 to Alice")
     = 4a0e9c7d8b6f2e1a3d5c9b0e7f4a2d8c1b6e3f0a9d7c5b2e8a1f4d7c3b0e6f9a

An attacker cannot forge this tag without knowing secret_key. Even if they modify the message, they cannot produce a valid HMAC without the key. The server verifies the HMAC against the received message using its copy of the secret key — if they match, the message is authentic.

How HMAC Works Internally

HMAC is defined by this formula (RFC 2104):

HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))

Where:

  • H — the underlying hash function (e.g., SHA-256)
  • K — the secret key
  • K' — the key padded to the hash's block size (64 bytes for SHA-256)
  • ipad — inner padding: 0x36 repeated to fill the block
  • opad — outer padding: 0x5C repeated to fill the block
  • — XOR operation
  • || — concatenation

In plain English, HMAC works like this:

  1. Pad the key to the block size of the hash function (64 bytes for SHA-256)
  2. Inner hash: XOR the padded key with ipad, concatenate the message, and hash the result
  3. Outer hash: XOR the padded key with opad, concatenate the inner hash output, and hash again

The two-layer structure is deliberate. It prevents certain attacks against simpler constructions like H(K || m) (length extension attacks). HMAC's double-hash design is why it remains secure even with hash functions that have length-extension vulnerabilities.

You don't need to implement this yourself — every cryptographic library provides HMAC. But understanding the structure helps you reason about security properties.

HMAC vs Hash vs Digital Signature

These three mechanisms are often confused. Here's the key difference:

Mechanism Key Required Verifiable By Use Case
SHA-256 hash No Anyone Data integrity only (checksum)
HMAC-SHA256 Yes — shared secret Anyone with the same key Mutual authentication, API signing, webhooks
RSA / ECDSA signature Yes — asymmetric key pair Anyone with the public key Public authentication, TLS, JWT RS256

Hash (no key): Proves data wasn't accidentally corrupted. Doesn't prove who sent it.

HMAC (shared secret key): Proves data came from someone who knows the secret. Both sender and receiver must have the same key. Suitable for server-to-server communication where you control both sides.

Digital signature (asymmetric key): Proves data came from someone with the private key, verifiable by anyone with the public key. No need to share secrets. Used in TLS certificates, code signing, and JWT RS256.

When to use HMAC vs a signature:

  • If both parties are in your control (e.g., your server calling your other server, webhook delivery between services): HMAC
  • If you need third parties to verify authenticity without sharing a secret (e.g., TLS, user-facing JWTs in a multi-service system): digital signatures

Common Uses of HMAC

Webhook Signature Verification

When a service delivers webhooks to your endpoint, it needs a way to prove the payload is genuine (not forged by a third party). The standard approach is HMAC.

GitHub webhooks: GitHub signs every webhook payload with HMAC-SHA256 using your webhook secret. The signature appears in the X-Hub-Signature-256 header:

X-Hub-Signature-256: sha256=3dca279e731c97c38e3019a075d02bae6f9c69980f2a7a33e38571df2a1a4e37

Stripe webhooks: Stripe sends the Stripe-Signature header containing a timestamp and HMAC-SHA256 signature. They include the timestamp in the signed payload to prevent replay attacks.

AWS Request Signing

AWS Signature Version 4 uses HMAC-SHA256 to authenticate every API request. AWS derives a signing key from your secret key, the date, region, service, and a constant — and uses that key to sign a canonical representation of the HTTP request.

The signing key is derived through multiple HMAC operations:

kDate    = HMAC-SHA256("AWS4" + SecretKey, Date)
kRegion  = HMAC-SHA256(kDate, Region)
kService = HMAC-SHA256(kRegion, Service)
kSigning = HMAC-SHA256(kService, "aws4_request")
signature = HMAC-SHA256(kSigning, StringToSign)

This hierarchy means that even if someone captures a signed request, they can't derive your secret key from the HMAC output.

JWT Signatures (HS256, HS384, HS512)

When a JWT uses the HS256 algorithm, its signature is an HMAC-SHA256 over the base64url-encoded header and payload:

signature = HMAC-SHA256(secret, base64url(header) + "." + base64url(payload))

The server that verifies the JWT must know the same secret. This is why HS256 JWTs are appropriate for systems where the same server both issues and verifies tokens — but RSA (RS256) is better when multiple services need to verify tokens without sharing a secret.

Cookie and Session Integrity

Frameworks like Django and Rails sign session cookies with HMAC to prevent users from tampering with their session data. The server signs the cookie value with HMAC using a SECRET_KEY. On each request, the HMAC is verified — if it doesn't match, the cookie was tampered with and is rejected.

API Request Authentication

Many APIs use HMAC for request authentication. The client signs a canonical string (method + path + timestamp + body hash) with HMAC-SHA256. The server independently computes the same HMAC and compares.

This pattern is used by payment processors, trading platforms, and any API where extra authentication security beyond OAuth is needed.

HMAC in Practice: Code Examples

Python

import hmac
import hashlib

message = b"transfer $100 to Alice"
secret_key = b"my-secret-key"

# Compute HMAC-SHA256
signature = hmac.new(secret_key, message, hashlib.sha256).hexdigest()
print(signature)
# 4a0e9c7d8b6f2e1a3d5c9b0e7f4a2d8c1b6e3f0a9d7c5b2e8a1f4d7c3b0e6f9a

# Verify (constant-time comparison to prevent timing attacks)
expected = b"4a0e9c7d..."
if hmac.compare_digest(signature.encode(), expected):
    print("Valid!")

Note the use of hmac.compare_digest() instead of ==. This is critical — a plain equality check is vulnerable to timing attacks where an attacker can infer how many characters matched by measuring response time.

Node.js

const crypto = require('crypto');

const message = 'transfer $100 to Alice';
const secretKey = 'my-secret-key';

// Compute HMAC-SHA256
const signature = crypto
  .createHmac('sha256', secretKey)
  .update(message)
  .digest('hex');

console.log(signature);

// Verify (constant-time comparison)
const expected = '4a0e9c7d...';
const valid = crypto.timingSafeEqual(
  Buffer.from(signature, 'hex'),
  Buffer.from(expected, 'hex')
);

Bash (using OpenSSL)

# Compute HMAC-SHA256
echo -n "transfer \$100 to Alice" | \
  openssl dgst -sha256 -hmac "my-secret-key"
# HMAC-SHA256(stdin)= 4a0e9c7d...

Go

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

func computeHMAC(message, key string) string {
    mac := hmac.New(sha256.New, []byte(key))
    mac.Write([]byte(message))
    return hex.EncodeToString(mac.Sum(nil))
}

func verifyHMAC(message, key, expected string) bool {
    actual := computeHMAC(message, key)
    return hmac.Equal([]byte(actual), []byte(expected))
}

Webhook Signature Verification: Step by Step

Here's the full flow for verifying a GitHub webhook:

1. Set a webhook secret in GitHub — a random string you store on your server.

2. On each webhook delivery, GitHub sends:

POST /webhook HTTP/1.1
X-Hub-Signature-256: sha256=3dca279e731c97c38e3019a075d02bae6f9c69980f2a7a33e38571df2a1a4e37
Content-Type: application/json

{"action": "opened", "number": 1, ...}

3. Your server verifies:

import hmac
import hashlib
from flask import request, abort

WEBHOOK_SECRET = b"your-webhook-secret"

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Hub-Signature-256', '')
    
    # Recompute the expected HMAC
    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET,
        request.data,
        hashlib.sha256
    ).hexdigest()
    
    # Constant-time comparison
    if not hmac.compare_digest(signature, expected):
        abort(401, "Invalid signature")
    
    # Process the webhook
    payload = request.json
    ...

The key steps:

  1. Get the raw request body (before any JSON parsing — parse after verification)
  2. Recompute HMAC-SHA256 using your copy of the secret
  3. Compare with the provided signature using constant-time comparison
  4. Only process the payload if verification succeeds

Replay attack prevention: For high-security applications, also verify a timestamp included in the signed payload (Stripe does this). If the timestamp is more than 5 minutes old, reject it — even if the signature is valid.

HMAC Security Considerations

Key length matters

The HMAC key should be at least as long as the hash output — 32 bytes (256 bits) for HMAC-SHA256. A shorter key reduces the effective security of the HMAC. Generate keys using a cryptographically secure random number generator:

import secrets
key = secrets.token_bytes(32)  # 256-bit key
const key = crypto.randomBytes(32);

Always use constant-time comparison

Never compare HMAC values with == or ===. String comparison in most languages short-circuits on the first non-matching character, which leaks timing information. An attacker can measure response times to infer how many characters of their forged HMAC matched — and gradually craft a valid forgery.

Use hmac.compare_digest() in Python, crypto.timingSafeEqual() in Node.js, subtle.timingSafeEqual() in Go.

Don't reuse keys across contexts

If the same key is used for signing webhook payloads and signing session cookies, a valid HMAC from one context could potentially be used in another. Use separate keys for each purpose, or include a context string in the HMAC computation:

# Include context in the signed message
payload = f"webhook:v1:{raw_body}"
hmac.new(key, payload.encode(), hashlib.sha256)

HMAC-SHA256 is preferred over HMAC-MD5 or HMAC-SHA1

While HMAC-MD5 and HMAC-SHA1 are not as directly broken as MD5/SHA-1 alone (the HMAC construction provides some protection), best practice is to use HMAC-SHA256 for all new implementations. Some legacy protocols (like OAuth 1.0a) use HMAC-SHA1 — for these, migration is preferable but HMAC-SHA1 is not immediately catastrophic.

Generate HMAC Values Online

Use our HMAC Generator to compute HMAC values for any algorithm (HMAC-MD5, HMAC-SHA1, HMAC-SHA256, HMAC-SHA512) without installing OpenSSL or writing code. The HMAC-SHA256 Generator is pre-configured for the most common use case.

All computation happens in your browser — your keys and messages are never sent to any server.

Summary

  • HMAC produces a message authentication code — it proves both integrity and authenticity
  • It requires a shared secret key — without the key, you cannot produce or verify the HMAC
  • HMAC is not encryption — the message itself remains readable; HMAC only authenticates it
  • The algorithm: two layers of hashing with the key XORed into each layer — prevents length-extension attacks
  • HMAC-SHA256 is the correct choice for new implementations — used by GitHub webhooks, AWS Signature V4, JWT HS256
  • Always use constant-time comparison when verifying HMACs — == is vulnerable to timing attacks
  • Generate secret keys with a CSPRNG and use at least 256 bits
  • Don't reuse the same key across different authentication contexts