Skip to main content

ma_did/
doc.rs

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