use chrono::{DateTime, Utc};
use agent_uri::CapabilityPath;
use crate::error::AttestationError;
#[must_use]
pub fn capability_covers(attested_capabilities: &[String], required: &CapabilityPath) -> bool {
attested_capabilities.iter().any(|cap| {
let required_str = required.as_str();
required_str == cap || required_str.starts_with(&format!("{cap}/"))
})
}
pub fn validate_issuer(
uri_trust_root: &str,
token_issuer: &str,
) -> Result<(), AttestationError> {
if uri_trust_root == token_issuer {
Ok(())
} else {
Err(AttestationError::TrustRootMismatch {
token_root: token_issuer.to_string(),
expected_root: uri_trust_root.to_string(),
})
}
}
pub fn validate_subject(
presented_uri: &str,
token_subject: &str,
) -> Result<(), AttestationError> {
if presented_uri == token_subject {
Ok(())
} else {
Err(AttestationError::UriMismatch {
token_uri: token_subject.to_string(),
expected_uri: presented_uri.to_string(),
})
}
}
pub fn check_expiration(exp: DateTime<Utc>, now: DateTime<Utc>) -> Result<(), AttestationError> {
if now < exp {
Ok(())
} else {
Err(AttestationError::TokenExpired {
expired_at: exp.to_rfc3339(),
})
}
}
pub fn check_capability_coverage(
attested_capabilities: &[String],
required: &CapabilityPath,
) -> Result<(), AttestationError> {
if capability_covers(attested_capabilities, required) {
Ok(())
} else {
Err(AttestationError::InsufficientCapabilities {
required: required.to_string(),
attested: attested_capabilities.to_vec(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
mod capability_covers_tests {
use super::*;
#[test]
fn exact_match_is_covered() {
let attested = vec!["workflow/approval".to_string()];
let required = CapabilityPath::parse("workflow/approval").unwrap();
assert!(capability_covers(&attested, &required));
}
#[test]
fn prefix_covers_child_path() {
let attested = vec!["workflow".to_string()];
let required = CapabilityPath::parse("workflow/approval").unwrap();
assert!(capability_covers(&attested, &required));
}
#[test]
fn prefix_covers_grandchild_path() {
let attested = vec!["workflow".to_string()];
let required = CapabilityPath::parse("workflow/approval/invoice").unwrap();
assert!(capability_covers(&attested, &required));
}
#[test]
fn sibling_path_not_covered() {
let attested = vec!["workflow/approval".to_string()];
let required = CapabilityPath::parse("workflow/rejection").unwrap();
assert!(!capability_covers(&attested, &required));
}
#[test]
fn unrelated_path_not_covered() {
let attested = vec!["assistant/chat".to_string()];
let required = CapabilityPath::parse("workflow/approval").unwrap();
assert!(!capability_covers(&attested, &required));
}
#[test]
fn empty_capabilities_cover_nothing() {
let attested: Vec<String> = vec![];
let required = CapabilityPath::parse("workflow").unwrap();
assert!(!capability_covers(&attested, &required));
}
#[test]
fn reverse_prefix_not_covered() {
let attested = vec!["workflow/approval".to_string()];
let required = CapabilityPath::parse("workflow").unwrap();
assert!(!capability_covers(&attested, &required));
}
#[test]
fn partial_segment_match_not_covered() {
let attested = vec!["work".to_string()];
let required = CapabilityPath::parse("workflow").unwrap();
assert!(!capability_covers(&attested, &required));
}
#[test]
fn multiple_capabilities_any_covers() {
let attested = vec![
"assistant/chat".to_string(),
"workflow".to_string(),
"storage/read".to_string(),
];
let required = CapabilityPath::parse("workflow/approval").unwrap();
assert!(capability_covers(&attested, &required));
}
}
mod issuer_validation_tests {
use super::*;
#[test]
fn exact_match_succeeds() {
assert!(validate_issuer("acme.com", "acme.com").is_ok());
}
#[test]
fn mismatch_fails() {
let result = validate_issuer("acme.com", "evil.com");
assert!(matches!(
result,
Err(AttestationError::TrustRootMismatch { .. })
));
}
#[test]
fn case_difference_fails() {
let result = validate_issuer("acme.com", "ACME.COM");
assert!(matches!(
result,
Err(AttestationError::TrustRootMismatch { .. })
));
}
#[test]
fn with_port_exact_match() {
assert!(validate_issuer("localhost:8472", "localhost:8472").is_ok());
}
}
mod subject_validation_tests {
use super::*;
#[test]
fn exact_match_succeeds() {
let uri = "agent://acme.com/workflow/agent_01h455vb4pex5vsknk084sn02q";
assert!(validate_subject(uri, uri).is_ok());
}
#[test]
fn mismatch_fails() {
let presented = "agent://acme.com/workflow/agent_01h455vb4pex5vsknk084sn02q";
let token_sub = "agent://acme.com/other/agent_01h455vb4pex5vsknk084sn02q";
let result = validate_subject(presented, token_sub);
assert!(matches!(result, Err(AttestationError::UriMismatch { .. })));
}
}
mod expiration_tests {
use super::*;
use chrono::Duration;
#[test]
fn future_expiration_is_valid() {
let now = Utc::now();
let exp = now + Duration::hours(1);
assert!(check_expiration(exp, now).is_ok());
}
#[test]
fn exact_expiration_is_expired() {
let now = Utc::now();
let result = check_expiration(now, now);
assert!(matches!(result, Err(AttestationError::TokenExpired { .. })));
}
#[test]
fn past_expiration_is_expired() {
let now = Utc::now();
let exp = now - Duration::hours(1);
let result = check_expiration(exp, now);
assert!(matches!(result, Err(AttestationError::TokenExpired { .. })));
}
#[test]
fn one_second_before_expiration_is_valid() {
let exp = Utc::now() + Duration::seconds(1);
let now = exp - Duration::seconds(1);
assert!(check_expiration(exp, now).is_ok());
}
}
mod check_capability_coverage_tests {
use super::*;
#[test]
fn returns_ok_when_covered() {
let attested = vec!["workflow".to_string()];
let required = CapabilityPath::parse("workflow/approval").unwrap();
assert!(check_capability_coverage(&attested, &required).is_ok());
}
#[test]
fn returns_err_with_details_when_not_covered() {
let attested = vec!["assistant/chat".to_string()];
let required = CapabilityPath::parse("workflow/approval").unwrap();
let result = check_capability_coverage(&attested, &required);
match result {
Err(AttestationError::InsufficientCapabilities {
required: req,
attested: att,
}) => {
assert_eq!(req, "workflow/approval");
assert_eq!(att, vec!["assistant/chat".to_string()]);
}
_ => panic!("Expected InsufficientCapabilities error"),
}
}
}
}