Skip to main content

wire/
trust.rs

1//! Trust state machine — v0.1 minimal subset, extended in v3.2 (RFC-001).
2//!
3//! Tier semantics:
4//!   - UNTRUSTED: card pinned, no claim verified yet; messages ignored.
5//!   - ORG_VERIFIED: (v3.2 / RFC-001 §5) peer shares a verified `org_did`
6//!     with us — *organisational* trust, NOT personal. Bilateral SAS is
7//!     still required to cross into VERIFIED. Promotion from UNTRUSTED is
8//!     one-way.
9//!   - VERIFIED: SAS confirmed bilateral; messages accepted. Promotion
10//!     accepts UNTRUSTED-or-ORG_VERIFIED as source (RFC-001 §5: "a
11//!     SAS-paired peer that happens to share our org is recorded at
12//!     VERIFIED, not downgraded").
13//!   - ATTESTED: reserved (v0.2+) — used today only for self-attest.
14//!   - TRUSTED: reserved (v0.2+).
15//!
16//! Promotion is one-way. Demotion would be ambiguous in a bilateral setting
17//! and is deliberately not modeled. RFC-001 §5 invariant:
18//!   "ORG_VERIFIED never satisfies a `>= VERIFIED` policy check."
19//! That invariant is captured by `tier_order` (ORG_VERIFIED=1 < VERIFIED=2)
20//! and by AC2 property test (tests/trust_ceiling_prop.rs) asserting no
21//! claim-event walk reaches VERIFIED without a SasConfirmed step.
22
23use 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
30/// Tier ranking — higher is more trusted. Useful for `>=` gating.
31///
32/// RFC-001 §5 invariant: ORG_VERIFIED sits strictly between UNTRUSTED and
33/// VERIFIED. A policy check of `tier >= VERIFIED` MUST NOT pass for an
34/// ORG_VERIFIED peer — only an explicit SAS-confirmation can cross that line.
35pub 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
68/// Trust state — kept as a free-form JSON Value so we can persist + read with
69/// any conforming impl. v0.2+ may swap this for a typed struct.
70pub 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
86/// Pin a peer's card into our trust at the given tier (default UNTRUSTED).
87///
88/// The caller must independently run SAS confirmation (via `compute_sas`)
89/// before calling `promote_to_verified`. Pinning alone DOES NOT verify.
90pub fn add_agent_card_pin(trust: &mut Trust, card: &Value, tier: Option<&str>) {
91    let did = card.get("did").and_then(Value::as_str).unwrap_or_default();
92    // v0.5.7+: prefer the explicit `handle` field on the card (display name).
93    // Fall back to stripping the DID prefix for legacy cards. For v0.5.7+
94    // pubkey-suffixed DIDs (`did:wire:paul-abc12345`), the display_handle
95    // helper strips the pubkey suffix back off.
96    let handle = card
97        .get("handle")
98        .and_then(Value::as_str)
99        .map(str::to_string)
100        .unwrap_or_else(|| crate::agent_card::display_handle_from_did(did).to_string());
101    if handle.is_empty() {
102        panic!("card has no resolvable handle (did={did:?})");
103    }
104    let tier = tier.unwrap_or("UNTRUSTED");
105    let now = now_iso();
106
107    let mut public_keys = Vec::new();
108    if let Some(vks) = card.get("verify_keys").and_then(Value::as_object) {
109        for (key_id_full, key_record) in vks {
110            // Strip the `ed25519:` algorithm prefix to match v3.1 trust.json shape.
111            let key_id = key_id_full.strip_prefix("ed25519:").unwrap_or(key_id_full);
112            public_keys.push(json!({
113                "key_id": key_id,
114                "key": key_record.get("key").cloned().unwrap_or(Value::Null),
115                "added_at": now,
116                "active": true,
117            }));
118        }
119    }
120
121    let agents = trust
122        .as_object_mut()
123        .expect("trust must be an object")
124        .entry("agents")
125        .or_insert_with(|| json!({}));
126
127    agents[handle] = json!({
128        "tier": tier,
129        "did": did,
130        "public_keys": public_keys,
131        "card": card.clone(),
132        "pinned_at": now,
133    });
134}
135
136/// Promote UNTRUSTED or ORG_VERIFIED → VERIFIED. Returns `Err(reason)` if
137/// not pinned or already past VERIFIED.
138///
139/// RFC-001 §5: a SAS-confirmed peer that happens to share our org is
140/// recorded at VERIFIED, not downgraded — so ORG_VERIFIED is an accepted
141/// source for VERIFIED promotion. ATTESTED and TRUSTED are above VERIFIED
142/// and would be a downgrade; we refuse.
143pub fn promote_to_verified(trust: &mut Trust, peer_handle: &str) -> Result<(), String> {
144    let agents = trust
145        .as_object_mut()
146        .ok_or("trust is not an object")?
147        .get_mut("agents")
148        .and_then(Value::as_object_mut)
149        .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
150
151    let agent = agents
152        .get_mut(peer_handle)
153        .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
154
155    let current = agent
156        .get("tier")
157        .and_then(Value::as_str)
158        .unwrap_or("UNTRUSTED")
159        .to_string();
160    if current != "UNTRUSTED" && current != "ORG_VERIFIED" {
161        return Err(format!(
162            "peer {peer_handle:?} already at tier {current:?} — promotion is one-way"
163        ));
164    }
165    agent["tier"] = json!("VERIFIED");
166    agent["verified_at"] = json!(now_iso());
167    Ok(())
168}
169
170/// Promote UNTRUSTED → ORG_VERIFIED. Returns `Err(reason)` if not pinned or
171/// already past UNTRUSTED.
172///
173/// RFC-001 §5: ORG_VERIFIED is granted on cryptographic + policy grounds
174/// (the peer's `member_cert` for an org we accept verifies against that
175/// org's pubkey) but DOES NOT satisfy the SAS-confirmation ceremony that
176/// VERIFIED requires. It is a one-way intermediate step a peer may cross
177/// before or after VERIFIED, but never *instead of* VERIFIED.
178///
179/// This function does NOT perform the cryptographic verification of
180/// `member_cert` — that lives in [`crate::identity::verify_member_cert`]
181/// and the caller must run it first. The trust mutation here is the policy
182/// recording: "we accept this peer as ORG_VERIFIED under our active org
183/// policy."
184pub fn promote_to_org_verified(trust: &mut Trust, peer_handle: &str) -> Result<(), String> {
185    let agents = trust
186        .as_object_mut()
187        .ok_or("trust is not an object")?
188        .get_mut("agents")
189        .and_then(Value::as_object_mut)
190        .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
191
192    let agent = agents
193        .get_mut(peer_handle)
194        .ok_or_else(|| format!("peer {peer_handle:?} not pinned"))?;
195
196    let current = agent
197        .get("tier")
198        .and_then(Value::as_str)
199        .unwrap_or("UNTRUSTED")
200        .to_string();
201    if current != "UNTRUSTED" {
202        return Err(format!(
203            "peer {peer_handle:?} already at tier {current:?} — \
204             org_verified promotion fires from UNTRUSTED only"
205        ));
206    }
207    agent["tier"] = json!("ORG_VERIFIED");
208    agent["org_verified_at"] = json!(now_iso());
209    Ok(())
210}
211
212/// Self-pin our own keypair into trust at ATTESTED. Convenience for `wire init`.
213pub fn add_self_to_trust(trust: &mut Trust, handle: &str, public_key: &[u8]) {
214    let agents = trust
215        .as_object_mut()
216        .expect("trust must be an object")
217        .entry("agents")
218        .or_insert_with(|| json!({}));
219    let key_id = make_key_id(handle, public_key);
220    agents[handle] = json!({
221        "tier": "ATTESTED",
222        "did": crate::agent_card::did_for_with_key(handle, public_key),
223        "public_keys": [{
224            "key_id": key_id,
225            "key": b64encode(public_key),
226            "added_at": now_iso(),
227            "active": true,
228        }],
229    });
230}
231
232fn now_iso() -> String {
233    let now = OffsetDateTime::now_utc();
234    now.format(&Rfc3339)
235        .unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::agent_card::{build_agent_card, sign_agent_card};
242    use crate::signing::generate_keypair;
243
244    #[test]
245    fn empty_trust_shape() {
246        let t = empty_trust();
247        assert_eq!(t["version"], 1);
248        assert!(t["agents"].is_object());
249        assert_eq!(t["agents"].as_object().unwrap().len(), 0);
250    }
251
252    #[test]
253    fn get_tier_unknown_returns_untrusted() {
254        assert_eq!(get_tier(&empty_trust(), "ghost"), "UNTRUSTED");
255    }
256
257    #[test]
258    fn add_agent_card_pin_defaults_untrusted() {
259        let (sk, pk) = generate_keypair();
260        let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
261        let mut t = empty_trust();
262        add_agent_card_pin(&mut t, &card, None);
263        assert_eq!(get_tier(&t, "paul"), "UNTRUSTED");
264        // v0.5.7+: DID is pubkey-suffixed.
265        let did = t["agents"]["paul"]["did"].as_str().unwrap();
266        assert!(did.starts_with("did:wire:paul-"), "got: {did}");
267    }
268
269    #[test]
270    fn add_pin_strips_ed25519_prefix_from_key_id() {
271        let (sk, pk) = generate_keypair();
272        let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
273        let mut t = empty_trust();
274        add_agent_card_pin(&mut t, &card, None);
275        let kid = t["agents"]["paul"]["public_keys"][0]["key_id"]
276            .as_str()
277            .unwrap();
278        assert!(kid.contains(':'));
279        assert!(!kid.starts_with("ed25519:"));
280    }
281
282    #[test]
283    fn promote_to_verified_one_way() {
284        let (sk, pk) = generate_keypair();
285        let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
286        let mut t = empty_trust();
287        add_agent_card_pin(&mut t, &card, None);
288        promote_to_verified(&mut t, "paul").unwrap();
289        assert_eq!(get_tier(&t, "paul"), "VERIFIED");
290        assert!(t["agents"]["paul"]["verified_at"].is_string());
291    }
292
293    #[test]
294    fn promote_to_verified_idempotent_block() {
295        let (sk, pk) = generate_keypair();
296        let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
297        let mut t = empty_trust();
298        add_agent_card_pin(&mut t, &card, None);
299        promote_to_verified(&mut t, "paul").unwrap();
300        let err = promote_to_verified(&mut t, "paul").unwrap_err();
301        assert!(err.contains("VERIFIED"), "got: {err}");
302    }
303
304    #[test]
305    fn promote_unknown_peer_fails() {
306        let mut t = empty_trust();
307        let err = promote_to_verified(&mut t, "ghost").unwrap_err();
308        assert!(err.contains("not pinned"), "got: {err}");
309    }
310
311    #[test]
312    fn add_self_to_trust_attests() {
313        let (_, pk) = generate_keypair();
314        let mut t = empty_trust();
315        add_self_to_trust(&mut t, "paul", &pk);
316        assert_eq!(get_tier(&t, "paul"), "ATTESTED");
317        let did = t["agents"]["paul"]["did"].as_str().unwrap();
318        assert!(did.starts_with("did:wire:paul-"), "got: {did}");
319    }
320
321    #[test]
322    fn tier_order_matches_promotion_semantics() {
323        let order = tier_order();
324        assert!(order["UNTRUSTED"] < order["ORG_VERIFIED"]);
325        assert!(order["ORG_VERIFIED"] < order["VERIFIED"]);
326        assert!(order["VERIFIED"] < order["ATTESTED"]);
327        assert!(order["ATTESTED"] < order["TRUSTED"]);
328    }
329
330    // ─── RFC-001 §5: Tier::OrgVerified ────────────────────────────────────
331
332    #[test]
333    fn tier_as_str_covers_org_verified() {
334        assert_eq!(Tier::OrgVerified.as_str(), "ORG_VERIFIED");
335    }
336
337    #[test]
338    fn promote_to_org_verified_one_way() {
339        let (sk, pk) = generate_keypair();
340        let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
341        let mut t = empty_trust();
342        add_agent_card_pin(&mut t, &card, None);
343        promote_to_org_verified(&mut t, "paul").unwrap();
344        assert_eq!(get_tier(&t, "paul"), "ORG_VERIFIED");
345        assert!(t["agents"]["paul"]["org_verified_at"].is_string());
346    }
347
348    #[test]
349    fn promote_to_org_verified_refuses_already_verified() {
350        // Once a peer is VERIFIED (bilateral SAS), regressing them to
351        // ORG_VERIFIED would be a downgrade. Refuse.
352        let (sk, pk) = generate_keypair();
353        let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
354        let mut t = empty_trust();
355        add_agent_card_pin(&mut t, &card, None);
356        promote_to_verified(&mut t, "paul").unwrap();
357        let err = promote_to_org_verified(&mut t, "paul").unwrap_err();
358        assert!(err.contains("VERIFIED"), "got: {err}");
359        assert_eq!(get_tier(&t, "paul"), "VERIFIED");
360    }
361
362    #[test]
363    fn promote_to_org_verified_refuses_self_idempotent() {
364        // Twice-applied org promotion is a no-op error, not a silent reset
365        // of `org_verified_at` — keeps the audit trail intact.
366        let (sk, pk) = generate_keypair();
367        let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
368        let mut t = empty_trust();
369        add_agent_card_pin(&mut t, &card, None);
370        promote_to_org_verified(&mut t, "paul").unwrap();
371        let err = promote_to_org_verified(&mut t, "paul").unwrap_err();
372        assert!(err.contains("ORG_VERIFIED"), "got: {err}");
373    }
374
375    #[test]
376    fn promote_to_verified_accepts_org_verified_source() {
377        // RFC-001 §5: a peer can be ORG_VERIFIED then later cross the SAS
378        // ceremony into VERIFIED — without losing the cryptographic
379        // membership claim. We preserve `org_verified_at` for audit.
380        let (sk, pk) = generate_keypair();
381        let card = sign_agent_card(&build_agent_card("paul", &pk, None, None, None), &sk);
382        let mut t = empty_trust();
383        add_agent_card_pin(&mut t, &card, None);
384        promote_to_org_verified(&mut t, "paul").unwrap();
385        promote_to_verified(&mut t, "paul").unwrap();
386        assert_eq!(get_tier(&t, "paul"), "VERIFIED");
387        assert!(t["agents"]["paul"]["org_verified_at"].is_string());
388        assert!(t["agents"]["paul"]["verified_at"].is_string());
389    }
390
391    #[test]
392    fn promote_to_verified_refuses_attested_source() {
393        // ATTESTED is reserved-but-above VERIFIED; a downgrade would lose
394        // information. Refuse.
395        let (_, pk) = generate_keypair();
396        let mut t = empty_trust();
397        add_self_to_trust(&mut t, "self", &pk);
398        let err = promote_to_verified(&mut t, "self").unwrap_err();
399        assert!(err.contains("ATTESTED"), "got: {err}");
400    }
401
402    #[test]
403    fn org_verified_does_not_satisfy_verified_policy_check() {
404        // The load-bearing RFC-001 invariant: a policy gate of
405        // `tier >= VERIFIED` MUST refuse an ORG_VERIFIED peer.
406        let order = tier_order();
407        let verified_rank = order["VERIFIED"];
408        let org_rank = order["ORG_VERIFIED"];
409        assert!(
410            org_rank < verified_rank,
411            "ORG_VERIFIED ({org_rank}) must rank strictly below VERIFIED ({verified_rank})"
412        );
413    }
414}