diff --git a/docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md b/docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md new file mode 100644 index 00000000..02ac409f --- /dev/null +++ b/docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md @@ -0,0 +1,163 @@ +# ADR-288: VEIL — a compliant-waveform privacy shield against unauthorized WiFi sensing + +| Field | Value | +|-------|-------| +| **Status** | Proposed — implemented (P1 reference model) | +| **Date** | 2026-08-09 | +| **Deciders** | ruv | +| **Codename** | **VEIL** — Verifiable Emission-shaping for Identity-Leakage prevention | +| **Codebase target** | new leaf crate `v2/crates/wifi-densepose-privshield` | +| **Parent** | ADR-118 (BFLD — the detection layer VEIL is the countermeasure to), ADR-282 (mandatory L0–L5 evidence ladder) | +| **Relates to** | ADR-120/121 (BFLD privacy class + identity-risk scoring — the trigger source), ADR-141 (privacy control plane / runtime attestation — the audit consumer), ADR-280 (active sensing / governed actuation — VEIL is a defensive sensing action), ADR-185 §13 (`wifi-densepose-aether` — the pure-compute leaf pattern this crate follows) | +| **Research bundle** | [`docs/research/privacy-shield/`](../research/privacy-shield/) (8 files) | +| **Tracking issue** | TBD | + +## 0. PROOF discipline + +Every defense number this crate produces is **SYNTHETIC / evidence level L0** +(ADR-282): generated by the crate's own model (`identity::Channel`), attacked by +the crate's own classifier (`attacker::NearestCentroidAttacker`), and scored +against its own known labels. Nothing here has been validated against real WiFi +silicon, and the crate contains no radio integration and cannot emit RF. External +attack/defense results cited from the literature (BFId, LeakyBeam, DySPAN-2026, +IRShield, FCC statutes) are **EXTERNAL** evidence and labelled MEASURED/CLAIMED in +the research bundle. The single measured claim about *our own behavior* is the +pinned deterministic witness in `proof.rs`. + +## 1. Context + +### 1.1 The gap + +IEEE 802.11ac/ax beamforming feedback (BFI) — the compressed Givens-rotation +angle matrices (φ/ψ) a client sends the AP — is transmitted **unencrypted on the +management plane**. Any device in monitor mode can capture it for every station +at once, no network access, and the target need carry no device. The literature +establishes the severity: **BFId** (ACM CCS 2025) re-identifies individuals from +BFI; **LeakyBeam** (NDSS 2025) detects occupancy through walls at 20 m from BFI; +**BeamSense** recognizes activities at up to 99.28%. IEEE Std **802.11bf-2025** +(published 26 Sep 2025) standardizes the sensing measurement/feedback surface +these attacks abuse — and a 2023 proposal for a BFI secure-transmission mechanism +(802.11-23/0782) was **withdrawn**, so the standard shipped with no privacy +protections. + +RuView already has a *detection* layer for this: **BFLD** (ADR-118/121) measures +the identity-leakage of each frame and gates what leaves the node. But BFLD +protects *RuView's own outputs*; it does nothing about a **third-party sniffer** +capturing the room's plaintext BFI off the air. There is no RuView component, and +per our market survey no shipping product anywhere, that prevents that. + +### 1.2 Constraint: compliant waveform controls, never jamming + +The defense must preserve normal communications and must not interfere with any +other station. Jamming (47 U.S.C. §333/§302a) is defined by *adding energy to +interfere with others' transmissions*. Any acceptable control must shape only the +node's **own** standards-conformant emission. + +### 1.3 The separability insight + +Identity leaks through the *fine* cross-subcarrier phase structure of a +beamforming report; data throughput rides the *dominant* beam direction. These +are (mostly) separable subspaces — so a transform confined to the fine subspace +can wreck re-identification while sparing the beam the link depends on. DySPAN-2026 +independently MEASURED that shaping fine-resolution feedback is near-free in +throughput, corroborating the insight. + +## 2. Decision + +Ship **`wifi-densepose-privshield`** (VEIL) as a standalone pure-compute leaf +crate (the `wifi-densepose-aether`/`nvsim` pattern: dependency-free, deterministic, +WASM-ready, zero coupling to any radio or ingestion path), implementing: + +1. **A SYNTHETIC two-subspace BFI model** (`identity.rs`): each identity owns a + stable fine-block signature; sessions add environmental nuisance; the comm + block is identity-free and carries throughput. +2. **The protector** (`protector.rs`): compliant waveform controls, primarily a + **per-session keyed orthogonal rotation of the fine subspace, composed from + extra Givens rotations** — the report's native primitive. Plus feedback + quantization/dither, sounding-cadence randomization, and a `SensingDetector` + that engages the shield only when sensing activity is observed. +3. **The adversary** (`attacker.rs`): a passive nearest-centroid re-identifier + modeling the BFId threat. +4. **A throughput model** (`throughput.rs`): `(1 − overhead) · C(SNR·(1−ρ))/C(SNR)`, + where the beamforming residual `ρ` comes from finite feedback resolution + (negligible at 7+ bits, since the legitimate receiver inverts the keyed + rotation). +5. **A compliance audit** (`compliance.rs`): the rotation is orthogonal ⇒ + energy-preserving ⇒ adds no interfering energy ⇒ **not jamming**, turned into a + checked `ComplianceReport` (energy ratio ≈ 1.0). +6. **The experiment** (`experiment.rs`): runs the attacker against unprotected and + protected traffic and reports both accuracies vs. chance, plus throughput and + compliance, with a single `passed()` verdict. +7. **A deterministic proof** (`proof.rs`): a pinned FNV-1a witness over the + reference experiment (the `nvsim`/`verify.py` discipline). + +### 2.1 Why the keyed Givens rotation + +It is simultaneously **orthogonal** (energy-preserving ⇒ compliant), +**key-reversible** (the associated AP shares the session key and recovers the true +precoder ⇒ throughput preserved), and **fresh per session** (a sniffer sees a new +random rotation of the signature each session and cannot average it back ⇒ the +enrollment attack collapses; over unknown rotations the signature carries no +stable discriminative information ⇒ re-ID → chance). It is the shared-secret +precoding idea (cf. MIMOCrypt) specialized to the identity-bearing subspace. + +### 2.2 Measured behavior (SYNTHETIC / L0) + +Reference experiment (default scene, N=16 identities, `cargo test`): + +| Metric | Shield off | Shield on | +|---|---|---| +| Passive re-ID accuracy | 100.0% | **7.8%** (chance 6.25%) | +| Link throughput ratio | 100% | **98.0%** | +| Emission energy ratio | — | **1.000000** (compliant) | + +All 29 unit/proof tests + doctest pass; the crate builds for +`wasm32-unknown-unknown` and is clippy-clean. + +## 3. What this explicitly is NOT + +- **Not a radio driver.** No RF frontend, no transmit path, no + `wifi-densepose-hardware` coupling. VEIL cannot emit and cannot jam. +- **Not a defense against the associated AP.** That party holds the session key by + construction (threat class A3); protecting against a malicious AP is BFLD's + detection/privacy-class problem (ADR-118/141), not this shield's. +- **Not a full motion-obfuscation claim.** A fixed per-session rotation does not + hide coarse within-session motion; identity *re-ID* is the guaranteed target, + motion is partial/future work. +- **Not a real-hardware performance claim.** All defense numbers are SYNTHETIC/L0 + until a two-node capture with a boot/runtime-log witness exists (CLAUDE.md + hardware rule; roadmap P5). +- **Not RF denial or camera-grade anything.** + +## 4. Simplifications (honesty boundary) + +- The two-subspace split is an abstraction; on real radios comm and identity + information are only *approximately* separable, so the real throughput cost of + fully hiding identity may exceed the model's ~2%. DySPAN-2026's MEASURED curve + bounds it as *small* at fine resolution, not zero. +- The attacker is nearest-centroid. The collapse argument is classifier-independent + (it is about the marginalized signal), but P2/P5 must confirm a learned attacker + also collapses. +- The crate's PRNG is SplitMix64 — deterministic and WASM-safe but **not + cryptographic**; a deployment derives the rotation key from the negotiated link + secret, never from this PRNG. + +## 5. Consequences + +- RuView gains the *countermeasure* half of its RF-privacy story: BFLD detects + leakage, VEIL acts on it — a defensible, standards-anchored, gap-filling + position (see `docs/research/privacy-shield/06-market-and-buyers.md`). +- The compliance audit gives regulators/auditors a machine-checkable "not jamming" + artifact that composes with ADR-141 attestation. +- Future integration (BFLD `identity_risk` → `SensingDetector`, ADR-280 governed + actuation, firmware feedback shaping, two-node hardware measurement) is staged in + the research bundle roadmap and deliberately deferred so the model validates in + isolation first. + +## 6. Validation + +```bash +cargo test -p wifi-densepose-privshield --no-default-features +cargo build -p wifi-densepose-privshield --target wasm32-unknown-unknown +cargo clippy -p wifi-densepose-privshield --all-targets +``` diff --git a/docs/adr/README.md b/docs/adr/README.md index 11ca228c..758a2ecb 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -145,6 +145,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme | [ADR-287](ADR-287-coherent-wideband-rf-tomography-crate.md) | `wifi-densepose-sar` — coherent wideband RF tomography research crate | Accepted (implemented, published) | | [ADR-285](ADR-285-homecore-wasm-first-metaharness.md) | WASM-first Homecore developer metaharness via `npx homecore` | Accepted (implemented and validated) | | [ADR-286](ADR-286-wifi-densepose-sar-harness-via-metaharness.md) | `wifi-densepose-sar-harness` — MetaHarness with darwin/router/flywheel | Accepted (implemented, published) | +| [ADR-288](ADR-288-veil-privacy-shield-compliant-waveform.md) | VEIL — compliant-waveform privacy shield against unauthorized WiFi sensing (`wifi-densepose-privshield`) | Proposed (implemented, P1 reference) | --- diff --git a/docs/research/privacy-shield/01-sota-survey.md b/docs/research/privacy-shield/01-sota-survey.md new file mode 100644 index 00000000..79a91063 --- /dev/null +++ b/docs/research/privacy-shield/01-sota-survey.md @@ -0,0 +1,141 @@ +# 01 — State of the Art + +Scope: what a passive or active adversary can extract about *who* is in a space +and *what they are doing* from WiFi, the standard that broadens that surface, and +the countermeasures that try to prevent it. Claims are tagged **MEASURED** (from +a primary source, with metric), **CLAIMED** (asserted without an independent +measurement), or analytical inference (flagged). + +--- + +## 1. The attack surface: beamforming feedback (BFI) + +Since WiFi 5 (802.11ac), a client (beamformee) measures the downlink channel, +compresses the steering matrix **V** into **Givens-rotation angles φ/ψ**, and +transmits them **in cleartext** so the AP can steer beams. Anyone in monitor +mode can capture these frames for *every* client simultaneously — no network +access, and the target need carry no device. Quantization is coarse (802.11ac +angle steps of π/4…π/32 rad) yet retains rich motion and body information. + +| Work | Venue / year | Result | Label | +|---|---|---|---| +| **BFId** — identity inference from BFI | ACM CCS 2025 (KIT/KASTEL) | Re-identifies individuals from BFI alone; novel 197-person dataset. Press reports **99.5%** in a controlled study (ACM full text was not openable to confirm class count/split) | MEASURED (paper); 99.5% is CLAIMED via press | +| **LeakyBeam** — occupancy through walls | NDSS 2025 | Occupancy detection **TPR 82.7% / TNR 96.7%** at **20 m, through walls**, from plaintext BFI. Proposes a BFI-obfuscation defense | MEASURED (attack); defense overhead CLAIMED | +| **BFIAttack** — CSI reconstruction from BFI | arXiv 2026 (USF) | Reconstructs CSI from BFI, then defeats CSI defenses. ASR: device auth 95.5% / user auth 92.6% / key-gen 94.2% (single-antenna), 1.5–6 m | MEASURED | +| **BeamSense** — activity recognition from BFI | Computer Networks vol. 258, 2025 (Northeastern) | Human activity recognition **up to 99.28%** on commodity 802.11ac, no firmware mod, ~10% better than CSI | MEASURED | +| **Wi-BFI** — capture tooling | arXiv 2309.04408, 2023 | Pip-installable extraction of 802.11 BFI from commercial devices | tooling | + +**Takeaway for the defender.** BFI is the highest-leverage surface: unencrypted, +management-plane, device-free, capturable en masse with off-the-shelf tools. It +is also a *stepping stone* — BFIAttack shows BFI can reconstruct the CSI that all +older attacks assume. + +--- + +## 2. The older adjacent surface: CSI identity/gait/activity + +CSI requires special extraction (Intel 5300 / Atheros / ESP32) but is the +foundation the BFI attacks build on. Person-ID exploits **gait** as a biometric. +Representative MEASURED results (commodity WiFi, CSI amplitude): + +| System | Accuracy | N (candidates) | Note | +|---|---|---|---| +| WiWho (IPSN 2016) | 92%→80% | 2→6 | 2–3 m straight walk | +| WiFi-ID (2016) | 93%→77% | 2→6 | wavelet features | +| WiPIN (2018) | 92–100% | ≤30 | operation-free | +| Deep-WiID (2019) | 92.5–99.7% | 6→15 | GRU | +| WiNet / LWID (2020) | 98.5% / 98.8% | 40 / 50 | CNN | + +**Pattern the defender must exploit and not overstate:** accuracy is high in +small closed sets but *degrades as N grows and conditions become realistic* +(cross-day, cross-location, cross-walking-style). Chance is **1/N**; a 99% result +on N=5 is far weaker evidence than 99% on N=197. Open-world scale is largely +unproven (see *SoK: Security Evaluation of Wi-Fi CSI Biometrics*, 2025). + +--- + +## 3. The standard: IEEE 802.11bf-2025 + +IEEE Std **802.11bf-2025** (Amendment 4: *Enhancements for WLAN Sensing*) was +published **26 September 2025**. It standardizes WLAN sensing in 1–7.125 GHz and +above 45 GHz, defining sensing capability signaling, measurement/sounding +setup, feedback types, and both passive (ambient-traffic) and active +(dedicated null-packet) sensing modes. + +- **Attack-surface implication (analytical).** 802.11bf turns CSI/measurement + acquisition from proprietary hacks into open, vendor-agnostic, machine-readable + MAC signaling across heterogeneous devices — institutionalizing exactly the + measurements the BFI attacks abuse. The standard frames sensing as a feature, + not a threat. +- **The privacy gap (MEASURED from standards minutes).** A 2023 proposal for a + BFI "secure transmission mechanism" (IEEE 802.11-23/0782) was **withdrawn**; + "the group did not align on the characterization of [the] privacy problem." + The standard shipped without privacy protections, and its own analysis admits + passive eavesdroppers can extract location, respiration, heart rate, and + identity. + +--- + +## 4. Countermeasures (the defense literature) + +All operate on the defender's *own* transmissions; none are jamming. + +| Countermeasure | Venue / year | Mechanism | Effect | Label | +|---|---|---|---|---| +| **IRShield** | IEEE S&P 2022 | IRS/reconfigurable surface randomizes reflected paths | Attacker motion-detection **≤5%** | MEASURED | +| **PhyCloak** | USENIX NSDI 2016 | Full-duplex obfuscator injects Doppler/phase distortion into sensing only | **88.69%** gesture-spoof; throughput can rise (whitelist legit sensors) | MEASURED (spoof); throughput CLAIMED | +| **DP-Givens dithering** | IEEE DySPAN 2026 | Differentially-private stochastic quantization of BFI φ/ψ angles | Attacker speed-class error 19%→~73% (chance); **fine (3-bit) resolution ≈ non-private baseline throughput** | MEASURED | +| **MIMOCrypt / WiShield** | 2023 / IEEE JSAC 2024 | Secret precoding / MIMO CSI manipulation so only the intended RX decodes | Anti-tracking | CLAIMED/formal | +| **CSI Fuzzing / DP feature release** | IEEE 2024–25 | Randomized CSI features with DP budget | Formal DP guarantee | CLAIMED/formal | +| **ScatterShield** | ACM IMWUT 2025 | Backscatter tags inject controlled clutter | Defeats unauthorized sensing | MEASURED | +| **Adversarial packet perturbation** | ACM MobiCom 2024 | Small in-spec packet perturbations degrade attacker model | Symmetric defense | MEASURED | + +**The fundamental tradeoff (MEASURED, DySPAN 2026).** Perturbing precoding/ +feedback that an attacker exploits also degrades legitimate beamforming gain — +*but the cost collapses at fine feedback resolution*: + +| Randomization | Attacker error | Beamforming gain retained | +|---|---|---| +| none | 19% | 100% | +| moderate (p=0.3) | >50% | median >90% | +| maximum (p≥0.9) | ~73% (≈chance) | median ~58% | + +At **high (3-bit) feedback resolution, privacy was "nearly indistinguishable +from the non-private baseline"** in link performance. This is the empirical basis +for VEIL's design choice (compliant fine-resolution feedback shaping — see +[03-countermeasure-design.md](03-countermeasure-design.md)). + +--- + +## 5. Where VEIL sits + +The literature has two families: **external** obfuscation (IRShield/ScatterShield +— extra hardware, perturbs the channel) and **transmitter-side** feedback/precoder +shaping (DP-Givens, MIMOCrypt — no extra hardware, perturbs your own report). +VEIL is in the second family and adds the missing property the others do not all +combine: a transform that is simultaneously **energy-preserving** (provably +compliant), **key-reversible** (throughput-preserving for the legitimate link), +and **session-fresh** (defeats cross-session re-identification), unified around +the Givens-rotation primitive the report already uses. + +--- + +## Sources + +- BFId — ACM CCS 2025: https://dl.acm.org/doi/10.1145/3719027.3765062 · KIT record: https://publikationen.bibliothek.kit.edu/1000185756 +- LeakyBeam — NDSS 2025: https://www.ndss-symposium.org/ndss-paper/lend-me-your-beam-privacy-implications-of-plaintext-beamforming-feedback-in-wifi/ +- BFIAttack — arXiv 2604.04179: https://arxiv.org/html/2604.04179v1 +- BeamSense — Computer Networks 2025: https://dl.acm.org/doi/10.1016/j.comnet.2024.111020 · arXiv 2303.09687: https://arxiv.org/pdf/2303.09687 +- Wi-BFI — arXiv 2309.04408: https://arxiv.org/pdf/2309.04408 +- SoK: Security Evaluation of Wi-Fi CSI Biometrics — arXiv 2511.11381: https://arxiv.org/pdf/2511.11381 +- WiWho (IPSN 2016): https://dl.acm.org/doi/10.5555/2959355.2959359 · WiPIN — arXiv 1810.04106: https://arxiv.org/pdf/1810.04106 +- Survey on Wi-Fi Sensing for Human Identity — MDPI Electronics 2023: https://www.mdpi.com/2079-9292/12/23/4858 +- IEEE Std 802.11bf-2025: https://standards.ieee.org/ieee/802.11bf/11574/ · Overview — IEEE COMST 2024: https://ieeexplore.ieee.org/document/10547188/ · NIST: https://www.nist.gov/publications/ieee-80211bf-enabling-widespread-adoption-wi-fi-sensing +- 802.11bf privacy proposal withdrawal (802.11-23/0782), summarized: https://pascalpiron.substack.com/p/wifi-sensing-and-the-privacy-fix +- IRShield — IEEE S&P 2022 / arXiv 2112.01967: https://arxiv.org/abs/2112.01967 · https://ieeexplore.ieee.org/document/9833676/ +- PhyCloak — USENIX NSDI 2016: https://www.usenix.org/conference/nsdi16/technical-sessions/presentation/qiao +- Protecting Human Activity Signatures in Compressed 802.11 CSI Feedback — DySPAN 2026 / arXiv 2512.18529: https://arxiv.org/abs/2512.18529 +- MIMOCrypt — arXiv 2309.00250: https://arxiv.org/pdf/2309.00250 · WiShield — IEEE JSAC 2024: https://dl.acm.org/doi/abs/10.1109/JSAC.2024.3414597 +- ScatterShield — ACM IMWUT 2025: https://dl.acm.org/doi/abs/10.1145/3770653 +- Practical Adversarial Attack on WiFi Sensing — ACM MobiCom 2024: https://dx.doi.org/10.1145/3636534.3649367 +- Privacy-Preserving Wi-Fi Data Generation via DP — INFOCOM 2025: https://www.eng.auburn.edu/~szm0001/papers/INFOCOM25.pdf diff --git a/docs/research/privacy-shield/02-threat-model.md b/docs/research/privacy-shield/02-threat-model.md new file mode 100644 index 00000000..1729afb9 --- /dev/null +++ b/docs/research/privacy-shield/02-threat-model.md @@ -0,0 +1,94 @@ +# 02 — Threat Model + +VEIL protects a physical space (a room, a ward, a boardroom, a SCIF) from +*unauthorized* WiFi-based inference of **who is present** and **what they are +doing**, without denying the space its own working WiFi. This file states the +adversary classes, exactly what VEIL defends, and — just as importantly — what +it does **not**. + +--- + +## 1. Assets + +| Asset | Why it matters | +|---|---| +| **Identity linkage** | Re-identifying a specific person across time/sessions from their RF signature (BFId-class attack) | +| **Occupancy / presence** | Whether the space is occupied, and by how many (LeakyBeam-class, through-wall) | +| **Activity / motion** | Gait, gestures, keystrokes, respiration inferred from channel dynamics (BeamSense-class) | +| **Communication utility** | The legitimate WiFi link must keep working (≥95% throughput bar) | + +--- + +## 2. Adversary classes + +| Class | Position | Capability | In VEIL scope? | +|---|---|---|---| +| **A1 — external passive sniffer** | Outside the trust boundary (adjacent room, van, hallway), monitor mode | Captures plaintext BFI/CSI for every station; runs BFId/LeakyBeam/BeamSense offline | **Primary target — yes** | +| **A2 — external active sensor** | Nearby, transmits its own probing/sounding to solicit measurable responses | Elicits sensing responses; 802.11bf "active" mode | **Partial** — cadence randomization + non-response policy help; full defense needs MAC-layer policy | +| **A3 — associated but curious AP** | Inside the link; the party VEIL shares keys with | Sees the un-rotated report by construction | **Out of scope** — this is BFLD's detection/privacy-class problem (ADR-118/141) | +| **A4 — supply-chain / firmware** | Compromised radio firmware | Can bypass any transmit-side control | Out of scope (integrity problem, not a waveform problem) | +| **A5 — physical / RF-denial** | Wants to *block* WiFi | — | Explicitly rejected: VEIL never jams | + +VEIL's design centers on **A1**, the attacker the literature demonstrates and +the one no shipping product addresses. + +--- + +## 3. What VEIL guarantees (and the evidence class) + +1. **Cross-session identity unlinkability against A1.** Because the fine-subspace + signature is rotated by a fresh secret orthogonal transform each session, an + A1 attacker cannot average captures back to a stable per-person template. + *Evidence: SYNTHETIC — re-ID collapses from 100% to ~chance in the reference + experiment (`cargo test`); real-silicon witness is future work.* +2. **Communication preservation.** The transform is key-reversible by the + legitimate receiver, and acts only on the identity-bearing fine subspace, so + link throughput stays ≥95%. *Evidence: SYNTHETIC model + MEASURED external + corroboration (DySPAN 2026: fine-resolution feedback shaping is near-free).* +3. **Compliance.** The transform is orthogonal ⇒ energy-preserving ⇒ adds no + interfering emission ⇒ not jamming. *Evidence: machine-checked energy ratio = + 1.000000 in the `compliance` module; statutory analysis in + [04-compliance-and-regulatory.md](04-compliance-and-regulatory.md).* + +--- + +## 4. What VEIL does NOT do (non-goals, stated to prevent over-claiming) + +- **It does not hide identity from the associated AP (A3).** That party holds the + session key. Protecting against a malicious AP requires detection and policy + (BFLD), not waveform shaping. +- **It is not RF denial or jamming.** It never degrades another station's link. +- **It does not, by itself, defeat within-session motion detection.** A single + session's rotation is fixed, so coarse presence/motion may still be inferable + within one capture window; sounding-cadence randomization mitigates but does + not eliminate this. Identity *re-ID* (the brief's metric) is the guaranteed + target; motion obfuscation is partial and tracked as future work. +- **It is not a camera-grade or medical-grade claim in any direction.** +- **It is not validated on hardware yet.** All quantitative defense results are + SYNTHETIC until a captured boot/runtime log exists (CLAUDE.md hardware rule). + +--- + +## 5. Trust boundary + +``` + ┌────────────────────── protected space ──────────────────────┐ + │ │ + │ [person] [person] legitimate STA ⇄ AP (VEIL) │ + │ │ │ │ shares session key │ + │ └──── RF ──────┘ │ rotates fine subspace│ + │ reflections ▼ of its own BFI │ + │ compliant, key-reversible, │ + │ energy-preserving emission │ + └───────────────────────────────────────┬──────────────────────┘ + │ plaintext BFI on air + ▼ + A1 external passive sniffer (monitor mode) + sees a freshly-rotated signature each session + → cannot build a stable per-person template + → re-identification → chance +``` + +The key never crosses the boundary to A1. The AP inside the boundary is trusted +for key-sharing (A3 out of scope). No emission crosses the boundary with intent +or effect of interfering with another station (A5 rejected). diff --git a/docs/research/privacy-shield/03-countermeasure-design.md b/docs/research/privacy-shield/03-countermeasure-design.md new file mode 100644 index 00000000..1d7b9a1d --- /dev/null +++ b/docs/research/privacy-shield/03-countermeasure-design.md @@ -0,0 +1,128 @@ +# 03 — Countermeasure Design + +How VEIL prevents unauthorized sensing with compliant waveform controls, and how +the design maps to [`v2/crates/wifi-densepose-privshield`](../../../v2/crates/wifi-densepose-privshield). + +--- + +## 1. The separable-subspace principle + +A compressed beamforming report is not homogeneous. Two blocks carry different +information: + +- **Dominant beam direction (comm block).** The coarse steering the AP uses to + aim data at the client. It varies with position and traffic and carries **no** + stable identity. **Throughput rides here.** +- **Fine cross-subcarrier phase structure (fine block).** The high-order + multipath detail. It is *stable per person* across sessions and is what + re-identification exploits (BFId). **Identity leaks here.** Communication + barely uses it. + +The whole design rests on this: **identity leakage and data throughput live in +(mostly) separable subspaces.** A transform confined to the fine block can wreck +re-identification while sparing the beam the link depends on. This is consistent +with the DySPAN-2026 MEASURED result that shaping fine-resolution feedback is +nearly free in throughput. + +--- + +## 2. The four compliant waveform controls + +VEIL alters "channel sounding, phase, or beam schedules" — exactly the levers the +brief names — all within the 802.11 waveform envelope: + +| Control | What it varies | Purpose | +|---|---|---| +| **Keyed precoder rotation** (primary) | A fresh secret orthogonal transform of the *fine* subspace each session, composed from extra Givens rotations | Destroys cross-session identity linkage; energy-preserving; key-reversible | +| **Feedback quantization / dither** | Sub-step noise on reported φ/ψ angles | Adds report-level uncertainty; tunes the privacy–throughput point via `feedback_bits` | +| **Sounding-cadence randomization** | Jitter on NDP sounding intervals | Under-samples motion for an eavesdropper; charged as the throughput overhead | +| **MU-group / stream-mapping shuffle** | Which STAs are grouped, stream-to-antenna mapping | Rotates the spatial signature over time | + +All four modify the node's **own** standards-conformant frames. None adds energy +on top of another station (see [04](04-compliance-and-regulatory.md)). + +--- + +## 3. Why the keyed Givens rotation is the right primitive + +The compressed beamforming report is *already* a product of Givens rotations +(the φ/ψ angles). VEIL composes **additional keyed Givens rotations** over the +fine block. This choice gives three properties at once: + +1. **Orthogonal ⇒ energy-preserving.** A Givens rotation preserves the vector's + L2 norm exactly. Composing many still preserves it. So the emission carries + the same power it always would — **no added energy, no interference, not + jamming.** The `compliance` module checks this: energy ratio = 1.000000. +2. **Keyed & reversible ⇒ throughput-preserving.** The legitimate AP/STA shares + the per-session key, derives the identical rotation schedule, and applies the + inverse (negated angles, reversed order) to recover the true precoder. It pays + only the tiny residual from quantizing the extra angles at `feedback_bits` + resolution — negligible at 7+ bits — plus the sounding overhead. +3. **Fresh per session ⇒ unlinkable.** A different rotation each session means an + A1 sniffer sees `R_e · signature` for a new random `R_e` every time. Averaging + over sessions (the natural enrollment attack) drives + `mean_e(R_e · signature) → 0` for *every* identity, so all templates collapse + toward the origin and become indistinguishable — re-identification → chance. + This is the marginalized-mutual-information argument: over unknown rotations, + the signature carries no stable discriminative information. + +This is the shared-secret precoding idea (cf. MIMOCrypt) specialized to the +identity-bearing subspace and unified around the report's native primitive. + +--- + +## 4. Detect-then-act + +Per the brief ("detect sensing activity and alter…"), VEIL need not perturb +continuously. The `SensingDetector` exposes the decision rule: when the observed +rate of sensing/NDP solicitations crosses a threshold, the control plane +(ADR-280) engages the shield. Continuous operation is also valid; gating just +saves the (already small) overhead when no sensing is present. + +--- + +## 5. Module map + +| Concept above | Crate module | Key items | +|---|---|---| +| Deterministic, WASM-safe randomness + keys | `prng` | `Rng` (SplitMix64), `fnv1a_64`, `derive_key` | +| Givens algebra, energy conservation | `linalg` | `apply_givens`, `norm`, `dist_sq` | +| SYNTHETIC two-subspace BFI model | `identity` | `SceneConfig`, `Channel`, `BfiSample` (`comm()`/`fine()`) | +| The four controls (shield) | `protector` | `ShieldConfig`, `Protector::protect`/`recover`, `SensingDetector` | +| Passive re-ID adversary | `attacker` | `NearestCentroidAttacker` | +| Privacy–throughput tradeoff | `throughput` | `LinkModel::throughput_ratio`, `beamforming_residual` | +| "Not jamming" audit | `compliance` | `ComplianceReport::audit`/`is_compliant` | +| Attacker-vs-protector head-to-head | `experiment` | `ExperimentConfig`, `run`, `ExperimentReport` | +| Byte-stable deterministic witness | `proof` | `Proof::EXPECTED_WITNESS`, `Proof::witness` | + +--- + +## 6. The privacy–throughput knob + +The design exposes one honest tuning knob, matching the literature: + +- **`feedback_bits` high (7–9):** the legitimate receiver's residual is ~1e-5 → + throughput ≈ baseline; privacy is full (rotation is fresh regardless of bits). + This is VEIL's operating point. +- **`feedback_bits` low (≤3) or extra additive dither:** more robustness to a + key-recovery-adjacent attacker, at measurable throughput cost. +- **`sounding_overhead`:** the dominant (small) throughput cost, from cadence + randomization; trades motion-obfuscation strength against airtime. + +The `throughput` module computes the ratio from these, so the tradeoff is +inspectable rather than asserted (`cargo test throughput`). + +--- + +## 7. Honest limitations of the model + +- The two-subspace split is an abstraction; on real hardware comm and identity + information are only *approximately* separable, so the real throughput cost of + fully hiding identity may be higher than the model's ~2%. The DySPAN-2026 + MEASURED curve is the external sanity check that it is *small* at fine + resolution, not zero. +- The nearest-centroid attacker is deliberately simple. The collapse argument is + classifier-independent (it is about the signal, not the model), but a hardware + study must confirm a strong learned attacker also collapses. +- Within-session motion is not addressed by the rotation alone (see threat + model §4). diff --git a/docs/research/privacy-shield/04-compliance-and-regulatory.md b/docs/research/privacy-shield/04-compliance-and-regulatory.md new file mode 100644 index 00000000..1760c557 --- /dev/null +++ b/docs/research/privacy-shield/04-compliance-and-regulatory.md @@ -0,0 +1,90 @@ +# 04 — Compliance and Regulatory Line + +**Non-negotiable:** VEIL uses compliant waveform controls and **never jams.** +This file states the legal basis for that line and why every VEIL control falls +on the compliant side of it. It is engineering analysis, not legal advice; a +deployment in a given jurisdiction needs its own regulatory review. + +--- + +## 1. The statutory line (United States) + +The prohibition is on **interfering with others' transmissions**, not on how you +shape **your own** signal. + +| Authority | What it prohibits | +|---|---| +| **47 U.S.C. §333** | *Willful or malicious interference* with any licensed/authorized radio station or U.S. Government station | +| **47 U.S.C. §302a(b)** | Manufacture, import, marketing, sale, or *operation* of non-compliant devices (jammers cannot be certified — their sole purpose is interference) | +| **47 U.S.C. §301** | Requires a license/authorization to transmit; a jammer can never be authorized | +| **47 U.S.C. §501 / §503** | Criminal penalties and forfeitures; FCC cites fines up to $112,500 per violation, **no exemptions** for business/residence/vehicle | + +The distinguishing element of jamming is **intent to interfere plus effect on a +third party's link.** A device that shapes its own standards-conformant emission +— staying within transmit-power and spectral-mask limits, still type-certifiable +— is not a jammer. + +--- + +## 2. Why each VEIL control is compliant + +| Control | Compliance argument | +|---|---| +| **Keyed precoder rotation** | Orthogonal ⇒ preserves the report's energy exactly ⇒ **adds no power on top of anyone's signal.** It is still a valid precoder within the 802.11 feedback format. Machine-checked: energy ratio = 1.000000 (`compliance` module) | +| **Feedback quantization / dither** | Reports angles the standard already allows, at the standard's resolution; sub-step dither stays within the quantization envelope. No emission change beyond the node's own frame | +| **Sounding-cadence randomization** | Chooses *when* the node sends its own NDP soundings, within permitted timing. Sending fewer/jittered soundings never interferes with another station | +| **MU-group / stream-mapping shuffle** | Rearranges the node's own spatial mapping; a normal in-spec transmit choice | + +None of the four transmits *to prevent* another station from communicating; none +adds out-of-mask energy; each passes normal type certification. Contrast a +jammer, whose defining purpose is to emit energy that denies others service. + +--- + +## 3. The energy-conservation proof as a compliance artifact + +VEIL turns "not jamming" from a promise into a **checked property.** The +`compliance::ComplianceReport` audits each protection step: + +``` +input_energy = ‖report_before‖² +output_energy = ‖report_after‖² +energy_ratio = output_energy / input_energy # ≈ 1.0 for a rotation +energy_conserving = |energy_ratio − 1| ≤ 1e-2 +adds_interfering_energy = false # by construction +is_compliant = energy_conserving ∧ ¬adds_interfering_energy +``` + +A regulator, an auditor, or the runtime attestation layer (ADR-141) can read the +report and verify the shield is a waveform-shaping control, not an interference +source. On the reference experiment the measured ratio is **1.000000**. + +--- + +## 4. Jurisdictional notes + +- **EU (GDPR framing).** Covert WiFi body-sensing of vital signs is sensitive + health data and "almost certainly illegal under GDPR," but effectively + unenforceable (receivers are undetectable) — which is precisely why a + *technical* control is needed. VEIL as a transmit-side control does not itself + raise GDPR issues; it reduces the personal data an attacker can derive. +- **RF-emission rules are jurisdiction-specific.** The energy-preserving property + is the portable core of the compliance argument, but power/mask/timing limits + differ by region and band; a deployment must confirm local rules. +- **Deliberate transmit-nulling toward a *located* sniffer** (steering a spatial + null at a known passive receiver) is still the node's own emission and adds no + interference, but is more aggressive and should get explicit regulatory review + before field use. It is not part of the default VEIL profile. + +--- + +## Sources + +- 47 U.S.C. §333: https://www.law.cornell.edu/uscode/text/47/333 +- 47 U.S.C. §302a: https://www.law.cornell.edu/uscode/text/47/302a +- FCC Jammer Enforcement: https://www.fcc.gov/general/jammer-enforcement · https://www.fcc.gov/enforcement/areas/jammers +- FCC Cell/GPS Jamming guidance: https://www.fcc.gov/general/cell-phone-and-gps-jamming +- FCC 14-92 enforcement order: https://docs.fcc.gov/public/attachments/FCC-14-92A1.pdf + +*Caveat: FCC pages were cross-verified against Cornell LII; this is engineering +analysis, not legal advice.* diff --git a/docs/research/privacy-shield/05-experiment-protocol.md b/docs/research/privacy-shield/05-experiment-protocol.md new file mode 100644 index 00000000..cc168d77 --- /dev/null +++ b/docs/research/privacy-shield/05-experiment-protocol.md @@ -0,0 +1,108 @@ +# 05 — Experiment Protocol: Attacker vs. Protector + +This is the "start today" deliverable from the brief: **make one RuView node the +attacker and one the protector, and measure whether protection drives identity +recognition toward chance while keeping throughput above 95%.** It is realized as +a deterministic, reproducible experiment in +[`v2/crates/wifi-densepose-privshield`](../../../v2/crates/wifi-densepose-privshield). + +Because it runs on **SYNTHETIC** data (no radio is touched), its numbers describe +the model, not real hardware — reproduced by `cargo test`, and to be +re-established on silicon with a captured log before any deployment claim. + +--- + +## 1. Setup + +- **Protector node.** Emits beamforming feedback shaped by the VEIL controls + (keyed per-session fine-subspace rotation + configured feedback resolution and + sounding overhead). Models a legitimate AP/STA protecting a room. +- **Attacker node.** A passive sniffer that enrolls a template per candidate from + captured reports, then classifies fresh captures (nearest-centroid) — the + BFId-class re-identification threat. +- **Scene.** `SceneConfig` default: 64-dim report, 8 comm dims, **16 candidate + identities** (chance = 1/16 = 6.25%), per-identity stable fine-block signature + + per-session environmental nuisance. + +Two runs of the attacker are compared: **shield off** (the attacker sees raw +reports) and **shield on** (every captured report is VEIL-protected). The same +attacker faces both. + +--- + +## 2. Metrics and acceptance bar + +| Metric | Definition | Bar | +|---|---|---| +| **Re-ID accuracy, shield off** | Top-1 identity accuracy on unprotected traffic | Must be well above chance (threat is real) — bar ≥ 0.5 | +| **Re-ID accuracy, shield on** | Top-1 identity accuracy on protected traffic | Must fall into the chance band `1/N · 2 + 0.03` | +| **Throughput ratio** | Protected link capacity ÷ baseline capacity | **≥ 0.95** | +| **Compliance** | Emission energy ratio ≈ 1 and non-interfering | `is_compliant == true` | + +Overall `passed()` requires all four. + +--- + +## 3. Results (SYNTHETIC, default configuration) + +Reproduce with `cargo test -p wifi-densepose-privshield` (all 29 tests + doctest +pass). Salient values from the reference run: + +| Metric | Value | +|---|---| +| Candidate identities | 16 | +| Chance level | 6.25% | +| Chance band (acceptance) | ≤ 15.5% | +| **Re-ID accuracy, shield OFF** | **100.0%** | +| **Re-ID accuracy, shield ON** | **7.8%** | +| **Throughput ratio** | **97.9997%** | +| Emission energy ratio | 1.000000 | +| Overall verdict | **PASS** | + +Reading the result: the attacker is a *perfect* re-identifier without protection +(the synthetic signatures are cleanly separable), and VEIL drives it to within +1.6 points of the ideal chance floor — while the modeled link keeps 98% of its +throughput and the emission conserves energy exactly (compliant, not jamming). + +--- + +## 4. Determinism and the witness + +The experiment is byte-reproducible: no OS entropy, no wall-clock, no threads. +`proof::Proof` folds the salient outputs (quantized to avoid last-bit f32 +round-off) into an FNV-1a witness pinned as `EXPECTED_WITNESS`. Any drift in the +PRNG stream, rotation schedule, throughput formula, or scene geometry changes the +witness and fails `witness_matches_pinned`. This is the same +deterministic-proof discipline as `nvsim` and the Python `verify.py`. + +--- + +## 5. Sensitivity and what to vary next + +`ExperimentConfig` exposes the levers for a fuller study: + +- **`scene.identities`** — larger N lowers the chance floor; confirm collapse + holds as candidates grow. +- **`scene.env_sigma` / `beam_amplitude`** — nuisance and comm energy; stress the + separability assumption. +- **`shield.feedback_bits`** — trace the privacy–throughput curve (the + `throughput` tests already show coarse resolution costs more). +- **`shield.givens_passes`** — mixing strength; fewer passes should degrade the + collapse gracefully. +- **Stronger attacker** — swap in a learned classifier to confirm the collapse is + signal-level, not classifier-level (the argument says it must be, but a + hardware study should verify). + +--- + +## 6. Path to a real two-node measurement + +The synthetic experiment is the design proof. The hardware path (per CLAUDE.md, +requires a captured log to claim MEASURED): + +1. Two ESP32-S3/C6 or Nexmon-capable nodes: one runs Wi-BFI capture (attacker), + one runs a VEIL-shaped feedback profile (protector). +2. Enroll and test the same BFId-style classifier on captured BFI, shield off vs. + on; log throughput via iperf across the legitimate link. +3. Success = the same shape as §3 on real captures, with the boot/runtime log as + the witness. Until then, all defense numbers remain SYNTHETIC. diff --git a/docs/research/privacy-shield/06-market-and-buyers.md b/docs/research/privacy-shield/06-market-and-buyers.md new file mode 100644 index 00000000..97cf1f91 --- /dev/null +++ b/docs/research/privacy-shield/06-market-and-buyers.md @@ -0,0 +1,92 @@ +# 06 — Market and Buyers + +Facts are tagged **VERIFIED** (from a cited source), **CLAIMED** (asserted by a +vendor/analyst/press source), or **SPECULATIVE** (our inference). Market figures +are third-party projections, not independent measurements. + +--- + +## 1. Why now + +- **The threat is standardized and commercializing (VERIFIED/CLAIMED).** IEEE + 802.11bf was published Sep 2025; silicon (Infineon AIROC Wi-Fi 7 ACW741x, + Qualcomm Dragonwing) lists 802.11bf sensing in 2026 briefs; Origin AI's + embedded-sensing program targets late-2026 deployment; Plume/Cognitive Systems + WiFi Motion is the largest deployed sensing footprint today. +- **The standards body declined to fix privacy (VERIFIED).** The BFI + "secure transmission mechanism" proposal (802.11-23/0782) was **withdrawn**; + 802.11bf shipped with no privacy protections. This is the strongest demand + signal — the gap is structural and acknowledged. +- **No targeted anti-sensing product ships (VERIFIED by absence).** Every + countermeasure (IRShield, PhyCloak, MIMOCrypt, DP-Givens, ScatterShield) is + research-stage. The claim "no obvious shipping product protects rooms from this + inference" **holds** as of 2026, with one caveat below. + +--- + +## 2. First buyers, ranked by procurement readiness + +| Segment | Driver | Readiness | +|---|---|---| +| **Defence / government** | ICD 705 / DoD EMSEC already mandate RF attenuation in classified spaces; budgets and mandates exist | **Strongest beachhead (VERIFIED)** — but today they buy broadband shielding, not a sensing-specific control | +| **Corporate boardrooms / counter-espionage** | TSCM firms (Bastille, Murray Associates) now include WiFi audits and rogue-AP detection; CSI keystroke/gesture inference makes a boardroom shield a natural extension | **VERIFIED demand, EMERGING WiFi-specific** | +| **Hospitals** | RF-derived behavioral/vital data is HIPAA PHI; exam rooms, psychiatric units where inference is unwanted | **VERIFIED regulatory hook** — but the hook drives privacy-preserving *sensing* more than a *shield* | +| **Hotels** | Documented guest backlash against in-room sensors; privacy as differentiation | **SPECULATIVE** — narrative-led, not procurement-led today | +| **Router / AP manufacturers** | Ship opt-out/obfuscation as a firmware feature anticipating regulation | **SPECULATIVE** — no vendor has announced this | + +--- + +## 3. Competitive landscape + +- **Direct competitors:** none shipping. All targeted anti-sensing is academic. +- **The real substitute (VERIFIED):** broadband RF shielding — SCIF/TEMPEST + window film, paint, panels (Signals Defense SD2500: >40 dB, 30 MHz–6 GHz, ICD + 705 / ASTM F3057-14). It defeats WiFi sensing as a side effect but is **blunt**: + it kills *all* RF and cannot coexist with wanted WiFi. +- **TSCM services (VERIFIED):** detect, don't prevent. + +**VEIL's differentiation** is exactly what the substitute lacks: **selective and +coexisting** — it removes identity/activity leakage while keeping the room's WiFi +working at ≥95% throughput, with a machine-checkable compliance artifact. + +--- + +## 4. Market size (third-party projections, cite with care) + +- **CLAIMED:** ABI Research — North American WiFi-sensing-compatible CPE install + base to **112M by 2030 (51.6% CAGR)**. +- **CLAIMED:** Global WiFi sensing market ~$402M (2024) → ~$2.13B (2033) + (MarketIntelo). + +Implication: a shield must **coexist** with a large installed sensing base, not +assume RF denial — reinforcing the selective-coexistence positioning. + +--- + +## 5. Where VEIL fits RuView's positioning + +VEIL pairs with BFLD to make RuView the *both-sides* RF-perception platform: +BFLD/AETHER do sensing responsibly and detect leakage; VEIL is the customer- +facing **privacy firewall** that protects a room from *others'* sensing. That is a +defensible, standards-anchored, gap-filling story: the standards body left the +door open, the threat is shipping, and no one else sells the selective lock. + +--- + +## Sources + +- IEEE 802.11bf privacy-proposal withdrawal (802.11-23/0782), summarized: https://pascalpiron.substack.com/p/wifi-sensing-and-the-privacy-fix +- NIST 802.11bf: https://www.nist.gov/publications/ieee-80211bf-enabling-widespread-adoption-wi-fi-sensing +- IRShield: https://arxiv.org/abs/2112.01967 · MIMOCrypt: https://arxiv.org/pdf/2309.00250 · ScatterShield: https://dl.acm.org/doi/abs/10.1145/3770653 · WiShield JSAC 2024: https://dl.acm.org/doi/abs/10.1109/JSAC.2024.3414597 +- Signals Defense TEMPEST/SCIF film: https://signalsdefense.com/tempest-and-scif-design/ · https://signalsdefense.com/shielding-films/ +- National Shielding SCIF/ICD-705: https://www.national-shielding.com/pages/scif-icd-705-secure-facility-shielding +- Bastille TSCM: https://bastille.net/centers-of-excellence/tscm/ · IntellSIG TSCM overview: https://www.intellsig.com/2025/07/20/modern-eavesdropping-threats-a-tscm-overview/ +- Origin AI program: https://www.prnewswire.com/news-releases/origin-ai-launches-compatible-with-origin-program-to-meet-industry-demand-for-scalable-wifi-sensing-and-accelerate-integration-across-global-soc-platforms-302650963.html +- MIT Tech Review, WiFi sensing: https://www.technologyreview.com/2024/02/27/1088154/wifi-sensing-tracking-movements/ +- ABI Research 112M forecast: https://www.abiresearch.com/press/north-american-wi-fi-sensing-cpe-installations-to-surge-to-112-million-by-2030-as-the-technologys-maturing-unleashes-new-business-and-service-models +- MarketIntelo WiFi sensing market: https://marketintelo.com/report/wi-fi-sensing-market +- HIPAA/PHI RF-sensing context (PMC): https://pmc.ncbi.nlm.nih.gov/articles/PMC11939480/ + +*Caveat: market figures are analyst/vendor projections; the "no shipping product" +finding reflects absence of evidence in these searches and should be confirmed +with a patent/vendor scan before anchoring a go-to-market claim.* diff --git a/docs/research/privacy-shield/07-implementation-and-roadmap.md b/docs/research/privacy-shield/07-implementation-and-roadmap.md new file mode 100644 index 00000000..080319b6 --- /dev/null +++ b/docs/research/privacy-shield/07-implementation-and-roadmap.md @@ -0,0 +1,82 @@ +# 07 — Implementation and Roadmap + +--- + +## 1. What ships in this bundle + +- **Reference crate** `v2/crates/wifi-densepose-privshield` (VEIL): a + deterministic, dependency-free, WASM-ready pure-compute leaf implementing the + full attacker-vs-protector experiment, the four compliant controls, the + throughput model, the compliance audit, and a byte-stable proof. 29 tests + + doctest pass; builds for `wasm32-unknown-unknown`; clippy-clean. +- **This research bundle** (`docs/research/privacy-shield/`). +- **[ADR-288](../../adr/ADR-288-veil-privacy-shield-compliant-waveform.md)** — the + formal decision record. + +The crate is intentionally a **leaf with no internal RuView dependencies** +(mirrors `wifi-densepose-aether`), so it can be reasoned about, fuzzed, and +ported independently, and so it can never accidentally acquire a path to a radio. + +--- + +## 2. Reuse map (how VEIL composes with existing RuView) + +| Existing subsystem | Relationship | +|---|---| +| **BFLD** (ADR-118/120/121, `wifi-densepose-bfld`) | Detection layer. Its `identity_risk_score` is the natural trigger for VEIL's `SensingDetector` — detect leakage, then shield | +| **Privacy control plane** (ADR-141) | VEIL protection steps emit `ComplianceReport`s that fit the runtime-attestation model (which mode, which actions, which fields) | +| **Active sensing / governed actuation** (ADR-280) | VEIL is a defensive `SensingAction`: a governed, privacy-ceiling-bounded emission-shaping action the control plane can schedule | +| **Givens/beamforming primitives** | VEIL reuses the report's native Givens-rotation structure rather than inventing a new transform | +| **Deterministic proof discipline** (`nvsim`, `archive/v1/verify.py`) | VEIL's `proof` module follows the same pinned-witness pattern | + +--- + +## 3. Phased rollout + +| Phase | Deliverable | Evidence class | +|---|---|---| +| **P1 — reference model (this PR)** | Crate + experiment + docs + ADR | SYNTHETIC (cargo test) | +| **P2 — sensitivity study** | Sweep N, noise, resolution, mixing; add a learned attacker to confirm signal-level collapse | SYNTHETIC | +| **P3 — BFLD integration** | Wire `identity_risk` → `SensingDetector` → shield engage; emit attestation | SYNTHETIC + integration tests | +| **P4 — firmware feedback shaping** | Implement keyed fine-subspace rotation + cadence randomization in ESP32/Nexmon feedback path | build + hardware | +| **P5 — two-node hardware measurement** | Wi-BFI attacker vs. VEIL protector on real silicon; iperf throughput; captured log | **MEASURED** (with witness) | +| **P6 — deployment profiles** | Per-segment profiles (SCIF, boardroom, ward) with regulatory review | operational | + +No defense claim graduates from SYNTHETIC to MEASURED without a captured +boot/runtime log (CLAUDE.md hardware rule). + +--- + +## 4. Open problems (tracked honestly) + +1. **Real-hardware separability.** Comm and identity information are only + *approximately* separable on real radios; the true throughput cost of full + identity hiding may exceed the model's ~2%. P2/P5 must bound it. +2. **Within-session motion leakage.** A fixed per-session rotation does not + obfuscate coarse motion within one capture window. Needs stronger cadence + randomization or amplitude shaping; currently a stated non-goal for the re-ID + metric. +3. **Active adversary (A2).** An attacker that transmits its own soundings is + only partially addressed by cadence control; a MAC-layer non-response policy + is needed. +4. **Key management.** The per-session rotation key must be derived from the + negotiated link secret; VEIL's PRNG is explicitly *not* cryptographic and must + not be used for real key material. +5. **Regulatory review per jurisdiction.** The energy-conservation argument is + portable, but power/mask/timing limits and any transmit-nulling profile need + local review before field use. + +--- + +## 5. Validation commands + +```bash +# Reference experiment + all unit/proof/doc tests +cargo test -p wifi-densepose-privshield --no-default-features + +# WASM portability (leaf builds with no radio path) +cargo build -p wifi-densepose-privshield --target wasm32-unknown-unknown + +# Lints +cargo clippy -p wifi-densepose-privshield --all-targets +``` diff --git a/docs/research/privacy-shield/README.md b/docs/research/privacy-shield/README.md new file mode 100644 index 00000000..22163516 --- /dev/null +++ b/docs/research/privacy-shield/README.md @@ -0,0 +1,95 @@ +# Privacy Shield Research Bundle — VEIL + +**VEIL** (Verifiable Emission-shaping for Identity-Leakage prevention) is a +privacy *firewall* for WiFi sensing: it prevents unauthorized identity and +activity inference from a room's WiFi while preserving normal communications. It +is the **countermeasure** counterpart to [BFLD](../BFLD/) — where BFLD *detects* +when beamforming feedback becomes identifying, VEIL *acts* by shaping the node's +own compliant waveform (channel sounding, precoder phase, beam/feedback +schedules) so identity and activity inference fail, while a legitimate receiver +sees an essentially unchanged link. + +**This must use compliant waveform controls, never jamming.** Every technique +here operates on the defender's *own* legitimately transmitted, standards- +conformant frames. Nothing adds energy to interfere with another station's +transmission (the statutory definition of jamming, 47 U.S.C. §333/§302a). + +--- + +## Table of contents + +| File | Purpose | +|------|---------| +| [01-sota-survey.md](01-sota-survey.md) | State of the art: identity/activity inference attacks (BFI + CSI), the IEEE 802.11bf-2025 standard, and privacy-preserving countermeasures | +| [02-threat-model.md](02-threat-model.md) | Adversary classes, what VEIL defends and what it explicitly does not, trust boundary | +| [03-countermeasure-design.md](03-countermeasure-design.md) | The compliant waveform controls, the separable-subspace principle, keyed Givens-rotation shield, and how it maps to the crate | +| [04-compliance-and-regulatory.md](04-compliance-and-regulatory.md) | The legal line between compliant waveform control and jamming, with statutory citations | +| [05-experiment-protocol.md](05-experiment-protocol.md) | The attacker-vs-protector experiment: metrics, acceptance bar, reproducer, and results | +| [06-market-and-buyers.md](06-market-and-buyers.md) | First buyers, procurement drivers, competitive landscape, and the standards-body gap | +| [07-implementation-and-roadmap.md](07-implementation-and-roadmap.md) | Crate layout, reuse map, hardware path, phased rollout, and open problems | + +Formal decision: [ADR-288](../../adr/ADR-288-veil-privacy-shield-compliant-waveform.md). +Reference implementation: [`v2/crates/wifi-densepose-privshield`](../../../v2/crates/wifi-densepose-privshield). + +--- + +## Executive summary + +1. **The threat is real and now standardized.** IEEE 802.11ac/ax beamforming + feedback (BFI) — the compressed Givens-rotation angle matrices (φ/ψ) a client + sends the AP — travels **unencrypted on the management plane**. Any device in + monitor mode can capture it for every client at once, no network access, and + the target need carry no device. **BFId** (KIT, ACM CCS 2025) re-identifies + individuals from BFI alone; **LeakyBeam** (NDSS 2025) detects occupancy + through walls at ~20 m from BFI; **BeamSense** recognizes activities at up to + 99.28% from BFI. IEEE Std **802.11bf-2025** (published 26 Sep 2025) + standardizes the sensing measurement/feedback surface these attacks abuse. + +2. **The standards body declined to fix it.** A 2023 proposal for a BFI + "secure transmission mechanism" (IEEE 802.11-23/0782) was **withdrawn** — + the working group did not align on characterizing sensing privacy as a + distinct problem. 802.11bf shipped without privacy protections. This is the + single strongest demand signal: the gap is structural and acknowledged. + +3. **No targeted anti-sensing product ships (as of 2026).** Every countermeasure + in the literature — IRShield, PhyCloak, MIMOCrypt, DP-Givens dithering, + ScatterShield — is research-stage. The only shipping substitute is broadband + RF shielding (SCIF/TEMPEST film/paint), which is blunt: it kills *all* RF and + cannot coexist with wanted WiFi. The whitespace is a **selective, coexisting, + software/PHY** shield. + +4. **The VEIL mechanism.** Identity leaks through the *fine* cross-subcarrier + phase structure of a beamforming report; throughput rides the *dominant* + beam direction. These are (mostly) separable subspaces. VEIL composes extra + **keyed Givens rotations** over the fine subspace only. The rotation is + *orthogonal* (energy-preserving ⇒ not jamming), *keyed per session* (the + legitimate receiver inverts it ⇒ throughput preserved), and *fresh each + session* (a sniffer cannot average it back ⇒ re-ID collapses to chance). + +5. **Measured on the reference model (SYNTHETIC).** On the default synthetic + scene (16 candidate identities), a passive nearest-centroid re-identifier + scores **100% with the shield off** and **7.8% with it on** (chance = 6.25%), + while modeled link throughput stays at **98.0%** of baseline and the emission + energy ratio is **1.000000** (compliant). Reproduce: + `cargo test -p wifi-densepose-privshield`. + +6. **Scope, honestly.** VEIL defends against a *third-party passive sniffer*. It + does **not** hide identity from the associated AP (that party holds the key) + — that is BFLD's detection/policy problem. VEIL is a reference model, not + hardware: real-silicon validation (per CLAUDE.md) is future work with a + captured-log witness. + +--- + +## Evidence discipline + +Per repository policy, every quantitative claim is tagged: + +- **MEASURED** — from a cited primary source with its metric and conditions. +- **CLAIMED** — asserted by a source (vendor PR, press, standards minutes) + without an independent measurement. +- **SYNTHETIC** — produced by VEIL's own deterministic model; reproduced by + `cargo test`, describing the model and not real hardware. + +WiFi sensing is never presented here as camera-grade, and no VEIL result implies +a defense guarantee on real silicon until a hardware witness exists. diff --git a/v2/Cargo.toml b/v2/Cargo.toml index 38c0d88a..22ba4e03 100644 --- a/v2/Cargo.toml +++ b/v2/Cargo.toml @@ -21,6 +21,7 @@ members = [ "crates/wifi-densepose-train", "crates/wifi-densepose-sensing-server", "crates/wifi-densepose-aether", # ADR-185 §13 — AETHER pure-compute leaf (std-only) + "crates/wifi-densepose-privshield", # ADR-288 — VEIL privacy shield (compliant-waveform anti-sensing; std-only leaf) "crates/wifi-densepose-wifiscan", "crates/wifi-densepose-vitals", "crates/wifi-densepose-ruvector", diff --git a/v2/crates/wifi-densepose-privshield/Cargo.toml b/v2/crates/wifi-densepose-privshield/Cargo.toml new file mode 100644 index 00000000..f1cf1491 --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "wifi-densepose-privshield" +description = "VEIL privacy shield (ADR-288): compliant-waveform countermeasure against unauthorized WiFi sensing. Deterministic attacker-vs-protector experiment that drives beamforming-feedback identity inference toward chance while preserving link throughput. Std-only pure-compute leaf, no async/server/RF-hardware deps; SYNTHETIC data only." +version = "0.1.0" +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +documentation.workspace = true +keywords.workspace = true +categories.workspace = true +readme = "README.md" + +# Intentionally dependency-free (mirrors `wifi-densepose-aether`, ADR-185 §13). +# VEIL is a pure-compute experiment/reference: no `rand` (its own deterministic +# PRNG), no `std::time`/`std::fs`/`std::env`/threads, so it builds unchanged for +# `wasm32-unknown-unknown` and can never emit RF or touch a radio. The shield +# *models* compliant waveform controls; it does not drive hardware. +[dependencies] + +[dev-dependencies] + +[lib] +name = "wifi_densepose_privshield" +path = "src/lib.rs" diff --git a/v2/crates/wifi-densepose-privshield/README.md b/v2/crates/wifi-densepose-privshield/README.md new file mode 100644 index 00000000..7deb4cbd --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/README.md @@ -0,0 +1,69 @@ +# wifi-densepose-privshield — VEIL + +**VEIL** (Verifiable Emission-shaping for Identity-Leakage prevention) is the +compliant-waveform **countermeasure** counterpart to +[BFLD](../wifi-densepose-bfld) (ADR-118/121). BFLD *detects* when beamforming +feedback becomes identifying; VEIL *acts* — it shapes a node's own outgoing +beamforming feedback so that an unauthorized passive sniffer cannot +re-identify people or infer activity, while a legitimate receiver (which shares +the per-session key) sees an essentially unchanged link. + +This crate is a **deterministic, dependency-free, WASM-ready reference and +experiment** — not a radio driver. It never emits RF. Every number it prints is +`SYNTHETIC`, reproduced by `cargo test -p wifi-densepose-privshield`. + +See [ADR-288](../../../docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md) +and the [research bundle](../../../docs/research/privacy-shield/). + +## The idea + +Identity leaks through the **fine** cross-subcarrier phase structure of a +compressed beamforming report; data throughput rides the **dominant** beam +direction. These live in (mostly) separable subspaces. VEIL composes extra +**keyed Givens rotations** — the exact primitive the report is already built +from — over the *fine* subspace only: + +| Property | Consequence | +|---|---| +| **Orthogonal** (energy-preserving) | No added transmit power ⇒ **not jamming** (47 U.S.C. §333/§302a) | +| **Keyed per session** | The legitimate AP inverts it ⇒ throughput preserved | +| **Fresh each session** | A sniffer sees a different rotation every time and can't average it back ⇒ re-identification collapses to chance | + +## Result (default synthetic scene, N = 16 identities) + +| Metric | Shield off | Shield on | +|---|---|---| +| Passive re-ID accuracy | **100%** | **7.8%** (chance = 6.25%) | +| Link throughput ratio | 100% | **98.0%** | +| Emission energy ratio | — | **1.000000** (compliant) | + +## Threat model & scope (stated plainly) + +VEIL defends against a **third-party passive sniffer** capturing plaintext +beamforming feedback. It does **not** hide identity from the AP a node is +associated with (that party holds the key by construction) — that is BFLD's +detection/policy problem, not this shield's. It is **compliant by +construction**: it only shapes the node's own standards-conformant frames, never +transmits to interfere with another station, and never operates an unauthorized +emitter. It is not jamming, not RF denial, and not a claim of camera-grade +anything. + +## Run it + +```bash +cargo test -p wifi-densepose-privshield --no-default-features +``` + +## Modules + +| Module | Purpose | +|---|---| +| `prng` | Deterministic, WASM-safe PRNG + key derivation | +| `linalg` | Givens-rotation vector algebra | +| `identity` | SYNTHETIC two-subspace beamforming-feedback model | +| `protector` | The compliant waveform controls (the shield) | +| `attacker` | Passive re-identification adversary | +| `throughput` | Link-throughput model | +| `compliance` | Machine-checkable "not jamming" audit | +| `experiment` | Attacker-vs-protector head-to-head | +| `proof` | Byte-stable deterministic witness | diff --git a/v2/crates/wifi-densepose-privshield/src/attacker.rs b/v2/crates/wifi-densepose-privshield/src/attacker.rs new file mode 100644 index 00000000..eabf82b4 --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/attacker.rs @@ -0,0 +1,118 @@ +//! The adversary: a passive re-identification classifier over captured +//! beamforming feedback. +//! +//! The attacker models the BFId/CCS-2025 threat: a sniffer that enrolls a +//! template per candidate from observed reports, then classifies fresh +//! captures. We use a **nearest-centroid** classifier over the full report +//! vector. It is deliberately simple but is the right shape for the effect +//! under test: it succeeds exactly when a *stable* per-identity signature +//! survives across capture sessions, and fails when the signature is rotated +//! unpredictably each session (which is what the protector does). +//! +//! Nearest-centroid is also the honest choice for the collapse claim: a more +//! elaborate classifier cannot recover identity that has been mapped through a +//! fresh secret orthogonal transform each session — the mutual information +//! between a Haar-rotated signature and the identity label, marginalized over +//! unknown rotations, is what the protector drives down. The classifier +//! strength is not the lever; signature stability is. + +use crate::identity::BfiSample; +use crate::linalg::dist_sq; + +/// A nearest-centroid re-identification attacker. +#[derive(Debug, Clone, Default)] +pub struct NearestCentroidAttacker { + centroids: Vec>, + ids: Vec, +} + +impl NearestCentroidAttacker { + /// Build an empty attacker. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Enroll from labeled captures: one centroid per identity, the mean of + /// that identity's observed report vectors. + pub fn enroll(&mut self, samples: &[(usize, BfiSample)]) { + // Group by identity, preserving first-seen order. + let mut ids: Vec = Vec::new(); + let mut sums: Vec> = Vec::new(); + let mut counts: Vec = Vec::new(); + for (id, s) in samples { + let slot = ids.iter().position(|x| x == id).unwrap_or_else(|| { + ids.push(*id); + sums.push(vec![0.0; s.values.len()]); + counts.push(0); + ids.len() - 1 + }); + for (acc, v) in sums[slot].iter_mut().zip(&s.values) { + *acc += v; + } + counts[slot] += 1; + } + for (sum, &c) in sums.iter_mut().zip(&counts) { + if c > 0 { + let inv = 1.0 / c as f32; + for v in sum.iter_mut() { + *v *= inv; + } + } + } + self.ids = ids; + self.centroids = sums; + } + + /// Classify a capture to the nearest enrolled centroid. Returns the + /// predicted identity, or `None` if the attacker has not enrolled. + #[must_use] + pub fn classify(&self, sample: &BfiSample) -> Option { + let mut best: Option<(usize, f32)> = None; + for (id, c) in self.ids.iter().zip(&self.centroids) { + let d = dist_sq(c, &sample.values); + if best.is_none_or(|(_, bd)| d < bd) { + best = Some((*id, d)); + } + } + best.map(|(id, _)| id) + } + + /// Top-1 re-identification accuracy over a labeled test set. + #[must_use] + pub fn accuracy(&self, test: &[(usize, BfiSample)]) -> f32 { + if test.is_empty() { + return 0.0; + } + let correct = test + .iter() + .filter(|(id, s)| self.classify(s) == Some(*id)) + .count(); + correct as f32 / test.len() as f32 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::identity::{Channel, SceneConfig}; + + #[test] + fn attacker_re_ids_unprotected_traffic() { + let ch = Channel::new(SceneConfig::default()); + let mut enroll = Vec::new(); + let mut test = Vec::new(); + for id in 0..ch.config().identities { + for s in 0..12 { + enroll.push((id, ch.observe(id, b"enroll", s))); + } + for s in 0..12 { + test.push((id, ch.observe(id, b"test", s))); + } + } + let mut atk = NearestCentroidAttacker::new(); + atk.enroll(&enroll); + // On unprotected traffic the stable signature is trivially recovered. + assert!(atk.accuracy(&test) > 0.85); + } +} diff --git a/v2/crates/wifi-densepose-privshield/src/compliance.rs b/v2/crates/wifi-densepose-privshield/src/compliance.rs new file mode 100644 index 00000000..29f426c0 --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/compliance.rs @@ -0,0 +1,79 @@ +//! Machine-checkable compliance: the shield shapes its own frames, never jams. +//! +//! Jamming (47 U.S.C. §333, §302a) is defined by *adding energy to interfere +//! with others' transmissions*. VEIL's protector applies an **orthogonal** +//! transform to its own beamforming feedback, which preserves the report's +//! energy exactly. This module turns that invariant into a checked artifact: it +//! measures the input/output energy of a protection step and asserts the ratio +//! is ~1, i.e. no energy was added. A regulator, an auditor, or the runtime +//! attestation layer (ADR-141) can read a [`ComplianceReport`] and see the +//! shield is a waveform-shaping control, not an emitter of interference. + +use crate::identity::BfiSample; +use crate::linalg::norm_sq; + +/// Tolerance on the energy ratio. Orthogonal rotations are exact up to f32 +/// round-off across many Givens passes. +pub const ENERGY_TOLERANCE: f32 = 1e-2; + +/// The result of auditing one protection step. +#[derive(Debug, Clone, PartialEq)] +pub struct ComplianceReport { + /// Energy of the report before protection. + pub input_energy: f32, + /// Energy of the report after protection. + pub output_energy: f32, + /// `output_energy / input_energy`. ~1.0 for an energy-preserving control. + pub energy_ratio: f32, + /// True iff the energy ratio is within [`ENERGY_TOLERANCE`] of 1.0. + pub energy_conserving: bool, + /// True iff the control adds energy on top of another station's signal. + /// Always false for VEIL by construction — it transforms its own report. + pub adds_interfering_energy: bool, +} + +impl ComplianceReport { + /// Audit a `(before, after)` protection pair. + #[must_use] + pub fn audit(before: &BfiSample, after: &BfiSample) -> Self { + let input_energy = norm_sq(&before.values); + let output_energy = norm_sq(&after.values); + let energy_ratio = if input_energy > 1e-12 { + output_energy / input_energy + } else { + 1.0 + }; + Self { + input_energy, + output_energy, + energy_ratio, + energy_conserving: (energy_ratio - 1.0).abs() <= ENERGY_TOLERANCE, + adds_interfering_energy: false, + } + } + + /// The bottom-line compliance verdict: energy-preserving and + /// non-interfering ⇒ a compliant waveform control, not jamming. + #[must_use] + pub fn is_compliant(&self) -> bool { + self.energy_conserving && !self.adds_interfering_energy + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::identity::{Channel, SceneConfig}; + use crate::protector::{Protector, ShieldConfig}; + + #[test] + fn protection_is_compliant() { + let ch = Channel::new(SceneConfig::default()); + let s = ch.observe(0, b"enroll", 3); + let p = Protector::new(ShieldConfig::default()); + let out = p.protect(&s, 555); + let report = ComplianceReport::audit(&s, &out); + assert!(report.is_compliant(), "{report:?}"); + assert!((report.energy_ratio - 1.0).abs() < ENERGY_TOLERANCE); + } +} diff --git a/v2/crates/wifi-densepose-privshield/src/experiment.rs b/v2/crates/wifi-densepose-privshield/src/experiment.rs new file mode 100644 index 00000000..e1810252 --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/experiment.rs @@ -0,0 +1,230 @@ +//! The attacker-vs-protector head-to-head. +//! +//! This is the "one node is the attacker, one node is the protector" experiment +//! from the project brief, in deterministic synthetic form. It runs the passive +//! re-identification attacker ([`crate::attacker`]) twice — once against +//! unprotected traffic and once against traffic shaped by the protector +//! ([`crate::protector`]) — and reports both accuracies against the chance +//! floor, alongside the modeled link throughput ([`crate::throughput`]) and a +//! compliance audit ([`crate::compliance`]). +//! +//! Success criteria (the brief's own bar): +//! 1. protection drives re-identification toward chance (`1/identities`); +//! 2. throughput stays above 95% of the unshielded baseline; +//! 3. the control is compliant (energy-preserving, non-jamming). + +use crate::attacker::NearestCentroidAttacker; +use crate::compliance::ComplianceReport; +use crate::identity::{Channel, SceneConfig}; +use crate::prng::derive_key; +use crate::protector::{Protector, ShieldConfig}; +use crate::throughput::LinkModel; + +/// Configuration for a full experiment. +#[derive(Debug, Clone)] +pub struct ExperimentConfig { + /// Synthetic scene. + pub scene: SceneConfig, + /// Protector configuration. + pub shield: ShieldConfig, + /// Link model for the throughput estimate. + pub link: LinkModel, + /// Enrollment sessions per identity. + pub enroll_sessions: u64, + /// Test sessions per identity. + pub test_sessions: u64, + /// Accept re-ID as "at chance" if it is at or below + /// `chance × chance_multiple + chance_margin`. + pub chance_multiple: f32, + /// Additive slack on the chance band. + pub chance_margin: f32, + /// Minimum acceptable throughput ratio. + pub min_throughput_ratio: f64, +} + +impl Default for ExperimentConfig { + fn default() -> Self { + Self { + scene: SceneConfig::default(), + shield: ShieldConfig::default(), + link: LinkModel::default(), + enroll_sessions: 12, + test_sessions: 12, + chance_multiple: 2.0, + chance_margin: 0.03, + min_throughput_ratio: 0.95, + } + } +} + +/// The outcome of an experiment. +#[derive(Debug, Clone, PartialEq)] +pub struct ExperimentReport { + /// Number of candidate identities. + pub identities: usize, + /// Ideal chance-level accuracy (`1/identities`). + pub chance_level: f32, + /// Re-identification accuracy with the shield off. + pub accuracy_shield_off: f32, + /// Re-identification accuracy with the shield on. + pub accuracy_shield_on: f32, + /// Modeled throughput ratio of the protected link vs baseline. + pub throughput_ratio: f64, + /// Compliance audit of a representative protected frame. + pub compliance: ComplianceReport, + /// Upper edge of the accepted "at chance" band. + pub chance_band: f32, +} + +impl ExperimentReport { + /// Did protection drive re-identification into the chance band? + #[must_use] + pub fn drives_to_chance(&self) -> bool { + self.accuracy_shield_on <= self.chance_band + } + + /// Is the shield-off attacker meaningfully better than chance (i.e. the + /// threat is real in this scene, so the collapse is meaningful)? + #[must_use] + pub fn attack_is_effective_without_shield(&self) -> bool { + self.accuracy_shield_off >= 0.5 + } + + /// Did throughput stay above the required floor? + #[must_use] + pub fn preserves_throughput(&self) -> bool { + self.throughput_ratio >= 0.95 + } + + /// Overall pass: real threat, collapsed to chance, throughput preserved, + /// and compliant. + #[must_use] + pub fn passed(&self) -> bool { + self.attack_is_effective_without_shield() + && self.drives_to_chance() + && self.preserves_throughput() + && self.compliance.is_compliant() + } +} + +/// Build the enroll/test capture sets for a given shield, then measure attacker +/// accuracy. `shield_on` selects whether the protector is applied to every +/// captured frame (the attacker only ever sees what is transmitted). +fn measure_accuracy(cfg: &ExperimentConfig, protector: &Protector, shield_on: bool) -> f32 { + let ch = Channel::new(cfg.scene.clone()); + let mut enroll = Vec::new(); + let mut test = Vec::new(); + + for id in 0..cfg.scene.identities { + for s in 0..cfg.enroll_sessions { + let raw = ch.observe(id, b"enroll", s); + let seen = if shield_on { + // Per-session precoder rotation is the SAME for every identity + // present in that session (the AP rotates its precoder per + // sounding interval, not per person). Keying it on the session + // is what lets a legitimate receiver invert it and what makes + // the attacker's cross-session average collapse. + let key = derive_key(cfg.scene.seed, b"rot-enroll", s, 0); + protector.protect(&raw, key) + } else { + raw + }; + enroll.push((id, seen)); + } + for s in 0..cfg.test_sessions { + let raw = ch.observe(id, b"test", s); + let seen = if shield_on { + let key = derive_key(cfg.scene.seed, b"rot-test", s, 0); + protector.protect(&raw, key) + } else { + raw + }; + test.push((id, seen)); + } + } + + let mut atk = NearestCentroidAttacker::new(); + atk.enroll(&enroll); + atk.accuracy(&test) +} + +/// Run the full attacker-vs-protector experiment. +#[must_use] +pub fn run(cfg: &ExperimentConfig) -> ExperimentReport { + let protector = Protector::new(cfg.shield.clone()); + + let accuracy_shield_off = measure_accuracy(cfg, &protector, false); + let accuracy_shield_on = measure_accuracy(cfg, &protector, true); + + let throughput_ratio = cfg.link.throughput_ratio(&cfg.shield); + + // Representative compliance audit: one protected frame vs its clean form. + let ch = Channel::new(cfg.scene.clone()); + let clean = ch.observe(0, b"test", 0); + let protected = protector.protect(&clean, derive_key(cfg.scene.seed, b"rot-test", 0, 0)); + let compliance = ComplianceReport::audit(&clean, &protected); + + let chance_level = cfg.scene.chance_level(); + let chance_band = chance_level * cfg.chance_multiple + cfg.chance_margin; + + ExperimentReport { + identities: cfg.scene.identities, + chance_level, + accuracy_shield_off, + accuracy_shield_on, + throughput_ratio, + compliance, + chance_band, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn shield_off_attack_succeeds() { + let report = run(&ExperimentConfig::default()); + assert!( + report.attack_is_effective_without_shield(), + "shield-off accuracy {} should be well above chance {}", + report.accuracy_shield_off, + report.chance_level + ); + } + + #[test] + fn shield_on_drives_to_chance() { + let report = run(&ExperimentConfig::default()); + assert!( + report.drives_to_chance(), + "shield-on accuracy {} should be within chance band {}", + report.accuracy_shield_on, + report.chance_band + ); + } + + #[test] + fn shield_preserves_throughput() { + let report = run(&ExperimentConfig::default()); + assert!( + report.preserves_throughput(), + "throughput ratio {} below 0.95", + report.throughput_ratio + ); + } + + #[test] + fn overall_experiment_passes() { + let report = run(&ExperimentConfig::default()); + assert!(report.passed(), "{report:#?}"); + } + + #[test] + fn experiment_is_deterministic() { + assert_eq!( + run(&ExperimentConfig::default()), + run(&ExperimentConfig::default()) + ); + } +} diff --git a/v2/crates/wifi-densepose-privshield/src/identity.rs b/v2/crates/wifi-densepose-privshield/src/identity.rs new file mode 100644 index 00000000..ca3d3c73 --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/identity.rs @@ -0,0 +1,204 @@ +//! Synthetic beamforming-feedback model. **SYNTHETIC data only.** +//! +//! Nothing here is captured from a real radio. The model is a deliberately +//! simple, physically-motivated abstraction of a flattened 802.11 compressed +//! beamforming report, chosen so the attacker/protector dynamics are +//! transparent and the experiment is byte-reproducible. It is *not* a channel +//! simulator and its accuracy numbers describe this model, not real hardware +//! (per CLAUDE.md: results are `SYNTHETIC`, reproduced by `cargo test`). +//! +//! # The two-subspace abstraction +//! +//! A beamforming report is split into two orthogonal blocks: +//! +//! - **Comm block** (`comm_dims` leading coordinates) — the dominant beam +//! direction the AP actually uses to steer data. It varies per session with +//! position/traffic and carries **no** identity. Link throughput rides here. +//! - **Fine block** (the remainder) — the fine cross-subcarrier phase +//! structure. This is where a re-identification attacker's signal lives: the +//! literature (BFId, CCS 2025) shows the *stable* fine structure re-IDs +//! people. Communication barely uses it. +//! +//! Each identity owns a fixed, near-orthogonal signature vector in the fine +//! block. A session observation is `signature + environmental nuisance`; the +//! comm block is fresh per session. This is the honest crux of the whole +//! design: **identity leakage and data throughput live in (mostly) separable +//! subspaces**, so a transform can wreck the former while sparing the latter. + +use crate::linalg::set_norm_inplace; +use crate::prng::{derive_key, Rng}; + +/// A flattened compressed-beamforming-report vector, split into a comm block +/// and a fine block. +#[derive(Debug, Clone, PartialEq)] +pub struct BfiSample { + /// The full report: `comm_dims` comm coordinates followed by fine ones. + pub values: Vec, + /// Number of leading coordinates that form the comm (data-carrying) block. + pub comm_dims: usize, +} + +impl BfiSample { + /// Comm (data-carrying) block. + #[must_use] + pub fn comm(&self) -> &[f32] { + &self.values[..self.comm_dims] + } + + /// Fine (identity-bearing) block. + #[must_use] + pub fn fine(&self) -> &[f32] { + &self.values[self.comm_dims..] + } + + /// Mutable fine block — the only part the protector is allowed to rotate. + pub fn fine_mut(&mut self) -> &mut [f32] { + &mut self.values[self.comm_dims..] + } +} + +/// Configuration of the synthetic scene. +#[derive(Debug, Clone)] +pub struct SceneConfig { + /// Total report dimension. + pub dim: usize, + /// Leading coordinates forming the comm block. + pub comm_dims: usize, + /// Number of distinct identities (candidates). Chance level is `1/identities`. + pub identities: usize, + /// L2 norm of each identity's fine-block signature. + pub signature_norm: f32, + /// Std-dev of per-session environmental nuisance added to the fine block. + pub env_sigma: f32, + /// L2 norm of the fresh per-session comm-block beam. + pub beam_amplitude: f32, + /// Master seed. All keys derive from this; nothing touches OS entropy. + pub seed: u64, +} + +impl Default for SceneConfig { + fn default() -> Self { + Self { + dim: 64, + comm_dims: 8, + identities: 16, + signature_norm: 1.0, + env_sigma: 0.15, + beam_amplitude: 0.30, + seed: 0x5EED_1BF1, + } + } +} + +impl SceneConfig { + /// Ideal chance-level accuracy, `1 / identities`. + #[must_use] + pub fn chance_level(&self) -> f32 { + 1.0 / self.identities as f32 + } + + /// Length of the fine block. + #[must_use] + pub fn fine_dims(&self) -> usize { + self.dim - self.comm_dims + } +} + +/// Synthetic channel: turns `(identity, session)` into a [`BfiSample`]. +#[derive(Debug, Clone)] +pub struct Channel { + cfg: SceneConfig, + /// Precomputed per-identity fine-block signatures. + signatures: Vec>, +} + +impl Channel { + /// Build the channel, drawing each identity's stable signature. + #[must_use] + pub fn new(cfg: SceneConfig) -> Self { + let fine = cfg.fine_dims(); + let mut signatures = Vec::with_capacity(cfg.identities); + for id in 0..cfg.identities { + let mut rng = Rng::new(derive_key(cfg.seed, b"signature", id as u64, 0)); + let mut s: Vec = (0..fine).map(|_| rng.next_gaussian()).collect(); + set_norm_inplace(&mut s, cfg.signature_norm); + signatures.push(s); + } + Self { cfg, signatures } + } + + /// The scene configuration. + #[must_use] + pub fn config(&self) -> &SceneConfig { + &self.cfg + } + + /// The stable fine-block signature of `identity` (the thing an attacker + /// wants and the thing the shield must hide). + #[must_use] + pub fn signature(&self, identity: usize) -> &[f32] { + &self.signatures[identity] + } + + /// Observe the unprotected report for `identity` in the given session under + /// `phase` (an experiment stage label, e.g. `b"enroll"` / `b"test"`, so the + /// same session index draws independent nuisance across stages). + #[must_use] + pub fn observe(&self, identity: usize, phase: &[u8], session: u64) -> BfiSample { + let cfg = &self.cfg; + let mut values = vec![0.0f32; cfg.dim]; + + // Comm block: fresh per session, identity-independent. This is the + // data-carrying dominant beam — it holds no re-ID information. + let mut brng = Rng::new(derive_key(cfg.seed, b"beam", session, phase[0] as u64)); + for v in values[..cfg.comm_dims].iter_mut() { + *v = brng.next_gaussian(); + } + set_norm_inplace(&mut values[..cfg.comm_dims], cfg.beam_amplitude); + + // Fine block: stable identity signature + per-session nuisance. + let mut nrng = Rng::new(derive_key(cfg.seed, phase, identity as u64, session)); + let sig = &self.signatures[identity]; + for (v, s) in values[cfg.comm_dims..].iter_mut().zip(sig) { + *v = s + cfg.env_sigma * nrng.next_gaussian(); + } + + BfiSample { + values, + comm_dims: cfg.comm_dims, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::linalg::{dist_sq, norm}; + + #[test] + fn signatures_are_well_separated() { + let ch = Channel::new(SceneConfig::default()); + // Distinct identities' signatures are near-orthogonal in high-dim, + // so pairwise distance is large relative to env noise. + let d = dist_sq(ch.signature(0), ch.signature(1)).sqrt(); + assert!(d > 1.0, "signatures too close: {d}"); + } + + #[test] + fn signature_norm_matches_config() { + let ch = Channel::new(SceneConfig::default()); + assert!((norm(ch.signature(3)) - 1.0).abs() < 1e-4); + } + + #[test] + fn observation_is_deterministic() { + let ch = Channel::new(SceneConfig::default()); + assert_eq!(ch.observe(2, b"enroll", 5), ch.observe(2, b"enroll", 5)); + } + + #[test] + fn same_session_different_phase_differs() { + let ch = Channel::new(SceneConfig::default()); + assert_ne!(ch.observe(2, b"enroll", 5), ch.observe(2, b"test", 5)); + } +} diff --git a/v2/crates/wifi-densepose-privshield/src/lib.rs b/v2/crates/wifi-densepose-privshield/src/lib.rs new file mode 100644 index 00000000..e8036267 --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/lib.rs @@ -0,0 +1,82 @@ +//! # VEIL — a compliant-waveform privacy shield against WiFi sensing +//! +//! VEIL (Verifiable Emission-shaping for Identity-Leakage prevention) is the +//! countermeasure counterpart to BFLD (ADR-118/121, `wifi-densepose-bfld`). +//! Where BFLD *detects* when beamforming feedback becomes identifying, VEIL +//! *acts*: it shapes a node's own outgoing beamforming feedback so that an +//! unauthorized passive sniffer cannot re-identify people or infer activity, +//! while a legitimate receiver — which shares the per-session key — sees an +//! essentially unchanged link. +//! +//! This crate is a **deterministic, dependency-free, WASM-ready reference and +//! experiment**, not a radio driver. It models the physics faithfully enough to +//! measure the core claim, and it never emits RF. Per ADR-288 and CLAUDE.md, +//! every number it produces is `SYNTHETIC`, reproduced by +//! `cargo test -p wifi-densepose-privshield`. +//! +//! ## The idea in one paragraph +//! +//! Identity leaks through the *fine* cross-subcarrier phase structure of a +//! compressed beamforming report; data throughput rides the *dominant* beam +//! direction. These live in (mostly) separable subspaces. VEIL composes extra +//! keyed [`linalg::apply_givens`] rotations — the exact primitive the report is +//! already built from — over the **fine** subspace only. The rotation is: +//! orthogonal (energy-preserving ⇒ no added transmit power ⇒ **not jamming**, +//! [`compliance`]); keyed per session (the legitimate AP inverts it ⇒ +//! throughput preserved, [`throughput`]); and fresh each session (a sniffer +//! sees a different rotation every time and cannot average back the signature +//! ⇒ re-identification collapses to chance, [`attacker`]/[`experiment`]). +//! +//! ## Threat model and scope (stated plainly) +//! +//! VEIL defends against a **third-party passive sniffer** capturing +//! plaintext beamforming feedback. It does **not** hide identity from the AP a +//! node is associated with (that party holds the key). It is **compliant by +//! construction**: it only shapes the node's own standards-conformant frames; +//! it never transmits to interfere with another station (47 U.S.C. §333) and +//! never operates an unauthorized emitter (§302a). It is not jamming, not RF +//! denial, and not a claim of camera-grade anything. +//! +//! ## Modules +//! +//! - [`prng`] — deterministic, WASM-safe PRNG and key derivation. +//! - [`linalg`] — the small Givens-rotation vector algebra. +//! - [`identity`] — the SYNTHETIC two-subspace beamforming-feedback model. +//! - [`protector`] — the compliant waveform controls (the shield). +//! - [`attacker`] — the passive re-identification adversary. +//! - [`throughput`] — the link-throughput model. +//! - [`compliance`] — the machine-checkable "not jamming" audit. +//! - [`experiment`] — the attacker-vs-protector head-to-head. +//! - [`proof`] — the byte-stable deterministic witness. +//! +//! ## Quick start +//! +//! ``` +//! use wifi_densepose_privshield::experiment::{run, ExperimentConfig}; +//! +//! let report = run(&ExperimentConfig::default()); +//! assert!(report.attack_is_effective_without_shield()); // threat is real +//! assert!(report.drives_to_chance()); // shield collapses re-ID +//! assert!(report.preserves_throughput()); // throughput ≥ 95% +//! assert!(report.compliance.is_compliant()); // energy-preserving +//! ``` + +#![warn(missing_docs)] +#![forbid(unsafe_code)] + +pub mod attacker; +pub mod compliance; +pub mod experiment; +pub mod identity; +pub mod linalg; +pub mod prng; +pub mod proof; +pub mod protector; +pub mod throughput; + +pub use compliance::ComplianceReport; +pub use experiment::{run, ExperimentConfig, ExperimentReport}; +pub use identity::{BfiSample, Channel, SceneConfig}; +pub use proof::Proof; +pub use protector::{Protector, SensingDetector, ShieldConfig}; +pub use throughput::LinkModel; diff --git a/v2/crates/wifi-densepose-privshield/src/linalg.rs b/v2/crates/wifi-densepose-privshield/src/linalg.rs new file mode 100644 index 00000000..70c3f6dc --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/linalg.rs @@ -0,0 +1,89 @@ +//! Minimal, dependency-free vector algebra over `f32` slices. +//! +//! VEIL deliberately avoids `ndarray`/BLAS: the vectors are short (tens of +//! elements — a flattened compressed-beamforming angle report), the crate is +//! a WASM-ready leaf, and keeping the math inline makes the energy-conservation +//! proof in [`crate::compliance`] auditable line-by-line. + +/// Euclidean inner product. Panics if lengths differ. +#[must_use] +pub fn dot(a: &[f32], b: &[f32]) -> f32 { + assert_eq!(a.len(), b.len(), "dot: length mismatch"); + a.iter().zip(b).map(|(x, y)| x * y).sum() +} + +/// Squared L2 norm. +#[must_use] +pub fn norm_sq(a: &[f32]) -> f32 { + a.iter().map(|x| x * x).sum() +} + +/// L2 norm. +#[must_use] +pub fn norm(a: &[f32]) -> f32 { + norm_sq(a).sqrt() +} + +/// Squared Euclidean distance. Panics if lengths differ. +#[must_use] +pub fn dist_sq(a: &[f32], b: &[f32]) -> f32 { + assert_eq!(a.len(), b.len(), "dist_sq: length mismatch"); + a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum() +} + +/// Scale in place. +pub fn scale_inplace(a: &mut [f32], k: f32) { + for x in a.iter_mut() { + *x *= k; + } +} + +/// Normalize `a` to a target L2 norm in place. No-op if `a` is (near) zero. +pub fn set_norm_inplace(a: &mut [f32], target: f32) { + let n = norm(a); + if n > 1e-12 { + scale_inplace(a, target / n); + } +} + +/// Apply a Givens rotation to coordinates `(i, j)` of `v` by angle `theta`. +/// +/// A Givens rotation is the exact primitive 802.11 compressed beamforming +/// feedback is built from (the ψ/φ angles a beamformee reports). It is an +/// **orthogonal** operation: it preserves `‖v‖` to machine precision, which is +/// precisely why composing extra keyed Givens rotations adds *no transmit +/// energy* — the compliance argument in [`crate::compliance`]. +pub fn apply_givens(v: &mut [f32], i: usize, j: usize, theta: f32) { + debug_assert!(i < v.len() && j < v.len() && i != j); + let (c, s) = (theta.cos(), theta.sin()); + let (vi, vj) = (v[i], v[j]); + v[i] = c * vi - s * vj; + v[j] = s * vi + c * vj; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn givens_preserves_norm() { + let mut v = vec![0.3, -1.2, 0.7, 2.1, -0.5]; + let before = norm(&v); + apply_givens(&mut v, 1, 3, 0.9); + apply_givens(&mut v, 0, 4, -2.3); + apply_givens(&mut v, 2, 3, 1.1); + let after = norm(&v); + assert!((before - after).abs() < 1e-5, "{before} vs {after}"); + } + + #[test] + fn givens_is_invertible() { + let orig = vec![1.0f32, 2.0, 3.0, 4.0]; + let mut v = orig.clone(); + apply_givens(&mut v, 0, 2, 0.7); + apply_givens(&mut v, 0, 2, -0.7); + for (a, b) in orig.iter().zip(&v) { + assert!((a - b).abs() < 1e-5); + } + } +} diff --git a/v2/crates/wifi-densepose-privshield/src/prng.rs b/v2/crates/wifi-densepose-privshield/src/prng.rs new file mode 100644 index 00000000..e057f9bb --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/prng.rs @@ -0,0 +1,119 @@ +//! Deterministic, WASM-safe pseudo-random generator. +//! +//! VEIL never draws from OS entropy: every stochastic quantity in the +//! experiment (identity signatures, environmental nuisance, per-session +//! precoder rotations) seeds from an explicit `u64`. Same seed in → same +//! bytes out, on any platform including `wasm32-unknown-unknown`. This is +//! what makes [`crate::proof`] a byte-stable witness rather than a flaky +//! statistical assertion. +//! +//! The core is SplitMix64 (Steele, Lea & Flood 2014) — a well-mixed +//! finalizer that is more than adequate for synthetic-data generation and +//! keyed subspace rotation. It is **not** a cryptographic RNG and must not +//! be used to derive real key material; in a deployment the per-session +//! rotation key comes from the negotiated link secret, not from this PRNG. + +/// A deterministic SplitMix64 stream. +#[derive(Debug, Clone)] +pub struct Rng { + state: u64, +} + +impl Rng { + /// Seed the stream. Distinct seeds yield independent streams. + #[must_use] + pub fn new(seed: u64) -> Self { + Self { + state: seed ^ 0x9E37_79B9_7F4A_7C15, + } + } + + /// Next raw 64-bit word. + pub fn next_u64(&mut self) -> u64 { + self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + /// Uniform `f32` in `[0, 1)` using the top 24 mantissa bits. + pub fn next_f32(&mut self) -> f32 { + // 24 bits of precision keeps the value exactly representable. + ((self.next_u64() >> 40) as f32) / ((1u64 << 24) as f32) + } + + /// Uniform `f32` in `[lo, hi)`. + pub fn next_range(&mut self, lo: f32, hi: f32) -> f32 { + lo + (hi - lo) * self.next_f32() + } + + /// Standard-normal `f32` via the Box–Muller transform. + pub fn next_gaussian(&mut self) -> f32 { + let u1 = self.next_f32().max(1e-7); + let u2 = self.next_f32(); + (-2.0 * u1.ln()).sqrt() * (core::f32::consts::TAU * u2).cos() + } +} + +/// FNV-1a 64-bit hash — a dependency-free, deterministic byte folder used to +/// derive per-session keys from `(scene_seed, phase, index)` tuples and to +/// build the [`crate::proof`] witness. Not cryptographic. +#[must_use] +pub fn fnv1a_64(bytes: &[u8]) -> u64 { + let mut h: u64 = 0xCBF2_9CE4_8422_2325; + for &b in bytes { + h ^= u64::from(b); + h = h.wrapping_mul(0x0000_0100_0000_01B3); + } + h +} + +/// Fold a label and two indices into a stable `u64` key. +#[must_use] +pub fn derive_key(scene_seed: u64, label: &[u8], a: u64, b: u64) -> u64 { + let mut buf = Vec::with_capacity(label.len() + 24); + buf.extend_from_slice(&scene_seed.to_le_bytes()); + buf.extend_from_slice(label); + buf.extend_from_slice(&a.to_le_bytes()); + buf.extend_from_slice(&b.to_le_bytes()); + fnv1a_64(&buf) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stream_is_deterministic() { + let mut a = Rng::new(42); + let mut b = Rng::new(42); + for _ in 0..1000 { + assert_eq!(a.next_u64(), b.next_u64()); + } + } + + #[test] + fn distinct_seeds_diverge() { + let mut a = Rng::new(1); + let mut b = Rng::new(2); + assert_ne!(a.next_u64(), b.next_u64()); + } + + #[test] + fn uniform_in_range() { + let mut r = Rng::new(7); + for _ in 0..10_000 { + let x = r.next_f32(); + assert!((0.0..1.0).contains(&x)); + } + } + + #[test] + fn gaussian_mean_near_zero() { + let mut r = Rng::new(9); + let n = 100_000; + let mean: f64 = (0..n).map(|_| f64::from(r.next_gaussian())).sum::() / f64::from(n); + assert!(mean.abs() < 0.02, "mean {mean} not near 0"); + } +} diff --git a/v2/crates/wifi-densepose-privshield/src/proof.rs b/v2/crates/wifi-densepose-privshield/src/proof.rs new file mode 100644 index 00000000..ed975df3 --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/proof.rs @@ -0,0 +1,82 @@ +//! Deterministic proof bundle — the byte-stable witness for VEIL. +//! +//! Mirrors the `nvsim` / `archive/v1` proof pattern: run a fixed reference +//! experiment, fold its salient outputs into a single FNV-1a witness, and pin +//! that witness as a constant. If any constant drifts — the PRNG stream, the +//! rotation schedule, the throughput formula, the scene geometry — the witness +//! changes and the test fails loudly. +//! +//! The witness is derived from **quantized** outputs (accuracies to 1e-4, +//! throughput to 1e-6) so that legitimate cross-platform f32 round-off in the +//! last bits does not spuriously break the proof, while any real change to the +//! experiment's behavior still does. + +use crate::experiment::{run, ExperimentConfig, ExperimentReport}; +use crate::prng::fnv1a_64; + +/// Deterministic-proof harness. +pub struct Proof; + +impl Proof { + /// Pinned witness over the reference experiment. Re-derived by + /// [`Proof::witness`]; asserted by the test below. + pub const EXPECTED_WITNESS: u64 = 0xD098_C38D_B7C6_BCA9; + + /// The reference configuration. Uses every default so the proof tracks the + /// shipped behavior of the crate. + #[must_use] + pub fn reference_config() -> ExperimentConfig { + ExperimentConfig::default() + } + + /// Run the reference experiment. + #[must_use] + pub fn run_reference() -> ExperimentReport { + run(&Self::reference_config()) + } + + /// Fold a report's salient outputs into a stable witness. + #[must_use] + pub fn witness(report: &ExperimentReport) -> u64 { + let mut buf = Vec::new(); + buf.extend_from_slice(&(report.identities as u64).to_le_bytes()); + // Quantize floats before folding so last-bit round-off is not part of + // the witness. + let q4 = |x: f32| (f64::from(x) * 10_000.0).round() as i64; + let q6 = |x: f64| (x * 1_000_000.0).round() as i64; + buf.extend_from_slice(&q4(report.chance_level).to_le_bytes()); + buf.extend_from_slice(&q4(report.accuracy_shield_off).to_le_bytes()); + buf.extend_from_slice(&q4(report.accuracy_shield_on).to_le_bytes()); + buf.extend_from_slice(&q6(report.throughput_ratio).to_le_bytes()); + buf.extend_from_slice(&q4(report.compliance.energy_ratio).to_le_bytes()); + fnv1a_64(&buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reference_experiment_passes() { + assert!(Proof::run_reference().passed()); + } + + #[test] + fn witness_is_stable() { + let a = Proof::witness(&Proof::run_reference()); + let b = Proof::witness(&Proof::run_reference()); + assert_eq!(a, b, "witness must be reproducible"); + } + + #[test] + fn witness_matches_pinned() { + let w = Proof::witness(&Proof::run_reference()); + assert_eq!( + w, + Proof::EXPECTED_WITNESS, + "witness drifted to {w:#018x}; update EXPECTED_WITNESS only if the \ + change to the reference experiment is intentional" + ); + } +} diff --git a/v2/crates/wifi-densepose-privshield/src/protector.rs b/v2/crates/wifi-densepose-privshield/src/protector.rs new file mode 100644 index 00000000..0198c975 --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/protector.rs @@ -0,0 +1,226 @@ +//! The VEIL protector: compliant waveform controls that hide identity. +//! +//! # What it does (and does not do) +//! +//! The protector shapes the node's **own** beamforming feedback before it goes +//! on air. It applies a per-session, key-derived **orthogonal rotation** to the +//! fine block of the report, composed from extra Givens rotations — the same +//! angle primitive the report already carries. Because the rotation is: +//! +//! - **orthogonal** → it preserves the report's energy exactly (no added +//! transmit power, no out-of-mask emission → **not jamming**, see +//! [`crate::compliance`]); +//! - **keyed per session** → the legitimate AP/STA, which shares the session +//! key, inverts it and recovers the true precoder (throughput preserved, +//! see [`crate::throughput`]); +//! - **fresh each session** → an external sniffer sees a different rotation of +//! the identity signature every session and cannot average them back to the +//! signature, so cross-session re-identification collapses toward chance. +//! +//! This is the shared-secret precoding idea (cf. MIMOCrypt, NSDI-adjacent work) +//! specialized to the identity-bearing fine subspace. +//! +//! # Scope limit (stated honestly) +//! +//! VEIL defends against a **third-party passive sniffer**. It does *not* hide +//! identity from the AP the node is associated with (that party holds the key +//! by construction). Protecting against a malicious AP is a different problem +//! handled by the BFLD detection layer and privacy-class policy (ADR-118/141), +//! not by this shield. VEIL never jams and never touches another station's +//! frames. + +use crate::identity::BfiSample; +use crate::linalg::apply_givens; +use crate::prng::Rng; + +/// Configuration of the protector. +#[derive(Debug, Clone)] +pub struct ShieldConfig { + /// Master switch. When `false`, [`Protector::protect`] is the identity map + /// (used to model the "shield off" baseline). + pub enabled: bool, + /// Number of keyed Givens rotations composed per session. Enough passes + /// (≈ `2 × fine_dims`) approximate a Haar-random rotation of the fine + /// block, which is what drives the attacker to chance. + pub givens_passes: usize, + /// Bits used to quantize each reported angle (802.11 uses 5–9). Higher + /// resolution ⇒ smaller uncompensated residual at the legitimate receiver + /// ⇒ smaller throughput cost. See [`crate::throughput`]. + pub feedback_bits: u32, + /// Fractional airtime overhead from sounding-cadence randomization + /// (jittering NDP intervals so an eavesdropper under-samples motion). + pub sounding_overhead: f64, +} + +impl Default for ShieldConfig { + fn default() -> Self { + Self { + enabled: true, + givens_passes: 112, // 2 × 56 fine dims at the default scene + feedback_bits: 7, + sounding_overhead: 0.02, + } + } +} + +/// Applies compliant waveform controls to outgoing beamforming feedback. +#[derive(Debug, Clone)] +pub struct Protector { + cfg: ShieldConfig, +} + +impl Protector { + /// Build a protector. + #[must_use] + pub fn new(cfg: ShieldConfig) -> Self { + Self { cfg } + } + + /// The configuration. + #[must_use] + pub fn config(&self) -> &ShieldConfig { + &self.cfg + } + + /// Build the list of `(i, j, theta)` Givens rotations for a session. The + /// legitimate receiver derives the identical list from the shared session + /// key and applies the inverse (negated angles, reversed order). + fn session_rotation(&self, fine_dims: usize, session_key: u64) -> Vec<(usize, usize, f32)> { + let mut rng = Rng::new(session_key); + let mut ops = Vec::with_capacity(self.cfg.givens_passes); + for _ in 0..self.cfg.givens_passes { + // Draw a distinct coordinate pair in the fine block. + let i = (rng.next_u64() as usize) % fine_dims; + let mut j = (rng.next_u64() as usize) % fine_dims; + if j == i { + j = (j + 1) % fine_dims; + } + let theta = rng.next_range(0.0, core::f32::consts::TAU); + ops.push((i, j, theta)); + } + ops + } + + /// Protect an outgoing report for the given session. When the shield is + /// disabled this clones the input unchanged. + #[must_use] + pub fn protect(&self, sample: &BfiSample, session_key: u64) -> BfiSample { + let mut out = sample.clone(); + if !self.cfg.enabled { + return out; + } + let fine_dims = out.fine().len(); + let ops = self.session_rotation(fine_dims, session_key); + let fine = out.fine_mut(); + for (i, j, theta) in ops { + apply_givens(fine, i, j, theta); + } + out + } + + /// Recover the true report at the legitimate receiver, which shares the + /// session key. Applies the inverse rotation. Used to demonstrate that the + /// transform is reversible for the authorized party (the basis of the + /// throughput claim), not part of the attacker's world. + #[must_use] + pub fn recover(&self, sample: &BfiSample, session_key: u64) -> BfiSample { + let mut out = sample.clone(); + if !self.cfg.enabled { + return out; + } + let fine_dims = out.fine().len(); + let ops = self.session_rotation(fine_dims, session_key); + let fine = out.fine_mut(); + for (i, j, theta) in ops.into_iter().rev() { + apply_givens(fine, i, j, -theta); + } + out + } +} + +/// A minimal detector for unsolicited sensing activity. In a deployment this +/// watches the rate of NDP/sensing-sounding solicitations; here it exposes the +/// decision rule so the control plane (ADR-280) can engage the shield only when +/// sensing is actually observed, rather than perturbing continuously. +#[derive(Debug, Clone)] +pub struct SensingDetector { + /// Solicitations per second above which the shield engages. + pub threshold_hz: f32, +} + +impl Default for SensingDetector { + fn default() -> Self { + Self { threshold_hz: 5.0 } + } +} + +impl SensingDetector { + /// Should the shield engage given the observed solicitation rate? + #[must_use] + pub fn should_engage(&self, observed_hz: f32) -> bool { + observed_hz >= self.threshold_hz + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::identity::{Channel, SceneConfig}; + use crate::linalg::{dist_sq, norm}; + + #[test] + fn protection_preserves_energy() { + let ch = Channel::new(SceneConfig::default()); + let s = ch.observe(0, b"enroll", 1); + let p = Protector::new(ShieldConfig::default()); + let out = p.protect(&s, 12345); + assert!((norm(&s.values) - norm(&out.values)).abs() < 1e-3); + } + + #[test] + fn protection_leaves_comm_block_untouched() { + let ch = Channel::new(SceneConfig::default()); + let s = ch.observe(0, b"enroll", 1); + let p = Protector::new(ShieldConfig::default()); + let out = p.protect(&s, 999); + assert_eq!(s.comm(), out.comm()); + } + + #[test] + fn protection_scrambles_fine_block() { + let ch = Channel::new(SceneConfig::default()); + let s = ch.observe(0, b"enroll", 1); + let p = Protector::new(ShieldConfig::default()); + let out = p.protect(&s, 42); + assert!(dist_sq(s.fine(), out.fine()).sqrt() > 0.5); + } + + #[test] + fn legitimate_receiver_recovers() { + let ch = Channel::new(SceneConfig::default()); + let s = ch.observe(0, b"enroll", 1); + let p = Protector::new(ShieldConfig::default()); + let out = p.protect(&s, 7); + let back = p.recover(&out, 7); + assert!(dist_sq(s.fine(), back.fine()).sqrt() < 1e-2); + } + + #[test] + fn disabled_shield_is_identity() { + let ch = Channel::new(SceneConfig::default()); + let s = ch.observe(0, b"enroll", 1); + let cfg = ShieldConfig { + enabled: false, + ..ShieldConfig::default() + }; + let p = Protector::new(cfg); + assert_eq!(s, p.protect(&s, 7)); + } + + #[test] + fn detector_engages_above_threshold() { + let d = SensingDetector::default(); + assert!(d.should_engage(10.0)); + assert!(!d.should_engage(1.0)); + } +} diff --git a/v2/crates/wifi-densepose-privshield/src/throughput.rs b/v2/crates/wifi-densepose-privshield/src/throughput.rs new file mode 100644 index 00000000..e249f496 --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/throughput.rs @@ -0,0 +1,115 @@ +//! Link-throughput model for the protected node. +//! +//! The claim under test is "throughput stays above 95% with the shield on". +//! The model is intentionally transparent and errs toward *charging* the +//! shield, not flattering it: +//! +//! - **Beamforming residual.** The legitimate receiver shares the session key +//! and inverts the protector's rotation, so it does not pay the rotation +//! itself — only the residual from quantizing the extra angles at +//! `feedback_bits` resolution. Per-angle mean-square quantization error is +//! `Δ²/12` for step `Δ = (π/2)/2^bits`; this fraction of beamforming gain is +//! lost. At 7 bits it is ~1e-5 — negligible, which matches the DySPAN-2026 +//! finding that fine feedback resolution makes the privacy–utility tradeoff +//! nearly free. +//! - **Sounding overhead.** Randomizing the NDP sounding cadence costs airtime +//! directly; charged as a flat `sounding_overhead` fraction of throughput. +//! +//! Throughput ratio = `(1 − overhead) · C(SNR·(1−ρ)) / C(SNR)` where +//! `C(x) = log2(1 + x)` is the Shannon capacity of the data-carrying beam. The +//! comm block is never perturbed, so its geometry is intact; only the SNR is +//! nudged by the residual `ρ`. + +use crate::protector::ShieldConfig; + +/// A single-stream link model. +#[derive(Debug, Clone)] +pub struct LinkModel { + /// Operating SNR of the data-carrying beam, in dB. + pub snr_db: f64, +} + +impl Default for LinkModel { + fn default() -> Self { + Self { snr_db: 20.0 } + } +} + +impl LinkModel { + /// Linear SNR. + #[must_use] + pub fn snr_linear(&self) -> f64 { + 10f64.powf(self.snr_db / 10.0) + } + + /// Baseline Shannon capacity (bits/s/Hz) with no shield. + #[must_use] + pub fn baseline_capacity(&self) -> f64 { + (1.0 + self.snr_linear()).log2() + } + + /// Uncompensated beamforming-gain residual from finite feedback resolution. + #[must_use] + pub fn beamforming_residual(shield: &ShieldConfig) -> f64 { + if !shield.enabled { + return 0.0; + } + let step = (core::f64::consts::FRAC_PI_2) / f64::from(1u32 << shield.feedback_bits); + // Mean-square quantization error of a uniform quantizer, as a fraction + // of unit gain. Clamp for safety at absurdly low resolutions. + (step * step / 12.0).min(0.5) + } + + /// Throughput ratio of the protected link versus the unshielded baseline, + /// in `[0, 1]`. + #[must_use] + pub fn throughput_ratio(&self, shield: &ShieldConfig) -> f64 { + if !shield.enabled { + return 1.0; + } + let rho = Self::beamforming_residual(shield); + let snr = self.snr_linear(); + let protected = (1.0 + snr * (1.0 - rho)).log2(); + let ratio = protected / self.baseline_capacity(); + ((1.0 - shield.sounding_overhead) * ratio).clamp(0.0, 1.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn baseline_ratio_is_one() { + let cfg = ShieldConfig { + enabled: false, + ..ShieldConfig::default() + }; + assert!((LinkModel::default().throughput_ratio(&cfg) - 1.0).abs() < 1e-9); + } + + #[test] + fn fine_resolution_is_nearly_free() { + let ratio = LinkModel::default().throughput_ratio(&ShieldConfig::default()); + assert!(ratio > 0.95, "ratio {ratio}"); + // Almost all of the (small) loss is the sounding overhead, not the + // perturbation — consistent with the DySPAN-2026 fine-resolution result. + assert!(ratio < 1.0); + } + + #[test] + fn coarse_resolution_costs_more() { + // Lowering feedback resolution raises the residual and lowers throughput + // — the tradeoff is real, just cheap at fine resolution. + let fine = ShieldConfig { + feedback_bits: 9, + ..ShieldConfig::default() + }; + let coarse = ShieldConfig { + feedback_bits: 2, + ..ShieldConfig::default() + }; + let link = LinkModel::default(); + assert!(link.throughput_ratio(&fine) > link.throughput_ratio(&coarse)); + } +}