1use idprova_core::{
28 dat::{constraints::EvaluationContext, Dat},
29 receipt::Receipt,
30 Result,
31};
32
33pub fn verify_dat(
52 compact_jws: &str,
53 pub_key: &[u8; 32],
54 required_scope: &str,
55 ctx: &EvaluationContext,
56) -> Result<Dat> {
57 let dat = Dat::from_compact(compact_jws)?;
58 dat.verify(pub_key, required_scope, ctx)?;
59 Ok(dat)
60}
61
62pub fn verify_dat_from_jws(compact_jws: &str, pub_key: &[u8; 32]) -> Result<Dat> {
77 let dat = Dat::from_compact(compact_jws)?;
78 dat.verify_signature(pub_key)?;
79 dat.validate_timing()?;
80 Ok(dat)
81}
82
83pub fn verify_receipt_log(receipts: &[Receipt]) -> Result<()> {
98 use idprova_core::receipt::ReceiptLog;
99 let log = ReceiptLog::from_entries(receipts.to_vec());
100 log.verify_integrity()
101}
102
103#[cfg(test)]
106mod tests {
107 use super::*;
108 use chrono::{Duration, Utc};
109 use idprova_core::receipt::entry::ChainLink;
110 use idprova_core::{
111 crypto::KeyPair,
112 dat::{constraints::DatConstraints, Dat},
113 receipt::{ActionDetails, Receipt, ReceiptLog},
114 };
115
116 fn make_dat(kp: &KeyPair, scope: &str, valid: bool) -> Dat {
119 let expires = if valid {
120 Utc::now() + Duration::hours(24)
121 } else {
122 Utc::now() - Duration::hours(1)
123 };
124 Dat::issue(
125 "did:idprova:test:issuer",
126 "did:idprova:test:agent",
127 vec![scope.to_string()],
128 expires,
129 None,
130 None,
131 kp,
132 )
133 .unwrap()
134 }
135
136 fn make_receipt(log: &ReceiptLog) -> Receipt {
137 Receipt {
138 id: ulid::Ulid::new().to_string(),
139 timestamp: Utc::now(),
140 agent: "did:idprova:test:agent".to_string(),
141 dat: "dat_test".to_string(),
142 action: ActionDetails {
143 action_type: "mcp:tool-call".to_string(),
144 server: None,
145 tool: Some("test_tool".to_string()),
146 input_hash: "blake3:abc123".to_string(),
147 output_hash: Some("blake3:def456".to_string()),
148 status: "success".to_string(),
149 duration_ms: None,
150 },
151 context: None,
152 chain: ChainLink {
153 previous_hash: log.last_hash(),
154 sequence_number: log.next_sequence(),
155 },
156 signature: "placeholder".to_string(),
157 }
158 }
159
160 #[test]
163 fn test_verify_dat_happy_path() {
164 let kp = KeyPair::generate();
165 let dat = make_dat(&kp, "mcp:tool:filesystem:read", true);
166 let compact = dat.to_compact().unwrap();
167 let ctx = EvaluationContext::default();
168
169 let result = verify_dat(
170 &compact,
171 &kp.public_key_bytes(),
172 "mcp:tool:filesystem:read",
173 &ctx,
174 );
175 assert!(result.is_ok(), "expected Ok, got: {:?}", result.err());
176 let verified = result.unwrap();
177 assert_eq!(verified.claims.iss, "did:idprova:test:issuer");
178 assert_eq!(verified.claims.sub, "did:idprova:test:agent");
179 }
180
181 #[test]
182 fn test_verify_dat_wrong_key_fails() {
183 let kp = KeyPair::generate();
184 let kp2 = KeyPair::generate();
185 let dat = make_dat(&kp, "mcp:tool:filesystem:read", true);
186 let compact = dat.to_compact().unwrap();
187 let ctx = EvaluationContext::default();
188
189 let result = verify_dat(
190 &compact,
191 &kp2.public_key_bytes(),
192 "mcp:tool:filesystem:read",
193 &ctx,
194 );
195 assert!(result.is_err(), "wrong key must fail");
196 }
197
198 #[test]
199 fn test_verify_dat_expired_fails() {
200 let kp = KeyPair::generate();
201 let dat = make_dat(&kp, "mcp:tool:filesystem:read", false); let compact = dat.to_compact().unwrap();
203 let ctx = EvaluationContext::default();
204
205 let result = verify_dat(
206 &compact,
207 &kp.public_key_bytes(),
208 "mcp:tool:filesystem:read",
209 &ctx,
210 );
211 assert!(result.is_err());
212 assert!(result.unwrap_err().to_string().contains("expired"));
213 }
214
215 #[test]
216 fn test_verify_dat_scope_denied_fails() {
217 let kp = KeyPair::generate();
218 let dat = make_dat(&kp, "mcp:tool:filesystem:read", true);
219 let compact = dat.to_compact().unwrap();
220 let ctx = EvaluationContext::default();
221
222 let result = verify_dat(
223 &compact,
224 &kp.public_key_bytes(),
225 "mcp:tool:filesystem:write",
226 &ctx,
227 );
228 assert!(result.is_err());
229 assert!(result.unwrap_err().to_string().contains("scope"));
230 }
231
232 #[test]
233 fn test_verify_dat_wildcard_scope_passes() {
234 let kp = KeyPair::generate();
235 let dat = make_dat(&kp, "mcp:*:*:*", true);
236 let compact = dat.to_compact().unwrap();
237 let ctx = EvaluationContext::default();
238
239 assert!(verify_dat(
240 &compact,
241 &kp.public_key_bytes(),
242 "mcp:tool:filesystem:write",
243 &ctx
244 )
245 .is_ok());
246 assert!(verify_dat(
247 &compact,
248 &kp.public_key_bytes(),
249 "mcp:resource:data:read",
250 &ctx
251 )
252 .is_ok());
253 }
254
255 #[test]
256 fn test_verify_dat_empty_scope_skips_check() {
257 let kp = KeyPair::generate();
258 let dat = make_dat(&kp, "mcp:tool:filesystem:read", true);
259 let compact = dat.to_compact().unwrap();
260 let ctx = EvaluationContext::default();
261
262 assert!(verify_dat(&compact, &kp.public_key_bytes(), "", &ctx).is_ok());
264 }
265
266 #[test]
267 fn test_verify_dat_constraint_rate_limit_blocks() {
268 let kp = KeyPair::generate();
269 let dat = Dat::issue(
270 "did:idprova:test:issuer",
271 "did:idprova:test:agent",
272 vec!["mcp:tool:filesystem:read".to_string()],
273 Utc::now() + Duration::hours(24),
274 Some(DatConstraints {
275 rate_limit: Some(idprova_core::dat::constraints::RateLimit {
276 max_actions: 5,
277 window_secs: 60,
278 }),
279 ..Default::default()
280 }),
281 None,
282 &kp,
283 )
284 .unwrap();
285 let compact = dat.to_compact().unwrap();
286 let mut ctx = EvaluationContext::default();
287 ctx.actions_in_window = 10; let result = verify_dat(
290 &compact,
291 &kp.public_key_bytes(),
292 "mcp:tool:filesystem:read",
293 &ctx,
294 );
295 assert!(result.is_err());
296 assert!(result.unwrap_err().to_string().contains("rate limit"));
297 }
298
299 #[test]
300 fn test_verify_dat_malformed_token_fails() {
301 let kp = KeyPair::generate();
302 let ctx = EvaluationContext::default();
303
304 assert!(verify_dat("not.a.token", &kp.public_key_bytes(), "", &ctx).is_err());
305 assert!(verify_dat("", &kp.public_key_bytes(), "", &ctx).is_err());
306 assert!(verify_dat("only.two", &kp.public_key_bytes(), "", &ctx).is_err());
307 }
308
309 #[test]
312 fn test_verify_dat_from_jws_happy_path() {
313 let kp = KeyPair::generate();
314 let dat = make_dat(&kp, "mcp:tool:filesystem:read", true);
315 let compact = dat.to_compact().unwrap();
316
317 let result = verify_dat_from_jws(&compact, &kp.public_key_bytes());
318 assert!(result.is_ok());
319 let verified = result.unwrap();
320 assert_eq!(verified.claims.iss, "did:idprova:test:issuer");
321 }
322
323 #[test]
324 fn test_verify_dat_from_jws_skips_scope_check() {
325 let kp = KeyPair::generate();
328 let dat = make_dat(&kp, "mcp:tool:filesystem:read", true);
329 let compact = dat.to_compact().unwrap();
330
331 assert!(verify_dat_from_jws(&compact, &kp.public_key_bytes()).is_ok());
333 }
334
335 #[test]
336 fn test_verify_dat_from_jws_wrong_key_fails() {
337 let kp = KeyPair::generate();
338 let kp2 = KeyPair::generate();
339 let dat = make_dat(&kp, "mcp:tool:filesystem:read", true);
340 let compact = dat.to_compact().unwrap();
341
342 assert!(verify_dat_from_jws(&compact, &kp2.public_key_bytes()).is_err());
343 }
344
345 #[test]
346 fn test_verify_dat_from_jws_expired_fails() {
347 let kp = KeyPair::generate();
348 let dat = make_dat(&kp, "mcp:tool:filesystem:read", false); let compact = dat.to_compact().unwrap();
350
351 assert!(verify_dat_from_jws(&compact, &kp.public_key_bytes()).is_err());
352 }
353
354 #[test]
357 fn test_verify_receipt_log_empty_passes() {
358 assert!(verify_receipt_log(&[]).is_ok());
359 }
360
361 #[test]
362 fn test_verify_receipt_log_single_receipt_passes() {
363 let mut log = ReceiptLog::new();
364 let r = make_receipt(&log);
365 log.append(r.clone());
366
367 assert!(verify_receipt_log(log.entries()).is_ok());
368 }
369
370 #[test]
371 fn test_verify_receipt_log_chain_passes() {
372 let mut log = ReceiptLog::new();
373 for _ in 0..5 {
374 let r = make_receipt(&log);
375 log.append(r);
376 }
377 assert_eq!(log.len(), 5);
378 assert!(verify_receipt_log(log.entries()).is_ok());
379 }
380
381 #[test]
382 fn test_verify_receipt_log_broken_chain_fails() {
383 let mut log = ReceiptLog::new();
384 let r0 = make_receipt(&log);
385 log.append(r0);
386 let r1 = make_receipt(&log);
387 log.append(r1);
388
389 let tampered = Receipt {
391 id: ulid::Ulid::new().to_string(),
392 timestamp: Utc::now(),
393 agent: "did:idprova:test:agent".to_string(),
394 dat: "dat_test".to_string(),
395 action: ActionDetails {
396 action_type: "mcp:tool-call".to_string(),
397 server: None,
398 tool: None,
399 input_hash: "blake3:bad".to_string(),
400 output_hash: None,
401 status: "success".to_string(),
402 duration_ms: None,
403 },
404 context: None,
405 chain: ChainLink {
406 previous_hash: "wrong_hash_here".to_string(), sequence_number: 2,
408 },
409 signature: "placeholder".to_string(),
410 };
411
412 let mut entries = log.entries().to_vec();
413 entries.push(tampered);
414
415 let result = verify_receipt_log(&entries);
416 assert!(result.is_err(), "broken chain must be detected");
417 }
418}