Skip to main content

ma_core/
doc.rs

1use ed25519_dalek::{Signature, Verifier, VerifyingKey};
2use ipld_core::ipld::Ipld;
3use serde::{Deserialize, Serialize};
4use time::{format_description::well_known::Rfc3339, OffsetDateTime};
5use web_time::{SystemTime, UNIX_EPOCH};
6
7use crate::{
8    did::Did,
9    error::{MaError, MaResult as Result},
10    key::{EncryptionKey, SigningKey, CODEC_ED25519_PUB, CODEC_EDDSA_SIG, CODEC_X25519_PUB},
11    multiformat::{
12        public_key_multibase_decode, signature_multibase_decode, signature_multibase_encode,
13    },
14};
15
16pub const DEFAULT_DID_CONTEXT: &[&str] = &["https://www.w3.org/ns/did/v1.1"];
17pub const DEFAULT_PROOF_TYPE: &str = "MultiformatSignature2023";
18pub const DEFAULT_PROOF_PURPOSE: &str = "assertionMethod";
19
20/// Returns the current UTC time as an RFC 3339 string with whole-second precision.
21pub fn now_iso_utc() -> String {
22    let unix_seconds = SystemTime::now()
23        .duration_since(UNIX_EPOCH)
24        .unwrap_or_default()
25        .as_secs();
26    i64::try_from(unix_seconds)
27        .ok()
28        .and_then(|seconds| OffsetDateTime::from_unix_timestamp(seconds).ok())
29        .and_then(|timestamp| timestamp.format(&Rfc3339).ok())
30        .unwrap_or_else(|| "1970-01-01T00:00:00Z".to_string())
31}
32
33#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct VerificationMethod {
35    pub id: String,
36    #[serde(rename = "type")]
37    pub key_type: String,
38    pub controller: String,
39    #[serde(rename = "publicKeyMultibase")]
40    pub public_key_multibase: String,
41}
42
43impl VerificationMethod {
44    pub fn new(
45        id: impl AsRef<str>,
46        controller: impl Into<String>,
47        key_type: impl Into<String>,
48        fragment: impl AsRef<str>,
49        public_key_multibase: impl Into<String>,
50    ) -> Result<Self> {
51        let base_id = id
52            .as_ref()
53            .split('#')
54            .next()
55            .ok_or(MaError::MissingIdentifier)?;
56
57        let method = Self {
58            id: format!("{base_id}#{}", fragment.as_ref()),
59            key_type: key_type.into(),
60            controller: controller.into(),
61            public_key_multibase: public_key_multibase.into(),
62        };
63        method.validate()?;
64        Ok(method)
65    }
66
67    pub fn fragment(&self) -> Result<String> {
68        let did = Did::try_from(self.id.as_str())?;
69        did.fragment.ok_or(MaError::MissingFragment)
70    }
71
72    pub fn validate(&self) -> Result<()> {
73        Did::validate_url(&self.id)?;
74
75        match self.key_type.as_str() {
76            "" => return Err(MaError::VerificationMethodMissingType),
77            "Multikey" => {}
78            _ => {
79                return Err(MaError::InvalidVerificationMethodType(
80                    self.key_type.clone(),
81                ));
82            }
83        }
84
85        if self.controller.is_empty() {
86            return Err(MaError::EmptyController);
87        }
88
89        validate_bare_did(&self.controller)?;
90
91        if self.public_key_multibase.is_empty() {
92            return Err(MaError::EmptyPublicKeyMultibase);
93        }
94
95        public_key_multibase_decode(&self.public_key_multibase)?;
96        Ok(())
97    }
98}
99
100#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
101pub struct Proof {
102    #[serde(rename = "type")]
103    pub proof_type: String,
104    #[serde(rename = "verificationMethod")]
105    pub verification_method: String,
106    #[serde(rename = "proofPurpose")]
107    pub proof_purpose: String,
108    #[serde(rename = "proofValue")]
109    pub proof_value: String,
110}
111
112impl Proof {
113    pub fn new(proof_value: impl Into<String>, verification_method: impl Into<String>) -> Self {
114        Self {
115            proof_type: DEFAULT_PROOF_TYPE.to_string(),
116            verification_method: verification_method.into(),
117            proof_purpose: DEFAULT_PROOF_PURPOSE.to_string(),
118            proof_value: proof_value.into(),
119        }
120    }
121
122    pub fn is_empty(&self) -> bool {
123        self.proof_value.is_empty()
124    }
125}
126
127// ─── ma: extension builder ──────────────────────────────────────────────────
128
129/// Fluent builder for the opaque `ma:` IPLD extension field on a [`Document`].
130///
131/// `MaExtension` collects the node type, transport service strings, and any
132/// custom IPLD fields, then produces the [`Ipld`] value ready for
133/// [`Document::set_ma_extension`].
134///
135/// The idiomatic way to populate `ma:` is to start from the endpoint — which
136/// pre-populates services — and chain any additional fields:
137///
138/// ```ignore
139/// // Endpoint pre-populates services; add type and any extras:
140/// let ma = endpoint.ma_extension()
141///     .kind("world");
142///
143/// // Build a complete, signed document in one call:
144/// let document = bundle.build_document(ma)?;
145/// ```
146///
147/// You can also build a `MaExtension` independently and attach it to an
148/// existing document with [`Document::set_ma_extension`] before re-signing.
149#[derive(Debug, Default, Clone)]
150pub struct MaExtension {
151    map: std::collections::BTreeMap<String, Ipld>,
152}
153
154impl MaExtension {
155    /// Create an empty extension builder.
156    pub fn new() -> Self {
157        Self::default()
158    }
159
160    /// Set `ma["type"]` to identify the kind of node or service.
161    ///
162    /// The key name `"type"` follows the existing convention in the ma ecosystem.
163    #[must_use]
164    pub fn kind(mut self, kind: &str) -> Self {
165        self.map
166            .insert("type".to_string(), Ipld::String(kind.to_string()));
167        self
168    }
169
170    /// Append one transport service string to `ma["services"]`.
171    ///
172    /// Service strings have the form `/iroh/<endpoint-id>/ma/<protocol>/<version>`.
173    #[must_use]
174    pub fn add_service(mut self, service: &str) -> Self {
175        let entry = self
176            .map
177            .entry("services".to_string())
178            .or_insert_with(|| Ipld::List(Vec::new()));
179        if let Ipld::List(list) = entry {
180            list.push(Ipld::String(service.to_string()));
181        }
182        self
183    }
184
185    /// Replace `ma["services"]` with the given list.
186    ///
187    /// Use this (rather than repeated [`Self::add_service`] calls) when you
188    /// already have the full service list, e.g. from [`crate::MaEndpoint::services`].
189    #[must_use]
190    pub fn services(mut self, services: Vec<String>) -> Self {
191        self.map.insert(
192            "services".to_string(),
193            Ipld::List(services.into_iter().map(Ipld::String).collect()),
194        );
195        self
196    }
197
198    /// Set an arbitrary IPLD entry in the extension map.
199    #[must_use]
200    pub fn extra(mut self, key: &str, val: Ipld) -> Self {
201        self.map.insert(key.to_string(), val);
202        self
203    }
204
205    /// Consume the builder and return the final [`Ipld`] value.
206    ///
207    /// Returns [`Ipld::Null`] if no fields have been set (which causes
208    /// [`Document::set_ma_extension`] to clear the `ma` field).
209    pub fn build(self) -> Ipld {
210        if self.map.is_empty() {
211            Ipld::Null
212        } else {
213            Ipld::Map(self.map)
214        }
215    }
216}
217
218fn is_valid_rfc3339_utc(value: &str) -> bool {
219    value.len() == 20 && value.ends_with('Z') && OffsetDateTime::parse(value, &Rfc3339).is_ok()
220}
221
222/// A `did:ma:` DID document.
223///
224/// Contains verification methods, proof, and optional extension data.
225/// Documents are signed with Ed25519 over a BLAKE3 hash of the dag-cbor-serialized
226/// payload (all fields except `proof`).
227///
228/// # Examples
229///
230/// ```
231/// use ma_core::{generate_identity_from_secret, Document};
232///
233/// let id = generate_identity_from_secret([7u8; 32]).unwrap();
234///
235/// // Verify the signature
236/// id.document.verify().unwrap();
237///
238/// // Validate structural correctness
239/// id.document.validate().unwrap();
240///
241/// // Round-trip through the canonical wire format
242/// let bytes = id.document.encode().unwrap();
243/// let restored = Document::decode(&bytes).unwrap();
244/// assert_eq!(id.document, restored);
245/// ```
246///
247/// # Extension namespace
248///
249/// The `ma` field is an opaque IPLD value for application-defined
250/// extension data. did-ma does not interpret or validate its contents.
251/// Using [`Ipld`] gives native support for CID links and canonical DAG-CBOR
252/// round-tripping.
253///
254/// ```
255/// use std::collections::BTreeMap;
256/// use ipld_core::ipld::Ipld;
257/// use ma_core::{Did, Document};
258///
259/// let did = Did::new_url("k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr", None::<String>).unwrap();
260/// let mut doc = Document::new(&did, &did);
261/// let ma = Ipld::Map(BTreeMap::from([
262///     ("type".into(), Ipld::String("agent".into())),
263///     ("services".into(), Ipld::Map(BTreeMap::new())),
264/// ]));
265/// doc.set_ma(ma);
266/// assert!(doc.ma.is_some());
267/// doc.clear_ma();
268/// assert!(doc.ma.is_none());
269/// ```
270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
271pub struct Document {
272    #[serde(rename = "@context")]
273    pub context: Vec<String>,
274    pub id: String,
275    pub controller: Vec<String>,
276    #[serde(rename = "verificationMethod")]
277    pub verification_method: Vec<VerificationMethod>,
278    #[serde(rename = "assertionMethod")]
279    pub assertion_method: Vec<String>,
280    #[serde(rename = "keyAgreement")]
281    pub key_agreement: Vec<String>,
282    pub proof: Proof,
283    #[serde(rename = "createdAt")]
284    pub created_at: String,
285    #[serde(rename = "updatedAt")]
286    pub updated_at: String,
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub ma: Option<Ipld>,
289}
290
291impl Document {
292    pub fn new(identity: &Did, controller: &Did) -> Self {
293        let now = now_iso_utc();
294        Self {
295            context: DEFAULT_DID_CONTEXT
296                .iter()
297                .map(|value| (*value).to_string())
298                .collect(),
299            id: identity.base_id(),
300            controller: vec![controller.base_id()],
301            verification_method: Vec::new(),
302            assertion_method: Vec::new(),
303            key_agreement: Vec::new(),
304            proof: Proof::default(),
305            created_at: now.clone(),
306            updated_at: now,
307            ma: None,
308        }
309    }
310
311    /// Set the opaque `ma` extension namespace from a raw [`Ipld`] value.
312    ///
313    /// For the ergonomic, structured way to populate this field, prefer
314    /// [`Document::set_ma_extension`] with a [`MaExtension`] builder.
315    pub fn set_ma(&mut self, ma: Ipld) {
316        match &ma {
317            Ipld::Null => self.ma = None,
318            Ipld::Map(m) if m.is_empty() => self.ma = None,
319            _ => self.ma = Some(ma),
320        }
321    }
322
323    /// Set the `ma` extension field from a [`MaExtension`] builder.
324    ///
325    /// This is the recommended way to populate the `ma:` namespace. Build an
326    /// extension with [`MaExtension`], then call this method before signing
327    /// the document. An empty builder (or one whose [`MaExtension::build`]
328    /// returns [`Ipld::Null`]) clears the field.
329    ///
330    /// # Example
331    ///
332    /// ```ignore
333    /// let ma = endpoint.ma_extension().kind("world");
334    /// document.set_ma_extension(ma);
335    /// document.sign(&signing_key, &assertion_vm)?;
336    /// ```
337    pub fn set_ma_extension(&mut self, ext: MaExtension) {
338        self.set_ma(ext.build());
339    }
340
341    /// Clear the `ma` extension namespace.
342    pub fn clear_ma(&mut self) {
343        self.ma = None;
344    }
345
346    /// Encode the DID document to its canonical wire format.
347    ///
348    /// DID documents are always serialized as DAG-CBOR. Use this for
349    /// transport, storage, hashing, signing, and IPFS/IPNS publication.
350    pub fn encode(&self) -> Result<Vec<u8>> {
351        serde_ipld_dagcbor::to_vec(self).map_err(|error| MaError::CborEncode(error.to_string()))
352    }
353
354    /// Decode a DID document from its canonical wire format.
355    ///
356    /// DID documents are always encoded as DAG-CBOR.
357    pub fn decode(bytes: &[u8]) -> Result<Self> {
358        serde_ipld_dagcbor::from_slice(bytes)
359            .map_err(|error| MaError::CborDecode(error.to_string()))
360    }
361
362    pub fn add_controller(&mut self, controller: impl Into<String>) -> Result<()> {
363        let controller = controller.into();
364        Did::validate(&controller)?;
365        if !self.controller.contains(&controller) {
366            self.controller.push(controller);
367        }
368        Ok(())
369    }
370
371    pub fn add_verification_method(&mut self, method: VerificationMethod) -> Result<()> {
372        method.validate()?;
373        let duplicate = self.verification_method.iter().any(|existing| {
374            existing.id == method.id || existing.public_key_multibase == method.public_key_multibase
375        });
376
377        if !duplicate {
378            self.verification_method.push(method);
379        }
380
381        Ok(())
382    }
383
384    pub fn get_verification_method_by_id(&self, method_id: &str) -> Result<&VerificationMethod> {
385        self.verification_method
386            .iter()
387            .find(|method| method.id == method_id)
388            .ok_or_else(|| MaError::UnknownVerificationMethod(method_id.to_string()))
389    }
390
391    /// Update the `updatedAt` timestamp to the current time.
392    pub fn touch(&mut self) {
393        self.updated_at = now_iso_utc();
394    }
395
396    pub fn assertion_method_public_key(&self) -> Result<VerifyingKey> {
397        let assertion_id = self
398            .assertion_method
399            .first()
400            .ok_or_else(|| MaError::UnknownVerificationMethod("assertionMethod".to_string()))?;
401        self.verifying_key_for_method(assertion_id)
402    }
403
404    fn verifying_key_for_method(&self, method_id: &str) -> Result<VerifyingKey> {
405        let vm = self.get_verification_method_by_id(method_id)?;
406        let (codec, public_key_bytes) = public_key_multibase_decode(&vm.public_key_multibase)?;
407        if codec != CODEC_ED25519_PUB {
408            return Err(MaError::InvalidMulticodec {
409                expected: CODEC_ED25519_PUB,
410                actual: codec,
411            });
412        }
413
414        let key_len = public_key_bytes.len();
415        let bytes: [u8; 32] =
416            public_key_bytes
417                .try_into()
418                .map_err(|_| MaError::InvalidKeyLength {
419                    expected: 32,
420                    actual: key_len,
421                })?;
422
423        VerifyingKey::from_bytes(&bytes).map_err(|_| MaError::Crypto)
424    }
425
426    pub fn key_agreement_public_key_bytes(&self) -> Result<[u8; 32]> {
427        let agreement_id = self
428            .key_agreement
429            .first()
430            .ok_or_else(|| MaError::UnknownVerificationMethod("keyAgreement".to_string()))?;
431        let vm = self.get_verification_method_by_id(agreement_id)?;
432        let (codec, public_key_bytes) = public_key_multibase_decode(&vm.public_key_multibase)?;
433        if codec != CODEC_X25519_PUB {
434            return Err(MaError::InvalidMulticodec {
435                expected: CODEC_X25519_PUB,
436                actual: codec,
437            });
438        }
439
440        let key_len = public_key_bytes.len();
441        public_key_bytes
442            .try_into()
443            .map_err(|_| MaError::InvalidKeyLength {
444                expected: 32,
445                actual: key_len,
446            })
447    }
448
449    #[must_use]
450    pub fn payload_document(&self) -> Self {
451        let mut payload = self.clone();
452        payload.proof = Proof::default();
453        payload
454    }
455
456    pub fn payload_bytes(&self) -> Result<Vec<u8>> {
457        self.payload_document().encode()
458    }
459
460    pub fn payload_hash(&self) -> Result<[u8; 32]> {
461        Ok(blake3::hash(&self.payload_bytes()?).into())
462    }
463
464    pub fn sign(
465        &mut self,
466        signing_key: &SigningKey,
467        verification_method: &VerificationMethod,
468    ) -> Result<()> {
469        if signing_key.public_key_multibase != verification_method.public_key_multibase {
470            return Err(MaError::InvalidPublicKeyMultibase);
471        }
472
473        let signature = signing_key.sign(&self.payload_hash()?);
474        let proof_value = signature_multibase_encode(CODEC_EDDSA_SIG, &signature);
475        self.proof = Proof::new(proof_value, verification_method.id.clone());
476        Ok(())
477    }
478
479    pub fn verify(&self) -> Result<()> {
480        if self.proof.is_empty() {
481            return Err(MaError::MissingProof);
482        }
483        if self.proof.proof_type != DEFAULT_PROOF_TYPE {
484            return Err(MaError::InvalidProofType(self.proof.proof_type.clone()));
485        }
486        if self.proof.proof_purpose != DEFAULT_PROOF_PURPOSE {
487            return Err(MaError::InvalidProofPurpose(
488                self.proof.proof_purpose.clone(),
489            ));
490        }
491
492        let (codec, sig_bytes) = signature_multibase_decode(&self.proof.proof_value)?;
493        if codec != CODEC_EDDSA_SIG {
494            return Err(MaError::InvalidDocumentSignature);
495        }
496        let signature =
497            Signature::from_slice(&sig_bytes).map_err(|_| MaError::InvalidDocumentSignature)?;
498        if !self
499            .assertion_method
500            .contains(&self.proof.verification_method)
501        {
502            return Err(MaError::UnknownVerificationMethod(
503                self.proof.verification_method.clone(),
504            ));
505        }
506        let public_key = self.verifying_key_for_method(&self.proof.verification_method)?;
507        public_key
508            .verify(&self.payload_hash()?, &signature)
509            .map_err(|_| MaError::InvalidDocumentSignature)
510    }
511
512    pub fn validate(&self) -> Result<()> {
513        if self.context != DEFAULT_DID_CONTEXT {
514            return Err(if self.context.is_empty() {
515                MaError::EmptyContext
516            } else {
517                MaError::InvalidContext
518            });
519        }
520
521        validate_bare_did(&self.id)?;
522
523        if self.controller.is_empty() {
524            return Err(MaError::EmptyController);
525        }
526
527        for controller in &self.controller {
528            validate_bare_did(controller)?;
529        }
530
531        if !is_valid_rfc3339_utc(&self.created_at) {
532            return Err(MaError::InvalidCreatedAt(self.created_at.clone()));
533        }
534
535        if !is_valid_rfc3339_utc(&self.updated_at) {
536            return Err(MaError::InvalidUpdatedAt(self.updated_at.clone()));
537        }
538
539        for method in &self.verification_method {
540            method.validate()?;
541        }
542
543        if self.assertion_method.is_empty() {
544            return Err(MaError::UnknownVerificationMethod(
545                "assertionMethod".to_string(),
546            ));
547        }
548
549        if self.key_agreement.is_empty() {
550            return Err(MaError::UnknownVerificationMethod(
551                "keyAgreement".to_string(),
552            ));
553        }
554
555        self.validate_relationships(&self.assertion_method, CODEC_ED25519_PUB)?;
556        self.validate_relationships(&self.key_agreement, CODEC_X25519_PUB)?;
557
558        Ok(())
559    }
560
561    fn validate_relationships(&self, relationships: &[String], expected_codec: u64) -> Result<()> {
562        for method_id in relationships {
563            Did::validate_url(method_id)?;
564            let method = self.get_verification_method_by_id(method_id)?;
565            let (codec, _) = public_key_multibase_decode(&method.public_key_multibase)?;
566            if codec != expected_codec {
567                return Err(MaError::InvalidMulticodec {
568                    expected: expected_codec,
569                    actual: codec,
570                });
571            }
572        }
573        Ok(())
574    }
575}
576
577fn validate_bare_did(value: &str) -> Result<()> {
578    let did = Did::try_from(value)?;
579    if did.fragment.is_some() {
580        return Err(MaError::UnexpectedFragment);
581    }
582    Ok(())
583}
584
585impl TryFrom<&[u8]> for Document {
586    type Error = MaError;
587
588    fn try_from(bytes: &[u8]) -> Result<Self> {
589        Self::decode(bytes)
590    }
591}
592
593impl TryFrom<&EncryptionKey> for VerificationMethod {
594    type Error = MaError;
595
596    fn try_from(value: &EncryptionKey) -> Result<Self> {
597        let fragment = value.did.fragment.clone().ok_or(MaError::MissingFragment)?;
598        VerificationMethod::new(
599            value.did.base_id(),
600            value.did.base_id(),
601            value.key_type.clone(),
602            fragment,
603            value.public_key_multibase.clone(),
604        )
605    }
606}
607
608impl TryFrom<&SigningKey> for VerificationMethod {
609    type Error = MaError;
610
611    fn try_from(value: &SigningKey) -> Result<Self> {
612        let fragment = value.did.fragment.clone().ok_or(MaError::MissingFragment)?;
613        VerificationMethod::new(
614            value.did.base_id(),
615            value.did.base_id(),
616            value.key_type.clone(),
617            fragment,
618            value.public_key_multibase.clone(),
619        )
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use std::collections::BTreeMap;
627
628    #[test]
629    fn encode_decode_round_trip() {
630        let identity = crate::generate_identity_from_secret([11u8; 32]).expect("identity");
631        let bytes = identity.document.encode().expect("encode");
632        let decoded = Document::decode(&bytes).expect("decode");
633        assert_eq!(decoded, identity.document);
634    }
635
636    #[test]
637    fn try_from_bytes_round_trip() {
638        let identity = crate::generate_identity_from_secret([12u8; 32]).expect("identity");
639        let bytes = identity.document.encode().expect("encode");
640        let decoded = Document::try_from(bytes.as_slice()).expect("try_from bytes");
641        assert_eq!(decoded, identity.document);
642    }
643
644    #[test]
645    fn decode_rejects_invalid_bytes() {
646        let err = Document::decode(b"not dag-cbor").expect_err("invalid bytes");
647        assert!(matches!(err, MaError::CborDecode(_)));
648    }
649
650    #[test]
651    fn payload_document_clears_proof_only() {
652        let identity = crate::generate_identity_from_secret([13u8; 32]).expect("identity");
653        let payload = identity.document.payload_document();
654
655        assert!(payload.proof.is_empty());
656        assert_eq!(payload.id, identity.document.id);
657        assert_eq!(payload.controller, identity.document.controller);
658        assert_eq!(
659            payload.verification_method,
660            identity.document.verification_method
661        );
662        assert_eq!(payload.assertion_method, identity.document.assertion_method);
663        assert_eq!(payload.key_agreement, identity.document.key_agreement);
664        assert_eq!(payload.created_at, identity.document.created_at);
665        assert_eq!(payload.updated_at, identity.document.updated_at);
666        assert_eq!(payload.ma, identity.document.ma);
667    }
668
669    #[test]
670    fn set_ma_stores_opaque_value() {
671        let root = Did::new_url(
672            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
673            None::<String>,
674        )
675        .expect("valid test did");
676        let mut document = Document::new(&root, &root);
677
678        let ma = Ipld::Map(BTreeMap::from([(
679            "type".into(),
680            Ipld::String("agent".into()),
681        )]));
682        document.set_ma(ma.clone());
683        assert_eq!(document.ma.as_ref(), Some(&ma));
684    }
685
686    #[test]
687    fn clear_ma_removes_value() {
688        let root = Did::new_url(
689            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
690            None::<String>,
691        )
692        .expect("valid test did");
693        let mut document = Document::new(&root, &root);
694
695        document.set_ma(Ipld::Map(BTreeMap::from([(
696            "type".into(),
697            Ipld::String("agent".into()),
698        )])));
699        assert!(document.ma.is_some());
700        document.clear_ma();
701        assert!(document.ma.is_none());
702    }
703
704    #[test]
705    fn set_ma_null_clears() {
706        let root = Did::new_url(
707            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
708            None::<String>,
709        )
710        .expect("valid test did");
711        let mut document = Document::new(&root, &root);
712
713        document.set_ma(Ipld::Map(BTreeMap::from([(
714            "type".into(),
715            Ipld::String("agent".into()),
716        )])));
717        document.set_ma(Ipld::Null);
718        assert!(document.ma.is_none());
719    }
720
721    #[test]
722    fn validate_accepts_opaque_ma() {
723        let identity = crate::identity::generate_identity(
724            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
725        )
726        .expect("generate identity");
727        let mut document = identity.document;
728        document.set_ma(Ipld::Map(BTreeMap::from([
729            ("type".into(), Ipld::String("bahner".into())),
730            ("custom".into(), Ipld::Integer(42)),
731        ])));
732        document
733            .validate()
734            .expect("validate should accept any ma value");
735    }
736
737    #[test]
738    fn canonical_document_passes_structural_validation() {
739        let identity = crate::generate_identity_from_secret([21u8; 32]).expect("identity");
740        identity.document.validate().expect("canonical document");
741    }
742
743    #[test]
744    fn document_validation_requires_exact_context() {
745        let identity = crate::generate_identity_from_secret([22u8; 32]).expect("identity");
746
747        for context in [
748            vec!["https://www.w3.org/ns/did/v1".to_string()],
749            vec![
750                "https://www.w3.org/ns/did/v1.1".to_string(),
751                "https://example.test/context".to_string(),
752            ],
753        ] {
754            let mut document = identity.document.clone();
755            document.context = context;
756            assert!(matches!(document.validate(), Err(MaError::InvalidContext)));
757        }
758    }
759
760    #[test]
761    fn document_validation_requires_bare_document_and_controller_dids() {
762        let identity = crate::generate_identity_from_secret([23u8; 32]).expect("identity");
763
764        let mut document = identity.document.clone();
765        document.id.push_str("#subject");
766        assert!(matches!(
767            document.validate(),
768            Err(MaError::UnexpectedFragment)
769        ));
770
771        let mut document = identity.document;
772        document.controller[0].push_str("#controller");
773        assert!(matches!(
774            document.validate(),
775            Err(MaError::UnexpectedFragment)
776        ));
777    }
778
779    #[test]
780    fn verification_method_validation_requires_multikey_and_bare_controller() {
781        let identity = crate::generate_identity_from_secret([24u8; 32]).expect("identity");
782
783        let mut method = identity.document.verification_method[0].clone();
784        method.key_type = "JsonWebKey2020".to_string();
785        assert!(matches!(
786            method.validate(),
787            Err(MaError::InvalidVerificationMethodType(_))
788        ));
789
790        let mut method = identity.document.verification_method[0].clone();
791        method.controller.push_str("#controller");
792        assert!(matches!(
793            method.validate(),
794            Err(MaError::UnexpectedFragment)
795        ));
796    }
797
798    #[test]
799    fn document_validation_requires_relationship_targets_to_exist() {
800        let identity = crate::generate_identity_from_secret([25u8; 32]).expect("identity");
801        let mut document = identity.document;
802        document.assertion_method[0] = format!("{}#unknown", document.id);
803
804        assert!(matches!(
805            document.validate(),
806            Err(MaError::UnknownVerificationMethod(_))
807        ));
808    }
809
810    #[test]
811    fn document_validation_requires_relationship_codecs() {
812        let identity = crate::generate_identity_from_secret([26u8; 32]).expect("identity");
813
814        let mut document = identity.document.clone();
815        document.assertion_method[0] = document.key_agreement[0].clone();
816        assert!(matches!(
817            document.validate(),
818            Err(MaError::InvalidMulticodec {
819                expected: CODEC_ED25519_PUB,
820                actual: CODEC_X25519_PUB,
821            })
822        ));
823
824        let mut document = identity.document;
825        document.key_agreement[0] = document.assertion_method[0].clone();
826        assert!(matches!(
827            document.validate(),
828            Err(MaError::InvalidMulticodec {
829                expected: CODEC_X25519_PUB,
830                actual: CODEC_ED25519_PUB,
831            })
832        ));
833    }
834
835    #[test]
836    fn document_verification_requires_exact_proof_type() {
837        let identity = crate::generate_identity_from_secret([27u8; 32]).expect("identity");
838        let mut document = identity.document;
839        document.proof.proof_type = "DataIntegrityProof".to_string();
840
841        assert!(matches!(
842            document.verify(),
843            Err(MaError::InvalidProofType(value)) if value == "DataIntegrityProof"
844        ));
845    }
846
847    #[test]
848    fn document_verification_requires_exact_proof_purpose() {
849        let identity = crate::generate_identity_from_secret([28u8; 32]).expect("identity");
850        let mut document = identity.document;
851        document.proof.proof_purpose = "authentication".to_string();
852
853        assert!(matches!(
854            document.verify(),
855            Err(MaError::InvalidProofPurpose(value)) if value == "authentication"
856        ));
857    }
858
859    #[test]
860    fn timestamp_validation_requires_valid_whole_utc_seconds() {
861        assert!(is_valid_rfc3339_utc("2026-08-08T12:34:56Z"));
862        assert!(is_valid_rfc3339_utc("2024-02-29T23:59:59Z"));
863
864        for invalid in [
865            "2026-08-08T12:34:56.123Z",
866            "2026-08-08T12:34:56+00:00",
867            "2026-13-08T12:34:56Z",
868            "2026-02-30T12:34:56Z",
869            "2026-08-08T24:00:00Z",
870            " 2026-08-08T12:34:56Z",
871        ] {
872            assert!(!is_valid_rfc3339_utc(invalid), "accepted {invalid}");
873        }
874    }
875
876    #[test]
877    fn document_validation_rejects_subsecond_created_at_format() {
878        let identity = crate::generate_identity_from_secret([29u8; 32]).expect("identity");
879        let mut document = identity.document;
880        document.created_at = "2026-08-08T12:34:56.123Z".to_string();
881
882        assert!(matches!(
883            document.validate(),
884            Err(MaError::InvalidCreatedAt(value)) if value == "2026-08-08T12:34:56.123Z"
885        ));
886    }
887
888    #[test]
889    fn document_validation_rejects_subsecond_updated_at_format() {
890        let identity = crate::generate_identity_from_secret([30u8; 32]).expect("identity");
891        let mut document = identity.document;
892        document.updated_at = "2026-08-08T12:34:56.123Z".to_string();
893
894        assert!(matches!(
895            document.validate(),
896            Err(MaError::InvalidUpdatedAt(value)) if value == "2026-08-08T12:34:56.123Z"
897        ));
898    }
899
900    #[test]
901    fn generated_timestamp_uses_whole_utc_seconds() {
902        let timestamp = now_iso_utc();
903        assert!(is_valid_rfc3339_utc(&timestamp));
904        assert!(!timestamp.contains('.'));
905    }
906}