An admin API compares its shared secret with ==; how do you show that timing leak is real and decide the fix?
answer
- vary exactly one thing
- one sub-benchmark per prefix length
- the compiler will delete an unused comparison
- wrong length is the fastest case of all
- a weak local signal is not an all-clear
basics
~20 sWrite a Go benchmark whose inputs share 0, 8, 16 and 31 leading bytes with the secret and see whether the time climbs with the matching prefix. Local noise may hide the effect, and that is not a defence: the constant-time fix is one line, so apply it.
solid answer
~60 sReach for a benchmark before an argument. Build sub-benchmarks over a range of matching prefix lengths, assign the comparison result to a package-level variable so the compiler cannot delete it, run with `-count=10` and compare the distributions rather than single numbers. Two honest outcomes are possible. Often the length difference shows up clearly — a wrong-length input is fastest because the comparison rejects on length first — while the per-byte content difference on a 32-byte secret is under a nanosecond and buried in scheduler noise. That is a measurement limit, not an all-clear: the benchmark measures one process on one machine, and it says nothing about an attacker who can sample millions of requests from the same network. Since replacing `==` with a constant-time compare over two digests is a one-line change with no behavioural cost, exploitability is the wrong argument to have. Then check the rest of the path — the early return when the header is absent, any lookup keyed by the secret, and distinct error strings all leak too.
code
go · 13 linesvar sink bool
func BenchmarkSecretCompare(b *testing.B) {
secret := strings.Repeat("s", 32)
for _, match := range []int{0, 8, 16, 31} {
guess := secret[:match] + strings.Repeat("x", 32-match)
b.Run(fmt.Sprintf("match=%d", match), func(b *testing.B) {
for b.Loop() {
sink = guess == secret
}
})
}
}go deeper
You are not expected to design this measurement. Know the rule it defends: secret comparisons must not return early, so use a constant-time compare rather than ==.
Be able to describe the benchmark shape — sub-benchmarks over matching prefix lengths, a sink variable so the comparison is not optimised away, repeated runs — and why the length case is fastest.
Show judgment about a weak signal. Say plainly that failing to measure a leak locally does not clear it, and that a one-line fix ends the argument faster than any experiment can.
Set how findings like this are triaged: for changes this cheap the team fixes first and measures for education, and reserves exploitability debates for changes with real cost or risk.
## The situation An internal admin API is guarded by a shared secret. The check is a plain `==` against the configured value. A security reviewer flags it as a timing leak. Somebody on the team says it is theoretical. Your job is to settle it with evidence and then make the right call regardless of how the evidence lands. ## Designing the measurement The hypothesis is specific: the running time of the comparison rises with the number of leading bytes the caller got right. So vary exactly that and hold everything else fixed. - Build one candidate per matching prefix length — 0, 8, 16, 31 bytes of the real secret, padded to full length with a filler byte so every candidate has the same length. - Put each in a sub-benchmark so each gets its own timing loop and its own reported ns/op. - Assign the boolean result to a package-level variable. Otherwise the compiler is entitled to notice that nothing observes the comparison and remove it, and you will benchmark an empty loop. - Run with `-count=10` and look at the spread, not at one number. A single run of a sub-nanosecond effect is noise. ## Reading the result honestly There are two effects hiding in the same measurement. **Length.** Go compares string lengths before contents, so an input of the wrong length is rejected fastest of all. This difference is comparatively large and usually visible. It means an attacker can learn the secret's length cheaply — which shrinks the search space before they start on contents. **Contents.** The per-byte effect is far smaller than most people expect. The runtime compares several bytes at a time rather than one, so a 32-byte secret differs by a handful of machine-word comparisons end to end. On a laptop with a busy scheduler you may not resolve it at all. The wrong conclusion to draw from an unresolvable local signal is that the check is safe. Your benchmark is one process, one machine, no adversary, and a few thousand samples. A real attacker chooses the sample count, filters jitter statistically, and may sit on the same host or the same rack — published work has recovered secrets over a local network by exactly this kind of averaging. Absence of a measurable difference in your harness is absence of evidence, not evidence of absence. ## The decision The economics decide this, not the exploitability debate. Replacing the comparison costs one line, changes no behaviour, adds a hash of a 32-byte input per request, and removes an entire class of finding permanently: ```go a := sha256.Sum256([]byte(presented)) b := sha256.Sum256(expected) if subtle.ConstantTimeCompare(a[:], b[:]) != 1 { /* reject */ } ``` A senior engineer who spends a week proving the leak is impractical and then still has to write the same line has spent the week badly. The correct posture to bring to the reviewer is: here is the measurement, here is what it does and does not show, here is the patch, it landed this morning. ## What the benchmark does not cover Fixing the comparison does not finish the job. Walk the whole authentication path looking for anything whose cost or shape depends on secret material: - The early return when the `Authorization` header is missing or malformed is much cheaper than the full path. That distinguishes 'no credential' from 'wrong credential', which is usually acceptable but should be a decision, not an accident. - Any lookup of an expected secret keyed by something the caller supplied has a data-dependent cost and a data-dependent miss path. - Different error messages, different response sizes, or different log volume for different failure modes all leak the same information the timing did. - Logging the presented secret, or including it in an error, defeats every one of these measures at once. ## What good looks like in the answer A method (vary one variable, sub-benchmarks, defeat dead-code elimination, repeat runs), an honest reading of a weak signal, an explicit statement that the fix is cheap enough that exploitability does not gate it, and a sweep of the neighbouring paths that leak the same way.
- The benchmark shows no difference between the prefix lengths. What do you conclude?That your harness cannot resolve it — one machine, a few thousand samples, a scheduler adding noise, and a runtime that compares several bytes at a time. An attacker picks the sample count and can average away jitter. Treat it as unproven rather than safe, and apply the one-line fix anyway.
- Which difference in that benchmark is usually the easiest to see, and why does it matter?The length difference. Go compares string lengths before contents, so an input of the wrong length rejects fastest. It tells an attacker the secret's length, which shrinks the search space before any content probing starts — and it is exactly the branch that comparing two fixed-size digests removes.
- After the comparison is constant-time, what else on that path still leaks?The cheap early return for a missing or malformed header, any lookup keyed by caller-supplied material, and distinct error strings, status bodies or log lines per failure mode. Each of them distinguishes failure causes just as timing did, so decide deliberately which distinctions you are willing to expose.
saying these in an interview costs you the question
- Argues network jitter makes any timing leak unexploitable
- Reads one benchmark run and calls the difference real
- Benchmarks a comparison the compiler eliminated as dead code
- Fixes the compare but leaves an early length check in front of it
- Times a single comparison with time.Now instead of a benchmark
- Declares the path safe without checking error and logging differences