# `Attesto.SecureCompare`
[🔗](https://github.com/XukuLLC/attesto/blob/v2.0.1/lib/attesto/secure_compare.ex#L1)

Result-independent comparison of two binaries.

Used wherever an attacker-controlled value is checked against a secret
or a derived digest (a DPoP `ath`, a PKCE challenge) and a
short-circuiting `==` would leak information through timing.

"Result-independent" rather than "constant-time": how long `equal?/2`
takes does not depend on HOW the operands differ - whether the length was
wrong, or a prefix matched - but it does depend on their total size, since
both are hashed. See `equal?/2` for what that does and does not withhold.

# `equal?`

```elixir
@spec equal?(binary(), binary()) :: boolean()
```

Returns `true` iff `a` and `b` are byte-identical, without the
*comparison* revealing whether they were the same length.

`:crypto.hash_equals/2` requires equal-length inputs, so a length guard
is unavoidable somewhere. Guarding with `byte_size(a) == byte_size(b)`
before it short-circuits, and the time to answer then separates "wrong
length" from "right length, wrong bytes" - a distinction that tells an
attacker probing a secret when to stop varying length and start varying
content. Hashing both operands to 32 bytes removes it: the comparison is
over equal lengths by construction and takes the same path either way.

## What this does not do

It is **not** constant-time in the total size of its inputs. SHA-256
reads every byte, so a call with two 100 KB operands takes materially
longer than one with two 1-byte operands. What is withheld is the
*result-dependent* signal - whether a guess was the right length, or
matched to some prefix - not the fact that a large value was compared.
A caller who needs the size itself hidden must pad or cap before calling.

Callers should still validate shape first where they can, and most here
do: `Attesto.PKCE.verify/3` gates on `Attesto.Thumbprint.valid?/1`,
which requires an exact byte size, so its operands are fixed-length
before this is reached. `Attesto.DPoP`'s `ath` comparison does not - the
presented value is an arbitrary-length claim from the proof - which is
precisely the case that benefits.

The final byte comparison runs only when the digests already match, which
IS a result-dependent branch - an equal pair does that extra work and an
unequal pair does not. It is deliberate and it leaks nothing useful:
reaching it means the caller already supplied the correct value (or a
SHA-256 collision), so the timing difference separates "right" from
"wrong", which the answer itself already reveals. What is withheld is the
distinction among WRONG inputs - the one an attacker probes with.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
