Chapter 1 · Practice 4 of 4
Compare MACs, tokens and signatures in constant time
Why it matters
A normal string comparison stops at the first byte that differs. Measured over many requests, that timing difference can let an attacker guess a valid MAC or token one byte at a time. Whenever you compare a secret value with one supplied by a caller, use a constant-time comparison:
| Language | Function |
|---|---|
| Node.js | crypto.timingSafeEqual(a, b) (same-length Buffers) |
| Python | hmac.compare_digest(a, b) |
| Go | subtle.ConstantTimeCompare(a, b) |
| Java | MessageDigest.isEqual(a, b) |
| .NET | CryptographicOperations.FixedTimeEquals(a, b) |
High-level verify functions (crypto_sign_verify_detached, Tink's Mac.verifyMac) already do this for you, which is one more reason to prefer them over computing a MAC and comparing it yourself.
The example verifies a webhook the way most providers document it: an HMAC over a timestamp and the raw body, a timestamp check to stop replays, and a constant-time compare.
Example
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyWebhook(rawBody: Buffer, header: string, ts: string, secret: string) {
const age = Math.abs(Date.now() / 1000 - Number(ts));
if (!Number.isFinite(age) || age > 300) return false; // reject old or replayed calls
const expected = createHmac('sha256', secret).update(`${ts}.`).update(rawBody).digest();
const given = Buffer.from(header, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}