Skip to main content

acdp_client/
trust.rs

1//! Registry trust lists online (RFC-ACDP-0020, 0.5.0 Draft): the
2//! consumer profile `acdp-consumer-trust-list` over [`VerifiedContext`].
3//!
4//! [`TrustLists`] holds the consumer's issuer pins, every list version
5//! that passed §5.2, and the per-pin floors to persist (§5.3). Feed it
6//! verified list bodies — fetched with [`VerifiedContext::fetch`] (whose
7//! policy enforces RFC-ACDP-0014 revocation for the issuer key) or
8//! carried out of band — then evaluate verified bodies against it.
9//!
10//! ```no_run
11//! # async fn demo(client: &acdp_client::RegistryClient, resolver: &acdp_did::WebResolver,
12//! #     list_ctx: &acdp_types::primitives::CtxId, body_ctx: &acdp_types::primitives::CtxId)
13//! # -> Result<(), Box<dyn std::error::Error>> {
14//! use acdp_client::trust::TrustLists;
15//! use acdp_client::VerifiedContext;
16//! use acdp_types::trust_list::IssuerPin;
17//!
18//! let mut lists = TrustLists::new(vec![IssuerPin::new(
19//!     "did:web:security.acme.example",
20//!     "tenant-acme.production",
21//! )]); // .with_floors(persisted)
22//! let list = VerifiedContext::fetch(client, resolver, list_ctx).await?;
23//! lists.add_verified(&list, resolver).await?;
24//! let body = VerifiedContext::fetch(client, resolver, body_ctx).await?;
25//! let node = lists.evaluate(&body, chrono::Utc::now(), None);
26//! if !node.is_trusted() { /* refuse: §6.2, only `trusted` permits reliance */ }
27//! // persist lists.floors()
28//! # Ok(()) }
29//! ```
30//!
31//! Selection here is the latest pass. Lists carried in an RFC-ACDP-0017
32//! bundle, with as-of DID documents and admitted retractions, go through
33//! `acdp_chain::verify_chain_with_trust` instead.
34
35use std::collections::BTreeMap;
36
37use acdp_did::WebResolver;
38use acdp_primitives::error::AcdpError;
39use acdp_types::lifecycle::retraction_state;
40use acdp_types::trust_list::{
41    advance_floor, evaluate, select_version, validate_list_version, IssuerPin, ListRejected,
42    ListVersion, NodeTrust, PinState, ReceiptInput, TrustPass, TrustReport, TrustSubject,
43    VerifiedListBody,
44};
45use chrono::{DateTime, Utc};
46use serde_json::Value;
47
48use crate::revocation_verdict::RevocationVerdict;
49use crate::VerifiedContext;
50
51/// A pin's series key: (`issuer`, `list_id`).
52pub type Series = (String, String);
53
54/// The consumer's trust-list state: pins, held versions, floors.
55#[derive(Debug, Clone, Default)]
56pub struct TrustLists {
57    pins: Vec<IssuerPin>,
58    floors: BTreeMap<Series, u64>,
59    versions: Vec<Vec<ListVersion>>,
60    refuse_unknown_issuer_revocation: bool,
61}
62
63fn series(pin: &IssuerPin) -> Series {
64    (pin.issuer.clone(), pin.list_id.clone())
65}
66
67fn rejected(step: u8, e: AcdpError) -> ListRejected {
68    ListRejected {
69        step,
70        reason: e.to_string(),
71    }
72}
73
74impl TrustLists {
75    /// Trust lists for `pins` (§5.1: from configuration only).
76    pub fn new(pins: Vec<IssuerPin>) -> Self {
77        let versions = vec![Vec::new(); pins.len()];
78        Self {
79            pins,
80            floors: BTreeMap::new(),
81            versions,
82            refuse_unknown_issuer_revocation: false,
83        }
84    }
85
86    /// Local policy: also refuse, in [`Self::add_verified`], a list whose
87    /// issuer-key revocation status is unknown (an `Unknown` verdict, or no
88    /// complete RFC-ACDP-0014 discovery). RFC-ACDP-0020 §5.2 step 5 refuses only
89    /// on an applied revocation and allows this as local policy.
90    pub fn refuse_unknown_issuer_revocation(mut self, refuse: bool) -> Self {
91        self.refuse_unknown_issuer_revocation = refuse;
92        self
93    }
94
95    /// Restore the floors persisted by an earlier run (§5.3).
96    pub fn with_floors(mut self, floors: BTreeMap<Series, u64>) -> Self {
97        self.floors = floors;
98        self
99    }
100
101    /// The pins.
102    pub fn pins(&self) -> &[IssuerPin] {
103        &self.pins
104    }
105
106    /// The floors to persist (§5.3): per pin, the highest serial among
107    /// the valid versions held, retracted ones included. Never decreases.
108    pub fn floors(&self) -> &BTreeMap<Series, u64> {
109        &self.floors
110    }
111
112    /// Hold the version `check` validates for each pin, and advance
113    /// those pins' floors. Returns how many pins accepted it.
114    fn hold(
115        &mut self,
116        mut check: impl FnMut(&IssuerPin) -> Result<ListVersion, ListRejected>,
117    ) -> Result<usize, ListRejected> {
118        let mut accepted = 0;
119        let mut first_err = None;
120        for (pin, held) in self.pins.iter().zip(self.versions.iter_mut()) {
121            match check(pin) {
122                Ok(v) => {
123                    accepted += 1;
124                    let key = series(pin);
125                    if let Some(f) = advance_floor(self.floors.get(&key).copied(), [&v]) {
126                        self.floors.insert(key, f);
127                    }
128                    // A refetch of the same copy replaces the old one, so
129                    // a later retraction is seen.
130                    held.retain(|h| (&h.content_hash, &h.ctx_id) != (&v.content_hash, &v.ctx_id));
131                    held.push(v);
132                }
133                Err(e) => {
134                    first_err.get_or_insert(e);
135                }
136            }
137        }
138        match (accepted, first_err) {
139            (0, Some(e)) => Err(e),
140            (0, None) => Err(ListRejected {
141                step: 2,
142                reason: "no pins are configured".into(),
143            }),
144            (n, _) => Ok(n),
145        }
146    }
147
148    /// Hold a list version retrieved and verified by
149    /// [`VerifiedContext::fetch`] (§5.2 step 1, with RFC-ACDP-0014
150    /// revocation per the fetch policy).
151    ///
152    /// Retractions (§5.3) are read from the retrieved
153    /// `registry_state.lifecycle_events`: only events whose actor is the
154    /// issuer and that verify under the issuer's key count. A
155    /// registry-actor retraction never withdraws a list.
156    pub async fn add_verified(
157        &mut self,
158        list: &VerifiedContext,
159        resolver: &WebResolver,
160    ) -> Result<usize, ListRejected> {
161        let body = list.body();
162        let fingerprint = acdp_crypto::fingerprint::fingerprint_for_key_id(
163            &body.signature.key_id,
164            &body.signature.algorithm,
165            resolver,
166        )
167        .await
168        .map_err(|e| rejected(1, e))?;
169        let mut issuer_events = Vec::new();
170        for e in list.registry_state().lifecycle_events.iter().flatten() {
171            if e.actor != body.agent_id {
172                continue;
173            }
174            let Ok(raw) = serde_json::to_value(e) else {
175                continue;
176            };
177            if let Ok(ev) = acdp_verify::verify_lifecycle_event(
178                &raw,
179                &body.ctx_id,
180                &body.agent_id,
181                None,
182                resolver,
183            )
184            .await
185            {
186                issuer_events.push(ev);
187            }
188        }
189        // §5.2 step 5: only an applied revocation refuses; an incomplete
190        // discovery (unknown) refuses only under local policy.
191        let verdict = list.revocation_verdict();
192        let unknown = matches!(
193            verdict,
194            RevocationVerdict::Unknown { .. }
195                | RevocationVerdict::NotRevoked {
196                    complete: false,
197                    ..
198                }
199        );
200        let revoked = verdict.is_refusal() || (self.refuse_unknown_issuer_revocation && unknown);
201        let ty = context_type(&body.context_type);
202        let candidate = VerifiedListBody {
203            agent_id: body.agent_id.as_str(),
204            context_type: &ty,
205            metadata: body.metadata.as_ref(),
206            content_hash: body.content_hash.as_str(),
207            ctx_id: Some(body.ctx_id.as_str()),
208            issuer_key_fingerprint: Some(&fingerprint),
209            issuer_key_revoked: revoked,
210            retracted: retraction_state(&issuer_events),
211        };
212        self.hold(|pin| validate_list_version(pin, &candidate))
213    }
214
215    /// Hold a `did:key`-issued list carried out of band (§7): a retrieved
216    /// body or its signed publish-request form, with any issuer-signed
217    /// lifecycle events. `issuer_key_revoked` is the caller's
218    /// RFC-ACDP-0014 §7 verdict for the issuer key.
219    pub fn add_offline(
220        &mut self,
221        raw: &Value,
222        retractions: &[Value],
223        issuer_key_revoked: bool,
224    ) -> Result<usize, ListRejected> {
225        self.hold(|pin| {
226            acdp_verify::trust::verify_list_offline(raw, pin, retractions, issuer_key_revoked)
227        })
228    }
229
230    /// §5.4 latest-pass state of every pin at `t`, against the floors.
231    pub fn select(&self, t: DateTime<Utc>) -> Vec<PinState> {
232        self.pins
233            .iter()
234            .zip(&self.versions)
235            .map(|(pin, held)| {
236                select_version(
237                    pin,
238                    held,
239                    t,
240                    TrustPass::Latest,
241                    self.floors.get(&series(pin)).copied(),
242                )
243            })
244            .collect()
245    }
246
247    /// §6.2 for one verified body at `t`. The receipt input is the
248    /// verified receipt's registry key (never its `key_fingerprint`);
249    /// `capabilities` is C, the profiles the body's registry advertises,
250    /// when the consumer fetched them.
251    pub fn evaluate(
252        &self,
253        body: &VerifiedContext,
254        t: DateTime<Utc>,
255        capabilities: Option<&[String]>,
256    ) -> NodeTrust {
257        evaluate_against(&self.select(t), body, t, capabilities)
258    }
259
260    /// The §8 latest-pass report over `bodies` at `t`. `capabilities`
261    /// maps a lowercase registry origin to its advertised profiles.
262    pub fn report(
263        &self,
264        bodies: &[&VerifiedContext],
265        t: DateTime<Utc>,
266        capabilities: &BTreeMap<String, Vec<String>>,
267    ) -> TrustReport {
268        let states = self.select(t);
269        let nodes = bodies
270            .iter()
271            .map(|b| {
272                let caps = capabilities
273                    .get(&b.body().origin_registry.to_ascii_lowercase())
274                    .map(Vec::as_slice);
275                evaluate_against(&states, b, t, caps)
276            })
277            .collect();
278        TrustReport::new(TrustPass::Latest, t, &states, nodes, None, None)
279    }
280}
281
282fn context_type(t: &acdp_types::primitives::ContextType) -> String {
283    serde_json::to_value(t)
284        .ok()
285        .and_then(|v| v.as_str().map(str::to_string))
286        .unwrap_or_default()
287}
288
289fn evaluate_against(
290    states: &[PinState],
291    b: &VerifiedContext,
292    t: DateTime<Utc>,
293    capabilities: Option<&[String]>,
294) -> NodeTrust {
295    let body = b.body();
296    let receipt = b.verified_receipt().and_then(|r| {
297        Some(ReceiptInput {
298            registry_did: r.registry_did.clone(),
299            registry_key_fingerprint: b.receipt_registry_key_fingerprint()?.to_string(),
300        })
301    });
302    let ty = context_type(&body.context_type);
303    evaluate(
304        &TrustSubject::of(body, &ty),
305        states,
306        t,
307        receipt.as_ref(),
308        capabilities,
309    )
310}