Skip to main content

claim169_core/
lib.rs

1//! # claim169-core
2//!
3//! A Rust library for encoding and decoding MOSIP Claim 169 QR codes.
4//!
5//! ## Overview
6//!
7//! [MOSIP Claim 169](https://github.com/mosip/id-claim-169/tree/main) defines a standard for encoding identity data
8//! in QR codes, designed for offline verification of digital identity credentials. The format
9//! uses a compact binary encoding optimized for QR code capacity constraints.
10//!
11//! The encoding pipeline:
12//! ```text
13//! Claim169 → CBOR → CWT → COSE_Sign1 → [COSE_Encrypt0] → zlib → Base45 → QR Code
14//! ```
15//!
16//! Key technologies:
17//! - **CBOR**: Compact binary encoding with numeric keys for minimal size
18//! - **CWT**: CBOR Web Token for standard claims (issuer, expiration, etc.)
19//! - **COSE_Sign1**: Digital signature for authenticity (Ed25519 or ECDSA P-256)
20//! - **COSE_Encrypt0**: Optional encryption layer (AES-GCM)
21//! - **zlib + Base45**: Compression and alphanumeric encoding for QR efficiency
22//!
23//! ## Quick Start
24//!
25//! ### Encoding (Creating QR Codes)
26//!
27//! ```rust,ignore
28//! use claim169_core::{Encoder, Claim169, CwtMeta};
29//!
30//! let claim169 = Claim169 {
31//!     id: Some("123456789".to_string()),
32//!     full_name: Some("John Doe".to_string()),
33//!     ..Default::default()
34//! };
35//!
36//! let cwt_meta = CwtMeta::new()
37//!     .with_issuer("https://issuer.example.com")
38//!     .with_expires_at(1800000000);
39//!
40//! // Ed25519 signed (recommended)
41//! let qr_data = Encoder::new(claim169, cwt_meta)
42//!     .sign_with_ed25519(&private_key)?
43//!     .encode()?;
44//!
45//! // Signed and encrypted
46//! let qr_data = Encoder::new(claim169, cwt_meta)
47//!     .sign_with_ed25519(&private_key)?
48//!     .encrypt_with_aes256(&aes_key)?
49//!     .encode()?;
50//!
51//! // Unsigned (testing only - requires explicit opt-in)
52//! let qr_data = Encoder::new(claim169, cwt_meta)
53//!     .allow_unsigned()
54//!     .encode()?;
55//! ```
56//!
57//! ### Decoding (Reading QR Codes)
58//!
59//! ```rust,ignore
60//! use claim169_core::Decoder;
61//!
62//! // With Ed25519 verification (recommended)
63//! let result = Decoder::new(qr_content)
64//!     .verify_with_ed25519(&public_key)?
65//!     .decode()?;
66//!
67//! println!("ID: {:?}", result.claim169.id);
68//! println!("Name: {:?}", result.claim169.full_name);
69//! println!("Issuer: {:?}", result.cwt_meta.issuer);
70//!
71//! // Decrypting encrypted credentials
72//! let result = Decoder::new(qr_content)
73//!     .decrypt_with_aes256(&aes_key)?
74//!     .verify_with_ed25519(&public_key)?
75//!     .decode()?;
76//!
77//! // Without verification (testing only - requires explicit opt-in)
78//! let result = Decoder::new(qr_content)
79//!     .allow_unverified()
80//!     .decode()?;
81//! ```
82//!
83//! ### Using Custom Cryptography (HSM Integration)
84//!
85//! For hardware security modules or custom cryptographic backends:
86//!
87//! ```rust,ignore
88//! use claim169_core::{Encoder, Decoder, Signer, SignatureVerifier};
89//!
90//! // Implement the Signer trait for your HSM
91//! struct HsmSigner { /* ... */ }
92//! impl Signer for HsmSigner { /* ... */ }
93//!
94//! // Encoding with HSM
95//! let qr_data = Encoder::new(claim169, cwt_meta)
96//!     .sign_with(hsm_signer, iana::Algorithm::EdDSA)
97//!     .encode()?;
98//!
99//! // Decoding with HSM
100//! let result = Decoder::new(qr_content)
101//!     .verify_with(hsm_verifier)
102//!     .decode()?;
103//! ```
104//!
105//! ## Security Considerations
106//!
107//! - **Always verify signatures** in production - use `.verify_with_*()` methods
108//! - **Always sign credentials** in production - use `.sign_with_*()` methods
109//! - Unsigned/unverified requires explicit opt-in with `.allow_unsigned()`/`.allow_unverified()`
110//! - Decompression is limited to prevent zip bomb attacks (default: 64KB)
111//! - Timestamps are validated by default; use `.without_timestamp_validation()` to disable
112//! - Weak cryptographic keys (all-zeros, small-order points) are automatically rejected
113//!
114//! ## Features
115//!
116//! | Feature | Default | Description |
117//! |---------|---------|-------------|
118//! | `software-crypto` | ✓ | Software implementations of Ed25519, ECDSA P-256, and AES-GCM |
119//!
120//! Disable default features to integrate with HSMs or custom cryptographic backends:
121//!
122//! ```toml
123//! [dependencies]
124//! claim169-core = { version = "0.1", default-features = false }
125//! ```
126//!
127//! Then implement the [`Signer`], [`SignatureVerifier`], [`Encryptor`], or [`Decryptor`] traits.
128//!
129//! ## Modules
130//!
131//! - [`crypto`]: Cryptographic traits and implementations
132//! - [`error`]: Error types for encoding, decoding, and crypto operations
133//! - [`model`]: Data structures for Claim 169 identity data
134//! - [`pipeline`]: Low-level encoding/decoding pipeline functions
135
136pub mod crypto;
137pub mod decode;
138pub mod encode;
139pub mod error;
140pub mod model;
141pub mod pipeline;
142pub mod serde_utils;
143
144// Re-export builder pattern API (primary interface)
145pub use decode::Decoder;
146pub use encode::{EncodeResult, Encoder};
147
148// Re-export nonce generation when software-crypto is enabled
149#[cfg(feature = "software-crypto")]
150pub use encode::generate_random_nonce;
151
152// Re-export cryptographic traits (for HSM integration)
153pub use crypto::traits::{Decryptor, Encryptor, KeyResolver, SignatureVerifier, Signer};
154
155// Re-export error types
156pub use error::{Claim169Error, CryptoError, CryptoResult, Result};
157
158// Re-export model types
159pub use model::{
160    Biometric, BiometricFormat, BiometricSubFormat, CertHashAlgorithm, CertificateHash, Claim169,
161    CwtMeta, Gender, MaritalStatus, PhotoFormat, VerificationStatus, X509Headers,
162};
163
164// Re-export compression types
165pub use pipeline::{Compression, DetectedCompression};
166
167// Re-export COSE AAD builder for Encrypt0 (shared between decode and encode pipelines)
168pub use pipeline::cose::build_encrypt0_aad;
169
170// Re-export software crypto implementations when feature is enabled
171#[cfg(feature = "software-crypto")]
172pub use crypto::{
173    AesGcmDecryptor, AesGcmEncryptor, EcdsaP256Signer, EcdsaP256Verifier, Ed25519Signer,
174    Ed25519Verifier,
175};
176
177/// Result of successfully decoding a Claim 169 QR code.
178///
179/// This struct contains all the data extracted from the QR code:
180/// - The identity data ([`Claim169`])
181/// - CWT metadata like issuer and expiration ([`CwtMeta`])
182/// - The signature verification status
183/// - Any warnings generated during decoding
184///
185/// # Example
186///
187/// ```rust,ignore
188/// let result = Decoder::new(qr_content)
189///     .verify_with_ed25519(&public_key)?
190///     .decode()?;
191///
192/// // Access identity data
193/// if let Some(name) = &result.claim169.full_name {
194///     println!("Welcome, {}!", name);
195/// }
196///
197/// // Check verification status
198/// match result.verification_status {
199///     VerificationStatus::Verified => println!("Signature verified"),
200///     VerificationStatus::Skipped => println!("Verification skipped"),
201///     VerificationStatus::Failed => println!("Verification failed"),
202/// }
203///
204/// // Check for warnings
205/// for warning in &result.warnings {
206///     println!("Warning: {}", warning.message);
207/// }
208/// ```
209#[non_exhaustive]
210#[derive(Debug)]
211pub struct DecodeResult {
212    /// The extracted Claim 169 identity data.
213    ///
214    /// Contains demographic information (name, date of birth, address, etc.)
215    /// and optionally biometric data (fingerprints, iris scans, face images).
216    pub claim169: Claim169,
217
218    /// CWT (CBOR Web Token) metadata.
219    ///
220    /// Contains standard claims like issuer, subject, expiration time,
221    /// and issued-at timestamp.
222    pub cwt_meta: CwtMeta,
223
224    /// Signature verification status.
225    ///
226    /// - `Verified`: Signature was checked and is valid
227    /// - `Skipped`: No verifier was provided (only if `allow_unverified` was set)
228    /// - `Failed`: Signature verification failed (this typically returns an error instead)
229    pub verification_status: VerificationStatus,
230
231    /// X.509 certificate headers from the COSE structure.
232    ///
233    /// Contains any X.509 certificate information present in the COSE
234    /// protected/unprotected headers (x5bag, x5chain, x5t, x5u).
235    pub x509_headers: X509Headers,
236
237    /// The compression format detected during decoding.
238    ///
239    /// Indicates which compression format was auto-detected and used:
240    /// `Zlib` (spec-compliant), `Brotli` (non-standard), or `None` (raw).
241    pub detected_compression: DetectedCompression,
242
243    /// Warnings generated during decoding.
244    ///
245    /// Non-fatal issues that don't prevent decoding but may warrant attention,
246    /// such as unknown fields (forward compatibility) or skipped validations.
247    pub warnings: Vec<Warning>,
248
249    /// Key ID from the COSE protected header, if present.
250    ///
251    /// Useful for identifying which key was used for signing in multi-issuer
252    /// or key-rotation scenarios.
253    pub key_id: Option<Vec<u8>>,
254
255    /// COSE algorithm used for signing or encryption.
256    ///
257    /// Reflects the algorithm declared in the COSE protected header (e.g., EdDSA, ES256).
258    pub algorithm: Option<coset::iana::Algorithm>,
259}
260
261/// A warning generated during the decoding process.
262///
263/// Warnings represent non-fatal issues that don't prevent successful decoding
264/// but may be relevant for logging or auditing purposes.
265#[derive(Debug, Clone)]
266pub struct Warning {
267    /// The type of warning.
268    pub code: WarningCode,
269    /// Human-readable description of the warning.
270    pub message: String,
271}
272
273/// Types of warnings that can be generated during decoding.
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum WarningCode {
276    /// The credential will expire soon (within a configurable threshold).
277    ExpiringSoon,
278    /// Unknown fields were found in the Claim 169 data.
279    ///
280    /// This supports forward compatibility - new fields added to the spec
281    /// won't break older decoders. The unknown fields are preserved in
282    /// `Claim169::unknown_fields`.
283    UnknownFields,
284    /// Timestamp validation was explicitly disabled via options.
285    TimestampValidationSkipped,
286    /// Biometric data parsing was skipped via options.
287    BiometricsSkipped,
288    /// Non-standard compression was detected during decoding or used during encoding.
289    ///
290    /// The Claim 169 spec mandates zlib compression. This warning indicates
291    /// that a different compression format (brotli, none) was used.
292    NonStandardCompression,
293}
294
295impl WarningCode {
296    /// Returns the snake_case string representation of this warning code.
297    pub fn as_str(self) -> &'static str {
298        match self {
299            WarningCode::ExpiringSoon => "expiring_soon",
300            WarningCode::UnknownFields => "unknown_fields",
301            WarningCode::TimestampValidationSkipped => "timestamp_validation_skipped",
302            WarningCode::BiometricsSkipped => "biometrics_skipped",
303            WarningCode::NonStandardCompression => "non_standard_compression",
304        }
305    }
306}
307
308impl std::fmt::Display for WarningCode {
309    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310        f.write_str(self.as_str())
311    }
312}
313
314/// Returns the standard string name for a COSE algorithm.
315///
316/// Maps well-known IANA COSE algorithm identifiers to their standard names
317/// (e.g., `EdDSA`, `ES256`, `A256GCM`). Unknown algorithms are formatted
318/// as `COSE_ALG_<id>`.
319pub fn algorithm_to_string(alg: coset::iana::Algorithm) -> String {
320    use coset::iana::EnumI64;
321    match alg {
322        coset::iana::Algorithm::EdDSA => "EdDSA".to_string(),
323        coset::iana::Algorithm::ES256 => "ES256".to_string(),
324        coset::iana::Algorithm::ES384 => "ES384".to_string(),
325        coset::iana::Algorithm::ES512 => "ES512".to_string(),
326        coset::iana::Algorithm::A128GCM => "A128GCM".to_string(),
327        coset::iana::Algorithm::A192GCM => "A192GCM".to_string(),
328        coset::iana::Algorithm::A256GCM => "A256GCM".to_string(),
329        other => format!("COSE_ALG_{}", other.to_i64()),
330    }
331}
332
333/// Parses a COSE algorithm string name back to its enum representation.
334///
335/// Accepts standard names (`EdDSA`, `ES256`, etc.) and the fallback format
336/// `COSE_ALG_<id>` for numeric algorithm identifiers.
337pub fn algorithm_from_string(s: &str) -> Result<coset::iana::Algorithm> {
338    use coset::iana::EnumI64;
339    match s {
340        "EdDSA" => Ok(coset::iana::Algorithm::EdDSA),
341        "ES256" => Ok(coset::iana::Algorithm::ES256),
342        "ES384" => Ok(coset::iana::Algorithm::ES384),
343        "ES512" => Ok(coset::iana::Algorithm::ES512),
344        "A128GCM" => Ok(coset::iana::Algorithm::A128GCM),
345        "A192GCM" => Ok(coset::iana::Algorithm::A192GCM),
346        "A256GCM" => Ok(coset::iana::Algorithm::A256GCM),
347        _ => {
348            if let Some(id_str) = s.strip_prefix("COSE_ALG_") {
349                let id: i64 = id_str.parse().map_err(|_| {
350                    Claim169Error::CoseParse(format!("invalid numeric algorithm ID: {}", s))
351                })?;
352                coset::iana::Algorithm::from_i64(id).ok_or_else(|| {
353                    Claim169Error::CoseParse(format!("unknown COSE algorithm ID: {}", id))
354                })
355            } else {
356                Err(Claim169Error::CoseParse(format!(
357                    "unknown algorithm: {}",
358                    s
359                )))
360            }
361        }
362    }
363}
364
365/// Metadata extracted from a credential without full verification or decoding.
366///
367/// Useful for determining which key to use before calling `Decoder::decode()`.
368/// This allows verifiers in multi-issuer scenarios to:
369/// 1. Inspect the credential to find the issuer and key ID
370/// 2. Look up the correct verification key
371/// 3. Perform full decoding with the appropriate key
372///
373/// # Example
374///
375/// ```rust,ignore
376/// use claim169_core::inspect;
377///
378/// let info = inspect(qr_text)?;
379/// println!("Issuer: {:?}, Key ID: {:?}", info.issuer, info.key_id);
380///
381/// // Use the metadata to select the right verification key
382/// let public_key = key_store.get(&info.issuer, &info.key_id);
383/// let result = Decoder::new(qr_text)
384///     .verify_with_ed25519(&public_key)?
385///     .decode()?;
386/// ```
387#[non_exhaustive]
388#[derive(Debug, Clone)]
389pub struct InspectResult {
390    /// Issuer from CWT claims (claim key 1).
391    pub issuer: Option<String>,
392    /// Subject from CWT claims (claim key 2).
393    pub subject: Option<String>,
394    /// Key ID from the COSE header.
395    pub key_id: Option<Vec<u8>>,
396    /// COSE algorithm declared in the protected header.
397    pub algorithm: Option<coset::iana::Algorithm>,
398    /// X.509 certificate headers from the COSE structure.
399    pub x509_headers: X509Headers,
400    /// Expiration time from CWT claims (Unix epoch seconds).
401    pub expires_at: Option<i64>,
402    /// COSE structure type (Sign1 or Encrypt0).
403    pub cose_type: pipeline::CoseType,
404}
405
406/// Inspect credential metadata without full decoding or verification.
407///
408/// Runs Base45 → decompress → COSE header parse → CWT parse, but skips
409/// signature verification. For encrypted credentials (COSE_Encrypt0), only
410/// the outer COSE headers are accessible since the CWT payload is encrypted;
411/// CWT-level fields (issuer, subject, expires_at) will be `None`.
412///
413/// This is useful for multi-issuer or key-rotation scenarios where you need
414/// to determine which verification key to use before decoding.
415pub fn inspect(qr_text: &str) -> error::Result<InspectResult> {
416    let compressed = pipeline::base45_decode(qr_text)?;
417    let (cose_bytes, _) =
418        pipeline::decompress(&compressed, decode::DEFAULT_MAX_DECOMPRESSED_BYTES)?;
419
420    // Try full path: parse COSE headers + CWT payload (works for Sign1)
421    match pipeline::cose_parse(&cose_bytes, None, None) {
422        Ok(cose_result) => {
423            let cwt_result = pipeline::cwt_parse(&cose_result.payload)?;
424            Ok(InspectResult {
425                issuer: cwt_result.meta.issuer,
426                subject: cwt_result.meta.subject,
427                key_id: cose_result.key_id,
428                algorithm: cose_result.algorithm,
429                x509_headers: cose_result.x509_headers,
430                expires_at: cwt_result.meta.expires_at,
431                cose_type: pipeline::CoseType::Sign1,
432            })
433        }
434        Err(Claim169Error::DecryptionFailed(_) | Claim169Error::UnsupportedCoseType(_)) => {
435            // Expected for Encrypt0 payloads or unsupported COSE types -
436            // fall back to header-only inspection
437            let inspect_result = pipeline::cose_inspect(&cose_bytes)?;
438            Ok(InspectResult {
439                issuer: None,
440                subject: None,
441                key_id: inspect_result.key_id,
442                algorithm: inspect_result.algorithm,
443                x509_headers: inspect_result.x509_headers,
444                expires_at: None,
445                cose_type: inspect_result.cose_type,
446            })
447        }
448        Err(e) => Err(e),
449    }
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    #[test]
457    fn test_warning_code_equality() {
458        assert_eq!(WarningCode::ExpiringSoon, WarningCode::ExpiringSoon);
459        assert_ne!(WarningCode::ExpiringSoon, WarningCode::UnknownFields);
460    }
461
462    #[test]
463    fn test_warning_clone() {
464        let warning = Warning {
465            code: WarningCode::BiometricsSkipped,
466            message: "Test warning".to_string(),
467        };
468        let cloned = warning.clone();
469        assert_eq!(cloned.code, warning.code);
470        assert_eq!(cloned.message, warning.message);
471    }
472
473    #[test]
474    fn test_warning_code_as_str() {
475        assert_eq!(WarningCode::ExpiringSoon.as_str(), "expiring_soon");
476        assert_eq!(WarningCode::UnknownFields.as_str(), "unknown_fields");
477        assert_eq!(
478            WarningCode::TimestampValidationSkipped.as_str(),
479            "timestamp_validation_skipped"
480        );
481        assert_eq!(
482            WarningCode::BiometricsSkipped.as_str(),
483            "biometrics_skipped"
484        );
485        assert_eq!(
486            WarningCode::NonStandardCompression.as_str(),
487            "non_standard_compression"
488        );
489    }
490
491    #[test]
492    fn test_warning_code_display() {
493        assert_eq!(format!("{}", WarningCode::ExpiringSoon), "expiring_soon");
494        assert_eq!(
495            format!("{}", WarningCode::NonStandardCompression),
496            "non_standard_compression"
497        );
498    }
499
500    #[test]
501    fn test_algorithm_to_string_known() {
502        assert_eq!(algorithm_to_string(coset::iana::Algorithm::EdDSA), "EdDSA");
503        assert_eq!(algorithm_to_string(coset::iana::Algorithm::ES256), "ES256");
504        assert_eq!(
505            algorithm_to_string(coset::iana::Algorithm::A256GCM),
506            "A256GCM"
507        );
508    }
509
510    #[test]
511    fn test_algorithm_to_string_unknown() {
512        let s = algorithm_to_string(coset::iana::Algorithm::ES384);
513        assert_eq!(s, "ES384");
514    }
515
516    #[test]
517    fn test_algorithm_from_string_known() {
518        assert_eq!(
519            algorithm_from_string("EdDSA").unwrap(),
520            coset::iana::Algorithm::EdDSA
521        );
522        assert_eq!(
523            algorithm_from_string("ES256").unwrap(),
524            coset::iana::Algorithm::ES256
525        );
526        assert_eq!(
527            algorithm_from_string("A256GCM").unwrap(),
528            coset::iana::Algorithm::A256GCM
529        );
530    }
531
532    #[test]
533    fn test_algorithm_from_string_roundtrip() {
534        let algs = [
535            coset::iana::Algorithm::EdDSA,
536            coset::iana::Algorithm::ES256,
537            coset::iana::Algorithm::ES384,
538            coset::iana::Algorithm::ES512,
539            coset::iana::Algorithm::A128GCM,
540            coset::iana::Algorithm::A192GCM,
541            coset::iana::Algorithm::A256GCM,
542        ];
543        for alg in algs {
544            let s = algorithm_to_string(alg);
545            let parsed = algorithm_from_string(&s).unwrap();
546            assert_eq!(parsed, alg, "roundtrip failed for {}", s);
547        }
548    }
549
550    #[test]
551    fn test_algorithm_from_string_invalid() {
552        assert!(algorithm_from_string("INVALID").is_err());
553        assert!(algorithm_from_string("COSE_ALG_abc").is_err());
554    }
555
556    #[cfg(feature = "software-crypto")]
557    #[test]
558    fn test_inspect_returns_issuer_kid_algorithm() {
559        use crypto::software::Ed25519Signer;
560
561        let claim = model::Claim169::minimal("inspect-test", "Test User");
562        let cwt = model::CwtMeta::new()
563            .with_issuer("https://inspect.issuer.io")
564            .with_subject("subject-456")
565            .with_expires_at(1900000000);
566
567        let mut signer = Ed25519Signer::generate();
568        signer.set_key_id(b"inspect-key-1".to_vec());
569
570        let qr_data = Encoder::new(claim, cwt)
571            .sign_with(signer, coset::iana::Algorithm::EdDSA)
572            .encode()
573            .unwrap()
574            .qr_data;
575
576        let info = inspect(&qr_data).unwrap();
577
578        assert_eq!(info.issuer.as_deref(), Some("https://inspect.issuer.io"));
579        assert_eq!(info.subject.as_deref(), Some("subject-456"));
580        assert_eq!(info.key_id, Some(b"inspect-key-1".to_vec()));
581        assert_eq!(info.algorithm, Some(coset::iana::Algorithm::EdDSA));
582        assert_eq!(info.expires_at, Some(1900000000));
583        assert_eq!(info.cose_type, pipeline::CoseType::Sign1);
584    }
585
586    #[test]
587    fn test_inspect_unsigned_credential() {
588        let claim = model::Claim169::minimal("unsigned-inspect", "Unsigned User");
589        let cwt = model::CwtMeta::new()
590            .with_issuer("https://unsigned.issuer")
591            .with_expires_at(i64::MAX);
592
593        let qr_data = Encoder::new(claim, cwt)
594            .allow_unsigned()
595            .encode()
596            .unwrap()
597            .qr_data;
598
599        let info = inspect(&qr_data).unwrap();
600
601        assert_eq!(info.issuer.as_deref(), Some("https://unsigned.issuer"));
602        assert_eq!(info.key_id, None);
603        assert_eq!(info.cose_type, pipeline::CoseType::Sign1);
604    }
605
606    #[cfg(feature = "software-crypto")]
607    #[test]
608    fn test_inspect_encrypted_credential_returns_header_info() {
609        use crypto::software::Ed25519Signer;
610
611        let claim = model::Claim169::minimal("encrypted-inspect", "Encrypted User");
612        let cwt = model::CwtMeta::new()
613            .with_issuer("https://encrypted.issuer")
614            .with_expires_at(i64::MAX);
615
616        let signer = Ed25519Signer::generate();
617
618        // Generate AES-256 key (32 bytes)
619        let aes_key = [0xABu8; 32];
620
621        let qr_data = Encoder::new(claim, cwt)
622            .sign_with(signer, coset::iana::Algorithm::EdDSA)
623            .encrypt_with_aes256(&aes_key)
624            .unwrap()
625            .encode()
626            .unwrap()
627            .qr_data;
628
629        let info = inspect(&qr_data).unwrap();
630
631        // For encrypted credentials, CWT-level fields are not accessible
632        assert_eq!(info.cose_type, pipeline::CoseType::Encrypt0);
633        // Algorithm should be the encryption algorithm
634        assert!(info.algorithm.is_some());
635    }
636
637    #[test]
638    fn test_inspect_invalid_base45() {
639        let result = inspect("!!!INVALID_BASE45!!!");
640        assert!(result.is_err());
641    }
642}