diff --git a/docs/adr/ADR-291-wifi-veil-integration.md b/docs/adr/ADR-291-wifi-veil-integration.md new file mode 100644 index 00000000..b580ae31 --- /dev/null +++ b/docs/adr/ADR-291-wifi-veil-integration.md @@ -0,0 +1,83 @@ +# ADR-291: WiFi Veil integration — emission-shaping countermeasure as an advisory BFLD dependency + +- **Status**: Accepted — initial implementation (this PR) +- **Date**: 2026-08-10 +- **Deciders**: ruv +- **Tags**: privacy, bfld, bfi, wifi-veil, countermeasure, dependency + +## Context + +RuView's BFLD layer (ADR-118, ADR-141) senses via beamforming feedback while +enforcing structural privacy invariants on data entering the node. The 2026 +research sweep identified the complementary, unaddressed surface: a node's own +*outgoing* BFI is unencrypted and enables passive third-party +re-identification (BFId, ACM CCS 2025); IEEE 802.11bf-2025 shipped with no +privacy mechanism; and no commercial product occupies the countermeasure +category. + +[`wifi-veil`](https://github.com/ruvnet/wifi-veil) (codename VEIL, extracted +from this monorepo as a standalone crate) models a compliant emission-shaping +defense: keyed Givens rotations over the fine subspace of compressed +beamforming reports, energy-preserving (never jamming), reversible by a +keyed legitimate receiver. The crate is dependency-free, deterministic, +std-only, WASM-ready, dual MIT/Apache-2.0, and explicitly SYNTHETIC/L0: it +models waveform controls and never drives a radio. + +RuView should consume this capability rather than re-implement it, giving the +sensing stack a defensive counterpart under one evidence regime. + +## Options considered + +1. **Vendor the veil sources into a RuView crate.** Rejected: forks the + witness-pinned upstream and duplicates maintenance. +2. **crates.io dependency.** Not yet available (v0.1.0 unpublished at + decision time); revisit when released. +3. **Git dependency pinned to an exact rev, feature-gated in + `wifi-densepose-bfld`.** Chosen. + +## Decision + +- Add `wifi-veil` to `v2/Cargo.toml` `[workspace.dependencies]` as a git + dependency pinned to rev `018468b5d2bf41f35c552910f35659830af0eb91` + (v0.1.0). Exact-rev pinning preserves provenance and reproducibility for a + pre-release upstream; bumping the rev is an explicit, reviewable change. +- Gate it in `wifi-densepose-bfld` behind a new `veil` feature + (`veil = ["std", "dep:wifi-veil"]`), off by default — the default build + remains dependency-light and unchanged. +- New `bfld::veil` module (advisory-only): + - `ShieldAssessment`: stable projection of wifi-veil's deterministic + attacker-vs-protector `ExperimentReport` (re-ID accuracy shield-off/on, + chance level, throughput ratio, energy-conservation audit), always + carrying the `SYNTHETIC/L0` evidence label. + - `assess` / `assess_default`: run the deterministic experiment. + - `optimized_shield`: wrap `hyper_optimize` to derive the + optimizer-shipped shield config plus its verifying assessment. +- Boundaries, stated structurally and in docs: + - **Advisory only.** Nothing in the integration emits RF, alters frames, + or relaxes any BFLD gate/invariant (I1–I3 untouched). + - **Evidence honesty.** Every veil-derived figure is labeled + `SYNTHETIC/L0`; no MEASURED claim is possible from this path (hardware + validation lives in wifi-veil's own P5 roadmap). + - ESP32 nodes cannot shield their own feedback (per wifi-veil's platform + matrix); the integration therefore informs posture and reporting, not + on-node emission control. + +## Consequences + +- RuView gains a sense-and-defend posture no commercial offering has, under + a single claim taxonomy. +- First git dependency in the workspace: builds now fetch one pinned + external rev. Acceptable: the crate is dependency-free, small, witness- + pinned upstream, and license-compatible (MIT OR Apache-2.0 into MIT). +- Feature-gated consumers (e.g. sensing-server privacy reporting, the + desktop UI) can surface shield assessments later without new deps. +- When wifi-veil publishes to crates.io, switch the workspace entry to a + version requirement in a follow-up ADR amendment. + +## Validation + +- `cargo test -p wifi-densepose-bfld --features veil` — determinism, + shield-reduces-re-ID, compliance (energy conservation), chance-band + attainment, evidence labeling, optimizer wrapper. +- `cargo test -p wifi-densepose-bfld` (default features) — unchanged + behavior with the feature off. diff --git a/docs/adr/README.md b/docs/adr/README.md index c88f08dc..07b72ba0 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -148,6 +148,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme | [ADR-288](ADR-288-public-benchmark-evaluation-harness.md) | Public-benchmark evaluation harness — Widar3.0 ingest, split protocols, leakage guards | Accepted (initial implementation) | | [ADR-289](ADR-289-wideband-80211ax-csi-ingest.md) | Wideband 802.11ax CSI ingest — FeitCSI/AX210 adapter, subcarrier-agnostic plumbing | Accepted (initial implementation) | | [ADR-290](ADR-290-vitals-ground-truth-rig.md) | Vitals ground-truth rig — reference ingest, alignment, agreement metrics | Accepted (initial implementation) | +| [ADR-291](ADR-291-wifi-veil-integration.md) | WiFi Veil integration — emission-shaping countermeasure as advisory BFLD dependency | Accepted (initial implementation) | --- diff --git a/v2/Cargo.lock b/v2/Cargo.lock index a3a400fc..64623879 100644 --- a/v2/Cargo.lock +++ b/v2/Cargo.lock @@ -11365,6 +11365,7 @@ dependencies = [ "serde_json", "static_assertions", "thiserror 2.0.18", + "wifi-veil", ] [[package]] @@ -11804,6 +11805,11 @@ dependencies = [ "wifi-densepose-geo", ] +[[package]] +name = "wifi-veil" +version = "0.1.0" +source = "git+https://github.com/ruvnet/wifi-veil?rev=018468b5d2bf41f35c552910f35659830af0eb91#018468b5d2bf41f35c552910f35659830af0eb91" + [[package]] name = "winapi" version = "0.3.9" diff --git a/v2/Cargo.toml b/v2/Cargo.toml index 38c0d88a..9ca8bcbd 100644 --- a/v2/Cargo.toml +++ b/v2/Cargo.toml @@ -121,6 +121,10 @@ categories = ["science", "computer-vision", "wasm"] [workspace.dependencies] # Core utilities thiserror = "2.0" +# WiFi Veil — compliant-waveform countermeasure against unauthorized WiFi +# sensing (ADR-291). Dependency-free, deterministic, SYNTHETIC-only leaf; +# pinned to an exact rev because the crate is consumed pre-crates.io-release. +wifi-veil = { git = "https://github.com/ruvnet/wifi-veil", rev = "018468b5d2bf41f35c552910f35659830af0eb91" } anyhow = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/v2/crates/wifi-densepose-bfld/Cargo.toml b/v2/crates/wifi-densepose-bfld/Cargo.toml index d49f6400..956cce37 100644 --- a/v2/crates/wifi-densepose-bfld/Cargo.toml +++ b/v2/crates/wifi-densepose-bfld/Cargo.toml @@ -25,6 +25,10 @@ mqtt = ["std", "dep:rumqttc"] # enables privacy_class = 1 (derived) mode and the SoulMatchOracle gate # exemption. Disabled by default per the structural class-2 default. soul-signature = [] +# WiFi Veil advisory integration (ADR-291): deterministic attacker-vs- +# protector assessment of BFI identity leakage and emission-shaping shield +# configs. All numbers it produces are SYNTHETIC / L0 by construction. +veil = ["std", "dep:wifi-veil"] [dependencies] thiserror.workspace = true @@ -36,6 +40,7 @@ serde_json = { workspace = true, optional = true } # MQTT publisher backend (optional). Matches the `rumqttc` choice already in # `wifi-densepose-sensing-server` so both crates share TLS / version posture. rumqttc = { version = "0.24", default-features = false, features = ["use-rustls"], optional = true } +wifi-veil = { workspace = true, optional = true } [dev-dependencies] proptest.workspace = true diff --git a/v2/crates/wifi-densepose-bfld/src/lib.rs b/v2/crates/wifi-densepose-bfld/src/lib.rs index 2789910b..4bec7125 100644 --- a/v2/crates/wifi-densepose-bfld/src/lib.rs +++ b/v2/crates/wifi-densepose-bfld/src/lib.rs @@ -53,6 +53,9 @@ pub mod signature_hasher; pub mod sink; pub mod soul_channels; pub mod soul_match; +/// WiFi Veil advisory integration (ADR-291). Feature-gated: `veil`. +#[cfg(feature = "veil")] +pub mod veil; pub use coherence_gate::{CoherenceGate, MatchOutcome, NullOracle, SoulMatchOracle}; #[cfg(feature = "std")] diff --git a/v2/crates/wifi-densepose-bfld/src/veil.rs b/v2/crates/wifi-densepose-bfld/src/veil.rs new file mode 100644 index 00000000..18ee90a4 --- /dev/null +++ b/v2/crates/wifi-densepose-bfld/src/veil.rs @@ -0,0 +1,187 @@ +//! WiFi Veil advisory integration (ADR-291). +//! +//! Bridges BFLD's privacy layer to the [`wifi-veil`](https://github.com/ruvnet/wifi-veil) +//! countermeasure crate: a deterministic, dependency-free attacker-vs-protector +//! model of BFI identity leakage and keyed emission-shaping ("shield") +//! configurations. +//! +//! # Evidence discipline +//! +//! Everything this module produces is **`SYNTHETIC` / evidence level L0** by +//! construction: `wifi-veil` models compliant waveform controls on synthetic +//! scenes and never touches a radio. Assessments quantify the *modeled* +//! re-identification risk of unprotected beamforming feedback and the *modeled* +//! effect of a shield; they are advisory inputs to privacy posture, never +//! measured hardware claims. See ADR-291 and the wifi-veil README. +//! +//! # Relationship to BFLD invariants +//! +//! BFLD's structural invariants (I1–I3, see the crate README) govern data that +//! *enters* this node. WiFi Veil addresses the complementary surface: what this +//! node's own *outgoing* feedback leaks to passive third parties. The +//! integration is advisory-only — nothing here emits RF, alters frames, or +//! relaxes a BFLD gate. + +use wifi_veil::{experiment, ExperimentConfig}; + +/// Evidence label attached to every veil-derived figure. +/// +/// Matches the repository-wide claim taxonomy (CLAUDE.md): synthetic model +/// output, reproduced by `cargo test`, not measured on hardware. +pub const VEIL_EVIDENCE: &str = "SYNTHETIC/L0"; + +/// Summary of one deterministic attacker-vs-protector experiment. +/// +/// A thin, stable projection of [`wifi_veil::ExperimentReport`] carrying only +/// the figures BFLD consumers need, plus the mandatory evidence label. +#[derive(Debug, Clone, PartialEq)] +pub struct ShieldAssessment { + /// Number of candidate identities in the synthetic scene. + pub identities: usize, + /// Ideal chance-level re-identification accuracy (`1 / identities`). + pub chance_level: f32, + /// Modeled passive re-identification accuracy with the shield **off**. + pub reid_accuracy_off: f32, + /// Modeled passive re-identification accuracy with the shield **on**. + pub reid_accuracy_on: f32, + /// Modeled protected-link throughput as a fraction of baseline. + pub throughput_ratio: f64, + /// `output_energy / input_energy` of a representative protected frame. + /// ~1.0 means the control is energy-preserving (compliant, not jamming). + pub energy_ratio: f32, + /// True iff the energy ratio is within tolerance of 1.0. + pub energy_conserving: bool, + /// True iff the shield drove re-identification into the accepted + /// chance band. + pub drives_to_chance: bool, + /// Evidence label; always [`VEIL_EVIDENCE`]. + pub evidence: &'static str, +} + +impl ShieldAssessment { + /// Residual re-identification margin above chance with the shield on. + /// + /// `0.0` (or below) means the modeled attacker is at or below chance; + /// larger values mean residual identity leakage in the model. + #[must_use] + pub fn residual_reid_margin(&self) -> f32 { + self.reid_accuracy_on - self.chance_level + } + + /// One-line human-readable summary, evidence-tagged. + #[must_use] + pub fn summary(&self) -> String { + format!( + "[{}] re-ID {:.1}% -> {:.1}% (chance {:.1}%, {} identities), \ + throughput {:.1}%, energy ratio {:.6} ({})", + self.evidence, + self.reid_accuracy_off * 100.0, + self.reid_accuracy_on * 100.0, + self.chance_level * 100.0, + self.identities, + self.throughput_ratio * 100.0, + self.energy_ratio, + if self.energy_conserving { + "energy-conserving" + } else { + "NOT energy-conserving" + }, + ) + } +} + +impl From for ShieldAssessment { + fn from(r: wifi_veil::ExperimentReport) -> Self { + Self { + identities: r.identities, + chance_level: r.chance_level, + reid_accuracy_off: r.accuracy_shield_off, + reid_accuracy_on: r.accuracy_shield_on, + throughput_ratio: r.throughput_ratio, + energy_ratio: r.compliance.energy_ratio, + energy_conserving: r.compliance.energy_conserving, + drives_to_chance: r.drives_to_chance(), + evidence: VEIL_EVIDENCE, + } + } +} + +/// Run the deterministic attacker-vs-protector experiment for `cfg`. +/// +/// Fully deterministic: identical configs produce identical assessments. +#[must_use] +pub fn assess(cfg: &ExperimentConfig) -> ShieldAssessment { + experiment::run(cfg).into() +} + +/// Run the experiment with wifi-veil's shipped default scene and shield. +#[must_use] +pub fn assess_default() -> ShieldAssessment { + assess(&ExperimentConfig::default()) +} + +/// Derive the optimizer-shipped shield configuration and its verifying +/// assessment for `base`. +/// +/// Wraps [`wifi_veil::hyper_optimize`]: the returned shield uses the +/// spec-allowed throughput-optimal feedback resolution and a Givens-pass +/// count grown by the privacy margin factor. +#[must_use] +pub fn optimized_shield(base: &ExperimentConfig) -> (wifi_veil::ShieldConfig, ShieldAssessment) { + let hyper = wifi_veil::hyper_optimize(base); + let assessment = hyper.report.into(); + (hyper.shield, assessment) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_assessment_is_deterministic() { + let a = assess_default(); + let b = assess_default(); + assert_eq!(a, b); + } + + #[test] + fn shield_reduces_modeled_reid_accuracy() { + let a = assess_default(); + assert!( + a.reid_accuracy_on < a.reid_accuracy_off, + "shield-on accuracy {} must be below shield-off {}", + a.reid_accuracy_on, + a.reid_accuracy_off + ); + } + + #[test] + fn default_shield_is_compliant_and_at_chance() { + let a = assess_default(); + assert!(a.energy_conserving, "veil must be energy-preserving"); + assert!(a.drives_to_chance, "shipped default must reach chance band"); + assert!(a.throughput_ratio > 0.9, "throughput ratio {} too low", a.throughput_ratio); + } + + #[test] + fn evidence_label_is_synthetic_l0() { + let a = assess_default(); + assert_eq!(a.evidence, VEIL_EVIDENCE); + assert!(a.summary().starts_with("[SYNTHETIC/L0]")); + } + + #[test] + fn residual_margin_matches_fields() { + let a = assess_default(); + let m = a.residual_reid_margin(); + assert!((m - (a.reid_accuracy_on - a.chance_level)).abs() < f32::EPSILON); + } + + #[test] + fn optimized_shield_verifies() { + let (shield, assessment) = optimized_shield(&ExperimentConfig::default()); + assert!(shield.enabled); + assert!(assessment.drives_to_chance); + assert!(assessment.energy_conserving); + } +}