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}