Skip to main content

auths_id/keri/
credential_registry.rs

1//! Credential registry — backerless TEL persistence anchored to the issuer KEL.
2//!
3//! A credential registry is a KERI-native, backerless (`NB`) Transaction Event Log
4//! (TEL). It derives all of its trust from the issuer's KEL: every TEL event
5//! (`vcp` registry inception, `iss` issuance, `rev` revocation) is anchored by an
6//! `ixn` in the issuer KEL carrying a [`TelAnchorSeal`]-shaped `{i, s, d}` key-event
7//! seal. The issuer is single-author, so anchoring reuses the same single-signature
8//! `ixn` machinery devices and org members use ([`stage_root_anchor_ixn`]).
9//!
10//! ## Atomicity
11//!
12//! A TEL event and its KEL anchor (and, for an `iss`, the ACDC blob) MUST land in
13//! one commit — see the `RegistryBackend` TEL doc-block. [`anchor_tel_event`] stages
14//! all of them into one [`AtomicWriteBatch`] and commits once, so a crash never
15//! leaves an anchored-but-absent TEL event or a TEL event the KEL never anchored.
16//!
17//! ## Single-signature only
18//!
19//! [`stage_root_anchor_ixn`] signs with one key, so the issuer must be `kt=1`. A
20//! `kt≥2` issuer is rejected with [`CredentialRegistryError::ThresholdUnsupported`];
21//! multi-sig registry anchoring is a tracked follow-up (mirrors the org delegator).
22
23use auths_core::error::AuthsErrorInfo;
24use auths_core::signing::PassphraseProvider;
25use auths_core::storage::keychain::{KeyAlias, KeyStorage};
26use auths_crypto::CurveType;
27use auths_keri::{Iss, Rev, TelAnchorSeal, TelEvent, Vcp, encode_tel_nonce, tel_to_wire_bytes};
28use rand::Rng;
29
30use crate::error::InitError;
31use crate::keri::delegation::stage_root_anchor_ixn;
32use crate::keri::{KeriSequence, Prefix, Said, Seal};
33use crate::storage::registry::backend::{AtomicWriteBatch, RegistryBackend, RegistryError};
34
35/// Errors raised while inceptioning, anchoring, or reading a credential registry.
36#[derive(Debug, thiserror::Error)]
37#[non_exhaustive]
38pub enum CredentialRegistryError {
39    /// The issuer's KEL is `kt≥2` — single-signature anchoring only.
40    #[error(
41        "issuer '{issuer}' is multi-signature (kt≥2); credential registry anchoring is single-author only"
42    )]
43    ThresholdUnsupported {
44        /// The offending issuer's KEL prefix.
45        issuer: String,
46    },
47
48    /// A TEL or ACDC type failed to build, SAID, or serialize.
49    #[error("TEL event error: {0}")]
50    Tel(String),
51
52    /// Authoring or committing the anchoring `ixn` failed.
53    #[error("KEL anchoring failed: {0}")]
54    Anchor(#[from] InitError),
55
56    /// A registry-backend storage operation failed.
57    #[error("registry storage error: {0}")]
58    Storage(#[from] RegistryError),
59}
60
61impl AuthsErrorInfo for CredentialRegistryError {
62    fn error_code(&self) -> &'static str {
63        match self {
64            Self::ThresholdUnsupported { .. } => "AUTHS-E4981",
65            Self::Tel(_) => "AUTHS-E4982",
66            Self::Anchor(_) => "AUTHS-E4983",
67            Self::Storage(_) => "AUTHS-E4984",
68        }
69    }
70
71    fn suggestion(&self) -> Option<&'static str> {
72        match self {
73            Self::ThresholdUnsupported { .. } => {
74                Some("Credential issuance currently requires a single-signature (kt=1) issuer")
75            }
76            _ => None,
77        }
78    }
79}
80
81/// Reject a `kt≥2` (multi-signature) issuer — the anchoring `ixn` is single-author.
82///
83/// `kt=1` issuers (the documented pre-launch baseline) pass; mirrors the org
84/// delegator's `ensure_single_sig` guard.
85fn ensure_single_sig(
86    backend: &(dyn RegistryBackend + Send + Sync),
87    issuer: &Prefix,
88) -> Result<(), CredentialRegistryError> {
89    let state = backend.get_key_state(issuer)?;
90    if state.threshold.simple_value() == Some(1) {
91        Ok(())
92    } else {
93        Err(CredentialRegistryError::ThresholdUnsupported {
94            issuer: issuer.as_str().to_string(),
95        })
96    }
97}
98
99/// The `(registry_said, credential_said, seal_aid, sn, event_said)` coordinate of a TEL event.
100///
101/// - `registry_said`/`credential_said` are the storage keys (for a `vcp` the
102///   credential segment is the registry SAID itself, self-addressing).
103/// - `seal_aid` is the AID the KEL anchor seal's `i` carries — the TEL event's own
104///   `i` field: the registry SAID for a `vcp`, the credential SAID for an `iss`/`rev`
105///   (matching keripy's `SealEvent(i=tev.pre, …)`).
106fn tel_coordinate(event: &TelEvent) -> (Said, Said, Said, u128, Said) {
107    match event {
108        TelEvent::Vcp(vcp) => (
109            vcp.d.clone(),
110            vcp.d.clone(),
111            vcp.i.clone(),
112            vcp.s.value(),
113            vcp.d.clone(),
114        ),
115        TelEvent::Iss(iss) => (
116            iss.ri.clone(),
117            iss.i.clone(),
118            iss.i.clone(),
119            iss.s.value(),
120            iss.d.clone(),
121        ),
122        TelEvent::Rev(rev) => (
123            rev.ri.clone(),
124            rev.i.clone(),
125            rev.i.clone(),
126            rev.s.value(),
127            rev.d.clone(),
128        ),
129    }
130}
131
132/// Canonical insertion-order JSON bytes of a TEL event.
133fn tel_event_bytes(event: &TelEvent) -> Result<Vec<u8>, CredentialRegistryError> {
134    let bytes = match event {
135        TelEvent::Vcp(vcp) => tel_to_wire_bytes(vcp),
136        TelEvent::Iss(iss) => tel_to_wire_bytes(iss),
137        TelEvent::Rev(rev) => tel_to_wire_bytes(rev),
138    };
139    bytes.map_err(|e| CredentialRegistryError::Tel(e.to_string()))
140}
141
142/// Anchor a single TEL event in the issuer KEL and persist it atomically.
143///
144/// Authors a single-author `ixn` on the issuer's KEL carrying the TEL anchor
145/// [`Seal::KeyEvent`] (`{i, s, d}` — the registry/credential AID, the TEL event
146/// sequence, and the TEL event SAID), and commits it **in one batch** with the TEL
147/// event blob (and the optional ACDC credential blob). Reuses
148/// [`stage_root_anchor_ixn`] for the ixn authoring — the ixn is staged, not
149/// committed, so it lands atomically with the TEL writes.
150///
151/// Rejects a `kt≥2` issuer ([`CredentialRegistryError::ThresholdUnsupported`]).
152///
153/// Args:
154/// * `backend`: Registry holding the issuer KEL and the TEL.
155/// * `issuer_prefix`: The issuer's KEL prefix (the anchoring controller).
156/// * `issuer_alias`: Keychain alias of the issuer's current signing key.
157/// * `issuer_curve`: Curve of the issuer's current key.
158/// * `tel_event`: The TEL event (`vcp`/`iss`/`rev`) to anchor and persist.
159/// * `credential_blob`: Optional ACDC blob (`(credential_said, bytes)`) committed
160///   atomically alongside the TEL event — supplied for an `iss`.
161/// * `passphrase_provider`: Passphrase source for the issuer key.
162/// * `keychain`: Key storage (the issuer's signing key).
163///
164/// Usage:
165/// ```ignore
166/// anchor_tel_event(backend, &issuer, &alias, curve, &TelEvent::Iss(iss),
167///     Some((acdc.d.clone(), acdc.to_wire_bytes()?)), &provider, &keychain)?;
168/// ```
169#[allow(clippy::too_many_arguments)]
170pub fn anchor_tel_event(
171    backend: &(dyn RegistryBackend + Send + Sync),
172    issuer_prefix: &Prefix,
173    issuer_alias: &KeyAlias,
174    issuer_curve: CurveType,
175    tel_event: &TelEvent,
176    credential_blob: Option<(Said, Vec<u8>)>,
177    passphrase_provider: &dyn PassphraseProvider,
178    keychain: &(dyn KeyStorage + Send + Sync),
179) -> Result<(), CredentialRegistryError> {
180    ensure_single_sig(backend, issuer_prefix)?;
181
182    let (registry_said, credential_said, seal_aid, sn, event_said) = tel_coordinate(tel_event);
183    let event_bytes = tel_event_bytes(tel_event)?;
184
185    // The TEL anchor seal's `i` is the TEL event's own AID (the registry SAID for a
186    // `vcp`, the credential SAID for an `iss`/`rev`) — keripy's `SealEvent(i=tev.pre)`.
187    let seal = Seal::KeyEvent {
188        i: Prefix::new_unchecked(seal_aid.as_str().to_string()),
189        s: KeriSequence::new(sn),
190        d: event_said,
191    };
192
193    let mut batch = AtomicWriteBatch::new();
194    stage_root_anchor_ixn(
195        backend,
196        issuer_prefix,
197        issuer_alias,
198        issuer_curve,
199        vec![seal],
200        passphrase_provider,
201        keychain,
202        &mut batch,
203    )?;
204    batch.stage_tel_event(
205        issuer_prefix.clone(),
206        registry_said.clone(),
207        credential_said.clone(),
208        sn,
209        event_bytes,
210    );
211    if let Some((cred_said, cred_bytes)) = credential_blob {
212        batch.stage_credential(issuer_prefix.clone(), cred_said, cred_bytes);
213    }
214
215    backend.commit_batch(&batch)?;
216    Ok(())
217}
218
219/// The registry SAID a TEL anchor seal carries, validated against its KERI shape.
220///
221/// Reads the issuer KEL `ixn` anchors and confirms one carries the TEL event's
222/// `{i, s, d}` key-event seal. The verifier (Epic F.5) does the full cross-check;
223/// this is the persistence-side equivalent used by tests.
224///
225/// Args:
226/// * `seal`: A `TelAnchorSeal` to convert into the matching KEL seal shape.
227///
228/// Usage:
229/// ```ignore
230/// let kel_seal = anchor_seal_for(&TelAnchorSeal::for_event(reg, iss.s, iss.d));
231/// ```
232pub fn anchor_seal_for(seal: &TelAnchorSeal) -> Seal {
233    Seal::KeyEvent {
234        i: seal.i.clone(),
235        s: seal.s,
236        d: seal.d.clone(),
237    }
238}
239
240/// Lazily incept and anchor a backerless registry (`vcp`) for an issuer if absent.
241///
242/// Idempotent and one-registry-per-issuer: if the issuer already has an anchored
243/// registry (a `vcp` anchor in its KEL), the existing registry SAID is returned
244/// untouched. Otherwise a fresh backerless `vcp` is incepted (with a random
245/// 128-bit nonce so its registry SAID is unique) and anchored via [`anchor_tel_event`].
246///
247/// Rejects a `kt≥2` issuer ([`CredentialRegistryError::ThresholdUnsupported`]).
248///
249/// Args:
250/// * `backend`: Registry holding the issuer KEL and the TEL.
251/// * `issuer_prefix`: The issuer's KEL prefix.
252/// * `issuer_alias`: Keychain alias of the issuer's current signing key.
253/// * `issuer_curve`: Curve of the issuer's current key.
254/// * `passphrase_provider`: Passphrase source for the issuer key.
255/// * `keychain`: Key storage (the issuer's signing key).
256///
257/// Usage:
258/// ```ignore
259/// let registry = ensure_registry(backend, &issuer, &alias, curve, &provider, &keychain)?;
260/// ```
261pub fn ensure_registry(
262    backend: &(dyn RegistryBackend + Send + Sync),
263    issuer_prefix: &Prefix,
264    issuer_alias: &KeyAlias,
265    issuer_curve: CurveType,
266    passphrase_provider: &dyn PassphraseProvider,
267    keychain: &(dyn KeyStorage + Send + Sync),
268) -> Result<Said, CredentialRegistryError> {
269    ensure_single_sig(backend, issuer_prefix)?;
270
271    if let Some(existing) = find_registry(backend, issuer_prefix)? {
272        return Ok(existing);
273    }
274
275    let mut nonce_bytes = [0u8; 16];
276    rand::rng().fill_bytes(&mut nonce_bytes);
277    let nonce =
278        encode_tel_nonce(&nonce_bytes).map_err(|e| CredentialRegistryError::Tel(e.to_string()))?;
279
280    let vcp = Vcp::new(issuer_prefix.clone(), nonce)
281        .saidify()
282        .map_err(|e| CredentialRegistryError::Tel(e.to_string()))?;
283    let registry_said = vcp.registry().clone();
284
285    anchor_tel_event(
286        backend,
287        issuer_prefix,
288        issuer_alias,
289        issuer_curve,
290        &TelEvent::Vcp(vcp),
291        None,
292        passphrase_provider,
293        keychain,
294    )?;
295
296    Ok(registry_said)
297}
298
299/// Find the issuer's anchored registry SAID, if any (the lazy-incept idempotency check).
300///
301/// Walks the issuer KEL for an `ixn` carrying a `vcp` anchor — a `Seal::KeyEvent`
302/// at TEL sequence `0` whose registry SAID has a persisted `vcp` TEL event. Returns
303/// the first such registry SAID, or `None` if the issuer has no registry yet.
304///
305/// Args:
306/// * `backend`: Registry holding the issuer KEL and TEL.
307/// * `issuer_prefix`: The issuer's KEL prefix.
308///
309/// Usage:
310/// ```ignore
311/// let existing = find_registry(backend, &issuer)?;
312/// ```
313pub fn find_registry(
314    backend: &(dyn RegistryBackend + Send + Sync),
315    issuer_prefix: &Prefix,
316) -> Result<Option<Said>, CredentialRegistryError> {
317    use std::ops::ControlFlow;
318
319    let mut candidates: Vec<Said> = Vec::new();
320    backend.visit_events(issuer_prefix, 0, &mut |event| {
321        for seal in event.anchors() {
322            if let Seal::KeyEvent { i, s, .. } = seal
323                && s.value() == 0
324            {
325                candidates.push(Said::new_unchecked(i.as_str().to_string()));
326            }
327        }
328        ControlFlow::Continue(())
329    })?;
330
331    for candidate in candidates {
332        if registry_exists(backend, issuer_prefix, &candidate)? {
333            return Ok(Some(candidate));
334        }
335    }
336    Ok(None)
337}
338
339/// True if a `vcp` TEL event is persisted for `registry_said` under `issuer`.
340fn registry_exists(
341    backend: &(dyn RegistryBackend + Send + Sync),
342    issuer_prefix: &Prefix,
343    registry_said: &Said,
344) -> Result<bool, CredentialRegistryError> {
345    use std::ops::ControlFlow;
346
347    let mut found = false;
348    backend.visit_tel_events(issuer_prefix, registry_said, registry_said, &mut |_bytes| {
349        found = true;
350        ControlFlow::Break(())
351    })?;
352    Ok(found)
353}
354
355/// Read back a credential's TEL events in order (`vcp`-anchored chain replay input).
356///
357/// Returns the parsed `vcp` (always first, read from its own self-addressed slot)
358/// followed by the credential's `iss`/`rev` chain in ascending sequence order. The
359/// result is the exact ordered slice [`auths_keri::validate_tel`] consumes.
360///
361/// Args:
362/// * `backend`: Registry holding the TEL.
363/// * `issuer_prefix`: The issuer's KEL prefix.
364/// * `registry_said`: The registry SAID (`vcp.d`).
365/// * `credential_said`: The credential SAID to read the `iss`/`rev` chain for.
366///
367/// Usage:
368/// ```ignore
369/// let events = read_credential_tel(backend, &issuer, &registry, &credential)?;
370/// let state = auths_keri::validate_tel(&events)?;
371/// ```
372pub fn read_credential_tel(
373    backend: &(dyn RegistryBackend + Send + Sync),
374    issuer_prefix: &Prefix,
375    registry_said: &Said,
376    credential_said: &Said,
377) -> Result<Vec<TelEvent>, CredentialRegistryError> {
378    use std::ops::ControlFlow;
379
380    let mut events = Vec::new();
381    let mut parse_err: Option<String> = None;
382
383    let mut collect = |bytes: &[u8]| match TelEvent::from_wire_bytes(bytes) {
384        Ok(event) => {
385            events.push(event);
386            ControlFlow::Continue(())
387        }
388        Err(e) => {
389            parse_err = Some(e.to_string());
390            ControlFlow::Break(())
391        }
392    };
393
394    backend.visit_tel_events(issuer_prefix, registry_said, registry_said, &mut collect)?;
395    if credential_said != registry_said {
396        backend.visit_tel_events(issuer_prefix, registry_said, credential_said, &mut collect)?;
397    }
398
399    if let Some(detail) = parse_err {
400        return Err(CredentialRegistryError::Tel(detail));
401    }
402    Ok(events)
403}
404
405/// Build a SAID'd backerless `iss` issuance event for a credential.
406///
407/// A thin, errors-mapped wrapper over [`Iss::new`]+`saidify` so callers in this
408/// crate don't depend on `auths_keri` directly.
409///
410/// Args:
411/// * `credential_said`: The credential SAID being issued.
412/// * `registry_said`: The registry SAID the issuance belongs to.
413/// * `dt`: ISO-8601 issuance datetime (informational; injected by the caller).
414///
415/// Usage:
416/// ```ignore
417/// let iss = build_iss(&acdc.d, &registry, dt.to_rfc3339())?;
418/// ```
419pub fn build_iss(
420    credential_said: &Said,
421    registry_said: &Said,
422    dt: String,
423) -> Result<Iss, CredentialRegistryError> {
424    Iss::new(credential_said.clone(), registry_said.clone(), dt)
425        .saidify()
426        .map_err(|e| CredentialRegistryError::Tel(e.to_string()))
427}
428
429/// Build a SAID'd backerless `rev` revocation event for a credential.
430///
431/// Args:
432/// * `credential_said`: The credential SAID being revoked.
433/// * `registry_said`: The registry SAID the revocation belongs to.
434/// * `prior_iss_said`: The prior `iss` event SAID (the chain back-link `p`).
435/// * `dt`: ISO-8601 revocation datetime (informational; injected by the caller).
436///
437/// Usage:
438/// ```ignore
439/// let rev = build_rev(&acdc.d, &registry, &iss.d, dt.to_rfc3339())?;
440/// ```
441pub fn build_rev(
442    credential_said: &Said,
443    registry_said: &Said,
444    prior_iss_said: &Said,
445    dt: String,
446) -> Result<Rev, CredentialRegistryError> {
447    Rev::new(
448        credential_said.clone(),
449        registry_said.clone(),
450        prior_iss_said.clone(),
451        dt,
452    )
453    .saidify()
454    .map_err(|e| CredentialRegistryError::Tel(e.to_string()))
455}