feat: add authenticated BLE and Channel Sounding evidence - #5
Conversation
|
Reviewed. The trust model here is genuinely good — the abstention chain is exhaustive and fail-closed, replay requires strictly-increasing sequences, capacity is bounded before insert, and Blocking: an unverified advertisement can permanently disable identity evidence
self.watermark_ns = self.watermark_ns.max(sample.timestamp_ns); // unconditional
self.active.retain(|_, b| b.expires_ns > self.watermark_ns); // evicts everything
self.last_sequence.retain(...);
let reason = if ...
else if matches!(&sample.trust, BleAnchorTrust::Unverified) { Some(Unverified) } // too late
The sample is correctly rejected; the damage is done before the rejection. Repro (added to
|
`promote()` advanced `watermark_ns` and evicted every expired binding at the top of the function, before it examined the sample's trust. An advertisement is attacker-supplied -- anyone can broadcast one, with no key, enrollment, or signature -- so an `Unverified` or `Revoked` sample carrying a far-future `timestamp_ns` pinned the clock and cleared `active` before being refused. Every subsequent legitimate sample then failed `timestamp_ns + ttl_ns <= watermark_ns` and abstained as `Expired`: one frame permanently disabled identity evidence. The hostile packet was correctly rejected; the damage happened before the rejection. Split `promote` into two phases. Phase 1 decides everything derivable from the sample alone -- shape, trust, confidence, and the self-contained half of the TTL rule -- and abstains without touching shared state. Only a sample that survives phase 1 advances the watermark and retires stale bindings; phase 2 then applies the watermark comparison and the state-dependent rules. Precedence is unchanged. `Expired` splits across the two phases but stays in its original position, so a sample failing several rules still reports the same reason it did before; all 42 pre-existing adapter tests pass untouched. The watermark comparison is also unaffected by the move: `max` makes the watermark at least `timestamp_ns`, and with `ttl_ns > 0` already enforced, `timestamp_ns + ttl_ns <= watermark` yields the same verdict whether or not the sample itself is folded in first. Also replaces the `unreachable!` guarding the enrolled-receipt destructure with an explicit fail-closed abstention. It is total today because phase 1 abstains on every other variant, but that is an invariant of the chain rather than one the compiler checks -- adding a `BleAnchorTrust` variant would compile cleanly and turn a refusal into a panic. Adds two regression tests, for `Unverified` and `Revoked`. Both fail on the previous ordering with `abstentions=[Unverified, Expired]` -- the hostile frame refused and the next honest one refused with it -- and pass here. 133 workspace tests pass; fmt and clippy -D warnings clean. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5
|
Fixed in The watermark ordering
Two properties worth stating, because a reordering like this is easy to get subtly wrong: Precedence is unchanged. The watermark comparison is unaffected by the move. Regression testsTwo, covering They also assert the hostile frame is still refused, and refused as
|
Textual conflict in rufield-core/src/lib.rs was a clean union -- #4 adds the inference exports (AbstentionReason, CalibratedInference, CalibrationContext, PredictionInterval, PredictionSet, UncertaintyEnvelope), main adds the event exports from #5 (channel sounding, identity evidence). Each side's list is a superset of the other's for its own module, so both were taken whole. Three semantic conflicts that the textual merge could not see: - rufield-interop and rufield-ruvector are new in #4 and match exhaustively on Modality, which #5 extended. Added the missing BleAdvertisementRssi arm to both, taking the wire string from Modality::as_str rather than retyping it -- these functions feed serialization, so a divergent spelling would be a wire bug rather than a compile error. - #5 added FieldInference::track_id; three fixtures predating it (two in rufield-uncertainty, one in rufield-core) construct the struct literally. They are not track-scoped, so None matches the field's own default. - clippy manual is_multiple_of in rufield-ruvector's new folding code. Not a CI blocker -- CI runs -W clippy::all, not -D warnings -- but fixed while here. 184 workspace tests pass; fmt and clippy -D warnings clean. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5
…on (#4) * feat: add governed evidence and uncertainty platform * merge: resolve #4 against main after the BLE evidence merge Textual conflict in rufield-core/src/lib.rs was a clean union -- #4 adds the inference exports (AbstentionReason, CalibratedInference, CalibrationContext, PredictionInterval, PredictionSet, UncertaintyEnvelope), main adds the event exports from #5 (channel sounding, identity evidence). Each side's list is a superset of the other's for its own module, so both were taken whole. Three semantic conflicts that the textual merge could not see: - rufield-interop and rufield-ruvector are new in #4 and match exhaustively on Modality, which #5 extended. Added the missing BleAdvertisementRssi arm to both, taking the wire string from Modality::as_str rather than retyping it -- these functions feed serialization, so a divergent spelling would be a wire bug rather than a compile error. - #5 added FieldInference::track_id; three fixtures predating it (two in rufield-uncertainty, one in rufield-core) construct the struct literally. They are not track-scoped, so None matches the field's own default. - clippy manual is_multiple_of in rufield-ruvector's new folding code. Not a CI blocker -- CI runs -W clippy::all, not -D warnings -- but fixed while here. 184 workspace tests pass; fmt and clippy -D warnings clean. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5
… main (#9) Re-creates #1 against a tree that exists. That PR shares no commit ancestry with `main` -- main's history was rewritten after it opened -- so merging it would have needed --allow-unrelated-histories and conflicted on 33 files wholesale, including the four that #5 and #7 had just changed. This carries the feature across instead of grafting the branch. Wire code moved from 16 to 17. #1 assigned `Modality::QuantumRf => 16`, which `main` now gives to `BleAdvertisementRssi` (#5). Two modalities cannot share a code, and a renumber is not a compile error -- it is a deployed decoder reading the wrong modality -- so 16 stays where it was published and quantum RF takes the next free code. `recent_wire_codes_are_pinned` asserts both by value, so a future edit cannot quietly swap them. Carried across unchanged (13 files, none of which exist on main): rufield-adapters: quantum_rf_{quality,replay,support,wire}.rs, three test suites, and the synthetic replay fixture rufield-fusion: bearing.rs, bearing_math.rs, bearing_trust.rs, and the quantum_bearing test suite docs/ADR-270-quantum-rf-vector-sensing.md Re-applied by hand, because these files exist on main and could not be taken from the branch without reverting recent work: - `Modality::QuantumRf` (code 17, `quantum_rf`) plus the registry contract tests, which correctly refused the addition until updated. - `FieldAxis::{CartesianComponent, ComplexComponent, DirectionCandidate}`. - Optional sensor pose on `SensorDescriptor` -- coordinate_frame, position_m, orientation_xyzw -- all `#[serde(default, skip_serializing_if)]`, so events that omit them round-trip unchanged. - `Observation::attributes`, likewise absent from the wire when empty. - `normalize_verifying_key_hex` and `verifying_key_from_hex` in rufield-provenance. `SensorDescriptor` loses its `Eq` derive: the pose carries f32 coordinates and float equality is not an equivalence relation. `PartialEq` is retained. ADR renumbered 266 -> 270. 266 is taken on main by field-evidence-promotion (#4). 255 workspace tests pass, 38 of them from the ported suites (13 replay, 23 bearing, 2 properties; the performance gate stays #[ignore]d by its author, requiring a release build). fmt and clippy -D warnings clean. Claude-Session: https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5
Summary
Adds the governed RuField contract for authenticated BLE advertisement evidence and external Bluetooth Channel Sounding measurements.
ble_advertisement_rssias an additive modality without renumbering existing codes.Capability boundaries
Validation
The crossing acceptance tests require byte identical replay, two stable pseudonym to track bindings, explicit spoof and expiry abstentions, no cross track breathing fusion, and fail closed handling for incomplete, duplicate, mixed, or oversized Channel Sounding procedures.
Related work
Hardware status
This PR validates contracts, simulation, provenance, and policy. It does not claim physical radio accuracy or clinical performance. Hardware acceptance remains gated on a real Channel Sounding companion, repeated crossings, coexistence measurements, and reference respiration instrumentation.