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 = crate::endpoints::peer_endpoints_in_priority_order(relay_state, handle)
124 .iter()
125 .any(|e| !e.slot_token.is_empty());
126 if has_slot {
127 raw
128 } else {
129 "PENDING_ACK".to_string()
130 }
131}
132
133pub fn resolve_peer_did(trust: &Value, peer_handle: &str) -> String {
146 trust
147 .get("agents")
148 .and_then(|a| a.get(peer_handle))
149 .and_then(|p| p.get("did"))
150 .and_then(Value::as_str)
151 .map(str::to_string)
152 .unwrap_or_else(|| format!("did:wire:{peer_handle}"))
153}
154
155pub fn add_agent_card_pin(
174 trust: &mut Trust,
175 card: &Value,
176 tier: Option<&str>,
177) -> Result<(), String> {
178 let did = card.get("did").and_then(Value::as_str).unwrap_or_default();
179 let handle = card
184 .get("handle")
185 .and_then(Value::as_str)
186 .map(str::to_string)
187 .unwrap_or_else(|| crate::agent_card::display_handle_from_did(did).to_string());
188 if handle.is_empty() {
189 return Err(format!("card has no resolvable handle (did={did:?})"));
190 }
191 if let Some(existing_did) = trust
193 .get("agents")
194 .and_then(|a| a.get(&handle))
195 .and_then(|e| e.get("did"))
196 .and_then(Value::as_str)
197 && !existing_did.is_empty()
198 && !did.is_empty()
199 && existing_did != did
200 {
201 return Err(format!(
202 "trust pin collision on nick '{handle}': already pinned to {existing_did}, refusing to overwrite with a DIFFERENT identity {did} (possible grindable-nick attack — #245). \
203 Use `wire forget-peer {handle}` first if you intend to replace it."
204 ));
205 }
206 let tier = tier.unwrap_or("UNTRUSTED");
207 let now = now_iso();
208
209 let mut public_keys = Vec::new();
210 if let Some(vks) = card.get("verify_keys").and_then(Value::as_object) {
211 for (key_id_full, key_record) in vks {
212 let key_id = key_id_full.strip_prefix("ed25519:").unwrap_or(key_id_full);
214 public_keys.push(json!({
215 "key_id": key_id,
216 "key": key_record.get("key").cloned().unwrap_or(Value::Null),
217 "added_at": now,
218 "active": true,
219 }));
220 }
221 }
222
223 let agents = trust
224 .as_object_mut()
225 .expect("trust must be an object")
226 .entry("agents")
227 .or_insert_with(|| json!({}));
228
229 agents[handle] = json!({
230 "tier": tier,
231 "did": did,
232 "public_keys": public_keys,
233 "card": card.clone(),
234 "pinned_at": now,
235 });
236 Ok(())
237}
238
239pub fn promote_to_verified(trust: &mut Trust, peer_handle: &str) -> Result<(), String> {
247 let agents = trust
248 .as_object_mut()
249 .ok_or("trust is not an object")?
250 .get_mut("agents")
251 .and_then(Value::as_object_mut)
252 .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
253
254 let agent = agents
255 .get_mut(peer_handle)
256 .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
257
258 let current = agent
259 .get("tier")
260 .and_then(Value::as_str)
261 .unwrap_or("UNTRUSTED")
262 .to_string();
263 if current != "UNTRUSTED" && current != "ORG_VERIFIED" {
264 return Err(format!(
265 "peer {peer_handle:?} already at tier {current:?} — promotion is one-way"
266 ));
267 }
268 agent["tier"] = json!("VERIFIED");
269 agent["verified_at"] = json!(now_iso());
270 Ok(())
271}
272
273pub fn promote_to_org_verified(trust: &mut Trust, peer_handle: &str) -> Result<(), String> {
288 let agents = trust
289 .as_object_mut()
290 .ok_or("trust is not an object")?
291 .get_mut("agents")
292 .and_then(Value::as_object_mut)
293 .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
294
295 let agent = agents
296 .get_mut(peer_handle)
297 .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
298
299 let current = agent
300 .get("tier")
301 .and_then(Value::as_str)
302 .unwrap_or("UNTRUSTED")
303 .to_string();
304 if current != "UNTRUSTED" {
305 return Err(format!(
306 "peer {peer_handle:?} already at tier {current:?} — \
307 org_verified promotion fires from UNTRUSTED only"
308 ));
309 }
310 agent["tier"] = json!("ORG_VERIFIED");
311 agent["org_verified_at"] = json!(now_iso());
312 Ok(())
313}
314
315pub fn project_recipients(
328 trust: &Value,
329 relay_state: &Value,
330 self_handle: &str,
331 project: &str,
332) -> Vec<String> {
333 let order = tier_order();
334 let floor = order.get("ORG_VERIFIED").copied().unwrap_or(1);
335 let mut out = Vec::new();
336 if let Some(agents) = trust.get("agents").and_then(Value::as_object) {
337 for (handle, agent) in agents {
338 if handle == self_handle {
339 continue;
340 }
341 let tier = effective_tier(trust, relay_state, handle);
342 let rank = order.get(tier.as_str()).copied().unwrap_or(0);
343 if rank < floor {
344 continue;
345 }
346 let proj = agent.get("card").and_then(crate::agent_card::card_project);
347 if proj == Some(project) {
348 out.push(handle.clone());
349 }
350 }
351 }
352 out.sort();
353 out
354}
355
356pub fn add_self_to_trust(trust: &mut Trust, handle: &str, public_key: &[u8]) {
358 let agents = trust
359 .as_object_mut()
360 .expect("trust must be an object")
361 .entry("agents")
362 .or_insert_with(|| json!({}));
363 let key_id = make_key_id(handle, public_key);
364 agents[handle] = json!({
365 "tier": "ATTESTED",
366 "did": crate::agent_card::did_for_with_key(handle, public_key),
367 "public_keys": [{
368 "key_id": key_id,
369 "key": b64encode(public_key),
370 "added_at": now_iso(),
371 "active": true,
372 }],
373 });
374}
375
376fn now_iso() -> String {
377 let now = OffsetDateTime::now_utc();
378 now.format(&Rfc3339)
379 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::agent_card::{build_agent_card, sign_agent_card};
386 use crate::signing::generate_keypair;
387
388 #[test]
389 fn empty_trust_shape() {
390 let t = empty_trust();
391 assert_eq!(t["version"], 1);
392 assert!(t["agents"].is_object());
393 assert_eq!(t["agents"].as_object().unwrap().len(), 0);
394 }
395
396 #[test]
397 fn get_tier_unknown_returns_untrusted() {
398 assert_eq!(get_tier(&empty_trust(), "ghost"), "UNTRUSTED");
399 }
400
401 #[test]
402 fn resolve_peer_did_returns_pinned_did_with_full_suffix() {
403 let (sk, pk) = generate_keypair();
408 let card = sign_agent_card(
409 &build_agent_card("sunlit-aurora", &pk, None, None, None),
410 &sk,
411 );
412 let pinned_did = card.get("did").and_then(Value::as_str).unwrap();
413 assert!(
414 pinned_did.starts_with("did:wire:sunlit-aurora-"),
415 "test setup: card DID should carry long-hex suffix"
416 );
417 let mut t = empty_trust();
418 add_agent_card_pin(&mut t, &card, Some("VERIFIED")).unwrap();
419
420 let resolved = resolve_peer_did(&t, "sunlit-aurora");
421 assert_eq!(
422 resolved, pinned_did,
423 "pinned peer must resolve to its full DID, not the bare handle"
424 );
425 }
426
427 #[test]
428 fn add_agent_card_pin_refuses_nick_collision_from_a_different_identity() {
429 let (sk_a, pk_a) = generate_keypair();
434 let card_a = sign_agent_card(
435 &build_agent_card("raven-kettle", &pk_a, None, None, None),
436 &sk_a,
437 );
438 let did_a = card_a
439 .get("did")
440 .and_then(Value::as_str)
441 .unwrap()
442 .to_string();
443
444 let (sk_b, pk_b) = generate_keypair();
445 let card_b = sign_agent_card(
446 &build_agent_card("raven-kettle", &pk_b, None, None, None),
447 &sk_b,
448 );
449 let did_b = card_b
450 .get("did")
451 .and_then(Value::as_str)
452 .unwrap()
453 .to_string();
454 assert_ne!(did_a, did_b, "test setup: distinct keys ⇒ distinct DIDs");
455
456 let mut t = empty_trust();
457 add_agent_card_pin(&mut t, &card_a, Some("VERIFIED")).unwrap();
458
459 let err = add_agent_card_pin(&mut t, &card_b, Some("VERIFIED")).unwrap_err();
461 assert!(
462 err.contains("collision"),
463 "expected collision error, got: {err}"
464 );
465 assert_eq!(
466 t["agents"]["raven-kettle"]["did"], did_a,
467 "incumbent A's pin must NOT be overwritten by colliding identity B"
468 );
469
470 add_agent_card_pin(&mut t, &card_a, Some("TRUSTED")).unwrap();
473 assert_eq!(t["agents"]["raven-kettle"]["tier"], "TRUSTED");
474 }
475
476 #[test]
477 fn resolve_peer_did_falls_back_to_bare_for_unknown_peer() {
478 let t = empty_trust();
482 assert_eq!(
483 resolve_peer_did(&t, "ghost-peer"),
484 "did:wire:ghost-peer",
485 "unknown peer falls back to bare-handle DID"
486 );
487 }
488
489 #[test]
490 fn add_agent_card_pin_defaults_untrusted() {
491 let (sk, pk) = generate_keypair();
492 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
493 let mut t = empty_trust();
494 add_agent_card_pin(&mut t, &card, None).unwrap();
495 assert_eq!(get_tier(&t, "paul"), "UNTRUSTED");
496 let did = t["agents"]["paul"]["did"].as_str().unwrap();
498 assert!(did.starts_with("did:wire:paul-"), "got: {did}");
499 }
500
501 #[test]
502 fn add_pin_strips_ed25519_prefix_from_key_id() {
503 let (sk, pk) = generate_keypair();
504 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
505 let mut t = empty_trust();
506 add_agent_card_pin(&mut t, &card, None).unwrap();
507 let kid = t["agents"]["paul"]["public_keys"][0]["key_id"]
508 .as_str()
509 .unwrap();
510 assert!(kid.contains(':'));
511 assert!(!kid.starts_with("ed25519:"));
512 }
513
514 #[test]
515 fn promote_to_verified_one_way() {
516 let (sk, pk) = generate_keypair();
517 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
518 let mut t = empty_trust();
519 add_agent_card_pin(&mut t, &card, None).unwrap();
520 promote_to_verified(&mut t, "paul").unwrap();
521 assert_eq!(get_tier(&t, "paul"), "VERIFIED");
522 assert!(t["agents"]["paul"]["verified_at"].is_string());
523 }
524
525 #[test]
526 fn promote_to_verified_idempotent_block() {
527 let (sk, pk) = generate_keypair();
528 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
529 let mut t = empty_trust();
530 add_agent_card_pin(&mut t, &card, None).unwrap();
531 promote_to_verified(&mut t, "paul").unwrap();
532 let err = promote_to_verified(&mut t, "paul").unwrap_err();
533 assert!(err.contains("VERIFIED"), "got: {err}");
534 }
535
536 #[test]
537 fn promote_unknown_peer_fails() {
538 let mut t = empty_trust();
539 let err = promote_to_verified(&mut t, "ghost").unwrap_err();
540 assert!(err.contains("not pinned"), "got: {err}");
541 }
542
543 #[test]
544 fn add_self_to_trust_attests() {
545 let (_, pk) = generate_keypair();
546 let mut t = empty_trust();
547 add_self_to_trust(&mut t, "paul", &pk);
548 assert_eq!(get_tier(&t, "paul"), "ATTESTED");
549 let did = t["agents"]["paul"]["did"].as_str().unwrap();
550 assert!(did.starts_with("did:wire:paul-"), "got: {did}");
551 }
552
553 #[test]
554 fn tier_order_matches_promotion_semantics() {
555 let order = tier_order();
556 assert!(order["UNTRUSTED"] < order["ORG_VERIFIED"]);
557 assert!(order["ORG_VERIFIED"] < order["VERIFIED"]);
558 assert!(order["VERIFIED"] < order["ATTESTED"]);
559 assert!(order["ATTESTED"] < order["TRUSTED"]);
560 }
561
562 #[test]
565 fn tier_as_str_covers_org_verified() {
566 assert_eq!(Tier::OrgVerified.as_str(), "ORG_VERIFIED");
567 }
568
569 #[test]
570 fn promote_to_org_verified_one_way() {
571 let (sk, pk) = generate_keypair();
572 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
573 let mut t = empty_trust();
574 add_agent_card_pin(&mut t, &card, None).unwrap();
575 promote_to_org_verified(&mut t, "paul").unwrap();
576 assert_eq!(get_tier(&t, "paul"), "ORG_VERIFIED");
577 assert!(t["agents"]["paul"]["org_verified_at"].is_string());
578 }
579
580 #[test]
581 fn promote_to_org_verified_refuses_already_verified() {
582 let (sk, pk) = generate_keypair();
585 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
586 let mut t = empty_trust();
587 add_agent_card_pin(&mut t, &card, None).unwrap();
588 promote_to_verified(&mut t, "paul").unwrap();
589 let err = promote_to_org_verified(&mut t, "paul").unwrap_err();
590 assert!(err.contains("VERIFIED"), "got: {err}");
591 assert_eq!(get_tier(&t, "paul"), "VERIFIED");
592 }
593
594 #[test]
595 fn promote_to_org_verified_refuses_self_idempotent() {
596 let (sk, pk) = generate_keypair();
599 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
600 let mut t = empty_trust();
601 add_agent_card_pin(&mut t, &card, None).unwrap();
602 promote_to_org_verified(&mut t, "paul").unwrap();
603 let err = promote_to_org_verified(&mut t, "paul").unwrap_err();
604 assert!(err.contains("ORG_VERIFIED"), "got: {err}");
605 }
606
607 #[test]
608 fn promote_to_verified_accepts_org_verified_source() {
609 let (sk, pk) = generate_keypair();
613 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
614 let mut t = empty_trust();
615 add_agent_card_pin(&mut t, &card, None).unwrap();
616 promote_to_org_verified(&mut t, "paul").unwrap();
617 promote_to_verified(&mut t, "paul").unwrap();
618 assert_eq!(get_tier(&t, "paul"), "VERIFIED");
619 assert!(t["agents"]["paul"]["org_verified_at"].is_string());
620 assert!(t["agents"]["paul"]["verified_at"].is_string());
621 }
622
623 #[test]
624 fn promote_to_verified_refuses_attested_source() {
625 let (_, pk) = generate_keypair();
628 let mut t = empty_trust();
629 add_self_to_trust(&mut t, "self", &pk);
630 let err = promote_to_verified(&mut t, "self").unwrap_err();
631 assert!(err.contains("ATTESTED"), "got: {err}");
632 }
633
634 #[test]
635 fn effective_tier_matrix() {
636 use serde_json::json;
637 let trust = json!({"agents": {"a": {"tier": "VERIFIED"}}});
639 let relay = json!({"peers": {"a": {"bilateral_completed_at": "t"}}});
640 assert_eq!(effective_tier(&trust, &relay, "a"), "VERIFIED");
641 let relay = json!({"peers": {"a": {"slot_token": "tok"}}});
644 assert_eq!(effective_tier(&trust, &relay, "a"), "PENDING_ACK");
645 let relay = json!({"peers": {"a": {"slot_token": ""}}});
647 assert_eq!(effective_tier(&trust, &relay, "a"), "PENDING_ACK");
648 let relay = json!({"peers": {}});
650 assert_eq!(effective_tier(&trust, &relay, "a"), "PENDING_ACK");
651 let relay = json!({"peers": {"a": {"endpoints": [
656 {"relay_url": "https://r", "slot_id": "s", "slot_token": "tok", "scope": "federation"}
657 ]}}});
658 assert_eq!(effective_tier(&trust, &relay, "a"), "VERIFIED");
659 let relay = json!({"peers": {"a": {"endpoints": [
661 {"relay_url": "https://r", "slot_id": "s", "slot_token": "", "scope": "federation"}
662 ]}}});
663 assert_eq!(effective_tier(&trust, &relay, "a"), "PENDING_ACK");
664 let trust = json!({"agents": {"a": {"tier": "UNTRUSTED"}}});
666 assert_eq!(effective_tier(&trust, &relay, "a"), "UNTRUSTED");
667 let trust = json!({"agents": {"a": {"tier": "ORG_VERIFIED"}}});
668 assert_eq!(effective_tier(&trust, &relay, "a"), "ORG_VERIFIED");
669 }
670
671 #[test]
672 fn project_recipients_filters_by_tier_and_project() {
673 use serde_json::json;
674 let trust = json!({"agents": {
675 "alice": {"tier": "ORG_VERIFIED", "card": {"project": "print-shop"}},
676 "bob": {"tier": "ORG_VERIFIED", "card": {"project": "lora-training"}},
677 "carol": {"tier": "UNTRUSTED", "card": {"project": "print-shop"}},
678 "dave": {"tier": "VERIFIED", "card": {"project": "print-shop"}},
679 "selfie": {"tier": "ATTESTED", "card": {"project": "print-shop"}},
680 "noproj": {"tier": "ORG_VERIFIED", "card": {}},
681 }});
682 let relay = json!({"peers": {"dave": {"bilateral_completed_at": "t"}}});
684 let r = project_recipients(&trust, &relay, "selfie", "print-shop");
685 assert_eq!(r, vec!["alice".to_string(), "dave".to_string()]);
688 }
689
690 #[test]
691 fn project_recipients_excludes_unreachable_verified() {
692 use serde_json::json;
693 let trust = json!({"agents": {
696 "ghost": {"tier": "VERIFIED", "card": {"project": "x"}},
697 }});
698 let relay = json!({"peers": {}});
699 assert!(project_recipients(&trust, &relay, "selfie", "x").is_empty());
700 }
701
702 #[test]
703 fn org_verified_does_not_satisfy_verified_policy_check() {
704 let order = tier_order();
707 let verified_rank = order["VERIFIED"];
708 let org_rank = order["ORG_VERIFIED"];
709 assert!(
710 org_rank < verified_rank,
711 "ORG_VERIFIED ({org_rank}) must rank strictly below VERIFIED ({verified_rank})"
712 );
713 }
714}