Skip to main content

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