Skip to main content

cdx_core/security/
signature.rs

1//! Signature types and structures.
2
3use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use crate::DocumentId;
9
10/// Signature file structure.
11///
12/// This represents the `security/signatures.json` file in a Codex document.
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct SignatureFile {
16    /// Format version.
17    pub version: String,
18
19    /// Document ID that was signed.
20    pub document_id: DocumentId,
21
22    /// Array of signatures.
23    pub signatures: Vec<Signature>,
24}
25
26impl SignatureFile {
27    /// Create a new signature file.
28    #[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    /// Add a signature to the file.
38    pub fn add_signature(&mut self, signature: Signature) {
39        self.signatures.push(signature);
40    }
41
42    /// Check if the file has any signatures.
43    #[must_use]
44    pub fn is_empty(&self) -> bool {
45        self.signatures.is_empty()
46    }
47
48    /// Get the number of signatures.
49    #[must_use]
50    pub fn len(&self) -> usize {
51        self.signatures.len()
52    }
53
54    /// Serialize to JSON.
55    ///
56    /// # Errors
57    ///
58    /// Returns an error if serialization fails.
59    pub fn to_json(&self) -> crate::Result<String> {
60        serde_json::to_string_pretty(self).map_err(Into::into)
61    }
62
63    /// Deserialize from JSON.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if deserialization fails.
68    pub fn from_json(json: &str) -> crate::Result<Self> {
69        serde_json::from_str(json).map_err(Into::into)
70    }
71
72    /// Find a signature by ID.
73    #[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/// A digital signature.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct Signature {
83    /// Unique signature identifier.
84    pub id: String,
85
86    /// Signature algorithm.
87    pub algorithm: SignatureAlgorithm,
88
89    /// Signing timestamp.
90    pub signed_at: DateTime<Utc>,
91
92    /// Signer information.
93    pub signer: SignerInfo,
94
95    /// Base64-encoded signature value.
96    ///
97    /// For standard signatures, this contains the signature bytes.
98    /// For WebAuthn signatures, this may be empty if `webauthn` is present.
99    #[serde(default, skip_serializing_if = "String::is_empty")]
100    pub value: String,
101
102    /// Optional certificate chain.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub certificate_chain: Option<Vec<String>>,
105
106    /// Optional signature scope for layout attestation.
107    ///
108    /// When present, the signature covers the scope object (serialized with JCS)
109    /// instead of just the document ID. This allows signatures to attest to
110    /// specific layout renditions in addition to the semantic content.
111    #[serde(default, skip_serializing_if = "Option::is_none")]
112    pub scope: Option<SignatureScope>,
113
114    /// Optional RFC 3161 trusted timestamp token.
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub timestamp: Option<TrustedTimestamp>,
117
118    /// Optional WebAuthn/FIDO2 signature data.
119    ///
120    /// When present, this contains the full WebAuthn assertion data
121    /// instead of using the `value` field. The document ID is used
122    /// as the challenge for WebAuthn verification.
123    #[serde(default, skip_serializing_if = "Option::is_none")]
124    pub webauthn: Option<WebAuthnSignature>,
125}
126
127/// WebAuthn/FIDO2 signature data.
128///
129/// Contains the assertion response from a WebAuthn authenticator.
130/// All fields are base64-encoded as per the Codex specification.
131///
132/// # Verification
133///
134/// To verify a WebAuthn signature:
135/// 1. Decode all base64 fields
136/// 2. Parse `client_data_json` and verify the challenge matches the document ID
137/// 3. Verify the authenticator data flags and counter
138/// 4. Verify the signature over authenticator data + SHA-256(client data JSON)
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct WebAuthnSignature {
142    /// Base64-encoded credential ID.
143    ///
144    /// Identifies the credential used for signing.
145    pub credential_id: String,
146
147    /// Base64-encoded authenticator data.
148    ///
149    /// Contains the RP ID hash, flags, and signature counter.
150    pub authenticator_data: String,
151
152    /// Base64-encoded client data JSON.
153    ///
154    /// Contains the challenge (document ID), origin, and type.
155    pub client_data_json: String,
156
157    /// Base64-encoded signature.
158    ///
159    /// The cryptographic signature over the authenticator data
160    /// concatenated with the SHA-256 hash of the client data JSON.
161    pub signature: String,
162}
163
164impl WebAuthnSignature {
165    /// Create a new WebAuthn signature.
166    #[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/// RFC 3161 trusted timestamp token.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct TrustedTimestamp {
186    /// Base64-encoded RFC 3161 timestamp token.
187    pub token: String,
188
189    /// TSA (Time Stamping Authority) URL.
190    #[serde(default, skip_serializing_if = "Option::is_none")]
191    pub tsa: Option<String>,
192}
193
194/// Signature scope for scoped signatures.
195///
196/// Scoped signatures allow attesting to specific layout renditions
197/// in addition to the semantic content. The scope is serialized using
198/// JSON Canonicalization Scheme (JCS, RFC 8785) before signing.
199///
200/// # Example
201///
202/// ```rust
203/// use cdx_core::security::SignatureScope;
204/// use cdx_core::{HashAlgorithm, Hasher};
205/// use std::collections::HashMap;
206///
207/// let doc_id = Hasher::hash(HashAlgorithm::Sha256, b"content");
208/// let layout_hash = Hasher::hash(HashAlgorithm::Sha256, b"layout");
209///
210/// let mut layouts = HashMap::new();
211/// layouts.insert("presentation/print.json".to_string(), layout_hash);
212///
213/// let scope = SignatureScope::new(doc_id).with_layouts(layouts);
214/// ```
215#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "camelCase")]
217pub struct SignatureScope {
218    /// Document ID that must match the top-level document ID.
219    pub document_id: DocumentId,
220
221    /// Layout paths and their content hashes for layout attestation.
222    ///
223    /// Keys are presentation file paths (e.g., "presentation/print.json"),
224    /// values are the content hashes of those files.
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub layouts: Option<HashMap<String, DocumentId>>,
227}
228
229impl SignatureScope {
230    /// Create a new signature scope for a document.
231    #[must_use]
232    pub fn new(document_id: DocumentId) -> Self {
233        Self {
234            document_id,
235            layouts: None,
236        }
237    }
238
239    /// Add layout attestations.
240    #[must_use]
241    pub fn with_layouts(mut self, layouts: HashMap<String, DocumentId>) -> Self {
242        self.layouts = Some(layouts);
243        self
244    }
245
246    /// Add a single layout attestation.
247    #[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    /// Check if this scope attests to any layouts.
256    #[must_use]
257    pub fn has_layouts(&self) -> bool {
258        self.layouts.as_ref().is_some_and(|l| !l.is_empty())
259    }
260
261    /// Serialize the scope using JSON Canonicalization Scheme (JCS).
262    ///
263    /// This produces a deterministic JSON representation suitable for signing.
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if serialization fails.
268    pub fn to_jcs(&self) -> crate::Result<Vec<u8>> {
269        use crate::error::invalid_manifest;
270
271        // Use json-canon for JCS serialization
272        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    /// Create a new signature.
281    #[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    /// Create a new WebAuthn signature.
302    ///
303    /// WebAuthn signatures use ES256 algorithm and store the full
304    /// assertion data rather than a simple signature value.
305    #[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    /// Set the signature scope for layout attestation.
325    #[must_use]
326    pub fn with_scope(mut self, scope: SignatureScope) -> Self {
327        self.scope = Some(scope);
328        self
329    }
330
331    /// Set the trusted timestamp for this signature.
332    #[must_use]
333    pub fn with_timestamp(mut self, timestamp: TrustedTimestamp) -> Self {
334        self.timestamp = Some(timestamp);
335        self
336    }
337
338    /// Check if this signature has a scope (scoped signature).
339    #[must_use]
340    pub fn is_scoped(&self) -> bool {
341        self.scope.is_some()
342    }
343
344    /// Check if this is a WebAuthn signature.
345    #[must_use]
346    pub fn is_webauthn(&self) -> bool {
347        self.webauthn.is_some()
348    }
349
350    /// Get the WebAuthn signature data, if present.
351    #[must_use]
352    pub fn webauthn_data(&self) -> Option<&WebAuthnSignature> {
353        self.webauthn.as_ref()
354    }
355}
356
357/// Signature algorithm.
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display)]
359pub enum SignatureAlgorithm {
360    /// ECDSA with P-256 (required).
361    ES256,
362    /// ECDSA with P-384 (recommended).
363    ES384,
364    /// Edwards-curve Digital Signature Algorithm (recommended).
365    EdDSA,
366    /// RSA-PSS with SHA-256 (optional).
367    PS256,
368    /// ML-DSA-65 post-quantum signature (FIPS-204).
369    #[serde(rename = "ML-DSA-65")]
370    #[strum(serialize = "ML-DSA-65")]
371    MlDsa65,
372}
373
374impl SignatureAlgorithm {
375    /// Get the algorithm identifier string.
376    #[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    /// Check if this is a post-quantum algorithm.
388    #[must_use]
389    pub const fn is_post_quantum(&self) -> bool {
390        matches!(self, Self::MlDsa65)
391    }
392}
393
394/// Information about the signer.
395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct SignerInfo {
398    /// Signer's display name.
399    pub name: String,
400
401    /// Signer's email address.
402    #[serde(default, skip_serializing_if = "Option::is_none")]
403    pub email: Option<String>,
404
405    /// Signer's organization.
406    #[serde(default, skip_serializing_if = "Option::is_none")]
407    pub organization: Option<String>,
408
409    /// X.509 certificate (PEM format).
410    #[serde(default, skip_serializing_if = "Option::is_none")]
411    pub certificate: Option<String>,
412
413    /// Key identifier (DID, URL, etc.).
414    #[serde(default, skip_serializing_if = "Option::is_none")]
415    pub key_id: Option<String>,
416}
417
418impl SignerInfo {
419    /// Create new signer info with just a name.
420    #[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    /// Set the email address.
432    #[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    /// Set the organization.
439    #[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/// Result of signature verification.
447#[derive(Debug, Clone, PartialEq, Eq)]
448pub struct SignatureVerification {
449    /// Signature ID.
450    pub signature_id: String,
451
452    /// Verification status.
453    pub status: VerificationStatus,
454
455    /// Error message if verification failed.
456    pub error: Option<String>,
457}
458
459impl SignatureVerification {
460    /// Create a successful verification result.
461    #[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    /// Create a failed verification result.
471    #[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    /// Check if the verification passed.
481    #[must_use]
482    pub fn is_valid(&self) -> bool {
483        self.status == VerificationStatus::Valid
484    }
485}
486
487/// Signature verification status.
488#[derive(Debug, Clone, Copy, PartialEq, Eq)]
489pub enum VerificationStatus {
490    /// Signature verifies correctly.
491    Valid,
492    /// Signature does not verify.
493    Invalid,
494    /// Certificate has expired.
495    Expired,
496    /// Certificate has been revoked.
497    Revoked,
498    /// Certificate chain not trusted.
499    Untrusted,
500    /// Cannot determine validity.
501    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        // JCS should produce valid JSON
593        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        // Roundtrip
623        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        // Test each algorithm variant serializes correctly
657        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        // Test deserialization
664        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        // Roundtrip
768        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        // Test all status variants exist
782        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        // JSON without timestamp field should deserialize fine
851        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}