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