What does subtle.ConstantTimeCompare return, and how does it behave on unequal-length slices?
answer
- not a bool
- one branch only, and not on content
- zero does not mean equal here
- lengths differ, answer comes back at once
- test the result with == 1
basics
~20 ssubtle.ConstantTimeCompare(x, y []byte) returns an int: 1 if the slices have equal contents, 0 otherwise. If the lengths differ it returns 0 immediately, so it hides which bytes differ but not that the lengths do.
solid answer
~40 s`subtle.ConstantTimeCompare(x, y []byte) int` returns 1 for equal contents and 0 otherwise — an `int`, not a `bool`, so the correct test is `== 1`. The int is deliberate: the whole `crypto/subtle` package is built to avoid branching on secret-dependent values, and the same 0/1 convention feeds `subtle.ConstantTimeSelect(v, x, y)` and `subtle.ConstantTimeCopy(v, x, y)`. Internally it ORs `x[i] ^ y[i]` across the whole slice and reduces the accumulator only at the end, so the running time is a function of the length rather than the contents. The one branch it does take is on length: unequal lengths return 0 straight away, which is why the timing guarantee only covers same-length inputs. Two empty slices return 1, which is worth remembering when a missing header decodes to an empty slice.
code
go · 5 lines// got is the decoded value from the request; want is what this service computed.
if subtle.ConstantTimeCompare(got, want) != 1 {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}go deeper
Remember the shape before the subtlety: two byte slices in, an int out, and the only value meaning equal is 1. If you would rather have a bool at the call site, use hmac.Equal instead.
Explain the loop. It ORs the XOR of each byte pair across the whole slice and reduces only at the end, so time tracks length. Then explain the single length branch and why it returns 0 before the loop.
Be ready to say what the length short-circuit does and does not cost you, and to insist that a decoded value of the wrong length is rejected during parsing rather than leaving that job to the comparison's early return.
The judgment worth owning is where an int-returning primitive is allowed to appear at all. Confining it to a reviewed helper, rather than letting it spread through handlers, is what keeps an inverted test from shipping.
## The signature and the contract ``` func ConstantTimeCompare(x, y []byte) int ``` The documented contract is precise and worth quoting almost verbatim: it returns 1 if the two slices have equal contents and 0 otherwise; the time taken is a function of the *length* of the slices and is independent of their *contents*; and if the lengths do not match it returns 0 immediately. Three separate facts are packed into that, and interviewers probe all three. ## Fact one: it is an int, not a bool The correct test is `subtle.ConstantTimeCompare(a, b) == 1`. Go has no truthiness, so writing `if subtle.ConstantTimeCompare(a, b) { ... }` does not compile and the mistake announces itself. The dangerous mistake is subtler: `== 0` reads naturally to anyone whose reflexes come from C's `strcmp` or from `bytes.Compare`, where zero means "the same". Here zero means "different". An inverted test compiles cleanly, type-checks cleanly, and accepts exactly the inputs it was written to reject — including every length mismatch, which also returns 0. It is a bug the compiler cannot help you with, which is the argument for using `hmac.Equal` (a `bool`-returning wrapper over exactly this call) wherever you are comparing an authentication tag. Why an int at all? Because `crypto/subtle` exists to let you write code that does not branch on secret-dependent values, and a 0/1 int is the currency the rest of the package trades in. `subtle.ConstantTimeSelect(v, x, y int) int` returns `x` when `v == 1` and `y` when `v == 0`. `subtle.ConstantTimeCopy(v int, x, y []byte)` copies `y` into `x` when `v == 1` and leaves `x` alone when `v == 0`. Both take exactly the value this function produces. A `bool` return would push every caller back toward an `if` over a secret. ## Fact two: how it achieves content-independence The implementation is a plain loop: ``` var v byte for i := 0; i < len(x); i++ { v |= x[i] ^ y[i] } ``` Equal bytes XOR to zero, so `v` stays zero exactly when every pair matched. The loop always runs to the end — there is no `break`, no early `return` — and only afterwards is `v` reduced to 1 or 0 by a byte-equality helper that is itself written without a branch. That is the whole trick, and it is why the timing depends on how long the inputs are but not on where they diverge. ## Fact three: the one branch, on length Before the loop there is a genuine `if len(x) != len(y) { return 0 }`. The function is not constant-time with respect to length and does not claim to be. In practice this is fine and deliberate: for a chosen algorithm the expected size is a public constant — 32 bytes for a SHA-256-based tag, for instance — so an observer who learns that a wrong-length input was rejected quickly has learned nothing they did not already know. What matters is that you understand the boundary of the guarantee rather than assuming it covers everything. The cleaner structure, and the one to argue for in review, is to validate the length as part of parsing: decode the incoming value, reject anything whose decoded length is not the expected size as malformed input, and only then compare. The function's early return then never fires in normal operation. ## The empty-slice case Two empty slices have equal lengths, the loop body never executes, `v` stays zero, and the function returns **1**. `subtle.ConstantTimeCompare(nil, []byte{})` is 1. A `nil` slice and a zero-length non-nil slice are indistinguishable to it, because it looks only at length and contents. This matters when a missing header decodes to an empty slice and the value you are comparing against could also be empty for some reason — an uninitialised field, a helper that returned early. The comparison will report a match. Reject empty input during parsing rather than relying on the comparison to catch it. ## When to call it directly If you are comparing an authentication tag, prefer `hmac.Equal`: it is the same computation, returns a `bool`, and is impossible to invert by accident. Call `subtle.ConstantTimeCompare` directly when you need the integer to feed one of the package's other primitives, or when you are comparing something that is not a MAC and the `hmac` name would mislead a reader. The package documentation opens with a warning that is not decoration: these functions "are often useful in cryptographic code but require careful thought to use correctly". The int return is the first place that thought is required.
- Why does it return an int rather than a bool?Because `crypto/subtle` is written so callers need not branch on secret-dependent values, and a 0/1 int is what the rest of the package consumes. `subtle.ConstantTimeSelect(v, x, y)` and `subtle.ConstantTimeCopy(v, x, y)` both take exactly that `v`. A bool return would push callers straight back into an `if` over a secret.
- Is it a problem that the length comparison is not constant-time?Rarely. For a chosen algorithm the expected size is a fixed, public constant, so an observer learns nothing from a fast rejection of a wrong-length input. The disciplined pattern is to treat a decoded value of the wrong length as malformed during parsing and reject it there, so the function's early return is never the thing protecting you.
- A reviewer sees `if subtle.ConstantTimeCompare(a, b) == 0 { accept() }`. What is wrong?The sense is inverted: 0 means the contents differ. Because the function returns an `int` rather than a `bool`, the compiler cannot catch it, so the code accepts precisely the requests it was written to reject — including every length mismatch, which also yields 0. Using `hmac.Equal` instead makes the mistake unrepresentable.
- What does it return for two empty byte slices?1. Both have length zero, so the accumulating loop never runs, the accumulator stays zero, and the function reports equality. `nil` and `[]byte{}` are indistinguishable to it. If an absent header can decode to an empty slice, reject that during parsing rather than expecting the comparison to fail.
saying these in an interview costs you the question
- Thinks 0 means equal, as in strcmp or bytes.Compare
- Assumes it returns a bool and assigns it to an ok variable
- Claims it panics when the slices have different lengths
- Believes the length check is also constant-time
- Says two empty slices are reported as unequal