Add VEIL privacy shield: compliant-waveform defense against WiFi sensing (ADR-288)

VEIL (Verifiable Emission-shaping for Identity-Leakage prevention) is the
countermeasure counterpart to BFLD (ADR-118/121): where BFLD detects when
beamforming feedback becomes identifying, VEIL shapes a node's own outgoing
feedback so an unauthorized passive sniffer cannot re-identify people, while
a legitimate receiver that shares the per-session key sees an unchanged link.

Mechanism: identity leaks through the fine cross-subcarrier phase structure of
a compressed beamforming report; throughput rides the dominant beam direction.
These are (mostly) separable subspaces. VEIL composes extra keyed Givens
rotations (the report's native primitive) over the fine subspace only. The
rotation is orthogonal (energy-preserving -> not jamming), keyed per session
(the AP inverts it -> throughput preserved), and fresh each session (a sniffer
cannot average it back -> re-identification collapses to chance).

Contents:
- v2/crates/wifi-densepose-privshield: deterministic, dependency-free,
  WASM-ready pure-compute leaf implementing the attacker-vs-protector
  experiment, the four compliant controls, a throughput model, a
  machine-checkable "not jamming" compliance audit, and a pinned witness.
  29 tests + doctest pass; clippy -D warnings clean; builds for
  wasm32-unknown-unknown.
- docs/research/privacy-shield: 8-file research bundle (SOTA, threat model,
  design, compliance/regulatory, experiment protocol, market, roadmap).
- docs/adr/ADR-288: formal decision record.

Reference results (SYNTHETIC / L0, N=16 identities): passive re-ID accuracy
100% shield-off -> 7.8% shield-on (chance 6.25%); modeled throughput ratio
98.0%; emission energy ratio 1.000000 (compliant). All defense numbers are
SYNTHETIC until a two-node hardware capture with a witness exists.

Compliant waveform controls only; never jamming (47 U.S.C. 333/302a analysis
in the bundle).

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
This commit is contained in:
Claude
2026-08-09 13:51:12 +00:00
parent 5780c239e4
commit 16b2a629d1
23 changed files with 2433 additions and 0 deletions
@@ -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 L0L5 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
```
+1
View File
@@ -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) |
---
@@ -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.56 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 | 23 m straight walk |
| WiFi-ID (2016) | 93%→77% | 2→6 | wavelet features |
| WiPIN (2018) | 92100% | ≤30 | operation-free |
| Deep-WiID (2019) | 92.599.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 17.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 202425 | 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
@@ -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).
@@ -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 privacythroughput 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` |
| Privacythroughput 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 privacythroughput knob
The design exposes one honest tuning knob, matching the literature:
- **`feedback_bits` high (79):** 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).
@@ -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.*
@@ -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 privacythroughput 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.
@@ -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 MHz6 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.*
@@ -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
```
+95
View File
@@ -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.
+1
View File
@@ -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",
@@ -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"
@@ -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 |
@@ -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<Vec<f32>>,
ids: Vec<usize>,
}
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<usize> = Vec::new();
let mut sums: Vec<Vec<f32>> = Vec::new();
let mut counts: Vec<usize> = 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<usize> {
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);
}
}
@@ -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);
}
}
@@ -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())
);
}
}
@@ -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<f32>,
/// 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<Vec<f32>>,
}
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<f32> = (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));
}
}
@@ -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;
@@ -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);
}
}
}
@@ -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 BoxMuller 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>() / f64::from(n);
assert!(mean.abs() < 0.02, "mean {mean} not near 0");
}
}
@@ -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"
);
}
}
@@ -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 59). 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));
}
}
@@ -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 privacyutility 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));
}
}