mirror of
https://github.com/ruvnet/RuView
synced 2026-06-22 12:23:18 +00:00
0bffe27288
* docs(adr-117): seed branch — ADR-117 pip-modernization spec + soul-signature research bundle
Two artifacts landing together on this new branch as the prerequisite
documentation for the v2.0.0 Python wheel modernization work:
1. **docs/adr/ADR-117-pip-wifi-densepose-modernization.md** (644 lines)
— Plan to bring the 2025-published `wifi-densepose` PyPI package
(last release v1.1.0, 2025-06-07, 11.5 months out of sync) up to
the current Rust v2/ workspace SOTA. Recommends PyO3 + maturin
with abi3-py310 (one binary covers Python 3.10–3.13 per OS/arch),
first-wheel scope = core + vitals + signal crates (~5 MB), v1.99.0
tombstone + 90-day un-yank window for v1.1.0, v2.0.0 hard break.
Open questions catalogued; phases P1–P6+ laid out with concrete
acceptance criteria.
2. **docs/research/soul/** (5 files, ~1,450 lines) — Soul Signature
research spec: 7-channel electromagnetic biometric fingerprint
(AETHER 128-dim + cardiac HR/HRV + cardiac waveform morphology +
respiratory pattern + gait timing + skeletal proportions +
subcarrier reflection profile), fused into one RVF graph file.
Includes 60s scanning protocol, 5-layer security model,
threat-model + mitigations, references to existing ADRs (014,
021, 024, 027, 030, 039, 079, 106, 108, 109, 110, 115). Marked
"Research Specification (Pre-Implementation)". Explicit "what
this is NOT" disclaimers preempt pseudoscience drift; every
discriminative-power claim either cites a measurement or is
marked "open research; baseline TBD".
Branch off main at HEAD; ready for /loop 10m implementation
iterations.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p1): scaffold python/ workspace — PyO3 + maturin + smoke tests (refs #785)
ADR-117 P1 — the python/ directory is now a working maturin-buildable
crate that produces the v2.x replacement for the legacy pure-Python
wifi-densepose==1.1.0 PyPI wheel.
## What lands
- `python/Cargo.toml` — PyO3 0.22 with `extension-module` + `abi3-py310`
(one binary covers Python 3.10–3.13 per OS/arch — keeps the
cibuildwheel matrix to 5 wheels per release, not 20). Depends on
`wifi-densepose-core` from the existing v2/ workspace via relative
path.
- `python/pyproject.toml` — maturin>=1.7 build backend with
`python-source = "python"` and `module-name = "wifi_densepose._native"`
so the compiled module loads as an internal underscore-private
submodule of the user-facing `wifi_densepose` package. PEP 621
metadata + classifiers + project URLs. Optional-deps:
`wifi-densepose[client]` for the P4 WS/MQTT pure-Python layer,
`wifi-densepose[dev]` for the test toolchain (pytest, ruff, mypy).
- `python/src/lib.rs` — minimal `#[pymodule] wifi_densepose_native`
exporting `__rust_version__`, `__rust_build_tag__`,
`__build_features__`, and a `hello()` smoke function. P2 will land
the core type bindings here.
- `python/wifi_densepose/__init__.py` — pure-Python facade re-exporting
the compiled module's symbols under their stable user-facing names.
Docstring teaches the v1→v2 migration story up-front.
- `python/wifi_densepose/py.typed` — PEP 561 marker so `mypy --strict`
in user code treats the wheel as fully typed (real stubs land in P2).
- `python/tests/test_smoke.py` — 6 P1 acceptance tests:
1. package imports without error
2. version string is PEP 440-compliant
3. `__rust_version__` is reachable from Python (the diagnostic
surface ADR-117 §5.2 promised)
4. `__build_features__` lists `p1-scaffold` marker
5. `wifi_densepose.hello()` returns "ok" (FFI round-trip)
6. `wifi_densepose._native` is reachable but the leading underscore
conveys "private; users should import the parent package"
- `python/README.md` — phase ledger, local build instructions
(`maturin develop`), layout diagram.
## What's deferred to P2+
- Core type bindings (`CsiFrame`, `Keypoint`, `PoseEstimate`) — P2
- Vitals + signal DSP bindings + witness v2 — P3
- Pure-Python WS/MQTT client layer (`wifi_densepose[client]`) — P4
- cibuildwheel + PyPI publish — P5
- v1.99.0 tombstone — concurrent with P5
The new `python/` crate is intentionally OUTSIDE the v2/ Cargo
workspace — it has its own Cargo.toml with `[package]` not
`[workspace.package]` inheritance — to keep maturin's `python-source`
+ `module-name` config self-contained and to avoid forcing every
`cargo test --workspace` invocation in v2/ to compile pyo3.
Refs ADR-117 §5 (Detailed design) and §6 (Phased migration).
Refs #785 (tracking issue).
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p1): standalone Cargo.toml + python-source=. + #[pyo3(name=_native)] (P1 GREEN)
Three fixes to make maturin develop actually work locally:
1. `python/Cargo.toml` removed `*.workspace = true` inheritance —
the python/ crate is intentionally outside the v2/ workspace
(ADR-117 §5.2) so it needs every `[package]` field local.
2. `python/pyproject.toml` `python-source = "python"` was wrong
because pyproject.toml lives at python/ — maturin was looking for
python/python/. Changed to `python-source = "."` so the
`wifi_densepose/` package directory sibling-to-pyproject is found.
3. `python/src/lib.rs` `#[pymodule] fn wifi_densepose_native` →
`#[pymodule] #[pyo3(name = "_native")] fn wifi_densepose_native`.
PyO3 generates `PyInit__native` from the pyo3-name attribute, which
must match the `module-name` in pyproject.toml's [tool.maturin]
block ("wifi_densepose._native"). Without this attribute the wheel
builds but `import wifi_densepose._native` fails with
ModuleNotFoundError.
## Local validation (P1 acceptance gate)
```
$ python -m venv .venv && .venv/Scripts/python -m pip install maturin pytest
$ VIRTUAL_ENV=… maturin develop --release
…
Finished `release` profile [optimized] target(s)
📦 Built wheel for abi3 Python ≥ 3.10
🛠 Installed wifi-densepose-2.0.0a1
$ .venv/Scripts/python -c 'import wifi_densepose; print(wifi_densepose.__version__, wifi_densepose.__rust_version__, wifi_densepose.hello())'
2.0.0a1 2.0.0-alpha.1 ok
$ .venv/Scripts/python -m pytest tests/ -v
tests/test_smoke.py::test_package_imports PASSED
tests/test_smoke.py::test_version_string_well_formed PASSED
tests/test_smoke.py::test_rust_version_surfaced PASSED
tests/test_smoke.py::test_build_features_listed PASSED
tests/test_smoke.py::test_hello_returns_ok PASSED
tests/test_smoke.py::test_native_module_private PASSED
======================== 6 passed in 0.05s =========================
```
P1 closed. Moving to P2 (core type bindings).
Refs #785, ADR-117 §6.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p2): Keypoint + KeypointType bindings — 23 new tests (29/29 GREEN)
Lands the first chunk of P2: PyO3 bindings for `Keypoint` and
`KeypointType` from `wifi_densepose_core`. Bound types surface to
Python as `wifi_densepose.Keypoint` / `wifi_densepose.KeypointType`.
## Design choices that affect the API surface
1. **`Confidence` is NOT bound as a separate class.** Users hate
wrapping a float in a constructor. Python-side, confidence is just
a `float in [0.0, 1.0]`; the binding validates on construction
(`ValueError` for out-of-range, matching the Rust core error).
2. **`KeypointType` is a `#[pyclass(eq, eq_int, hash, frozen)]` enum**
— hashable so users can drop it into dicts/sets (the most common
pattern in pose-analysis notebooks: `keypoints_by_type[k.type] = k`).
3. **`Keypoint.__init__` keyword-only `z`** so 2D users don't have to
write `None` and 3D users get a clear named arg:
`Keypoint(KeypointType.LeftWrist, 0.2, 0.4, 0.8, z=0.1)`.
4. **`Keypoint` is `#[pyclass(frozen)]`** — no in-place mutation. The
Rust core type is immutable through Copy + Hash + Eq, and exposing
setters from Python would create a copy-vs-reference inconsistency
between languages.
## Files
- `python/src/bindings/keypoint.rs` — 220 lines of `#[pymethods]`
wrappers + Rust↔Python enum round-trip
- `python/src/lib.rs` — `mod bindings { pub mod keypoint; }` +
`bindings::keypoint::register(m)?` call from `#[pymodule]`
- `python/wifi_densepose/__init__.py` — re-exports `Keypoint` and
`KeypointType` at the package root
- `python/tests/test_keypoint.py` — 23 tests covering:
- 17-element COCO ordering of `KeypointType.all()`
- index→type mapping for every variant
- snake_name matches COCO spec
- `is_face()` / `is_upper_body()` predicates
- hashability (the bug I caught when I added the set-based face
test — fixed by adding `hash` to the `#[pyclass]` attribute)
- 2D + 3D constructor variants
- position_2d / position_3d tuples
- is_visible threshold
- confidence validation (Err on out-of-range)
- distance_to (2D Euclidean, 3D Euclidean, fallback when one is 2D
and the other is 3D)
- __repr__ + __eq__
- the new `p2-keypoint-bindings` feature marker landed
## Local validation
\`\`\`
$ cd python && .venv/Scripts/python -m pytest tests/ -v
tests/test_smoke.py::test_package_imports PASSED
tests/test_smoke.py::test_version_string_well_formed PASSED
tests/test_smoke.py::test_rust_version_surfaced PASSED
tests/test_smoke.py::test_build_features_listed PASSED
tests/test_smoke.py::test_hello_returns_ok PASSED
tests/test_smoke.py::test_native_module_private PASSED
tests/test_keypoint.py::test_keypoint_type_all_returns_17 PASSED
…
======================== 29 passed in 0.06s =========================
\`\`\`
Wheel size after both bindings: still well under the 5 MB ADR §5.4
budget (release build with --strip on Windows: ~340 KB).
Also adds `python/.gitignore` to prevent the `.venv/` + `target/` +
`_native.abi3.pyd` artifacts from getting committed.
## What's left in P2
CsiFrame + PoseEstimate bindings land in the next iteration. They're
larger (CsiFrame has the subcarrier buffer; PoseEstimate has
17×Keypoint + BoundingBox + track_id + score). Pattern is now proven
so they go faster.
Refs #785, ADR-117 §6.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p2): BoundingBox + PersonPose + PoseEstimate — P2 COMPLETE (57/57 tests GREEN)
Lands the second + third chunks of P2: PyO3 bindings for `BoundingBox`,
`PersonPose`, `PoseEstimate` from `wifi_densepose_core`. Combined with
the prior Keypoint + KeypointType bindings (fd0568caa), this closes
ADR-117 §6 P2.
## Coverage
| Type | Bound | Tests | Mutability |
|---|---|---|---|
| Confidence | exposed as `float` with validation | (covered in keypoint tests) | n/a |
| KeypointType | `#[pyclass(eq, eq_int, hash, frozen)]` | 7 tests | immutable |
| Keypoint | `#[pyclass(frozen)]` | 16 tests | immutable |
| BoundingBox | `#[pyclass(frozen)]` | 8 tests | immutable |
| PersonPose | `#[pyclass]` (mutable, builder-style) | 12 tests | mutable |
| PoseEstimate | `#[pyclass(frozen)]` | 8 tests | immutable |
Smoke (P1) + new tests: **57/57 PASS** locally on Windows.
## What's deferred to P3
CsiFrame intentionally NOT bound in P2 because it uses
`Array2<Complex64>` (ndarray) — the natural Python surface is via the
`numpy` pyo3 bridge, which lands in P3 alongside the vitals + signal
DSP bindings. Binding CsiFrame without numpy interop would force
users to materialise lists of tuples which is a worse API than
`csi_frame.amplitude_array()` returning an ndarray.
## Design choices that affect the API surface
1. **PersonPose.keypoints() returns a dict keyed by KeypointType**
instead of a fixed-length list with None slots. Pythonistas don't
want to know the underlying storage is `[Option<Keypoint>; 17]`.
2. **PoseEstimate.id and .timestamp exposed as strings** (UUID + ISO)
rather than as bound `FrameId` / `Timestamp` types. Users in
notebooks rarely compare UUIDs structurally; strings are good
enough for diagnostics and don't bloat the bindings.
3. **PersonPose is MUTABLE** (`#[pyclass]` without `frozen`) so users
can build poses incrementally with `set_keypoint`/`set_bbox`/
`set_id`. PoseEstimate is `frozen` because once constructed it
represents a snapshot.
## Three PyO3 0.22 gotchas surfaced this iteration
1. `#[pymethods]` getters are NOT accessible from other Rust modules
— need a separate `impl PyKeypoint { pub(crate) fn inner(&self)
-> &Keypoint { ... } }` block for cross-module use.
2. `PyDict::new(py)` was removed in PyO3 0.21 → 0.22 in favour of
`PyDict::new_bound(py)`. (Confusing because `Bound<'py, PyDict>`
is the return type either way.)
3. `dict.set_item(K, V)` requires both K and V to impl
`ToPyObject`. `#[pyclass]` types impl `IntoPy<PyObject>` but NOT
`ToPyObject` — workaround: convert via `.into_py(py)` first, then
`set_item(py_object_k, py_object_v)`.
Saved as PyO3 0.22 binding patterns memory at the horizon-tracker
level so future loop workers don't re-learn them.
## Local validation
\`\`\`
$ cd python && .venv/Scripts/python -m pytest tests/ -v
…
======================== 57 passed in 0.24s =========================
\`\`\`
Wheel size: still ~340 KB on Windows release build.
Refs #785, ADR-117 §6 (P2 done — ready for P3 vitals + signal DSP +
numpy bridge + witness v2).
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-117): add BFLD support (§5.7a + P3.5 phase + §11.11/12 open questions)
Per maintainer feedback during P3 implementation, expand ADR-117 to
include Beamforming Feedback Loop Data (BFLD) as a first-class binding
target alongside CSI. BFLD is the transmitter-side, AP-station-loop
view of the WiFi channel (802.11ac/ax/be compressed beamforming feedback
frames) — complementary to receiver-side CSI, with three properties
that make it strategically important for the pip wheel:
1. **Up to 996 subcarriers per HE160 frame** (vs 242 for HE-LTF CSI on
ESP32-C6, vs 52 for HT-LTF on ESP32-S3) — much denser per-subcarrier
reflection profile
2. **Works on stock 802.11ac+ hardware** — no Nexmon patch, no ESP32
monitor mode, no firmware drift. Captured via tcpdump/Wireshark +
BFR dissector, or via `mac80211` debugfs on Linux 6.10+
3. **Direct input for the soul-signature spec** (`docs/research/soul/`)
— the seven-channel biometric needs dense subcarrier reflection;
BFLD provides it without specialized hardware
## Three additions to ADR-117
### §5.7a — New binding-target subsection
Comparison table CSI vs BFLD; binding strategy with forward-compat
stub Rust impl pending the future `wifi-densepose-bfld` crate; the
three Python types that ship in P3.5:
- `BfldFrame` (frozen) — one compressed feedback matrix snapshot
- `BfldReport` (frozen) — aggregator over a 60-s scan window
- `BfldKind` enum — `CompressedHE20/40/80/160`, `UncompressedHT20/40`
### §6 P3.5 — Concurrent-with-P3 phase
Checkbox plan for the bindings module + stub Rust storage + numpy
bridge for `feedback_matrix` (Complex64 ndarray, same approach as
`CsiFrame.amplitude` from P3). Lands in the same wheel as P3, no
schedule cushion needed.
### §11.11/12 — Two new open questions
- **§11.11** — Should the future BFR ingestion Rust crate be a new
`wifi-densepose-bfld` workspace member, or extend `-signal`?
*Tentative: new dedicated crate. Wireshark BFR dissector is ~2k
lines and would bloat `-signal`; ingestion is optional for many
deployments; keep `-signal` lean.*
- **§11.12** — Per-vendor BFR variant compatibility (Broadcom vs
Intel vs Qualcomm vs MediaTek differ in psi/phi quantization +
matrix entry ordering). How much normalisation in the Python
binding vs. the future Rust crate? *Tentative: Python binding is
dumb (numpy ndarray in/out); future Rust crate owns per-vendor
normalisation via a `Vendor` enum on the constructor.*
### §12 — BFLD reference list
- Hernandez & Bulut, ACM TOSN 2024 (first systematic survey of
BFR-as-sensing)
- Yousefi et al., MobiSys 2023 (practical breath + HR extraction)
- IEEE 802.11ax-2021 §27.3.10 (frame format)
- Wireshark `packet-ieee80211.c` dissector
- AX210 Linux mac80211 debugfs path (kernel 6.10+)
ADR line count: 644 → 807 (+163). Refs #785 (tracking issue).
The implementation work for P3.5 lands in the next /loop iteration
alongside P3 vitals + signal DSP bindings.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p3+p3.5): vitals + BFLD bindings
P3 — Vital sign extraction bindings (wifi-densepose-vitals):
- VitalStatus enum (eq, eq_int, hash, frozen) — Valid/Degraded/Unreliable/Unavailable
- VitalEstimate (frozen) — value_bpm + confidence + status
- VitalReading (frozen) — HR + BR + signal quality composite
- BreathingExtractor — 0.1–0.5 Hz bandpass + zero-crossing
- HeartRateExtractor — 0.8–2.0 Hz bandpass + autocorrelation
- py.allow_threads on extract() hot loops (Q5 audit confirmed
core/vitals/signal are pure-sync — zero tokio deps, safe to release
GIL with no embedded runtime needed)
- 17 tests covering construction, getters, frozen immutability,
esp32_default + explicit ctors, synthetic-signal end-to-end
P3.5 — BFLD bindings (forward-compat surface, stub Rust):
- BfldKind enum — CompressedHE20/40/80/160 + UncompressedHT20/40
with n_subcarriers, bandwidth_mhz, is_he metadata getters
- BfldFrame (frozen) — from_compressed_feedback() accepts numpy
Complex64 ndarray [Nr x Nc x Nsc], validates dims against kind,
feedback_matrix() returns lossless roundtrip ndarray
- BfldReport — aggregates frames, rejects mismatched kinds,
computes inverse-CV coherence score
- 19 tests covering all 6 PHY variants + numpy roundtrip +
dim-mismatch error + aggregation
- Real Rust ingestion (wifi-densepose-bfld crate) lands post-v2.0
per ADR-117 §11.11/12 — Python API will not change
Total Python test count: 93 (was 57, +36 P3+P3.5). All passing.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p4): pure-Python WS/MQTT client layer
New sub-package `wifi_densepose.client` (no PyO3, no Rust deps):
- ws.SensingClient — asyncio websockets>=12 wrapper for the Rust
sensing-server /ws/sensing endpoint. Yields typed dataclasses
(ConnectionEstablishedMessage, EdgeVitalsMessage, PoseDataMessage)
with raw-payload fallback for forward-compat with unknown types.
Malformed frames log+drop without breaking the stream.
- mqtt.RuViewMqttClient — paho-mqtt v2 wrapper using the explicit
CallbackAPIVersion.VERSION2 API. Per-instance unique client_id by
default (rumqttc memory lesson). MQTT v5-spec-correct topic
wildcard matcher: + as whole-level wildcard, # matches the prefix
itself plus all sub-levels. Auto-resubscribes on reconnect.
Handler exceptions are caught and logged so a misbehaving callback
can't crash the network loop.
- primitives.SemanticPrimitiveListener — typed router for the 10
HA-MIND fused inference outputs from ADR-115 §3.12
(SomeoneSleeping, PossibleDistress, RoomActive, ElderlyInactivity-
Anomaly, MeetingInProgress, BathroomOccupied, FallRiskElevated,
BedExit, NoMovementSafety, MultiRoomTransition). Decodes both
JSON payloads with confidence+explanation AND plain HA state
strings ("ON"/"OFF"/numeric). Pluggable into RuViewMqttClient.
- ha.HABlueprintHelper — read-only parser for the
homeassistant/<kind>/wifi_densepose_<node>/<id>/config payload
family. Aggregator queries: entities_for_node, by_device_class,
nodes. Useful for blueprint authors + dashboard introspection.
Test coverage (63 new tests, 156 total in Python suite):
- test_client_ha — 18 tests (topic+payload parsing, aggregator)
- test_client_primitives — 13 tests (enum coverage, listener routing)
- test_client_mqtt — 17 tests (matcher parametrize, dispatch path,
on_connect, exception isolation) — no broker needed
- test_client_ws — 6 tests including end-to-end against an in-process
websockets.serve() fixture exercising all 4 message types plus a
malformed-frame survival check
Post-bridge wheel size: 238 KB (well under ADR §5.4 5 MB budget).
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md §5.6
Refs: docs/adr/ADR-115-home-assistant-integration.md §3.12
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117/p5+p-tomb): pip-release workflow + v1.99.0 tombstone wheel
P5 — `.github/workflows/pip-release.yml`:
- cibuildwheel matrix per ADR §5.4: manylinux x86_64 + aarch64,
macos x86_64 + arm64, win amd64 (5 wheels via abi3-py310 stable
ABI — one binary per OS/arch covers Python 3.10–3.13)
- Linux aarch64 cross-builds via QEMU; rustup 1.82 pinned in
CIBW_BEFORE_ALL_LINUX for reproducibility
- Per-wheel smoke test: import wifi_densepose, assert hello()=="ok"
- sdist via `maturin sdist`
- Trigger: workflow_dispatch + push to `v*-pip` tags ONLY (never
on regular commits — won't accidentally publish)
- TestPyPI dry-run gate via `repository-url: https://test.pypi.org/legacy/`
- Production PyPI publish via Trusted Publisher OIDC (no API tokens
in GH secrets per ADR §9). Requires one-time PyPI Trusted Publisher
registration before the first publish can fire.
- Q3 (witness hash v2 — ADR-117 §11.3) flagged in workflow comments
as a hard gate before the first tag.
P-tomb — `python/tombstone/`:
- Separate `wifi-densepose==1.99.0` sdist+wheel using setuptools
backend (NOT maturin — tombstone is pure Python, no Rust).
- `src/wifi_densepose/__init__.py` raises ImportError with the
migration URL on import. Verified locally: 2.7 KB wheel,
`pip install` then `import wifi_densepose` raises ImportError
with `pip install wifi-densepose==2.0.0` hint + repo URL.
- 5 unit tests (`tests/test_tombstone.py`) lock the file content
down: must `raise ImportError`, must contain v2 install hint
and migration URL, must NOT contain any `def`/`class`/`import`
beyond the bare `raise` — so a well-intentioned refactor can't
accidentally bloat the tombstone into a real module that loads
partway before failing.
Both wheels are published by the same pip-release.yml workflow:
- `v1.99.0-pip` tag → publishes tombstone (or via workflow_dispatch
with `target: v1-99-tombstone`)
- `v2.X.Y-pip` tag → publishes the v2 wheel matrix
Per ADR-117 §7.3: tag and publish 1.99.0-pip FIRST so the tombstone
claims the "current" slot in pip's resolver, THEN publish 2.0.0-pip.
Test count unchanged in main python/ suite (156/156). Tombstone
sub-suite: 5 passing.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md §5.4, §7
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* hardening(adr-117): benchmarks + security/robustness test suite
Benchmarks (`python/bench/`, pytest-benchmark — opt-in via --benchmark-only):
| Hot path | Mean | Ops/sec | % of 100 Hz budget |
|---|---|---|---|
| BfldFrame HT20 1×1×52 | 800 ns | 1.25 Mops | 0.008% |
| BfldFrame HE20 2×1×242 | 1.3 μs | 750 kops | 0.013% |
| BfldFrame HE80 2×1×996 | 4.2 μs | 236 kops | 0.042% |
| BfldFrame HE160 2×2×1992 | 14 μs | 71 kops | 0.14% |
| BfldFrame.feedback_matrix() | 2.8 μs | 352 kops | — |
| WS edge_vitals decode | 7.4 μs | 134 kops | 0.074% |
| WS pose_data decode (3 persons) | 23 μs | 42 kops | 0.24% |
| BreathingExtractor.extract() 56sc | 28 μs | 35 kops | 0.28% |
| BreathingExtractor.extract() 114sc | 44 μs | 23 kops | 0.44% |
| BreathingExtractor.extract() 242sc | 79 μs | 13 kops | 0.79% |
| HeartRateExtractor.extract() 56sc | 105 μs | 9.5 kops | 1.05% |
All hot paths well under the 100 Hz ESP32 frame budget (10 ms).
Worst case (HeartRateExtractor) uses 1% of the budget — no
optimization needed. Scaling on n_subcarriers is sub-quadratic
(56→242 = 4.3× input, 2.8× time) — catches future O(n²)
regressions.
Security & robustness tests (`tests/test_security.py`, +27 tests):
- WS decoder: rejects non-object roots cleanly, survives 1 MB string
values, handles non-ASCII node IDs, survives deeply-nested JSON
(Python's json.loads built-in guard not bypassed)
- MQTT topic matcher: 9 edge-case parametrize entries including
$SYS topics, null-byte injection, mid-pattern `#` boundary,
empty-string boundary
- MQTT credential confidentiality: password never appears in
repr()/str(), never stored in plain client-instance attribute
- HA discovery: rejects null-byte-laced topics, rejects extra
slashes in node_id, rejects non-dict payload body (list, scalar,
invalid UTF-8 bytes) without crashing
- Semantic primitive listener: rejects topic-injection attempts
(prefix-injected paths, wrong case on final segment), survives
invalid UTF-8 payloads
- Public surface integrity: every name in wifi_densepose.__all__
AND wifi_densepose.client.__all__ resolves — catches accidental
re-export breakage between phases
- Multi-handler MQTT exception isolation: a crashing handler in
the middle of the registered list doesn't stop later handlers
from firing
Test count: 156 → 183 (+27). All passing.
Bench results steady-state confirm no Rust-binding-layer
optimization is needed before the v2.0.0 publish.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p5): switch publish workflow to PYPI_API_TOKEN + user-facing README
- Workflow rewired from OIDC Trusted Publisher to token-based publish
via the `PYPI_API_TOKEN` GitHub Actions secret. Both publish jobs
(v2 wheels + tombstone) pass `password: ${{ secrets.PYPI_API_TOKEN }}`
to `pypa/gh-action-pypi-publish@release/v1`. Workflow comments now
document the GCP → GH secret-refresh command.
- Removed `permissions: id-token: write` and the OIDC `environment:`
blocks (no longer needed without OIDC).
- Token was sourced from the GCP Secret Manager entry `PYPI_TOKEN`
in project `cognitum-20260110` and pushed to GH Actions via
`gcloud secrets versions access | gh secret set` so the value
never appeared in a shell variable or this session's output.
- Rewrote `python/README.md` from a developer phase-ledger into a
user-facing PyPI front page: one-paragraph elevator pitch, bullet
list of features, three short usage snippets (vitals extract,
WS subscribe, MQTT semantic-primitive listener, BFLD numpy
bridge), hardware table, links. The README is the FIRST thing
pip users see at https://pypi.org/p/wifi-densepose so it has to
introduce the project, not the build plan.
Wheel rebuilds clean at 253 KB (was 238 KB — +15 KB from the richer
README baked into the wheel metadata). Test suite unchanged at 183/183.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* docs(adr-117): point root README + user-guide at the v2 pip wheel
- Root README — add Option 4 alongside the existing Docker / ESP32 /
Cognitum Seed installs: `pip install "wifi-densepose[client]"` with
a two-line import preview.
- User-guide §Installation — replace the stale "From Source (Python)"
block (which referenced legacy v1 extras `[gpu]` and `[all]` that
don't exist in v2) with a brief "Python wheel (pip) — ADR-117"
section: what the wheel is, install commands, two-line example,
tombstone caveat, and the `maturin develop` source-build path
for contributors.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p5): pin Python 3.12 + isolated venv for tombstone smoke-test
First v1.99.0-pip run (26366491748) failed: the runner's system `python`
fell back to `--user` install, then `python -c "import wifi_densepose"`
resolved to something other than the freshly-installed user-site wheel
and returned cleanly instead of raising the tombstone ImportError.
Fixes:
- `actions/setup-python@v5` with explicit 3.12 — owns its own site-
packages so pip won't fall back to --user.
- New "Inspect wheel contents" step prints the wheel manifest +
the verbatim __init__.py inside it. If a future regression ships
an empty __init__.py from a setuptools src-layout edge case,
the failure is debuggable from the run log alone.
- Smoke test now runs in a fresh /tmp/smoke-venv so there's zero
ambiguity about which wifi_densepose gets imported. Also uses
importlib.util.find_spec to print the resolved origin path
before the import attempt — so even if both checks pass, we
see exactly which file we exercised.
No code changes to the tombstone source itself.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(adr-117/p5): smoke-test must cd out of repo root before importing
Root cause from run 26366579422 diagnostics: the wheel built correctly
(872 bytes, valid ImportError) but `import wifi_densepose` resolved to
the legacy `./wifi_densepose/__init__.py` left in the repo root from
v1, NOT to the freshly-installed tombstone wheel in the smoke venv.
Python places the cwd at sys.path[0] for `python -c "..."`, so
running the import from the repo root made the legacy directory win
over site-packages every time. The "isolated venv" was not the
problem — the cwd was.
Fix: copy the wheel to /tmp, cd /tmp before the import. Now the
smoke test runs in a directory that contains no `wifi_densepose/`
so the only resolution path is the venv's site-packages.
The repo-root `./wifi_densepose/__init__.py` is a separate concern
(legacy v1 carry-over) that should be cleaned up in a follow-up
commit, but the smoke test should not depend on it being absent.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(adr-117): publish wifi-densepose 2.0.0a1 + ruview 2.0.0a1 to PyPI
Three PyPI artifacts now live (published from .env-sourced PYPI_TOKEN
via twine from the maintainer box — direct upload bypassed the GH
Actions workflow auth churn):
1. wifi-densepose==1.99.0 — tombstone (raises ImportError with migration URL)
https://pypi.org/project/wifi-densepose/1.99.0/
2. wifi-densepose==2.0.0a1 — PyO3 wheel (win_amd64 cp310-abi3) + sdist
https://pypi.org/project/wifi-densepose/2.0.0a1/
3. ruview==2.0.0a1 — meta-package re-exporting wifi_densepose
https://pypi.org/project/ruview/2.0.0a1/
New `python/ruview-meta/` subdirectory:
- pyproject.toml — name="ruview", version="2.0.0a1", setuptools backend,
dependencies = ["wifi-densepose==2.0.0a1"]
- src/ruview/__init__.py — re-exports every name from
`wifi_densepose.__all__` so `from ruview import BreathingExtractor`
is equivalent to `from wifi_densepose import BreathingExtractor`.
Also re-exports `__version__`, `__rust_version__`,
`__rust_build_tag__`, `__build_features__`. Aliases the `client`
sub-package transparently when wifi-densepose[client] extras are
installed.
- README.md — explains why two PyPI names ship the same code (brand
vs technical name) and shows install commands for both.
End-to-end verified: fresh venv, `pip install ruview`,
`import ruview` + `import wifi_densepose` both succeed,
`ruview.BreathingExtractor is wifi_densepose.BreathingExtractor` → True.
Multi-platform wheels (manylinux x86_64+aarch64, macos x86_64+arm64)
still pending — the cibuildwheel workflow path remains for that.
Linux/macOS users today install via the sdist (requires rustup +
maturin locally).
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #785
Co-Authored-By: claude-flow <ruv@ruv.net>
* ci(adr-117): kics-compatible workflow comments + fix-marker guards
- KICS error fix (.github/workflows/pip-release.yml:20): the inline
`gcloud secrets versions access --secret=PYPI_TOKEN ...` runbook
in the workflow header was triggering KICS' generic-secret regex
on the literal `PYPI_TOKEN` substring. Moved the refresh runbook
to docs/integrations/pypi-release.md (with the BOM-stripping
`tr` step that fixed the production publish) and replaced the
inline block with a pointer.
- Three new fix-marker guards in scripts/fix-markers.json so the
next person to touch this code can't silently regress what
PR #786 just shipped:
* RuView#786-tombstone-import — the tombstone __init__.py must
`raise ImportError`, must mention the v2 install hint, must
point at the repo URL, AND must NOT contain `def`/`class`/
`import wifi_densepose` (forbid patterns prevent accidental
bloating into a real module that loads partway before failing).
* RuView#786-tombstone-smoke-cwd — pip-release.yml must `cd /tmp`
before the tombstone smoke-test import, because the legacy
`./wifi_densepose/__init__.py` at repo root would otherwise
shadow the venv install. This was the root cause of run
26366648768; locking it in.
* RuView#786-pypi-token-auth — the workflow must use
`password: ${{ secrets.PYPI_API_TOKEN }}` and must NOT carry
`id-token: write`. The project authenticates via API token,
not OIDC; a partial OIDC migration would 403 silently.
Local check: all 25 markers pass.
Refs: docs/adr/ADR-117-pip-wifi-densepose-modernization.md
Refs: #786
Co-Authored-By: claude-flow <ruv@ruv.net>
368 lines
18 KiB
Markdown
368 lines
18 KiB
Markdown
# Soul Signature — Security, Privacy, and Threat Model
|
|
|
|
**Status:** Research Specification (Pre-Implementation)
|
|
**Date:** 2026-05-24
|
|
**Author:** ruv
|
|
|
|
---
|
|
|
|
## 1. Scope
|
|
|
|
This document defines the threat model, mitigations, cryptographic primitive
|
|
choices, privacy architecture, and open security research items for the Soul
|
|
Signature system. It is intended to be reviewed by a security engineer or
|
|
privacy counsel before any production deployment.
|
|
|
|
The soul signature is a passive biometric system. The security bar is:
|
|
**attacker cost to achieve a false accept must exceed the value of the
|
|
protected resource for the relevant threat model**. The soul signature does
|
|
not claim to be unbreakable. It claims to be hard enough.
|
|
|
|
---
|
|
|
|
## 2. What We Explicitly Do NOT Claim
|
|
|
|
- Not equal to fingerprint scanners on FBI-tier datasets in EER terms. RF
|
|
biometrics are a younger discipline. No independent benchmark with the soul
|
|
signature's specific multi-channel fusion exists yet.
|
|
- Not legal evidence. Passive RF biometric identification has no established
|
|
legal precedent in any jurisdiction.
|
|
- Not a replacement for explicit consent in regulated contexts (healthcare,
|
|
employment, border control).
|
|
- Not unbreakable under a nation-state adversary with full physical access to
|
|
the sensing infrastructure.
|
|
- Not validated at scale beyond the constituent ADR baselines. The AETHER
|
|
channel (ADR-024) targets >80% mAP at 5 subjects; at 100+ subjects the
|
|
false-accept rate is open research.
|
|
|
|
---
|
|
|
|
## 3. Threat Model
|
|
|
|
### 3.1 Attacker: Passive Eavesdropper on the WiFi Medium
|
|
|
|
**Capability:** An attacker near the WiFi sensing zone can observe CSI of any
|
|
person who passes through. With enough CSI, the attacker could construct an
|
|
unauthorized soul signature enrollment of an unconsenting bystander.
|
|
|
|
**Impact:** Unauthorized enrollment → unauthorized recognition → attribution of
|
|
presence to a person who did not consent.
|
|
|
|
**Mitigation:**
|
|
- Ambient CSI capture does NOT trigger enrollment. Enrollment requires the
|
|
explicit 60-second structured protocol. Ambient bystander CSI produces
|
|
`unauthenticated` pose tracks tagged as `person_id: NULL`.
|
|
- Unauthenticated RVF nodes are pruned from the HNSW index after 24 hours.
|
|
- The enrollment protocol requires presence confirmation from at least two
|
|
sensing nodes simultaneously, making drive-by enrollment geometrically
|
|
harder to achieve without physical proximity.
|
|
|
|
**Residual risk:** An attacker who can be physically present in the scanning
|
|
zone for 60 seconds, under the observation of the scanning protocol, can cause
|
|
enrollment of a fake person. This requires physical co-location and is
|
|
equivalent to the threat model for any in-person biometric registration.
|
|
|
|
### 3.2 Attacker: Active Replay
|
|
|
|
**Capability:** An attacker records a CSI stream from a legitimate enrollment
|
|
or recognition event and replays it to a sensing node to impersonate the
|
|
enrolled person.
|
|
|
|
**Impact:** False positive recognition; unauthorized access or presence attribution.
|
|
|
|
**Mitigation:**
|
|
- Each enrollment is bound to the room's ADR-030 field model eigenstate at
|
|
enrollment time. The `environment_id` field in every vector node is a
|
|
SHA-256 of the field model's eigenmode matrix. A replay in a different room
|
|
produces a different `environment_id` and a dramatically different
|
|
Subcarrier_Reflection_Profile — the cross-validation between these two
|
|
signed fields fails.
|
|
- The Ed25519 witness chain (ADR-110) includes a monotonic timestamp
|
|
(`timestamp_ns`). A replay of an old signature is detected by the timestamp
|
|
freshness check at recognition time (configurable; default: reject any
|
|
signature older than 7 days for high-assurance contexts).
|
|
- The ADR-030 field model continuously updates. Even if the replay is in the
|
|
same room, the field model's eigenstate changes as furniture is moved or
|
|
temperature shifts the propagation medium; cross-validation degrades over
|
|
time.
|
|
|
|
**Residual risk:** Replay within the same room within a short time window
|
|
(< 4 hours, before the field model rotates) by an attacker who has recorded the
|
|
original CSI with high fidelity remains a plausible attack vector. This is not
|
|
defended against by the current architecture. It requires a future ADR for
|
|
challenge-response liveness detection.
|
|
|
|
### 3.3 Attacker: Phased-Array Vest / RF Body Emulator
|
|
|
|
**Capability:** An attacker wears a device capable of emitting RF signals that
|
|
mimic another person's backscatter profile, allowing them to be recognized as
|
|
the enrolled person.
|
|
|
|
**Impact:** The strongest impersonation attack; if successful, bypasses all
|
|
electromagnetic biometric channels simultaneously.
|
|
|
|
**Mitigation:**
|
|
- The RuvSense `adversarial.rs` module (ADR-030 Tier 7) enforces four
|
|
physics-based consistency checks:
|
|
1. Multi-link consistency: a real body perturbs all mesh links passing
|
|
through its location. A vest emitting signals affects only the targeted
|
|
link(s). Detection: at least 4 links must show correlated perturbation.
|
|
2. Field model constraints: the perturbation must lie within the span of
|
|
the room's eigenmode structure. Artificially injected signals produce
|
|
perturbations inconsistent with room geometry.
|
|
3. Temporal continuity: real movement is smooth in embedding space; injected
|
|
signals can produce discontinuities flagged by the embedding velocity
|
|
monitor.
|
|
4. Energy conservation: total perturbation energy across all links must be
|
|
consistent with the number and geometry of bodies present.
|
|
- The adversarial detector fires `FAIL_ADVERSARIAL_SIGNAL` before the soul
|
|
signature match is considered.
|
|
|
|
**Residual risk:** A sophisticated attacker with a calibrated phased-array
|
|
system who also knows the room's eigenmode structure and the enrolled person's
|
|
exact multi-link backscatter pattern could in principle construct a convincing
|
|
emulation. This is a high-capability, high-cost attack. Practical countermeasure:
|
|
require multi-node confirmation (ADR-029 multistatic) which raises the
|
|
geometric complexity of the emulation exponentially with node count.
|
|
|
|
### 3.4 Attacker: Insider with Broker Access
|
|
|
|
**Capability:** A privileged operator or compromised service with read access
|
|
to the stored `.rvf` files and the HNSW person_track index.
|
|
|
|
**Impact:** Exfiltration of biometric signatures; linkage of person_id to PII
|
|
if linkage tables also accessible; replay or cross-site re-enrollment.
|
|
|
|
**Mitigation:**
|
|
- At-rest encryption: all `.rvf` files are encrypted with ChaCha20-Poly1305
|
|
using a key derived via Argon2id from a user-provided passphrase (or a FIDO2
|
|
hardware token binding). The Cognitum Seed appliance NEVER stores the
|
|
decryption key; it is re-derived from the passphrase on each access.
|
|
- The opaque `person_id` (u64) in the `.rvf` file is not PII. PII linkage, if
|
|
any, requires access to a separate application-layer database not stored on
|
|
the sensing appliance.
|
|
- The HNSW index stores only the 128-dim AETHER embedding, not raw CSI or full
|
|
soul signatures. Exfiltration of the index exposes the embedding but not the
|
|
full biometric record.
|
|
- Differential privacy (ADR-106 DP-SGD) applies at training time when AETHER
|
|
is fine-tuned on enrolled-person data, preventing membership inference attacks
|
|
that could recover training samples from model weights.
|
|
|
|
**Residual risk:** If the passphrase is weak or the FIDO2 token is compromised,
|
|
the at-rest encryption fails. Key management is a deployment responsibility.
|
|
|
|
### 3.5 Attacker: Manufacturer / Firmware Supply Chain
|
|
|
|
**Capability:** A malicious firmware update to the ESP32 node or Cognitum Seed
|
|
appliance could silently exfiltrate soul signatures or CSI streams.
|
|
|
|
**Impact:** Large-scale passive surveillance; biometric data exfiltration across
|
|
all installed appliances.
|
|
|
|
**Mitigation:**
|
|
- All firmware releases are signed with Ed25519 (ADR-100 cog packaging) and
|
|
verified by the appliance before installation. A Dilithium-3 post-quantum
|
|
co-signature is added in the transition window (ADR-109).
|
|
- The Ed25519 witness chain (ADR-110) signs each CSI frame bundle at the
|
|
sensor level. A firmware change that alters the witness chain is detectable
|
|
by downstream audit.
|
|
- Network egress from the Cognitum Seed in `--privacy-mode` is blocked for
|
|
raw CSI and soul signatures by default. Only MQTT auto-discovery messages
|
|
(ADR-115) and OTA metadata are permitted outbound.
|
|
- Open-source firmware. The ESP32 firmware and Cognitum Seed Rust crates are
|
|
open source (this repository). Independent audit is possible.
|
|
|
|
**Residual risk:** A zero-day exploit in the ESP-IDF WiFi stack or the Rust
|
|
codebase could bypass these controls. This is mitigated by regular security
|
|
audits (run `npx @claude-flow/cli@latest security scan` per CLAUDE.md) but not
|
|
eliminated.
|
|
|
|
---
|
|
|
|
## 4. Consent Architecture
|
|
|
|
### 4.1 The Enrollment-vs-Recognition Distinction
|
|
|
|
The soul signature system enforces a hard distinction:
|
|
|
|
| Action | Consent required | Mechanism |
|
|
|---|---|---|
|
|
| Enrollment | Explicit, active | 60-second protocol with operator confirmation; produces signed `.rvf` |
|
|
| Recognition of enrolled person | Implicit (enrollment = consent for recognition) | Continuous mode; HNSW match |
|
|
| Ambient sensing of unenrolled person | No — but data is transient and pruned | Unauthenticated tracks; 24h TTL |
|
|
| Updating stored profile from continuous mode | Implicit (set at enrollment time) | Aggregator auto-refresh; configurable |
|
|
|
|
The system operator is responsible for obtaining appropriate consent from
|
|
persons before performing enrollment. The technical system enforces that
|
|
enrollment cannot happen accidentally or from drive-by sensing.
|
|
|
|
### 4.2 Bystander Protection
|
|
|
|
Persons who pass through a sensing zone without being enrolled are sensed but
|
|
not persistently identified. Their data flow:
|
|
1. Pose tracker produces a track tagged `person_id: NULL`.
|
|
2. AETHER embedding is computed for motion detection and occupancy counting
|
|
(ADR-115 HA-MIND).
|
|
3. The embedding is written to the `temporal_baseline` HNSW index with a 24-hour
|
|
TTL and `authenticated: false`.
|
|
4. After 24 hours, the entry is automatically pruned by the `EmbeddingIndex::prune()`
|
|
method (ADR-024 §2.4).
|
|
5. No `.rvf` file is created. No persistent record exists.
|
|
|
|
This architecture satisfies the GDPR principle of data minimization (Article 5(1)(c))
|
|
for bystander data: the retention period is bounded, the data is not linked to
|
|
an identity, and the storage is proportionate to the functional purpose
|
|
(occupancy counting).
|
|
|
|
### 4.3 GDPR / HIPAA Mode
|
|
|
|
When `--privacy-mode enabled` (from ADR-115 HA-MIND §privacy):
|
|
|
|
1. Soul signatures are computed and stored locally only. They are NEVER
|
|
published to MQTT topics, Matter clusters, or any external endpoint.
|
|
2. The local REST API for accessing soul signatures requires a valid bearer
|
|
token (ADR-028 bearer_auth.rs). No unauthenticated endpoint exposes
|
|
biometric data.
|
|
3. The JSON-LD sidecar is written to the local encrypted store only. It is not
|
|
included in MQTT auto-discovery payloads.
|
|
4. The longitudinal drift metrics (ADR-030 Tier 4) are published to MQTT in
|
|
aggregated form only (e.g., `drift_detected: true`, never raw metric values
|
|
that could be used for medical inference).
|
|
5. A data deletion endpoint must be implemented: `DELETE /api/v1/persons/{id}`
|
|
removes the `.rvf` file, the HNSW index entry, the JSON-LD sidecar, and all
|
|
longitudinal Welford statistics for that person_id.
|
|
|
|
---
|
|
|
|
## 5. Cryptographic Primitives
|
|
|
|
All primitives are chosen from NIST-approved or widely-audited standards.
|
|
|
|
| Purpose | Primitive | Rationale |
|
|
|---|---|---|
|
|
| Content integrity (per-segment) | CRC32 (IEEE 802.3) | Already implemented in `rvf_container.rs:line 70`. Corruption detection, not security. |
|
|
| Content addressing | SHA-256 | File name derivation; pre-image resistance prevents name collisions |
|
|
| Ed25519 signatures | Ed25519 (RFC 8032) | ADR-110 witness chain; 64-byte signatures; 128-bit security |
|
|
| At-rest encryption | ChaCha20-Poly1305 (RFC 8439) | AEAD; software-friendly; no timing-attack surface like AES-CBC; 256-bit key |
|
|
| Key derivation from passphrase | Argon2id (RFC 9106) | Memory-hard KDF; resistant to GPU/ASIC brute-force; recommended by NIST SP 800-132 draft (2024) |
|
|
| DP-SGD noise | Gaussian N(0, σ²C²I) per ADR-106 | (ε, δ)-DP per Abadi et al. 2016 Moments Accountant |
|
|
| Post-quantum key exchange (future) | Kyber-768 (NIST FIPS 203, 2024) | ADR-108; ~AES-192 security; NIST CNSA 2.0 recommended |
|
|
| Post-quantum signatures (future) | Dilithium-3 (NIST FIPS 204, 2024) | ADR-109; hybrid mode with Ed25519 during transition window |
|
|
|
|
### 5.1 Argon2id Parameters
|
|
|
|
Default parameters for soul signature key derivation:
|
|
|
|
```
|
|
m_cost = 65536 (64 MB memory)
|
|
t_cost = 3 (3 iterations)
|
|
p_cost = 4 (4 parallel lanes)
|
|
output_len = 32 bytes (256-bit key for ChaCha20-Poly1305)
|
|
salt = 16 random bytes stored alongside encrypted blob (NOT the person_id)
|
|
```
|
|
|
|
These parameters provide ~100ms KDF time on a Pi 5, which is acceptable for
|
|
enrollment (one-time) and recognition (HNSW match precedes decryption, so
|
|
decryption is only triggered after a candidate match).
|
|
|
|
### 5.2 Forward Secrecy
|
|
|
|
Old soul signature files are NOT keys for new ones. Compromise of a 90-day-old
|
|
`.rvf` file does not unlock the current profile. The key is derived from the
|
|
user's passphrase each time, not derived from the previous file.
|
|
|
|
Archived files (kept for audit purposes) are re-encrypted on passphrase rotation
|
|
if the operator elects to do so via the `soul-signature re-encrypt --all` CLI
|
|
command (not yet implemented; specified here for future ADR).
|
|
|
|
---
|
|
|
|
## 6. Privacy Mode Integration (ADR-115)
|
|
|
|
The `--privacy-mode` flag defined in ADR-115 HA-MIND §9 is extended to cover
|
|
soul signature data:
|
|
|
|
| Privacy mode | MQTT publish | REST API | Local storage | HNSW index |
|
|
|---|---|---|---|---|
|
|
| `disabled` (default for home users) | Aggregated presence/count only | Authenticated bearer required | Encrypted at rest | Local only |
|
|
| `enabled` | Nothing biometric | Authenticated bearer required | Encrypted at rest | Local only |
|
|
| `research` (explicit opt-in) | Full soul signature nodes (anonymized person_id) | Open (for research deployments only) | Encrypted at rest | Exportable |
|
|
|
|
The `research` mode requires a separate `--research-consent-token` flag and is
|
|
intended for academic data collection under IRB approval. It must never be the
|
|
default.
|
|
|
|
---
|
|
|
|
## 7. Open Research and Outstanding Security Work
|
|
|
|
The following items are known security gaps or open research questions. Each
|
|
warrants a future ADR before production deployment at scale.
|
|
|
|
**7.1 Challenge-Response Liveness Detection**
|
|
Replay attacks within a short time window (see §3.2 residual risk) are not
|
|
defended against. A future mechanism should issue a random challenge (e.g.,
|
|
"please raise your left hand") and verify the CSI response matches the challenge
|
|
before accepting a recognition. This eliminates replay as a practical attack
|
|
vector. Future ADR: ADR-120 (proposed).
|
|
|
|
**7.2 False-Accept Rate at Scale (N > 20 subjects)**
|
|
The AETHER baseline (ADR-024) is tested at 5 subjects (>80% mAP). For household
|
|
deployments this is sufficient. For building-scale deployments (50-500 subjects),
|
|
the FAR is open research. Independent benchmarking on a dataset of 20+ subjects
|
|
with the full 7-channel fusion is required before building-scale deployment can
|
|
be recommended. Publication target: co-locate with ADR-027 MERIDIAN evaluation.
|
|
|
|
**7.3 Side-Channel Leakage from Encrypted RVF Files**
|
|
The file size of an encrypted `.rvf` blob is observable by an attacker with
|
|
filesystem access. File size is a function of the number of nodes present, which
|
|
reveals whether the cardiac channel was captured (high-SNR enrollment vs
|
|
low-SNR enrollment). This is a minor information leak. Mitigation: pad all
|
|
`.rvf` files to a fixed 64 KB boundary. Future ADR: append to ADR-106.
|
|
|
|
**7.4 Membership Inference in Continuous Mode**
|
|
In continuous mode, the AETHER model is fine-tuned on the enrolled person's
|
|
data over months. An adversary with access to the model weights before and after
|
|
a re-train cycle could infer that a specific enrollment occurred, even without
|
|
the soul signature file, via membership inference (Shokri et al. 2017).
|
|
ADR-106 DP-SGD mitigates this for federation round deltas but not for local
|
|
single-device fine-tuning. Extension of DP-SGD to the local continuous-mode
|
|
update is required. Future ADR: extend ADR-106.
|
|
|
|
**7.5 Physical Access to Sensing Nodes**
|
|
An attacker with physical access to an ESP32 node can extract the firmware and
|
|
attempt to reverse the Ed25519 signing key (if the key is stored in ESP32
|
|
NVS without protection). ADR-110 uses NVS for key storage. A future ADR should
|
|
mandate secure element storage (e.g., ATECC608A co-processor on the Cognitum
|
|
Seed) for the signing key. Future ADR: ADR-121 (proposed).
|
|
|
|
**7.6 Federated Learning Linkability**
|
|
When AETHER is retrained via federated learning (ADR-105), the LoRA weight
|
|
deltas carry information about enrolled persons. ADR-106 applies DP-SGD to
|
|
these deltas, but the post-quantum migration path (ADR-108 Kyber-768) is not
|
|
yet integrated with the federation protocol. Until ADR-108 Phase 2 ships, the
|
|
federation link is classically encrypted and vulnerable to harvest-now-decrypt-later
|
|
attacks by quantum-capable adversaries. Assessed risk: low until 2027.
|
|
|
|
---
|
|
|
|
## 8. Summary Security Properties Table
|
|
|
|
| Property | Status | Evidence |
|
|
|---|---|---|
|
|
| At-rest encryption | Specified (ChaCha20-Poly1305 + Argon2id) | This document §5 |
|
|
| Ed25519 attestation | Implemented | ADR-110 witness chain |
|
|
| Replay resistance (cross-room) | Implemented | ADR-030 field model environment_id binding |
|
|
| Replay resistance (same-room, short window) | Open gap | §7.1 |
|
|
| Anti-spoofing (single-link injection) | Implemented | adversarial.rs multi-link consistency |
|
|
| Anti-spoofing (phased-array vest) | Partial | adversarial.rs + energy conservation; residual risk documented |
|
|
| Bystander protection | Specified | 24h TTL on unauthenticated tracks; §4.2 |
|
|
| DP-SGD training privacy | Implemented (federation) | ADR-106 |
|
|
| DP-SGD training privacy (local continuous mode) | Open gap | §7.4 |
|
|
| GDPR data deletion | Specified | §4.3 `DELETE /api/v1/persons/{id}` |
|
|
| Post-quantum migration path | Specified (Kyber-768, Dilithium-3) | ADR-108, ADR-109 |
|
|
| Firmware supply chain integrity | Implemented (Ed25519 cog signing) | ADR-100, ADR-109 hybrid |
|
|
| False-accept rate at scale | Open research | §7.2 |
|
|
| Liveness detection | Open gap | §7.1 |
|
|
| Secure element key storage | Open gap | §7.5 |
|