Constant-time Comparisons
A timing attack measures how long an operation takes and deduces secret information from the variance. Classic targets: password compares, MAC verification, table lookups in AES. The fix is constant-time code: branch-free comparisons that take the same number of CPU cycles regardless of input.
Constant-time compare and MAC verify
EXAMPLE
// ===== Python =====
import hmac, secrets, time
# 1) WRONG: == compares character by character, returns early on first mismatch.
def naive_eq(a: bytes, b: bytes) -> bool:
return a == b
# 2) RIGHT: hmac.compare_digest runs in constant time
def safe_eq(a: bytes, b: bytes) -> bool:
return hmac.compare_digest(a, b)
# Sketch a measurement showing the leak on toy data
def time_eq(fn, target):
candidates = [target] + [b'X' * len(target)]
candidates += [bytes([target[0]]) + b'X' * (len(target) - 1)]
for c in candidates:
t = time.perf_counter()
for _ in range(50_000): fn(c, target)
print(f'{c[:6]}... {(time.perf_counter() - t) * 1e6:.0f}us')
# ===== JavaScript / Node =====
const { timingSafeEqual, createHmac, randomBytes } = require('node:crypto');
function safeEqualUtf8(a, b) {
const A = Buffer.from(a, 'utf8'); const B = Buffer.from(b, 'utf8');
if (A.length !== B.length) return false; // length leak is unavoidable
return timingSafeEqual(A, B);
}
// MAC over a webhook payload, then constant-time compare
function verifyWebhook(signatureHeader, body, secret) {
const expected = createHmac('sha256', secret).update(body).digest('hex');
// Equalise lengths before timingSafeEqual; reject early on mismatch length.
if (signatureHeader.length !== expected.length) return false;
return timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}
// ===== Go =====
// import "crypto/subtle"
// func eq(a, b []byte) bool {
// return subtle.ConstantTimeCompare(a, b) == 1 // returns int 1/0
// }
// ===== PHP =====
// hash_equals(\$expected, \$user_supplied)
// ===== Pitfalls that re-introduce timing leaks =====
// - Comparing strings with == AFTER you used a constant-time function.
// - Stopping early (return) the moment lengths differ — leaks length.
// Defence: derive expected from the secret-keyed HMAC and use its length;
// a length-mismatched attacker controls nothing useful.
// - Using string slicing / indexing as an 'optimisation' (str[:n] == other[:n]).
// - Database password compares (== in SQL) - move auth into the app layer
// using a real password hash (argon2, bcrypt).
// ===== Beyond compares: constant-time crypto primitives =====
// - Use library primitives (libsodium, Bouncy Castle, BoringSSL) not hand-rolled.
// - AES-GCM, ChaCha20-Poly1305 over AES-CBC where possible.
// - Ed25519 signing (deterministic, no nonce-reuse footgun).
// - HKDF for key derivation; never plain SHA over the shared secret.
Why it matters
Reach for the libraries built-in constant-time compare every single time you compare anything that depends on a secret — HMACs, tokens, CSRF tokens, signed cookies, webhook signatures. Equality of secrets is not the place to outsmart the standard library.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Naive == on secrets leaks bits via timing. // Use crypto.timingSafeEqual / hmac.compare_digest / hash_equals.Try it Yourself »
Discussion
Loading…