ADR-1021: Constant-time session-token comparison + JWT nbf-claim validation¶
- Status: Accepted
- Date: 2026-06-04
- Deciders: Lusoris
- Tags:
security,auth
Context¶
A security audit (r5-crypto) identified four findings across the auth surfaces of the vmafx-controller and MCP HTTP transport:
-
cmd/vmafx-controller/nodes/registry.go—Heartbeat(line 133) andValidateSession(line 162) compared 32-char hex session tokens with plain!=/==. String equality in Go is not constant-time; the comparison returns as soon as a differing byte is found. An attacker with sub-millisecond clock access to the endpoint can enumerate tokens one character at a time (timing oracle), reducing a 256-bit brute-force space to at most 64 sequential guesses of 16 hex values. -
mcp-server/vmaf-mcp/src/vmaf_mcp/http_transport.py— theAuthorization: Bearerheader was compared against the configured token using Python's!=operator, which is also non-constant-time for strings of identical prefix. -
cmd/vmafx-controller/auth/middleware.go—verifyJWTvalidated theexpclaim but did not check thenbf(not-before) claim. A token issued with a futurenbfwas accepted immediately, allowing pre-issued tokens to be used before their intended validity window.
All three issues violate SEI CERT C / Go and Python secure-coding guidance (timing-safe comparisons for secrets; full standard-claim validation for JWTs).
Decision¶
- In Go, replace all session-token
==/!=comparisons withcrypto/subtle.ConstantTimeCompare— the standard library primitive for constant-time byte-slice comparison. - In Python, replace the
!=Bearer-token comparison withhmac.compare_digestfrom the standard library. - In
verifyJWT, after parsing the payload struct, add annbffield and reject tokens wherenbf != 0 && now < nbf.
No new dependencies are introduced; all three fixes use standard-library primitives available in the minimum supported language versions.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
Keep == / !=, rely on network latency jitter | Zero code change | Latency jitter on a local network or localhost loopback is insufficient to mask per-byte timing; not a viable mitigation | Rejected |
| Switch to opaque tokens (not hex-encoded) | Shorter tokens, harder to partially guess | Does not eliminate the timing channel for the comparison itself; orthogonal improvement | Out of scope for this fix |
| Third-party constant-time library | Potentially more ergonomic API | Adds a dependency; standard library covers the need | Unnecessary |
Consequences¶
- Positive: session-token comparisons are now safe against timing-oracle attacks. JWT
nbfis enforced, closing a window for pre-issued tokens. - Negative:
subtle.ConstantTimeComparealways compares all bytes (even after a mismatch), adding at most O(32) byte comparisons on the hot path — negligible for a heartbeat RPC. - Neutral / follow-ups: existing tests are extended with
TestValidateSession_ConstantTime(nodes) andTestVerifyToken_NbfRejected/TestVerifyToken_NbfInPastAccepted(auth) to guard against regression.
References¶
- r5-crypto audit findings HIGH × 2 + MEDIUM × 2.
- Go
crypto/subtlepackage: https://pkg.go.dev/crypto/subtle - Python
hmac.compare_digest: https://docs.python.org/3/library/hmac.html#hmac.compare_digest - RFC 7519 §4.1.5 — "nbf" (Not Before) Claim.
- SEI CERT MSC61-J (timing-safe comparisons).