Before and after#
You have measured the leak and you know which line causes it. Now write the fix. How will you know it is one?
The function, as written#
for i in 0 .. len(secret):
if given[i] != secret[i]:
return False
return TrueWeek 4 measured this. Mean time rises with the number of leading bytes that match, and 10,000 calls make the trend unmistakable.
The rewrite#
diff = 0
for i in 0 .. len(secret):
diff |= given[i] ^ secret[i]
return diff == 0Every byte is read. No branch depends on the secret. The loop count is a function of the length, which is not the secret.
Three series, same axes#
The middle line is the whole lecture#
Its slope is a fifth of the original’s. It is faster than the correct rewrite. It still leaks — it just needs more samples.
A rewrite that lowers the slope buys the attacker’s patience, not the defender’s safety.
Three rules, in order of how often they are missed#
- No secret-dependent branch.
- No secret-dependent memory address — Bernstein, 2005: a table lookup indexed by a secret leaks through the cache even with no branch in sight.
- No compiler left free to reintroduce either.
What the rules cannot reach#
Week 4’s extra reductions and week 3’s power correlation are not control flow. There is no branch to remove.
Blinding closes those: randomise the input so the data-dependent quantity is no longer a function of the secret alone. Different countermeasure, different cost.
Reading the code proves nothing#
The compiler is entitled to reintroduce a branch you removed, and does.
Verification here means measuring the rewrite the same way you measured the original — which is why this Bench reuses week 4’s harness rather than supplying a new one.
Name what you made worse#
The correct rewrite is slower on average and harder to read than the code it replaces. Both are true and neither is a reason not to ship it.
The Bench asks for both timing plots and one honest sentence about the cost.