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, ®istry, &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, ®istry, 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, ®istry, &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}