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>
This commit is contained in:
ruv
2026-05-24 15:47:21 -04:00
parent 9c518f6e36
commit 0ca8a38cbc
5 changed files with 241 additions and 5 deletions
+2
View File
@@ -26,6 +26,7 @@ pub mod identity_risk;
pub mod payload;
#[cfg(feature = "std")]
pub mod privacy_gate;
pub mod signature_hasher;
pub mod sink;
pub use coherence_gate::{CoherenceGate, MatchOutcome, NullOracle, SoulMatchOracle};
@@ -43,6 +44,7 @@ pub use frame::BfldFrame;
pub use payload::BfldPayload;
#[cfg(feature = "std")]
pub use privacy_gate::PrivacyGate;
pub use signature_hasher::{SignatureHasher, RF_SIGNATURE_LEN, SITE_SALT_LEN};
pub use sink::{check_class, LocalSink, MatterSink, NetworkSink, Sink};
/// Privacy classification carried in every `BfldFrame`. See ADR-120 §2.1.