Secure Coding in C · intermediate · ~12 min
## What you will learn - Explain what a **timing side channel** is and how an early-exit comparison like `memcmp` leaks where two secrets first differ. - Implement a **constant-time equality** check whose runtime depends only on the length `n`, never on the data. - Use the XOR-and-OR-accumulate pattern correctly: fold every byte pair so a single accumulator answers "equal or not". - Recognise and avoid the subtle mistakes that silently reintroduce the leak (early `break`, branching on a secret, signed return values). - Decide *when* a constant-time compare is required (secrets: MACs, tokens, password hashes) and when an ordinary `memcmp` is perfectly fine. - Verify your implementation defensively and know which vetted library functions to reach for in production.
Imagine a lock that, instead of just saying "wrong key," rattled a little longer each time you got one more pin correct. You could pick it by listening: try every first pin until the rattle lasts longer, lock that one in, then move to the second. You never see inside the lock — you just time it.
That is exactly the weakness in a naive secret comparison. When a server checks whether the security tag you sent matches the one it expected, the most natural code is memcmp. But memcmp stops at the first differing byte. The closer your guess is to correct, the longer the check runs before failing. An attacker who can measure that time can recover the secret one byte at a time — without ever guessing the whole thing at once.
A constant-time comparison removes that signal. It looks at every byte regardless of where the inputs differ, so the running time reveals nothing about the data. The whole defence is a tiny loop, but getting it exactly right — and understanding why each line matters — is what this lesson is about.
This builds directly on two earlier topics. From bitwise-operators you already know that a ^ b is zero only when a and b are identical bits, and that | (OR) keeps a bit set once any input sets it — those two facts are the algorithm. From pointers you know that a byte buffer is just an address plus a length, which is why our function takes const void * pointers and a size, and why bounds and NULL checks matter.
Key terms we will use throughout:
Secret comparison happens on nearly every authenticated request your software handles, and a single missing line turns it into an exploitable leak.
256^32 brute force into roughly 256 * 32 measured attempts. The asymmetry is enormous, which is what makes the defence non-negotiable for secrets.For a learner moving into defensive security, this is one of the highest-value patterns to internalise: small, ubiquitous, easy to get wrong, and directly tied to attacker capability.
Definition. A side channel is any way a computation reveals information through its physical or observable behaviour — time taken, power drawn, cache state — rather than through its intended return value.
Plain language. Two functions can return the exact same answer yet take different amounts of time depending on the secret data they handled. If an attacker can measure that time, the duration itself becomes an output you never meant to expose.
How it works internally. memcmp(a, b, n) walks the bytes and returns the moment it finds a mismatch. So if the first byte already differs, it does ~1 comparison; if the first 10 bytes match, it does ~11. The number of loop iterations — and therefore the time — encodes how many leading bytes matched.
Attacker guesses tag, server checks with memcmp:
guess: 5A | 00 00 00 ... first byte wrong -> fail FAST (1 compare)
guess: A1 | 00 00 00 ... first byte right -> fail slower (2 compares)
^ time increases as the leading-match count grows
Measure time -> learn how many leading bytes are correct -> fix one byte, repeat.
When it matters / when it doesn't. It matters whenever one operand is a secret the attacker is trying to learn (MAC tags, API tokens, session ids, password-hash outputs, OTP codes). It does not matter when both operands are public, or when the result is not attacker-observable (e.g. comparing two filenames in a local tool).
Pitfall. Believing the network jitter "hides" the leak. Attackers average over many samples; statistics recover the signal from noise. Do not rely on noise as a defence.
Knowledge check (explain in your own words): Why does returning the correct equal/not-equal answer NOT make a comparison safe for secrets?
Definition. Code is constant-time when its execution time depends only on the sizes of its inputs, never on the values.
Plain language. Same length in, same amount of work out — whether the data matches on byte 0, byte 1000, or never.
Structure of the defence. Touch every byte unconditionally, combine the per-byte results without any data-dependent branch, and produce one final verdict at the end.
Constant-time equality of a[0..n) and b[0..n):
acc = 0
for i in 0..n: # loop count fixed by n, not by data
diff_i = a[i] XOR b[i] # 0 iff bytes equal
acc = acc OR diff_i # sticky: stays non-zero once any diff seen
equal = (acc == 0) # single verdict, computed after the loop
time spent ~ n (always) <- independent of WHERE a and b differ
When to use / not use. Use it for any equality check on a secret. Do not use it as a general-purpose memcmp replacement where ordering (</>) is needed — it only answers equal/not-equal, and it is intentionally slower for matching inputs.
Pitfall. Adding any if that depends on the running result ("stop early if we already differ") destroys the property. The loop must always run to n.
Knowledge check (predict the output): If n == 8 and the buffers differ only at index 7, how many loop iterations run in the constant-time version? In memcmp?
Definition. ^ (XOR) yields a zero byte exactly when two bytes are equal; | (OR) is a sticky accumulator — once a bit becomes 1 it never goes back to 0 by OR-ing more values.
Plain language. XOR asks "are these two bytes the same?" (answer 0 = yes). OR remembers "has any pair ever said no?".
How it works internally. After the loop, acc holds the OR of all per-byte differences. If even one byte pair differed, at least one bit in acc is set, so acc != 0. If every pair matched, every diff_i was 0x00, so acc stays 0x00.
a: 6F 6B 21 b: 6F 6B 21 ("ok!" vs "ok!")
^: 00 00 00 -> acc = 00 | 00 | 00 = 00 -> EQUAL
a: 6F 6B 21 b: 6F 6B 3F
^: 00 00 1E -> acc = 00 | 00 | 1E = 1E -> NOT EQUAL (and 1E != 0)
When to use / not use. This is the right engine for fixed-length secret comparison. It is not suitable when the two inputs have different lengths you must hide — leaking length is a separate concern handled by hashing both sides first.
Pitfall. Using + instead of | to accumulate. Addition can overflow and, in pathological cases, wrap back toward patterns that obscure a difference; OR is safe because difference bits are sticky. (XOR-accumulate also works; OR is the conventional, clearest choice.)
Knowledge check (find the bug): A learner writes acc &= a[i] ^ b[i]; using AND instead of OR. Why does this give the wrong answer for most inputs?
The function signature mirrors memcmp so it is a drop-in for secret comparison, but it returns a simple equal/not-equal verdict.
#include <stddef.h> /* size_t */
/* Returns 0 if the first n bytes of a and b are all equal, non-zero otherwise.
Runtime depends only on n, not on the contents. */
int ct_memcmp(const void *a, const void *b, size_t n);
Key syntax points:
const void * — accepts any byte buffer; cast to const unsigned char * inside before indexing (you cannot index a void *).size_t n — unsigned length type, the natural type for buffer sizes.unsigned char so XOR/OR stay defined and the high bit cannot be misread as a sign.i < n is the only loop control — there is no break.Annotated core:
const unsigned char *pa = a, *pb = b; /* index bytes through unsigned char */
unsigned char acc = 0; /* sticky difference accumulator */
for (size_t i = 0; i < n; i++)
acc |= (unsigned char)(pa[i] ^ pb[i]); /* no data-dependent branch here */
return acc; /* 0 == equal; promoted to int safely */
A MAC tag (Message Authentication Code) proves that a message has not been tampered with. To check it, you compare the received tag against the expected one.
If you compare them with memcmp, you create a problem. memcmp stops at the first byte that differs. So the time it takes to fail depends on how many bytes matched before the mismatch.
That timing difference is a side channel: an attacker can measure it and learn how far their guess got. By repeating this, they can recover the correct tag one byte at a time.
The fix is a constant-time compare: a comparison that always touches every byte, so the runtime does not reveal where the inputs first differed.
This is one of the most-cited defensive patterns in cryptography. The core of it is three lines.
unsigned char acc = 0;
for (size_t i = 0; i < n; i++) acc |= a[i] ^ b[i];
return acc; /* zero iff a and b are equal in all n bytes */
How it works:
a[i] ^ b[i] is 0 only if the two bytes are equal.OR-ing those results into acc keeps acc at 0 only if every pair was equal.Implement:
int ct_memcmp(const void *a, const void *b, size_t n);
Rules:
n bytes match.a or b is NULL while n > 0, return 1.if (acc != 0) break; "optimisation". That early exit is exactly the timing leak you were trying to avoid.unsigned char first.memcmp. For matching inputs it is strictly slower, and that is the point.#include <stdio.h> #include <stddef.h>
/*
Constant-time byte equality.
Returns 0 if the first n bytes of a and b are all equal, non-zero otherwise.
The number of loop iterations is fixed by n, so the runtime does not reveal
WHERE (or whether) the buffers differ. This is the defence used when one of
the operands is a secret such as a MAC tag or an authentication token.
Defensive contract: if a or b is NULL while n > 0, treat as "not equal"
(return non-zero) rather than dereferencing a NULL pointer. */ int ct_memcmp(const void *a, const void b, size_t n) { if (n == 0) return 0; / nothing to compare -> equal by definition / if (a == NULL || b == NULL) return 1; / cannot compare; report not-equal, no deref */
const unsigned char pa = a; / void* cannot be indexed; view as bytes */ const unsigned char pb = b; unsigned char acc = 0; / stays 0 only if every byte pair matches */
for (size_t i = 0; i < n; i++) acc |= (unsigned char)(pa[i] ^ pb[i]); /* sticky OR; NO early exit */
return acc; /* 0 == all bytes equal; non-zero otherwise */ }
static void report(const char *label, int r) { printf("%-28s -> %s (ret=%d)\n", label, r == 0 ? "EQUAL" : "NOT EQUAL", r); }
int main(void) { unsigned char expected[] = { 0x9F, 0x12, 0xAB, 0x40, 0x01 }; unsigned char same[] = { 0x9F, 0x12, 0xAB, 0x40, 0x01 }; unsigned char diff_last[]= { 0x9F, 0x12, 0xAB, 0x40, 0xFF }; unsigned char diff_first[]={ 0x00, 0x12, 0xAB, 0x40, 0x01 };
size_t n = sizeof expected;
report("identical tags", ct_memcmp(expected, same, n));
report("differs at last byte", ct_memcmp(expected, diff_last, n));
report("differs at first byte",ct_memcmp(expected, diff_first,n));
report("zero length", ct_memcmp(expected, same, 0));
report("NULL pointer guard", ct_memcmp(expected, NULL, n));
return 0;
}
Walkthrough of the key call ct_memcmp(expected, diff_last, 5), where the buffers match on bytes 0–3 and differ at byte 4.
if (n == 0) — n is 5, so we skip this; there is real work to do.if (a == NULL || b == NULL) — both pointers are valid, so we continue. (This guard exists so a buggy caller can't crash us.)pa and pb are set to view the buffers as unsigned char arrays so we can index byte-by-byte. Indexing a raw void * is illegal in C; this cast is what makes byte access well-defined.acc = 0 — the accumulator starts clean. It will stay 0 only if no byte pair differs.i = 0,1,2,3,4 — always five iterations, set by n alone.return acc — after the loop, acc is 0xFE (non-zero), so the caller sees "not equal".Trace of acc through the loop:
i | pa[i] | pb[i] | pa^pb | acc (after OR)
---+-------+-------+-------+---------------
0 | 9F | 9F | 00 | 00
1 | 12 | 12 | 00 | 00
2 | AB | AB | 00 | 00
3 | 40 | 40 | 00 | 00
4 | 01 | FF | FE | FE <- first/only difference; loop still finishes
The decisive insight: had the difference been at i = 0 instead, acc would become non-zero on the first iteration — but the loop would still execute all five iterations. Same iteration count, same time, no leak. Contrast memcmp, which would return at i = 0 and finish far sooner, leaking that the first byte was wrong.
/* WRONG: reintroduces the exact timing leak */
for (size_t i = 0; i < n; i++) {
if (pa[i] != pb[i])
return 1; /* exits early -> time depends on data */
}
return 0;
Why it is wrong: this is just memcmp again. The number of iterations now depends on where the first difference is, so timing reveals the leading-match count.
Corrected: keep the unconditional fold and the single post-loop verdict.
unsigned char acc = 0;
for (size_t i = 0; i < n; i++) acc |= (unsigned char)(pa[i] ^ pb[i]);
return acc;
How to recognise it: any return, break, or continue inside the loop that depends on the bytes is a red flag.
/* WRONG: the branch's outcome is itself data-dependent */
return (acc != 0) ? 1 : 0; /* often fine, but... */
if (acc) log("mismatch at hidden position"); /* DON'T act differently on secret */
A plain acc != 0 verdict is acceptable because it runs after the full loop and reveals only the final answer (which the caller is allowed to know). The danger is doing more secret-dependent work afterwards. Keep post-comparison work uniform.
char acc = 0; /* WRONG on platforms where char is signed */
acc |= a[i] ^ b[i]; /* high-bit values may sign-extend / surprise */
Why it is wrong: a difference byte like 0x80 can be interpreted as negative, and sign-extension during integer promotion can produce confusing values. Corrected: always use unsigned char for the accumulator and for byte access.
const unsigned char *pa = a;
if (a == NULL) return 1; /* WRONG order is harmless here, but... */
acc |= pa[0] ^ pb[0]; /* ...indexing before checking n>0 reads OOB if n==0 */
Corrected: check n == 0 and NULL before any indexing, as in the lesson code.
Compiler stage
error: invalid use of void expression or arithmetic on a pointer to void — you indexed a[i] directly. Cast to const unsigned char * first.-Wall -Wextra about comparison of integers of different signs usually point at mixing int and size_t; make the loop counter size_t.gcc -std=c11 -Wall -Wextra -O2 file.c (or clang). Warnings here are cheap bugs caught early.Runtime stage
valgrind ./a.out or build with -fsanitize=address,undefined to get the exact offending access.n is larger than either buffer, you read out of bounds even though the logic is "correct". Confirm the caller passes the true length.Logic stage
&= instead of |=, or initialised acc to a non-zero value, or never assigned the XOR into acc.+ 1 on a pointer.Questions to ask when it doesn't work
n == 0 and NULL before touching memory?unsigned char and the counter size_t?if/break/return inside the loop?n bytes long?Authorisation & ethics. Everything here is defensive. Practise only on your own code, in a local or containerised lab. Do not use timing measurements against systems you are not explicitly authorised to test. The goal is to write comparisons that cannot be attacked, not to attack anyone.
Threat model for tag comparison
Asset: the secret tag / token the server holds (must stay unknown)
Entry point: attacker-controlled bytes submitted for comparison
Trust bdry: [ network ] --untrusted guess--> [ server compares vs secret ]
Leak vector: response TIME (a side channel), not the response body
Goal: attacker reconstructs the secret one byte at a time via timing
Defence: constant-time compare -> time carries no info about the secret
Memory-safety concerns specific to this code
n. If n exceeds the real length of either buffer, the loop reads out of bounds (undefined behaviour, possible crash or info leak). The caller must pass the correct length; document this.acc must start at 0. An uninitialised accumulator is UB and can yield wrong verdicts.unsigned char (OR/XOR are well-defined, no sign surprises) and size_t for the counter to avoid signed-overflow UB and signed/unsigned comparison bugs.Defensive practices
crypto_verify_* / sodium_memcmp (libsodium), CRYPTO_memcmp (OpenSSL), or timingsafe_bcmp (BSD). Write your own only to learn the pattern.Logging guidance. Log the event ("tag verification failed for request X"), never the secret, the attacker's guess, or the position of the mismatch. Logging "mismatch at byte 4" would hand the attacker exactly the side-channel data you removed.
Where this runs in real systems
Professional best-practice habits
Beginner rules
break/return inside the comparison loop.Advanced rules
explicit_bzero / sodium_memzero) so they don't linger in memory.Objective. Write int ct_memcmp(const void *a, const void *b, size_t n) exactly as specified in this lesson.
Requirements. Return 0 when all n bytes match, non-zero otherwise; runtime must depend only on n. Handle n == 0 (equal) and NULL with n > 0 (not equal) without dereferencing.
Example. ct_memcmp("abc", "abc", 3) == 0; ct_memcmp("abc", "abd", 3) != 0.
Constraints. No break/return/continue inside the loop. Accumulator is unsigned char; counter is size_t.
Hints. Cast void * to const unsigned char *; fold with |= (unsigned char)(pa[i] ^ pb[i]).
Concepts: XOR/OR engine, constant-time property.
Objective. Given three candidate comparison functions, write int is_timing_safe(int which) that returns 1 for the safe one and 0 otherwise, then explain in a comment why each unsafe one leaks.
Requirements. One candidate uses memcmp; one uses a loop with if (a[i]!=b[i]) return 1;; one uses the XOR/OR fold. Identify the safe one.
Hints. Look for any data-dependent early exit.
Concepts: timing side channels, recognising early exits.
Objective. Build int ct_equal(const unsigned char *a, const unsigned char *b, size_t n) that returns 1 for equal, 0 for not equal (note: inverted from ct_memcmp), still in constant time.
Requirements. Derive the boolean from acc without a data-dependent branch on the secret bytes themselves. The final acc != 0 verdict (computed once, after the loop) is allowed.
Input/Output. ct_equal((u8*)"key",(u8*)"key",3) == 1; differing input returns 0.
Hints. A common branch-free trick maps non-zero acc to 1 using bit operations; or simply return acc == 0; after the full loop, which is fine because it runs once at the end.
Concepts: constant-time verdict, avoiding mid-loop branching.
Objective. Write int token_matches(const char *submitted, const char *stored) for NUL-terminated tokens of possibly different lengths, without leaking the length difference.
Requirements. Conceptually: reduce both inputs to a fixed-size value first (e.g. a fixed-length digest stub you define for the lab), then constant-time compare the fixed-size values. Do not branch early on strlen differences in a way that leaks.
Hints. If you compared raw strings of different lengths directly, where would the length leak? Fixing both to the same length before comparing removes it.
Concepts: length leakage, fixed-length compare, threat modelling.
Objective. In a local, isolated program, write a deliberately leaky bad_compare (early-exit) and a ct_memcmp, time many runs of each against inputs that match 0, 1, 2, … leading bytes, and chart how bad_compare's time rises with leading-match count while ct_memcmp's stays flat.
Requirements. Run thousands of iterations and average to beat noise. Label the leaky function with the required warning. Compare the two timing curves and write up what an attacker could infer from each.
Constraints. WARNING: Intentionally vulnerable training example — use only in a local, isolated, authorized lab. Do not deploy. Never time a system you do not own.
Hints. Use a monotonic clock (clock_gettime(CLOCK_MONOTONIC, …)); warm up before measuring; the shape of the curve matters more than absolute numbers.
Concepts: side-channel measurement, mitigation verification, ethics.
memcmp leaks timing, because memcmp exits at the first differing byte. An attacker measures the time and recovers the secret one byte at a time.n — never on where the inputs differ.acc = 0; then for (i<n) acc |= (unsigned char)(a[i] ^ b[i]); then return acc; (0 means equal). XOR detects per-byte differences; OR makes them sticky; the loop never early-exits.const void * parameters cast to const unsigned char *; size_t counter; unsigned char accumulator; no break/return inside the loop; NULL and n == 0 guarded before any dereference.char accumulator, using &= instead of |=, leaking the mismatch position in logs, and comparing different lengths so the length itself leaks.sodium_memcmp, CRYPTO_memcmp, timingsafe_bcmp); hash variable-length inputs to a fixed length before comparing; and log the failure event, never the secret or the mismatch position.