Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33723,3 +33723,23 @@ Evidence: PR #107 (https://github.com/QuantumShieldLabs/qsl-protocol/pull/107) m
- Both formatter test shadow copies (`identity_binding.rs:37`, `identity_foundation_contract_na0217d.rs:133`) must be kept in lockstep with the production formatter forever and must remain duplicates rather than imports.
- qsl-desktop's displayed verification code changes when it next bumps its qsc pin; `DESIGN_SPEC_AppendixD.md` and `ui/style.css` go stale at that moment. Desktop pin-bump lane's concern.
- **References:** NA-0669; D605 (`QSL-DIR-2026-07-23-605`, approved 2026-07-23, sha256 `4852f471e7bf10d91bd9a8d95d7e08f6af46b8276d816b081e71a74de9274b73`, 374 lines); operator ruling (A) extending §5a to `tests/identity_foundation_contract_na0217d.rs` for the one lockstep edit; D-1295 (the implementation decision this closes, merged as `ccc6c008` over head `3d32a1c0`, PR #1630); D-1294 (NA-0668, whose standing method this lane discharges by observation); the 2026-07-22 independent audit (findings C-1a, C-4, C-6; C-1b, C-2 and the target-width decision deliberately out of scope); `docs/governance/evidence/NA-0669_as_built.md`; `tests/NA-0669_audit_client_security_fixes_testplan.md`; `docs/ops/ROLLING_OPERATIONS_JOURNAL.md` (2026-07-23 entry + closeout). **Nothing was filed in `docs/ops/IMPROVEMENT_LEDGER.md` — deliberately.**

- **ID:** D-1297
- **Title:** NA-0670 closeout — the last unfixed HIGH of the 2026-07-22 independent audit (C-2) shipped: qsl-server `auth_ok` now compares the bearer token in constant time (SHA-256 both sides, then the client's `hs_ct_eq_32` XOR-fold), closing a remote timing oracle on the one network-exposed component; like NA-0669 and unlike the four docs-only lanes before it, the qsl-server implementation PR changed `src/` so its required `rust` check RAN and its green IS evidence for the behavioural half
- **Date:** 2026-07-23
- **Status:** Accepted
- **Goal IDs:** G4
- **Decision:** Close NA-0670 per **QSL-DIR-2026-07-23-606 (D606)**. The qsl-server implementation (satellite decision **D-0014**) merged as `5235c2bf` (PR #64) over base `b4f86a3c`; the queue returns to `READY=NONE | HIGHEST_NA=0670 | HIGHEST_D=1297`. **THE FIX, IN PLAIN TERMS:** `auth_ok`'s final `provided == token` used `str::eq`, which short-circuits on the first differing byte — a **remote timing oracle** on the one component deliberately exposed to the network, in the low-jitter LAN/tailnet regime where byte-at-a-time statistical amplification is actually practical. It is now `ct_eq_secret(provided, token)`, a private 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 (`diff |= da[i] ^ db[i]`, then `diff == 0`) — the exact shape of the qsc client's `hs_ct_eq_32` (ENG-0003) and consistent with this file's own `route_key_for`, which already hashes the **other** secret (the route token), so the relay now treats both its secrets the same way (the strongest form of the fix, §1b). Hashing normalises both inputs to 32 bytes, so the fold does identical work for every input, **closing the length leak** the audit offered as optional (§2c). The two `return false` guards (missing / malformed `Authorization`) are kept — they branch on attacker-known request shape, not on the secret. **NO dependency change:** `sha2` is already a direct dependency and `use sha2::{Digest, Sha256}` already imported; `subtle` was **deliberately NOT added** (it is present in `Cargo.lock` only via rustls under the reqwest DEV-dependency; a direct use would newly enter the production graph, and — being defined for equal-length slices — would not close the length leak without a length-visible branch; §2b/§2c) — the `Cargo.toml`/`Cargo.lock` diff is verified empty. **NO wire/API/protocol/schema/env change;** the four gated handlers (`server-info`, `push`, `pull`, `pull/ack`) still reject-before-mutation because `auth_ok` remains their first statement. **⚠ THIS LANE'S GREEN IS EVIDENCE, AND THE DISTINCTION IS STATED SO THE CLOSEOUT DOES NOT INHERIT THE WRONG CAVEAT:** the qsl-server implementation PR changed `src/lib.rs`, so `classify_ci_scope` returns runtime/non-docs and the required `rust` check **RAN** — green on `82df531`, the **exact reviewed commit** (run head == PR head == local HEAD, 1m36s). That green is real evidence for the **behavioural** half (auth still accepts the right token and rejects wrong ones, including the same-length case, under CI). It is the inverse of the queue-promotion PR (docs_only, green proved nothing) and of the four docs-only lanes NA-0664/0666/0667/0668. **The constant-time property ITSELF is STRUCTURAL — fixed work over the full 32-byte digest, no data-dependent early return — verified by READING, not by measurement; no timing proof was run or claimed (§3.4 held).** A **same-length wrong-token test** was added (`"topsecreX"` vs `"topsecret"`, both 9 bytes): the pre-existing wrong-token test uses different-length tokens, so `==` rejected on length before comparing a single byte and **passed against the buggy code** — the same-length case is the only behavioural test that exercises the fold, and it proves the fold's ANSWER (reject + no mutation), not its timing. Phase 0 re-verified every §0/§1 anchor at the seating base `b4f86a3c` (`provided == token` at `src/lib.rs:507` exactly; `sha2` imported at `:10`; `route_key_for` at `:486`; the `==` body-sweep confirming `:507` is the **sole** secret comparison — so no second fix is owed, §1c). **FOUR LEDGER FILINGS, NONE FIXED (scope discipline held — one function, nothing else touched):** ENG-0063 (precompute `Sha256::digest(token)` once at `AppState` construction — a refinement riding the `new_with_auth*` constructor chain, §5), ENG-0064 (the two-repo seat leaked qsl-protocol's `CARGO_TARGET_DIR` into the qsl-server seat, defeating the per-repo cache separation NA-0667's registration established — worked around with an explicit pin, not fixed), ENG-0065 (a pre-existing flaky log-capture test, `tests::logs_do_not_contain_raw_channel`, **surfaced not caused** by the required new test at 6-core contention — CI-irrelevant at ≤4 threads), ENG-0066 (qsl-server `TRACEABILITY.md` lapsed for three satellite lanes). Commit identity on the qsl-server implementation commit and on this closeout is the GH007 noreply (`238594419+Tebbens4832@users.noreply.github.com`), author and committer, trailers empty, verified on the object (WF-0029 per-commit override). Queue returns to `READY=NONE`; the operator promotes; the executor cannot self-promote.
- **Rationale:** The last unfixed HIGH of the independent audit is shipped on the single network-exposed component, in the file's own existing idiom (SHA-256 then fold), with no dependency, wire, or behavioural change. Because the implementation PR changed `src/`, its required `rust` check ran and its green is real evidence for the behavioural half — while the constant-time property remains a structural, read-verified claim, never a measured one, holding the near-miss line before there is a near-miss. Unlike NA-0669, this lane DID file — four ledger entries — because each records a real, decision-bearing finding that would otherwise evaporate in a relay (an observation that lives only in a turn record never gets scheduled), and filing them is recording, not a second fix; scope discipline barred doing any of that work inside a one-function lane, and none was done.
- **Security invariants introduced/changed:** The relay's bearer-token comparison is now **constant-time** (fixed work over a 32-byte digest, no data-dependent early return) and **length-independent**, removing a remote timing oracle on the network-exposed shared secret. `auth_ok` accepts and rejects EXACTLY the tokens it did before. No protocol, wire, handshake, ratchet, vault, or cryptographic-primitive change; no marker-vocabulary change; no new error code; no new dependency. A residual **constant per-deployment** timing offset from re-hashing the token on each request remains (proportional to the token's block count, not per-guess and not content-revealing); it is filed as ENG-0063, not folded in. This closeout ships governance records only.
- **Alternatives considered:**
- **`subtle::ConstantTimeEq` on the raw bytes.** Declined (§2b/§2c) — it would newly enter the production dependency graph, and being defined for equal-length slices it would not close the length leak without a length-visible branch; hashing-first closes it for free and yields the `hs_ct_eq_32` idiom the file already uses.
- **HMAC with a random per-process key.** Declined (§2d) — its random key defends against precomputation, but an attacker who already holds a candidate token can simply send it; plain SHA-256 delivers the property actually needed (comparison time independent of matching-prefix length) with no added machinery.
- **Precomputing the token digest at `AppState` construction.** Deferred and filed as ENG-0063 — it removes the secret from the per-request path and erases the block-count offset, but touches the multi-constructor `new_with_auth*` chain, correctly out of a one-function lane.
- **Filing nothing (the NA-0669 posture).** Declined — the four findings are real and decision-bearing; recording them at closeout is the correct home and does not reopen scope discipline.
- **Implications for spec/impl/tests:**
- Queue at `READY=NONE | HIGHEST_NA=0670 | HIGHEST_D=1297`; helper `READY_COUNT 0` with exit 2 is CORRECT at `READY=NONE` (`--allow-nonready-count` not passed) and is not a failure.
- qsl-server records satellite decision **D-0014** and a `TRACEABILITY.md` row; the required `rust` check ran green on the implementation PR (#64, head `82df531`, merged `5235c2bf`).
- No qsl-desktop, qsc, or protocol/spec change. The 2026-07-22 audit finding **C-2 is now closed**, and with it the last audit HIGH; the route-token path (`route_key_for`) is named as evidence, not a target, and is byte-unchanged.
- Four new `IMPROVEMENT_LEDGER.md` entries (ENG-0063…ENG-0066), all `open`, none fixed by this lane.
- **References:** NA-0670; D606 (`QSL-DIR-2026-07-23-606`, approved 2026-07-23, sha256 `ef3bd063f936564935b86e806e459e2bc93be6a4b52fdba685611e43e8a177ca`, 284 lines); D-0014 (qsl-server satellite decision, merged as `5235c2bf`, PR #64 over base `b4f86a3c`); the 2026-07-22 independent audit finding **C-2** (the last unfixed HIGH, and the only one outside the spine); qsl-protocol **ENG-0003** (client `hs_ct_eq_32`); this closeout's four filings **ENG-0063 / ENG-0064 / ENG-0065 / ENG-0066**; `docs/governance/evidence/NA-0670_as_built.md`; `tests/NA-0670_qsl_server_c2_constant_time_bearer_testplan.md`; `docs/ops/ROLLING_OPERATIONS_JOURNAL.md` (2026-07-23 NA-0670 closeout entry).
7 changes: 4 additions & 3 deletions NEXT_ACTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ Goals: G4 (primary), drives G1–G3 delivery

## LIVE QUEUE

`STATE: READY=NA-0670 | HIGHEST_NA=0670 | HIGHEST_D=1296 | BACKLOG_SOURCE=docs/ops/IMPROVEMENT_LEDGER.md`
<!-- prior: STATE: READY=NONE | HIGHEST_NA=0669 | HIGHEST_D=1296 (NA-0670 promoted for D606; this promotion PR) -->
`STATE: READY=NONE | HIGHEST_NA=0670 | HIGHEST_D=1297 | BACKLOG_SOURCE=docs/ops/IMPROVEMENT_LEDGER.md`
<!-- prior: STATE: READY=NA-0670 | HIGHEST_NA=0670 | HIGHEST_D=1296 (NA-0670 closed for D606 at D-1297; this closeout PR) -->
<!-- NA-0670 (constant-time bearer-token comparison in the relay, D606) DONE 2026-07-23 (D-0014 qsl-server implementation merged as 5235c2bf via PR #64 over base b4f86a3c; D-1297 this spine closeout; result class QSL_SERVER_C2_CONSTANT_TIME_BEARER_PASS): ONE FUNCTION, AND ITS GREEN CARRIES INFORMATION -- auth_ok's `provided == token` (str::eq, short-circuits on the first differing byte, a remote timing oracle on the network-exposed shared bearer token) became ct_eq_secret, which SHA-256s both sides to a fixed 32-byte digest and folds them with the client's hs_ct_eq_32 XOR-accumulate shape (ENG-0003) over all 32 bytes with no data-dependent early return -- the same idiom the file already uses for its OTHER secret in route_key_for, so the relay now treats both secrets alike. Hashing normalises both inputs to 32 bytes, closing the length leak for free. NO dependency change (sha2 already direct; subtle deliberately NOT added -- it is dev-only via rustls under the reqwest dev-dep, and Cargo.toml/Cargo.lock diff is empty); NO wire/API/protocol/schema/env change; the two non-secret Authorization-header guards kept; the four gated handlers (server-info/push/pull/pull-ack) still reject-before-mutation because auth_ok stays their first statement. Added the same-length wrong-token test ("topsecreX" vs "topsecret", both 9 bytes) -- the only behavioural test exercising the fold, since the pre-existing wrong-token test differs in LENGTH and passed against the buggy ==. ⚠ THE CONTRAST: unlike the docs_only promotion/closeout PRs around it, the qsl-server implementation PR changed src/lib.rs, so classify returns runtime/non-docs, the required `rust` check RAN, and its GREEN on 82df531 (the EXACT reviewed commit; run head == PR head == local HEAD) IS EVIDENCE for the behavioural half -- the constant-time property itself is STRUCTURAL, read-verified, NOT measured (no timing proof run or claimed; §3.4 held). Phase 0 re-verified every §0/§1 anchor at the seating base b4f86a3c (provided == token at src/lib.rs:507 exactly; sha2 imported at :10; route_key_for at :486; the == body-sweep confirming :507 the SOLE secret comparison -- no second fix owed, §1c). FOUR LEDGER FILINGS, NONE FIXED: ENG-0063 (precompute Sha256::digest(token) at AppState construction -- refinement riding the new_with_auth* chain, §5), ENG-0064 (the two-repo seat leaked qsl-protocol's CARGO_TARGET_DIR into the qsl-server seat, which classified the inherited value as "explicit" and preserved it, so qsl-server built into qsl-PROTOCOL's target dir and its registered per-repo cache went unused -- worked around with an explicit CARGO_TARGET_DIR pin, not fixed; the first real exercise of the two-repo seat path NA-0667 enabled), ENG-0065 (a pre-existing flaky log-capture test, tests::logs_do_not_contain_raw_channel, SURFACED not caused by the required new test at 6-core contention -- 0/20 pristine vs 8/25 with the change, always that same unrelated test; 0/30 at RUST_TEST_THREADS=2 and =4, so CI's 2-vCPU runner is reliably green), ENG-0066 (qsl-server TRACEABILITY.md lapsed for three satellite lanes -- ends at NA-0012 while D-0011/0012/0013 have no rows; only the NA-0670 row was added). See D-1297, docs/governance/evidence/NA-0670_as_built.md, tests/NA-0670_qsl_server_c2_constant_time_bearer_testplan.md. STOPPED AT THE OPEN CLOSEOUT PR -- the executor cannot self-promote; queue returns to READY=NONE. -->
<!-- prior: STATE: READY=NA-0669 | HIGHEST_NA=0669 | HIGHEST_D=1294 (NA-0669 promoted for D605; that promotion PR) -->
<!-- NA-0669 (the three client security fixes of the 2026-07-22 independent audit, D605) DONE 2026-07-23 (D-1295 implementation merged as ccc6c008 over head 3d32a1c0; D-1296 this closeout; result class AUDIT_CLIENT_SECURITY_THREE_FIXES_PASS): THREE SHIPPED FIXES, IN PLAIN TERMS -- C-1a takes the verification code from 44 to 64 bits by STRIPPING THE CONSTANT QSCFP- PREFIX BEFORE TRUNCATING (the alphanumeric filter had dropped the prefix's hyphen but kept its five letters, so five of sixteen displayed characters were constant and every code ever shown began QSCF-P); C-6 adds the PARENT-DIRECTORY FSYNC THAT vault_init_core ALREADY HAD to write_vault_atomic after its rename, closing a crash window in which a power loss could lose the DIRECTORY ENTRY -- and thus the whole store -- while the tmp file's contents were durable (three callers: secret_set, secret_set_with_passphrase, persist_session, i.e. every steady-state vault mutation, and it was the crate's ONLY rename-then-nothing site); C-4 REJECTS non-UTF-8 passphrase files (String::from_utf8) instead of running them through from_utf8_lossy, which SILENTLY DISCARDED ~44% of their entropy (measured H=4.500 bits of 8 per random byte; all 128 bytes 0x80-0xFF collapse to U+FFFD), so head -c 32 /dev/urandom > pass.txt yielded a ~144-bit vault the operator believed was 256-bit. ⚠ THIS LANE'S GREEN IS EVIDENCE -- AND IT IS THE EXACT INVERSE OF THE FOUR LANES BEFORE IT (NA-0664/0667/0668 and NA-0666), whose docs_only=true diffs SKIPPED both full suites so their greens proved nothing: this lane changed src/, classify_ci_scope returned runtime_critical, BOTH FULL SUITES AND THE FORMAL PROOFS RAN RATHER THAN SKIPPED (ci-4a..4d, ci-4d-dur, formal-proverif-composition, formal-scka-model, suite2-vectors, qsc-adversarial-*, macos build -- 34 checks green, 2 skips being the known NA-0633 PR-skips), AND THE SUITE WAS DEMONSTRATED TO REJECT A REAL DEFECT BEFORE IT WAS TRUSTED TO ACCEPT: the first full cargo test -p qsc went RED on a THIRD COPY OF THE FORMATTER the census missed -- shadow #2, verification_code_from_fingerprint at tests/identity_foundation_contract_na0217d.rs:133, named WITHOUT the format_ prefix so every symbol-name sweep (the directive's and the executor's Phase-0 caller-sweep) missed it; only a BODY sweep for the Crockford constant finds all three copies. It was fixed under operator ruling (A) with the identical lockstep prefix-strip, duplicate kept, assertion unchanged, and the re-run completed clean (434 passed, 0 failed, cargo exit 0). ROOT-CAUSE LESSON, recorded not filed: hunt duplicate implementations by a DISTINCTIVE BODY FRAGMENT, NOT THE SYMBOL NAME -- names diverge, bodies do not -- the same class as NA-0668's costume set, an instrument pointed slightly off the question. NO FOURTH COPY: qsl-desktop (mirror main 02cc9b9, body-swept) holds none, calls the qsc symbol and pins no format literal, so its CI will not break on the pin bump. OWED, CARRIED FORWARD TO THE VAULT WORK (operator §9.7) AND NONE OF THEM A VAULT-FORMAT CHANGE -- correcting the D-1295 "vault-format lane" phrasing: (1) the C-1 verification-code TARGET-WIDTH decision (20/24, to reach beyond 64 bits), (2) the C-1b CHECKSUM TRANSPOSITION weakness, (3) C-4's RAW-BYTES form (the uncontained ~10-site pipeline change deferred here), (4) the TRAILING-NEWLINE STRIPPER (a while-loop self-consistent at init and unlock, so any fix must move both together); and identity_pin_matches_seen:562 STAYS UNTOUCHED per F1(a), to be ruled together with the width decision. C-2 (constant-time bearer compare) remains a separate qsl-server lane. NOTHING NEW FILED IN IMPROVEMENT_LEDGER.md -- observations went to the operator relay, scope discipline being the lane's second deliverable. Queue returns to READY=NONE; the operator promotes; the executor cannot self-promote. -->
<!-- prior: STATE: READY=NONE | HIGHEST_NA=0668 | HIGHEST_D=1294 (NA-0669 promoted for D605; this promotion PR) -->
Expand Down Expand Up @@ -36208,7 +36209,7 @@ Begins at D-1295. TWO PRs, two decisions: D-1295 implementation, D-1296 closeout

### NA-0670 — Constant-time bearer-token comparison in the relay (D606, CROSS-REPO, SHIPPING CODE): fix audit C-2 — replace `auth_ok`'s short-circuiting `provided == token` with a length-independent constant-time check (SHA-256 both sides, then the in-house `hs_ct_eq_32` XOR-fold), on the one component deliberately exposed to the network — ITS `rust` CHECK RUNS AND ITS GREEN IS EVIDENCE

Status: READY
Status: DONE
Goals: G4
Wire/behavior change allowed? NO observable wire/API/protocol/schema/env/behaviour change — `auth_ok` accepts and rejects EXACTLY the tokens it did; only the internal byte-comparison method changes (first-differing-byte short-circuit → fixed-width XOR-fold over a 32-byte digest). No dependency or lockfile motion (`sha2` is already a direct dep and already imported; `subtle` MUST NOT be added — it is dev-only in the lock).
Crypto/state-machine change allowed? NO protocol/handshake/vault crypto or state-machine change. SHA-256 (already a direct dependency) is used purely as a length-normaliser for a LOCAL constant-time comparison; no key, no wire byte, no stored-format byte changes.
Expand Down
Loading