1use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use crate::DocumentId;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct SignatureFile {
16 pub version: String,
18
19 pub document_id: DocumentId,
21
22 pub signatures: Vec<Signature>,
24}
25
26impl SignatureFile {
27 #[must_use]
29 pub fn new(document_id: DocumentId) -> Self {
30 Self {
31 version: crate::SPEC_VERSION.to_string(),
32 document_id,
33 signatures: Vec::new(),
34 }
35 }
36
37 pub fn add_signature(&mut self, signature: Signature) {
39 self.signatures.push(signature);
40 }
41
42 #[must_use]
44 pub fn is_empty(&self) -> bool {
45 self.signatures.is_empty()
46 }
47
48 #[must_use]
50 pub fn len(&self) -> usize {
51 self.signatures.len()
52 }
53
54 pub fn to_json(&self) -> crate::Result<String> {
60 serde_json::to_string_pretty(self).map_err(Into::into)
61 }
62
63 pub fn from_json(json: &str) -> crate::Result<Self> {
69 serde_json::from_str(json).map_err(Into::into)
70 }
71
72 #[must_use]
74 pub fn find_signature(&self, id: &str) -> Option<&Signature> {
75 self.signatures.iter().find(|s| s.id == id)
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct Signature {
83 pub id: String,
85
86 pub algorithm: SignatureAlgorithm,
88
89 pub signed_at: DateTime<Utc>,
91
92 pub signer: SignerInfo,
94
95 #[serde(default, skip_serializing_if = "String::is_empty")]
100 pub value: String,
101
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub certificate_chain: Option<Vec<String>>,
105
106 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub scope: Option<SignatureScope>,
113
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub timestamp: Option<TrustedTimestamp>,
117
118 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub webauthn: Option<WebAuthnSignature>,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct WebAuthnSignature {
142 pub credential_id: String,
146
147 pub authenticator_data: String,
151
152 pub client_data_json: String,
156
157 pub signature: String,
162}
163
164impl WebAuthnSignature {
165 #[must_use]
167 pub fn new(
168 credential_id: impl Into<String>,
169 authenticator_data: impl Into<String>,
170 client_data_json: impl Into<String>,
171 signature: impl Into<String>,
172 ) -> Self {
173 Self {
174 credential_id: credential_id.into(),
175 authenticator_data: authenticator_data.into(),
176 client_data_json: client_data_json.into(),
177 signature: signature.into(),
178 }
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct TrustedTimestamp {
186 pub token: String,
188
189 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub tsa: Option<String>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct SignatureScope {
218 pub document_id: DocumentId,
220
221 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub layouts: Option<HashMap<String, DocumentId>>,
227}
228
229impl SignatureScope {
230 #[must_use]
232 pub fn new(document_id: DocumentId) -> Self {
233 Self {
234 document_id,
235 layouts: None,
236 }
237 }
238
239 #[must_use]
241 pub fn with_layouts(mut self, layouts: HashMap<String, DocumentId>) -> Self {
242 self.layouts = Some(layouts);
243 self
244 }
245
246 #[must_use]
248 pub fn with_layout(mut self, path: impl Into<String>, hash: DocumentId) -> Self {
249 self.layouts
250 .get_or_insert_with(HashMap::new)
251 .insert(path.into(), hash);
252 self
253 }
254
255 #[must_use]
257 pub fn has_layouts(&self) -> bool {
258 self.layouts.as_ref().is_some_and(|l| !l.is_empty())
259 }
260
261 pub fn to_jcs(&self) -> crate::Result<Vec<u8>> {
269 use crate::error::invalid_manifest;
270
271 let value = serde_json::to_value(self)?;
273 let canonical = json_canon::to_string(&value)
274 .map_err(|e| invalid_manifest(format!("JCS serialization failed: {e}")))?;
275 Ok(canonical.into_bytes())
276 }
277}
278
279impl Signature {
280 #[must_use]
282 pub fn new(
283 id: impl Into<String>,
284 algorithm: SignatureAlgorithm,
285 signer: SignerInfo,
286 value: impl Into<String>,
287 ) -> Self {
288 Self {
289 id: id.into(),
290 algorithm,
291 signed_at: Utc::now(),
292 signer,
293 value: value.into(),
294 certificate_chain: None,
295 scope: None,
296 timestamp: None,
297 webauthn: None,
298 }
299 }
300
301 #[must_use]
306 pub fn new_webauthn(
307 id: impl Into<String>,
308 signer: SignerInfo,
309 webauthn: WebAuthnSignature,
310 ) -> Self {
311 Self {
312 id: id.into(),
313 algorithm: SignatureAlgorithm::ES256,
314 signed_at: Utc::now(),
315 signer,
316 value: String::new(),
317 certificate_chain: None,
318 scope: None,
319 timestamp: None,
320 webauthn: Some(webauthn),
321 }
322 }
323
324 #[must_use]
326 pub fn with_scope(mut self, scope: SignatureScope) -> Self {
327 self.scope = Some(scope);
328 self
329 }
330
331 #[must_use]
333 pub fn with_timestamp(mut self, timestamp: TrustedTimestamp) -> Self {
334 self.timestamp = Some(timestamp);
335 self
336 }
337
338 #[must_use]
340 pub fn is_scoped(&self) -> bool {
341 self.scope.is_some()
342 }
343
344 #[must_use]
346 pub fn is_webauthn(&self) -> bool {
347 self.webauthn.is_some()
348 }
349
350 #[must_use]
352 pub fn webauthn_data(&self) -> Option<&WebAuthnSignature> {
353 self.webauthn.as_ref()
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display)]
359pub enum SignatureAlgorithm {
360 ES256,
362 ES384,
364 EdDSA,
366 PS256,
368 #[serde(rename = "ML-DSA-65")]
370 #[strum(serialize = "ML-DSA-65")]
371 MlDsa65,
372}
373
374impl SignatureAlgorithm {
375 #[must_use]
377 pub const fn as_str(&self) -> &'static str {
378 match self {
379 Self::ES256 => "ES256",
380 Self::ES384 => "ES384",
381 Self::EdDSA => "EdDSA",
382 Self::PS256 => "PS256",
383 Self::MlDsa65 => "ML-DSA-65",
384 }
385 }
386
387 #[must_use]
389 pub const fn is_post_quantum(&self) -> bool {
390 matches!(self, Self::MlDsa65)
391 }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct SignerInfo {
398 pub name: String,
400
401 #[serde(default, skip_serializing_if = "Option::is_none")]
403 pub email: Option<String>,
404
405 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub organization: Option<String>,
408
409 #[serde(default, skip_serializing_if = "Option::is_none")]
411 pub certificate: Option<String>,
412
413 #[serde(default, skip_serializing_if = "Option::is_none")]
415 pub key_id: Option<String>,
416}
417
418impl SignerInfo {
419 #[must_use]
421 pub fn new(name: impl Into<String>) -> Self {
422 Self {
423 name: name.into(),
424 email: None,
425 organization: None,
426 certificate: None,
427 key_id: None,
428 }
429 }
430
431 #[must_use]
433 pub fn with_email(mut self, email: impl Into<String>) -> Self {
434 self.email = Some(email.into());
435 self
436 }
437
438 #[must_use]
440 pub fn with_organization(mut self, org: impl Into<String>) -> Self {
441 self.organization = Some(org.into());
442 self
443 }
444}
445
446#[derive(Debug, Clone, PartialEq, Eq)]
448pub struct SignatureVerification {
449 pub signature_id: String,
451
452 pub status: VerificationStatus,
454
455 pub error: Option<String>,
457}
458
459impl SignatureVerification {
460 #[must_use]
462 pub fn valid(signature_id: impl Into<String>) -> Self {
463 Self {
464 signature_id: signature_id.into(),
465 status: VerificationStatus::Valid,
466 error: None,
467 }
468 }
469
470 #[must_use]
472 pub fn invalid(signature_id: impl Into<String>, error: impl Into<String>) -> Self {
473 Self {
474 signature_id: signature_id.into(),
475 status: VerificationStatus::Invalid,
476 error: Some(error.into()),
477 }
478 }
479
480 #[must_use]
482 pub fn is_valid(&self) -> bool {
483 self.status == VerificationStatus::Valid
484 }
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum VerificationStatus {
490 Valid,
492 Invalid,
494 Expired,
496 Revoked,
498 Untrusted,
500 Unknown,
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507 use crate::HashAlgorithm;
508
509 #[test]
510 fn test_signature_file_new() {
511 let doc_id = crate::Hasher::hash(HashAlgorithm::Sha256, b"test");
512 let file = SignatureFile::new(doc_id);
513 assert_eq!(file.version, "0.1");
514 assert!(file.is_empty());
515 }
516
517 #[test]
518 fn test_signature_new() {
519 let signer = SignerInfo::new("Test User").with_email("test@example.com");
520 let sig = Signature::new("sig-1", SignatureAlgorithm::ES256, signer, "base64value");
521
522 assert_eq!(sig.id, "sig-1");
523 assert_eq!(sig.algorithm, SignatureAlgorithm::ES256);
524 assert_eq!(sig.value, "base64value");
525 }
526
527 #[test]
528 fn test_signer_info() {
529 let info = SignerInfo::new("Alice")
530 .with_email("alice@example.com")
531 .with_organization("Acme Corp");
532
533 assert_eq!(info.name, "Alice");
534 assert_eq!(info.email, Some("alice@example.com".to_string()));
535 assert_eq!(info.organization, Some("Acme Corp".to_string()));
536 }
537
538 #[test]
539 fn test_serialization() {
540 let doc_id = crate::Hasher::hash(HashAlgorithm::Sha256, b"test");
541 let mut file = SignatureFile::new(doc_id);
542
543 let signer = SignerInfo::new("Test User");
544 let sig = Signature::new("sig-1", SignatureAlgorithm::ES256, signer, "base64value");
545 file.add_signature(sig);
546
547 let json = serde_json::to_string_pretty(&file).unwrap();
548 assert!(json.contains("\"algorithm\": \"ES256\""));
549 assert!(json.contains("\"documentId\":"));
550 }
551
552 #[test]
553 fn test_verification_result() {
554 let valid = SignatureVerification::valid("sig-1");
555 assert!(valid.is_valid());
556
557 let invalid = SignatureVerification::invalid("sig-2", "bad signature");
558 assert!(!invalid.is_valid());
559 assert_eq!(invalid.error, Some("bad signature".to_string()));
560 }
561
562 #[test]
563 fn test_signature_scope_new() {
564 let doc_id = crate::Hasher::hash(HashAlgorithm::Sha256, b"test");
565 let scope = SignatureScope::new(doc_id.clone());
566
567 assert_eq!(scope.document_id, doc_id);
568 assert!(scope.layouts.is_none());
569 assert!(!scope.has_layouts());
570 }
571
572 #[test]
573 fn test_signature_scope_with_layouts() {
574 let doc_id = crate::Hasher::hash(HashAlgorithm::Sha256, b"test");
575 let layout_hash = crate::Hasher::hash(HashAlgorithm::Sha256, b"layout");
576
577 let scope =
578 SignatureScope::new(doc_id).with_layout("presentation/print.json", layout_hash.clone());
579
580 assert!(scope.has_layouts());
581 let layouts = scope.layouts.as_ref().unwrap();
582 assert_eq!(layouts.get("presentation/print.json"), Some(&layout_hash));
583 }
584
585 #[test]
586 fn test_signature_scope_jcs_serialization() {
587 let doc_id = crate::Hasher::hash(HashAlgorithm::Sha256, b"test");
588 let scope = SignatureScope::new(doc_id);
589
590 let jcs = scope.to_jcs().unwrap();
591 assert!(!jcs.is_empty());
592 let json_str = String::from_utf8(jcs).unwrap();
594 assert!(json_str.contains("documentId"));
595 }
596
597 #[test]
598 fn test_scoped_signature() {
599 let doc_id = crate::Hasher::hash(HashAlgorithm::Sha256, b"test");
600 let scope = SignatureScope::new(doc_id);
601
602 let signer = SignerInfo::new("Test User");
603 let sig = Signature::new("sig-1", SignatureAlgorithm::ES256, signer, "base64value")
604 .with_scope(scope);
605
606 assert!(sig.is_scoped());
607 assert!(sig.scope.is_some());
608 }
609
610 #[test]
611 fn test_signature_scope_serialization() {
612 let doc_id = crate::Hasher::hash(HashAlgorithm::Sha256, b"test");
613 let layout_hash = crate::Hasher::hash(HashAlgorithm::Sha256, b"layout");
614
615 let scope = SignatureScope::new(doc_id).with_layout("presentation/print.json", layout_hash);
616
617 let json = serde_json::to_string(&scope).unwrap();
618 assert!(json.contains("\"documentId\":"));
619 assert!(json.contains("\"layouts\":"));
620 assert!(json.contains("presentation/print.json"));
621
622 let parsed: SignatureScope = serde_json::from_str(&json).unwrap();
624 assert_eq!(parsed, scope);
625 }
626
627 #[test]
628 fn test_signature_algorithm_display() {
629 assert_eq!(SignatureAlgorithm::ES256.to_string(), "ES256");
630 assert_eq!(SignatureAlgorithm::ES384.to_string(), "ES384");
631 assert_eq!(SignatureAlgorithm::EdDSA.to_string(), "EdDSA");
632 assert_eq!(SignatureAlgorithm::PS256.to_string(), "PS256");
633 assert_eq!(SignatureAlgorithm::MlDsa65.to_string(), "ML-DSA-65");
634 }
635
636 #[test]
637 fn test_signature_algorithm_as_str() {
638 assert_eq!(SignatureAlgorithm::ES256.as_str(), "ES256");
639 assert_eq!(SignatureAlgorithm::ES384.as_str(), "ES384");
640 assert_eq!(SignatureAlgorithm::EdDSA.as_str(), "EdDSA");
641 assert_eq!(SignatureAlgorithm::PS256.as_str(), "PS256");
642 assert_eq!(SignatureAlgorithm::MlDsa65.as_str(), "ML-DSA-65");
643 }
644
645 #[test]
646 fn test_signature_algorithm_is_post_quantum() {
647 assert!(!SignatureAlgorithm::ES256.is_post_quantum());
648 assert!(!SignatureAlgorithm::ES384.is_post_quantum());
649 assert!(!SignatureAlgorithm::EdDSA.is_post_quantum());
650 assert!(!SignatureAlgorithm::PS256.is_post_quantum());
651 assert!(SignatureAlgorithm::MlDsa65.is_post_quantum());
652 }
653
654 #[test]
655 fn test_signature_algorithm_serialization() {
656 let json = serde_json::to_string(&SignatureAlgorithm::ES256).unwrap();
658 assert_eq!(json, "\"ES256\"");
659
660 let json = serde_json::to_string(&SignatureAlgorithm::MlDsa65).unwrap();
661 assert_eq!(json, "\"ML-DSA-65\"");
662
663 let algo: SignatureAlgorithm = serde_json::from_str("\"EdDSA\"").unwrap();
665 assert_eq!(algo, SignatureAlgorithm::EdDSA);
666 }
667
668 #[test]
669 fn test_signature_file_roundtrip() {
670 let doc_id = crate::Hasher::hash(HashAlgorithm::Sha256, b"test document");
671 let mut file = SignatureFile::new(doc_id.clone());
672
673 let signer = SignerInfo::new("Test User").with_email("test@example.com");
674 let sig = Signature::new("sig-1", SignatureAlgorithm::ES256, signer, "base64value");
675 file.add_signature(sig);
676
677 let json = file.to_json().unwrap();
678 let parsed = SignatureFile::from_json(&json).unwrap();
679
680 assert_eq!(parsed.document_id, doc_id);
681 assert_eq!(parsed.signatures.len(), 1);
682 assert_eq!(parsed.signatures[0].id, "sig-1");
683 }
684
685 #[test]
686 fn test_signature_file_find_signature() {
687 let doc_id = crate::Hasher::hash(HashAlgorithm::Sha256, b"test");
688 let mut file = SignatureFile::new(doc_id);
689
690 let signer1 = SignerInfo::new("User 1");
691 let signer2 = SignerInfo::new("User 2");
692 file.add_signature(Signature::new(
693 "sig-1",
694 SignatureAlgorithm::ES256,
695 signer1,
696 "val1",
697 ));
698 file.add_signature(Signature::new(
699 "sig-2",
700 SignatureAlgorithm::EdDSA,
701 signer2,
702 "val2",
703 ));
704
705 assert!(file.find_signature("sig-1").is_some());
706 assert!(file.find_signature("sig-2").is_some());
707 assert!(file.find_signature("sig-3").is_none());
708
709 let sig1 = file.find_signature("sig-1").unwrap();
710 assert_eq!(sig1.algorithm, SignatureAlgorithm::ES256);
711 }
712
713 #[test]
714 fn test_signature_file_len() {
715 let doc_id = crate::Hasher::hash(HashAlgorithm::Sha256, b"test");
716 let mut file = SignatureFile::new(doc_id);
717
718 assert_eq!(file.len(), 0);
719 assert!(file.is_empty());
720
721 let signer = SignerInfo::new("User");
722 file.add_signature(Signature::new(
723 "sig-1",
724 SignatureAlgorithm::ES256,
725 signer,
726 "val",
727 ));
728
729 assert_eq!(file.len(), 1);
730 assert!(!file.is_empty());
731 }
732
733 #[test]
734 fn test_webauthn_signature() {
735 let signer = SignerInfo::new("WebAuthn User");
736 let webauthn = WebAuthnSignature::new(
737 "credential-id-base64",
738 "authenticator-data-base64",
739 "client-data-json-base64",
740 "signature-base64",
741 );
742
743 let sig = Signature::new_webauthn("sig-webauthn", signer, webauthn);
744
745 assert!(sig.is_webauthn());
746 assert!(!sig.is_scoped());
747 assert_eq!(sig.algorithm, SignatureAlgorithm::ES256);
748 assert!(sig.value.is_empty());
749
750 let webauthn_data = sig.webauthn_data().unwrap();
751 assert_eq!(webauthn_data.credential_id, "credential-id-base64");
752 }
753
754 #[test]
755 fn test_webauthn_signature_serialization() {
756 let signer = SignerInfo::new("WebAuthn User");
757 let webauthn = WebAuthnSignature::new("cred-id", "auth-data", "client-data", "sig-value");
758
759 let sig = Signature::new_webauthn("sig-1", signer, webauthn);
760 let json = serde_json::to_string_pretty(&sig).unwrap();
761
762 assert!(json.contains("\"webauthn\":"));
763 assert!(json.contains("\"credentialId\":"));
764 assert!(json.contains("\"authenticatorData\":"));
765 assert!(json.contains("\"clientDataJson\":"));
766
767 let parsed: Signature = serde_json::from_str(&json).unwrap();
769 assert!(parsed.is_webauthn());
770 assert_eq!(parsed.webauthn_data().unwrap().credential_id, "cred-id");
771 }
772
773 #[test]
774 fn test_verification_status_variants() {
775 let valid = SignatureVerification::valid("sig-1");
776 assert_eq!(valid.status, VerificationStatus::Valid);
777
778 let invalid = SignatureVerification::invalid("sig-2", "bad sig");
779 assert_eq!(invalid.status, VerificationStatus::Invalid);
780
781 assert!(matches!(
783 VerificationStatus::Valid,
784 VerificationStatus::Valid
785 ));
786 assert!(matches!(
787 VerificationStatus::Invalid,
788 VerificationStatus::Invalid
789 ));
790 assert!(matches!(
791 VerificationStatus::Expired,
792 VerificationStatus::Expired
793 ));
794 assert!(matches!(
795 VerificationStatus::Revoked,
796 VerificationStatus::Revoked
797 ));
798 assert!(matches!(
799 VerificationStatus::Untrusted,
800 VerificationStatus::Untrusted
801 ));
802 assert!(matches!(
803 VerificationStatus::Unknown,
804 VerificationStatus::Unknown
805 ));
806 }
807
808 #[test]
809 fn test_trusted_timestamp_roundtrip() {
810 let ts = TrustedTimestamp {
811 token: "base64-timestamp-token".to_string(),
812 tsa: Some("https://tsa.example.com".to_string()),
813 };
814 let json = serde_json::to_string(&ts).unwrap();
815 assert!(json.contains("\"token\":"));
816 assert!(json.contains("\"tsa\":"));
817
818 let parsed: TrustedTimestamp = serde_json::from_str(&json).unwrap();
819 assert_eq!(parsed, ts);
820 }
821
822 #[test]
823 fn test_trusted_timestamp_without_tsa() {
824 let ts = TrustedTimestamp {
825 token: "base64-timestamp-token".to_string(),
826 tsa: None,
827 };
828 let json = serde_json::to_string(&ts).unwrap();
829 assert!(!json.contains("tsa"));
830
831 let parsed: TrustedTimestamp = serde_json::from_str(&json).unwrap();
832 assert_eq!(parsed, ts);
833 }
834
835 #[test]
836 fn test_signature_with_timestamp_builder() {
837 let signer = SignerInfo::new("Test User");
838 let ts = TrustedTimestamp {
839 token: "base64-timestamp-token".to_string(),
840 tsa: Some("https://tsa.example.com".to_string()),
841 };
842 let sig = Signature::new("sig-1", SignatureAlgorithm::ES256, signer, "base64value")
843 .with_timestamp(ts.clone());
844
845 assert_eq!(sig.timestamp, Some(ts));
846 }
847
848 #[test]
849 fn test_signature_backward_compat_no_timestamp() {
850 let json = r#"{
852 "id": "sig-1",
853 "algorithm": "ES256",
854 "signedAt": "2024-01-01T00:00:00Z",
855 "signer": { "name": "Test User" },
856 "value": "base64value"
857 }"#;
858 let sig: Signature = serde_json::from_str(json).unwrap();
859 assert!(sig.timestamp.is_none());
860 }
861}