1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
2use chacha20poly1305::aead::{Aead, KeyInit, Payload};
3use chacha20poly1305::{ChaCha20Poly1305, Nonce};
4use chrono::{DateTime, Duration, SecondsFormat, Utc};
5use hkdf::Hkdf;
6use rand::rngs::OsRng;
7use rand::RngCore;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use sha2::{Digest, Sha256};
11use zeroize::Zeroizing;
12
13use crate::document::{public_key_from_secret, public_keys_equal, root_key_fingerprint};
14use crate::keystore::{SealIfAbsent, SecretRef};
15use crate::registry::{
16 read_identity, read_registry, write_identity, write_journal, write_registry, CreationJournal,
17 DocumentCheckpoint, IdentityRecord, KeyOrigin, KeyState, RootCapabilityState,
18};
19use crate::secret::SecretBytes;
20use crate::{DidError, DidIdentity, DidResult, IdentityState, KeyRole, VerifiedDocumentEvidence};
21
22pub const WRAPPED_ROOT_ENVELOPE_TYPE: &str = "anp.identity.root-transfer.wrapped";
23pub const WRAPPED_ROOT_ENVELOPE_VERSION: u32 = 1;
24const MAX_TTL_SECONDS: i64 = 600;
25const CLOCK_SKEW_SECONDS: i64 = 120;
26const NONCE_LEN: usize = 12;
27const MAX_REPLAY_RECORDS: usize = 256;
28const KDF_SALT_LABEL: &[u8] = b"anp-identity:root-transfer:salt:v1\0";
29const KDF_INFO_LABEL: &[u8] = b"anp-identity:root-transfer:aead:v1\0";
30const SIGNATURE_LABEL: &[u8] = b"anp-identity:root-transfer:signature:v1\0";
31#[cfg(feature = "root-export")]
32const ED25519_PKCS8_PREFIX: [u8; 16] = [
33 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
34];
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(deny_unknown_fields)]
38pub struct RootTransferContext {
39 pub source_did: String,
40 pub target_did: String,
41 pub sender_device_id: String,
42 pub recipient_device_id: String,
43 pub recipient_agreement_kid: String,
44 pub root_kid: String,
45 pub checkpoint: DocumentCheckpoint,
46 pub created_at: String,
47 pub expires_at: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(deny_unknown_fields)]
52pub struct WrappedRootEnvelope {
53 #[serde(rename = "type")]
54 pub envelope_type: String,
55 pub version: u32,
56 pub context: RootTransferContext,
57 pub ephemeral_public_b64u: String,
58 pub nonce_b64u: String,
59 pub ciphertext_b64u: String,
60 pub signature_b64u: String,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct RootTransferExportSpec {
65 pub target_did: String,
66 pub sender_device_id: String,
67 pub recipient_device_id: String,
68 pub recipient_agreement_kid: String,
69 pub recipient_agreement_public: [u8; 32],
70 pub root_kid: String,
71 pub ttl_seconds: u32,
72}
73
74#[cfg(feature = "root-export")]
79pub struct ExportedRootPrivateKey {
80 pkcs8_der: Zeroizing<Vec<u8>>,
81}
82
83#[cfg(feature = "root-export")]
84impl ExportedRootPrivateKey {
85 pub fn as_pkcs8_der(&self) -> &[u8] {
86 self.pkcs8_der.as_slice()
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
91#[serde(deny_unknown_fields)]
92pub struct RootPromotionSpec {
93 pub document: Value,
94 pub evidence: VerifiedDocumentEvidence,
95}
96
97#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
98#[serde(rename_all = "snake_case")]
99pub enum RootTransferImportOutcome {
100 Pending,
101 Active,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
105#[serde(deny_unknown_fields)]
106pub(crate) struct PendingRootTransferRecord {
107 pub(crate) transfer_digest: String,
108 pub(crate) root_kid: String,
109 pub(crate) imported_at: String,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
113#[serde(deny_unknown_fields)]
114pub(crate) struct RootTransferReplayRecord {
115 pub(crate) nonce: String,
116 pub(crate) transfer_digest: String,
117 pub(crate) expires_at: String,
118}
119
120#[cfg(feature = "key-import")]
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122#[serde(deny_unknown_fields)]
123pub struct LegacyRootTransferEvidence {
124 pub transfer_id: String,
125 pub source_did: String,
126 pub target_did: String,
127 pub sender_device_id: String,
128 pub recipient_device_id: String,
129 pub recipient_agreement_kid: String,
130 pub root_kid: String,
131 pub checkpoint: DocumentCheckpoint,
132 pub accepted_at: String,
133}
134
135#[cfg(feature = "key-import")]
136pub struct LegacyRootTransferImportSpec {
137 pub evidence: LegacyRootTransferEvidence,
138 pub encoding: crate::PrivateKeyEncoding,
139 pub root_key: Zeroizing<Vec<u8>>,
140}
141
142impl DidIdentity {
143 #[cfg(feature = "root-export")]
149 pub fn export_root_private_key(&self, root_kid: &str) -> DidResult<ExportedRootPrivateKey> {
150 if self.state() != IdentityState::Active
151 || self.root_capability() != RootCapabilityState::Active
152 {
153 return Err(DidError::RootCapabilityUnavailable);
154 }
155 let root = self.managed_key_metadata(root_kid)?;
156 if root.role != KeyRole::RootControl || root.state != KeyState::Active {
157 return Err(DidError::RootCapabilityUnavailable);
158 }
159 let secret = self.load_managed_secret(root)?;
160 if secret.expose().len() != 32 {
161 return Err(DidError::InvalidIdentity);
162 }
163 let mut pkcs8_der = Zeroizing::new(Vec::with_capacity(
164 ED25519_PKCS8_PREFIX.len() + secret.expose().len(),
165 ));
166 pkcs8_der.extend_from_slice(&ED25519_PKCS8_PREFIX);
167 pkcs8_der.extend_from_slice(secret.expose());
168 Ok(ExportedRootPrivateKey { pkcs8_der })
169 }
170
171 pub fn export_wrapped_root(
172 &self,
173 spec: RootTransferExportSpec,
174 ) -> DidResult<WrappedRootEnvelope> {
175 let now = Utc::now();
176 let mut ephemeral = x25519_dalek::StaticSecret::random_from_rng(OsRng).to_bytes();
177 let mut nonce = [0_u8; NONCE_LEN];
178 OsRng.fill_bytes(&mut nonce);
179 let result = self.export_wrapped_root_inner(spec, now, ephemeral, nonce);
180 ephemeral.fill(0);
181 nonce.fill(0);
182 result
183 }
184
185 pub fn import_wrapped_root(
186 &mut self,
187 envelope: &WrappedRootEnvelope,
188 ) -> DidResult<RootTransferImportOutcome> {
189 self.import_wrapped_root_at(envelope, Utc::now())
190 }
191
192 pub fn confirm_root_promotion(&mut self, spec: RootPromotionSpec) -> DidResult<()> {
193 crate::adoption::validate_verified_document(&spec.document, &spec.evidence)?;
194 if spec.document.get("id").and_then(Value::as_str) != Some(self.did())
195 || root_key_fingerprint(&spec.document)? != self.root_key_fingerprint()
196 {
197 return Err(DidError::InvalidRootTransfer);
198 }
199 let guard = self.runtime().acquire_write()?;
200 let mut record = read_identity(self.runtime().root(), self.identity_id())?;
201 if record.generation != self.record().generation {
202 return Err(DidError::Conflict);
203 }
204 if record.root_capability == RootCapabilityState::Active
205 && record.pending_root_transfer.is_none()
206 {
207 return Ok(());
208 }
209 if record.root_capability != RootCapabilityState::Pending
210 || record.pending_root_transfer.is_none()
211 {
212 return Err(DidError::RootCapabilityUnavailable);
213 }
214 validate_checkpoint_progression(record.checkpoint.as_ref(), &spec.evidence)?;
215 let root = record
216 .keys
217 .iter_mut()
218 .find(|key| key.role == KeyRole::RootControl && key.origin == KeyOrigin::Managed)
219 .ok_or(DidError::RootCapabilityUnavailable)?;
220 root.state = KeyState::Active;
221 record.root_capability = RootCapabilityState::Active;
222 record.pending_root_transfer = None;
223 record.document = spec.document;
224 record.checkpoint = Some(DocumentCheckpoint {
225 document_version: spec.evidence.document_version,
226 registry_version: spec.evidence.registry_version,
227 document_digest: spec.evidence.document_digest,
228 });
229 record.revision = record
230 .checkpoint
231 .as_ref()
232 .map(|checkpoint| checkpoint.document_version)
233 .ok_or(DidError::InvalidIdentity)?;
234 crate::adoption::persist_state_transition(self, &guard, &mut record)?;
235 drop(guard);
236 self.replace_record(record);
237 Ok(())
238 }
239
240 #[cfg(feature = "key-import")]
241 pub fn import_legacy_root_transfer(
242 &mut self,
243 spec: LegacyRootTransferImportSpec,
244 ) -> DidResult<RootTransferImportOutcome> {
245 validate_legacy_evidence(self, &spec.evidence)?;
246 let material = crate::key_import::parse_private_key(
247 KeyRole::RootControl,
248 spec.encoding,
249 &spec.root_key,
250 )?;
251 let public = material.public_key();
252 let raw = crate::key_import::raw_private_key(material, KeyRole::RootControl)?;
253 let digest = digest_serialized(&spec.evidence)?;
254 self.commit_pending_root(
255 &spec.evidence.root_kid,
256 raw,
257 public,
258 format!("legacy:{}", spec.evidence.transfer_id),
259 digest,
260 spec.evidence.accepted_at.clone(),
261 )
262 }
263
264 fn export_wrapped_root_inner(
265 &self,
266 spec: RootTransferExportSpec,
267 created_at: DateTime<Utc>,
268 ephemeral_bytes: [u8; 32],
269 nonce: [u8; NONCE_LEN],
270 ) -> DidResult<WrappedRootEnvelope> {
271 if self.state() != IdentityState::Active
272 || self.root_capability() != RootCapabilityState::Active
273 || spec.ttl_seconds == 0
274 || i64::from(spec.ttl_seconds) > MAX_TTL_SECONDS
275 || spec.target_did != self.did()
276 || spec.sender_device_id.trim().is_empty()
277 || spec.recipient_device_id.trim().is_empty()
278 {
279 return Err(DidError::RootCapabilityUnavailable);
280 }
281 let root = self.managed_key_metadata(&spec.root_kid)?;
282 if root.role != KeyRole::RootControl || root.state != KeyState::Active {
283 return Err(DidError::RootCapabilityUnavailable);
284 }
285 let checkpoint = self
286 .checkpoint()
287 .cloned()
288 .ok_or(DidError::InvalidIdentity)?;
289 let context = RootTransferContext {
290 source_did: self.did().to_string(),
291 target_did: spec.target_did.clone(),
292 sender_device_id: spec.sender_device_id.clone(),
293 recipient_device_id: spec.recipient_device_id.clone(),
294 recipient_agreement_kid: spec.recipient_agreement_kid.clone(),
295 root_kid: root.kid.clone(),
296 checkpoint,
297 created_at: timestamp(created_at),
298 expires_at: timestamp(created_at + Duration::seconds(i64::from(spec.ttl_seconds))),
299 };
300 let ephemeral = x25519_dalek::StaticSecret::from(ephemeral_bytes);
301 let ephemeral_public = x25519_dalek::PublicKey::from(&ephemeral).to_bytes();
302 let shared = Zeroizing::new(
303 ephemeral
304 .diffie_hellman(&x25519_dalek::PublicKey::from(
305 spec.recipient_agreement_public,
306 ))
307 .to_bytes(),
308 );
309 if shared.iter().all(|byte| *byte == 0) {
310 return Err(DidError::InvalidPeerKey);
311 }
312 validate_export_recipient(self, &spec)?;
313 let aad = envelope_aad(&context, &ephemeral_public, &nonce)?;
314 let key = derive_transfer_key(shared.as_slice(), &aad)?;
315 let cipher =
316 ChaCha20Poly1305::new_from_slice(key.as_slice()).map_err(|_| DidError::Crypto)?;
317 let root_secret = self.load_managed_secret(root)?;
318 let nonce_value = Nonce::from(nonce);
319 let ciphertext = cipher
320 .encrypt(
321 &nonce_value,
322 Payload {
323 msg: root_secret.expose(),
324 aad: &aad,
325 },
326 )
327 .map_err(|_| DidError::Crypto)?;
328 let mut envelope = WrappedRootEnvelope {
329 envelope_type: WRAPPED_ROOT_ENVELOPE_TYPE.to_string(),
330 version: WRAPPED_ROOT_ENVELOPE_VERSION,
331 context,
332 ephemeral_public_b64u: URL_SAFE_NO_PAD.encode(ephemeral_public),
333 nonce_b64u: URL_SAFE_NO_PAD.encode(nonce),
334 ciphertext_b64u: URL_SAFE_NO_PAD.encode(ciphertext),
335 signature_b64u: String::new(),
336 };
337 let signature_input = envelope_signature_input(&envelope)?;
338 let signing_key = root_signing_key(&root_secret)?;
339 use ed25519_dalek::Signer;
340 envelope.signature_b64u =
341 URL_SAFE_NO_PAD.encode(signing_key.sign(&signature_input).to_bytes());
342 Ok(envelope)
343 }
344
345 fn import_wrapped_root_at(
346 &mut self,
347 envelope: &WrappedRootEnvelope,
348 now: DateTime<Utc>,
349 ) -> DidResult<RootTransferImportOutcome> {
350 let digest = digest_serialized(envelope)?;
351 let known_replay =
352 self.record().root_transfer_replays.iter().any(|replay| {
353 replay.nonce == envelope.nonce_b64u && replay.transfer_digest == digest
354 });
355 validate_wrapped_envelope(self, envelope, now, known_replay)?;
356 let ephemeral_public: [u8; 32] = decode_fixed(&envelope.ephemeral_public_b64u)?;
357 let nonce: [u8; NONCE_LEN] = decode_fixed(&envelope.nonce_b64u)?;
358 let ciphertext = URL_SAFE_NO_PAD
359 .decode(&envelope.ciphertext_b64u)
360 .map_err(|_| DidError::InvalidRootTransfer)?;
361 let agreement = self.managed_key_metadata(&envelope.context.recipient_agreement_kid)?;
362 if agreement.role != KeyRole::E2eeAgreement || agreement.state != KeyState::Active {
363 return Err(DidError::KeyRoleViolation);
364 }
365 let agreement_secret = self.load_managed_secret(agreement)?;
366 let private = x25519_private(&agreement_secret)?;
367 let shared = Zeroizing::new(
368 private
369 .diffie_hellman(&x25519_dalek::PublicKey::from(ephemeral_public))
370 .to_bytes(),
371 );
372 if shared.iter().all(|byte| *byte == 0) {
373 return Err(DidError::InvalidPeerKey);
374 }
375 let aad = envelope_aad(&envelope.context, &ephemeral_public, &nonce)?;
376 let key = derive_transfer_key(shared.as_slice(), &aad)?;
377 let cipher =
378 ChaCha20Poly1305::new_from_slice(key.as_slice()).map_err(|_| DidError::Crypto)?;
379 let nonce_value = Nonce::from(nonce);
380 let plaintext = Zeroizing::new(
381 cipher
382 .decrypt(
383 &nonce_value,
384 Payload {
385 msg: &ciphertext,
386 aad: &aad,
387 },
388 )
389 .map_err(|_| DidError::InvalidRootTransfer)?,
390 );
391 let raw: [u8; 32] = plaintext
392 .as_slice()
393 .try_into()
394 .map_err(|_| DidError::InvalidRootTransfer)?;
395 let raw = Zeroizing::new(raw);
396 let public = anp::PublicKeyMaterial::Ed25519(
397 ed25519_dalek::SigningKey::from_bytes(&raw).verifying_key(),
398 );
399 self.commit_pending_root(
400 &envelope.context.root_kid,
401 raw,
402 public,
403 envelope.nonce_b64u.clone(),
404 digest,
405 envelope.context.expires_at.clone(),
406 )
407 }
408
409 fn commit_pending_root(
410 &mut self,
411 root_kid: &str,
412 raw: Zeroizing<[u8; 32]>,
413 public: anp::PublicKeyMaterial,
414 replay_nonce: String,
415 transfer_digest: String,
416 replay_expires_at: String,
417 ) -> DidResult<RootTransferImportOutcome> {
418 let guard = self.runtime().acquire_write()?;
419 let mut record = read_identity(self.runtime().root(), self.identity_id())?;
420 if record.generation != self.record().generation || record.state != IdentityState::Active {
421 return Err(DidError::Conflict);
422 }
423 if record.pending_revision.is_some() {
424 return Err(DidError::PendingRevisionExists);
425 }
426 if let Some(replay) = record
427 .root_transfer_replays
428 .iter()
429 .find(|replay| replay.nonce == replay_nonce)
430 {
431 if replay.transfer_digest != transfer_digest {
432 return Err(DidError::InvalidRootTransfer);
433 }
434 return match record.root_capability {
435 RootCapabilityState::Pending => Ok(RootTransferImportOutcome::Pending),
436 RootCapabilityState::Active => Ok(RootTransferImportOutcome::Active),
437 RootCapabilityState::Absent => Err(DidError::InvalidIdentity),
438 };
439 }
440 if record.root_capability == RootCapabilityState::Active {
441 verify_root_public(&record, root_kid, &public)?;
442 push_replay(
443 &mut record,
444 replay_nonce,
445 transfer_digest,
446 replay_expires_at,
447 );
448 crate::adoption::persist_state_transition(self, &guard, &mut record)?;
449 drop(guard);
450 self.replace_record(record);
451 return Ok(RootTransferImportOutcome::Active);
452 }
453 if let Some(pending) = &record.pending_root_transfer {
454 return if pending.transfer_digest == transfer_digest {
455 Ok(RootTransferImportOutcome::Pending)
456 } else {
457 Err(DidError::PendingRootTransferExists)
458 };
459 }
460 if record.root_capability != RootCapabilityState::Absent {
461 return Err(DidError::RootCapabilityUnavailable);
462 }
463 verify_root_public(&record, root_kid, &public)?;
464 let root_index = record
465 .keys
466 .iter()
467 .position(|key| key.role == KeyRole::RootControl && key.kid == root_kid)
468 .ok_or(DidError::KeyNotFound)?;
469 let secret_ref = SecretRef {
470 identity_id: record.identity_id.clone(),
471 key_id: root_kid.to_string(),
472 role: KeyRole::RootControl,
473 version: record.keys[root_index].version,
474 };
475 let transaction_id = crate::identity::random_id();
476 let imported_at = timestamp(Utc::now());
477 let journal = CreationJournal::new_root_import(
478 transaction_id.clone(),
479 record.identity_id.clone(),
480 record.did.clone(),
481 vec![secret_ref.clone()],
482 imported_at.clone(),
483 );
484 write_journal(self.runtime().root(), &guard, &journal)?;
485 let sealed = self.runtime().key_store().seal_if_absent(
486 &guard,
487 self.runtime().root_key(),
488 secret_ref.clone(),
489 SecretBytes::new(raw.to_vec()),
490 )?;
491 if sealed == SealIfAbsent::AlreadyExists {
492 let persisted = self
493 .runtime()
494 .key_store()
495 .open(self.runtime().root_key(), &secret_ref)?;
496 let actual = public_key_from_secret(KeyRole::RootControl, &persisted)?;
497 if !public_keys_equal(&actual, &public) {
498 return Err(DidError::InvalidIdentity);
499 }
500 }
501 record.keys[root_index].origin = KeyOrigin::Managed;
502 record.keys[root_index].state = KeyState::Pending;
503 record.root_capability = RootCapabilityState::Pending;
504 record.pending_root_transfer = Some(PendingRootTransferRecord {
505 transfer_digest: transfer_digest.clone(),
506 root_kid: root_kid.to_string(),
507 imported_at,
508 });
509 push_replay(
510 &mut record,
511 replay_nonce,
512 transfer_digest,
513 replay_expires_at,
514 );
515 record.generation = record.generation.checked_add(1).ok_or(DidError::Conflict)?;
516 write_identity(self.runtime().root(), &guard, &record)?;
517 let mut registry = read_registry(self.runtime().root())?;
518 let summary = registry
519 .identities
520 .get_mut(&record.did)
521 .filter(|summary| summary.identity_id == record.identity_id)
522 .ok_or(DidError::IdentityNotFound)?;
523 *summary = record.summary();
524 registry.generation = registry
525 .generation
526 .checked_add(1)
527 .ok_or(DidError::Conflict)?;
528 write_registry(self.runtime().root(), &guard, ®istry)?;
529 crate::registry::remove_journal(self.runtime().root(), &guard, &transaction_id)?;
530 drop(guard);
531 self.replace_record(record);
532 Ok(RootTransferImportOutcome::Pending)
533 }
534}
535
536fn validate_export_recipient(
537 identity: &DidIdentity,
538 spec: &RootTransferExportSpec,
539) -> DidResult<()> {
540 let canonical_kid =
541 crate::input::canonicalize_kid(identity.did(), &spec.recipient_agreement_kid)?;
542 if canonical_kid != spec.recipient_agreement_kid {
543 return Err(DidError::InvalidRootTransfer);
544 }
545 if identity
546 .record()
547 .local_authorization
548 .as_ref()
549 .is_some_and(|local| local.device_id != spec.sender_device_id)
550 {
551 return Err(DidError::InvalidRootTransfer);
552 }
553 let method = anp::authentication::find_verification_method(
554 identity.document(),
555 &spec.recipient_agreement_kid,
556 )
557 .ok_or(DidError::InvalidRootTransfer)?;
558 let expected =
559 anp::authentication::extract_public_key(&method).map_err(|_| DidError::InvalidPublicKey)?;
560 if !public_keys_equal(
561 &expected,
562 &anp::PublicKeyMaterial::X25519(spec.recipient_agreement_public),
563 ) || !relationship_contains(
564 identity.document(),
565 "keyAgreement",
566 &spec.recipient_agreement_kid,
567 ) {
568 return Err(DidError::InvalidRootTransfer);
569 }
570 let manifest = anp::authentication::validate_device_manifest(identity.document())
571 .map_err(|_| DidError::InvalidRootTransfer)?
572 .ok_or(DidError::InvalidRootTransfer)?;
573 let matches = manifest
574 .devices
575 .iter()
576 .filter(|device| device.device_id == spec.recipient_device_id)
577 .collect::<Vec<_>>();
578 if matches.len() != 1 || matches[0].e2ee_key_id != spec.recipient_agreement_kid {
579 return Err(DidError::InvalidRootTransfer);
580 }
581 Ok(())
582}
583
584fn push_replay(
585 record: &mut IdentityRecord,
586 nonce: String,
587 transfer_digest: String,
588 expires_at: String,
589) {
590 record.root_transfer_replays.push(RootTransferReplayRecord {
591 nonce,
592 transfer_digest,
593 expires_at,
594 });
595 if record.root_transfer_replays.len() > MAX_REPLAY_RECORDS {
596 let remove = record.root_transfer_replays.len() - MAX_REPLAY_RECORDS;
597 record.root_transfer_replays.drain(..remove);
598 }
599}
600
601fn relationship_contains(document: &Value, name: &str, kid: &str) -> bool {
602 document
603 .get(name)
604 .and_then(Value::as_array)
605 .is_some_and(|values| {
606 values.iter().any(|value| {
607 value.as_str() == Some(kid) || value.get("id").and_then(Value::as_str) == Some(kid)
608 })
609 })
610}
611
612fn validate_wrapped_envelope(
613 identity: &DidIdentity,
614 envelope: &WrappedRootEnvelope,
615 now: DateTime<Utc>,
616 allow_stale_checkpoint_for_known_replay: bool,
617) -> DidResult<()> {
618 if envelope.envelope_type != WRAPPED_ROOT_ENVELOPE_TYPE
619 || envelope.version != WRAPPED_ROOT_ENVELOPE_VERSION
620 || envelope.context.source_did != identity.did()
621 || envelope.context.target_did != identity.did()
622 || envelope.context.sender_device_id.trim().is_empty()
623 || envelope.context.recipient_device_id.trim().is_empty()
624 {
625 return Err(DidError::InvalidRootTransfer);
626 }
627 let local = identity
628 .record()
629 .local_authorization
630 .as_ref()
631 .ok_or(DidError::InvalidRootTransfer)?;
632 if local.device_id != envelope.context.recipient_device_id
633 || local.e2ee_kid != envelope.context.recipient_agreement_kid
634 || (!allow_stale_checkpoint_for_known_replay
635 && identity.checkpoint() != Some(&envelope.context.checkpoint))
636 {
637 return Err(DidError::InvalidRootTransfer);
638 }
639 validate_time_window(&envelope.context, now)?;
640 let root = identity.key_metadata(&envelope.context.root_kid)?;
641 if root.role != KeyRole::RootControl {
642 return Err(DidError::KeyRoleViolation);
643 }
644 let method = anp::authentication::find_verification_method(identity.document(), &root.kid)
645 .ok_or(DidError::InvalidIdentity)?;
646 let public =
647 anp::authentication::extract_public_key(&method).map_err(|_| DidError::InvalidPublicKey)?;
648 let signature = URL_SAFE_NO_PAD
649 .decode(&envelope.signature_b64u)
650 .map_err(|_| DidError::InvalidRootTransfer)?;
651 public
652 .verify_message(&envelope_signature_input(envelope)?, &signature)
653 .map_err(|_| DidError::InvalidRootTransfer)
654}
655
656#[cfg(feature = "key-import")]
657fn validate_legacy_evidence(
658 identity: &DidIdentity,
659 evidence: &LegacyRootTransferEvidence,
660) -> DidResult<()> {
661 let local = identity
662 .record()
663 .local_authorization
664 .as_ref()
665 .ok_or(DidError::InvalidRootTransfer)?;
666 if evidence.transfer_id.trim().is_empty()
667 || evidence.source_did != identity.did()
668 || evidence.target_did != identity.did()
669 || evidence.sender_device_id.trim().is_empty()
670 || evidence.recipient_device_id != local.device_id
671 || evidence.recipient_agreement_kid != local.e2ee_kid
672 || identity.checkpoint() != Some(&evidence.checkpoint)
673 || DateTime::parse_from_rfc3339(&evidence.accepted_at).is_err()
674 {
675 return Err(DidError::InvalidRootTransfer);
676 }
677 let root = identity.key_metadata(&evidence.root_kid)?;
678 if root.role != KeyRole::RootControl {
679 return Err(DidError::KeyRoleViolation);
680 }
681 Ok(())
682}
683
684fn verify_root_public(
685 record: &IdentityRecord,
686 root_kid: &str,
687 public: &anp::PublicKeyMaterial,
688) -> DidResult<()> {
689 let method = anp::authentication::find_verification_method(&record.document, root_kid)
690 .ok_or(DidError::KeyNotFound)?;
691 let expected =
692 anp::authentication::extract_public_key(&method).map_err(|_| DidError::InvalidPublicKey)?;
693 if !public_keys_equal(public, &expected)
694 || root_key_fingerprint(&record.document)? != record.root_key_fingerprint
695 {
696 return Err(DidError::InvalidPublicKey);
697 }
698 Ok(())
699}
700
701fn validate_checkpoint_progression(
702 current: Option<&DocumentCheckpoint>,
703 next: &VerifiedDocumentEvidence,
704) -> DidResult<()> {
705 let current = current.ok_or(DidError::InvalidIdentity)?;
706 if next.document_version < current.document_version
707 || next.registry_version < current.registry_version
708 || (next.document_version == current.document_version
709 && next.document_digest != current.document_digest)
710 {
711 return Err(DidError::Conflict);
712 }
713 Ok(())
714}
715
716fn validate_time_window(context: &RootTransferContext, now: DateTime<Utc>) -> DidResult<()> {
717 let created = parse_timestamp(&context.created_at)?;
718 let expires = parse_timestamp(&context.expires_at)?;
719 let ttl = expires.signed_duration_since(created).num_seconds();
720 if ttl <= 0
721 || ttl > MAX_TTL_SECONDS
722 || now < created - Duration::seconds(CLOCK_SKEW_SECONDS)
723 || now > expires + Duration::seconds(CLOCK_SKEW_SECONDS)
724 {
725 return Err(DidError::RootTransferExpired);
726 }
727 Ok(())
728}
729
730fn envelope_aad(
731 context: &RootTransferContext,
732 ephemeral_public: &[u8; 32],
733 nonce: &[u8; NONCE_LEN],
734) -> DidResult<Vec<u8>> {
735 #[derive(Serialize)]
736 struct Aad<'a> {
737 #[serde(rename = "type")]
738 envelope_type: &'a str,
739 version: u32,
740 context: &'a RootTransferContext,
741 ephemeral_public_b64u: String,
742 nonce_b64u: String,
743 }
744 canonical(&Aad {
745 envelope_type: WRAPPED_ROOT_ENVELOPE_TYPE,
746 version: WRAPPED_ROOT_ENVELOPE_VERSION,
747 context,
748 ephemeral_public_b64u: URL_SAFE_NO_PAD.encode(ephemeral_public),
749 nonce_b64u: URL_SAFE_NO_PAD.encode(nonce),
750 })
751}
752
753fn envelope_signature_input(envelope: &WrappedRootEnvelope) -> DidResult<Vec<u8>> {
754 #[derive(Serialize)]
755 struct Unsigned<'a> {
756 #[serde(rename = "type")]
757 envelope_type: &'a str,
758 version: u32,
759 context: &'a RootTransferContext,
760 ephemeral_public_b64u: &'a str,
761 nonce_b64u: &'a str,
762 ciphertext_b64u: &'a str,
763 }
764 let canonical = canonical(&Unsigned {
765 envelope_type: &envelope.envelope_type,
766 version: envelope.version,
767 context: &envelope.context,
768 ephemeral_public_b64u: &envelope.ephemeral_public_b64u,
769 nonce_b64u: &envelope.nonce_b64u,
770 ciphertext_b64u: &envelope.ciphertext_b64u,
771 })?;
772 let mut input = SIGNATURE_LABEL.to_vec();
773 input.extend_from_slice(&canonical);
774 Ok(input)
775}
776
777fn derive_transfer_key(shared: &[u8], aad: &[u8]) -> DidResult<Zeroizing<[u8; 32]>> {
778 let mut salt = Sha256::new();
779 salt.update(KDF_SALT_LABEL);
780 salt.update(aad);
781 let salt = salt.finalize();
782 let hkdf = Hkdf::<Sha256>::new(Some(&salt), shared);
783 let mut info = KDF_INFO_LABEL.to_vec();
784 info.extend_from_slice(aad);
785 let mut key = Zeroizing::new([0_u8; 32]);
786 hkdf.expand(&info, key.as_mut())
787 .map_err(|_| DidError::Crypto)?;
788 Ok(key)
789}
790
791fn root_signing_key(secret: &SecretBytes) -> DidResult<ed25519_dalek::SigningKey> {
792 let raw: [u8; 32] = secret
793 .expose()
794 .try_into()
795 .map_err(|_| DidError::InvalidIdentity)?;
796 Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
797}
798
799fn x25519_private(secret: &SecretBytes) -> DidResult<x25519_dalek::StaticSecret> {
800 let raw: [u8; 32] = secret
801 .expose()
802 .try_into()
803 .map_err(|_| DidError::InvalidIdentity)?;
804 Ok(x25519_dalek::StaticSecret::from(raw))
805}
806
807fn parse_timestamp(value: &str) -> DidResult<DateTime<Utc>> {
808 DateTime::parse_from_rfc3339(value)
809 .map(|value| value.with_timezone(&Utc))
810 .map_err(|_| DidError::InvalidRootTransfer)
811}
812
813fn timestamp(value: DateTime<Utc>) -> String {
814 value.to_rfc3339_opts(SecondsFormat::Secs, true)
815}
816
817fn canonical(value: &impl Serialize) -> DidResult<Vec<u8>> {
818 serde_json_canonicalizer::to_vec(value)
819 .map_err(|error| DidError::Serialization(error.to_string()))
820}
821
822fn digest_serialized(value: &impl Serialize) -> DidResult<String> {
823 Ok(format!(
824 "sha256:{}",
825 URL_SAFE_NO_PAD.encode(Sha256::digest(canonical(value)?))
826 ))
827}
828
829fn decode_fixed<const N: usize>(value: &str) -> DidResult<[u8; N]> {
830 URL_SAFE_NO_PAD
831 .decode(value)
832 .map_err(|_| DidError::InvalidRootTransfer)?
833 .try_into()
834 .map_err(|_| DidError::InvalidRootTransfer)
835}
836
837#[cfg(test)]
838mod tests;