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(®istry_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}