1use std::collections::HashMap;
10
11use ed25519_dalek::VerifyingKey;
12
13use crate::attestation::{Envelope, Verifier};
14use crate::capability::is_key_bound;
15use crate::statements::ReceiptStatement;
16use crate::trust::{decode_ed25519_pubkey, TrustRootKind, TrustRootStore};
17
18pub fn verifier_from_trust(trust: &TrustRootStore) -> Verifier {
22 let mut map: HashMap<String, VerifyingKey> = HashMap::new();
23 for r in trust.roots() {
24 if let Ok(vk) = decode_ed25519_pubkey(&r.public_key) {
25 map.insert(r.key_id.clone(), vk);
26 }
27 }
28 Verifier::new(map)
29}
30
31pub struct ChainVerdict {
34 pub cert_id: String,
35 pub subject_key: VerifyingKey,
36}
37
38pub fn chain_verify_card(
54 card_env: &Envelope,
55 card_keyid: &str,
56 agent: &str,
57 certs: &[(String, Envelope)],
58 trust: &TrustRootStore,
59 now: &str,
60) -> Option<ChainVerdict> {
61 let card_signer = card_env.signatures.first().map(|s| s.keyid.as_str())?;
65 if card_keyid.is_empty() || card_keyid != card_signer {
66 return None;
67 }
68
69 for (cert_id, cert_env) in certs {
70 let cert_signer = match cert_env.signatures.first() {
73 Some(s) => s.keyid.as_str(),
74 None => continue,
75 };
76 let Some(ship_root) = trust
77 .roots()
78 .iter()
79 .find(|r| r.key_id == cert_signer && r.kind == TrustRootKind::CertIssuer)
80 else {
81 continue;
82 };
83 let Ok(ship_vk) = decode_ed25519_pubkey(&ship_root.public_key) else {
84 continue;
85 };
86 let mut cert_verifier = Verifier::new(HashMap::new());
87 cert_verifier.add_key(cert_signer.to_string(), ship_vk);
88 if cert_verifier.verify_any(cert_env).is_err() {
89 continue;
90 }
91
92 let Ok(stmt) = cert_env.unmarshal_statement::<ReceiptStatement>() else {
94 continue;
95 };
96 if stmt.kind != "agent_cert.v1" {
97 continue;
98 }
99 let Some(p) = stmt.payload else { continue };
100
101 if p.get("agent").and_then(|v| v.as_str()) != Some(agent)
103 || p.get("subject_key_id").and_then(|v| v.as_str()) != Some(card_signer)
104 {
105 continue;
106 }
107
108 let (Some(issued), Some(until)) = (
112 p.get("issued_at").and_then(|v| v.as_str()),
113 p.get("valid_until").and_then(|v| v.as_str()),
114 ) else {
115 continue;
116 };
117 if now < issued || now > until {
118 continue;
119 }
120
121 let Some(subject_b64) = p.get("subject_public_key").and_then(|v| v.as_str()) else {
123 continue;
124 };
125 let Ok(subject_vk) = decode_ed25519_pubkey(&format!("ed25519:{subject_b64}")) else {
126 continue;
127 };
128 let mut card_verifier = Verifier::new(HashMap::new());
129 card_verifier.add_key(card_signer.to_string(), subject_vk);
130 if card_verifier.verify_any(card_env).is_err() {
131 continue;
132 }
133
134 return Some(ChainVerdict {
135 cert_id: cert_id.clone(),
136 subject_key: subject_vk,
137 });
138 }
139 None
140}
141
142pub struct ResolutionBundle {
147 pub agent: String,
148 pub card: Envelope,
150 pub certs: Vec<(String, Envelope)>,
152 pub revocations: Vec<Envelope>,
154}
155
156#[derive(serde::Serialize)]
160pub struct ResolutionVerdict {
161 pub sig_ok: bool,
164 pub key_bound: bool,
167 pub chain_cert_id: Option<String>,
169 pub revoked: bool,
171 pub revocation_reason: Option<String>,
173}
174
175pub fn verify_resolution(
181 bundle: &ResolutionBundle,
182 trust: &TrustRootStore,
183 now: &str,
184) -> Result<ResolutionVerdict, String> {
185 let verifier = verifier_from_trust(trust);
186 let mut sig_ok = verifier.verify_any(&bundle.card).is_ok();
187
188 let stmt: ReceiptStatement = bundle
189 .card
190 .unmarshal_statement()
191 .map_err(|e| format!("unparseable card: {e}"))?;
192 if stmt.kind != "agent_card.v1" {
193 return Err(format!(
194 "bundle card is a `{}`, not an agent_card.v1",
195 stmt.kind
196 ));
197 }
198 let card = stmt.payload.unwrap_or(serde_json::Value::Null);
199 let card_keyid = card.get("keyid").and_then(|v| v.as_str()).unwrap_or("");
200 let signer = bundle
201 .card
202 .signatures
203 .first()
204 .map(|s| s.keyid.as_str())
205 .unwrap_or("");
206 let mut key_bound = sig_ok && is_key_bound(card_keyid, signer, trust);
207
208 let mut chain_cert_id: Option<String> = None;
212 let mut rev_verifier = verifier;
213 if !key_bound {
214 if let Some(verdict) = chain_verify_card(
215 &bundle.card,
216 card_keyid,
217 &bundle.agent,
218 &bundle.certs,
219 trust,
220 now,
221 ) {
222 sig_ok = true;
223 key_bound = true;
224 rev_verifier.add_key(signer.to_string(), verdict.subject_key);
225 chain_cert_id = Some(verdict.cert_id);
226 }
227 }
228 let verifier = rev_verifier;
229
230 let mut revocation_reason: Option<String> = None;
232 for rev_env in &bundle.revocations {
233 if verifier.verify_any(rev_env).is_err() {
234 continue; }
236 let Ok(rev_stmt) = rev_env.unmarshal_statement::<ReceiptStatement>() else {
237 continue;
238 };
239 if rev_stmt.kind != "agent_card_revocation.v1" {
240 continue;
241 }
242 let rev_signer = rev_env
243 .signatures
244 .first()
245 .map(|s| s.keyid.as_str())
246 .unwrap_or("");
247 let self_revoke = !card_keyid.is_empty() && rev_signer == card_keyid;
248 let issuer = trust
250 .roots()
251 .iter()
252 .any(|r| r.key_id == rev_signer && r.kind == TrustRootKind::Revoker);
253 if self_revoke || issuer {
254 revocation_reason = Some(
255 rev_stmt
256 .payload
257 .as_ref()
258 .and_then(|p| p.get("reason"))
259 .and_then(|v| v.as_str())
260 .unwrap_or("(no reason given)")
261 .to_string(),
262 );
263 break;
264 }
265 }
266
267 Ok(ResolutionVerdict {
268 sig_ok,
269 key_bound,
270 chain_cert_id,
271 revoked: revocation_reason.is_some(),
272 revocation_reason,
273 })
274}
275
276#[cfg(test)]
277mod chain_tests {
278 use super::*;
279 use crate::attestation::{sign, Ed25519Signer, Signer};
280 use crate::statements::payload_type;
281 use crate::trust::TrustRoot;
282 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
283
284 const NOW: &str = "2026-07-06T12:00:00Z";
285
286 fn cert_payload(agent: &str, subject: &Ed25519Signer) -> serde_json::Value {
287 serde_json::json!({
288 "agent": agent,
289 "subject_key_id": subject.key_id(),
290 "subject_public_key": URL_SAFE_NO_PAD.encode(subject.public_key_bytes()),
291 "issuer": "ship://ship_test",
292 "issued_at": "2026-01-01T00:00:00Z",
293 "valid_until": "2027-01-01T00:00:00Z",
294 })
295 }
296
297 fn signed_receipt(kind: &str, payload: serde_json::Value, signer: &Ed25519Signer) -> Envelope {
298 let mut stmt = ReceiptStatement::new("ship://ship_test", kind);
299 stmt.payload = Some(payload);
300 sign(&payload_type("receipt"), &stmt, signer)
301 .unwrap()
302 .envelope
303 }
304
305 fn signed_card(agent: &str, keyid_claim: &str, signer: &Ed25519Signer) -> Envelope {
306 let mut stmt = ReceiptStatement::new("ship://ship_test", "agent_card.v1");
307 stmt.payload = Some(serde_json::json!({ "agent": agent, "keyid": keyid_claim }));
308 sign(&payload_type("receipt"), &stmt, signer)
309 .unwrap()
310 .envelope
311 }
312
313 fn ship_pinned(ship: &Ed25519Signer, kind: TrustRootKind) -> TrustRootStore {
314 TrustRootStore::with_roots(vec![TrustRoot {
315 key_id: ship.key_id().to_string(),
316 public_key: format!(
317 "ed25519:{}",
318 URL_SAFE_NO_PAD.encode(ship.public_key_bytes())
319 ),
320 kind,
321 label: "test ship".into(),
322 added_at: String::new(),
323 }])
324 }
325
326 #[test]
327 fn chain_verifies_card_through_pinned_ship_root() {
328 let ship = Ed25519Signer::generate("key_ship").unwrap();
329 let agent_key = Ed25519Signer::generate("key_agent").unwrap();
330 let cert = signed_receipt(
331 "agent_cert.v1",
332 cert_payload("agent://a", &agent_key),
333 &ship,
334 );
335 let card = signed_card("agent://a", "key_agent", &agent_key);
336 let trust = ship_pinned(&ship, TrustRootKind::CertIssuer);
337
338 let verdict = chain_verify_card(
339 &card,
340 "key_agent",
341 "agent://a",
342 &[("art_cert".into(), cert)],
343 &trust,
344 NOW,
345 );
346 assert!(verdict.is_some(), "valid chain must verify");
347 assert_eq!(verdict.unwrap().cert_id, "art_cert");
348 }
349
350 #[test]
351 fn chain_rejects_unpinned_ship() {
352 let ship = Ed25519Signer::generate("key_ship").unwrap();
353 let agent_key = Ed25519Signer::generate("key_agent").unwrap();
354 let cert = signed_receipt(
355 "agent_cert.v1",
356 cert_payload("agent://a", &agent_key),
357 &ship,
358 );
359 let card = signed_card("agent://a", "key_agent", &agent_key);
360
361 let empty = TrustRootStore::with_roots(vec![]);
363 assert!(chain_verify_card(
364 &card,
365 "key_agent",
366 "agent://a",
367 &[("art_cert".into(), cert.clone())],
368 &empty,
369 NOW
370 )
371 .is_none());
372
373 let wrong_kind = ship_pinned(&ship, TrustRootKind::AgentCert);
376 assert!(chain_verify_card(
377 &card,
378 "key_agent",
379 "agent://a",
380 &[("art_cert".into(), cert)],
381 &wrong_kind,
382 NOW
383 )
384 .is_none());
385 }
386
387 #[test]
388 fn chain_rejects_expired_and_not_yet_valid_certs() {
389 let ship = Ed25519Signer::generate("key_ship").unwrap();
390 let agent_key = Ed25519Signer::generate("key_agent").unwrap();
391 let card = signed_card("agent://a", "key_agent", &agent_key);
392 let trust = ship_pinned(&ship, TrustRootKind::CertIssuer);
393
394 let mut expired = cert_payload("agent://a", &agent_key);
395 expired["valid_until"] = serde_json::json!("2026-01-02T00:00:00Z");
396 let cert = signed_receipt("agent_cert.v1", expired, &ship);
397 assert!(
398 chain_verify_card(
399 &card,
400 "key_agent",
401 "agent://a",
402 &[("c".into(), cert)],
403 &trust,
404 NOW
405 )
406 .is_none(),
407 "expired cert must reject"
408 );
409
410 let mut future = cert_payload("agent://a", &agent_key);
411 future["issued_at"] = serde_json::json!("2026-12-01T00:00:00Z");
412 let cert = signed_receipt("agent_cert.v1", future, &ship);
413 assert!(
414 chain_verify_card(
415 &card,
416 "key_agent",
417 "agent://a",
418 &[("c".into(), cert)],
419 &trust,
420 NOW
421 )
422 .is_none(),
423 "not-yet-valid cert must reject"
424 );
425
426 let mut missing = cert_payload("agent://a", &agent_key);
427 missing.as_object_mut().unwrap().remove("valid_until");
428 let cert = signed_receipt("agent_cert.v1", missing, &ship);
429 assert!(
430 chain_verify_card(
431 &card,
432 "key_agent",
433 "agent://a",
434 &[("c".into(), cert)],
435 &trust,
436 NOW
437 )
438 .is_none(),
439 "missing window must fail closed"
440 );
441 }
442
443 #[test]
444 fn chain_rejects_subject_and_agent_mismatches() {
445 let ship = Ed25519Signer::generate("key_ship").unwrap();
446 let agent_key = Ed25519Signer::generate("key_agent").unwrap();
447 let other_key = Ed25519Signer::generate("key_other").unwrap();
448 let trust = ship_pinned(&ship, TrustRootKind::CertIssuer);
449
450 let cert = signed_receipt(
452 "agent_cert.v1",
453 cert_payload("agent://a", &other_key),
454 &ship,
455 );
456 let card = signed_card("agent://a", "key_agent", &agent_key);
457 assert!(
458 chain_verify_card(
459 &card,
460 "key_agent",
461 "agent://a",
462 &[("c".into(), cert)],
463 &trust,
464 NOW
465 )
466 .is_none(),
467 "subject mismatch must reject"
468 );
469
470 let cert = signed_receipt(
473 "agent_cert.v1",
474 cert_payload("agent://b", &agent_key),
475 &ship,
476 );
477 let card = signed_card("agent://a", "key_agent", &agent_key);
478 assert!(
479 chain_verify_card(
480 &card,
481 "key_agent",
482 "agent://a",
483 &[("c".into(), cert)],
484 &trust,
485 NOW
486 )
487 .is_none(),
488 "agent URI mismatch must reject"
489 );
490
491 let cert = signed_receipt(
494 "agent_cert.v1",
495 cert_payload("agent://a", &agent_key),
496 &ship,
497 );
498 let card = signed_card("agent://a", "key_agent", &other_key);
499 assert!(
500 chain_verify_card(
501 &card,
502 "key_agent",
503 "agent://a",
504 &[("c".into(), cert)],
505 &trust,
506 NOW
507 )
508 .is_none(),
509 "wrong card signer must reject"
510 );
511
512 let cert = signed_receipt(
514 "agent_cert.v1",
515 cert_payload("agent://a", &agent_key),
516 &ship,
517 );
518 let card = signed_card("agent://a", "key_someone_else", &agent_key);
519 assert!(
520 chain_verify_card(
521 &card,
522 "key_someone_else",
523 "agent://a",
524 &[("c".into(), cert)],
525 &trust,
526 NOW
527 )
528 .is_none(),
529 "keyid/signer mismatch must reject"
530 );
531 }
532
533 #[test]
534 fn verify_resolution_chains_and_honors_revocation() {
535 let ship = Ed25519Signer::generate("key_ship").unwrap();
536 let agent_key = Ed25519Signer::generate("key_agent").unwrap();
537 let cert = signed_receipt(
538 "agent_cert.v1",
539 cert_payload("agent://a", &agent_key),
540 &ship,
541 );
542 let card = signed_card("agent://a", "key_agent", &agent_key);
543 let trust = ship_pinned(&ship, TrustRootKind::CertIssuer);
544
545 let bundle = ResolutionBundle {
547 agent: "agent://a".into(),
548 card: card.clone(),
549 certs: vec![("art_cert".into(), cert.clone())],
550 revocations: vec![],
551 };
552 let v = verify_resolution(&bundle, &trust, NOW).unwrap();
553 assert!(v.key_bound && v.sig_ok, "valid chain must be key-bound");
554 assert_eq!(v.chain_cert_id.as_deref(), Some("art_cert"));
555 assert!(!v.revoked);
556
557 let mut rev_stmt = ReceiptStatement::new("ship://ship_test", "agent_card_revocation.v1");
560 rev_stmt.payload =
561 Some(serde_json::json!({ "card_ref": "card_x", "reason": "key-rotation" }));
562 let rev = sign(&payload_type("receipt"), &rev_stmt, &agent_key)
563 .unwrap()
564 .envelope;
565 let bundle = ResolutionBundle {
566 agent: "agent://a".into(),
567 card,
568 certs: vec![("art_cert".into(), cert)],
569 revocations: vec![rev],
570 };
571 let v = verify_resolution(&bundle, &trust, NOW).unwrap();
572 assert!(v.revoked, "self-revocation must be honored");
573 assert_eq!(v.revocation_reason.as_deref(), Some("key-rotation"));
574 }
575}