Skip to main content

claim169_core/
decode.rs

1//! Decoder builder for parsing Claim 169 QR codes.
2//!
3//! The [`Decoder`] provides a fluent builder API for decoding identity credentials
4//! from QR-scanned Base45 strings.
5//!
6//! # Basic Usage
7//!
8//! ```rust,ignore
9//! use claim169_core::Decoder;
10//!
11//! // Decode without verification (testing only)
12//! let result = Decoder::new(qr_text)
13//!     .allow_unverified()
14//!     .decode()?;
15//!
16//! // Decode with Ed25519 verification
17//! let result = Decoder::new(qr_text)
18//!     .verify_with_ed25519(&public_key)?
19//!     .decode()?;
20//!
21//! // Decode encrypted payload
22//! let result = Decoder::new(qr_text)
23//!     .decrypt_with_aes256(&aes_key)?
24//!     .verify_with_ed25519(&public_key)?
25//!     .decode()?;
26//! ```
27//!
28//! # HSM Integration
29//!
30//! For hardware security modules, use the generic `verify_with()` method:
31//!
32//! ```rust,ignore
33//! use claim169_core::{Decoder, SignatureVerifier};
34//!
35//! struct HsmVerifier { /* ... */ }
36//! impl SignatureVerifier for HsmVerifier { /* ... */ }
37//!
38//! let result = Decoder::new(qr_text)
39//!     .verify_with(hsm_verifier)
40//!     .decode()?;
41//! ```
42
43use crate::crypto::traits::{Decryptor, KeyResolver, SignatureVerifier};
44use crate::error::{Claim169Error, Result};
45use crate::model::VerificationStatus;
46use crate::pipeline;
47use crate::pipeline::decompress::DetectedCompression;
48use crate::{DecodeResult, Warning, WarningCode};
49
50#[cfg(feature = "software-crypto")]
51use crate::crypto::software::{AesGcmDecryptor, EcdsaP256Verifier, Ed25519Verifier};
52
53use std::time::{SystemTime, UNIX_EPOCH};
54
55/// Default maximum decompressed size (64KB)
56pub(crate) const DEFAULT_MAX_DECOMPRESSED_BYTES: usize = 65536;
57
58/// Builder for decoding Claim 169 credentials from QR strings.
59///
60/// The decoder follows a builder pattern where configuration methods return `Self`
61/// and the final `decode()` method consumes the builder to produce the result.
62///
63/// # Operation Order
64///
65/// When both decryption and verification are configured, the credential is always
66/// **decrypted first, then verified** (reverse of encoding), regardless of the order
67/// in which builder methods are called.
68///
69/// # Security
70///
71/// - By default, decoding requires signature verification
72/// - Use [`allow_unverified()`](Self::allow_unverified) to explicitly opt out
73/// - Decompression is limited to prevent zip bomb attacks (default: 64KB)
74///
75/// # Example
76///
77/// ```rust,ignore
78/// use claim169_core::Decoder;
79///
80/// let result = Decoder::new("6BF5YZB2...")
81///     .verify_with_ed25519(&public_key)?
82///     .decode()?;
83///
84/// println!("ID: {:?}", result.claim169.id);
85/// println!("Verified: {:?}", result.verification_status);
86/// ```
87pub struct Decoder {
88    qr_text: String,
89    verifier: Option<Box<dyn SignatureVerifier + Send + Sync>>,
90    decryptor: Option<Box<dyn Decryptor + Send + Sync>>,
91    resolver: Option<Box<dyn KeyResolver + Send + Sync>>,
92    allow_unverified: bool,
93    skip_biometrics: bool,
94    validate_timestamps: bool,
95    clock_skew_tolerance_seconds: i64,
96    max_decompressed_bytes: usize,
97    strict_compression: bool,
98}
99
100impl Decoder {
101    /// Create a new decoder with the given QR text.
102    ///
103    /// # Arguments
104    ///
105    /// * `qr_text` - The Base45-encoded QR code content (accepts `&str` or `String`)
106    ///
107    /// # Example
108    ///
109    /// ```rust,ignore
110    /// let decoder = Decoder::new("6BF5YZB2...");
111    /// let decoder = Decoder::new(qr_string);
112    /// ```
113    pub fn new(qr_text: impl Into<String>) -> Self {
114        Self {
115            qr_text: qr_text.into(),
116            verifier: None,
117            decryptor: None,
118            resolver: None,
119            allow_unverified: false,
120            skip_biometrics: false,
121            validate_timestamps: true,
122            clock_skew_tolerance_seconds: 0,
123            max_decompressed_bytes: DEFAULT_MAX_DECOMPRESSED_BYTES,
124            strict_compression: false,
125        }
126    }
127
128    /// Verify with a custom verifier implementation.
129    ///
130    /// Use this method for HSM integration or custom cryptographic backends.
131    ///
132    /// # Arguments
133    ///
134    /// * `verifier` - A type implementing the [`SignatureVerifier`] trait
135    ///
136    /// # Example
137    ///
138    /// ```rust,ignore
139    /// let decoder = Decoder::new(qr_text)
140    ///     .verify_with(hsm_verifier);
141    /// ```
142    pub fn verify_with<V: SignatureVerifier + 'static>(mut self, verifier: V) -> Self {
143        self.verifier = Some(Box::new(verifier));
144        self
145    }
146
147    /// Verify with an Ed25519 public key.
148    ///
149    /// The key is validated immediately and an error is returned if invalid.
150    ///
151    /// # Arguments
152    ///
153    /// * `public_key` - 32-byte Ed25519 public key
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if the public key is invalid or represents a weak key.
158    ///
159    /// # Example
160    ///
161    /// ```rust,ignore
162    /// let decoder = Decoder::new(qr_text)
163    ///     .verify_with_ed25519(&public_key)?;
164    /// ```
165    #[cfg(feature = "software-crypto")]
166    pub fn verify_with_ed25519(self, public_key: &[u8]) -> Result<Self> {
167        let verifier = Ed25519Verifier::from_bytes(public_key)
168            .map_err(|e| Claim169Error::Crypto(e.to_string()))?;
169
170        Ok(self.verify_with(verifier))
171    }
172
173    /// Verify with an Ed25519 public key in PEM format.
174    ///
175    /// Supports SPKI format with "BEGIN PUBLIC KEY" headers.
176    ///
177    /// # Arguments
178    ///
179    /// * `pem` - PEM-encoded Ed25519 public key
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if the PEM is invalid or the key is weak.
184    #[cfg(feature = "software-crypto")]
185    pub fn verify_with_ed25519_pem(self, pem: &str) -> Result<Self> {
186        let verifier =
187            Ed25519Verifier::from_pem(pem).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
188
189        Ok(self.verify_with(verifier))
190    }
191
192    /// Verify with an ECDSA P-256 public key.
193    ///
194    /// The key is validated immediately and an error is returned if invalid.
195    ///
196    /// # Arguments
197    ///
198    /// * `public_key` - SEC1-encoded P-256 public key (33 or 65 bytes)
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if the public key format is invalid or represents a weak key.
203    #[cfg(feature = "software-crypto")]
204    pub fn verify_with_ecdsa_p256(self, public_key: &[u8]) -> Result<Self> {
205        let verifier = EcdsaP256Verifier::from_sec1_bytes(public_key)
206            .map_err(|e| Claim169Error::Crypto(e.to_string()))?;
207
208        Ok(self.verify_with(verifier))
209    }
210
211    /// Verify with an ECDSA P-256 public key in PEM format.
212    ///
213    /// Supports SPKI format with "BEGIN PUBLIC KEY" headers.
214    ///
215    /// # Arguments
216    ///
217    /// * `pem` - PEM-encoded P-256 public key
218    ///
219    /// # Errors
220    ///
221    /// Returns an error if the PEM is invalid or the key is weak.
222    #[cfg(feature = "software-crypto")]
223    pub fn verify_with_ecdsa_p256_pem(self, pem: &str) -> Result<Self> {
224        let verifier =
225            EcdsaP256Verifier::from_pem(pem).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
226
227        Ok(self.verify_with(verifier))
228    }
229
230    /// Decrypt with a custom decryptor implementation.
231    ///
232    /// Use this method for HSM integration or custom cryptographic backends.
233    ///
234    /// # Arguments
235    ///
236    /// * `decryptor` - A type implementing the [`Decryptor`] trait
237    pub fn decrypt_with<D: Decryptor + 'static>(mut self, decryptor: D) -> Self {
238        self.decryptor = Some(Box::new(decryptor));
239        self
240    }
241
242    /// Decrypt with AES-256-GCM.
243    ///
244    /// # Arguments
245    ///
246    /// * `key` - 32-byte AES-256 decryption key
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if the key is not exactly 32 bytes.
251    #[cfg(feature = "software-crypto")]
252    pub fn decrypt_with_aes256(self, key: &[u8]) -> Result<Self> {
253        let decryptor =
254            AesGcmDecryptor::aes256(key).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
255
256        Ok(self.decrypt_with(decryptor))
257    }
258
259    /// Decrypt with AES-128-GCM.
260    ///
261    /// # Arguments
262    ///
263    /// * `key` - 16-byte AES-128 decryption key
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if the key is not exactly 16 bytes.
268    #[cfg(feature = "software-crypto")]
269    pub fn decrypt_with_aes128(self, key: &[u8]) -> Result<Self> {
270        let decryptor =
271            AesGcmDecryptor::aes128(key).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
272
273        Ok(self.decrypt_with(decryptor))
274    }
275
276    /// Use a key resolver for dynamic key lookup.
277    ///
278    /// The resolver receives the key ID and algorithm from the COSE header
279    /// and returns the appropriate verifier or decryptor. This is the
280    /// recommended approach for multi-issuer or key-rotation scenarios.
281    ///
282    /// A resolver cannot be combined with `verify_with()` or `decrypt_with()` —
283    /// doing so will return a `DecodingConfig` error at decode time.
284    ///
285    /// # Example
286    ///
287    /// ```rust,ignore
288    /// use claim169_core::{Decoder, KeyResolver, SignatureVerifier, Decryptor};
289    /// use claim169_core::error::CryptoResult;
290    ///
291    /// struct MyKeyStore { /* ... */ }
292    ///
293    /// impl KeyResolver for MyKeyStore {
294    ///     fn resolve_verifier(&self, key_id: Option<&[u8]>, algorithm: coset::iana::Algorithm)
295    ///         -> CryptoResult<Box<dyn SignatureVerifier>> { /* ... */ }
296    ///     fn resolve_decryptor(&self, key_id: Option<&[u8]>, algorithm: coset::iana::Algorithm)
297    ///         -> CryptoResult<Box<dyn Decryptor>> { /* ... */ }
298    /// }
299    ///
300    /// let result = Decoder::new(qr_text)
301    ///     .resolve_with(MyKeyStore { /* ... */ })
302    ///     .decode()?;
303    /// ```
304    pub fn resolve_with<R: KeyResolver + 'static>(mut self, resolver: R) -> Self {
305        self.resolver = Some(Box::new(resolver));
306        self
307    }
308
309    /// Allow decoding without signature verification.
310    ///
311    /// **Security Warning**: Unverified credentials cannot be trusted for authenticity.
312    /// Only use this for testing or when verification is handled externally.
313    ///
314    /// # Example
315    ///
316    /// ```rust,ignore
317    /// let result = Decoder::new(qr_text)
318    ///     .allow_unverified()
319    ///     .decode()?;
320    /// ```
321    pub fn allow_unverified(mut self) -> Self {
322        self.allow_unverified = true;
323        self
324    }
325
326    /// Skip biometric fields during decoding.
327    ///
328    /// This speeds up decoding by not parsing fingerprint, iris, face,
329    /// palm, and voice biometric data.
330    pub fn skip_biometrics(mut self) -> Self {
331        self.skip_biometrics = true;
332        self
333    }
334
335    /// Disable timestamp validation.
336    ///
337    /// By default, the decoder validates that:
338    /// - The credential has not expired (`exp` claim)
339    /// - The credential is valid for use (`nbf` claim)
340    ///
341    /// Use this method to skip these checks (e.g., for offline scenarios).
342    pub fn without_timestamp_validation(mut self) -> Self {
343        self.validate_timestamps = false;
344        self
345    }
346
347    /// Set the clock skew tolerance for timestamp validation.
348    ///
349    /// This allows for some difference between the system clock and the
350    /// issuer's clock when validating `exp` and `nbf` claims.
351    ///
352    /// # Arguments
353    ///
354    /// * `seconds` - Number of seconds to tolerate (default: 0)
355    pub fn clock_skew_tolerance(mut self, seconds: i64) -> Self {
356        self.clock_skew_tolerance_seconds = seconds;
357        self
358    }
359
360    /// Set the maximum decompressed size.
361    ///
362    /// This protects against zip bomb attacks by limiting how much data
363    /// can be decompressed from the QR payload.
364    ///
365    /// # Arguments
366    ///
367    /// * `bytes` - Maximum decompressed size in bytes (default: 65536)
368    pub fn max_decompressed_bytes(mut self, bytes: usize) -> Self {
369        self.max_decompressed_bytes = bytes;
370        self
371    }
372
373    /// Require spec-compliant zlib compression.
374    ///
375    /// When enabled, the decoder will reject payloads that use non-standard
376    /// compression (brotli, uncompressed). This enforces strict compliance
377    /// with the Claim 169 specification.
378    ///
379    /// By default, the decoder auto-detects and accepts all supported
380    /// compression formats.
381    pub fn strict_compression(mut self) -> Self {
382        self.strict_compression = true;
383        self
384    }
385
386    /// Decode the QR text and return the result.
387    ///
388    /// This method consumes the decoder and performs the full decoding pipeline:
389    ///
390    /// ```text
391    /// Base45 → zlib → COSE_Encrypt0 → COSE_Sign1 → CWT → Claim169
392    /// ```
393    ///
394    /// # Errors
395    ///
396    /// Returns an error if:
397    /// - Neither a verifier nor `allow_unverified()` was configured
398    /// - Base45 decoding fails
399    /// - Decompression fails or exceeds the limit
400    /// - COSE structure is invalid
401    /// - Signature verification fails
402    /// - Decryption fails
403    /// - CWT parsing fails
404    /// - Timestamp validation fails
405    ///
406    /// # Example
407    ///
408    /// ```rust,ignore
409    /// let result = Decoder::new(qr_text)
410    ///     .verify_with_ed25519(&public_key)?
411    ///     .decode()?;
412    ///
413    /// println!("Name: {:?}", result.claim169.full_name);
414    /// ```
415    pub fn decode(self) -> Result<DecodeResult> {
416        let mut warnings = Vec::new();
417
418        // Convert trait objects for pipeline functions
419        let verifier_ref: Option<&dyn SignatureVerifier> = self
420            .verifier
421            .as_ref()
422            .map(|v| v.as_ref() as &dyn SignatureVerifier);
423        let decryptor_ref: Option<&dyn Decryptor> = self
424            .decryptor
425            .as_ref()
426            .map(|d| d.as_ref() as &dyn Decryptor);
427
428        // Step 1: Base45 decode
429        let compressed = pipeline::base45_decode(&self.qr_text)?;
430
431        // Step 2: Decompress with auto-detection
432        let (cose_bytes, detected_compression) =
433            pipeline::decompress(&compressed, self.max_decompressed_bytes)?;
434
435        // Check strict compression mode
436        if self.strict_compression && detected_compression != DetectedCompression::Zlib {
437            return Err(Claim169Error::Decompress(format!(
438                "strict compression mode requires zlib, but detected: {}",
439                detected_compression
440            )));
441        }
442
443        // Warn about non-standard compression
444        if detected_compression != DetectedCompression::Zlib {
445            warnings.push(Warning {
446                code: WarningCode::NonStandardCompression,
447                message: format!(
448                    "non-standard compression detected: {}",
449                    detected_compression
450                ),
451            });
452        }
453
454        // Check for conflicting configuration
455        if self.resolver.is_some() && self.verifier.is_some() {
456            return Err(Claim169Error::DecodingConfig(
457                "cannot use both resolve_with() and verify_with() — \
458                 resolve_with() provides its own verifier via key lookup"
459                    .to_string(),
460            ));
461        }
462        if self.resolver.is_some() && self.decryptor.is_some() {
463            return Err(Claim169Error::DecodingConfig(
464                "cannot use both resolve_with() and decrypt_with() — \
465                 resolve_with() provides its own decryptor via key lookup"
466                    .to_string(),
467            ));
468        }
469
470        // Step 3-4: Parse COSE and verify/decrypt
471        let cose_result = if let Some(ref resolver) = self.resolver {
472            // Use key resolver for dynamic key lookup
473            pipeline::cose_parse_with_resolver(&cose_bytes, resolver.as_ref())?
474        } else {
475            pipeline::cose_parse(&cose_bytes, verifier_ref, decryptor_ref)?
476        };
477
478        // Check if verification was required but skipped
479        if !self.allow_unverified && cose_result.verification_status == VerificationStatus::Skipped
480        {
481            return Err(Claim169Error::DecodingConfig(
482                "verification required but no verifier or resolver provided - \
483                 use verify_with(), resolve_with(), or allow_unverified() to configure"
484                    .to_string(),
485            ));
486        }
487
488        // Check if verification failed
489        if cose_result.verification_status == VerificationStatus::Failed {
490            return Err(Claim169Error::SignatureInvalid(
491                "signature verification failed".to_string(),
492            ));
493        }
494
495        // Step 5: Parse CWT
496        let cwt_result = pipeline::cwt_parse(&cose_result.payload)?;
497
498        // Step 6: Validate timestamps
499        if self.validate_timestamps {
500            let now = SystemTime::now()
501                .duration_since(UNIX_EPOCH)
502                .map(|d| d.as_secs() as i64)
503                .map_err(|_| {
504                    Claim169Error::DecodingConfig("system clock is before Unix epoch".to_string())
505                })?;
506
507            let skew = self.clock_skew_tolerance_seconds;
508
509            if let Some(exp) = cwt_result.meta.expires_at {
510                if now > exp + skew {
511                    return Err(Claim169Error::Expired(exp));
512                }
513            }
514
515            if let Some(nbf) = cwt_result.meta.not_before {
516                if now + skew < nbf {
517                    return Err(Claim169Error::NotYetValid(nbf));
518                }
519            }
520        } else {
521            warnings.push(Warning {
522                code: WarningCode::TimestampValidationSkipped,
523                message: "Timestamp validation was disabled".to_string(),
524            });
525        }
526
527        // Step 7: Transform claim 169
528        let claim169 = pipeline::claim169_transform(cwt_result.claim_169, self.skip_biometrics)?;
529
530        if self.skip_biometrics {
531            warnings.push(Warning {
532                code: WarningCode::BiometricsSkipped,
533                message: "Biometric data was skipped".to_string(),
534            });
535        }
536
537        if !claim169.unknown_fields.is_empty() {
538            warnings.push(Warning {
539                code: WarningCode::UnknownFields,
540                message: format!(
541                    "Found {} unknown fields (keys: {:?})",
542                    claim169.unknown_fields.len(),
543                    claim169.unknown_fields.keys().collect::<Vec<_>>()
544                ),
545            });
546        }
547
548        Ok(DecodeResult {
549            claim169,
550            cwt_meta: cwt_result.meta,
551            verification_status: cose_result.verification_status,
552            x509_headers: cose_result.x509_headers,
553            detected_compression,
554            warnings,
555            key_id: cose_result.key_id,
556            algorithm: cose_result.algorithm,
557        })
558    }
559}
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564    use crate::model::{Claim169, CwtMeta, VerificationStatus};
565
566    // Create a minimal test QR payload (unsigned, for testing basic functionality)
567    fn create_test_qr() -> String {
568        use crate::Encoder;
569
570        let claim169 = Claim169 {
571            id: Some("test-123".to_string()),
572            full_name: Some("Test User".to_string()),
573            ..Default::default()
574        };
575
576        let cwt_meta = CwtMeta::new()
577            .with_issuer("https://test.issuer")
578            .with_expires_at(i64::MAX); // Far future expiration
579
580        Encoder::new(claim169, cwt_meta)
581            .allow_unsigned()
582            .encode()
583            .unwrap()
584            .qr_data
585    }
586
587    #[test]
588    fn test_decoder_requires_verifier_or_allow_unverified() {
589        let qr_text = create_test_qr();
590
591        let result = Decoder::new(&qr_text).decode();
592
593        assert!(result.is_err());
594        match result.unwrap_err() {
595            Claim169Error::DecodingConfig(msg) => {
596                assert!(msg.contains("allow_unverified"));
597            }
598            e => panic!("Expected DecodingConfig error, got: {:?}", e),
599        }
600    }
601
602    #[test]
603    fn test_decoder_allow_unverified() {
604        let qr_text = create_test_qr();
605
606        let result = Decoder::new(&qr_text).allow_unverified().decode();
607
608        assert!(result.is_ok());
609        let decoded = result.unwrap();
610        assert_eq!(decoded.claim169.id, Some("test-123".to_string()));
611        assert_eq!(decoded.claim169.full_name, Some("Test User".to_string()));
612        assert_eq!(decoded.verification_status, VerificationStatus::Skipped);
613    }
614
615    #[test]
616    fn test_decoder_accepts_string() {
617        let qr_text = create_test_qr();
618
619        // Test with String
620        let result = Decoder::new(qr_text.clone()).allow_unverified().decode();
621        assert!(result.is_ok());
622
623        // Test with &str
624        let result = Decoder::new(&qr_text).allow_unverified().decode();
625        assert!(result.is_ok());
626    }
627
628    #[test]
629    fn test_decoder_skip_biometrics() {
630        let qr_text = create_test_qr();
631
632        let result = Decoder::new(&qr_text)
633            .allow_unverified()
634            .skip_biometrics()
635            .decode()
636            .unwrap();
637
638        // Should have a warning about skipped biometrics
639        assert!(result
640            .warnings
641            .iter()
642            .any(|w| w.code == WarningCode::BiometricsSkipped));
643    }
644
645    #[test]
646    fn test_decoder_without_timestamp_validation() {
647        let qr_text = create_test_qr();
648
649        let result = Decoder::new(&qr_text)
650            .allow_unverified()
651            .without_timestamp_validation()
652            .decode()
653            .unwrap();
654
655        // Should have a warning about skipped validation
656        assert!(result
657            .warnings
658            .iter()
659            .any(|w| w.code == WarningCode::TimestampValidationSkipped));
660    }
661
662    #[cfg(feature = "software-crypto")]
663    #[test]
664    fn test_decoder_roundtrip_ed25519() {
665        use crate::crypto::software::Ed25519Signer;
666        use crate::Encoder;
667        use coset::iana;
668
669        let claim169 = Claim169 {
670            id: Some("signed-test".to_string()),
671            full_name: Some("Signed User".to_string()),
672            email: Some("signed@example.com".to_string()),
673            ..Default::default()
674        };
675
676        let cwt_meta = CwtMeta::new()
677            .with_issuer("https://signed.test")
678            .with_expires_at(i64::MAX);
679
680        // Generate key pair
681        let signer = Ed25519Signer::generate();
682        let public_key = signer.public_key_bytes();
683
684        // Encode
685        let encode_result = Encoder::new(claim169.clone(), cwt_meta)
686            .sign_with(signer, iana::Algorithm::EdDSA)
687            .encode()
688            .unwrap();
689
690        // Decode with verification
691        let result = Decoder::new(&encode_result.qr_data)
692            .verify_with_ed25519(&public_key)
693            .unwrap()
694            .decode()
695            .unwrap();
696
697        assert_eq!(result.verification_status, VerificationStatus::Verified);
698        assert_eq!(result.claim169.id, claim169.id);
699        assert_eq!(result.claim169.full_name, claim169.full_name);
700        assert_eq!(result.claim169.email, claim169.email);
701        assert_eq!(result.detected_compression, DetectedCompression::Zlib);
702    }
703
704    #[cfg(feature = "software-crypto")]
705    #[test]
706    fn test_decoder_roundtrip_encrypted() {
707        use crate::crypto::software::Ed25519Signer;
708        use crate::Encoder;
709        use coset::iana;
710
711        let claim169 = Claim169 {
712            id: Some("encrypted-test".to_string()),
713            full_name: Some("Encrypted User".to_string()),
714            ..Default::default()
715        };
716
717        let cwt_meta = CwtMeta::new()
718            .with_issuer("https://encrypted.test")
719            .with_expires_at(i64::MAX);
720
721        // Generate keys
722        let signer = Ed25519Signer::generate();
723        let public_key = signer.public_key_bytes();
724        let aes_key = [42u8; 32];
725        let nonce = [7u8; 12];
726
727        // Encode with signing and encryption
728        let encode_result = Encoder::new(claim169.clone(), cwt_meta)
729            .sign_with(signer, iana::Algorithm::EdDSA)
730            .encrypt_with_aes256_nonce(&aes_key, &nonce)
731            .unwrap()
732            .encode()
733            .unwrap();
734
735        // Decode with decryption and verification
736        let result = Decoder::new(&encode_result.qr_data)
737            .decrypt_with_aes256(&aes_key)
738            .unwrap()
739            .verify_with_ed25519(&public_key)
740            .unwrap()
741            .decode()
742            .unwrap();
743
744        assert_eq!(result.verification_status, VerificationStatus::Verified);
745        assert_eq!(result.claim169.id, claim169.id);
746        assert_eq!(result.claim169.full_name, claim169.full_name);
747    }
748
749    #[cfg(feature = "software-crypto")]
750    #[test]
751    fn test_decoder_wrong_key_fails() {
752        use crate::crypto::software::Ed25519Signer;
753        use crate::Encoder;
754        use coset::iana;
755
756        let claim169 = Claim169::minimal("test", "Test");
757        let cwt_meta = CwtMeta::default();
758
759        let signer = Ed25519Signer::generate();
760        let wrong_signer = Ed25519Signer::generate();
761        let wrong_public_key = wrong_signer.public_key_bytes();
762
763        let qr_data = Encoder::new(claim169, cwt_meta)
764            .sign_with(signer, iana::Algorithm::EdDSA)
765            .encode()
766            .unwrap()
767            .qr_data;
768
769        // Try to decode with wrong key
770        let result = Decoder::new(&qr_data)
771            .verify_with_ed25519(&wrong_public_key)
772            .unwrap()
773            .decode();
774
775        assert!(result.is_err());
776        assert!(matches!(
777            result.unwrap_err(),
778            Claim169Error::SignatureInvalid(_)
779        ));
780    }
781
782    #[test]
783    fn test_decoder_strict_compression_accepts_zlib() {
784        let qr_text = create_test_qr();
785
786        let result = Decoder::new(&qr_text)
787            .allow_unverified()
788            .strict_compression()
789            .decode();
790
791        assert!(result.is_ok());
792        assert_eq!(
793            result.unwrap().detected_compression,
794            DetectedCompression::Zlib
795        );
796    }
797
798    #[test]
799    fn test_decoder_strict_compression_rejects_non_zlib() {
800        use crate::{Compression, Encoder};
801
802        let claim169 = Claim169::minimal("test", "Test");
803        let cwt_meta = CwtMeta::new().with_expires_at(i64::MAX);
804
805        let qr_data = Encoder::new(claim169, cwt_meta)
806            .allow_unsigned()
807            .compression(Compression::None)
808            .encode()
809            .unwrap()
810            .qr_data;
811
812        let result = Decoder::new(&qr_data)
813            .allow_unverified()
814            .strict_compression()
815            .decode();
816
817        assert!(result.is_err());
818        assert!(matches!(result.unwrap_err(), Claim169Error::Decompress(_)));
819    }
820
821    #[test]
822    fn test_decoder_non_standard_compression_warning() {
823        use crate::{Compression, Encoder};
824
825        let claim169 = Claim169::minimal("test", "Test");
826        let cwt_meta = CwtMeta::new().with_expires_at(i64::MAX);
827
828        let qr_data = Encoder::new(claim169, cwt_meta)
829            .allow_unsigned()
830            .compression(Compression::None)
831            .encode()
832            .unwrap()
833            .qr_data;
834
835        let result = Decoder::new(&qr_data).allow_unverified().decode().unwrap();
836
837        assert_eq!(result.detected_compression, DetectedCompression::None);
838        assert!(result
839            .warnings
840            .iter()
841            .any(|w| w.code == WarningCode::NonStandardCompression));
842    }
843
844    #[test]
845    fn test_decoder_invalid_base45() {
846        let result = Decoder::new("!!!invalid base45!!!")
847            .allow_unverified()
848            .decode();
849
850        assert!(result.is_err());
851        assert!(matches!(
852            result.unwrap_err(),
853            Claim169Error::Base45Decode(_)
854        ));
855    }
856
857    #[test]
858    fn test_decoder_max_decompressed_bytes() {
859        let qr_text = create_test_qr();
860
861        // Set a very small limit that will be exceeded
862        let result = Decoder::new(&qr_text)
863            .allow_unverified()
864            .max_decompressed_bytes(10)
865            .decode();
866
867        assert!(result.is_err());
868        assert!(matches!(
869            result.unwrap_err(),
870            Claim169Error::DecompressLimitExceeded { .. }
871        ));
872    }
873
874    #[cfg(feature = "software-crypto")]
875    #[test]
876    fn test_resolver_selects_verifier_by_kid() {
877        use crate::crypto::software::Ed25519Signer;
878        use crate::crypto::traits::{Decryptor as DecryptorTrait, KeyResolver};
879        use crate::error::{CryptoError, CryptoResult};
880        use coset::iana;
881        use std::collections::HashMap;
882
883        let claim169 = Claim169 {
884            id: Some("resolver-test".to_string()),
885            ..Default::default()
886        };
887        let cwt_meta = CwtMeta::new()
888            .with_issuer("https://issuer-a.test")
889            .with_expires_at(i64::MAX);
890
891        // Create two signers with different kids
892        let mut signer_a = Ed25519Signer::generate();
893        signer_a.set_key_id(b"key-a".to_vec());
894        let pub_key_a = signer_a.public_key_bytes();
895
896        let mut signer_b = Ed25519Signer::generate();
897        signer_b.set_key_id(b"key-b".to_vec());
898        let pub_key_b = signer_b.public_key_bytes();
899
900        // Encode with signer A
901        let qr_data = crate::Encoder::new(claim169, cwt_meta)
902            .sign_with(signer_a, iana::Algorithm::EdDSA)
903            .encode()
904            .unwrap()
905            .qr_data;
906
907        // Resolver that maps kid -> public key bytes
908        struct SimpleResolver {
909            keys: HashMap<Vec<u8>, Vec<u8>>,
910        }
911
912        impl KeyResolver for SimpleResolver {
913            fn resolve_verifier(
914                &self,
915                key_id: Option<&[u8]>,
916                _algorithm: iana::Algorithm,
917            ) -> CryptoResult<Box<dyn SignatureVerifier>> {
918                let kid = key_id.ok_or(CryptoError::KeyNotFound)?;
919                let public_key = self.keys.get(kid).ok_or(CryptoError::KeyNotFound)?;
920                let verifier = crate::crypto::software::Ed25519Verifier::from_bytes(public_key)
921                    .map_err(|e| CryptoError::Other(e.to_string()))?;
922                Ok(Box::new(verifier))
923            }
924
925            fn resolve_decryptor(
926                &self,
927                _key_id: Option<&[u8]>,
928                _algorithm: iana::Algorithm,
929            ) -> CryptoResult<Box<dyn DecryptorTrait>> {
930                Err(CryptoError::KeyNotFound)
931            }
932        }
933
934        let mut keys = HashMap::new();
935        keys.insert(b"key-a".to_vec(), pub_key_a.to_vec());
936        keys.insert(b"key-b".to_vec(), pub_key_b.to_vec());
937
938        let resolver = SimpleResolver { keys };
939
940        let result = Decoder::new(&qr_data)
941            .resolve_with(resolver)
942            .decode()
943            .unwrap();
944
945        assert_eq!(result.claim169.id, Some("resolver-test".to_string()));
946        assert_eq!(result.verification_status, VerificationStatus::Verified);
947        assert_eq!(result.key_id, Some(b"key-a".to_vec()));
948    }
949
950    #[cfg(feature = "software-crypto")]
951    #[test]
952    fn test_resolver_unknown_kid_returns_error() {
953        use crate::crypto::traits::{Decryptor as DecryptorTrait, KeyResolver};
954        use crate::error::{CryptoError, CryptoResult};
955        use coset::iana;
956
957        let claim169 = Claim169 {
958            id: Some("unknown-kid-test".to_string()),
959            ..Default::default()
960        };
961        let cwt_meta = CwtMeta::new()
962            .with_issuer("https://issuer.test")
963            .with_expires_at(i64::MAX);
964
965        let mut signer = crate::crypto::software::Ed25519Signer::generate();
966        signer.set_key_id(b"unknown-key".to_vec());
967
968        let qr_data = crate::Encoder::new(claim169, cwt_meta)
969            .sign_with(signer, iana::Algorithm::EdDSA)
970            .encode()
971            .unwrap()
972            .qr_data;
973
974        // Resolver that never finds a key
975        struct EmptyResolver;
976        impl KeyResolver for EmptyResolver {
977            fn resolve_verifier(
978                &self,
979                _key_id: Option<&[u8]>,
980                _algorithm: iana::Algorithm,
981            ) -> CryptoResult<Box<dyn SignatureVerifier>> {
982                Err(CryptoError::KeyNotFound)
983            }
984            fn resolve_decryptor(
985                &self,
986                _key_id: Option<&[u8]>,
987                _algorithm: iana::Algorithm,
988            ) -> CryptoResult<Box<dyn DecryptorTrait>> {
989                Err(CryptoError::KeyNotFound)
990            }
991        }
992
993        let result = Decoder::new(&qr_data).resolve_with(EmptyResolver).decode();
994
995        assert!(result.is_err());
996    }
997
998    #[cfg(feature = "software-crypto")]
999    #[test]
1000    fn test_decode_result_exposes_key_id_and_algorithm() {
1001        use crate::crypto::software::Ed25519Signer;
1002        use crate::Encoder;
1003        use coset::iana;
1004
1005        let claim169 = Claim169 {
1006            id: Some("kid-decode-test".to_string()),
1007            ..Default::default()
1008        };
1009
1010        let cwt_meta = CwtMeta::new()
1011            .with_issuer("https://test.issuer")
1012            .with_expires_at(i64::MAX);
1013
1014        let mut signer = Ed25519Signer::generate();
1015        let kid = b"my-key-id-123";
1016        signer.set_key_id(kid.to_vec());
1017        let public_key = signer.public_key_bytes();
1018
1019        let qr_data = Encoder::new(claim169, cwt_meta)
1020            .sign_with(signer, iana::Algorithm::EdDSA)
1021            .encode()
1022            .unwrap()
1023            .qr_data;
1024
1025        let result = Decoder::new(&qr_data)
1026            .verify_with_ed25519(&public_key)
1027            .unwrap()
1028            .decode()
1029            .unwrap();
1030
1031        assert_eq!(result.key_id, Some(kid.to_vec()));
1032        assert_eq!(result.algorithm, Some(iana::Algorithm::EdDSA));
1033        assert_eq!(result.verification_status, VerificationStatus::Verified);
1034    }
1035
1036    #[test]
1037    fn test_decode_result_no_key_id_when_absent() {
1038        let qr_text = create_test_qr();
1039
1040        let result = Decoder::new(&qr_text).allow_unverified().decode().unwrap();
1041
1042        assert_eq!(result.key_id, None);
1043    }
1044
1045    #[cfg(feature = "software-crypto")]
1046    #[test]
1047    fn test_resolver_conflicts_with_verify_with() {
1048        use crate::crypto::traits::{Decryptor as DecryptorTrait, KeyResolver};
1049        use crate::error::{CryptoError, CryptoResult};
1050        use coset::iana;
1051
1052        struct DummyResolver;
1053        impl KeyResolver for DummyResolver {
1054            fn resolve_verifier(
1055                &self,
1056                _key_id: Option<&[u8]>,
1057                _algorithm: iana::Algorithm,
1058            ) -> CryptoResult<Box<dyn SignatureVerifier>> {
1059                Err(CryptoError::KeyNotFound)
1060            }
1061            fn resolve_decryptor(
1062                &self,
1063                _key_id: Option<&[u8]>,
1064                _algorithm: iana::Algorithm,
1065            ) -> CryptoResult<Box<dyn DecryptorTrait>> {
1066                Err(CryptoError::KeyNotFound)
1067            }
1068        }
1069
1070        let signer = crate::crypto::software::Ed25519Signer::generate();
1071        let public_key = signer.public_key_bytes();
1072
1073        let qr_text = create_test_qr();
1074
1075        let result = Decoder::new(&qr_text)
1076            .resolve_with(DummyResolver)
1077            .verify_with_ed25519(&public_key)
1078            .unwrap()
1079            .decode();
1080
1081        assert!(result.is_err());
1082        match result.unwrap_err() {
1083            Claim169Error::DecodingConfig(msg) => {
1084                assert!(msg.contains("resolve_with()"));
1085                assert!(msg.contains("verify_with()"));
1086            }
1087            e => panic!("Expected DecodingConfig error, got: {:?}", e),
1088        }
1089    }
1090
1091    #[cfg(feature = "software-crypto")]
1092    #[test]
1093    fn test_resolver_conflicts_with_decrypt_with() {
1094        use crate::crypto::traits::{Decryptor as DecryptorTrait, KeyResolver};
1095        use crate::error::{CryptoError, CryptoResult};
1096        use coset::iana;
1097
1098        struct DummyResolver;
1099        impl KeyResolver for DummyResolver {
1100            fn resolve_verifier(
1101                &self,
1102                _key_id: Option<&[u8]>,
1103                _algorithm: iana::Algorithm,
1104            ) -> CryptoResult<Box<dyn SignatureVerifier>> {
1105                Err(CryptoError::KeyNotFound)
1106            }
1107            fn resolve_decryptor(
1108                &self,
1109                _key_id: Option<&[u8]>,
1110                _algorithm: iana::Algorithm,
1111            ) -> CryptoResult<Box<dyn DecryptorTrait>> {
1112                Err(CryptoError::KeyNotFound)
1113            }
1114        }
1115
1116        let aes_key = [42u8; 32];
1117        let qr_text = create_test_qr();
1118
1119        let result = Decoder::new(&qr_text)
1120            .resolve_with(DummyResolver)
1121            .decrypt_with_aes256(&aes_key)
1122            .unwrap()
1123            .decode();
1124
1125        assert!(result.is_err());
1126        match result.unwrap_err() {
1127            Claim169Error::DecodingConfig(msg) => {
1128                assert!(msg.contains("resolve_with()"));
1129                assert!(msg.contains("decrypt_with()"));
1130            }
1131            e => panic!("Expected DecodingConfig error, got: {:?}", e),
1132        }
1133    }
1134
1135    #[cfg(feature = "software-crypto")]
1136    #[test]
1137    fn test_empty_key_id_roundtrips_as_none() {
1138        // COSE headers treat an empty key_id (zero-length bstr) as semantically
1139        // absent. When we encode with set_key_id(vec![]), the decoded result
1140        // should report key_id as None rather than Some(vec![]).
1141        use crate::crypto::software::Ed25519Signer;
1142        use crate::Encoder;
1143        use coset::iana;
1144
1145        let claim169 = Claim169 {
1146            id: Some("empty-kid-test".to_string()),
1147            ..Default::default()
1148        };
1149        let cwt_meta = CwtMeta::new()
1150            .with_issuer("https://empty-kid.test")
1151            .with_expires_at(i64::MAX);
1152
1153        let mut signer = Ed25519Signer::generate();
1154        signer.set_key_id(vec![]); // Empty key_id
1155        let public_key = signer.public_key_bytes();
1156
1157        let qr_data = Encoder::new(claim169, cwt_meta)
1158            .sign_with(signer, iana::Algorithm::EdDSA)
1159            .encode()
1160            .unwrap()
1161            .qr_data;
1162
1163        let result = Decoder::new(&qr_data)
1164            .verify_with_ed25519(&public_key)
1165            .unwrap()
1166            .decode()
1167            .unwrap();
1168
1169        // Empty key_id should be normalized to None
1170        assert_eq!(result.key_id, None);
1171        assert_eq!(result.verification_status, VerificationStatus::Verified);
1172    }
1173}