use super::scope::ScopeSet;
use super::token::Dat;
use crate::{IdprovaError, Result};
#[derive(Debug, Clone)]
pub struct ChainValidationConfig {
pub max_depth: u32,
}
impl Default for ChainValidationConfig {
fn default() -> Self {
Self { max_depth: 5 }
}
}
impl ChainValidationConfig {
pub const HARD_MAX_DEPTH: u32 = 10;
pub fn with_max_depth(max_depth: u32) -> Self {
Self {
max_depth: max_depth.min(Self::HARD_MAX_DEPTH),
}
}
}
pub fn validate_chain(chain: &[Dat]) -> Result<()> {
validate_chain_with_config(chain, &ChainValidationConfig::default())
}
pub fn validate_chain_with_config(chain: &[Dat], config: &ChainValidationConfig) -> Result<()> {
if chain.is_empty() {
return Ok(());
}
let depth = chain.len() as u32;
let effective_max = config.max_depth.min(ChainValidationConfig::HARD_MAX_DEPTH);
if depth > effective_max {
return Err(IdprovaError::InvalidDelegationChain(format!(
"delegation chain depth {} exceeds maximum allowed depth {}",
depth, effective_max
)));
}
for i in 1..chain.len() {
let parent = &chain[i - 1];
let child = &chain[i];
if child.claims.iss != parent.claims.sub {
return Err(IdprovaError::InvalidDelegationChain(format!(
"DAT {} was issued by {} but expected {}",
child.claims.jti, child.claims.iss, parent.claims.sub
)));
}
let parent_scopes = ScopeSet::parse(&parent.claims.scope)?;
let child_scopes = ScopeSet::parse(&child.claims.scope)?;
if !child_scopes.is_subset_of(&parent_scopes) {
return Err(IdprovaError::InvalidDelegationChain(format!(
"DAT {} has scopes that exceed parent DAT {}",
child.claims.jti, parent.claims.jti
)));
}
if child.claims.exp > parent.claims.exp {
return Err(IdprovaError::InvalidDelegationChain(format!(
"DAT {} expires after parent DAT {}",
child.claims.jti, parent.claims.jti
)));
}
child.validate_timing()?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::KeyPair;
use crate::dat::token::Dat;
use chrono::{Duration, Utc};
fn make_dat(issuer: &str, subject: &str, scopes: Vec<&str>, kp: &KeyPair) -> Dat {
Dat::issue(
issuer,
subject,
scopes.into_iter().map(String::from).collect(),
Utc::now() + Duration::hours(24),
None,
None,
kp,
)
.unwrap()
}
fn build_chain(depth: usize) -> Vec<Dat> {
let kp = KeyPair::generate();
let mut chain = Vec::new();
let scopes = vec!["mcp:*:*:*"];
chain.push(make_dat(
"did:aid:example.com:human",
&format!("did:aid:example.com:agent0"),
scopes.clone(),
&kp,
));
for i in 0..depth.saturating_sub(1) {
let issuer = format!("did:aid:example.com:agent{i}");
let subject = format!("did:aid:example.com:agent{}", i + 1);
chain.push(make_dat(&issuer, &subject, scopes.clone(), &kp));
}
chain
}
#[test]
fn test_chain_depth_5_passes_default_config() {
let chain = build_chain(5);
assert!(
validate_chain(&chain).is_ok(),
"chain of depth 5 must pass with default config (max_depth=5)"
);
}
#[test]
fn test_sr8_chain_depth_6_fails_default_config() {
let chain = build_chain(6);
assert!(
validate_chain(&chain).is_err(),
"chain of depth 6 must fail with default config (max_depth=5)"
);
}
#[test]
fn test_sr8_custom_depth_config() {
let chain = build_chain(8);
let config = ChainValidationConfig::with_max_depth(8);
assert!(
validate_chain_with_config(&chain, &config).is_ok(),
"chain of depth 8 must pass with max_depth=8"
);
let chain9 = build_chain(9);
assert!(
validate_chain_with_config(&chain9, &config).is_err(),
"chain of depth 9 must fail with max_depth=8"
);
}
#[test]
fn test_sr8_hard_max_depth_10_cannot_be_exceeded() {
let config = ChainValidationConfig::with_max_depth(20);
assert_eq!(
config.max_depth,
ChainValidationConfig::HARD_MAX_DEPTH,
"max_depth=20 must be clamped to HARD_MAX_DEPTH=10"
);
let chain11 = build_chain(11);
assert!(
validate_chain_with_config(&chain11, &config).is_err(),
"chain of depth 11 must fail even with max_depth config of 20 (clamped to 10)"
);
}
#[test]
fn test_chain_depth_10_passes_hard_max() {
let chain = build_chain(10);
let config = ChainValidationConfig::with_max_depth(10);
assert!(
validate_chain_with_config(&chain, &config).is_ok(),
"chain of depth 10 must pass with max_depth=10 (HARD_MAX)"
);
}
}