use crate::cx::macaroon::{CaveatPredicate, MacaroonToken, VerificationContext, VerificationError};
use crate::security::{AuthKey, AuthenticatedSymbol, AuthenticationTag, SecurityContext};
use crate::types::{Symbol, SymbolId, SymbolKind};
fn create_test_symbol(id_seed: u64, data_pattern: u8, size: usize) -> Symbol {
let id = SymbolId::new_for_test(id_seed, 0, 0);
let data = vec![data_pattern; size];
Symbol::new(id, data, SymbolKind::Source)
}
fn test_auth_key(seed: u64) -> AuthKey {
AuthKey::from_seed(seed)
}
#[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::*;
#[test]
fn hmac_verify_rejects_invalid_tags_at_all_byte_positions() {
let key = test_auth_key(42);
let symbol = create_test_symbol(1, 0xAA, 1024);
let valid_tag = AuthenticationTag::compute(&key, &symbol);
let valid_bytes = *valid_tag.as_bytes();
assert!(valid_tag.verify(&key, &symbol));
for byte_idx in 0..valid_bytes.len() {
let mut invalid_bytes = valid_bytes;
invalid_bytes[byte_idx] ^= 0xFF;
let invalid_tag = AuthenticationTag::from_bytes(invalid_bytes);
assert!(
!invalid_tag.verify(&key, &symbol),
"invalid tag differing at byte {byte_idx} must not verify"
);
}
}
#[test]
fn hmac_verify_binds_payload_length_and_contents() {
let key = test_auth_key(99);
let symbol_small = create_test_symbol(1, 0x42, 16);
let symbol_large = create_test_symbol(1, 0x42, 16_384);
let tag_small = AuthenticationTag::compute(&key, &symbol_small);
let tag_large = AuthenticationTag::compute(&key, &symbol_large);
assert!(tag_small.verify(&key, &symbol_small));
assert!(tag_large.verify(&key, &symbol_large));
assert!(
!tag_small.verify(&key, &symbol_large),
"short-payload tag must not replay against same-id longer payload"
);
assert!(
!tag_large.verify(&key, &symbol_small),
"long-payload tag must not replay against same-id shorter payload"
);
}
#[test]
fn authentication_tag_equality_checks_every_byte() {
let key = test_auth_key(42);
let symbol = create_test_symbol(1, 0xBB, 512);
let tag1 = AuthenticationTag::compute(&key, &symbol);
let tag2 = tag1;
let tag_bytes = *tag1.as_bytes();
assert_eq!(tag1, tag2);
for byte_idx in 0..tag_bytes.len() {
let mut diff_bytes = tag_bytes;
diff_bytes[byte_idx] ^= 0x01;
let diff_tag = AuthenticationTag::from_bytes(diff_bytes);
assert_ne!(
tag1, diff_tag,
"tag equality must account for byte {byte_idx}"
);
}
}
#[test]
fn macaroon_caveat_layering_cannot_expand_privileges() {
let key = test_auth_key(100);
let base_token = MacaroonToken::mint(&key, "admin:full", "auth-service");
let time_restricted = base_token
.clone()
.add_caveat(CaveatPredicate::TimeBefore(5000));
let doubly_restricted = time_restricted
.clone()
.add_caveat(CaveatPredicate::RegionScope(42));
let ctx_early_wrong_region = VerificationContext::new().with_time(1000).with_region(999);
let ctx_early_right_region = VerificationContext::new().with_time(1000).with_region(42);
let ctx_late_right_region = VerificationContext::new().with_time(6000).with_region(42);
assert!(base_token.verify(&key, &ctx_early_wrong_region).is_ok());
assert!(base_token.verify(&key, &ctx_early_right_region).is_ok());
assert!(base_token.verify(&key, &ctx_late_right_region).is_ok());
assert!(
time_restricted
.verify(&key, &ctx_early_wrong_region)
.is_ok()
);
assert!(
time_restricted
.verify(&key, &ctx_early_right_region)
.is_ok()
);
assert!(
time_restricted
.verify(&key, &ctx_late_right_region)
.is_err()
);
assert!(
doubly_restricted
.verify(&key, &ctx_early_wrong_region)
.is_err()
);
assert!(
doubly_restricted
.verify(&key, &ctx_early_right_region)
.is_ok()
);
assert!(
doubly_restricted
.verify(&key, &ctx_late_right_region)
.is_err()
);
}
#[test]
fn macaroon_caveat_ordering_security() {
let key = test_auth_key(200);
let token_a = MacaroonToken::mint(&key, "resource:read", "service")
.add_caveat(CaveatPredicate::TimeBefore(1000))
.add_caveat(CaveatPredicate::MaxUses(5));
let token_b = MacaroonToken::mint(&key, "resource:read", "service")
.add_caveat(CaveatPredicate::MaxUses(5))
.add_caveat(CaveatPredicate::TimeBefore(1000));
assert_ne!(
token_a.signature().as_bytes(),
token_b.signature().as_bytes(),
"Caveat reordering should change HMAC signature"
);
let ctx = VerificationContext::new().with_time(500).with_use_count(2);
assert!(token_a.verify(&key, &ctx).is_ok());
assert!(token_b.verify(&key, &ctx).is_ok());
}
#[test]
fn macaroon_third_party_caveat_security_boundary() {
let root_key = test_auth_key(300);
let service_a_key = test_auth_key(301);
let service_b_key = test_auth_key(302);
let root_token = MacaroonToken::mint(&root_key, "data:access", "root")
.add_caveat(CaveatPredicate::TimeBefore(10000))
.add_third_party_caveat("service-a", "auth-check", &service_a_key);
let discharge_a = MacaroonToken::mint(&service_a_key, "auth-check", "service-a")
.add_caveat(CaveatPredicate::ResourceScope("data/public/*".to_string()));
let malicious_discharge = MacaroonToken::mint(&service_b_key, "auth-check", "service-a")
.add_caveat(CaveatPredicate::ResourceScope("data/**".to_string()));
let bound_legit = root_token.bind_for_request(&discharge_a).unwrap();
let bound_malicious = root_token.bind_for_request(&malicious_discharge).unwrap();
let ctx = VerificationContext::new()
.with_time(5000)
.with_resource("data/public/file.txt");
assert!(
root_token
.verify_with_discharges(&root_key, &ctx, &[bound_legit])
.is_ok()
);
assert!(
root_token
.verify_with_discharges(&root_key, &ctx, &[bound_malicious])
.is_err()
);
}
#[test]
fn macaroon_caveat_bypass_attempt_detection() {
let key = test_auth_key(400);
let restricted_token = MacaroonToken::mint(&key, "admin:write", "service")
.add_caveat(CaveatPredicate::TimeBefore(2000))
.add_caveat(CaveatPredicate::RegionScope(1))
.add_caveat(CaveatPredicate::MaxUses(3))
.add_caveat(CaveatPredicate::ResourceScope("admin/users/*".to_string()));
let bypass_attempts = vec![
VerificationContext::new()
.with_region(1)
.with_use_count(1)
.with_resource("admin/users/list"),
VerificationContext::new()
.with_time(1000)
.with_region(999)
.with_use_count(1)
.with_resource("admin/users/list"),
VerificationContext::new()
.with_time(3000)
.with_region(1)
.with_use_count(1)
.with_resource("admin/users/list"),
VerificationContext::new()
.with_time(1000)
.with_region(1)
.with_use_count(5)
.with_resource("admin/users/list"),
VerificationContext::new()
.with_time(1000)
.with_region(1)
.with_use_count(1)
.with_resource("admin/system/config"),
];
for (i, ctx) in bypass_attempts.into_iter().enumerate() {
let result = restricted_token.verify(&key, &ctx);
assert!(
result.is_err(),
"Bypass attempt {i} should have failed: {result:?}"
);
}
let valid_ctx = VerificationContext::new()
.with_time(1000)
.with_region(1)
.with_use_count(1)
.with_resource("admin/users/profile");
assert!(restricted_token.verify(&key, &valid_ctx).is_ok());
}
#[test]
fn symbol_authentication_rejects_tampered_signatures() {
let key = test_auth_key(500);
let symbol = create_test_symbol(1, 0xDD, 256);
let valid_tag = AuthenticationTag::compute(&key, &symbol);
let tampering_patterns: Vec<(&str, Box<dyn Fn(&mut [u8; 32])>)> = vec![
(
"flip_first_bit",
Box::new(|bytes: &mut [u8; 32]| bytes[0] ^= 0x01),
),
(
"flip_last_bit",
Box::new(|bytes: &mut [u8; 32]| bytes[31] ^= 0x01),
),
(
"flip_middle_byte",
Box::new(|bytes: &mut [u8; 32]| bytes[16] ^= 0xFF),
),
(
"zero_first_half",
Box::new(|bytes: &mut [u8; 32]| {
bytes[..16].fill(0);
}),
),
(
"zero_last_half",
Box::new(|bytes: &mut [u8; 32]| bytes[16..].fill(0)),
),
(
"all_ones",
Box::new(|bytes: &mut [u8; 32]| bytes.fill(0xFF)),
),
(
"increment_all",
Box::new(|bytes: &mut [u8; 32]| {
for byte in bytes.iter_mut() {
*byte = byte.wrapping_add(1);
}
}),
),
];
for (name, tamper_fn) in tampering_patterns {
let mut tampered_bytes = *valid_tag.as_bytes();
tamper_fn(&mut tampered_bytes);
let tampered_tag = AuthenticationTag::from_bytes(tampered_bytes);
assert!(
!tampered_tag.verify(&key, &symbol),
"Tampering pattern '{name}' should be detected"
);
let ctx = SecurityContext::new(key.clone());
let mut tampered_auth_symbol =
AuthenticatedSymbol::from_parts(symbol.clone(), tampered_tag);
assert!(
ctx.verify_authenticated_symbol(&mut tampered_auth_symbol)
.is_err(),
"SecurityContext should reject tampering pattern '{name}'"
);
}
}
#[test]
fn macaroon_signature_tampering_detection() {
let key = test_auth_key(600);
let token = MacaroonToken::mint(&key, "test:capability", "service")
.add_caveat(CaveatPredicate::TimeBefore(5000));
let original_bytes = token.to_binary();
let sig_start = original_bytes.len() - 32;
let tampering_scenarios = vec![
("corrupt_sig_start", 0),
("corrupt_sig_middle", 16),
("corrupt_sig_end", 31),
];
for (name, offset) in tampering_scenarios {
let mut tampered_bytes = original_bytes.clone();
tampered_bytes[sig_start + offset] ^= 0xFF;
let tampered_token = MacaroonToken::from_binary(&tampered_bytes)
.expect("should parse despite signature corruption");
assert!(
!tampered_token.verify_signature(&key),
"Signature tampering '{name}' should be detected"
);
let ctx = VerificationContext::new().with_time(1000);
let result = tampered_token.verify(&key, &ctx);
assert!(
matches!(result, Err(VerificationError::InvalidSignature)),
"Full verification should detect signature tampering '{name}': {result:?}"
);
}
}
#[test]
fn macaroon_caveat_tampering_detection() {
let key = test_auth_key(700);
let token = MacaroonToken::mint(&key, "test:write", "service")
.add_caveat(CaveatPredicate::TimeBefore(5000))
.add_caveat(CaveatPredicate::MaxUses(10));
let mut bytes = token.to_binary();
let max_uses_bytes = 10u32.to_le_bytes();
if let Some(pos) = bytes.windows(4).position(|window| window == max_uses_bytes) {
let escalated_bytes = 1000u32.to_le_bytes();
bytes[pos..pos + 4].copy_from_slice(&escalated_bytes);
let tampered_token =
MacaroonToken::from_binary(&bytes).expect("should parse despite caveat tampering");
assert!(
!tampered_token.verify_signature(&key),
"Caveat tampering should invalidate HMAC signature"
);
let ctx = VerificationContext::new()
.with_time(1000)
.with_use_count(500);
assert!(
tampered_token.verify(&key, &ctx).is_err(),
"Tampered caveat should be rejected"
);
}
}
#[test]
fn wrong_key_rejection_comprehensive() {
let correct_key = test_auth_key(800);
let wrong_keys = (801..810).map(test_auth_key).collect::<Vec<_>>();
let symbol = create_test_symbol(1, 0xCC, 128);
let tag = AuthenticationTag::compute(&correct_key, &symbol);
for (i, wrong_key) in wrong_keys.iter().enumerate() {
assert!(
!tag.verify(wrong_key, &symbol),
"Symbol verification should reject wrong key {i}"
);
}
let token = MacaroonToken::mint(&correct_key, "secure:operation", "service")
.add_caveat(CaveatPredicate::TimeBefore(5000));
let ctx = VerificationContext::new().with_time(1000);
for (i, wrong_key) in wrong_keys.iter().enumerate() {
assert!(
!token.verify_signature(wrong_key),
"Macaroon signature should reject wrong key {i}"
);
assert!(
token.verify(wrong_key, &ctx).is_err(),
"Macaroon verification should reject wrong key {i}"
);
}
}
#[test]
fn zero_key_security_boundary() {
let weak_err = AuthKey::from_bytes([0u8; 32]);
assert!(
weak_err.is_err(),
"all-zero key MUST be rejected at construction (q3terg)"
);
let key1 = test_auth_key(900);
let key2 = test_auth_key(901);
let symbol = create_test_symbol(1, 0x88, 64);
let tag1 = AuthenticationTag::compute(&key1, &symbol);
let tag2 = AuthenticationTag::compute(&key2, &symbol);
assert_ne!(
tag1.as_bytes(),
tag2.as_bytes(),
"Different keys should produce different authentication tags"
);
assert!(!tag1.verify(&key2, &symbol));
assert!(!tag2.verify(&key1, &symbol));
assert!(tag1.verify(&key1, &symbol));
assert!(tag2.verify(&key2, &symbol));
}
#[test]
fn replay_attack_prevention() {
let key = test_auth_key(1000);
let symbol1 = create_test_symbol(1, 0x11, 64);
let symbol2 = create_test_symbol(2, 0x22, 64);
let symbol3 = create_test_symbol(1, 0x11, 128);
let tag1 = AuthenticationTag::compute(&key, &symbol1);
assert!(
!tag1.verify(&key, &symbol2),
"Tag replay should fail for different symbol"
);
assert!(
!tag1.verify(&key, &symbol3),
"Tag replay should fail for same ID but different data"
);
assert!(
tag1.verify(&key, &symbol1),
"Original verification should still work"
);
}
use crate::cx::Cx;
use crate::types::{Budget, RegionId, TaskId};
use crate::util::ArenaIndex;
fn boundary_cx() -> Cx {
Cx::new(
RegionId::from_arena(ArenaIndex::new(0, 0)),
TaskId::from_arena(ArenaIndex::new(0, 0)),
Budget::INFINITE,
)
}
#[test]
fn cx_without_macaroon_fails_capability_check_default_deny() {
let key = test_auth_key(2000);
let cx = boundary_cx();
let ctx = VerificationContext::new().with_time(100);
let result = cx.verify_capability(&key, "io:net", &ctx);
assert!(
matches!(result, Err(VerificationError::InvalidSignature)),
"no-macaroon Cx must fail closed; got {result:?}"
);
}
#[test]
fn cx_with_macaroon_for_wrong_identifier_is_rejected() {
let key = test_auth_key(2100);
let token = MacaroonToken::mint(&key, "spawn:r1", "boundary-test");
let cx = boundary_cx().with_macaroon(token);
let ctx = VerificationContext::new().with_time(100);
assert!(
cx.verify_capability(&key, "spawn:r1", &ctx).is_ok(),
"intended capability must verify"
);
let cross = cx.verify_capability(&key, "io:net", &ctx);
assert!(
matches!(cross, Err(VerificationError::UnexpectedIdentifier { .. })),
"cross-capability use must be rejected with identifier mismatch; got {cross:?}"
);
}
#[test]
fn child_cx_attenuation_cannot_expand_parent_capabilities() {
let key = test_auth_key(2200);
let token = MacaroonToken::mint(&key, "io:write", "boundary-test");
let parent = boundary_cx().with_macaroon(token);
let child = parent
.attenuate(CaveatPredicate::TimeBefore(2000))
.expect("attenuate should produce child Cx");
assert_eq!(parent.macaroon().unwrap().caveat_count(), 0);
assert_eq!(child.macaroon().unwrap().caveat_count(), 1);
let ctx_late = VerificationContext::new().with_time(5000);
let result = child.verify_capability(&key, "io:write", &ctx_late);
assert!(
matches!(result, Err(VerificationError::CaveatFailed { .. })),
"child must reject context outside its tighter window; got {result:?}"
);
assert!(
parent
.verify_capability(&key, "io:write", &ctx_late)
.is_ok(),
"parent must remain authoritative for the broader window"
);
}
#[test]
fn child_cx_caveats_are_monotonically_additive() {
let key = test_auth_key(2300);
let token = MacaroonToken::mint(&key, "data:read", "boundary-test");
let l0 = boundary_cx().with_macaroon(token);
let l1 = l0.attenuate(CaveatPredicate::TimeBefore(5000)).expect("l1");
let l2 = l1.attenuate(CaveatPredicate::RegionScope(7)).expect("l2");
let l3 = l2.attenuate(CaveatPredicate::MaxUses(3)).expect("l3");
assert_eq!(l3.macaroon().unwrap().caveat_count(), 3);
let ctx_ok = VerificationContext::new()
.with_time(1000)
.with_region(7)
.with_use_count(1);
assert!(l3.verify_capability(&key, "data:read", &ctx_ok).is_ok());
let ctx_bad_time = VerificationContext::new()
.with_time(9999)
.with_region(7)
.with_use_count(1);
assert!(
l3.verify_capability(&key, "data:read", &ctx_bad_time)
.is_err(),
"deepest child must still enforce l1's TimeBefore caveat"
);
let ctx_bad_region = VerificationContext::new()
.with_time(1000)
.with_region(99)
.with_use_count(1);
assert!(
l3.verify_capability(&key, "data:read", &ctx_bad_region)
.is_err(),
"deepest child must still enforce l2's RegionScope caveat"
);
let ctx_bad_uses = VerificationContext::new()
.with_time(1000)
.with_region(7)
.with_use_count(99);
assert!(
l3.verify_capability(&key, "data:read", &ctx_bad_uses)
.is_err(),
"deepest child must still enforce its own MaxUses caveat"
);
}
#[test]
fn child_cx_inherits_macaroon_through_clone() {
let key = test_auth_key(2400);
let token = MacaroonToken::mint(&key, "rpc:invoke", "boundary-test");
let parent = boundary_cx().with_macaroon(token);
let child = parent
.attenuate(CaveatPredicate::MaxUses(1))
.expect("attenuate");
let child_clone = child.clone();
assert_eq!(child.macaroon().unwrap().caveat_count(), 1);
assert_eq!(child_clone.macaroon().unwrap().caveat_count(), 1);
let ctx_overuse = VerificationContext::new().with_time(0).with_use_count(99);
assert!(
child
.verify_capability(&key, "rpc:invoke", &ctx_overuse)
.is_err()
);
assert!(
child_clone
.verify_capability(&key, "rpc:invoke", &ctx_overuse)
.is_err()
);
}
#[test]
fn child_cx_attenuation_with_wrong_root_key_is_rejected() {
let real_key = test_auth_key(2500);
let attacker_key = test_auth_key(2501);
let token = MacaroonToken::mint(&real_key, "admin:rotate", "issuer");
let cx = boundary_cx().with_macaroon(token);
let ctx = VerificationContext::new().with_time(100);
let result = cx.verify_capability(&attacker_key, "admin:rotate", &ctx);
assert!(
matches!(result, Err(VerificationError::InvalidSignature)),
"attacker-controlled verify with the wrong key must fail with InvalidSignature; got {result:?}"
);
assert!(
cx.verify_capability(&real_key, "admin:rotate", &ctx)
.is_ok()
);
}
#[test]
fn macaroon_binary_truncation_is_rejected_gracefully() {
let key = test_auth_key(3000);
let token = MacaroonToken::mint(&key, "fs:read", "issuer")
.add_caveat(CaveatPredicate::TimeBefore(5000));
let bytes = token.to_binary();
assert!(bytes.len() > 32);
for cut in (1..bytes.len())
.step_by(7)
.chain(std::iter::once(bytes.len() - 1))
{
let truncated = &bytes[..cut];
match MacaroonToken::from_binary(truncated) {
None => { }
Some(parsed) => {
assert!(
!parsed.verify_signature(&key),
"truncated input parsed at cut={cut} must NOT verify"
);
}
}
}
}
}