mirror of
https://github.com/ruvnet/RuView
synced 2026-07-22 17:23:19 +00:00
fix(engine security): close witness domain-separation collision in governed-trust cycle + prove privacy monotonicity (#1074)
* fix(engine): length-prefix witness fields to close domain-separation collision The BLAKE3 trust witness concatenated model_version, calibration_version, and privacy_decision boundary-to-boundary, with the variable-length evidence list lacking an explicit count. A string straddling a field boundary (e.g. a per-room adapter id absorbing the leading bytes of the calibration epoch, or a model_version absorbing a trailing evidence ref) collided with a different trust decision — silently un-distinguishing two distinct privacy-relevant inputs and defeating the ADR-137 tamper/drift audit guarantee. model_version is operator-influenceable via the adapter id (ADR-150 §3.4), so the ambiguity was reachable. Fix: domain-tag the hash and length-prefix every field (8-byte LE length), plus an explicit evidence count. Pinned by two fails-on-old tests: witness_distinguishes_model_calibration_boundary and witness_distinguishes_evidence_model_boundary. Co-Authored-By: claude-flow <ruv@ruv.net> * test(engine): pin privacy monotonicity, fail-closed boundaries; de-magic constants Review hardening for the governed-trust cycle (no behavior change): - forced_contradiction_never_relaxes_class: property test over all 5 privacy modes proving a forced contradiction only ever raises the emitted class byte (more restrictive) and a clean cycle emits exactly the base class — the ADR-141/120 information-only-removed invariant. - empty_cycle_fails_closed: a zero-frame cycle errors (fusion NoFrames), emits no SemanticState, and does not advance the cycle counter. - single_node_cycle_is_well_formed: characterizes the n=1 boundary (no mesh, no directional, base class, witness still emitted) — documents single-node sensing as a valid non-demoting mode, not a bypass. - De-magicked the engine-construction literals (coherence accept gate, ADR-143 SLAM discovery + static-anchor thresholds) into named documented consts, value-identical, pinned by engine_constants_match_prior_values. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(engine-review): record witness domain-separation fix + monotonicity clean bill CHANGELOG [Unreleased] Security entry and review notes appended to ADR-137 (witness domain-separation fix) and ADR-141 (privacy monotonicity confirmed clean over all 5 modes, fail-closed boundaries pinned). Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -495,3 +495,34 @@ Rejected. `ViewpointFusionEvent` (viewpoint/fusion.rs lines 183–219) is an int
|
||||
**Integration glue -- not yet on the live path:** emission of `CalibrationIdMismatch` / `DriftProfileConflict` / `PhaseAlignmentFailed` once `calibration_id` propagation and the phase-align convergence signal are threaded onto frames; the BFLD witness record emitted on privacy demotion.
|
||||
|
||||
**Trust contribution:** sensor *agreement made explicit* -- fusion records the evidence it relied on, and any disagreement automatically tightens the downstream privacy class.
|
||||
|
||||
---
|
||||
|
||||
## Witness Integrity Review (2026-06-14) — domain-separation fix
|
||||
|
||||
A beyond-SOTA security review of `wifi-densepose-engine` (the composition root
|
||||
that builds the §2.7 trust witness in `witness_of`) found a real **witness
|
||||
domain-separation gap**, now fixed.
|
||||
|
||||
**Finding (witness-gap, HIGH).** `witness_of` concatenated `model_version`,
|
||||
`calibration_version`, and `privacy_decision` boundary-to-boundary, and the
|
||||
variable-length `evidence` list carried no explicit count. A string straddling a
|
||||
field boundary therefore collided with a *different* trust decision —
|
||||
e.g. a per-room adapter id (ADR-150 §3.4, operator-influenceable) that absorbs
|
||||
the leading bytes of the calibration epoch (`model="…cal:00a"`, `cal="b"`)
|
||||
produces the **same** witness as `model="…"`, `cal="cal:00ab"`. Two distinct
|
||||
privacy-relevant input tuples → one witness defeats the "any privacy-relevant
|
||||
delta → different witness" guarantee this ADR's §2.7 witness exists to provide.
|
||||
|
||||
**Fix.** The witness now (a) prepends a domain tag `ruview.engine.witness.v1`,
|
||||
(b) writes an explicit 8-byte evidence count, and (c) **length-prefixes every
|
||||
field** (8-byte LE length ‖ bytes), so field framing is unambiguous regardless
|
||||
of contents. This is a witness-layout change (all prior witness bytes are
|
||||
invalidated by design); downstream consumers only assert witness *relationships*
|
||||
(`assert_ne`/`assert_eq` across runs), not absolute bytes, so nothing breaks.
|
||||
|
||||
Pinned by `witness_distinguishes_model_calibration_boundary` and
|
||||
`witness_distinguishes_evidence_model_boundary` (both fail on the old
|
||||
concatenation). Witness **determinism** was reviewed and confirmed clean: no
|
||||
HashMap iteration and no float formatting feed the hash (floats appear only in
|
||||
the `SemanticState` statement, which is outside the witness).
|
||||
|
||||
@@ -599,3 +599,33 @@ Per ADR-028/ADR-010, three rows are added to the witness log:
|
||||
**Integration glue -- not yet on the live path:** wiring the registry into `PrivacyGate` class transitions, the MQTT discovery payload, and a read-only Home Assistant diagnostic entity exposing the active mode + proof hash.
|
||||
|
||||
**Trust contribution:** the *policy spine* -- privacy posture is a tamper-evident, auditable chain rather than a checkbox; an operator's mode choice actively governs whether identity data may even exist.
|
||||
|
||||
---
|
||||
|
||||
## Privacy Monotonicity Review (2026-06-14) — confirmed clean
|
||||
|
||||
A beyond-SOTA security review of the governed-trust cycle
|
||||
(`wifi-densepose-engine::StreamingEngine::process_cycle_calibrated`) examined
|
||||
the privacy-demotion path this ADR governs. **The monotonicity invariant holds:
|
||||
demotion only ever makes the emitted class more restrictive, never less.**
|
||||
|
||||
Verification (no behaviour change, the result is a clean bill with evidence):
|
||||
|
||||
- Each cycle computes `effective_class` fresh from the active mode's
|
||||
`target_class()` (the floor) and applies at most a **single-step** demotion
|
||||
(`demote_one`, clamped at `Restricted`). There is no cross-cycle state that
|
||||
could let a permissive class overwrite a restrictive one.
|
||||
- A forced contradiction (calibration mismatch / array-geometry insufficiency /
|
||||
mesh partition risk, ADR-032) raises the class byte; a clean cycle emits
|
||||
exactly the base class.
|
||||
- Pinned by `forced_contradiction_never_relaxes_class`, a property test over
|
||||
**all five** `PrivacyMode`s asserting `effective_class.as_u8() >=
|
||||
base_class.as_u8()` (strictly greater unless already clamped at `Restricted`)
|
||||
under a forced contradiction, and `== base` on a clean cycle.
|
||||
|
||||
Fail-closed boundaries were also pinned: an empty cycle errors (no degenerate
|
||||
over-permissive output, `empty_cycle_fails_closed`) and the single-node boundary
|
||||
is characterized as a valid non-demoting mode (`single_node_cycle_is_well_formed`).
|
||||
|
||||
The related witness domain-separation fix from the same review is recorded in
|
||||
ADR-137 (the witness folds `effective_class`, so the demotion is auditable).
|
||||
|
||||
Reference in New Issue
Block a user