NA-0670: constant-time bearer-token comparison in the relay (audit C-2) - #64
Merged
Conversation
Replace `provided == token` (str::eq, which short-circuits on the first
differing byte -- a remote timing oracle on the network-exposed shared
bearer token) with a new private `ct_eq_secret` helper that reduces both
sides to a fixed 32-byte SHA-256 digest and folds them with an
XOR-accumulate loop over all 32 bytes. This is the same shape as the qsc
client handshake's hs_ct_eq_32 (ENG-0003) and consistent with this file's
existing route_key_for, which already hashes the route token. Hashing
first normalizes both inputs to 32 bytes, so the fold does identical work
for every input, closing the length leak as well. The two non-secret
`return false` guards (missing/malformed Authorization header) are kept --
they branch on attacker-known request shape, not on the secret.
No new dependency: sha2 is already a direct dependency; subtle stays
dev-only (rustls under the reqwest dev-dependency) and is not added to the
production graph. No wire, API, protocol, schema, or env change.
Add auth_enabled_wrong_token_same_length_401_no_mutation ("topsecreX" vs
"topsecret", both 9 bytes): the pre-existing wrong-token test differs in
length, so `==` rejected before comparing a byte and passed against the
buggy code; the same-length case is the only behavioural test exercising
the fold.
Fixes the last unfixed HIGH (C-2) from the 2026-07-22 independent audit.
Governance: qsl-protocol NA-0670 / D606 / D-1297. Records D-0014.
Tebbens4832
force-pushed
the
na0670-c2-constant-time-bearer
branch
from
July 23, 2026 22:07
88cf678 to
82df531
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fixes the last unfixed HIGH (C-2) from the 2026-07-22 independent audit — the only one outside the governance spine. One function:
auth_okinsrc/lib.rsno longer usesprovided == token(str::eq, which short-circuits on the first differing byte — a remote timing oracle on the network-exposed shared bearer token). Both sides are reduced to a fixed 32-byte SHA-256 digest and folded with an XOR-accumulate loop over all 32 bytes in a new privatect_eq_secrethelper — the same shape as the qsc client'shs_ct_eq_32(ENG-0003) and consistent with this file's existingroute_key_for, which already hashes the other secret (the route token). Hashing first normalizes both inputs to 32 bytes, so the fold does identical work for every input — this also closes the length leak.The two non-secret
return falseguards (missing / malformedAuthorizationheader) are retained: they branch on attacker-known request shape, not on the secret.Not changed
sha2is already a direct dependency;subtlestays dev-only (rustls under the reqwest dev-dependency) and is deliberately not added to the production graph.Cargo.toml/Cargo.lockdiff is empty.auth_okremains the first statement of every gated handler, so reject-before-mutation is undisturbed.Tests
Adds
auth_enabled_wrong_token_same_length_401_no_mutation("topsecreX"vs"topsecret", both 9 bytes). The pre-existing wrong-token test differs in length, so==rejected before comparing a byte and passed against the buggy code; the same-length case is the only behavioural test that exercises the fold. It proves the fold returns the right answer (reject + no mutation) — the constant-time property is structural and read-verified, not a measured claim.Why this PR's green is evidence
Unlike the docs-only satellite/governance PRs around this lane — whose green proves nothing because the required
rustcheck has nosrc/to exercise — this PR changessrc/lib.rs, so the requiredrustcheck compiles and runs the test suite, including the new same-length reject. A green here is real evidence for the behavioural half of acceptance. (The constant-time property itself remains a structural claim verified by reading, per the directive; no timing measurement is claimed.)Governance
qsl-protocol NA-0670 / QSL-DIR-2026-07-23-606 (D606) / D-1297. Records qsl-server Decision D-0014.
🤖 Generated with Claude Code