Files
ruvnet--RuView/v2/crates/wifi-densepose-bfld/tests/signature_hasher.rs
T
ruv 0ca8a38cbc feat(adr-118/p3.5): SignatureHasher (BLAKE3-keyed) — 117/117 GREEN
Iter 15. Lands ADR-120 §2.3 — the cryptographic foundation of invariant
I3 ("cross-site identity correlation is impossible"). rf_signature_hash
is now derived from a per-site secret and a daily epoch, so two nodes
observing the same physical person produce uncorrelated 256-bit digests.

Added (no_std-compatible):
- blake3 = "1.5", default-features = false (no_std, no SIMD by default)
- src/signature_hasher.rs:
  * Constants SECONDS_PER_DAY (86_400), SITE_SALT_LEN (32), RF_SIGNATURE_LEN (32)
  * SignatureHasher { site_salt: [u8; 32] } with new(salt) const ctor
  * compute(day_epoch, &features) -> [u8; 32]  (BLAKE3 keyed mode)
  * compute_at(unix_secs, &features) -> [u8; 32] convenience
  * day_epoch_from_unix_secs(unix_secs) -> u32 helper (floor(t / 86400))
- pub use SignatureHasher, RF_SIGNATURE_LEN, SITE_SALT_LEN from lib.rs

tests/signature_hasher.rs (8 named tests, all green):
  deterministic_under_identical_inputs
  different_site_salts_produce_different_hashes
  different_day_epochs_rotate_the_hash
  different_features_produce_different_hashes
  output_length_is_32_bytes
  day_epoch_from_unix_secs_matches_floor_division
    (covers 0, 86_399, 86_400, and the 1.7e9 modern timestamp)
  compute_at_matches_compute_with_derived_day
  cross_site_hamming_distance_is_statistically_high
    *** ADR-120 §2.7 AC2 acceptance test ***
    Runs 100 trials with distinct (salt_a, salt_b) pairs observing
    identical features, computes per-trial Hamming distance, asserts
    mean >= 120 bits and min >= 80 bits. Empirically lands at ~128 bits
    mean (the expected value for two independent 256-bit hashes), with
    no trial below 80 bits — i.e., zero suspicious near-collisions.

ACs progressed:
- ADR-120 §2.7 AC2 — structurally enforced cross-site isolation, now
  proven empirically by the Hamming-distance test. This is the
  cryptographic half of invariant I3 in code, not just docs.
- ADR-118 invariant I3 — first runtime witness that two sites with
  independent site_salts cannot correlate the same person's signature.

Test config:
- cargo test --no-default-features → 72 passed (64 + 8; signature_hasher is no_std)
- cargo test                       → 117 passed (109 + 8)

Out of scope (next iter target):
- Wire SignatureHasher into BfldEmitter: replace caller-supplied
  rf_signature_hash with hasher.compute_at(ts, &features) so the
  pipeline produces correct hashes end-to-end.
- IdentityFeatures canonical-bytes encoder so callers don't need to
  hand-serialize per-feature representations.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 15:47:21 -04:00

123 lines
3.9 KiB
Rust

//! Acceptance tests for ADR-120 §2.3 / §2.7 — `SignatureHasher` cross-site
//! isolation and daily rotation.
use wifi_densepose_bfld::{SignatureHasher, RF_SIGNATURE_LEN, SITE_SALT_LEN};
fn salt(seed: u8) -> [u8; SITE_SALT_LEN] {
let mut s = [0u8; SITE_SALT_LEN];
for (i, b) in s.iter_mut().enumerate() {
*b = seed.wrapping_add(i as u8);
}
s
}
fn features(seed: u8) -> Vec<u8> {
(0..64u8).map(|i| i.wrapping_add(seed)).collect()
}
fn hamming_distance(a: &[u8; RF_SIGNATURE_LEN], b: &[u8; RF_SIGNATURE_LEN]) -> u32 {
a.iter()
.zip(b.iter())
.map(|(x, y)| (x ^ y).count_ones())
.sum()
}
#[test]
fn deterministic_under_identical_inputs() {
let h = SignatureHasher::new(salt(7));
let a = h.compute(42, &features(0));
let b = h.compute(42, &features(0));
assert_eq!(a, b, "identical inputs must produce identical hashes");
}
#[test]
fn different_site_salts_produce_different_hashes() {
let a = SignatureHasher::new(salt(1)).compute(42, &features(0));
let b = SignatureHasher::new(salt(2)).compute(42, &features(0));
assert_ne!(a, b);
}
#[test]
fn different_day_epochs_rotate_the_hash() {
let h = SignatureHasher::new(salt(7));
let day0 = h.compute(0, &features(0));
let day1 = h.compute(1, &features(0));
assert_ne!(day0, day1, "day rotation must change the hash");
}
#[test]
fn different_features_produce_different_hashes() {
let h = SignatureHasher::new(salt(7));
let a = h.compute(42, &features(0));
let b = h.compute(42, &features(1));
assert_ne!(a, b);
}
#[test]
fn output_length_is_32_bytes() {
let h = SignatureHasher::new(salt(0));
let out = h.compute(0, b"");
assert_eq!(out.len(), RF_SIGNATURE_LEN);
assert_eq!(RF_SIGNATURE_LEN, 32);
}
#[test]
fn day_epoch_from_unix_secs_matches_floor_division() {
assert_eq!(SignatureHasher::day_epoch_from_unix_secs(0), 0);
assert_eq!(SignatureHasher::day_epoch_from_unix_secs(86_399), 0);
assert_eq!(SignatureHasher::day_epoch_from_unix_secs(86_400), 1);
// Unix epoch ≈ 1.7e9 sec on date in 2024-ish; just check the math:
assert_eq!(
SignatureHasher::day_epoch_from_unix_secs(1_700_000_000),
(1_700_000_000u64 / 86_400) as u32,
);
}
#[test]
fn compute_at_matches_compute_with_derived_day() {
let h = SignatureHasher::new(salt(3));
let unix_secs: u64 = 1_700_000_000;
let day = SignatureHasher::day_epoch_from_unix_secs(unix_secs);
let a = h.compute(day, &features(0));
let b = h.compute_at(unix_secs, &features(0));
assert_eq!(a, b);
}
/// ADR-120 §2.7 AC2 — structural cross-site isolation.
///
/// Two BFLD nodes with different `site_salt` values observing the same
/// (simulated) person produce `rf_signature_hash` values whose Hamming
/// distance is statistically high (≈ 128 bits expected for two independent
/// 256-bit outputs; ADR threshold is ≥ 120 over 100 trials).
#[test]
fn cross_site_hamming_distance_is_statistically_high() {
let n_trials: usize = 100;
let mut total: u32 = 0;
let mut min_observed: u32 = u32::MAX;
for trial in 0..n_trials {
let site_a = SignatureHasher::new(salt(trial as u8));
let site_b = SignatureHasher::new(salt((trial as u8).wrapping_add(0xA5)));
let day = trial as u32;
let feats = features(trial as u8);
let h_a = site_a.compute(day, &feats);
let h_b = site_b.compute(day, &feats);
let d = hamming_distance(&h_a, &h_b);
total += d;
min_observed = min_observed.min(d);
}
let mean = total as f32 / n_trials as f32;
// Expectation for two independent 256-bit hashes is 128 bits; require ≥ 120
// per ADR-120 §2.7 AC2.
assert!(
mean >= 120.0,
"mean Hamming distance must be >= 120, got {mean}",
);
// Minimum observed should also be far above 0 (no collisions).
assert!(
min_observed >= 80,
"min Hamming distance suspiciously low: {min_observed}",
);
}