1use serde_json::{Value, json};
13use std::collections::BTreeMap;
14use time::OffsetDateTime;
15use time::format_description::well_known::Rfc3339;
16
17use crate::signing::{b64encode, make_key_id};
18
19pub fn tier_order() -> BTreeMap<&'static str, u32> {
21 [
22 ("UNTRUSTED", 0u32),
23 ("VERIFIED", 1),
24 ("ATTESTED", 2),
25 ("TRUSTED", 3),
26 ]
27 .into_iter()
28 .collect()
29}
30
31#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
32pub enum Tier {
33 Untrusted,
34 Verified,
35 Attested,
36 Trusted,
37}
38
39impl Tier {
40 pub fn as_str(self) -> &'static str {
41 match self {
42 Tier::Untrusted => "UNTRUSTED",
43 Tier::Verified => "VERIFIED",
44 Tier::Attested => "ATTESTED",
45 Tier::Trusted => "TRUSTED",
46 }
47 }
48}
49
50pub type Trust = Value;
53
54pub fn empty_trust() -> Trust {
55 json!({"version": 1, "agents": {}})
56}
57
58pub fn get_tier(trust: &Trust, peer_handle: &str) -> String {
59 trust
60 .get("agents")
61 .and_then(|a| a.get(peer_handle))
62 .and_then(|a| a.get("tier"))
63 .and_then(Value::as_str)
64 .unwrap_or("UNTRUSTED")
65 .to_string()
66}
67
68pub fn add_agent_card_pin(trust: &mut Trust, card: &Value, tier: Option<&str>) {
73 let did = card.get("did").and_then(Value::as_str).unwrap_or_default();
74 let handle = card
79 .get("handle")
80 .and_then(Value::as_str)
81 .map(str::to_string)
82 .unwrap_or_else(|| crate::agent_card::display_handle_from_did(did).to_string());
83 if handle.is_empty() {
84 panic!("card has no resolvable handle (did={did:?})");
85 }
86 let tier = tier.unwrap_or("UNTRUSTED");
87 let now = now_iso();
88
89 let mut public_keys = Vec::new();
90 if let Some(vks) = card.get("verify_keys").and_then(Value::as_object) {
91 for (key_id_full, key_record) in vks {
92 let key_id = key_id_full.strip_prefix("ed25519:").unwrap_or(key_id_full);
94 public_keys.push(json!({
95 "key_id": key_id,
96 "key": key_record.get("key").cloned().unwrap_or(Value::Null),
97 "added_at": now,
98 "active": true,
99 }));
100 }
101 }
102
103 let agents = trust
104 .as_object_mut()
105 .expect("trust must be an object")
106 .entry("agents")
107 .or_insert_with(|| json!({}));
108
109 agents[handle] = json!({
110 "tier": tier,
111 "did": did,
112 "public_keys": public_keys,
113 "card": card.clone(),
114 "pinned_at": now,
115 });
116}
117
118pub fn promote_to_verified(trust: &mut Trust, peer_handle: &str) -> Result<(), String> {
121 let agents = trust
122 .as_object_mut()
123 .ok_or("trust is not an object")?
124 .get_mut("agents")
125 .and_then(Value::as_object_mut)
126 .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
127
128 let agent = agents
129 .get_mut(peer_handle)
130 .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
131
132 let current = agent
133 .get("tier")
134 .and_then(Value::as_str)
135 .unwrap_or("UNTRUSTED")
136 .to_string();
137 if current != "UNTRUSTED" {
138 return Err(format!(
139 "peer {peer_handle:?} already at tier {current:?} — promotion is one-way"
140 ));
141 }
142 agent["tier"] = json!("VERIFIED");
143 agent["verified_at"] = json!(now_iso());
144 Ok(())
145}
146
147pub fn add_self_to_trust(trust: &mut Trust, handle: &str, public_key: &[u8]) {
149 let agents = trust
150 .as_object_mut()
151 .expect("trust must be an object")
152 .entry("agents")
153 .or_insert_with(|| json!({}));
154 let key_id = make_key_id(handle, public_key);
155 agents[handle] = json!({
156 "tier": "ATTESTED",
157 "did": crate::agent_card::did_for_with_key(handle, public_key),
158 "public_keys": [{
159 "key_id": key_id,
160 "key": b64encode(public_key),
161 "added_at": now_iso(),
162 "active": true,
163 }],
164 });
165}
166
167fn now_iso() -> String {
168 let now = OffsetDateTime::now_utc();
169 now.format(&Rfc3339)
170 .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::agent_card::{build_agent_card, sign_agent_card};
177 use crate::signing::generate_keypair;
178
179 #[test]
180 fn empty_trust_shape() {
181 let t = empty_trust();
182 assert_eq!(t["version"], 1);
183 assert!(t["agents"].is_object());
184 assert_eq!(t["agents"].as_object().unwrap().len(), 0);
185 }
186
187 #[test]
188 fn get_tier_unknown_returns_untrusted() {
189 assert_eq!(get_tier(&empty_trust(), "ghost"), "UNTRUSTED");
190 }
191
192 #[test]
193 fn add_agent_card_pin_defaults_untrusted() {
194 let (sk, pk) = generate_keypair();
195 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
196 let mut t = empty_trust();
197 add_agent_card_pin(&mut t, &card, None);
198 assert_eq!(get_tier(&t, "paul"), "UNTRUSTED");
199 let did = t["agents"]["paul"]["did"].as_str().unwrap();
201 assert!(did.starts_with("did:wire:paul-"), "got: {did}");
202 }
203
204 #[test]
205 fn add_pin_strips_ed25519_prefix_from_key_id() {
206 let (sk, pk) = generate_keypair();
207 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
208 let mut t = empty_trust();
209 add_agent_card_pin(&mut t, &card, None);
210 let kid = t["agents"]["paul"]["public_keys"][0]["key_id"]
211 .as_str()
212 .unwrap();
213 assert!(kid.contains(':'));
214 assert!(!kid.starts_with("ed25519:"));
215 }
216
217 #[test]
218 fn promote_to_verified_one_way() {
219 let (sk, pk) = generate_keypair();
220 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
221 let mut t = empty_trust();
222 add_agent_card_pin(&mut t, &card, None);
223 promote_to_verified(&mut t, "paul").unwrap();
224 assert_eq!(get_tier(&t, "paul"), "VERIFIED");
225 assert!(t["agents"]["paul"]["verified_at"].is_string());
226 }
227
228 #[test]
229 fn promote_to_verified_idempotent_block() {
230 let (sk, pk) = generate_keypair();
231 let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
232 let mut t = empty_trust();
233 add_agent_card_pin(&mut t, &card, None);
234 promote_to_verified(&mut t, "paul").unwrap();
235 let err = promote_to_verified(&mut t, "paul").unwrap_err();
236 assert!(err.contains("VERIFIED"), "got: {err}");
237 }
238
239 #[test]
240 fn promote_unknown_peer_fails() {
241 let mut t = empty_trust();
242 let err = promote_to_verified(&mut t, "ghost").unwrap_err();
243 assert!(err.contains("not pinned"), "got: {err}");
244 }
245
246 #[test]
247 fn add_self_to_trust_attests() {
248 let (_, pk) = generate_keypair();
249 let mut t = empty_trust();
250 add_self_to_trust(&mut t, "paul", &pk);
251 assert_eq!(get_tier(&t, "paul"), "ATTESTED");
252 let did = t["agents"]["paul"]["did"].as_str().unwrap();
253 assert!(did.starts_with("did:wire:paul-"), "got: {did}");
254 }
255
256 #[test]
257 fn tier_order_matches_promotion_semantics() {
258 let order = tier_order();
259 assert!(order["UNTRUSTED"] < order["VERIFIED"]);
260 assert!(order["VERIFIED"] < order["ATTESTED"]);
261 assert!(order["ATTESTED"] < order["TRUSTED"]);
262 }
263}