idprova_core/dat/
chain.rs1use super::scope::ScopeSet;
7use super::token::Dat;
8use crate::{IdprovaError, Result};
9
10#[derive(Debug, Clone)]
18pub struct ChainValidationConfig {
19 pub max_depth: u32,
25}
26
27impl Default for ChainValidationConfig {
28 fn default() -> Self {
29 Self { max_depth: 5 }
30 }
31}
32
33impl ChainValidationConfig {
34 pub const HARD_MAX_DEPTH: u32 = 10;
36
37 pub fn with_max_depth(max_depth: u32) -> Self {
39 Self {
40 max_depth: max_depth.min(Self::HARD_MAX_DEPTH),
41 }
42 }
43}
44
45pub fn validate_chain(chain: &[Dat]) -> Result<()> {
49 validate_chain_with_config(chain, &ChainValidationConfig::default())
50}
51
52pub fn validate_chain_with_config(chain: &[Dat], config: &ChainValidationConfig) -> Result<()> {
61 if chain.is_empty() {
62 return Ok(());
63 }
64
65 let depth = chain.len() as u32;
67 let effective_max = config.max_depth.min(ChainValidationConfig::HARD_MAX_DEPTH);
68 if depth > effective_max {
69 return Err(IdprovaError::InvalidDelegationChain(format!(
70 "delegation chain depth {} exceeds maximum allowed depth {}",
71 depth, effective_max
72 )));
73 }
74
75 for i in 1..chain.len() {
76 let parent = &chain[i - 1];
77 let child = &chain[i];
78
79 if child.claims.iss != parent.claims.sub {
81 return Err(IdprovaError::InvalidDelegationChain(format!(
82 "DAT {} was issued by {} but expected {}",
83 child.claims.jti, child.claims.iss, parent.claims.sub
84 )));
85 }
86
87 let parent_scopes = ScopeSet::parse(&parent.claims.scope)?;
89 let child_scopes = ScopeSet::parse(&child.claims.scope)?;
90 if !child_scopes.is_subset_of(&parent_scopes) {
91 return Err(IdprovaError::InvalidDelegationChain(format!(
92 "DAT {} has scopes that exceed parent DAT {}",
93 child.claims.jti, parent.claims.jti
94 )));
95 }
96
97 if child.claims.exp > parent.claims.exp {
99 return Err(IdprovaError::InvalidDelegationChain(format!(
100 "DAT {} expires after parent DAT {}",
101 child.claims.jti, parent.claims.jti
102 )));
103 }
104
105 child.validate_timing()?;
107 }
108
109 Ok(())
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use crate::crypto::KeyPair;
116 use crate::dat::token::Dat;
117 use chrono::{Duration, Utc};
118
119 fn make_dat(issuer: &str, subject: &str, scopes: Vec<&str>, kp: &KeyPair) -> Dat {
120 Dat::issue(
121 issuer,
122 subject,
123 scopes.into_iter().map(String::from).collect(),
124 Utc::now() + Duration::hours(24),
125 None,
126 None,
127 kp,
128 )
129 .unwrap()
130 }
131
132 fn build_chain(depth: usize) -> Vec<Dat> {
133 let kp = KeyPair::generate();
134 let mut chain = Vec::new();
135 let scopes = vec!["mcp:*:*:*"];
136
137 chain.push(make_dat(
139 "did:idprova:example.com:human",
140 &format!("did:idprova:example.com:agent0"),
141 scopes.clone(),
142 &kp,
143 ));
144
145 for i in 0..depth.saturating_sub(1) {
147 let issuer = format!("did:idprova:example.com:agent{i}");
148 let subject = format!("did:idprova:example.com:agent{}", i + 1);
149 chain.push(make_dat(&issuer, &subject, scopes.clone(), &kp));
150 }
151
152 chain
153 }
154
155 #[test]
156 fn test_chain_depth_5_passes_default_config() {
157 let chain = build_chain(5);
158 assert!(
159 validate_chain(&chain).is_ok(),
160 "chain of depth 5 must pass with default config (max_depth=5)"
161 );
162 }
163
164 #[test]
166 fn test_sr8_chain_depth_6_fails_default_config() {
167 let chain = build_chain(6);
168 assert!(
169 validate_chain(&chain).is_err(),
170 "chain of depth 6 must fail with default config (max_depth=5)"
171 );
172 }
173
174 #[test]
175 fn test_sr8_custom_depth_config() {
176 let chain = build_chain(8);
177 let config = ChainValidationConfig::with_max_depth(8);
178 assert!(
179 validate_chain_with_config(&chain, &config).is_ok(),
180 "chain of depth 8 must pass with max_depth=8"
181 );
182
183 let chain9 = build_chain(9);
184 assert!(
185 validate_chain_with_config(&chain9, &config).is_err(),
186 "chain of depth 9 must fail with max_depth=8"
187 );
188 }
189
190 #[test]
192 fn test_sr8_hard_max_depth_10_cannot_be_exceeded() {
193 let config = ChainValidationConfig::with_max_depth(20);
195 assert_eq!(
196 config.max_depth,
197 ChainValidationConfig::HARD_MAX_DEPTH,
198 "max_depth=20 must be clamped to HARD_MAX_DEPTH=10"
199 );
200
201 let chain11 = build_chain(11);
202 assert!(
203 validate_chain_with_config(&chain11, &config).is_err(),
204 "chain of depth 11 must fail even with max_depth config of 20 (clamped to 10)"
205 );
206 }
207
208 #[test]
209 fn test_chain_depth_10_passes_hard_max() {
210 let chain = build_chain(10);
211 let config = ChainValidationConfig::with_max_depth(10);
212 assert!(
213 validate_chain_with_config(&chain, &config).is_ok(),
214 "chain of depth 10 must pass with max_depth=10 (HARD_MAX)"
215 );
216 }
217}