Skip to main content

anp_identity/
root_transfer.rs

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/// Zeroizing PKCS#8 DER returned only for a user-confirmed root transfer.
75///
76/// This value is intentionally non-serializable, non-cloneable, and does not
77/// implement `Debug`. Callers must not persist it or use it as a backup API.
78#[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    /// Exports the active managed root-control key for `RootKeyEnvelopeV1`.
144    ///
145    /// The host is responsible for enforcing its user-confirmation interaction
146    /// before calling this method and for immediately passing the returned DER
147    /// to an end-to-end encrypted root-transfer envelope.
148    #[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, &registry)?;
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;