HMAC-SHA256 vs RSA asymmetric webhook signatures
Choosing a signing scheme is the first irreversible decision you make when securing an outbound webhook channel, because it dictates how secrets are distributed, who can prove what about a delivered event, and how much CPU each verification burns. This comparison sits under the HMAC Signature Verification reference and weighs symmetric HMAC-SHA256 against RSA and ECDSA asymmetric signatures so you can pick deliberately rather than by default. If you have already settled on the shared-secret model, the companion walkthrough HMAC webhook validation in Node.js shows the verification pipeline end to end. This page focuses on the trade-offs that decide which scheme you should run in the first place.
The core distinction is key symmetry. HMAC-SHA256 uses one shared secret that both the sender and receiver hold, so the same value that creates a signature also verifies it. RSA and ECDSA use a key pair: the sender signs with a private key it never shares, and every receiver verifies with a public key that carries no signing power. That single property cascades into every other dimension below.
Key distribution and the shared-secret blast radius
With HMAC-SHA256 the secret must reach every party that verifies signatures. For a single consumer this is trivial. The problem compounds as the number of receivers grows: if you fan one event stream out to ten internal services and each verifies independently, ten copies of the signing secret now exist, and a leak in any one of them lets an attacker forge events that all ten will accept. There is no way to grant verify-only capability with a symmetric key — possession of the verifying material is possession of the signing material.
Asymmetric signing breaks that coupling. The private signing key lives in exactly one place (the sender’s signer, ideally a KMS or HSM), and the public key can be published openly. A compromised receiver leaks only a public key, which grants an attacker nothing: they still cannot sign anything the other receivers will trust. This is the decisive advantage for multi-tenant platforms and any provider that signs events consumed by customers it does not control.
The cost is operational. Asymmetric schemes still require key rotation, and distributing rotated public keys to many consumers is its own problem — typically solved by a published JWKS endpoint. For the rotation mechanics that apply to both schemes, see Key Rotation Strategies.
Non-repudiation and what a signature actually proves
HMAC offers integrity and authenticity but not non-repudiation. Because both parties hold the same secret, either one could have produced a given signature. If a dispute arises over whether the sender truly emitted an event, an HMAC signature cannot settle it: the receiver had the means to forge it. For most internal integrations this is irrelevant — you trust both ends. For regulated financial or legal events where the recipient may later contest what was sent, it matters.
Asymmetric signatures provide non-repudiation. Only the holder of the private key could have produced a valid signature, so a verified RSA or ECDSA signature is cryptographic evidence that the sender — and only the sender — emitted that exact payload. This is why audit-grade webhook channels and inter-company event exchange lean asymmetric even though it costs more per verification.
Performance and verification cost
Performance is where HMAC dominates. An HMAC-SHA256 computation is a pair of hash passes — microseconds, and effectively free at any realistic webhook volume. RSA verification is cheap relative to RSA signing but still orders of magnitude slower than HMAC, and RSA signing with 2048-bit keys is genuinely expensive. ECDSA narrows the gap considerably: signing is fast and keys are tiny (a P-256 key is 32 bytes versus hundreds for RSA), at the cost of slower verification than HMAC and a hard requirement for a secure random nonce per signature.
| Property | HMAC-SHA256 | RSA-2048 (PSS) | ECDSA (P-256) |
|---|---|---|---|
| Key model | One shared secret | Private + public key pair | Private + public key pair |
| Verifier capability | Can also forge | Verify only | Verify only |
| Non-repudiation | No | Yes | Yes |
| Sign speed | Fastest | Slow | Fast |
| Verify speed | Fastest | Fast | Moderate |
| Signature size | 32 bytes | 256 bytes | 64 bytes |
| Best fit | Single/trusted consumer | Audit-grade, many consumers | Audit-grade, size-sensitive |
Verifying each scheme in TypeScript
The verification code makes the API difference concrete. HMAC verification recomputes the digest with the shared secret and compares in constant time:
import crypto from 'node:crypto';
export function verifyHmac(rawBody: Buffer, signatureHex: string, secret: string): boolean {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
const provided = Buffer.from(signatureHex, 'hex');
// Length check guards timingSafeEqual against RangeError and length leaks.
return expected.length === provided.length && crypto.timingSafeEqual(expected, provided);
}
Asymmetric verification needs only the public key — note there is no shared secret anywhere in the receiver’s code, which is exactly the point:
import crypto from 'node:crypto';
// publicKeyPem is safe to ship to every consumer; it cannot sign anything.
export function verifyRsa(rawBody: Buffer, signatureB64: string, publicKeyPem: string): boolean {
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(rawBody);
verifier.end();
return verifier.verify(
{ key: publicKeyPem, padding: crypto.constants.RSA_PKCS1_PSS_PADDING },
Buffer.from(signatureB64, 'base64'),
);
}
// ECDSA verification differs only in the key type and signature encoding.
export function verifyEcdsa(rawBody: Buffer, signatureB64: string, publicKeyPem: string): boolean {
const verifier = crypto.createVerify('SHA256');
verifier.update(rawBody);
verifier.end();
return verifier.verify(
{ key: publicKeyPem, dsaEncoding: 'ieee-p1363' },
Buffer.from(signatureB64, 'base64'),
);
}
crypto.verify is constant-time with respect to the secret material by construction, so unlike HMAC you do not manage timingSafeEqual yourself. The trade-off is that you must pin the padding (RSA_PKCS1_PSS_PADDING, not legacy PKCS#1 v1.5) and the ECDSA signature encoding (ieee-p1363 versus ASN.1 DER) to exactly match what the sender emitted, or every verification silently fails.
Choosing a scheme
Reach for HMAC-SHA256 when there is a single trusted consumer, the channel is internal, and you want the simplest possible verification with negligible CPU cost. Reach for asymmetric signing — ECDSA P-256 as the default, RSA-PSS where a counterparty mandates RSA — when events leave your trust boundary, when many independent consumers verify the same stream, or when you need non-repudiation for audit or compliance. A common middle path is HMAC internally with an asymmetric outer signature only on the public-facing edge, isolating the expensive scheme to the boundary where its guarantees actually pay off.