skip to content

Constant-Time Comparison

Comparing a token or a MAC tag with == or bytes.Equal returns on the first differing byte; subtle.ConstantTimeCompare does not, though it still gives away a length mismatch at once.

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

questions

4

What does Go's hmac.Equal do that bytes.Equal does not, and what does it return?

level: juniorimportance: must knowfreq 45%

answer

  1. one stops early, one never does
  2. same two arguments, same bool result
  3. crypto/hmac, not bytes
  4. bytes.Equal is string(a) == string(b)
  5. hmac.Equal wraps subtle.ConstantTimeCompare

basics

~20 s

hmac.Equal(mac1, mac2 []byte) returns a bool and scans every byte, so its running time does not depend on the contents. bytes.Equal stops at the first differing byte, so its timing reveals how much of the input matched.

solid answer

~40 s

Both take two byte slices and return a `bool`, but they differ in where they stop. `bytes.Equal` is literally `string(a) == string(b)`, which lowers to an optimised memory compare that returns as soon as it hits a differing byte, so its running time is a function of how many leading bytes matched. `hmac.Equal` is a one-line wrapper over `subtle.ConstantTimeCompare(mac1, mac2) == 1`: it ORs the XOR of every byte pair and only then decides, so its time depends on the length, not the contents. Neither one recomputes anything — `hmac.Equal` just compares two MACs you already hold, and despite the package name it will compare any two byte slices. Note also that `==` does not compile on two `[]byte` values, and `reflect.DeepEqual` is both variable-time and slow, so it is not an alternative.

code

go · 8 lines
go
// got is the decoded value from the request; want is what this service computed.
func verify(got, want []byte) bool {
	return hmac.Equal(got, want) // no early exit
}

func verifyBad(got, want []byte) bool {
	return bytes.Equal(got, want) // returns at the first differing byte
}

go deeper

for a junior

Be ready to name the function: hmac.Equal from crypto/hmac, taking two byte slices and returning a bool. Know too that == does not compile on two slices, so some function call is always required here.

for a middle

Explain the mechanical difference. bytes.Equal is string(a) == string(b) and returns at the first differing byte, while hmac.Equal delegates to subtle.ConstantTimeCompare and always walks the full length before deciding.

for a senior

In a pull request you should spot the variable-time compare in a verification path and say what else must change with it: decoding the encoded header value, and rejecting a wrong-length decode as malformed before any comparison runs.

for a principal

Own the convention rather than the call. Deciding that verification lives in one reviewed helper, so no team writes its own comparison, is what keeps this from being relitigated in every service.

## The two functions `bytes.Equal(a, b []byte) bool` reports whether two byte slices have the same length and the same contents. Its entire implementation is `string(a) == string(b)` — a conversion the compiler does not allocate for — which lowers to the runtime's optimised memory compare. That compare checks the lengths first and then walks the two regions a machine word at a time, **returning the moment it finds a difference**. Two 32-byte values that differ in the first byte are settled after one word comparison; two identical ones take four. The verdict is the same either way; the time is not. `hmac.Equal(mac1, mac2 []byte) bool` lives in `crypto/hmac` and is documented as comparing two MACs "for equality without leaking timing information". Its body is a single line: `return subtle.ConstantTimeCompare(mac1, mac2) == 1`. That primitive accumulates `x[i] ^ y[i]` into one byte with `|=` across the whole slice and reduces the accumulator to 1 or 0 only at the very end. There is no early exit, so the running time is a function of the length of the inputs and not of where — or whether — they differ. ## What each returns Both `bytes.Equal` and `hmac.Equal` return a plain `bool`. The `int` result belongs to the lower-level primitive, `subtle.ConstantTimeCompare`, not to `hmac.Equal`; a candidate who says `hmac.Equal` gives back 1 or 0 has the two layers confused. If you want a bool, `hmac.Equal` is the layer that gives you one. ## What neither of them does Neither function recomputes a MAC, looks at a key, checks a timestamp, or validates a length against an algorithm. `hmac.Equal` is *only* a comparison. Everything that produced the two byte slices — reading the body, deriving the expected value, decoding the incoming one — happens before the call and is entirely the caller's problem. It is also worth knowing that `hmac.Equal` is not HMAC-specific in any technical sense. It will happily compare any two byte slices. Its placement in `crypto/hmac` is a naming convention: seeing `hmac.Equal` at a call site tells a reviewer what the two slices are for. ## The alternatives people reach for, and why they are wrong - `sigA == sigB` on two `[]byte` values **does not compile**. Slice types are not comparable in Go; the only legal comparison is against `nil`. That is why every byte-slice comparison in Go goes through a function. - `reflect.DeepEqual(a, b)` compiles and gives the right verdict, but it walks values through reflection: slow, allocating, and with no timing property whatsoever. It is the wrong tool twice over. - `bytes.Compare(a, b)` returns an ordering (`-1`, `0`, `+1`) and also stops at the first difference, so it leaks exactly what `bytes.Equal` leaks. - `bytes.EqualFold` and `strings.EqualFold` apply Unicode case folding. On a binary value that is meaningless, and on hex text it makes `AB` and `ab` equal, which quietly widens what you accept. - Fixed-size arrays such as `[32]byte` *are* comparable with `==`, and that comparison is convenient, but the compiler is free to emit a branching comparison for it, so it carries no content-independence guarantee. ## Getting to two byte slices in the first place A value being checked usually arrives as text in an HTTP request header, hex- or base64-encoded. Decode it before comparing: `hex.DecodeString(s string) ([]byte, error)`, or `base64.StdEncoding.DecodeString` / `base64.RawURLEncoding.DecodeString` depending on the encoding. A decode error is a rejection, not a comparison — there is nothing to compare. Comparing the *encoded text* with `==` on two strings puts you straight back on the ordinary variable-time path. ## Lengths `hmac.Equal` returns `false` immediately when the two slices have different lengths, because the primitive underneath returns 0 immediately in that case. For a fixed algorithm the expected length is a public constant, so nothing sensitive is exposed by that early return. The cleanest structure is nevertheless to check the decoded length against the expected one and reject a mismatch as malformed input, so the comparison itself only ever sees two same-length slices. ## In review The practical takeaway for the person reading a pull request is small and mechanical: in any path that checks an authentication tag, a `bytes.Equal`, a `bytes.Compare`, a `reflect.DeepEqual` or a string `==` is a comment. The replacement is `hmac.Equal` on decoded bytes, and it is a one-line change.

  • Does hmac.Equal only work on HMAC tags?
    No. Its body is `return subtle.ConstantTimeCompare(mac1, mac2) == 1`, so it will compare any two byte slices. Its home in `crypto/hmac` is a naming convention that documents intent at the call site. It does not recompute a MAC, look at a key, or know anything about the algorithm that produced the bytes.
  • The value arrives as a hex string in an HTTP header. What has to happen before hmac.Equal?
    Decode it. `hex.DecodeString` returns `([]byte, error)`, and a decode error is a rejection rather than a comparison. Check the decoded length against the expected size, then pass the bytes and your computed value to `hmac.Equal`. Comparing the hex text with `==` on two strings would be an ordinary variable-time string compare.
  • Why does sigA == sigB not compile when both are []byte?
    Slice types are not comparable in Go; the only legal comparison for a slice is against `nil`. That is why every byte-slice comparison goes through a function such as `bytes.Equal`, `hmac.Equal` or `subtle.ConstantTimeCompare`. Fixed-size arrays like `[32]byte` are comparable with `==`, but that comparison carries no guarantee about content-independent timing.

bytes.Equal is a proofreader who stops at the first typo; hmac.Equal is one who always reads to the end of the page. They reach the same verdict, but only one of them takes the same time doing it.

saying these in an interview costs you the question

  • Says bytes.Equal is fine because comparisons are fast anyway
  • Thinks hmac.Equal returns an int like subtle.ConstantTimeCompare
  • Reaches for reflect.DeepEqual to compare two byte slices
  • Believes hmac.Equal recomputes the MAC or checks the key
  • Uses bytes.EqualFold or strings.EqualFold on a hex value
open as a page

What does subtle.ConstantTimeCompare return, and how does it behave on unequal-length slices?

level: middleimportance: should knowfreq 38%

basics

~20 s

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

open as a page

Your HMAC webhook check calls subtle.ConstantTimeCompare — what still leaks timing?

level: seniorimportance: should knowfreq 28%

basics

~20 s

The comparison is only one step. Whether the signature header is present, whether it decodes, whether the sender's key was found, and how early the middleware returns all take measurably different time before the constant-time compare ever runs.

open as a page

What does subtle.WithDataIndependentTiming do, and on which hardware does it matter?

level: seniorimportance: nice to knowfreq 12%

basics

~20 s

subtle.WithDataIndependentTiming(f func()) runs f with the processor's data-independent-timing mode enabled. On arm64 chips with FEAT_DIT it sets PSTATE.DIT for the duration; on every other architecture it simply calls f. It does not make variable-time code constant-time.

open as a page