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, SignatureVerifier};
44use crate::error::{Claim169Error, Result};
45use crate::model::VerificationStatus;
46use crate::pipeline;
47use crate::{DecodeResult, Warning, WarningCode};
48
49#[cfg(feature = "software-crypto")]
50use crate::crypto::software::{AesGcmDecryptor, EcdsaP256Verifier, Ed25519Verifier};
51
52use std::time::{SystemTime, UNIX_EPOCH};
53
54/// Default maximum decompressed size (64KB)
55const DEFAULT_MAX_DECOMPRESSED_BYTES: usize = 65536;
56
57/// Builder for decoding Claim 169 credentials from QR strings.
58///
59/// The decoder follows a builder pattern where configuration methods return `Self`
60/// and the final `decode()` method consumes the builder to produce the result.
61///
62/// # Operation Order
63///
64/// When both decryption and verification are configured, the credential is always
65/// **decrypted first, then verified** (reverse of encoding), regardless of the order
66/// in which builder methods are called.
67///
68/// # Security
69///
70/// - By default, decoding requires signature verification
71/// - Use [`allow_unverified()`](Self::allow_unverified) to explicitly opt out
72/// - Decompression is limited to prevent zip bomb attacks (default: 64KB)
73///
74/// # Example
75///
76/// ```rust,ignore
77/// use claim169_core::Decoder;
78///
79/// let result = Decoder::new("6BF5YZB2...")
80///     .verify_with_ed25519(&public_key)?
81///     .decode()?;
82///
83/// println!("ID: {:?}", result.claim169.id);
84/// println!("Verified: {:?}", result.verification_status);
85/// ```
86pub struct Decoder {
87    qr_text: String,
88    verifier: Option<Box<dyn SignatureVerifier + Send + Sync>>,
89    decryptor: Option<Box<dyn Decryptor + Send + Sync>>,
90    allow_unverified: bool,
91    skip_biometrics: bool,
92    validate_timestamps: bool,
93    clock_skew_tolerance_seconds: i64,
94    max_decompressed_bytes: usize,
95}
96
97impl Decoder {
98    /// Create a new decoder with the given QR text.
99    ///
100    /// # Arguments
101    ///
102    /// * `qr_text` - The Base45-encoded QR code content (accepts `&str` or `String`)
103    ///
104    /// # Example
105    ///
106    /// ```rust,ignore
107    /// let decoder = Decoder::new("6BF5YZB2...");
108    /// let decoder = Decoder::new(qr_string);
109    /// ```
110    pub fn new(qr_text: impl Into<String>) -> Self {
111        Self {
112            qr_text: qr_text.into(),
113            verifier: None,
114            decryptor: None,
115            allow_unverified: false,
116            skip_biometrics: false,
117            validate_timestamps: true,
118            clock_skew_tolerance_seconds: 0,
119            max_decompressed_bytes: DEFAULT_MAX_DECOMPRESSED_BYTES,
120        }
121    }
122
123    /// Verify with a custom verifier implementation.
124    ///
125    /// Use this method for HSM integration or custom cryptographic backends.
126    ///
127    /// # Arguments
128    ///
129    /// * `verifier` - A type implementing the [`SignatureVerifier`] trait
130    ///
131    /// # Example
132    ///
133    /// ```rust,ignore
134    /// let decoder = Decoder::new(qr_text)
135    ///     .verify_with(hsm_verifier);
136    /// ```
137    pub fn verify_with<V: SignatureVerifier + 'static>(mut self, verifier: V) -> Self {
138        self.verifier = Some(Box::new(verifier));
139        self
140    }
141
142    /// Verify with an Ed25519 public key.
143    ///
144    /// The key is validated immediately and an error is returned if invalid.
145    ///
146    /// # Arguments
147    ///
148    /// * `public_key` - 32-byte Ed25519 public key
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if the public key is invalid or represents a weak key.
153    ///
154    /// # Example
155    ///
156    /// ```rust,ignore
157    /// let decoder = Decoder::new(qr_text)
158    ///     .verify_with_ed25519(&public_key)?;
159    /// ```
160    #[cfg(feature = "software-crypto")]
161    pub fn verify_with_ed25519(self, public_key: &[u8]) -> Result<Self> {
162        let verifier = Ed25519Verifier::from_bytes(public_key)
163            .map_err(|e| Claim169Error::Crypto(e.to_string()))?;
164
165        Ok(self.verify_with(verifier))
166    }
167
168    /// Verify with an Ed25519 public key in PEM format.
169    ///
170    /// Supports SPKI format with "BEGIN PUBLIC KEY" headers.
171    ///
172    /// # Arguments
173    ///
174    /// * `pem` - PEM-encoded Ed25519 public key
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if the PEM is invalid or the key is weak.
179    #[cfg(feature = "software-crypto")]
180    pub fn verify_with_ed25519_pem(self, pem: &str) -> Result<Self> {
181        let verifier =
182            Ed25519Verifier::from_pem(pem).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
183
184        Ok(self.verify_with(verifier))
185    }
186
187    /// Verify with an ECDSA P-256 public key.
188    ///
189    /// The key is validated immediately and an error is returned if invalid.
190    ///
191    /// # Arguments
192    ///
193    /// * `public_key` - SEC1-encoded P-256 public key (33 or 65 bytes)
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if the public key format is invalid or represents a weak key.
198    #[cfg(feature = "software-crypto")]
199    pub fn verify_with_ecdsa_p256(self, public_key: &[u8]) -> Result<Self> {
200        let verifier = EcdsaP256Verifier::from_sec1_bytes(public_key)
201            .map_err(|e| Claim169Error::Crypto(e.to_string()))?;
202
203        Ok(self.verify_with(verifier))
204    }
205
206    /// Verify with an ECDSA P-256 public key in PEM format.
207    ///
208    /// Supports SPKI format with "BEGIN PUBLIC KEY" headers.
209    ///
210    /// # Arguments
211    ///
212    /// * `pem` - PEM-encoded P-256 public key
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if the PEM is invalid or the key is weak.
217    #[cfg(feature = "software-crypto")]
218    pub fn verify_with_ecdsa_p256_pem(self, pem: &str) -> Result<Self> {
219        let verifier =
220            EcdsaP256Verifier::from_pem(pem).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
221
222        Ok(self.verify_with(verifier))
223    }
224
225    /// Decrypt with a custom decryptor implementation.
226    ///
227    /// Use this method for HSM integration or custom cryptographic backends.
228    ///
229    /// # Arguments
230    ///
231    /// * `decryptor` - A type implementing the [`Decryptor`] trait
232    pub fn decrypt_with<D: Decryptor + 'static>(mut self, decryptor: D) -> Self {
233        self.decryptor = Some(Box::new(decryptor));
234        self
235    }
236
237    /// Decrypt with AES-256-GCM.
238    ///
239    /// # Arguments
240    ///
241    /// * `key` - 32-byte AES-256 decryption key
242    ///
243    /// # Errors
244    ///
245    /// Returns an error if the key is not exactly 32 bytes.
246    #[cfg(feature = "software-crypto")]
247    pub fn decrypt_with_aes256(self, key: &[u8]) -> Result<Self> {
248        let decryptor =
249            AesGcmDecryptor::aes256(key).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
250
251        Ok(self.decrypt_with(decryptor))
252    }
253
254    /// Decrypt with AES-128-GCM.
255    ///
256    /// # Arguments
257    ///
258    /// * `key` - 16-byte AES-128 decryption key
259    ///
260    /// # Errors
261    ///
262    /// Returns an error if the key is not exactly 16 bytes.
263    #[cfg(feature = "software-crypto")]
264    pub fn decrypt_with_aes128(self, key: &[u8]) -> Result<Self> {
265        let decryptor =
266            AesGcmDecryptor::aes128(key).map_err(|e| Claim169Error::Crypto(e.to_string()))?;
267
268        Ok(self.decrypt_with(decryptor))
269    }
270
271    /// Allow decoding without signature verification.
272    ///
273    /// **Security Warning**: Unverified credentials cannot be trusted for authenticity.
274    /// Only use this for testing or when verification is handled externally.
275    ///
276    /// # Example
277    ///
278    /// ```rust,ignore
279    /// let result = Decoder::new(qr_text)
280    ///     .allow_unverified()
281    ///     .decode()?;
282    /// ```
283    pub fn allow_unverified(mut self) -> Self {
284        self.allow_unverified = true;
285        self
286    }
287
288    /// Skip biometric fields during decoding.
289    ///
290    /// This speeds up decoding by not parsing fingerprint, iris, face,
291    /// palm, and voice biometric data.
292    pub fn skip_biometrics(mut self) -> Self {
293        self.skip_biometrics = true;
294        self
295    }
296
297    /// Disable timestamp validation.
298    ///
299    /// By default, the decoder validates that:
300    /// - The credential has not expired (`exp` claim)
301    /// - The credential is valid for use (`nbf` claim)
302    ///
303    /// Use this method to skip these checks (e.g., for offline scenarios).
304    pub fn without_timestamp_validation(mut self) -> Self {
305        self.validate_timestamps = false;
306        self
307    }
308
309    /// Set the clock skew tolerance for timestamp validation.
310    ///
311    /// This allows for some difference between the system clock and the
312    /// issuer's clock when validating `exp` and `nbf` claims.
313    ///
314    /// # Arguments
315    ///
316    /// * `seconds` - Number of seconds to tolerate (default: 0)
317    pub fn clock_skew_tolerance(mut self, seconds: i64) -> Self {
318        self.clock_skew_tolerance_seconds = seconds;
319        self
320    }
321
322    /// Set the maximum decompressed size.
323    ///
324    /// This protects against zip bomb attacks by limiting how much data
325    /// can be decompressed from the QR payload.
326    ///
327    /// # Arguments
328    ///
329    /// * `bytes` - Maximum decompressed size in bytes (default: 65536)
330    pub fn max_decompressed_bytes(mut self, bytes: usize) -> Self {
331        self.max_decompressed_bytes = bytes;
332        self
333    }
334
335    /// Decode the QR text and return the result.
336    ///
337    /// This method consumes the decoder and performs the full decoding pipeline:
338    ///
339    /// ```text
340    /// Base45 → zlib → COSE_Encrypt0 → COSE_Sign1 → CWT → Claim169
341    /// ```
342    ///
343    /// # Errors
344    ///
345    /// Returns an error if:
346    /// - Neither a verifier nor `allow_unverified()` was configured
347    /// - Base45 decoding fails
348    /// - Decompression fails or exceeds the limit
349    /// - COSE structure is invalid
350    /// - Signature verification fails
351    /// - Decryption fails
352    /// - CWT parsing fails
353    /// - Timestamp validation fails
354    ///
355    /// # Example
356    ///
357    /// ```rust,ignore
358    /// let result = Decoder::new(qr_text)
359    ///     .verify_with_ed25519(&public_key)?
360    ///     .decode()?;
361    ///
362    /// println!("Name: {:?}", result.claim169.full_name);
363    /// ```
364    pub fn decode(self) -> Result<DecodeResult> {
365        let mut warnings = Vec::new();
366
367        // Convert trait objects for pipeline functions
368        let verifier_ref: Option<&dyn SignatureVerifier> = self
369            .verifier
370            .as_ref()
371            .map(|v| v.as_ref() as &dyn SignatureVerifier);
372        let decryptor_ref: Option<&dyn Decryptor> = self
373            .decryptor
374            .as_ref()
375            .map(|d| d.as_ref() as &dyn Decryptor);
376
377        // Step 1: Base45 decode
378        let compressed = pipeline::base45_decode(&self.qr_text)?;
379
380        // Step 2: zlib decompress
381        let cose_bytes = pipeline::decompress(&compressed, self.max_decompressed_bytes)?;
382
383        // Step 3-4: Parse COSE and verify/decrypt
384        let cose_result = pipeline::cose_parse(&cose_bytes, verifier_ref, decryptor_ref)?;
385
386        // Check if verification was required but skipped
387        if !self.allow_unverified && cose_result.verification_status == VerificationStatus::Skipped
388        {
389            return Err(Claim169Error::DecodingConfig(
390                "verification required but no verifier provided - use allow_unverified() to skip"
391                    .to_string(),
392            ));
393        }
394
395        // Check if verification failed
396        if cose_result.verification_status == VerificationStatus::Failed {
397            return Err(Claim169Error::SignatureInvalid(
398                "signature verification failed".to_string(),
399            ));
400        }
401
402        // Step 5: Parse CWT
403        let cwt_result = pipeline::cwt_parse(&cose_result.payload)?;
404
405        // Step 6: Validate timestamps
406        if self.validate_timestamps {
407            let now = SystemTime::now()
408                .duration_since(UNIX_EPOCH)
409                .map(|d| d.as_secs() as i64)
410                .unwrap_or(0);
411
412            let skew = self.clock_skew_tolerance_seconds;
413
414            if let Some(exp) = cwt_result.meta.expires_at {
415                if now > exp + skew {
416                    return Err(Claim169Error::Expired(exp));
417                }
418            }
419
420            if let Some(nbf) = cwt_result.meta.not_before {
421                if now + skew < nbf {
422                    return Err(Claim169Error::NotYetValid(nbf));
423                }
424            }
425        } else {
426            warnings.push(Warning {
427                code: WarningCode::TimestampValidationSkipped,
428                message: "Timestamp validation was disabled".to_string(),
429            });
430        }
431
432        // Step 7: Transform claim 169
433        let claim169 = pipeline::claim169_transform(cwt_result.claim_169, self.skip_biometrics)?;
434
435        if self.skip_biometrics {
436            warnings.push(Warning {
437                code: WarningCode::BiometricsSkipped,
438                message: "Biometric data was skipped".to_string(),
439            });
440        }
441
442        if !claim169.unknown_fields.is_empty() {
443            warnings.push(Warning {
444                code: WarningCode::UnknownFields,
445                message: format!(
446                    "Found {} unknown fields (keys: {:?})",
447                    claim169.unknown_fields.len(),
448                    claim169.unknown_fields.keys().collect::<Vec<_>>()
449                ),
450            });
451        }
452
453        Ok(DecodeResult {
454            claim169,
455            cwt_meta: cwt_result.meta,
456            verification_status: cose_result.verification_status,
457            warnings,
458        })
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465    use crate::model::{Claim169, CwtMeta, VerificationStatus};
466
467    // Create a minimal test QR payload (unsigned, for testing basic functionality)
468    fn create_test_qr() -> String {
469        use crate::Encoder;
470
471        let claim169 = Claim169 {
472            id: Some("test-123".to_string()),
473            full_name: Some("Test User".to_string()),
474            ..Default::default()
475        };
476
477        let cwt_meta = CwtMeta::new()
478            .with_issuer("https://test.issuer")
479            .with_expires_at(i64::MAX); // Far future expiration
480
481        Encoder::new(claim169, cwt_meta)
482            .allow_unsigned()
483            .encode()
484            .unwrap()
485    }
486
487    #[test]
488    fn test_decoder_requires_verifier_or_allow_unverified() {
489        let qr_text = create_test_qr();
490
491        let result = Decoder::new(&qr_text).decode();
492
493        assert!(result.is_err());
494        match result.unwrap_err() {
495            Claim169Error::DecodingConfig(msg) => {
496                assert!(msg.contains("allow_unverified"));
497            }
498            e => panic!("Expected DecodingConfig error, got: {:?}", e),
499        }
500    }
501
502    #[test]
503    fn test_decoder_allow_unverified() {
504        let qr_text = create_test_qr();
505
506        let result = Decoder::new(&qr_text).allow_unverified().decode();
507
508        assert!(result.is_ok());
509        let decoded = result.unwrap();
510        assert_eq!(decoded.claim169.id, Some("test-123".to_string()));
511        assert_eq!(decoded.claim169.full_name, Some("Test User".to_string()));
512        assert_eq!(decoded.verification_status, VerificationStatus::Skipped);
513    }
514
515    #[test]
516    fn test_decoder_accepts_string() {
517        let qr_text = create_test_qr();
518
519        // Test with String
520        let result = Decoder::new(qr_text.clone()).allow_unverified().decode();
521        assert!(result.is_ok());
522
523        // Test with &str
524        let result = Decoder::new(&qr_text).allow_unverified().decode();
525        assert!(result.is_ok());
526    }
527
528    #[test]
529    fn test_decoder_skip_biometrics() {
530        let qr_text = create_test_qr();
531
532        let result = Decoder::new(&qr_text)
533            .allow_unverified()
534            .skip_biometrics()
535            .decode()
536            .unwrap();
537
538        // Should have a warning about skipped biometrics
539        assert!(result
540            .warnings
541            .iter()
542            .any(|w| w.code == WarningCode::BiometricsSkipped));
543    }
544
545    #[test]
546    fn test_decoder_without_timestamp_validation() {
547        let qr_text = create_test_qr();
548
549        let result = Decoder::new(&qr_text)
550            .allow_unverified()
551            .without_timestamp_validation()
552            .decode()
553            .unwrap();
554
555        // Should have a warning about skipped validation
556        assert!(result
557            .warnings
558            .iter()
559            .any(|w| w.code == WarningCode::TimestampValidationSkipped));
560    }
561
562    #[cfg(feature = "software-crypto")]
563    #[test]
564    fn test_decoder_roundtrip_ed25519() {
565        use crate::crypto::software::Ed25519Signer;
566        use crate::Encoder;
567        use coset::iana;
568
569        let claim169 = Claim169 {
570            id: Some("signed-test".to_string()),
571            full_name: Some("Signed User".to_string()),
572            email: Some("signed@example.com".to_string()),
573            ..Default::default()
574        };
575
576        let cwt_meta = CwtMeta::new()
577            .with_issuer("https://signed.test")
578            .with_expires_at(i64::MAX);
579
580        // Generate key pair
581        let signer = Ed25519Signer::generate();
582        let public_key = signer.public_key_bytes();
583
584        // Encode
585        let qr_data = Encoder::new(claim169.clone(), cwt_meta)
586            .sign_with(signer, iana::Algorithm::EdDSA)
587            .encode()
588            .unwrap();
589
590        // Decode with verification
591        let result = Decoder::new(&qr_data)
592            .verify_with_ed25519(&public_key)
593            .unwrap()
594            .decode()
595            .unwrap();
596
597        assert_eq!(result.verification_status, VerificationStatus::Verified);
598        assert_eq!(result.claim169.id, claim169.id);
599        assert_eq!(result.claim169.full_name, claim169.full_name);
600        assert_eq!(result.claim169.email, claim169.email);
601    }
602
603    #[cfg(feature = "software-crypto")]
604    #[test]
605    fn test_decoder_roundtrip_encrypted() {
606        use crate::crypto::software::Ed25519Signer;
607        use crate::Encoder;
608        use coset::iana;
609
610        let claim169 = Claim169 {
611            id: Some("encrypted-test".to_string()),
612            full_name: Some("Encrypted User".to_string()),
613            ..Default::default()
614        };
615
616        let cwt_meta = CwtMeta::new()
617            .with_issuer("https://encrypted.test")
618            .with_expires_at(i64::MAX);
619
620        // Generate keys
621        let signer = Ed25519Signer::generate();
622        let public_key = signer.public_key_bytes();
623        let aes_key = [42u8; 32];
624        let nonce = [7u8; 12];
625
626        // Encode with signing and encryption
627        let qr_data = Encoder::new(claim169.clone(), cwt_meta)
628            .sign_with(signer, iana::Algorithm::EdDSA)
629            .encrypt_with_aes256_nonce(&aes_key, &nonce)
630            .unwrap()
631            .encode()
632            .unwrap();
633
634        // Decode with decryption and verification
635        let result = Decoder::new(&qr_data)
636            .decrypt_with_aes256(&aes_key)
637            .unwrap()
638            .verify_with_ed25519(&public_key)
639            .unwrap()
640            .decode()
641            .unwrap();
642
643        assert_eq!(result.verification_status, VerificationStatus::Verified);
644        assert_eq!(result.claim169.id, claim169.id);
645        assert_eq!(result.claim169.full_name, claim169.full_name);
646    }
647
648    #[cfg(feature = "software-crypto")]
649    #[test]
650    fn test_decoder_wrong_key_fails() {
651        use crate::crypto::software::Ed25519Signer;
652        use crate::Encoder;
653        use coset::iana;
654
655        let claim169 = Claim169::minimal("test", "Test");
656        let cwt_meta = CwtMeta::default();
657
658        let signer = Ed25519Signer::generate();
659        let wrong_signer = Ed25519Signer::generate();
660        let wrong_public_key = wrong_signer.public_key_bytes();
661
662        let qr_data = Encoder::new(claim169, cwt_meta)
663            .sign_with(signer, iana::Algorithm::EdDSA)
664            .encode()
665            .unwrap();
666
667        // Try to decode with wrong key
668        let result = Decoder::new(&qr_data)
669            .verify_with_ed25519(&wrong_public_key)
670            .unwrap()
671            .decode();
672
673        assert!(result.is_err());
674        assert!(matches!(
675            result.unwrap_err(),
676            Claim169Error::SignatureInvalid(_)
677        ));
678    }
679
680    #[test]
681    fn test_decoder_invalid_base45() {
682        let result = Decoder::new("!!!invalid base45!!!")
683            .allow_unverified()
684            .decode();
685
686        assert!(result.is_err());
687        assert!(matches!(
688            result.unwrap_err(),
689            Claim169Error::Base45Decode(_)
690        ));
691    }
692
693    #[test]
694    fn test_decoder_max_decompressed_bytes() {
695        let qr_text = create_test_qr();
696
697        // Set a very small limit that will be exceeded
698        let result = Decoder::new(&qr_text)
699            .allow_unverified()
700            .max_decompressed_bytes(10)
701            .decode();
702
703        assert!(result.is_err());
704        assert!(matches!(
705            result.unwrap_err(),
706            Claim169Error::DecompressLimitExceeded { .. }
707        ));
708    }
709}