1use serde_json::{Value, json};
24use std::collections::BTreeMap;
25use time::OffsetDateTime;
26use time::format_description::well_known::Rfc3339;
27
28use crate::signing::{b64encode, make_key_id};
29
30pub fn tier_order() -> BTreeMap<&'static str, u32> {
36 [
37 ("UNTRUSTED", 0u32),
38 ("ORG_VERIFIED", 1),
39 ("VERIFIED", 2),
40 ("ATTESTED", 3),
41 ("TRUSTED", 4),
42 ]
43 .into_iter()
44 .collect()
45}
46
47#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
48pub enum Tier {
49 Untrusted,
50 OrgVerified,
51 Verified,
52 Attested,
53 Trusted,
54}
55
56impl Tier {
57 pub fn as_str(self) -> &'static str {
58 match self {
59 Tier::Untrusted => "UNTRUSTED",
60 Tier::OrgVerified => "ORG_VERIFIED",
61 Tier::Verified => "VERIFIED",
62 Tier::Attested => "ATTESTED",
63 Tier::Trusted => "TRUSTED",
64 }
65 }
66}
67
68pub type Trust = Value;
71
72pub fn empty_trust() -> Trust {
73 json!({"version": 1, "agents": {}})
74}
75
76pub fn get_tier(trust: &Trust, peer_handle: &str) -> String {
77 trust
78 .get("agents")
79 .and_then(|a| a.get(peer_handle))
80 .and_then(|a| a.get("tier"))
81 .and_then(Value::as_str)
82 .unwrap_or("UNTRUSTED")
83 .to_string()
84}
85
86pub fn effective_tier(trust: &Value, relay_state: &Value, handle: &str) -> String {
108 let raw = get_tier(trust, handle);
109 if raw != "VERIFIED" {
110 return raw;
111 }
112 let peer_obj = relay_state.get("peers").and_then(|p| p.get(handle));
113 let bilateral_at = peer_obj
114 .and_then(|p| p.get("bilateral_completed_at"))
115 .and_then(Value::as_str);
116 if bilateral_at.is_some() {
117 return raw;
118 }
119 let has_slot = peer_obj
125 .and_then(|p| p.get("slot_token"))
126 .and_then(Value::as_str)
127 .map(|t| !t.is_empty())
128 .unwrap_or(false)
129 || crate::endpoints::peer_endpoints_in_priority_order(relay_state, handle)
130 .iter()
131 .any(|e| !e.slot_token.is_empty());
132 if has_slot {
133 raw
134 } else {
135 "PENDING_ACK".to_string()
136 }
137}
138
139pub fn resolve_peer_did(trust: &Value, peer_handle: &str) -> String {
152 trust
153 .get("agents")
154 .and_then(|a| a.get(peer_handle))
155 .and_then(|p| p.get("did"))
156 .and_then(Value::as_str)
157 .map(str::to_string)
158 .unwrap_or_else(|| format!("did:wire:{peer_handle}"))
159}
160
161pub fn add_agent_card_pin(trust: &mut Trust, card: &Value, tier: Option<&str>) {
166 let did = card.get("did").and_then(Value::as_str).unwrap_or_default();
167 let handle = card
172 .get("handle")
173 .and_then(Value::as_str)
174 .map(str::to_string)
175 .unwrap_or_else(|| crate::agent_card::display_handle_from_did(did).to_string());
176 if handle.is_empty() {
177 panic!("card has no resolvable handle (did={did:?})");
178 }
179 let tier = tier.unwrap_or("UNTRUSTED");
180 let now = now_iso();
181
182 let mut public_keys = Vec::new();
183 if let Some(vks) = card.get("verify_keys").and_then(Value::as_object) {
184 for (key_id_full, key_record) in vks {
185 let key_id = key_id_full.strip_prefix("ed25519:").unwrap_or(key_id_full);
187 public_keys.push(json!({
188 "key_id": key_id,
189 "key": key_record.get("key").cloned().unwrap_or(Value::Null),
190 "added_at": now,
191 "active": true,
192 }));
193 }
194 }
195
196 let agents = trust
197 .as_object_mut()
198 .expect("trust must be an object")
199 .entry("agents")
200 .or_insert_with(|| json!({}));
201
202 agents[handle] = json!({
203 "tier": tier,
204 "did": did,
205 "public_keys": public_keys,
206 "card": card.clone(),
207 "pinned_at": now,
208 });
209}
210
211pub fn promote_to_verified(trust: &mut Trust, peer_handle: &str) -> Result<(), String> {
219 let agents = trust
220 .as_object_mut()
221 .ok_or("trust is not an object")?
222 .get_mut("agents")
223 .and_then(Value::as_object_mut)
224 .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
225
226 let agent = agents
227 .get_mut(peer_handle)
228 .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
229
230 let current = agent
231 .get("tier")
232 .and_then(Value::as_str)
233 .unwrap_or("UNTRUSTED")
234 .to_string();
235 if current != "UNTRUSTED" && current != "ORG_VERIFIED" {
236 return Err(format!(
237 "peer {peer_handle:?} already at tier {current:?} — promotion is one-way"
238 ));
239 }
240 agent["tier"] = json!("VERIFIED");
241 agent["verified_at"] = json!(now_iso());
242 Ok(())
243}
244
245pub fn promote_to_org_verified(trust: &mut Trust, peer_handle: &str) -> Result<(), String> {
260 let agents = trust
261 .as_object_mut()
262 .ok_or("trust is not an object")?
263 .get_mut("agents")
264 .and_then(Value::as_object_mut)
265 .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
266
267 let agent = agents
268 .get_mut(peer_handle)
269 .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
270
271 let current = agent
272 .get("tier")
273 .and_then(Value::as_str)
274 .unwrap_or("UNTRUSTED")
275 .to_string();
276 if current != "UNTRUSTED" {
277 return Err(format!(
278 "peer {peer_handle:?} already at tier {current:?} — \
279 org_verified promotion fires from UNTRUSTED only"
280 ));
281 }
282 agent["tier"] = json!("ORG_VERIFIED");
283 agent["org_verified_at"] = json!(now_iso());
284 Ok(())
285}
286
287pub fn project_recipients(
300 trust: &Value,
301 relay_state: &Value,
302 self_handle: &str,
303 project: &str,
304) -> Vec<String> {
305 let order = tier_order();
306 let floor = order.get("ORG_VERIFIED").copied().unwrap_or(1);
307 let mut out = Vec::new();
308 if let Some(agents) = trust.get("agents").and_then(Value::as_object) {
309 for (handle, agent) in agents {
310 if handle == self_handle {
311 continue;
312 }
313 let tier = effective_tier(trust, relay_state, handle);
314 let rank = order.get(tier.as_str()).copied().unwrap_or(0);
315 if rank < floor {
316 continue;
317 }
318 let proj = agent.get("card").and_then(crate::agent_card::card_project);
319 if proj == Some(project) {
320 out.push(handle.clone());
321 }
322 }
323 }
324 out.sort();
325 out
326}
327
328pub fn add_self_to_trust(trust: &mut Trust, handle: &str, public_key: &[u8]) {
330 let agents = trust
331 .as_object_mut()
332 .expect("trust must be an object")
333 .entry("agents")
334 .or_insert_with(|| json!({}));
335 let key_id = make_key_id(handle, public_key);
336 agents[handle] = json!({
337 "tier": "ATTESTED",
338 "did": crate::agent_card::did_for_with_key(handle, public_key),
339 "public_keys": [{
340 "key_id": key_id,
341 "key": b64encode(public_key),
342 "added_at": now_iso(),
343 "active": true,
344 }],
345 });
346}
347
348fn now_iso() -> String {
349 let now = OffsetDateTime::now_utc();
350 now.format(&Rfc3339)
351 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use crate::agent_card::{build_agent_card, sign_agent_card};
358 use crate::signing::generate_keypair;
359
360 #[test]
361 fn empty_trust_shape() {
362 let t = empty_trust();
363 assert_eq!(t["version"], 1);
364 assert!(t["agents"].is_object());
365 assert_eq!(t["agents"].as_object().unwrap().len(), 0);
366 }
367
368 #[test]
369 fn get_tier_unknown_returns_untrusted() {
370 assert_eq!(get_tier(&empty_trust(), "ghost"), "UNTRUSTED");
371 }
372
373 #[test]
374 fn resolve_peer_did_returns_pinned_did_with_full_suffix() {
375 let (sk, pk) = generate_keypair();
380 let card = sign_agent_card(
381 &build_agent_card("sunlit-aurora", &pk, None, None, None),
382 &sk,
383 );
384 let pinned_did = card.get("did").and_then(Value::as_str).unwrap();
385 assert!(
386 pinned_did.starts_with("did:wire:sunlit-aurora-"),
387 "test setup: card DID should carry long-hex suffix"
388 );
389 let mut t = empty_trust();
390 add_agent_card_pin(&mut t, &card, Some("VERIFIED"));
391
392 let resolved = resolve_peer_did(&t, "sunlit-aurora");
393 assert_eq!(
394 resolved, pinned_did,
395 "pinned peer must resolve to its full DID, not the bare handle"
396 );
397 }
398
399 #[test]
400 fn resolve_peer_did_falls_back_to_bare_for_unknown_peer() {
401 let t = empty_trust();
405 assert_eq!(
406 resolve_peer_did(&t, "ghost-peer"),
407 "did:wire:ghost-peer",
408 "unknown peer falls back to bare-handle DID"
409 );
410 }
411
412 #[test]
413 fn add_agent_card_pin_defaults_untrusted() {
414 let (sk, pk) = generate_keypair();
415 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
416 let mut t = empty_trust();
417 add_agent_card_pin(&mut t, &card, None);
418 assert_eq!(get_tier(&t, "paul"), "UNTRUSTED");
419 let did = t["agents"]["paul"]["did"].as_str().unwrap();
421 assert!(did.starts_with("did:wire:paul-"), "got: {did}");
422 }
423
424 #[test]
425 fn add_pin_strips_ed25519_prefix_from_key_id() {
426 let (sk, pk) = generate_keypair();
427 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
428 let mut t = empty_trust();
429 add_agent_card_pin(&mut t, &card, None);
430 let kid = t["agents"]["paul"]["public_keys"][0]["key_id"]
431 .as_str()
432 .unwrap();
433 assert!(kid.contains(':'));
434 assert!(!kid.starts_with("ed25519:"));
435 }
436
437 #[test]
438 fn promote_to_verified_one_way() {
439 let (sk, pk) = generate_keypair();
440 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
441 let mut t = empty_trust();
442 add_agent_card_pin(&mut t, &card, None);
443 promote_to_verified(&mut t, "paul").unwrap();
444 assert_eq!(get_tier(&t, "paul"), "VERIFIED");
445 assert!(t["agents"]["paul"]["verified_at"].is_string());
446 }
447
448 #[test]
449 fn promote_to_verified_idempotent_block() {
450 let (sk, pk) = generate_keypair();
451 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
452 let mut t = empty_trust();
453 add_agent_card_pin(&mut t, &card, None);
454 promote_to_verified(&mut t, "paul").unwrap();
455 let err = promote_to_verified(&mut t, "paul").unwrap_err();
456 assert!(err.contains("VERIFIED"), "got: {err}");
457 }
458
459 #[test]
460 fn promote_unknown_peer_fails() {
461 let mut t = empty_trust();
462 let err = promote_to_verified(&mut t, "ghost").unwrap_err();
463 assert!(err.contains("not pinned"), "got: {err}");
464 }
465
466 #[test]
467 fn add_self_to_trust_attests() {
468 let (_, pk) = generate_keypair();
469 let mut t = empty_trust();
470 add_self_to_trust(&mut t, "paul", &pk);
471 assert_eq!(get_tier(&t, "paul"), "ATTESTED");
472 let did = t["agents"]["paul"]["did"].as_str().unwrap();
473 assert!(did.starts_with("did:wire:paul-"), "got: {did}");
474 }
475
476 #[test]
477 fn tier_order_matches_promotion_semantics() {
478 let order = tier_order();
479 assert!(order["UNTRUSTED"] < order["ORG_VERIFIED"]);
480 assert!(order["ORG_VERIFIED"] < order["VERIFIED"]);
481 assert!(order["VERIFIED"] < order["ATTESTED"]);
482 assert!(order["ATTESTED"] < order["TRUSTED"]);
483 }
484
485 #[test]
488 fn tier_as_str_covers_org_verified() {
489 assert_eq!(Tier::OrgVerified.as_str(), "ORG_VERIFIED");
490 }
491
492 #[test]
493 fn promote_to_org_verified_one_way() {
494 let (sk, pk) = generate_keypair();
495 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
496 let mut t = empty_trust();
497 add_agent_card_pin(&mut t, &card, None);
498 promote_to_org_verified(&mut t, "paul").unwrap();
499 assert_eq!(get_tier(&t, "paul"), "ORG_VERIFIED");
500 assert!(t["agents"]["paul"]["org_verified_at"].is_string());
501 }
502
503 #[test]
504 fn promote_to_org_verified_refuses_already_verified() {
505 let (sk, pk) = generate_keypair();
508 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
509 let mut t = empty_trust();
510 add_agent_card_pin(&mut t, &card, None);
511 promote_to_verified(&mut t, "paul").unwrap();
512 let err = promote_to_org_verified(&mut t, "paul").unwrap_err();
513 assert!(err.contains("VERIFIED"), "got: {err}");
514 assert_eq!(get_tier(&t, "paul"), "VERIFIED");
515 }
516
517 #[test]
518 fn promote_to_org_verified_refuses_self_idempotent() {
519 let (sk, pk) = generate_keypair();
522 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
523 let mut t = empty_trust();
524 add_agent_card_pin(&mut t, &card, None);
525 promote_to_org_verified(&mut t, "paul").unwrap();
526 let err = promote_to_org_verified(&mut t, "paul").unwrap_err();
527 assert!(err.contains("ORG_VERIFIED"), "got: {err}");
528 }
529
530 #[test]
531 fn promote_to_verified_accepts_org_verified_source() {
532 let (sk, pk) = generate_keypair();
536 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
537 let mut t = empty_trust();
538 add_agent_card_pin(&mut t, &card, None);
539 promote_to_org_verified(&mut t, "paul").unwrap();
540 promote_to_verified(&mut t, "paul").unwrap();
541 assert_eq!(get_tier(&t, "paul"), "VERIFIED");
542 assert!(t["agents"]["paul"]["org_verified_at"].is_string());
543 assert!(t["agents"]["paul"]["verified_at"].is_string());
544 }
545
546 #[test]
547 fn promote_to_verified_refuses_attested_source() {
548 let (_, pk) = generate_keypair();
551 let mut t = empty_trust();
552 add_self_to_trust(&mut t, "self", &pk);
553 let err = promote_to_verified(&mut t, "self").unwrap_err();
554 assert!(err.contains("ATTESTED"), "got: {err}");
555 }
556
557 #[test]
558 fn effective_tier_matrix() {
559 use serde_json::json;
560 let trust = json!({"agents": {"a": {"tier": "VERIFIED"}}});
562 let relay = json!({"peers": {"a": {"bilateral_completed_at": "t"}}});
563 assert_eq!(effective_tier(&trust, &relay, "a"), "VERIFIED");
564 let relay = json!({"peers": {"a": {"slot_token": "tok"}}});
566 assert_eq!(effective_tier(&trust, &relay, "a"), "VERIFIED");
567 let relay = json!({"peers": {"a": {"slot_token": ""}}});
569 assert_eq!(effective_tier(&trust, &relay, "a"), "PENDING_ACK");
570 let relay = json!({"peers": {}});
572 assert_eq!(effective_tier(&trust, &relay, "a"), "PENDING_ACK");
573 let relay = json!({"peers": {"a": {"endpoints": [
578 {"relay_url": "https://r", "slot_id": "s", "slot_token": "tok", "scope": "federation"}
579 ]}}});
580 assert_eq!(effective_tier(&trust, &relay, "a"), "VERIFIED");
581 let relay = json!({"peers": {"a": {"endpoints": [
583 {"relay_url": "https://r", "slot_id": "s", "slot_token": "", "scope": "federation"}
584 ]}}});
585 assert_eq!(effective_tier(&trust, &relay, "a"), "PENDING_ACK");
586 let trust = json!({"agents": {"a": {"tier": "UNTRUSTED"}}});
588 assert_eq!(effective_tier(&trust, &relay, "a"), "UNTRUSTED");
589 let trust = json!({"agents": {"a": {"tier": "ORG_VERIFIED"}}});
590 assert_eq!(effective_tier(&trust, &relay, "a"), "ORG_VERIFIED");
591 }
592
593 #[test]
594 fn project_recipients_filters_by_tier_and_project() {
595 use serde_json::json;
596 let trust = json!({"agents": {
597 "alice": {"tier": "ORG_VERIFIED", "card": {"project": "print-shop"}},
598 "bob": {"tier": "ORG_VERIFIED", "card": {"project": "lora-training"}},
599 "carol": {"tier": "UNTRUSTED", "card": {"project": "print-shop"}},
600 "dave": {"tier": "VERIFIED", "card": {"project": "print-shop"}},
601 "selfie": {"tier": "ATTESTED", "card": {"project": "print-shop"}},
602 "noproj": {"tier": "ORG_VERIFIED", "card": {}},
603 }});
604 let relay = json!({"peers": {"dave": {"bilateral_completed_at": "t"}}});
606 let r = project_recipients(&trust, &relay, "selfie", "print-shop");
607 assert_eq!(r, vec!["alice".to_string(), "dave".to_string()]);
610 }
611
612 #[test]
613 fn project_recipients_excludes_unreachable_verified() {
614 use serde_json::json;
615 let trust = json!({"agents": {
618 "ghost": {"tier": "VERIFIED", "card": {"project": "x"}},
619 }});
620 let relay = json!({"peers": {}});
621 assert!(project_recipients(&trust, &relay, "selfie", "x").is_empty());
622 }
623
624 #[test]
625 fn org_verified_does_not_satisfy_verified_policy_check() {
626 let order = tier_order();
629 let verified_rank = order["VERIFIED"];
630 let org_rank = order["ORG_VERIFIED"];
631 assert!(
632 org_rank < verified_rank,
633 "ORG_VERIFIED ({org_rank}) must rank strictly below VERIFIED ({verified_rank})"
634 );
635 }
636}