use std::sync::atomic::Ordering;
use super::evaluator::SensingCounters;
use super::identity::AudienceScopeCommitment;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ScopeError {
WireClaimMismatch,
CrossRootRefused,
AudienceMismatch,
}
impl ScopeError {
pub const fn is_security_relevant(self) -> bool {
matches!(self, Self::WireClaimMismatch)
}
}
impl std::fmt::Display for ScopeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WireClaimMismatch => {
f.write_str("wire-claimed root not backed by session identity")
}
Self::CrossRootRefused => f.write_str("cross-root subscriber refused (v1 boundary)"),
Self::AudienceMismatch => {
f.write_str("interest audience does not match the session-proven root")
}
}
}
}
impl std::error::Error for ScopeError {}
pub fn validate_subscriber_scope(
session_root: &AudienceScopeCommitment,
claimed_root: &AudienceScopeCommitment,
local_root: &AudienceScopeCommitment,
interest_audience: &AudienceScopeCommitment,
counters: &SensingCounters,
) -> Result<AudienceScopeCommitment, ScopeError> {
let refuse = |error: ScopeError| {
counters.scope_refusals.fetch_add(1, Ordering::Relaxed);
if error.is_security_relevant() {
counters.protocol_invalid.fetch_add(1, Ordering::Relaxed);
}
Err(error)
};
if claimed_root != session_root {
return refuse(ScopeError::WireClaimMismatch);
}
if session_root != local_root {
return refuse(ScopeError::CrossRootRefused);
}
if interest_audience != session_root {
return refuse(ScopeError::AudienceMismatch);
}
Ok(*session_root)
}
#[cfg(test)]
mod tests {
use super::*;
fn root(byte: u8) -> AudienceScopeCommitment {
AudienceScopeCommitment::from_bytes([byte; 32])
}
fn count(counter: &std::sync::atomic::AtomicU64) -> u64 {
SensingCounters::get(counter)
}
#[test]
fn happy_path_returns_the_proven_root() {
let counters = SensingCounters::default();
let owner = root(0xAA);
let proven = validate_subscriber_scope(&owner, &owner, &owner, &owner, &counters).unwrap();
assert_eq!(proven, owner);
assert_eq!(count(&counters.scope_refusals), 0);
assert_eq!(count(&counters.protocol_invalid), 0);
}
#[test]
fn wire_claim_is_never_load_bearing() {
let counters = SensingCounters::default();
let owner = root(0xAA);
let forged = root(0xEE);
assert_eq!(
validate_subscriber_scope(&owner, &forged, &owner, &owner, &counters),
Err(ScopeError::WireClaimMismatch),
);
assert_eq!(count(&counters.scope_refusals), 1);
assert_eq!(count(&counters.protocol_invalid), 1);
}
#[test]
fn cross_root_sessions_are_refused_without_security_noise() {
let counters = SensingCounters::default();
let ours = root(0xAA);
let theirs = root(0xBB);
assert_eq!(
validate_subscriber_scope(&theirs, &theirs, &ours, &theirs, &counters),
Err(ScopeError::CrossRootRefused),
);
assert_eq!(count(&counters.scope_refusals), 1);
assert_eq!(count(&counters.protocol_invalid), 0);
}
#[test]
fn interest_audience_must_match_the_proven_root() {
let counters = SensingCounters::default();
let owner = root(0xAA);
let other_audience = root(0xCC);
assert_eq!(
validate_subscriber_scope(&owner, &owner, &owner, &other_audience, &counters),
Err(ScopeError::AudienceMismatch),
);
assert_eq!(count(&counters.scope_refusals), 1);
assert_eq!(count(&counters.protocol_invalid), 0);
}
}