skip to content

Credential Checks

The middleware that reads Authorization, compares the secret in constant time and answers 401, and knows why 403 is a different answer. In Go you write every line of it yourself.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

Why compare an API shared secret with crypto/subtle.ConstantTimeCompare rather than Go's == operator?

level: middleimportance: must knowfreq 61%

answer

  1. how long it takes is data
  2. one of them returns early
  3. extend the guess one byte at a time
  4. the result is an int, not a bool
  5. different lengths short-circuit, so hash first

basics

~20 s

Comparing strings with == stops at the first differing byte, so how long it takes depends on how much of the secret the caller guessed. subtle.ConstantTimeCompare examines every byte and returns 1 for equal or 0 otherwise, in time that does not depend on the contents.

solid answer

~50 s

`==` on strings or `bytes.Equal` on slices is allowed to return as soon as it finds a mismatch, so the time it takes is a function of the secret's contents — an attacker who can measure it can extend a guessed prefix one byte at a time instead of brute-forcing the whole secret. `subtle.ConstantTimeCompare(x, y []byte) int` reads both slices completely and returns `1` when the contents are equal and `0` otherwise, in time that depends only on the length. Two details bite people: it returns an **int**, not a bool, so the call site reads `== 1`; and it returns 0 immediately when the lengths differ, so it hides contents but not length. The clean pattern is to hash both sides with `sha256.Sum256` and compare the two fixed-size digests, which equalises the length as well. For MAC tags, `hmac.Equal` is the same guarantee with a bool result.

code

go · 7 lines
go
var adminSecret = []byte(os.Getenv("ADMIN_SECRET"))

func secretOK(presented string) bool {
	a := sha256.Sum256([]byte(presented))
	b := sha256.Sum256(adminSecret)
	return subtle.ConstantTimeCompare(a[:], b[:]) == 1
}

go deeper

for a junior

Remember the rule and the call: never compare secret material with ==, use subtle.ConstantTimeCompare and check its result against 1. Knowing that much is enough at this level.

for a middle

Explain the mechanism: early return on the first mismatch makes running time a function of the matching prefix, which converts guessing the secret from exponential to linear work.

for a senior

Show that you know the fix is not complete on its own — the length short-circuit, the surrounding lookup and error paths, and logging can all leak after the compare itself is safe.

for a principal

Set the standard the codebase is held to: no data-dependent branch on secret material anywhere in the auth path, enforced by review and by a helper everyone calls, rather than argued case by case.

## The problem being solved An internal admin API is protected by a shared secret the caller presents in a header. The server has the expected secret in memory and has to answer one question: is what arrived the same as what we hold? The obvious code is ```go if presented == adminSecret { // do not do this ``` The correctness of that line is not in doubt. Its *timing* is the problem. ## Why == leaks Go's string comparison first compares lengths, then compares the bytes, and it is free to return the moment two bytes differ. (The runtime compares several bytes at a time, which blunts but does not remove the effect.) So the running time of the check is a function of how many leading bytes the caller got right. A caller who can measure the server's response time can therefore play a game: try every possible first byte, keep the one that was measurably slower, then move to the second byte. That turns an exponential search into a linear one — 32 bytes of secret costs a few thousand probes instead of astronomically many. The same applies to `bytes.Equal`, to `strings.EqualFold`, to a hand-written loop with an early `break`, and to a `map` lookup keyed by the secret. ## What ConstantTimeCompare guarantees ```go func subtle.ConstantTimeCompare(x, y []byte) int ``` It returns `1` if the two slices have equal contents and `0` otherwise, and the time it takes is a function of the **length** of the slices and independent of their **contents**. It does not branch on the data; it accumulates a difference across all bytes and collapses it at the end. Because it never returns early, the byte-at-a-time extension attack has nothing to measure. Two call-site details: - **It returns an int.** `if subtle.ConstantTimeCompare(a, b) == 1 { ... }`. Reviewers should look for a comparison against 1 (or against 0); a wrapper that converts to bool is fine, but the conversion has to happen. - **Unequal lengths return 0 immediately.** That is documented behaviour, and it means the function hides contents but not length. If secret length is itself sensitive, or if you would rather not think about it, do not feed it raw secrets of caller-controlled length. ## The pattern that removes the length question ```go a := sha256.Sum256([]byte(presented)) b := sha256.Sum256(expected) ok := subtle.ConstantTimeCompare(a[:], b[:]) == 1 ``` Both digests are always 32 bytes, so the length branch never fires and the comparison is over fixed-size data. Hashing is cheap relative to an HTTP request, and it also means the raw secret is not the thing being scanned byte by byte. ## Neighbours in crypto/subtle The package is a small toolbox of branch-free primitives: `ConstantTimeByteEq`, `ConstantTimeEq`, `ConstantTimeSelect`, `ConstantTimeCopy`, `ConstantTimeLessOrEq`. For comparing message authentication codes specifically, `crypto/hmac` exposes `hmac.Equal(mac1, mac2 []byte) bool` — the same constant-time property with the bool result you wanted in the first place. Using `bytes.Equal` on a MAC tag is a genuine finding, not a style nit. ## Mistakes that undo the fix - Adding your own `if len(a) != len(b) { return false }` before the constant-time call. You have just reintroduced a branch, and worse, you have made it the first thing that happens. - Comparing the username with `==` and only the password in constant time. The username comparison then leaks which usernames exist. - Looking the expected secret up in a map keyed by the presented one. The lookup's cost and its miss path are data-dependent. - Logging the presented secret, or returning a different error message for a wrong-length secret than for a wrong-content one. The timing side channel is not the only side channel. ## How to frame it in an interview Say what leaks (the position of the first difference), why it matters (byte-at-a-time extraction rather than brute force), what the standard library gives you (`subtle.ConstantTimeCompare`, int result, length short-circuit), and the digest trick that makes the length question go away. Then note that the fix is one line, which is why arguing about exploitability is usually the wrong hill.

  • subtle.ConstantTimeCompare returns 0 immediately when the lengths differ. Does that matter?
    It means the call hides the contents but not the length of the secret. For a fixed-length secret that is usually acceptable; the tidy answer is to hash both sides with `sha256.Sum256` and compare the 32-byte digests, so the length branch can never fire regardless of what the caller sends.
  • Someone adds an explicit length check before the constant-time compare. What do you say in review?
    Reject it. The early `if len(a) != len(b)` is exactly the data-dependent branch the constant-time call was chosen to avoid, and putting it first makes it the cheapest thing to measure. Either compare fixed-size digests or let the library handle length itself.
  • Which standard-library call would you use to compare two HMAC tags?
    `hmac.Equal(mac1, mac2 []byte) bool` from `crypto/hmac`. It is constant-time like `subtle.ConstantTimeCompare` but returns a bool, so it reads naturally at the call site. Using `bytes.Equal` on a tag is a real finding — it can return early on the first differing byte.

saying these in an interview costs you the question

  • Claims == on Go strings is already constant time
  • Thinks ConstantTimeCompare returns a bool
  • Adds an early length check in front of the constant-time compare
  • Uses bytes.Equal to compare a MAC tag
  • Says a timing leak on a compare is theoretical and needs no fix
  • Compares the username with == and only the password in constant time
open as a page

What does (*http.Request).BasicAuth return in Go, and when is its ok value false?

level: juniorimportance: should knowfreq 52%

basics

~20 s

BasicAuth on *http.Request returns username, password and ok. It reads the Authorization header, base64-decodes the credentials and splits them at the first colon. ok is false when the header is missing, is not the Basic scheme, or does not decode.

open as a page

An admin API compares its shared secret with ==; how do you show that timing leak is real and decide the fix?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Write 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.

open as a page

Your Go service checks credentials per route rather than around the whole ServeMux; how do you decide which default is right?

level: principalimportance: nice to knowfreq 31%

basics

~20 s

Decide by what a route added six months from now does on its own. Wrapping the whole mux fails closed and turns the exceptions into one reviewable list. Per-route checks fail open, and are defensible only with a registry and a test proving every path is covered.

open as a page