Skip to main content

anp_identity/
registry.rs

1use std::collections::BTreeMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8use sha2::{Digest, Sha256};
9
10use crate::adoption::{
11    LocalAuthorizationRecord, PendingEnrollmentRecord, PendingRequestSigningRecord,
12};
13use crate::fs_util::{ensure_private_dir, write_atomic_private};
14use crate::keystore::SecretRef;
15use crate::lifecycle::PendingRevisionRecord;
16use crate::root_transfer::{PendingRootTransferRecord, RootTransferReplayRecord};
17use crate::store_lock::StoreWriteGuard;
18use crate::{Capabilities, DidError, DidResult, KeyRole};
19
20const REGISTRY_SCHEMA_VERSION: u32 = 1;
21const IDENTITY_SCHEMA_VERSION: u32 = 1;
22const JOURNAL_SCHEMA_VERSION: u32 = 1;
23const UPDATE_JOURNAL_SCHEMA_VERSION: u32 = 1;
24
25#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
26#[serde(rename_all = "snake_case")]
27pub enum KeyOrigin {
28    Managed,
29    External,
30}
31
32#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "snake_case")]
34pub enum KeyState {
35    Pending,
36    Active,
37    Retired,
38    Revoked,
39}
40
41#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(rename_all = "snake_case")]
43pub enum IdentityState {
44    Creating,
45    Enrolling,
46    Active,
47    Revoked,
48}
49
50#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(rename_all = "snake_case")]
52pub enum RootCapabilityState {
53    Absent,
54    Pending,
55    Active,
56}
57
58impl Default for RootCapabilityState {
59    fn default() -> Self {
60        Self::Active
61    }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65#[serde(deny_unknown_fields)]
66pub struct DocumentCheckpoint {
67    pub document_version: u64,
68    pub registry_version: u64,
69    pub document_digest: String,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
73#[serde(deny_unknown_fields)]
74pub struct KeyMetadata {
75    pub kid: String,
76    pub role: KeyRole,
77    pub origin: KeyOrigin,
78    pub state: KeyState,
79    #[serde(default)]
80    pub material_erased: bool,
81    pub version: u32,
82    pub public_key_multibase: String,
83    pub created_at: String,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
87#[serde(deny_unknown_fields)]
88pub struct IdentitySummary {
89    pub identity_id: String,
90    pub did: String,
91    pub state: IdentityState,
92    #[serde(default)]
93    pub root_capability: RootCapabilityState,
94    pub created_at: String,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
98#[serde(deny_unknown_fields)]
99pub(crate) struct IdentityRecord {
100    pub(crate) schema_version: u32,
101    pub(crate) identity_id: String,
102    pub(crate) did: String,
103    pub(crate) state: IdentityState,
104    pub(crate) revision: u64,
105    pub(crate) generation: u64,
106    pub(crate) document: Value,
107    pub(crate) keys: Vec<KeyMetadata>,
108    pub(crate) capabilities: Capabilities,
109    #[serde(default)]
110    pub(crate) root_capability: RootCapabilityState,
111    #[serde(default)]
112    pub(crate) root_key_fingerprint: String,
113    #[serde(default)]
114    pub(crate) checkpoint: Option<DocumentCheckpoint>,
115    #[serde(default)]
116    pub(crate) local_authorization: Option<LocalAuthorizationRecord>,
117    #[serde(default)]
118    pub(crate) local_request_signing_kid: Option<String>,
119    #[serde(default)]
120    pub(crate) pending_enrollment: Option<PendingEnrollmentRecord>,
121    #[serde(default)]
122    pub(crate) pending_request_signing: Option<PendingRequestSigningRecord>,
123    #[serde(default)]
124    pub(crate) pending_root_transfer: Option<PendingRootTransferRecord>,
125    #[serde(default)]
126    pub(crate) root_transfer_replays: Vec<RootTransferReplayRecord>,
127    pub(crate) created_at: String,
128    #[serde(default)]
129    pub(crate) pending_revision: Option<PendingRevisionRecord>,
130}
131
132impl IdentityRecord {
133    pub(crate) fn summary(&self) -> IdentitySummary {
134        IdentitySummary {
135            identity_id: self.identity_id.clone(),
136            did: self.did.clone(),
137            state: self.state,
138            root_capability: self.root_capability,
139            created_at: self.created_at.clone(),
140        }
141    }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
145#[serde(deny_unknown_fields)]
146pub(crate) struct CreationJournal {
147    schema_version: u32,
148    #[serde(default)]
149    pub(crate) kind: CreationJournalKind,
150    pub(crate) transaction_id: String,
151    pub(crate) identity_id: String,
152    pub(crate) did: String,
153    pub(crate) secret_refs: Vec<SecretRef>,
154    pub(crate) created_at: String,
155}
156
157#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
158#[serde(rename_all = "snake_case")]
159pub(crate) enum CreationJournalKind {
160    #[default]
161    Create,
162    StateTransition,
163    RootImport,
164    NamespaceDelete,
165}
166
167#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
168#[serde(rename_all = "snake_case")]
169pub(crate) enum UpdateJournalKind {
170    Prepare,
171    Cleanup,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
175#[serde(deny_unknown_fields)]
176pub(crate) struct UpdateJournal {
177    schema_version: u32,
178    pub(crate) revision_id: String,
179    pub(crate) identity_id: String,
180    pub(crate) secret_refs: Vec<SecretRef>,
181    pub(crate) kind: UpdateJournalKind,
182    pub(crate) created_at: String,
183}
184
185impl UpdateJournal {
186    pub(crate) fn new(
187        revision_id: String,
188        identity_id: String,
189        secret_refs: Vec<SecretRef>,
190        kind: UpdateJournalKind,
191        created_at: String,
192    ) -> Self {
193        Self {
194            schema_version: UPDATE_JOURNAL_SCHEMA_VERSION,
195            revision_id,
196            identity_id,
197            secret_refs,
198            kind,
199            created_at,
200        }
201    }
202}
203
204impl CreationJournal {
205    pub(crate) fn new(
206        transaction_id: String,
207        identity_id: String,
208        did: String,
209        secret_refs: Vec<SecretRef>,
210        created_at: String,
211    ) -> Self {
212        Self {
213            schema_version: JOURNAL_SCHEMA_VERSION,
214            kind: CreationJournalKind::Create,
215            transaction_id,
216            identity_id,
217            did,
218            secret_refs,
219            created_at,
220        }
221    }
222
223    pub(crate) fn new_state_transition(
224        transaction_id: String,
225        identity_id: String,
226        did: String,
227        created_at: String,
228    ) -> Self {
229        Self {
230            schema_version: JOURNAL_SCHEMA_VERSION,
231            kind: CreationJournalKind::StateTransition,
232            transaction_id,
233            identity_id,
234            did,
235            secret_refs: Vec::new(),
236            created_at,
237        }
238    }
239
240    pub(crate) fn new_root_import(
241        transaction_id: String,
242        identity_id: String,
243        did: String,
244        secret_refs: Vec<SecretRef>,
245        created_at: String,
246    ) -> Self {
247        Self {
248            schema_version: JOURNAL_SCHEMA_VERSION,
249            kind: CreationJournalKind::RootImport,
250            transaction_id,
251            identity_id,
252            did,
253            secret_refs,
254            created_at,
255        }
256    }
257
258    pub(crate) fn new_namespace_delete(
259        transaction_id: String,
260        identity_id: String,
261        did: String,
262        secret_refs: Vec<SecretRef>,
263        created_at: String,
264    ) -> Self {
265        Self {
266            schema_version: JOURNAL_SCHEMA_VERSION,
267            kind: CreationJournalKind::NamespaceDelete,
268            transaction_id,
269            identity_id,
270            did,
271            secret_refs,
272            created_at,
273        }
274    }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
278#[serde(deny_unknown_fields)]
279pub(crate) struct IdentityRegistry {
280    schema_version: u32,
281    pub(crate) generation: u64,
282    pub(crate) identities: BTreeMap<String, IdentitySummary>,
283}
284
285impl Default for IdentityRegistry {
286    fn default() -> Self {
287        Self {
288            schema_version: REGISTRY_SCHEMA_VERSION,
289            generation: 0,
290            identities: BTreeMap::new(),
291        }
292    }
293}
294
295pub(crate) fn read_registry(root: &Path) -> DidResult<IdentityRegistry> {
296    let path = registry_path(root);
297    let bytes = match fs::read(path) {
298        Ok(bytes) => bytes,
299        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
300            return Ok(IdentityRegistry::default());
301        }
302        Err(error) => return Err(DidError::Io(error.to_string())),
303    };
304    let registry: IdentityRegistry =
305        serde_json::from_slice(&bytes).map_err(|_| DidError::InvalidIdentity)?;
306    if registry.schema_version != REGISTRY_SCHEMA_VERSION {
307        return Err(DidError::InvalidIdentity);
308    }
309    Ok(registry)
310}
311
312pub(crate) fn write_registry(
313    root: &Path,
314    guard: &StoreWriteGuard,
315    registry: &IdentityRegistry,
316) -> DidResult<()> {
317    guard.require_store(root)?;
318    let bytes = serde_json::to_vec_pretty(registry).map_err(|_| DidError::InvalidIdentity)?;
319    write_atomic_private(&registry_path(root), &bytes)
320}
321
322pub(crate) fn read_identity(root: &Path, identity_id: &str) -> DidResult<IdentityRecord> {
323    let bytes = fs::read(identity_path(root, identity_id)).map_err(|error| {
324        if error.kind() == std::io::ErrorKind::NotFound {
325            DidError::IdentityNotFound
326        } else {
327            DidError::Io(error.to_string())
328        }
329    })?;
330    let mut record: IdentityRecord =
331        serde_json::from_slice(&bytes).map_err(|_| DidError::InvalidIdentity)?;
332    if record.schema_version != IDENTITY_SCHEMA_VERSION || record.identity_id != identity_id {
333        return Err(DidError::InvalidIdentity);
334    }
335    if record.root_key_fingerprint.is_empty() {
336        record.root_key_fingerprint = crate::document::root_key_fingerprint(&record.document)?;
337    }
338    if record.checkpoint.is_none() {
339        record.checkpoint = Some(DocumentCheckpoint {
340            document_version: record.revision,
341            registry_version: 1,
342            document_digest: crate::document::document_digest(&record.document)?,
343        });
344    }
345    if record.local_authorization.is_none() {
346        record.local_authorization =
347            crate::adoption::infer_local_authorization(&record.document, &record.keys)?;
348    }
349    Ok(record)
350}
351
352pub(crate) fn write_identity(
353    root: &Path,
354    guard: &StoreWriteGuard,
355    record: &IdentityRecord,
356) -> DidResult<()> {
357    guard.require_store(root)?;
358    ensure_private_dir(&root.join("identities"))?;
359    let bytes = serde_json::to_vec_pretty(record).map_err(|_| DidError::InvalidIdentity)?;
360    write_atomic_private(&identity_path(root, &record.identity_id), &bytes)
361}
362
363pub(crate) fn remove_identity(
364    root: &Path,
365    guard: &StoreWriteGuard,
366    identity_id: &str,
367) -> DidResult<()> {
368    guard.require_store(root)?;
369    remove_if_exists(&identity_path(root, identity_id))
370}
371
372pub(crate) fn write_journal(
373    root: &Path,
374    guard: &StoreWriteGuard,
375    journal: &CreationJournal,
376) -> DidResult<()> {
377    guard.require_store(root)?;
378    ensure_private_dir(&root.join("transactions"))?;
379    let bytes = serde_json::to_vec_pretty(journal).map_err(|_| DidError::InvalidIdentity)?;
380    write_atomic_private(&journal_path(root, &journal.transaction_id), &bytes)
381}
382
383pub(crate) fn list_journals(root: &Path) -> DidResult<Vec<CreationJournal>> {
384    let entries = match fs::read_dir(root.join("transactions")) {
385        Ok(entries) => entries,
386        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
387        Err(error) => return Err(DidError::Io(error.to_string())),
388    };
389    let mut journals = Vec::new();
390    for entry in entries {
391        let entry = entry.map_err(|error| DidError::Io(error.to_string()))?;
392        if entry.path().extension().and_then(|value| value.to_str()) != Some("json") {
393            continue;
394        }
395        let bytes = fs::read(entry.path()).map_err(|_| DidError::InvalidIdentity)?;
396        let journal: CreationJournal =
397            serde_json::from_slice(&bytes).map_err(|_| DidError::InvalidIdentity)?;
398        if journal.schema_version != JOURNAL_SCHEMA_VERSION {
399            return Err(DidError::InvalidIdentity);
400        }
401        journals.push(journal);
402    }
403    journals.sort_by(|left, right| left.transaction_id.cmp(&right.transaction_id));
404    Ok(journals)
405}
406
407pub(crate) fn remove_journal(
408    root: &Path,
409    guard: &StoreWriteGuard,
410    transaction_id: &str,
411) -> DidResult<()> {
412    guard.require_store(root)?;
413    remove_if_exists(&journal_path(root, transaction_id))
414}
415
416pub(crate) fn write_update_journal(
417    root: &Path,
418    guard: &StoreWriteGuard,
419    journal: &UpdateJournal,
420) -> DidResult<()> {
421    guard.require_store(root)?;
422    ensure_private_dir(&root.join("update-transactions"))?;
423    let bytes = serde_json::to_vec_pretty(journal).map_err(|_| DidError::InvalidIdentity)?;
424    write_atomic_private(&update_journal_path(root, &journal.revision_id), &bytes)
425}
426
427pub(crate) fn list_update_journals(root: &Path) -> DidResult<Vec<UpdateJournal>> {
428    let entries = match fs::read_dir(root.join("update-transactions")) {
429        Ok(entries) => entries,
430        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
431        Err(error) => return Err(DidError::Io(error.to_string())),
432    };
433    let mut journals = Vec::new();
434    for entry in entries {
435        let entry = entry.map_err(|error| DidError::Io(error.to_string()))?;
436        if entry.path().extension().and_then(|value| value.to_str()) != Some("json") {
437            continue;
438        }
439        let bytes = fs::read(entry.path()).map_err(|_| DidError::InvalidIdentity)?;
440        let journal: UpdateJournal =
441            serde_json::from_slice(&bytes).map_err(|_| DidError::InvalidIdentity)?;
442        if journal.schema_version != UPDATE_JOURNAL_SCHEMA_VERSION {
443            return Err(DidError::InvalidIdentity);
444        }
445        journals.push(journal);
446    }
447    journals.sort_by(|left, right| left.revision_id.cmp(&right.revision_id));
448    Ok(journals)
449}
450
451pub(crate) fn remove_update_journal(
452    root: &Path,
453    guard: &StoreWriteGuard,
454    revision_id: &str,
455) -> DidResult<()> {
456    guard.require_store(root)?;
457    remove_if_exists(&update_journal_path(root, revision_id))
458}
459
460pub(crate) struct NewIdentityRecord {
461    pub(crate) identity_id: String,
462    pub(crate) did: String,
463    pub(crate) document: Value,
464    pub(crate) keys: Vec<KeyMetadata>,
465    pub(crate) capabilities: Capabilities,
466    pub(crate) root_capability: RootCapabilityState,
467    pub(crate) root_key_fingerprint: String,
468    pub(crate) checkpoint: DocumentCheckpoint,
469    pub(crate) local_authorization: Option<LocalAuthorizationRecord>,
470    pub(crate) created_at: String,
471}
472
473pub(crate) fn new_identity_record(input: NewIdentityRecord) -> IdentityRecord {
474    IdentityRecord {
475        schema_version: IDENTITY_SCHEMA_VERSION,
476        identity_id: input.identity_id,
477        did: input.did,
478        state: IdentityState::Creating,
479        revision: 1,
480        generation: 1,
481        document: input.document,
482        keys: input.keys,
483        capabilities: input.capabilities,
484        root_capability: input.root_capability,
485        root_key_fingerprint: input.root_key_fingerprint,
486        checkpoint: Some(input.checkpoint),
487        local_authorization: input.local_authorization,
488        local_request_signing_kid: None,
489        pending_enrollment: None,
490        pending_request_signing: None,
491        pending_root_transfer: None,
492        root_transfer_replays: Vec::new(),
493        created_at: input.created_at,
494        pending_revision: None,
495    }
496}
497
498fn registry_path(root: &Path) -> PathBuf {
499    root.join("registry.json")
500}
501
502fn identity_path(root: &Path, identity_id: &str) -> PathBuf {
503    root.join("identities")
504        .join(format!("{}.json", storage_key(identity_id)))
505}
506
507fn journal_path(root: &Path, transaction_id: &str) -> PathBuf {
508    root.join("transactions")
509        .join(format!("{}.json", storage_key(transaction_id)))
510}
511
512fn update_journal_path(root: &Path, revision_id: &str) -> PathBuf {
513    root.join("update-transactions")
514        .join(format!("{}.json", storage_key(revision_id)))
515}
516
517fn storage_key(value: &str) -> String {
518    URL_SAFE_NO_PAD.encode(Sha256::digest(value.as_bytes()))
519}
520
521fn remove_if_exists(path: &Path) -> DidResult<()> {
522    match fs::remove_file(path) {
523        Ok(()) => Ok(()),
524        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
525        Err(error) => Err(DidError::Io(error.to_string())),
526    }
527}