Files
ruvnet--RuView/v2/crates/wifi-densepose-bfld/tests/privacy_gate_demote.rs
T
ruv 4a6498fc2f feat(adr-118/p3.1): PrivacyGate::demote monotonic class transformer (60/60 GREEN)
Iter 9. Lands ADR-120 §2.4 — the only operation that can lower a frame's
information content. Demote is monotonic by construction (Result::Err
on non-monotone target), strips payload sections per the target class
table, and re-syncs header.privacy_class + CRC32.

Added:
- src/privacy_gate.rs (gated on `feature = "std"`):
  * PrivacyGate unit struct (+ Default impl)
  * PrivacyGate::demote(BfldFrame, target: PrivacyClass) -> Result<BfldFrame>
  * Stripping policy:
      target >= Anonymous (2): zeros + clears compressed_angle_matrix and
        csi_delta; sets csi_delta = None so from_payload clears HAS_CSI_DELTA
      target >= Restricted (3): also zeros + clears amplitude_proxy and phase_proxy
  * zeroize_then_clear helper — overwrite with 0 then black_box then truncate
- BfldError::InvalidDemote { from: u8, to: u8 } variant
- pub use PrivacyGate from lib.rs

Note: demote does NOT zero the original Vec capacity that the heap allocator
may still hold — the buffers we own are zeroed and cleared, but the
intermediate Vec passed back to BfldFrame::from_payload reallocates anew.
For strict heap zeroization in regulated deployments, a follow-up iter can
substitute zeroize::Zeroizing<Vec<u8>>.

tests/privacy_gate_demote.rs (7 named tests, all green):
  demote_to_same_class_is_identity
  demote_derived_to_anonymous_strips_compressed_angle_matrix
    (also asserts csi_delta dropped, snr_vector and amplitude_proxy preserved)
  demote_derived_to_restricted_strips_amplitude_and_phase_too
    (snr_vector and vendor_extension survive at class 3)
  demote_anonymous_to_derived_is_rejected
    (asserts InvalidDemote { from: 2, to: 1 })
  demote_to_raw_is_rejected_from_any_higher_class
    (parameterized over Derived, Anonymous, Restricted as sources)
  demote_preserves_frame_crc_consistency_through_wire_roundtrip
    (post-demote frame survives to_bytes -> from_bytes with no CRC error)
  demote_clears_has_csi_delta_flag_bit

ACs progressed:
- AC5 ↑ — privacy_mode enforcement at the frame-class boundary now works
  through PrivacyGate, not just the BfldEvent emitter (deferred). When the
  active class is Anonymous (2) or Restricted (3), the angle matrix /
  csi_delta / amplitude / phase sections that carry identity information
  are zeroed before any downstream code sees them.
- AC4 ↑ — demoted frames retain valid CRC; the round-trip-through-bytes
  test proves bit-correctness after the class transition.

Test config:
- cargo test --no-default-features → 31 passed (privacy_gate cfg-out)
- cargo test                       → 60 passed (53 + 7)

Out of scope (next iter target):
- SoulMatchOracle stub trait + no-op default impl (ADR-121 §2.6) so the
  Recalibrate exemption hook is wireable from `--features soul-signature`.
- IdentityRiskEngine — multiplicative formula on (sep, stab, consist, conf)
  with the coherence-gate GateAction enum (ADR-121 §2.2 + §2.4).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 14:48:01 -04:00

115 lines
4.3 KiB
Rust

//! Acceptance tests for ADR-120 §2.4 — `PrivacyGate::demote` monotonic class
//! transitions and payload-section zeroization.
#![cfg(feature = "std")]
use wifi_densepose_bfld::{
BfldError, BfldFrame, BfldFrameHeader, BfldPayload, PrivacyClass, PrivacyGate,
};
fn frame_at_class(class: PrivacyClass, with_csi: bool) -> BfldFrame {
let payload = BfldPayload {
compressed_angle_matrix: vec![0x11; 32],
amplitude_proxy: vec![0x22; 16],
phase_proxy: vec![0x33; 16],
snr_vector: vec![0x44; 8],
csi_delta: if with_csi { Some(vec![0x55; 24]) } else { None },
vendor_extension: vec![0xAA],
};
let mut header = BfldFrameHeader::empty();
header.privacy_class = class.as_u8();
BfldFrame::from_payload(header, &payload)
}
#[test]
fn demote_to_same_class_is_identity() {
let f = frame_at_class(PrivacyClass::Derived, false);
let out = PrivacyGate::demote(f, PrivacyClass::Derived).expect("same-class demote OK");
assert_eq!({ out.header.privacy_class }, PrivacyClass::Derived.as_u8());
}
#[test]
fn demote_derived_to_anonymous_strips_compressed_angle_matrix() {
let f = frame_at_class(PrivacyClass::Derived, true);
let out = PrivacyGate::demote(f, PrivacyClass::Anonymous).expect("demote");
assert_eq!({ out.header.privacy_class }, PrivacyClass::Anonymous.as_u8());
let payload = out.parse_payload().expect("payload still parses");
assert!(
payload.compressed_angle_matrix.is_empty(),
"angle matrix must be stripped at class 2",
);
// CSI delta also dropped at Anonymous.
assert!(payload.csi_delta.is_none(), "csi_delta dropped at class 2");
// Sensing sections preserved.
assert_eq!(payload.snr_vector.len(), 8);
assert_eq!(payload.amplitude_proxy.len(), 16);
}
#[test]
fn demote_derived_to_restricted_strips_amplitude_and_phase_too() {
let f = frame_at_class(PrivacyClass::Derived, true);
let out = PrivacyGate::demote(f, PrivacyClass::Restricted).expect("demote");
assert_eq!({ out.header.privacy_class }, PrivacyClass::Restricted.as_u8());
let payload = out.parse_payload().expect("payload parses");
assert!(payload.compressed_angle_matrix.is_empty());
assert!(payload.amplitude_proxy.is_empty(), "amplitude stripped at class 3");
assert!(payload.phase_proxy.is_empty(), "phase stripped at class 3");
// SNR + vendor still survive.
assert_eq!(payload.snr_vector.len(), 8);
assert_eq!(payload.vendor_extension.len(), 1);
}
#[test]
fn demote_anonymous_to_derived_is_rejected() {
let f = frame_at_class(PrivacyClass::Anonymous, false);
match PrivacyGate::demote(f, PrivacyClass::Derived) {
Err(BfldError::InvalidDemote { from, to }) => {
assert_eq!(from, PrivacyClass::Anonymous.as_u8());
assert_eq!(to, PrivacyClass::Derived.as_u8());
}
other => panic!("expected InvalidDemote, got {other:?}"),
}
}
#[test]
fn demote_to_raw_is_rejected_from_any_higher_class() {
for src in [
PrivacyClass::Derived,
PrivacyClass::Anonymous,
PrivacyClass::Restricted,
] {
let f = frame_at_class(src, false);
match PrivacyGate::demote(f, PrivacyClass::Raw) {
Err(BfldError::InvalidDemote { .. }) => {}
other => panic!("expected InvalidDemote from {src:?}, got {other:?}"),
}
}
}
#[test]
fn demote_preserves_frame_crc_consistency_through_wire_roundtrip() {
// Demote produces a frame; that frame must round-trip through bytes
// with no CRC error.
let f = frame_at_class(PrivacyClass::Derived, true);
let demoted = PrivacyGate::demote(f, PrivacyClass::Anonymous).expect("demote");
let bytes = demoted.to_bytes();
let parsed = BfldFrame::from_bytes(&bytes).expect("post-demote frame must round-trip");
assert_eq!({ parsed.header.privacy_class }, PrivacyClass::Anonymous.as_u8());
}
#[test]
fn demote_clears_has_csi_delta_flag_bit() {
use wifi_densepose_bfld::frame::flags;
let f = frame_at_class(PrivacyClass::Derived, true);
assert_ne!({ f.header.flags } & flags::HAS_CSI_DELTA, 0);
let out = PrivacyGate::demote(f, PrivacyClass::Anonymous).expect("demote");
assert_eq!(
{ out.header.flags } & flags::HAS_CSI_DELTA,
0,
"HAS_CSI_DELTA must clear when csi_delta is stripped",
);
}