Timing Attack - Zeitbasierter Seitenangriff
Timing attacks exploit differences in the execution time of cryptographic operations or comparison functions to infer confidential information. Classic attacks: password timing (early termination upon the first difference), RSA timing (modular exponentiation), cache timing (Spectre/Meltdown). Protection: Constant-time comparisons (hmac.compare_digest), blinding, branch-free implementations.
Timing attacks belong to the class of side-channel attacks: Attackers do not exploit logical weaknesses in an algorithm, but rather physical or temporal properties of its implementation. Even a cryptographically sound algorithm can become vulnerable due to an insecure implementation if the execution time reveals secrets.
The Fundamental Problem
Insecure String Comparison Implementation (Conceptual)
Function: verify_token(user_input, secret)
for i in range(len(user_input)):
if user_input[i] != secret[i]:
return False ← EARLY TERMINATION!
return True
Timing Leak
"AAAA..."→ terminates after position 1 → very fast"XBBB..."→ terminates after position 1 → just as fast"XAAA..."→ terminates after position 2 → slightly slower"XYAA..."→ terminates after position 2 → just as fast
Measurement: Which first character is the correct one? → The one with the longest response time! Try each byte individually instead of all combinations!
- Without timing: O(2^n) combinations for an n-bit key
- With timing: O(n * 256) attempts for an n-byte key
- → Reduced from exponential to linear!
Where does timing variance come from?
- Early termination during comparisons (most common cause)
- Branch prediction: if/else depending on the secret
- Cache hits: data in the CPU cache → faster
- Multiplication vs. exponentiation: different cycles
- Garbage collection: GC pauses can also mask it!
- Network jitter: makes remote timing more difficult
Attack scenarios
Scenario 1 - Web token validation (most common web vulnerability)
Attacker guesses HMAC token byte by byte through timing measurement.
Method:
- Thousands of requests with first byte = 'A'
- Thousands of requests with first byte = 'B'
- ...until the byte with the longest response time is found → first byte correct!
- Continue with the second byte...
Requirements:
- Measurable response times (latency < noise)
- Server without rate limiting
- Many measurements required (statistical analysis)
Remote timing: more difficult (network jitter), but possible! Cloudflare research: microsecond differences across tens of thousands of requests are statistically measurable (t-test).
Scenario 2 - Username Enumeration
- Valid username + incorrect password: 200 ms (password hash calculated!)
- Invalid username: 5 ms (aborted early, no hash)
Attacker: Username enumeration via timing difference—even if the HTTP response looks identical.
> Proof: Amazon AWS SDK had this bug (CVE-2015-3154)
Scenario 3 - RSA Private Key (Kocher 1996, historical)
RSA decryption: Modular exponentiation using private exponents. Square-and-Multiply algorithm:
- Bit = 1: Square + Multiply (more operations!)
- Bit = 0: Square only (fewer operations)
Timing difference → Private key bits can be extrapolated! Affected early implementations without blinding.
Protection: Montgomery blinding (random blind factor before decryption)
Scenario 4 - Cache Timing (Spectre, 2018)
CPU cache stores recently accessed data. Attacker process measures access time to its own array elements. If access time is fast → data still in cache → CPU accessed this address during a previous operation!
Spectre tricks CPU branch prediction → speculative access to kernel memory → timing side channel → read data!
Affects: practically all modern CPUs (hardware problem!)
Mitigation: Kernel Page Table Isolation (KPTI), Retpoline, microcode updates (performance overhead!)
Scenario 5 - TOTP bypass via timing
TOTP validation without constant time:
- Attacker sends 000000, measures response time
- If "00" matches → slightly longer
- Iterates through all 6 digits sequentially
Practically limited by: short TOTP validity (30s), network jitter, modern CT implementations.
Constant-Time Implementations
Python - WRONG vs. RIGHT
# WRONG: string == string → early termination possible!
if user_token == secret_token:
grant_access()
# RIGHT: hmac.compare_digest - constant time!
import hmac
if hmac.compare_digest(user_token.encode(), secret_token.encode()):
grant_access()
# Same execution time regardless of whether tokens match or not!
# ALSO CORRECT: secrets.compare_digest (Python 3.3+):
import secrets
if secrets.compare_digest(user_token, secret_token):
grant_access()
Node.js - WRONG vs. RIGHT
// WRONG:
if (userToken === secretToken) { ... }
if (userToken == secretToken) { ... }
// CORRECT: crypto.timingSafeEqual (Node.js built-in):
const crypto = require('crypto');
const a = Buffer.from(userToken);
const b = Buffer.from(secretToken);
// IMPORTANT: Lengths must be equal for timingSafeEqual!
if (a.length === b.length && crypto.timingSafeEqual(a, b)) {
grantAccess();
}
// Compare lengths separately → does not reveal length information,
// since constant-time-equal returns 'false' anyway if lengths differ
Java - WRONG vs. RIGHT
// WRONG:
if (userToken.equals(secretToken)) { ... }
// CORRECT: MessageDigest.isEqual (constant-time):
import java.security.MessageDigest;
if (MessageDigest.isEqual(
userToken.getBytes(),
secretToken.getBytes())) { ... }
Go - WRONG vs. RIGHT
// WRONG:
if userToken == secretToken { ... }
// RIGHT: subtle.ConstantTimeCompare:
import "crypto/subtle"
if subtle.ConstantTimeCompare([]byte(userToken), []byte(secretToken)) == 1 {
// access granted
}
PHP - WRONG vs. RIGHT
// WRONG: strcmp(), ==, ===
// RIGHT: hash_equals() (PHP 5.6+)
if (hash_equals($secretToken, $userToken)) { ... }
Timing Tests in Penetration Testing
1. Username Enumeration (Timing)
# Tool: ffuf, Burp Intruder with timing analysis
for user in wordlist:
time curl -s -X POST /login -d "user=$user&pass;=WRONG"
# → Mean value per username + standard deviation
# → Outlier on the high end → valid username!
2. Token Comparison (Remote Timing)
# Tool: timing-attack-framework or custom script
for prefix in range(256):
times = []
for _ in range(100): # many measurements!
t = measure(POST /verify, token=chr(prefix)+garbage)
times.append(t)
results[prefix] = median(times)
# → Byte with highest median → likely correct
3. Statistical Analysis
- Single measurement: too much noise!
- Median over 50–100 measurements
- Welch’s t-test: Is the difference statistically significant?
- Tools: timing-attack (Python), bcheck (Burp)
4. Local Timing Tests (for Code Reviews)
# Python timeit: nanosecond resolution
# Find == vs hmac.compare_digest in codebase:
grep -r "== request\|== token\|== key\|== secret" src/
5. What to test
- Login endpoints (username enumeration)
- Token validation (API keys, CSRF tokens, OTP)
- Password reset token verification
- Authorization checks using string comparison
- Crypto libraries (HMAC comparison)
Mitigation measures
1. Use constant-time functions everywhere
- ONLY use constant-time comparisons for secrets: HMAC, tokens, passwords, keys
hmac.compare_digest(Python),timingSafeEqual(Node.js)hash_equals(PHP),MessageDigest.isEqual(Java)- Code review checklist: no
==for security-critical comparisons
2. Prevent username enumeration
- Consistent response time during login (always compute the password hash!)
- Compute a dummy hash even for invalid usernames
- Same HTTP response for incorrect username AND incorrect password
3. Rate limiting
- Makes timing attacks impractical (too many requests required)
- Max 5–10 login attempts per minute per IP
4. Use network jitter (weaker mitigation)
- Add a small random delay:
time.sleep(random(0, 5ms)) - Masks timing differences in the millisecond range
- But: often still detectable with enough measurements!
- Not a real solution, just makes it harder
5. Hardware-side (Spectre/Meltdown)
- Install microcode updates
- Kernel with KPTI, Retpoline
- Browser: reduced timer resolution (SharedArrayBuffer disabled)
- Accept performance trade-off
6. Cryptographic libraries
- Use only verified cryptographic libraries (OpenSSL, libsodium, Bouncy Castle)
- DO NOT implement your own cryptography!
- RSA: Ensure padding blinding (OAEP instead of PKCS#1 v1.5)
- AES-GCM: Avoid nonce reuse (GCM nonce misuse)