use crate::security::authenticated::AuthenticatedSymbol;
use crate::security::error::{AuthError, AuthErrorKind};
use crate::security::key::AuthKey;
use crate::security::tag::AuthenticationTag;
use crate::types::Symbol;
use hmac::{Hmac, KeyInit, Mac};
use parking_lot::RwLock;
use sha2::Sha256;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
type HmacSha256 = Hmac<Sha256>;
const REPLICA_AUTHORIZATION_DOMAIN: &[u8] = b"asupersync::security::replica_authorization::v1";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthMode {
Strict,
Permissive,
Disabled,
}
impl AuthMode {
#[inline]
#[must_use]
pub const fn is_at_least_as_strict_as(self, other: Self) -> bool {
let lhs = self.strictness_rank();
let rhs = other.strictness_rank();
lhs >= rhs
}
const fn strictness_rank(self) -> u8 {
match self {
Self::Strict => 2,
Self::Permissive => 1,
Self::Disabled => 0,
}
}
}
#[derive(Debug, Default)]
pub struct AuthStats {
pub signed: AtomicU64,
pub verified_ok: AtomicU64,
pub verified_fail: AtomicU64,
pub failures_allowed: AtomicU64,
pub skipped: AtomicU64,
}
#[derive(Debug, Clone)]
pub struct SecurityContext {
key: AuthKey,
mode: AuthMode,
stats: Arc<AuthStats>,
replica_authorizations: Arc<RwLock<BTreeMap<String, ReplicaAuthorization>>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplicaAuthorization {
pub replica_id: String,
pub region_id: Option<String>,
pub signature: [u8; 32],
}
impl SecurityContext {
#[must_use]
pub fn new(key: AuthKey) -> Self {
Self {
key,
mode: AuthMode::Strict,
stats: Arc::new(AuthStats::default()),
replica_authorizations: Arc::new(RwLock::new(BTreeMap::new())),
}
}
#[must_use]
pub fn for_testing(seed: u64) -> Self {
Self::new(AuthKey::from_seed(seed))
}
#[cfg(any(test, feature = "test-internals"))]
#[must_use]
pub fn for_testing_with_mode(seed: u64, mode: AuthMode) -> Self {
let mut ctx = Self::new(AuthKey::from_seed(seed));
ctx.mode = mode;
ctx
}
#[cfg(any(test, feature = "test-internals"))]
#[must_use]
pub fn for_testing_with_key_and_mode(key: AuthKey, mode: AuthMode) -> Self {
let mut ctx = Self::new(key);
ctx.mode = mode;
ctx
}
#[must_use]
pub fn with_mode(mut self, mode: AuthMode) -> Self {
assert!(
mode.is_at_least_as_strict_as(self.mode),
"br-asupersync-jgpcvp: SecurityContext::with_mode rejects downgrade from {:?} to {:?}. \
Mode transitions may only INCREASE strictness (Disabled < Permissive < Strict). \
If this is a test that needs to start in {:?} mode, use \
SecurityContext::for_testing_with_mode instead.",
self.mode,
mode,
mode,
);
self.mode = mode;
self
}
#[must_use]
pub fn sign_symbol(&self, symbol: &Symbol) -> AuthenticatedSymbol {
let tag = AuthenticationTag::compute(&self.key, symbol);
self.stats.signed.fetch_add(1, Ordering::Relaxed);
AuthenticatedSymbol::new_verified(symbol.clone(), tag)
}
pub fn verify_authenticated_symbol(
&self,
auth: &mut AuthenticatedSymbol,
) -> Result<(), AuthError> {
if self.mode == AuthMode::Disabled {
self.stats.skipped.fetch_add(1, Ordering::Relaxed);
return Ok(());
}
let is_valid = auth.tag().verify(&self.key, auth.symbol());
auth.set_verified(is_valid);
if is_valid {
self.stats.verified_ok.fetch_add(1, Ordering::Relaxed);
Ok(())
} else {
self.stats.verified_fail.fetch_add(1, Ordering::Relaxed);
match self.mode {
AuthMode::Strict => Err(AuthError::new(
AuthErrorKind::InvalidTag,
format!("symbol verification failed for {}", auth.symbol().id()),
)),
AuthMode::Permissive => {
self.stats.failures_allowed.fetch_add(1, Ordering::Relaxed);
Ok(())
}
AuthMode::Disabled => unreachable!(),
}
}
}
#[must_use]
pub fn derive_context(&self, purpose: &[u8]) -> Self {
Self {
key: self.key.derive_subkey(purpose),
mode: self.mode,
stats: Arc::new(AuthStats::default()), replica_authorizations: Arc::new(RwLock::new(BTreeMap::new())),
}
}
#[must_use]
pub fn stats(&self) -> &AuthStats {
&self.stats
}
pub fn authorize_replica(
&self,
replica_id: &str,
region_id: Option<&str>,
) -> Result<ReplicaAuthorization, AuthError> {
Self::validate_replica_identifier(replica_id)?;
if let Some(region_id) = region_id {
Self::validate_region_identifier(region_id)?;
}
let record = ReplicaAuthorization {
replica_id: replica_id.to_owned(),
region_id: region_id.map(str::to_owned),
signature: self.replica_authorization_signature(replica_id, region_id),
};
self.replica_authorizations
.write()
.insert(replica_id.to_owned(), record.clone());
Ok(record)
}
pub fn import_replica_authorization(
&self,
record: ReplicaAuthorization,
) -> Result<(), AuthError> {
Self::validate_replica_identifier(&record.replica_id)?;
if let Some(region_id) = record.region_id.as_deref() {
Self::validate_region_identifier(region_id)?;
}
if !self.replica_authorization_signature_matches(&record) {
return Err(AuthError::new(
AuthErrorKind::InvalidTag,
"replica authorization signature mismatch",
));
}
self.replica_authorizations
.write()
.insert(record.replica_id.clone(), record);
Ok(())
}
pub fn revoke_replica_authorization(&self, replica_id: &str) -> bool {
self.replica_authorizations
.write()
.remove(replica_id)
.is_some()
}
#[must_use]
pub fn is_replica_authorized(&self, replica_id: &str, region_id: Option<&str>) -> bool {
if Self::validate_replica_identifier(replica_id).is_err() {
return false;
}
if let Some(region_id) = region_id {
if Self::validate_region_identifier(region_id).is_err() {
return false;
}
}
let authorizations = self.replica_authorizations.read();
let Some(record) = authorizations.get(replica_id) else {
return false;
};
if !self.replica_authorization_signature_matches(record) {
return false;
}
match (record.region_id.as_deref(), region_id) {
(None, _) => true,
(Some(expected), Some(actual)) => expected == actual,
(Some(_), None) => false,
}
}
fn replica_authorization_signature_matches(&self, record: &ReplicaAuthorization) -> bool {
use subtle::ConstantTimeEq;
let expected =
self.replica_authorization_signature(&record.replica_id, record.region_id.as_deref());
record.signature.ct_eq(&expected).into()
}
fn replica_authorization_signature(
&self,
replica_id: &str,
region_id: Option<&str>,
) -> [u8; 32] {
let mut mac =
HmacSha256::new_from_slice(self.key.as_bytes()).expect("HMAC accepts any key length");
mac.update(REPLICA_AUTHORIZATION_DOMAIN);
Self::update_len_prefixed(&mut mac, replica_id.as_bytes());
match region_id {
Some(region_id) => {
mac.update(&[1]);
Self::update_len_prefixed(&mut mac, region_id.as_bytes());
}
None => mac.update(&[0]),
}
mac.finalize().into_bytes().into()
}
fn update_len_prefixed(mac: &mut HmacSha256, bytes: &[u8]) {
mac.update(&(bytes.len() as u64).to_be_bytes());
mac.update(bytes);
}
fn validate_replica_identifier(value: &str) -> Result<(), AuthError> {
Self::validate_wire_identifier("replica", value)
}
fn validate_region_identifier(value: &str) -> Result<(), AuthError> {
Self::validate_wire_identifier("region", value)
}
fn validate_wire_identifier(kind: &str, value: &str) -> Result<(), AuthError> {
if value.is_empty() || value.len() > 256 {
return Err(AuthError::new(
AuthErrorKind::MalformedPayload,
format!("{kind} identifier length is outside 1..=256 bytes"),
));
}
if value.contains("../")
|| value.contains('\0')
|| value.bytes().any(|byte| {
!matches!(
byte,
b'a'..=b'z'
| b'A'..=b'Z'
| b'0'..=b'9'
| b'.'
| b'_'
| b'-'
| b':'
)
})
{
return Err(AuthError::new(
AuthErrorKind::MalformedPayload,
format!("{kind} identifier contains unsupported bytes"),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::pedantic,
clippy::nursery,
clippy::expect_fun_call,
clippy::map_unwrap_or,
clippy::cast_possible_wrap,
clippy::future_not_send
)]
use super::*;
use crate::types::{SymbolId, SymbolKind};
#[test]
fn context_creation() {
let key = AuthKey::from_seed(42);
let ctx = SecurityContext::new(key);
assert!(matches!(ctx.mode, AuthMode::Strict));
}
#[test]
fn replica_authorization_basic_validation() {
let ctx = SecurityContext::for_testing(42);
assert!(!ctx.is_replica_authorized("replica-1", None));
ctx.authorize_replica("replica-1", None)
.expect("global replica authorization should mint");
ctx.authorize_replica("node-auth", Some("region-a"))
.expect("region-scoped replica authorization should mint");
assert!(ctx.is_replica_authorized("replica-1", None));
assert!(ctx.is_replica_authorized("replica-1", Some("any-region")));
assert!(ctx.is_replica_authorized("node-auth", Some("region-a")));
assert!(!ctx.is_replica_authorized("node-auth", Some("region-b")));
assert!(!ctx.is_replica_authorized("node-auth", None));
assert!(!ctx.is_replica_authorized("", None));
assert!(!ctx.is_replica_authorized("../../../etc/passwd", None));
assert!(!ctx.is_replica_authorized("replica\0null", None));
assert!(!ctx.is_replica_authorized(&"x".repeat(300), None)); }
#[test]
fn replica_authorization_import_rejects_tampered_records() {
let issuer = SecurityContext::for_testing(42);
let verifier = SecurityContext::for_testing(42);
let mut record = issuer
.authorize_replica("replica-2", Some("region-a"))
.expect("issuer should mint record");
verifier
.import_replica_authorization(record.clone())
.expect("matching verifier key should accept record");
assert!(verifier.is_replica_authorized("replica-2", Some("region-a")));
record.signature[0] ^= 0x80;
let error = verifier
.import_replica_authorization(record)
.expect_err("tampered signature must fail closed");
assert_eq!(error.kind(), AuthErrorKind::InvalidTag);
}
#[test]
fn replica_authorization_revocation_removes_membership() {
let ctx = SecurityContext::for_testing(42);
ctx.authorize_replica("replica-3", None)
.expect("authorization should mint");
assert!(ctx.is_replica_authorized("replica-3", None));
assert!(ctx.revoke_replica_authorization("replica-3"));
assert!(!ctx.is_replica_authorized("replica-3", None));
assert!(!ctx.revoke_replica_authorization("replica-3"));
}
#[test]
fn context_sign_and_verify() {
let ctx = SecurityContext::for_testing(123);
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![1, 2, 3], SymbolKind::Source);
let auth = ctx.sign_symbol(&symbol);
assert!(auth.is_verified());
let mut received = AuthenticatedSymbol::from_parts(auth.clone().into_symbol(), *auth.tag());
assert!(!received.is_verified());
ctx.verify_authenticated_symbol(&mut received)
.expect("verification failed");
assert!(received.is_verified());
}
#[test]
fn disabled_mode_skips_verification() {
let ctx = SecurityContext::for_testing_with_mode(123, AuthMode::Disabled);
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![1, 2, 3], SymbolKind::Source);
let tag = AuthenticationTag::zero();
let mut auth = AuthenticatedSymbol::from_parts(symbol, tag);
ctx.verify_authenticated_symbol(&mut auth)
.expect("disabled mode should not error");
assert!(!auth.is_verified()); assert_eq!(ctx.stats().skipped.load(Ordering::Relaxed), 1);
}
#[test]
fn strict_mode_fails_verification() {
let ctx = SecurityContext::for_testing(123);
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![1, 2, 3], SymbolKind::Source);
let tag = AuthenticationTag::zero();
let mut auth = AuthenticatedSymbol::from_parts(symbol, tag);
let result = ctx.verify_authenticated_symbol(&mut auth);
assert!(result.is_err());
assert!(result.unwrap_err().is_invalid_tag());
assert!(!auth.is_verified());
assert_eq!(ctx.stats().verified_fail.load(Ordering::Relaxed), 1);
}
#[test]
fn permissive_mode_allows_failures() {
let ctx = SecurityContext::for_testing_with_mode(123, AuthMode::Permissive);
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![1, 2, 3], SymbolKind::Source);
let tag = AuthenticationTag::zero();
let mut auth = AuthenticatedSymbol::from_parts(symbol, tag);
let result = ctx.verify_authenticated_symbol(&mut auth);
assert!(result.is_ok());
assert!(!auth.is_verified());
assert_eq!(ctx.stats().verified_fail.load(Ordering::Relaxed), 1);
assert_eq!(ctx.stats().failures_allowed.load(Ordering::Relaxed), 1);
}
#[test]
fn strict_mode_clears_preverified_flag_on_failure() {
let signing_ctx = SecurityContext::for_testing(123);
let verifying_ctx = SecurityContext::for_testing(456);
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![1, 2, 3], SymbolKind::Source);
let mut auth = signing_ctx.sign_symbol(&symbol);
assert!(auth.is_verified());
let result = verifying_ctx.verify_authenticated_symbol(&mut auth);
assert!(result.is_err());
assert!(result.unwrap_err().is_invalid_tag());
assert!(
!auth.is_verified(),
"failed re-verification must clear any stale trusted state"
);
assert_eq!(
verifying_ctx.stats().verified_fail.load(Ordering::Relaxed),
1
);
assert_eq!(verifying_ctx.stats().verified_ok.load(Ordering::Relaxed), 0);
}
#[test]
fn permissive_mode_clears_preverified_flag_on_failure() {
let signing_ctx = SecurityContext::for_testing(123);
let verifying_ctx = SecurityContext::for_testing_with_mode(456, AuthMode::Permissive);
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![1, 2, 3], SymbolKind::Source);
let mut auth = signing_ctx.sign_symbol(&symbol);
assert!(auth.is_verified());
let result = verifying_ctx.verify_authenticated_symbol(&mut auth);
assert!(result.is_ok());
assert!(
!auth.is_verified(),
"permissive mode may allow the symbol through, but it must not preserve a stale verified flag"
);
assert_eq!(
verifying_ctx.stats().verified_fail.load(Ordering::Relaxed),
1
);
assert_eq!(
verifying_ctx
.stats()
.failures_allowed
.load(Ordering::Relaxed),
1
);
}
#[test]
fn accessor_delegation() {
let ctx = SecurityContext::for_testing(123);
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![1, 2, 3], SymbolKind::Source);
let auth = ctx.sign_symbol(&symbol);
assert_eq!(ctx.stats().signed.load(Ordering::Relaxed), 1);
assert_eq!(auth.symbol(), &symbol);
}
#[test]
fn cross_context_verification_isolation() {
let primary = SecurityContext::for_testing(99);
let transport_ctx = primary.derive_context(b"transport");
let storage_ctx = primary.derive_context(b"storage");
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![10, 20, 30], SymbolKind::Source);
let auth = transport_ctx.sign_symbol(&symbol);
assert!(auth.is_verified());
let mut received = AuthenticatedSymbol::from_parts(auth.clone().into_symbol(), *auth.tag());
let result = storage_ctx.verify_authenticated_symbol(&mut received);
assert!(result.is_err());
assert!(result.unwrap_err().is_invalid_tag());
assert!(!received.is_verified());
let mut received2 =
AuthenticatedSymbol::from_parts(auth.clone().into_symbol(), *auth.tag());
transport_ctx
.verify_authenticated_symbol(&mut received2)
.expect("same context verification must succeed");
assert!(received2.is_verified());
}
#[test]
fn permissive_mode_with_valid_tag_marks_verified() {
let ctx = SecurityContext::for_testing_with_mode(42, AuthMode::Permissive);
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![5, 6, 7], SymbolKind::Source);
let auth = ctx.sign_symbol(&symbol);
let mut received = AuthenticatedSymbol::from_parts(auth.clone().into_symbol(), *auth.tag());
assert!(!received.is_verified());
ctx.verify_authenticated_symbol(&mut received)
.expect("valid tag in permissive mode should succeed");
assert!(received.is_verified());
assert_eq!(ctx.stats().verified_ok.load(Ordering::Relaxed), 1);
assert_eq!(ctx.stats().failures_allowed.load(Ordering::Relaxed), 0);
}
#[test]
fn disabled_mode_preserves_pre_verified_flag() {
let signing_ctx = SecurityContext::for_testing(42);
let disabled_ctx = SecurityContext::for_testing_with_mode(42, AuthMode::Disabled);
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![1, 2], SymbolKind::Source);
let mut auth = signing_ctx.sign_symbol(&symbol);
assert!(auth.is_verified());
disabled_ctx
.verify_authenticated_symbol(&mut auth)
.expect("disabled mode never errors");
assert!(
auth.is_verified(),
"disabled mode must not clear pre-existing verified flag"
);
}
#[test]
fn cloned_context_shares_stats() {
let ctx1 = SecurityContext::for_testing(77);
let ctx2 = ctx1.clone();
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![1], SymbolKind::Source);
let _ = ctx1.sign_symbol(&symbol);
let _ = ctx2.sign_symbol(&symbol);
assert_eq!(ctx1.stats().signed.load(Ordering::Relaxed), 2);
assert_eq!(ctx2.stats().signed.load(Ordering::Relaxed), 2);
}
#[test]
#[should_panic(expected = "br-asupersync-jgpcvp")]
fn with_mode_panics_on_strict_to_permissive_downgrade() {
let _ = SecurityContext::for_testing(1).with_mode(AuthMode::Permissive);
}
#[test]
#[should_panic(expected = "br-asupersync-jgpcvp")]
fn with_mode_panics_on_strict_to_disabled_downgrade() {
let _ = SecurityContext::for_testing(2).with_mode(AuthMode::Disabled);
}
#[test]
#[should_panic(expected = "br-asupersync-jgpcvp")]
fn with_mode_panics_on_permissive_to_disabled_downgrade() {
let ctx = SecurityContext::for_testing_with_mode(3, AuthMode::Permissive);
let _ = ctx.with_mode(AuthMode::Disabled);
}
#[test]
fn with_mode_allows_strict_to_strict_no_op() {
let ctx = SecurityContext::for_testing(4).with_mode(AuthMode::Strict);
assert!(matches!(ctx.mode, AuthMode::Strict));
}
#[test]
fn with_mode_allows_permissive_to_strict_upgrade() {
let ctx = SecurityContext::for_testing_with_mode(5, AuthMode::Permissive);
let upgraded = ctx.with_mode(AuthMode::Strict);
assert!(matches!(upgraded.mode, AuthMode::Strict));
}
#[test]
fn with_mode_allows_disabled_to_permissive_upgrade() {
let ctx = SecurityContext::for_testing_with_mode(6, AuthMode::Disabled);
let upgraded = ctx.with_mode(AuthMode::Permissive);
assert!(matches!(upgraded.mode, AuthMode::Permissive));
}
#[test]
fn with_mode_allows_disabled_to_strict_upgrade() {
let ctx = SecurityContext::for_testing_with_mode(7, AuthMode::Disabled);
let upgraded = ctx.with_mode(AuthMode::Strict);
assert!(matches!(upgraded.mode, AuthMode::Strict));
}
#[test]
fn auth_mode_strictness_ordering() {
assert!(AuthMode::Strict.is_at_least_as_strict_as(AuthMode::Strict));
assert!(AuthMode::Strict.is_at_least_as_strict_as(AuthMode::Permissive));
assert!(AuthMode::Strict.is_at_least_as_strict_as(AuthMode::Disabled));
assert!(AuthMode::Permissive.is_at_least_as_strict_as(AuthMode::Permissive));
assert!(AuthMode::Permissive.is_at_least_as_strict_as(AuthMode::Disabled));
assert!(AuthMode::Disabled.is_at_least_as_strict_as(AuthMode::Disabled));
assert!(!AuthMode::Permissive.is_at_least_as_strict_as(AuthMode::Strict));
assert!(!AuthMode::Disabled.is_at_least_as_strict_as(AuthMode::Permissive));
assert!(!AuthMode::Disabled.is_at_least_as_strict_as(AuthMode::Strict));
}
#[test]
fn derived_context_has_independent_stats() {
let primary = SecurityContext::for_testing(42);
let derived = primary.derive_context(b"child");
let id = SymbolId::new_for_test(1, 0, 0);
let symbol = Symbol::new(id, vec![1], SymbolKind::Source);
let _ = primary.sign_symbol(&symbol);
assert_eq!(primary.stats().signed.load(Ordering::Relaxed), 1);
assert_eq!(
derived.stats().signed.load(Ordering::Relaxed),
0,
"derived context must have independent stats"
);
}
}