Skip to main content

wire/
trust.rs

1//! Trust state machine — v0.1 minimal subset.
2//!
3//! Tier semantics:
4//!   - UNTRUSTED: card pinned, SAS not yet confirmed; messages ignored.
5//!   - VERIFIED:  SAS confirmed bilateral; messages accepted.
6//!   - ATTESTED:  reserved (v0.2+) — used today only for self-attest.
7//!   - TRUSTED:   reserved (v0.2+).
8//!
9//! Promotion is one-way (UNTRUSTED → VERIFIED). Demotion would be
10//! ambiguous in a bilateral setting and is deliberately not modeled.
11
12use 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
19/// Tier ranking — higher is more trusted. Useful for `>=` gating.
20pub 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
50/// Trust state — kept as a free-form JSON Value so we can persist + read with
51/// any conforming impl. v0.2+ may swap this for a typed struct.
52pub 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
68/// Pin a peer's card into our trust at the given tier (default UNTRUSTED).
69///
70/// The caller must independently run SAS confirmation (via `compute_sas`)
71/// before calling `promote_to_verified`. Pinning alone DOES NOT verify.
72pub 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    // v0.5.7+: prefer the explicit `handle` field on the card (display name).
75    // Fall back to stripping the DID prefix for legacy cards. For v0.5.7+
76    // pubkey-suffixed DIDs (`did:wire:paul-abc12345`), the display_handle
77    // helper strips the pubkey suffix back off.
78    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            // Strip the `ed25519:` algorithm prefix to match v3.1 trust.json shape.
93            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
118/// Promote UNTRUSTED → VERIFIED. Returns `Err(reason)` if not pinned or
119/// already past UNTRUSTED (promotion is one-way).
120pub 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
147/// Self-pin our own keypair into trust at ATTESTED. Convenience for `wire init`.
148pub 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        // v0.5.7+: DID is pubkey-suffixed.
200        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}