cybersecurity · intermediate · ~15 min · safe pentest lab
Understand why secret-comparison must be constant-time and be able to implement a branchless, all-bytes byte comparison that eliminates a timing side channel, while still bounds-checking length and rejecting NULL by default.
A CSRF defense stores an expected anti-forgery token server-side and compares it against the token the browser submits with each state-changing request. If that comparison uses a normal strcmp/memcmp or bails out on the first mismatching byte, an attacker who can measure response time can recover the secret token one byte at a time: guesses that match a longer prefix take measurably longer to reject. Recovering the token lets the attacker forge requests and defeat CSRF protection entirely.
Asset: the secret expected CSRF token (a fixed n-byte buffer baked into the harness). Threat: a timing side channel that leaks how many leading bytes of a guess were correct. Insecure assumption: "it's fine to return early once the bytes differ."
Your task: implement ct_token_eq(a, b, n) that compares all n bytes of the two tokens in constant time — it must inspect every byte regardless of where (or whether) they differ, so the running time reveals nothing about the contents. Return 1 if the two n-byte tokens are equal, else 0.
Deny by default: if either pointer is NULL, reject (return 0). Never short-circuit on a mismatch, and never stop early at an embedded NUL — these are raw n-byte tokens, not C strings. An n of 0 means "nothing to compare", which is trivially equal (1).
Given expected and a copy match of the same n bytes, and forged differing in one byte:
ct_token_eq(expected, match, n) -> 1
ct_token_eq(expected, forged, n) -> 0
The function receives two token buffers a and b (each at least n bytes, or NULL) and a byte count n (size_t). Buffers are raw bytes and may contain any value including 0x00.
Return int 1 if the first n bytes of a and b are byte-for-byte equal, otherwise 0. Return 0 if either pointer is NULL.
C11, freestanding-friendly: the harness provides all includes. Must run in O(n) time that depends only on n, never on the byte values (no early return, no data-dependent branch that ends the loop). Do not read past n bytes. Treat buffers as raw bytes, not NUL-terminated strings. n may be 0. No dynamic allocation, no global state.
int ct_token_eq(const char *a, const char *b, size_t n){
/* TODO: compare ALL n bytes of a and b in constant time.
Reject NULL by default; never return early on a mismatch.
Return 1 if equal, else 0. This insecure stub does neither. */
(void)a; (void)b; (void)n;
return -1;
}Using strcmp/memcmp/strncmp and returning as soon as bytes differ (leaks a timing signal and, for strcmp, stops at the first NUL). Returning inside the loop on the first mismatch. Stopping the loop at an embedded NUL and thus comparing fewer than n bytes. Forgetting the NULL check and dereferencing a null pointer. Returning the accumulated diff (or its negation) directly instead of normalizing to exactly 1 or 0. Using a signed char XOR that sign-extends and corrupts the accumulator.
n == 0 returns 1 (nothing to compare). NULL a or NULL b returns 0. Tokens that differ only in the very last byte must still be rejected (0). Tokens differing only in the first byte must be rejected but the loop must NOT stop early. Buffers containing an embedded 0x00 must be compared over the full n bytes, not truncated at the NUL. Identical buffers (including a == b) return 1.
Solve this exercise in the browser editor — compile and run against the test harness, no setup required.