Skip to main content

ma_core/
doc.rs

1use cid::Cid;
2use ed25519_dalek::{Signature, Verifier, VerifyingKey};
3use ipld_core::ipld::Ipld;
4use serde::{Deserialize, Serialize};
5#[cfg(not(target_arch = "wasm32"))]
6use web_time::{SystemTime, UNIX_EPOCH};
7
8use crate::{
9    did::Did,
10    error::{MaError, MaResult as Result},
11    key::{EncryptionKey, SigningKey, ED25519_PUB_CODEC, EDDSA_SIG_CODEC, X25519_PUB_CODEC},
12    multiformat::{
13        public_key_multibase_decode, signature_multibase_decode, signature_multibase_encode,
14    },
15};
16
17pub const DEFAULT_DID_CONTEXT: &[&str] = &["https://www.w3.org/ns/did/v1.1"];
18pub const DEFAULT_PROOF_TYPE: &str = "MultiformatSignature2023";
19pub const DEFAULT_PROOF_PURPOSE: &str = "assertionMethod";
20
21/// Returns the current UTC time as an ISO 8601 string with millisecond precision.
22pub fn now_iso_utc() -> String {
23    #[cfg(target_arch = "wasm32")]
24    {
25        // Bruk JS Date for ISO-format
26        return js_sys::Date::new_0()
27            .to_iso_string()
28            .as_string()
29            .unwrap_or_else(|| "1970-01-01T00:00:00.000Z".to_string());
30    }
31
32    #[cfg(not(target_arch = "wasm32"))]
33    {
34        let duration = SystemTime::now()
35            .duration_since(UNIX_EPOCH)
36            .unwrap_or_default();
37        unix_millis_to_iso(duration.as_secs(), duration.subsec_millis())
38    }
39}
40
41#[cfg(not(target_arch = "wasm32"))]
42fn unix_millis_to_iso(secs: u64, millis: u32) -> String {
43    // Howard Hinnant's civil_from_days algorithm.
44    let days = i64::try_from(secs / 86_400).unwrap_or(i64::MAX);
45    let z = days + 719_468;
46    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
47    let doe = u64::try_from(z - era * 146_097).unwrap_or_default();
48    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
49    let y = i64::try_from(yoe).unwrap_or(i64::MAX) + era * 400;
50    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
51    let mp = (5 * doy + 2) / 153;
52    let d = doy - (153 * mp + 2) / 5 + 1;
53    let m = if mp < 10 { mp + 3 } else { mp - 9 };
54    let y = if m <= 2 { y + 1 } else { y };
55    let tod = secs % 86400;
56    format!(
57        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
58        y,
59        m,
60        d,
61        tod / 3600,
62        (tod % 3600) / 60,
63        tod % 60,
64        millis,
65    )
66}
67
68#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
69pub struct VerificationMethod {
70    pub id: String,
71    #[serde(rename = "type")]
72    pub key_type: String,
73    pub controller: String,
74    #[serde(rename = "publicKeyMultibase")]
75    pub public_key_multibase: String,
76}
77
78impl VerificationMethod {
79    pub fn new(
80        id: impl AsRef<str>,
81        controller: impl Into<String>,
82        key_type: impl Into<String>,
83        fragment: impl AsRef<str>,
84        public_key_multibase: impl Into<String>,
85    ) -> Result<Self> {
86        let base_id = id
87            .as_ref()
88            .split('#')
89            .next()
90            .ok_or(MaError::MissingIdentifier)?;
91
92        let method = Self {
93            id: format!("{base_id}#{}", fragment.as_ref()),
94            key_type: key_type.into(),
95            controller: controller.into(),
96            public_key_multibase: public_key_multibase.into(),
97        };
98        method.validate()?;
99        Ok(method)
100    }
101
102    pub fn fragment(&self) -> Result<String> {
103        let did = Did::try_from(self.id.as_str())?;
104        did.fragment.ok_or(MaError::MissingFragment)
105    }
106
107    pub fn validate(&self) -> Result<()> {
108        Did::validate_url(&self.id)?;
109
110        if self.key_type.is_empty() {
111            return Err(MaError::VerificationMethodMissingType);
112        }
113
114        if self.controller.is_empty() {
115            return Err(MaError::EmptyController);
116        }
117
118        Did::validate(&self.controller)?;
119
120        if self.public_key_multibase.is_empty() {
121            return Err(MaError::EmptyPublicKeyMultibase);
122        }
123
124        public_key_multibase_decode(&self.public_key_multibase)?;
125        Ok(())
126    }
127}
128
129#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
130pub struct Proof {
131    #[serde(rename = "type")]
132    pub proof_type: String,
133    #[serde(rename = "verificationMethod")]
134    pub verification_method: String,
135    #[serde(rename = "proofPurpose")]
136    pub proof_purpose: String,
137    #[serde(rename = "proofValue")]
138    pub proof_value: String,
139}
140
141impl Proof {
142    pub fn new(proof_value: impl Into<String>, verification_method: impl Into<String>) -> Self {
143        Self {
144            proof_type: DEFAULT_PROOF_TYPE.to_string(),
145            verification_method: verification_method.into(),
146            proof_purpose: DEFAULT_PROOF_PURPOSE.to_string(),
147            proof_value: proof_value.into(),
148        }
149    }
150
151    pub fn is_empty(&self) -> bool {
152        self.proof_value.is_empty()
153    }
154}
155
156fn is_valid_rfc3339_utc(value: &str) -> bool {
157    let trimmed = value.trim();
158    // Strict enough for ISO-8601 UTC produced by current implementations.
159    if !trimmed.ends_with('Z') {
160        return false;
161    }
162    let bytes = trimmed.as_bytes();
163    if bytes.len() < 20 {
164        return false;
165    }
166    let expected_punct = [
167        (4usize, b'-'),
168        (7usize, b'-'),
169        (10usize, b'T'),
170        (13usize, b':'),
171        (16usize, b':'),
172    ];
173    if expected_punct
174        .iter()
175        .any(|(idx, punct)| bytes.get(*idx).copied() != Some(*punct))
176    {
177        return false;
178    }
179    let core_digits = [0usize, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18];
180    if core_digits.iter().any(|idx| {
181        !bytes
182            .get(*idx)
183            .copied()
184            .unwrap_or_default()
185            .is_ascii_digit()
186    }) {
187        return false;
188    }
189    let tail = &trimmed[19..trimmed.len() - 1];
190    if tail.is_empty() {
191        return true;
192    }
193    if let Some(frac) = tail.strip_prefix('.') {
194        return !frac.is_empty() && frac.chars().all(|ch| ch.is_ascii_digit());
195    }
196    false
197}
198
199/// A `did:ma:` DID document.
200///
201/// Contains verification methods, proof, and optional extension data.
202/// Documents are signed with Ed25519 over a BLAKE3 hash of the dag-cbor-serialized
203/// payload (all fields except `proof`).
204///
205/// # Examples
206///
207/// ```
208/// use ma_core::{generate_identity_from_secret, Document};
209///
210/// let id = generate_identity_from_secret([7u8; 32]).unwrap();
211///
212/// // Verify the signature
213/// id.document.verify().unwrap();
214///
215/// // Validate structural correctness
216/// id.document.validate().unwrap();
217///
218/// // Round-trip through the canonical wire format
219/// let bytes = id.document.encode().unwrap();
220/// let restored = Document::decode(&bytes).unwrap();
221/// assert_eq!(id.document, restored);
222/// ```
223///
224/// # Extension namespace
225///
226/// The `ma` field is an opaque IPLD value for application-defined
227/// extension data. did-ma does not interpret or validate its contents.
228/// Using [`Ipld`] gives native support for CID links and canonical DAG-CBOR
229/// round-tripping.
230///
231/// ```
232/// use std::collections::BTreeMap;
233/// use ipld_core::ipld::Ipld;
234/// use ma_core::{Did, Document};
235///
236/// let did = Did::new_url("k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr", None::<String>).unwrap();
237/// let mut doc = Document::new(&did, &did);
238/// let ma = Ipld::Map(BTreeMap::from([
239///     ("type".into(), Ipld::String("agent".into())),
240///     ("services".into(), Ipld::Map(BTreeMap::new())),
241/// ]));
242/// doc.set_ma(ma);
243/// assert!(doc.ma.is_some());
244/// doc.clear_ma();
245/// assert!(doc.ma.is_none());
246/// ```
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248pub struct Document {
249    #[serde(rename = "@context")]
250    pub context: Vec<String>,
251    pub id: String,
252    pub controller: Vec<String>,
253    #[serde(rename = "verificationMethod")]
254    pub verification_method: Vec<VerificationMethod>,
255    #[serde(rename = "assertionMethod")]
256    pub assertion_method: Vec<String>,
257    #[serde(rename = "keyAgreement")]
258    pub key_agreement: Vec<String>,
259    pub proof: Proof,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub identity: Option<String>,
262    #[serde(rename = "createdAt")]
263    pub created_at: String,
264    #[serde(rename = "updatedAt")]
265    pub updated_at: String,
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub ma: Option<Ipld>,
268}
269
270impl Document {
271    pub fn new(identity: &Did, controller: &Did) -> Self {
272        let now = now_iso_utc();
273        Self {
274            context: DEFAULT_DID_CONTEXT
275                .iter()
276                .map(|value| (*value).to_string())
277                .collect(),
278            id: identity.base_id(),
279            controller: vec![controller.base_id()],
280            verification_method: Vec::new(),
281            assertion_method: Vec::new(),
282            key_agreement: Vec::new(),
283            proof: Proof::default(),
284            identity: None,
285            created_at: now.clone(),
286            updated_at: now,
287            ma: None,
288        }
289    }
290
291    /// Set the opaque `ma` extension namespace.
292    pub fn set_ma(&mut self, ma: Ipld) {
293        match &ma {
294            Ipld::Null => self.ma = None,
295            Ipld::Map(m) if m.is_empty() => self.ma = None,
296            _ => self.ma = Some(ma),
297        }
298    }
299
300    /// Clear the `ma` extension namespace.
301    pub fn clear_ma(&mut self) {
302        self.ma = None;
303    }
304
305    /// Encode the DID document to its canonical wire format.
306    ///
307    /// DID documents are always serialized as DAG-CBOR. Use this for
308    /// transport, storage, hashing, signing, and IPFS/IPNS publication.
309    pub fn encode(&self) -> Result<Vec<u8>> {
310        serde_ipld_dagcbor::to_vec(self).map_err(|error| MaError::CborEncode(error.to_string()))
311    }
312
313    /// Decode a DID document from its canonical wire format.
314    ///
315    /// DID documents are always encoded as DAG-CBOR.
316    pub fn decode(bytes: &[u8]) -> Result<Self> {
317        serde_ipld_dagcbor::from_slice(bytes)
318            .map_err(|error| MaError::CborDecode(error.to_string()))
319    }
320
321    pub fn add_controller(&mut self, controller: impl Into<String>) -> Result<()> {
322        let controller = controller.into();
323        Did::validate(&controller)?;
324        if !self.controller.contains(&controller) {
325            self.controller.push(controller);
326        }
327        Ok(())
328    }
329
330    pub fn add_verification_method(&mut self, method: VerificationMethod) -> Result<()> {
331        method.validate()?;
332        let duplicate = self.verification_method.iter().any(|existing| {
333            existing.id == method.id || existing.public_key_multibase == method.public_key_multibase
334        });
335
336        if !duplicate {
337            self.verification_method.push(method);
338        }
339
340        Ok(())
341    }
342
343    pub fn get_verification_method_by_id(&self, method_id: &str) -> Result<&VerificationMethod> {
344        self.verification_method
345            .iter()
346            .find(|method| method.id == method_id)
347            .ok_or_else(|| MaError::UnknownVerificationMethod(method_id.to_string()))
348    }
349
350    pub fn set_identity(&mut self, identity: impl Into<String>) -> Result<()> {
351        let identity = identity.into();
352        Cid::try_from(identity.as_str()).map_err(|_| MaError::InvalidIdentity)?;
353        self.identity = Some(identity);
354        Ok(())
355    }
356
357    /// Update the `updatedAt` timestamp to the current time.
358    pub fn touch(&mut self) {
359        self.updated_at = now_iso_utc();
360    }
361
362    pub fn assertion_method_public_key(&self) -> Result<VerifyingKey> {
363        let assertion_id = self
364            .assertion_method
365            .first()
366            .ok_or_else(|| MaError::UnknownVerificationMethod("assertionMethod".to_string()))?;
367        let vm = self.get_verification_method_by_id(assertion_id)?;
368        let (codec, public_key_bytes) = public_key_multibase_decode(&vm.public_key_multibase)?;
369        if codec != ED25519_PUB_CODEC {
370            return Err(MaError::InvalidMulticodec {
371                expected: ED25519_PUB_CODEC,
372                actual: codec,
373            });
374        }
375
376        let key_len = public_key_bytes.len();
377        let bytes: [u8; 32] =
378            public_key_bytes
379                .try_into()
380                .map_err(|_| MaError::InvalidKeyLength {
381                    expected: 32,
382                    actual: key_len,
383                })?;
384
385        VerifyingKey::from_bytes(&bytes).map_err(|_| MaError::Crypto)
386    }
387
388    pub fn key_agreement_public_key_bytes(&self) -> Result<[u8; 32]> {
389        let agreement_id = self
390            .key_agreement
391            .first()
392            .ok_or_else(|| MaError::UnknownVerificationMethod("keyAgreement".to_string()))?;
393        let vm = self.get_verification_method_by_id(agreement_id)?;
394        let (codec, public_key_bytes) = public_key_multibase_decode(&vm.public_key_multibase)?;
395        if codec != X25519_PUB_CODEC {
396            return Err(MaError::InvalidMulticodec {
397                expected: X25519_PUB_CODEC,
398                actual: codec,
399            });
400        }
401
402        let key_len = public_key_bytes.len();
403        public_key_bytes
404            .try_into()
405            .map_err(|_| MaError::InvalidKeyLength {
406                expected: 32,
407                actual: key_len,
408            })
409    }
410
411    #[must_use]
412    pub fn payload_document(&self) -> Self {
413        let mut payload = self.clone();
414        payload.proof = Proof::default();
415        payload
416    }
417
418    pub fn payload_bytes(&self) -> Result<Vec<u8>> {
419        self.payload_document().encode()
420    }
421
422    pub fn payload_hash(&self) -> Result<[u8; 32]> {
423        Ok(blake3::hash(&self.payload_bytes()?).into())
424    }
425
426    pub fn sign(
427        &mut self,
428        signing_key: &SigningKey,
429        verification_method: &VerificationMethod,
430    ) -> Result<()> {
431        if signing_key.public_key_multibase != verification_method.public_key_multibase {
432            return Err(MaError::InvalidPublicKeyMultibase);
433        }
434
435        let signature = signing_key.sign(&self.payload_hash()?);
436        let proof_value = signature_multibase_encode(EDDSA_SIG_CODEC, &signature);
437        self.proof = Proof::new(proof_value, verification_method.id.clone());
438        Ok(())
439    }
440
441    pub fn verify(&self) -> Result<()> {
442        if self.proof.is_empty() {
443            return Err(MaError::MissingProof);
444        }
445
446        let (codec, sig_bytes) = signature_multibase_decode(&self.proof.proof_value)?;
447        if codec != EDDSA_SIG_CODEC {
448            return Err(MaError::InvalidDocumentSignature);
449        }
450        let signature =
451            Signature::from_slice(&sig_bytes).map_err(|_| MaError::InvalidDocumentSignature)?;
452        let public_key = self.assertion_method_public_key()?;
453        public_key
454            .verify(&self.payload_hash()?, &signature)
455            .map_err(|_| MaError::InvalidDocumentSignature)
456    }
457
458    pub fn validate(&self) -> Result<()> {
459        if self.context.is_empty() {
460            return Err(MaError::EmptyContext);
461        }
462
463        Did::validate(&self.id)?;
464
465        if self.controller.is_empty() {
466            return Err(MaError::EmptyController);
467        }
468
469        for controller in &self.controller {
470            Did::validate(controller)?;
471        }
472
473        if let Some(identity) = &self.identity {
474            Cid::try_from(identity.as_str()).map_err(|_| MaError::InvalidIdentity)?;
475        }
476
477        if !is_valid_rfc3339_utc(&self.created_at) {
478            return Err(MaError::InvalidCreatedAt(self.created_at.clone()));
479        }
480
481        if !is_valid_rfc3339_utc(&self.updated_at) {
482            return Err(MaError::InvalidUpdatedAt(self.updated_at.clone()));
483        }
484
485        for method in &self.verification_method {
486            method.validate()?;
487        }
488
489        if self.assertion_method.is_empty() {
490            return Err(MaError::UnknownVerificationMethod(
491                "assertionMethod".to_string(),
492            ));
493        }
494
495        if self.key_agreement.is_empty() {
496            return Err(MaError::UnknownVerificationMethod(
497                "keyAgreement".to_string(),
498            ));
499        }
500
501        Ok(())
502    }
503}
504
505impl TryFrom<&[u8]> for Document {
506    type Error = MaError;
507
508    fn try_from(bytes: &[u8]) -> Result<Self> {
509        Self::decode(bytes)
510    }
511}
512
513impl TryFrom<&EncryptionKey> for VerificationMethod {
514    type Error = MaError;
515
516    fn try_from(value: &EncryptionKey) -> Result<Self> {
517        let fragment = value.did.fragment.clone().ok_or(MaError::MissingFragment)?;
518        VerificationMethod::new(
519            value.did.base_id(),
520            value.did.base_id(),
521            value.key_type.clone(),
522            fragment,
523            value.public_key_multibase.clone(),
524        )
525    }
526}
527
528impl TryFrom<&SigningKey> for VerificationMethod {
529    type Error = MaError;
530
531    fn try_from(value: &SigningKey) -> Result<Self> {
532        let fragment = value.did.fragment.clone().ok_or(MaError::MissingFragment)?;
533        VerificationMethod::new(
534            value.did.base_id(),
535            value.did.base_id(),
536            value.key_type.clone(),
537            fragment,
538            value.public_key_multibase.clone(),
539        )
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546    use std::collections::BTreeMap;
547
548    #[test]
549    fn encode_decode_round_trip() {
550        let identity = crate::generate_identity_from_secret([11u8; 32]).expect("identity");
551        let bytes = identity.document.encode().expect("encode");
552        let decoded = Document::decode(&bytes).expect("decode");
553        assert_eq!(decoded, identity.document);
554    }
555
556    #[test]
557    fn try_from_bytes_round_trip() {
558        let identity = crate::generate_identity_from_secret([12u8; 32]).expect("identity");
559        let bytes = identity.document.encode().expect("encode");
560        let decoded = Document::try_from(bytes.as_slice()).expect("try_from bytes");
561        assert_eq!(decoded, identity.document);
562    }
563
564    #[test]
565    fn decode_rejects_invalid_bytes() {
566        let err = Document::decode(b"not dag-cbor").expect_err("invalid bytes");
567        assert!(matches!(err, MaError::CborDecode(_)));
568    }
569
570    #[test]
571    fn payload_document_clears_proof_only() {
572        let identity = crate::generate_identity_from_secret([13u8; 32]).expect("identity");
573        let payload = identity.document.payload_document();
574
575        assert!(payload.proof.is_empty());
576        assert_eq!(payload.id, identity.document.id);
577        assert_eq!(payload.controller, identity.document.controller);
578        assert_eq!(
579            payload.verification_method,
580            identity.document.verification_method
581        );
582        assert_eq!(payload.assertion_method, identity.document.assertion_method);
583        assert_eq!(payload.key_agreement, identity.document.key_agreement);
584        assert_eq!(payload.identity, identity.document.identity);
585        assert_eq!(payload.created_at, identity.document.created_at);
586        assert_eq!(payload.updated_at, identity.document.updated_at);
587        assert_eq!(payload.ma, identity.document.ma);
588    }
589
590    #[test]
591    fn set_ma_stores_opaque_value() {
592        let root = Did::new_url(
593            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
594            None::<String>,
595        )
596        .expect("valid test did");
597        let mut document = Document::new(&root, &root);
598
599        let ma = Ipld::Map(BTreeMap::from([(
600            "type".into(),
601            Ipld::String("agent".into()),
602        )]));
603        document.set_ma(ma.clone());
604        assert_eq!(document.ma.as_ref(), Some(&ma));
605    }
606
607    #[test]
608    fn clear_ma_removes_value() {
609        let root = Did::new_url(
610            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
611            None::<String>,
612        )
613        .expect("valid test did");
614        let mut document = Document::new(&root, &root);
615
616        document.set_ma(Ipld::Map(BTreeMap::from([(
617            "type".into(),
618            Ipld::String("agent".into()),
619        )])));
620        assert!(document.ma.is_some());
621        document.clear_ma();
622        assert!(document.ma.is_none());
623    }
624
625    #[test]
626    fn set_ma_null_clears() {
627        let root = Did::new_url(
628            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
629            None::<String>,
630        )
631        .expect("valid test did");
632        let mut document = Document::new(&root, &root);
633
634        document.set_ma(Ipld::Map(BTreeMap::from([(
635            "type".into(),
636            Ipld::String("agent".into()),
637        )])));
638        document.set_ma(Ipld::Null);
639        assert!(document.ma.is_none());
640    }
641
642    #[test]
643    fn validate_accepts_opaque_ma() {
644        let identity = crate::identity::generate_identity(
645            "k51qzi5uqu5dj9807pbuod1pplf0vxh8m4lfy3ewl9qbm2s8dsf9ugdf9gedhr",
646        )
647        .expect("generate identity");
648        let mut document = identity.document;
649        document.set_ma(Ipld::Map(BTreeMap::from([
650            ("type".into(), Ipld::String("bahner".into())),
651            ("custom".into(), Ipld::Integer(42)),
652        ])));
653        document
654            .validate()
655            .expect("validate should accept any ma value");
656    }
657}