mirror of
https://github.com/ruvnet/RuView.git
synced 2026-09-01 04:55:54 +00:00
feat(privshield): E2E hardware program — validated C core + multi-provider firmware scaffolds
Take VEIL from the synthetic Rust reference model toward real WiFi silicon
across multiple hardware providers, around one shared, host-validated core.
Answers the questions "can OpenWRT / open WiFi software implement this?" and
"can ESP32 help scramble signals?" with an honest per-platform feasibility map.
Portable C shield core (firmware/privshield/core/) — VALIDATED (host test):
- veil_shield.{h,c}: keyed Givens-rotation obfuscation of the identity-bearing
"fine" subspace, C99, no malloc / no libc I/O, only <math.h>. SplitMix64 key
schedule byte-identical to the Rust crate, so on-air behavior is consistent
everywhere and every adapter links the same math.
- make test passes: energy conservation (orthogonal => "not jamming"),
reversibility (recover inverts apply), wrong-key-fails, and PRNG stream parity
with the Rust crate. This is build/host evidence, NOT silicon.
Per-provider adapters (all SYNTHETIC / L0, build-only, TODO(hw) markers):
- openwifi/ grade B (ceiling A, effort D): only open PHY/MAC (FPGA) that can
host the full keyed rotation + inverse; needs new HDL + 2nd TX chain. Carries
the P5 measurement protocol (MEASUREMENT.md) for the first MEASURED result.
- openwrt/ grade C: per-packet keyed unitary is blob-blocked on commodity APs;
coarse compliant knobs (TX antenna map, sounding-cadence jitter) reachable
from userspace/hostapd; ath9k is the one credible driver-patch route.
- nexmon/ grade C: reading the compressed-BF angles is solved (nexmon_csi /
Wi-BFI); shaping the transmitted report is research-grade (D11 ucode-adjacent).
- esp32/ grade F (self) / B (supporting): cannot shape its own BF feedback
(closed esp-phy-lib blob); legitimate as a sensing detector and external-RIS
controller — the honest way ESP32 "helps scramble", via an external surface.
Docs:
- firmware/privshield/README.md: architecture, layout, and the feasibility matrix.
- ADR-290: the E2E hardware program, PROOF discipline, and per-provider decision;
added to docs/adr/README.md index.
Compliant waveform controls only, never jamming. No adapter has run on silicon;
no MEASURED claim is made (that is roadmap P5, gated on a captured log).
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
This commit is contained in:
23
firmware/privshield/esp32/veil_ris_controller/CMakeLists.txt
Normal file
23
firmware/privshield/esp32/veil_ris_controller/CMakeLists.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
# veil_ris_controller — ESP-IDF component (SYNTHETIC / L0, build-only)
|
||||
#
|
||||
# Drives an EXTERNAL reconfigurable intelligent surface (RIS) over GPIO/SPI to
|
||||
# scramble the *sensing-direction* channel while preserving the *comm-direction*
|
||||
# channel (the PrivISAC pattern, arXiv:2601.04488). This is the honest way an
|
||||
# ESP32 "helps scramble": through an external passive surface, NOT its own
|
||||
# closed Wi-Fi PHY. See the subdir README.md.
|
||||
#
|
||||
# The keyed configuration schedule reuses the portable VEIL core's SplitMix64
|
||||
# `veil_rng` (../../core/veil_shield.{h,c}) so the schedule is deterministic and
|
||||
# byte-consistent with the Rust reference — the same key can be shared with an
|
||||
# associated receiver.
|
||||
#
|
||||
# NOTE: build-only skeleton, never run on silicon. Hardware paths -> TODO(hw).
|
||||
|
||||
set(VEIL_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../core")
|
||||
|
||||
idf_component_register(
|
||||
SRCS "veil_ris_controller.c"
|
||||
"${VEIL_CORE_DIR}/veil_shield.c" # reuse veil_rng from the portable core
|
||||
INCLUDE_DIRS "include" "${VEIL_CORE_DIR}"
|
||||
REQUIRES esp_timer esp_driver_gpio esp_driver_spi
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
/* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
*
|
||||
* veil_ris_controller — drive an EXTERNAL reconfigurable intelligent surface
|
||||
* (RIS) to obfuscate the sensing-direction channel.
|
||||
*
|
||||
* STATUS: SYNTHETIC / L0. Build-only ESP-IDF component skeleton. Never flashed,
|
||||
* never captured on silicon. No RIS hardware exists in this repo. Do NOT claim
|
||||
* runtime or on-air behavior without a captured hardware log.
|
||||
*
|
||||
* WHY THIS EXISTS (honest framing): the ESP32 cannot shape its own transmitted
|
||||
* beamforming feedback — the precoding / compressed-BF-report path lives in the
|
||||
* closed Espressif Wi-Fi PHY blob (esp-phy-lib) and is not modifiable (see
|
||||
* README.md). The legitimate, compliant way an ESP32 can "help scramble" a
|
||||
* sensing signal is to act as the *controller for a separate passive surface*:
|
||||
* a RIS whose per-element phase states are switched over time. Following the
|
||||
* PrivISAC pattern (arXiv:2601.04488), each element is toggled between two
|
||||
* states chosen so the surface's response is ~identical in the *communication*
|
||||
* direction (throughput preserved) but differs sharply in the *sensing*
|
||||
* direction (an eavesdropper's channel is perturbed). The ESP32 is a GPIO/SPI
|
||||
* pin-driver here; it emits no RF of its own.
|
||||
*
|
||||
* The state schedule is *keyed* and deterministic: it is drawn from the
|
||||
* portable core's `veil_rng` (SplitMix64), so an associated / authorized
|
||||
* sensor holding the same key can reconstruct — and thus tolerate — the
|
||||
* schedule, while an unauthorized observer cannot.
|
||||
*/
|
||||
#ifndef VEIL_RIS_CONTROLLER_H
|
||||
#define VEIL_RIS_CONTROLLER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* How the surface's element bits are clocked out. */
|
||||
typedef enum {
|
||||
VEIL_RIS_IFACE_GPIO = 0, /* small surfaces: one GPIO per element / bank */
|
||||
VEIL_RIS_IFACE_SPI, /* larger surfaces: shift-register / driver IC */
|
||||
} veil_ris_iface_t;
|
||||
|
||||
typedef struct {
|
||||
veil_ris_iface_t iface;
|
||||
|
||||
/* Number of independently switchable RIS elements (or 1-bit banks). */
|
||||
size_t n_elements;
|
||||
|
||||
/* Keyed, deterministic schedule (shared with the associated receiver). */
|
||||
uint64_t key;
|
||||
|
||||
/* Dwell time per configuration, microseconds. Must be short vs. the
|
||||
* channel coherence time to spread perturbation across the sensing burst,
|
||||
* yet long enough for the surface's switching diodes to settle. */
|
||||
uint32_t dwell_us;
|
||||
|
||||
/* GPIO backend: one pin per element (n_elements <= number of pins). */
|
||||
const int *gpio_pins; /* borrowed; length == n_elements */
|
||||
|
||||
/* SPI backend: bits are packed MSB-first into ceil(n_elements/8) bytes and
|
||||
* shifted out per configuration. */
|
||||
int spi_host; /* e.g. SPI2_HOST */
|
||||
int spi_cs_gpio; /* latch / chip-select */
|
||||
int spi_clock_hz; /* driver-IC clock */
|
||||
} veil_ris_controller_cfg_t;
|
||||
|
||||
/* Initialize the chosen interface. Registration only — says nothing about a
|
||||
* physical surface actually switching. */
|
||||
esp_err_t veil_ris_controller_init(const veil_ris_controller_cfg_t *cfg);
|
||||
|
||||
/* Compute the next keyed configuration bitmap and clock it to the surface.
|
||||
* `out_bits` (optional, may be NULL) receives the packed bitmap for tests.
|
||||
* `out_len` is the byte length of `out_bits` on input. The bit pattern is
|
||||
* derived purely from `veil_rng` + the PrivISAC two-state assignment, so it is
|
||||
* reproducible from (key, step_index). */
|
||||
esp_err_t veil_ris_controller_step(uint8_t *out_bits, size_t out_len);
|
||||
|
||||
/* Start/stop a periodic timer that calls _step() every dwell_us. */
|
||||
esp_err_t veil_ris_controller_start(void);
|
||||
esp_err_t veil_ris_controller_stop(void);
|
||||
|
||||
/* Monotonic count of configurations applied since init (telemetry/tests). */
|
||||
uint64_t veil_ris_controller_step_count(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* VEIL_RIS_CONTROLLER_H */
|
||||
@@ -0,0 +1,196 @@
|
||||
/* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
*
|
||||
* veil_ris_controller — see veil_ris_controller.h.
|
||||
*
|
||||
* STATUS: SYNTHETIC / L0. Build-only skeleton. Never run on silicon; no RIS
|
||||
* hardware exists here. Hardware-touching paths are marked TODO(hw). The keyed
|
||||
* bitmap generator (pure math over veil_rng) is fully implemented and testable
|
||||
* off target; the GPIO/SPI clock-out is stubbed.
|
||||
*
|
||||
* Compliance: the ESP32 only toggles control pins of a *passive* external
|
||||
* surface. It emits no RF and does not transmit into any band. The surface
|
||||
* re-reflects ambient energy; it does not add energy or occupy spectrum, which
|
||||
* is what keeps this on the compliant side of the jamming line. (A powered,
|
||||
* amplifying, or spectrum-occupying surface would NOT be compliant and is out
|
||||
* of scope.)
|
||||
*/
|
||||
#include "veil_ris_controller.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/spi_master.h"
|
||||
|
||||
#include "veil_shield.h" /* portable core: veil_rng, veil_rng_next_u64/_f32 */
|
||||
|
||||
static const char *TAG = "veil_ris";
|
||||
|
||||
static veil_ris_controller_cfg_t s_cfg;
|
||||
static bool s_inited;
|
||||
static uint64_t s_step; /* configurations applied so far */
|
||||
static esp_timer_handle_t s_timer;
|
||||
|
||||
/* ---- keyed configuration generator (pure, testable off-target) ----------- */
|
||||
|
||||
/* PrivISAC two-state assignment: every element has two candidate phase states
|
||||
* (A/B) designed offline so the *comm-direction* array response is ~invariant
|
||||
* under A<->B while the *sensing-direction* response changes. At runtime we
|
||||
* only pick, per element, which of the two states is active this step. That
|
||||
* choice is the single bit we clock out. Drawing the bits from the keyed
|
||||
* veil_rng makes the whole schedule reproducible from (key, step_index) and
|
||||
* shareable with an authorized receiver.
|
||||
*
|
||||
* `step_index` seeds a per-step substream so any step can be regenerated
|
||||
* without replaying history (matches the core's deterministic style).
|
||||
* Fills `bits` (packed MSB-first) with n_elements selection bits. */
|
||||
void veil_ris_gen_bits(uint64_t key, uint64_t step_index,
|
||||
size_t n_elements, uint8_t *bits, size_t bits_len)
|
||||
{
|
||||
if (!bits || bits_len == 0) {
|
||||
return;
|
||||
}
|
||||
memset(bits, 0, bits_len);
|
||||
|
||||
veil_rng r;
|
||||
/* Mix the step index into the key so each dwell gets an independent draw
|
||||
* while staying a pure function of (key, step_index). */
|
||||
veil_rng_seed(&r, key ^ (step_index * 0x9E3779B97F4A7C15ULL));
|
||||
|
||||
for (size_t e = 0; e < n_elements; e++) {
|
||||
size_t byte = e >> 3;
|
||||
if (byte >= bits_len) {
|
||||
break;
|
||||
}
|
||||
/* Top bit of the draw selects state B (1) vs state A (0). */
|
||||
uint64_t w = veil_rng_next_u64(&r);
|
||||
if (w >> 63) {
|
||||
bits[byte] |= (uint8_t)(0x80u >> (e & 7));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- interface clock-out (stubs) ----------------------------------------- */
|
||||
|
||||
static esp_err_t veil_ris_write(const uint8_t *bits, size_t bits_len)
|
||||
{
|
||||
switch (s_cfg.iface) {
|
||||
case VEIL_RIS_IFACE_GPIO:
|
||||
/* TODO(hw): for each element e, set its pin to the selected state.
|
||||
* for (size_t e = 0; e < s_cfg.n_elements; e++) {
|
||||
* int level = (bits[e >> 3] >> (7 - (e & 7))) & 1;
|
||||
* gpio_set_level(s_cfg.gpio_pins[e], level);
|
||||
* }
|
||||
* Requires each pin configured as output in _init(). Unverified. */
|
||||
ESP_LOGD(TAG, "TODO(hw) GPIO write %u bits (stub)",
|
||||
(unsigned)s_cfg.n_elements);
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
case VEIL_RIS_IFACE_SPI:
|
||||
/* TODO(hw): shift the packed bitmap to the surface driver IC.
|
||||
* spi_transaction_t t = {
|
||||
* .length = bits_len * 8,
|
||||
* .tx_buffer = bits,
|
||||
* };
|
||||
* spi_device_transmit(s_spi_dev, &t); // then latch via CS
|
||||
* s_spi_dev created in _init() via spi_bus_add_device(). Unverified. */
|
||||
ESP_LOGD(TAG, "TODO(hw) SPI write %u bytes (stub)", (unsigned)bits_len);
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
default:
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- public API ---------------------------------------------------------- */
|
||||
|
||||
esp_err_t veil_ris_controller_init(const veil_ris_controller_cfg_t *cfg)
|
||||
{
|
||||
if (!cfg || cfg->n_elements == 0) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (s_inited) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
s_cfg = *cfg;
|
||||
s_step = 0;
|
||||
|
||||
if (s_cfg.iface == VEIL_RIS_IFACE_GPIO) {
|
||||
/* TODO(hw): configure each s_cfg.gpio_pins[e] as GPIO_MODE_OUTPUT via
|
||||
* gpio_config() (build a pin_bit_mask over all elements). */
|
||||
ESP_LOGW(TAG, "TODO(hw) configure %u GPIO element pins (stub)",
|
||||
(unsigned)s_cfg.n_elements);
|
||||
} else {
|
||||
/* TODO(hw): spi_bus_initialize(s_cfg.spi_host, &buscfg, ...) +
|
||||
* spi_bus_add_device(s_cfg.spi_host, &devcfg, &s_spi_dev). */
|
||||
ESP_LOGW(TAG, "TODO(hw) init SPI host %d @ %d Hz (stub)",
|
||||
s_cfg.spi_host, s_cfg.spi_clock_hz);
|
||||
}
|
||||
|
||||
s_inited = true;
|
||||
ESP_LOGI(TAG, "init (SYNTHETIC/L0): %u elements, dwell=%uus, keyed schedule",
|
||||
(unsigned)s_cfg.n_elements, s_cfg.dwell_us);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t veil_ris_controller_step(uint8_t *out_bits, size_t out_len)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
/* Bounded, malloc-free scratch: cap at 256 elements (32 bytes) for the
|
||||
* skeleton. Larger surfaces would stream in chunks. */
|
||||
enum { VEIL_RIS_MAX_BYTES = 32 };
|
||||
uint8_t bits[VEIL_RIS_MAX_BYTES];
|
||||
size_t need = (s_cfg.n_elements + 7) / 8;
|
||||
if (need > sizeof bits) {
|
||||
need = sizeof bits;
|
||||
}
|
||||
|
||||
veil_ris_gen_bits(s_cfg.key, s_step, s_cfg.n_elements, bits, need);
|
||||
esp_err_t err = veil_ris_write(bits, need); /* stub on host/no-hw */
|
||||
s_step++;
|
||||
|
||||
if (out_bits && out_len) {
|
||||
size_t n = out_len < need ? out_len : need;
|
||||
memcpy(out_bits, bits, n);
|
||||
}
|
||||
/* NOT_SUPPORTED from the stubbed writer is expected off-silicon; surface
|
||||
* the generator result as OK so tests can validate the keyed bitmap. */
|
||||
return (err == ESP_ERR_NOT_SUPPORTED) ? ESP_OK : err;
|
||||
}
|
||||
|
||||
static void veil_ris_timer_cb(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
(void)veil_ris_controller_step(NULL, 0);
|
||||
}
|
||||
|
||||
esp_err_t veil_ris_controller_start(void)
|
||||
{
|
||||
if (!s_inited) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
/* TODO(hw): a real deployment would gate this on the sensing detector's
|
||||
* engage trigger so the surface only churns during a sensing burst. */
|
||||
const esp_timer_create_args_t args = {
|
||||
.callback = veil_ris_timer_cb,
|
||||
.name = "veil_ris",
|
||||
};
|
||||
esp_err_t err = esp_timer_create(&args, &s_timer);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
return esp_timer_start_periodic(s_timer, s_cfg.dwell_us);
|
||||
}
|
||||
|
||||
esp_err_t veil_ris_controller_stop(void)
|
||||
{
|
||||
if (s_timer) {
|
||||
esp_timer_stop(s_timer);
|
||||
esp_timer_delete(s_timer);
|
||||
s_timer = NULL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint64_t veil_ris_controller_step_count(void) { return s_step; }
|
||||
Reference in New Issue
Block a user