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, 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                .map_err(|_| {
411                    Claim169Error::DecodingConfig("system clock is before Unix epoch".to_string())
412                })?;
413
414            let skew = self.clock_skew_tolerance_seconds;
415
416            if let Some(exp) = cwt_result.meta.expires_at {
417                if now > exp + skew {
418                    return Err(Claim169Error::Expired(exp));
419                }
420            }
421
422            if let Some(nbf) = cwt_result.meta.not_before {
423                if now + skew < nbf {
424                    return Err(Claim169Error::NotYetValid(nbf));
425                }
426            }
427        } else {
428            warnings.push(Warning {
429                code: WarningCode::TimestampValidationSkipped,
430                message: "Timestamp validation was disabled".to_string(),
431            });
432        }
433
434        // Step 7: Transform claim 169
435        let claim169 = pipeline::claim169_transform(cwt_result.claim_169, self.skip_biometrics)?;
436
437        if self.skip_biometrics {
438            warnings.push(Warning {
439                code: WarningCode::BiometricsSkipped,
440                message: "Biometric data was skipped".to_string(),
441            });
442        }
443
444        if !claim169.unknown_fields.is_empty() {
445            warnings.push(Warning {
446                code: WarningCode::UnknownFields,
447                message: format!(
448                    "Found {} unknown fields (keys: {:?})",
449                    claim169.unknown_fields.len(),
450                    claim169.unknown_fields.keys().collect::<Vec<_>>()
451                ),
452            });
453        }
454
455        Ok(DecodeResult {
456            claim169,
457            cwt_meta: cwt_result.meta,
458            verification_status: cose_result.verification_status,
459            x509_headers: cose_result.x509_headers,
460            warnings,
461        })
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468    use crate::model::{Claim169, CwtMeta, VerificationStatus};
469
470    // Create a minimal test QR payload (unsigned, for testing basic functionality)
471    fn create_test_qr() -> String {
472        use crate::Encoder;
473
474        let claim169 = Claim169 {
475            id: Some("test-123".to_string()),
476            full_name: Some("Test User".to_string()),
477            ..Default::default()
478        };
479
480        let cwt_meta = CwtMeta::new()
481            .with_issuer("https://test.issuer")
482            .with_expires_at(i64::MAX); // Far future expiration
483
484        Encoder::new(claim169, cwt_meta)
485            .allow_unsigned()
486            .encode()
487            .unwrap()
488    }
489
490    #[test]
491    fn test_decoder_requires_verifier_or_allow_unverified() {
492        let qr_text = create_test_qr();
493
494        let result = Decoder::new(&qr_text).decode();
495
496        assert!(result.is_err());
497        match result.unwrap_err() {
498            Claim169Error::DecodingConfig(msg) => {
499                assert!(msg.contains("allow_unverified"));
500            }
501            e => panic!("Expected DecodingConfig error, got: {:?}", e),
502        }
503    }
504
505    #[test]
506    fn test_decoder_allow_unverified() {
507        let qr_text = create_test_qr();
508
509        let result = Decoder::new(&qr_text).allow_unverified().decode();
510
511        assert!(result.is_ok());
512        let decoded = result.unwrap();
513        assert_eq!(decoded.claim169.id, Some("test-123".to_string()));
514        assert_eq!(decoded.claim169.full_name, Some("Test User".to_string()));
515        assert_eq!(decoded.verification_status, VerificationStatus::Skipped);
516    }
517
518    #[test]
519    fn test_decoder_accepts_string() {
520        let qr_text = create_test_qr();
521
522        // Test with String
523        let result = Decoder::new(qr_text.clone()).allow_unverified().decode();
524        assert!(result.is_ok());
525
526        // Test with &str
527        let result = Decoder::new(&qr_text).allow_unverified().decode();
528        assert!(result.is_ok());
529    }
530
531    #[test]
532    fn test_decoder_skip_biometrics() {
533        let qr_text = create_test_qr();
534
535        let result = Decoder::new(&qr_text)
536            .allow_unverified()
537            .skip_biometrics()
538            .decode()
539            .unwrap();
540
541        // Should have a warning about skipped biometrics
542        assert!(result
543            .warnings
544            .iter()
545            .any(|w| w.code == WarningCode::BiometricsSkipped));
546    }
547
548    #[test]
549    fn test_decoder_without_timestamp_validation() {
550        let qr_text = create_test_qr();
551
552        let result = Decoder::new(&qr_text)
553            .allow_unverified()
554            .without_timestamp_validation()
555            .decode()
556            .unwrap();
557
558        // Should have a warning about skipped validation
559        assert!(result
560            .warnings
561            .iter()
562            .any(|w| w.code == WarningCode::TimestampValidationSkipped));
563    }
564
565    #[cfg(feature = "software-crypto")]
566    #[test]
567    fn test_decoder_roundtrip_ed25519() {
568        use crate::crypto::software::Ed25519Signer;
569        use crate::Encoder;
570        use coset::iana;
571
572        let claim169 = Claim169 {
573            id: Some("signed-test".to_string()),
574            full_name: Some("Signed User".to_string()),
575            email: Some("signed@example.com".to_string()),
576            ..Default::default()
577        };
578
579        let cwt_meta = CwtMeta::new()
580            .with_issuer("https://signed.test")
581            .with_expires_at(i64::MAX);
582
583        // Generate key pair
584        let signer = Ed25519Signer::generate();
585        let public_key = signer.public_key_bytes();
586
587        // Encode
588        let qr_data = Encoder::new(claim169.clone(), cwt_meta)
589            .sign_with(signer, iana::Algorithm::EdDSA)
590            .encode()
591            .unwrap();
592
593        // Decode with verification
594        let result = Decoder::new(&qr_data)
595            .verify_with_ed25519(&public_key)
596            .unwrap()
597            .decode()
598            .unwrap();
599
600        assert_eq!(result.verification_status, VerificationStatus::Verified);
601        assert_eq!(result.claim169.id, claim169.id);
602        assert_eq!(result.claim169.full_name, claim169.full_name);
603        assert_eq!(result.claim169.email, claim169.email);
604    }
605
606    #[cfg(feature = "software-crypto")]
607    #[test]
608    fn test_decoder_roundtrip_encrypted() {
609        use crate::crypto::software::Ed25519Signer;
610        use crate::Encoder;
611        use coset::iana;
612
613        let claim169 = Claim169 {
614            id: Some("encrypted-test".to_string()),
615            full_name: Some("Encrypted User".to_string()),
616            ..Default::default()
617        };
618
619        let cwt_meta = CwtMeta::new()
620            .with_issuer("https://encrypted.test")
621            .with_expires_at(i64::MAX);
622
623        // Generate keys
624        let signer = Ed25519Signer::generate();
625        let public_key = signer.public_key_bytes();
626        let aes_key = [42u8; 32];
627        let nonce = [7u8; 12];
628
629        // Encode with signing and encryption
630        let qr_data = Encoder::new(claim169.clone(), cwt_meta)
631            .sign_with(signer, iana::Algorithm::EdDSA)
632            .encrypt_with_aes256_nonce(&aes_key, &nonce)
633            .unwrap()
634            .encode()
635            .unwrap();
636
637        // Decode with decryption and verification
638        let result = Decoder::new(&qr_data)
639            .decrypt_with_aes256(&aes_key)
640            .unwrap()
641            .verify_with_ed25519(&public_key)
642            .unwrap()
643            .decode()
644            .unwrap();
645
646        assert_eq!(result.verification_status, VerificationStatus::Verified);
647        assert_eq!(result.claim169.id, claim169.id);
648        assert_eq!(result.claim169.full_name, claim169.full_name);
649    }
650
651    #[cfg(feature = "software-crypto")]
652    #[test]
653    fn test_decoder_wrong_key_fails() {
654        use crate::crypto::software::Ed25519Signer;
655        use crate::Encoder;
656        use coset::iana;
657
658        let claim169 = Claim169::minimal("test", "Test");
659        let cwt_meta = CwtMeta::default();
660
661        let signer = Ed25519Signer::generate();
662        let wrong_signer = Ed25519Signer::generate();
663        let wrong_public_key = wrong_signer.public_key_bytes();
664
665        let qr_data = Encoder::new(claim169, cwt_meta)
666            .sign_with(signer, iana::Algorithm::EdDSA)
667            .encode()
668            .unwrap();
669
670        // Try to decode with wrong key
671        let result = Decoder::new(&qr_data)
672            .verify_with_ed25519(&wrong_public_key)
673            .unwrap()
674            .decode();
675
676        assert!(result.is_err());
677        assert!(matches!(
678            result.unwrap_err(),
679            Claim169Error::SignatureInvalid(_)
680        ));
681    }
682
683    #[test]
684    fn test_decoder_invalid_base45() {
685        let result = Decoder::new("!!!invalid base45!!!")
686            .allow_unverified()
687            .decode();
688
689        assert!(result.is_err());
690        assert!(matches!(
691            result.unwrap_err(),
692            Claim169Error::Base45Decode(_)
693        ));
694    }
695
696    #[test]
697    fn test_decoder_max_decompressed_bytes() {
698        let qr_text = create_test_qr();
699
700        // Set a very small limit that will be exceeded
701        let result = Decoder::new(&qr_text)
702            .allow_unverified()
703            .max_decompressed_bytes(10)
704            .decode();
705
706        assert!(result.is_err());
707        assert!(matches!(
708            result.unwrap_err(),
709            Claim169Error::DecompressLimitExceeded { .. }
710        ));
711    }
712}