Skip to content
ecdsa.com

HMAC vs digital signatures: shared secret or key pair

Both prove that a message was not changed. Only a signature proves who made it to someone who cannot make one themselves.

HMAC vs digital signature is the most important design decision in any message authentication scheme, and it is about trust, not speed. An HMAC tag is computed and checked with the same secret key, so everyone who can verify can also forge. A signature is made with a private key and checked with a public key, so verifiers cannot create new valid messages.

At a glance

HMAC (RFC 2104, FIPS 198-1)Digital signature
KeysOne shared secretPrivate key signs, public key verifies
Who can create a valid tag or signatureAnyone holding the secret, including every verifierOnly the private key holder
Non-repudiation (provable to third parties)NoYes, given a trusted binding of key to signer
Key distributionSecret must reach every verifier confidentiallyPublic key can be published (JWKS, certificate, DNS)
Size of the proof32 bytes for HMAC-SHA25664 bytes (Ed25519, ECDSA P-256), 256–512 bytes (RSA)
Speed, one Apple M4 Max coreabout 7 million tags per second for 256-byte messages (OpenSSL 3.6.5)about 60,000 Ed25519 or 87,000 P-256 signatures per second
Typical usesWebhooks, session cookies, AWS SigV4, TOTP, HS256 JWTs, TLS 1.2 record MACsTLS certificates and handshakes, OIDC ID tokens, package signing, Git, passkeys
Post-quantumSafe with SHA-256 (Grover only halves preimage effort)Classical schemes broken by Shor; needs ML-DSA or similar

HMAC throughput measured with openssl speed -hmac sha256 (about 1.83 GB/s with 256-byte inputs); signature rates with openssl speed.

The trust model is the difference

An HMAC is a keyed hash: H((K ⊕ opad) ‖ H((K ⊕ ipad) ‖ m)). Its security goal is that nobody without the key can produce a valid tag. That is enough when exactly two parties share the key and trust each other not to forge, because a valid tag then means "the other party sent this". With three or more parties it breaks down: any verifier can impersonate the sender to the others, and in a dispute the sender can claim the receiver made the tag.

A digital signature separates the abilities. Publishing the verification key does not help anyone sign, so one issuer can serve thousands of verifiers, and a signature can be shown to an auditor or a court as evidence of origin, provided the key was bound to the signer by a certificate, a key directory or an account record. That separation is why identity tokens consumed by many services (OpenID Connect ID tokens, access tokens checked by resource servers) use signatures, and why a leaked verifier configuration is harmless with signatures but catastrophic with HMAC.

Practicalities

Key management. HMAC secrets must be generated randomly (at least 256 bits for HMAC-SHA256; RFC 7518 §3.2 requires that for HS256), transmitted confidentially, stored by every verifier and rotated everywhere at once or with a grace period of two valid secrets. Signature keys keep the private half in one place, ideally an HSM or KMS, and rotate through key identifiers in a published key set.

Performance and size. HMAC is two hash computations: millions per second per core, with a 32-byte tag. Signatures cost tens of microseconds and are 64 bytes or more. Neither matters for most web traffic; it matters for per-packet authentication or very high-volume internal messaging, where HMAC or an AEAD is the norm.

Pitfalls. HMAC must be compared in constant time, must not be replaced by hash(key ‖ message) (which SHA-256's length extension breaks), and must authenticate exactly the bytes received (raw request body, not a re-serialized JSON object). Signatures must be verified with an algorithm the verifier pins, never one chosen by the message.

The script below, run with Node.js 26.10, authenticates the same webhook body both ways.

Node.js: HMAC-SHA256 tag and Ed25519 signature over one webhook body
import { createHmac, generateKeyPairSync, randomBytes, sign, timingSafeEqual, verify } from "node:crypto";

const body = Buffer.from('{"event":"invoice.paid","id":"evt_123"}');

// HMAC: sender and receiver share one secret (in production it comes from a secret store).
const secret = randomBytes(32);
const tag = createHmac("sha256", secret).update(body).digest();
const expected = createHmac("sha256", secret).update(body).digest();
console.log("HMAC-SHA256 tag:", tag.length, "bytes, valid:", timingSafeEqual(tag, expected));

// Signature: sender keeps the private key, receivers hold only the public key.
const { privateKey, publicKey } = generateKeyPairSync("ed25519");
const signature = sign(null, body, privateKey);
console.log("Ed25519 signature:", signature.length, "bytes, valid:", verify(null, body, publicKey, signature));
Output
HMAC-SHA256 tag: 32 bytes, valid: true
Ed25519 signature: 64 bytes, valid: true

The HMAC secret must be shared with every verifier; the Ed25519 private key never leaves the sender, and only the 32-byte public key is distributed.

Webhooks and APIs in practice

Most webhook providers use HMAC because each subscriber gets its own secret: GitHub sends X-Hub-Signature-256: sha256= followed by a hex HMAC-SHA256 of the body, and Stripe signs a timestamp plus the body in its Stripe-Signature header so receivers can reject replays. Because each subscriber's secret is unique, the multi-party problem does not arise. Signed webhooks, built on HTTP Message Signatures (RFC 9421) or JWS, appear when one sender talks to many receivers that should not share a secret, or when receivers want to verify without provisioning secrets. AWS Signature Version 4 is HMAC-based request signing; OAuth DPoP and Web Bot Auth use signatures because the verifier must not be able to impersonate the client.

When to choose which

  • Same service issues and verifies (session cookies, CSRF tokens, internal JWTs): HMAC-SHA256.
  • One sender, one receiver, secret shared out of band (per-customer webhooks): HMAC with a timestamp in the signed data.
  • One issuer, many verifiers (identity tokens, public APIs, mobile apps verifying server data): signatures, with keys published in a JWKS.
  • Evidence for third parties (contracts, audit logs, software releases): signatures plus trusted timestamps.
  • Clients that must not be able to impersonate each other (DPoP, bot authentication, device attestation): signatures with per-client keys.
  • Very high message rates on a trusted link: HMAC or an AEAD.

Migration notes

Moving from HMAC to signatures usually happens when a second verifier appears or a secret leaks.

  1. Introduce an algorithm identifier if your format lacks one (JOSE alg, an HTTP signature alg parameter, a version prefix), so verifiers can know which scheme a message uses without trusting it blindly.
  2. Publish the public key with a key identifier (JWKS, HTTP signature key directory) and give verifiers time to fetch it.
  3. Dual-sign during the transition: send both the HMAC header and the signature, and let verifiers prefer the signature.
  4. Remove the shared secret from verifiers that only need to verify; this is the security gain, so do not keep it "just in case".
  5. Never let one key serve both schemes. An RSA public key reused as an HMAC secret is exactly the algorithm confusion that broke JWT libraries; see HS256 vs RS256.

References

All comparisons