BlogHMAC Explained: How to Generate and Verify HMAC Signatures Securely
·Updated September 22, 2026·9 min read·JWTSecrets Team

HMAC Explained: How to Generate and Verify HMAC Signatures Securely

What HMAC is, how it differs from plain hashing and encryption, how to generate an HMAC key and signature in Node.js, Python, and Go, and common pitfalls.

HMAC Explained: How to Generate and Verify HMAC Signatures Securely

HMAC shows up everywhere once you start looking: webhook signature verification (Stripe, GitHub, Slack all use it), API request signing, and — directly relevant if you're on this site — it's the mechanism behind HS256 JWT signing. If you've generated a JWT secret before, you've already generated an HMAC key without necessarily thinking of it that way.

This guide explains what HMAC actually is, how it's different from both plain hashing and encryption (a mix-up that causes real bugs), and how to generate keys and compute/verify signatures correctly.

What HMAC is (and isn't)

HMAC (Hash-based Message Authentication Code) combines a cryptographic hash function (typically SHA-256, sometimes SHA-512) with a secret key to produce a signature that proves two things at once:

1. Integrity — the message hasn't been altered.

2. Authenticity — the message came from someone who knows the secret key.

A plain hash (just SHA256(message)) only gives you integrity against accidental corruption — anyone can compute a hash, so it proves nothing about who sent the message, and an attacker who intercepts the message can recompute a new hash for a tampered version. HMAC fixes this by keying the hash with a secret only the legitimate parties know: HMAC(secret, message). Without the secret, you can't produce a valid signature for a modified message, even though you can still read and recompute a plain hash freely.

It's also worth being clear about what HMAC is *not*: it's not encryption. HMAC doesn't hide the message content — anyone can read the message; the signature just proves it wasn't tampered with and came from someone holding the key. If you need to also hide the content, you need actual encryption (like AES-256) alongside or instead of HMAC.

HMAC vs a digital signature (RSA/ECDSA)

Both prove authenticity and integrity, but the trust model differs:

HMACRSA/ECDSA signature
Key typeSingle shared secretPublic/private key pair
Who can verifyAnyone with the secret (same key that signs)Anyone with the public key
Who can forgeAnyone with the secretOnly the private key holder
Typical useTwo parties who already share a secret (webhooks, HS256 JWTs)Verification needs to be public (RS256 JWTs, code signing, TLS certs)

This is the same symmetric-vs-asymmetric tradeoff covered in HS256 vs RS256 — HMAC is the mechanism underneath HS256 specifically.

Generating an HMAC key

An HMAC key should be a random byte string, ideally at least as long as the hash function's output — for HMAC-SHA256, that means at least 32 bytes (256 bits). Shorter keys don't break HMAC's security proof outright, but there's no reason to under-provision entropy here; generate a full-length random key the same way you would an AES or JWT secret.

Node.js:

const crypto = require('crypto');

const key = crypto.randomBytes(32); // 256-bit HMAC key
console.log(key.toString('hex'));

Python:

import secrets

key = secrets.token_bytes(32)
print(key.hex())

Go:

key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
    panic(err)
}

Prefer generating this with a real CSPRNG (as above) over deriving it from a typed password, for the same reasons covered in our AES-256 key generation guide — and if you'd rather skip the code, our HMAC Generator produces one client-side in your browser.

Computing and verifying an HMAC signature

Node.js:

const crypto = require('crypto');

function sign(message, key) {
  return crypto.createHmac('sha256', key).update(message).digest('hex');
}

function verify(message, key, signature) {
  const expected = sign(message, key);
  // constant-time comparison — see the pitfall below
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(signature, 'hex')
  );
}

Python:

import hmac
import hashlib

def sign(message: bytes, key: bytes) -> str:
    return hmac.new(key, message, hashlib.sha256).hexdigest()

def verify(message: bytes, key: bytes, signature: str) -> bool:
    expected = sign(message, key)
    return hmac.compare_digest(expected, signature)  # constant-time

Go:

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

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

func verify(message, key []byte, signature string) bool {
	expectedMAC := hmac.New(sha256.New, key)
	expectedMAC.Write(message)
	expected := expectedMAC.Sum(nil)
	sigBytes, _ := hex.DecodeString(signature)
	return hmac.Equal(expected, sigBytes) // constant-time
}

The pitfall that actually matters: timing attacks

Notice that every verify function above uses a constant-time comparison (timingSafeEqual, compare_digest, hmac.Equal) instead of a normal === / == string comparison. This isn't stylistic — it's the single most common real-world HMAC implementation bug.

A naive string comparison returns as soon as it finds the first mismatched byte, which means comparing a correct signature takes measurably longer than comparing one that's wrong in the very first byte. An attacker who can measure response timing precisely enough (this has been demonstrated practically, including over networks in some conditions) can exploit this to guess the correct signature one byte at a time, turning what should be an infeasible brute-force into a tractable attack. Always use your language's constant-time comparison function for signature verification — never a plain equality check.

Where you'll actually use this

  • Webhook verification — Stripe, GitHub, Shopify, and most webhook providers sign their payloads with HMAC-SHA256 and send the signature in a header (e.g. Stripe-Signature). Your endpoint recomputes the HMAC over the raw request body using the shared webhook secret and compares it to the header, using a constant-time comparison, before trusting the payload.
  • HS256 JWTs — the JWT's signature segment *is* an HMAC-SHA256 (or SHA-384/512 for HS384/HS512) computed over the header and payload, keyed with your JWT secret. This is exactly why JWT secrets need the same entropy and storage discipline as any other HMAC key — see what size JWT secret do I need.
  • API request signing — some APIs require you to HMAC-sign requests (method, path, timestamp, body) to prove the request wasn't forged or replayed.

FAQ

Can I use HMAC-SHA1?

It's not broken in the way SHA-1 itself is for collision resistance, but there's no reason to choose it over SHA-256 today — use HMAC-SHA256 or HMAC-SHA512 for new systems.

Is a longer key always more secure?

Beyond the hash function's block size (64 bytes for SHA-256), additional key length doesn't add meaningful security — the key gets hashed down internally either way. 32 bytes (256 bits) is a solid, standard choice.

What's the difference between HMAC-SHA256 and plain SHA-256 with the key appended to the message?

Naively concatenating a key and message before hashing (SHA256(key + message)) is vulnerable to a length-extension attack against SHA-256 specifically — an attacker can sometimes compute a valid hash for message + extra data without knowing the key. HMAC's construction (a specific double-hashing scheme, not simple concatenation) is specifically designed to prevent this. Always use a real HMAC implementation, never hand-roll the concatenation.

---

Generate an HMAC key now: our free HMAC Generator runs entirely in your browser.

*Related: SHA-256 vs SHA-512 · AES-256 key generation · What size JWT secret do I need*

Written by

JWTSecrets Team

Editorial Team

The JWTSecrets editorial team writes practical guides on JWT authentication, cryptographic key management, and browser-based security tooling. Our content is reviewed against IETF RFCs and current library documentation.