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
20pub 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#[derive(Debug, Default, Clone)]
150pub struct MaExtension {
151 map: std::collections::BTreeMap<String, Ipld>,
152}
153
154impl MaExtension {
155 pub fn new() -> Self {
157 Self::default()
158 }
159
160 #[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 #[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 #[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 #[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 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#[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 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 pub fn set_ma_extension(&mut self, ext: MaExtension) {
338 self.set_ma(ext.build());
339 }
340
341 pub fn clear_ma(&mut self) {
343 self.ma = None;
344 }
345
346 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 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 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(×tamp));
904 assert!(!timestamp.contains('.'));
905 }
906}