Cryptography Best Practices for Developers

Chapter 1 · Practice 4 of 4

Compare MACs, tokens and signatures in constant time

From Choosing the right tool and library

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:

LanguageFunction
Node.jscrypto.timingSafeEqual(a, b) (same-length Buffers)
Pythonhmac.compare_digest(a, b)
Gosubtle.ConstantTimeCompare(a, b)
JavaMessageDigest.isEqual(a, b)
.NETCryptographicOperations.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

TypeScript
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);
}