Skip to main content

lib_q_core/
api.rs

1//! Unified API for lib-Q cryptographic operations
2//!
3//! This module provides a consistent, secure API that works identically
4//! whether used as a Rust crate or compiled to WASM.
5
6// PhantomData import removed - no longer needed after removing old Context<T>
7
8use crate::error::Result;
9#[cfg(feature = "alloc")]
10use crate::traits::*;
11
12#[cfg(feature = "alloc")]
13extern crate alloc;
14#[cfg(feature = "alloc")]
15use alloc::{
16    format,
17    string::String,
18    vec::Vec,
19};
20
21#[cfg(feature = "getrandom")]
22#[allow(unused_imports)] // Used in getrandom::fill() call
23use getrandom;
24pub use lib_q_types::{
25    Algorithm,
26    AlgorithmCategory,
27    SecurityLevel,
28};
29// Hash function imports
30// #[cfg(feature = "hash")]
31// use lib_q_sha3::{
32//     Digest,
33//     Sha3_224,
34//     Sha3_256,
35//     Sha3_384,
36//     Sha3_512,
37//     Shake128,
38//     Shake256,
39//     digest::ExtendableOutput,
40// };
41#[cfg(any(feature = "getrandom", feature = "rand"))]
42#[allow(unused_imports)]
43use rand_core::Rng;
44use subtle::ConstantTimeEq;
45
46// Define cryptographic operation traits for dependency injection
47// This allows implementations to be provided by higher-level crates
48
49/// Key Encapsulation Mechanism operations
50#[cfg(feature = "alloc")]
51pub trait KemOperations {
52    fn generate_keypair(
53        &self,
54        algorithm: Algorithm,
55        randomness: Option<&[u8]>,
56    ) -> Result<KemKeypair>;
57    fn encapsulate(
58        &self,
59        algorithm: Algorithm,
60        public_key: &KemPublicKey,
61        randomness: Option<&[u8]>,
62    ) -> Result<(Vec<u8>, Vec<u8>)>;
63    fn decapsulate(
64        &self,
65        algorithm: Algorithm,
66        secret_key: &KemSecretKey,
67        ciphertext: &[u8],
68    ) -> Result<Vec<u8>>;
69    fn derive_public_key(
70        &self,
71        algorithm: Algorithm,
72        secret_key: &KemSecretKey,
73    ) -> Result<KemPublicKey>;
74}
75
76/// Digital Signature operations
77#[cfg(feature = "alloc")]
78pub trait SignatureOperations {
79    fn generate_keypair(
80        &self,
81        algorithm: Algorithm,
82        randomness: Option<&[u8]>,
83    ) -> Result<SigKeypair>;
84    fn sign(
85        &self,
86        algorithm: Algorithm,
87        secret_key: &SigSecretKey,
88        message: &[u8],
89        randomness: Option<&[u8]>,
90    ) -> Result<Vec<u8>>;
91    fn verify(
92        &self,
93        algorithm: Algorithm,
94        public_key: &SigPublicKey,
95        message: &[u8],
96        signature: &[u8],
97    ) -> Result<bool>;
98
99    /// Sign under a signing context (FIPS-204 / FIPS-205 domain separation).
100    ///
101    /// The default implementation forwards an empty context to [`Self::sign`] and rejects any
102    /// non-empty context, so a provider that has not opted in cannot silently drop the context
103    /// and produce a signature that is not bound to it.
104    fn sign_with_context(
105        &self,
106        algorithm: Algorithm,
107        secret_key: &SigSecretKey,
108        message: &[u8],
109        context: &[u8],
110        randomness: Option<&[u8]>,
111    ) -> Result<Vec<u8>> {
112        if context.is_empty() {
113            return self.sign(algorithm, secret_key, message, randomness);
114        }
115        Err(crate::error::Error::NotImplemented {
116            feature: "signing context not supported by this provider".into(),
117        })
118    }
119
120    /// Verify under a signing context (FIPS-204 / FIPS-205 domain separation).
121    ///
122    /// The default implementation forwards an empty context to [`Self::verify`] and rejects any
123    /// non-empty context, so a provider that has not opted in cannot silently ignore the
124    /// context and report a context-bound signature as valid.
125    fn verify_with_context(
126        &self,
127        algorithm: Algorithm,
128        public_key: &SigPublicKey,
129        message: &[u8],
130        context: &[u8],
131        signature: &[u8],
132    ) -> Result<bool> {
133        if context.is_empty() {
134            return self.verify(algorithm, public_key, message, signature);
135        }
136        Err(crate::error::Error::NotImplemented {
137            feature: "signing context not supported by this provider".into(),
138        })
139    }
140}
141
142/// Hash operations
143#[cfg(feature = "alloc")]
144pub trait HashOperations {
145    fn hash(&self, algorithm: Algorithm, data: &[u8]) -> Result<Vec<u8>>;
146}
147
148/// AEAD operations (Layer A — `Result` only)
149///
150/// This trait mirrors [`crate::traits::Aead`] at the algorithm-dispatch boundary: `decrypt`
151/// returns [`Result`] only. Semantic decrypt ([`crate::AeadDecryptSemantic`],
152/// [`crate::DecryptSemanticOutcome`]) is **not** part of this object-safe surface; use a
153/// concrete AEAD type when Layer B is required. See `docs/adr/003-aead-decrypt-layers.md`.
154#[cfg(feature = "alloc")]
155pub trait AeadOperations {
156    fn encrypt(
157        &self,
158        algorithm: Algorithm,
159        key: &AeadKey,
160        nonce: &Nonce,
161        plaintext: &[u8],
162        associated_data: Option<&[u8]>,
163    ) -> Result<Vec<u8>>;
164    fn decrypt(
165        &self,
166        algorithm: Algorithm,
167        key: &AeadKey,
168        nonce: &Nonce,
169        ciphertext: &[u8],
170        associated_data: Option<&[u8]>,
171    ) -> Result<Vec<u8>>;
172}
173
174/// Cryptographic provider that supplies implementations
175pub trait CryptoProvider: Send + Sync {
176    #[cfg(feature = "alloc")]
177    fn kem(&self) -> Option<&dyn KemOperations>;
178    #[cfg(feature = "alloc")]
179    fn signature(&self) -> Option<&dyn SignatureOperations>;
180    #[cfg(feature = "alloc")]
181    fn hash(&self) -> Option<&dyn HashOperations>;
182    #[cfg(feature = "alloc")]
183    fn aead(&self) -> Option<&dyn AeadOperations>;
184}
185
186// Old Context<T> struct removed - use the new modular contexts instead
187// The new architecture provides better separation of concerns and security validation
188
189// KEM context is now implemented in the contexts module
190// Re-export for backward compatibility
191#[cfg(feature = "alloc")]
192pub use crate::contexts::KemContext;
193
194// Old DefaultCryptoProvider removed - use LibQCryptoProvider from providers module instead
195
196// Old Default*Impl structs and implementations removed
197// Use the new LibQCryptoProvider and its implementations from the providers module instead
198
199// Context implementations are now in the contexts module
200// These re-exports are maintained for API consistency
201
202/// The core API provides a clean interface that:
203/// - Defines cryptographic operation traits (KemOperations, SignatureOperations, etc.)
204/// - Uses dependency injection via CryptoProvider trait
205/// - Returns [`ProviderNotConfigured`](crate::error::Error::ProviderNotConfigured) when no provider is set on a context
206/// - Maintains no circular dependencies with implementation crates
207/// - Provides proper validation and error handling
208///
209/// Real implementations are provided by the main lib-q crate through LibQCryptoProvider.
210///
211/// Utility functions that work consistently across platforms
212pub struct Utils;
213
214impl Utils {
215    /// Generate cryptographically secure random bytes
216    ///
217    /// This function works in both std and no_std environments:
218    /// - In std environments with the "rand" feature: Uses rand::rng()
219    /// - In no_std environments with the "getrandom" feature: Uses getrandom directly
220    /// - In no_std environments without getrandom: Returns an error
221    #[cfg(feature = "rand")]
222    pub fn random_bytes(length: usize) -> Result<Vec<u8>> {
223        const MIN_RANDOM_SIZE: usize = 1;
224        const MAX_RANDOM_SIZE: usize = 1024 * 1024; // 1MB limit
225        if !(MIN_RANDOM_SIZE..=MAX_RANDOM_SIZE).contains(&length) {
226            return Err(crate::error::Error::RandomBytesLengthInvalid {
227                min: MIN_RANDOM_SIZE,
228                max: MAX_RANDOM_SIZE,
229                requested: length,
230            });
231        }
232
233        let mut bytes = alloc::vec![0u8; length];
234
235        // Use rand for cryptographically secure random generation
236        let mut rng = rand::rng();
237        rng.fill_bytes(&mut bytes);
238
239        // Zeroize the bytes on error paths (handled by Vec's Drop implementation)
240        Ok(bytes)
241    }
242
243    #[cfg(all(feature = "getrandom", not(feature = "rand")))]
244    #[cfg(feature = "alloc")]
245    pub fn random_bytes(length: usize) -> Result<Vec<u8>> {
246        const MIN_RANDOM_SIZE: usize = 1;
247        const MAX_RANDOM_SIZE: usize = 1024 * 1024; // 1MB limit
248        if !(MIN_RANDOM_SIZE..=MAX_RANDOM_SIZE).contains(&length) {
249            return Err(crate::error::Error::RandomBytesLengthInvalid {
250                min: MIN_RANDOM_SIZE,
251                max: MAX_RANDOM_SIZE,
252                requested: length,
253            });
254        }
255
256        let mut bytes = alloc::vec![0u8; length];
257
258        // Generate cryptographically secure random bytes using getrandom
259        // This works across all platforms including WASM (using crypto.getRandomValues())
260        // The getrandom crate automatically selects the appropriate entropy source:
261        // - Native: OS entropy sources (e.g., /dev/urandom, CryptGenRandom)
262        // - WASM: crypto.getRandomValues() in browsers, WebCrypto API in Node.js
263        getrandom::fill(&mut bytes).map_err(|_| crate::error::Error::RandomGenerationFailed {
264            operation: String::from("random_bytes"),
265        })?;
266
267        // Zeroize the bytes on error paths (handled by Vec's Drop implementation)
268        Ok(bytes)
269    }
270
271    #[cfg(all(feature = "getrandom", not(feature = "rand")))]
272    #[cfg(not(feature = "alloc"))]
273    pub fn random_bytes(length: usize) -> Result<&'static [u8]> {
274        const MIN_RANDOM_SIZE: usize = 1;
275        const MAX_RANDOM_SIZE: usize = 1024; // Limit for no_std without alloc
276        if !(MIN_RANDOM_SIZE..=MAX_RANDOM_SIZE).contains(&length) {
277            return Err(crate::error::Error::RandomBytesLengthInvalid {
278                min: MIN_RANDOM_SIZE,
279                max: MAX_RANDOM_SIZE,
280                requested: length,
281            });
282        }
283
284        // For no_std without alloc, we need to handle platform-specific RNG
285        // This provides a graceful fallback for platforms where getrandom is not available.
286        // WASM builds should enable the root crate's `wasm` or `wasm_js` feature so that
287        // lib-q-core/wasm_getrandom is enabled and getrandom works (this path is then avoided).
288        #[cfg(target_arch = "wasm32")]
289        {
290            // For WASM targets, getrandom might not be available
291            return Err(crate::error::Error::RandomGenerationFailed {
292                operation: "random_bytes",
293            });
294        }
295
296        #[cfg(not(target_arch = "wasm32"))]
297        {
298            // For native targets, getrandom might not be available in this configuration
299            // Note: This is a simplified approach - in production, you'd want proper platform detection
300            return Err(crate::error::Error::RandomGenerationFailed {
301                operation: "random_bytes",
302            });
303        }
304    }
305
306    #[cfg(not(any(feature = "rand", feature = "getrandom")))]
307    #[cfg(feature = "alloc")]
308    pub fn random_bytes(_length: usize) -> Result<Vec<u8>> {
309        Err(crate::error::Error::RandomGenerationFailed {
310            operation: String::from("random_bytes"),
311        })
312    }
313
314    #[cfg(not(any(feature = "rand", feature = "getrandom")))]
315    #[cfg(not(feature = "alloc"))]
316    pub fn random_bytes(_length: usize) -> Result<&'static [u8]> {
317        Err(crate::error::Error::RandomGenerationFailed {
318            operation: "random_bytes",
319        })
320    }
321
322    /// Convert bytes to hex string
323    #[cfg(feature = "alloc")]
324    pub fn bytes_to_hex(bytes: &[u8]) -> String {
325        let mut hex = String::new();
326        for &byte in bytes {
327            hex.push_str(&format!("{:02x}", byte));
328        }
329        hex
330    }
331
332    #[cfg(not(feature = "alloc"))]
333    pub fn bytes_to_hex(_bytes: &[u8]) -> &'static str {
334        "hex conversion not available in no_std without alloc"
335    }
336
337    /// Convert hex string to bytes
338    ///
339    /// # Errors
340    ///
341    /// Returns [`crate::error::Error::HexDecode`] with a [`crate::error::HexDecodeError`] reason when the
342    /// trimmed input is not valid hexadecimal (odd length or non-hex digit).
343    #[cfg(feature = "alloc")]
344    pub fn hex_to_bytes(hex: &str) -> Result<Vec<u8>> {
345        use crate::error::HexDecodeError;
346
347        let hex = hex.trim();
348
349        if !hex.len().is_multiple_of(2) {
350            return Err(crate::error::Error::HexDecode(HexDecodeError::OddLength {
351                char_count: hex.len(),
352            }));
353        }
354
355        let mut bytes = Vec::with_capacity(hex.len() / 2);
356        for i in (0..hex.len()).step_by(2) {
357            let byte = u8::from_str_radix(&hex[i..i + 2], 16).map_err(|_| {
358                crate::error::Error::HexDecode(HexDecodeError::InvalidDigit {
359                    pair_start: i,
360                    char_count: hex.len(),
361                })
362            })?;
363            bytes.push(byte);
364        }
365
366        Ok(bytes)
367    }
368
369    #[cfg(not(feature = "alloc"))]
370    pub fn hex_to_bytes(_hex: &str) -> Result<&'static [u8]> {
371        Err(crate::error::Error::MemoryAllocationFailed {
372            operation: "hex_to_bytes",
373        })
374    }
375
376    /// Constant-time comparison of two byte slices
377    pub fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
378        if a.len() != b.len() {
379            return false;
380        }
381        a.ct_eq(b).into()
382    }
383}
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388    #[cfg(feature = "alloc")]
389    use crate::contexts::{
390        HashContext,
391        SignatureContext,
392    };
393
394    #[test]
395    fn test_provider_architecture() {
396        #[cfg(feature = "std")]
397        {
398            // Test that default provider is properly configured
399            let mut ctx = KemContext::with_default_provider();
400
401            // Stub core provider: NotImplemented if configured, or ProviderNotConfigured if init failed
402            let result = ctx.generate_keypair(Algorithm::MlKem512, None);
403            assert!(result.is_err());
404
405            match result {
406                Err(crate::error::Error::NotImplemented { feature }) => {
407                    assert!(
408                        feature.contains(
409                            "ML-KEM implementations are provided by the main lib-q crate"
410                        )
411                    );
412                }
413                Err(crate::error::Error::ProviderNotConfigured { operation }) => {
414                    assert_eq!(operation, "KEM");
415                }
416                _ => panic!("Expected NotImplemented or ProviderNotConfigured"),
417            }
418        }
419
420        // Test that context without provider returns clear error
421        #[cfg(feature = "alloc")]
422        {
423            let mut ctx = KemContext::new();
424            let result = ctx.generate_keypair(Algorithm::MlKem512, None);
425            assert!(result.is_err());
426
427            if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
428                assert_eq!(operation, "KEM");
429            } else {
430                panic!("Expected ProviderNotConfigured error, got different error type");
431            }
432        }
433    }
434
435    #[test]
436    fn test_algorithm_security_levels() {
437        assert_eq!(Algorithm::MlKem512.security_level(), 1);
438        assert_eq!(Algorithm::MlKem768.security_level(), 3);
439        assert_eq!(Algorithm::MlKem1024.security_level(), 4);
440        assert_eq!(Algorithm::MlDsa44.security_level(), 1);
441        assert_eq!(Algorithm::MlDsa65.security_level(), 3);
442        assert_eq!(Algorithm::MlDsa87.security_level(), 4);
443    }
444
445    #[test]
446    fn test_algorithm_categories() {
447        assert_eq!(Algorithm::MlKem512.category(), AlgorithmCategory::Kem);
448        assert_eq!(Algorithm::MlDsa44.category(), AlgorithmCategory::Signature);
449        assert_eq!(Algorithm::Shake256.category(), AlgorithmCategory::Hash);
450    }
451
452    #[test]
453    #[cfg(feature = "alloc")]
454    fn test_kem_context() {
455        let mut ctx = KemContext::new();
456        let result = ctx.generate_keypair(Algorithm::MlKem512, None);
457        assert!(result.is_err());
458        if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
459            assert_eq!(operation, "KEM");
460        } else {
461            panic!("Expected ProviderNotConfigured error");
462        }
463    }
464
465    #[test]
466    #[cfg(feature = "alloc")]
467    fn test_signature_context() {
468        let mut ctx = SignatureContext::new();
469        let result = ctx.generate_keypair(Algorithm::MlDsa65, None);
470        assert!(result.is_err());
471        if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
472            assert_eq!(operation, "signature");
473        } else {
474            panic!("Expected ProviderNotConfigured error");
475        }
476    }
477
478    #[test]
479    #[cfg(feature = "alloc")]
480    fn test_hash_context() {
481        let mut ctx = HashContext::new();
482        let result = ctx.hash(Algorithm::Shake256, b"test");
483        assert!(result.is_err());
484        if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
485            assert_eq!(operation, "hash");
486        } else {
487            panic!("Expected ProviderNotConfigured error");
488        }
489    }
490
491    #[test]
492    fn test_utils() {
493        #[cfg(feature = "getrandom")]
494        {
495            let bytes = Utils::random_bytes(32).unwrap();
496            assert_eq!(bytes.len(), 32);
497        }
498
499        #[cfg(feature = "alloc")]
500        {
501            let hex = Utils::bytes_to_hex(&[0x01, 0x23, 0x45, 0x67]);
502            assert_eq!(hex, "01234567");
503
504            let decoded = Utils::hex_to_bytes(&hex).unwrap();
505            assert_eq!(decoded, alloc::vec![0x01, 0x23, 0x45, 0x67]);
506        }
507    }
508
509    #[test]
510    fn test_random_bytes_generation() {
511        // Test that random_bytes generates different values when available
512        match Utils::random_bytes(32) {
513            Ok(bytes1) => {
514                let bytes2 = Utils::random_bytes(32).expect("Should generate random bytes");
515                assert_eq!(bytes1.len(), 32);
516                assert_eq!(bytes2.len(), 32);
517
518                // Verify that we get different bytes on subsequent calls
519                // (This test has a very small probability of failure, but it's acceptable for testing)
520                assert_ne!(
521                    bytes1, bytes2,
522                    "Random bytes should be different on subsequent calls"
523                );
524
525                // Test that all bytes are not zero (very unlikely with proper RNG)
526                let all_zero1 = bytes1.iter().all(|&b| b == 0);
527                let all_zero2 = bytes2.iter().all(|&b| b == 0);
528
529                assert!(!all_zero1, "Random bytes should not all be zero");
530                assert!(!all_zero2, "Random bytes should not all be zero");
531            }
532            Err(crate::error::Error::RandomGenerationFailed { .. }) => {
533                // This is expected in no_std mode without getrandom feature
534                // The test passes by not panicking
535            }
536            Err(e) => {
537                panic!("Unexpected error: {:?}", e);
538            }
539        }
540    }
541
542    #[test]
543    fn test_constant_time_compare() {
544        assert!(Utils::constant_time_compare(b"hello", b"hello"));
545        assert!(!Utils::constant_time_compare(b"hello", b"world"));
546        assert!(!Utils::constant_time_compare(b"hello", b"hell"));
547    }
548
549    #[cfg(feature = "getrandom")]
550    #[test]
551    fn test_random_bytes_entropy_quality() {
552        // Test entropy quality by checking byte distribution
553        const NUM_SAMPLES: usize = 1000;
554        const BYTE_LENGTH: usize = 32;
555
556        let mut byte_counts = [0u32; 256];
557        let mut total_bytes = 0u32;
558
559        for _ in 0..NUM_SAMPLES {
560            let bytes = Utils::random_bytes(BYTE_LENGTH).expect("Should generate random bytes");
561            for &byte in &bytes {
562                byte_counts[byte as usize] += 1;
563                total_bytes += 1;
564            }
565        }
566
567        // Check that no byte value is completely absent (extremely unlikely with good RNG)
568        let zero_count = byte_counts.iter().filter(|&&count| count == 0).count();
569        assert!(
570            zero_count < 50,
571            "Too many byte values are missing from random generation"
572        );
573
574        // Chi-square goodness-of-fit test for uniform byte distribution (χ² with ν=255).
575        // Wilson-Hilferty approximation converts χ² to z; reject if z > 5 (false positive ~2.9e-7).
576        let expected_per_byte = total_bytes as f64 / 256.0;
577        let chi_sq: f64 = byte_counts
578            .iter()
579            .map(|&count| {
580                let d = count as f64 - expected_per_byte;
581                d * d / expected_per_byte
582            })
583            .sum();
584        const NU: f64 = 255.0;
585        let z =
586            ((chi_sq / NU).powf(1.0 / 3.0) - (1.0 - 2.0 / (9.0 * NU))) / (2.0 / (9.0 * NU)).sqrt();
587        assert!(
588            z <= 5.0,
589            "Random bytes show poor entropy distribution (chi-square z = {})",
590            z
591        );
592    }
593
594    #[cfg(feature = "getrandom")]
595    #[test]
596    fn test_random_bytes_uniformity() {
597        // Test that random bytes are uniformly distributed
598        const NUM_SAMPLES: usize = 10000;
599        const BYTE_LENGTH: usize = 16;
600
601        let mut all_bytes = alloc::vec![0u8; NUM_SAMPLES * BYTE_LENGTH];
602        let mut offset = 0;
603
604        for _ in 0..NUM_SAMPLES {
605            let bytes = Utils::random_bytes(BYTE_LENGTH).expect("Should generate random bytes");
606            all_bytes[offset..offset + BYTE_LENGTH].copy_from_slice(&bytes);
607            offset += BYTE_LENGTH;
608        }
609
610        // Test for patterns that would indicate poor randomness
611        // Check for runs of identical bytes (should be rare)
612        let mut max_run_length = 0;
613        let mut current_run_length = 1;
614
615        for i in 1..all_bytes.len() {
616            if all_bytes[i] == all_bytes[i - 1] {
617                current_run_length += 1;
618                max_run_length = max_run_length.max(current_run_length);
619            } else {
620                current_run_length = 1;
621            }
622        }
623
624        // Runs longer than 4 identical bytes are suspicious
625        assert!(
626            max_run_length <= 4,
627            "Random bytes show suspicious patterns (run length: {})",
628            max_run_length
629        );
630    }
631
632    #[cfg(any(feature = "rand", all(feature = "getrandom", feature = "alloc")))]
633    #[test]
634    fn test_random_bytes_size_limits() {
635        const MAX_SIZE: usize = 1024 * 1024; // 1MB
636        assert_eq!(
637            Utils::random_bytes(0),
638            Err(crate::error::Error::RandomBytesLengthInvalid {
639                min: 1,
640                max: MAX_SIZE,
641                requested: 0,
642            }),
643            "zero length"
644        );
645
646        assert!(
647            Utils::random_bytes(MAX_SIZE).is_ok(),
648            "Should accept maximum size"
649        );
650        assert_eq!(
651            Utils::random_bytes(MAX_SIZE + 1),
652            Err(crate::error::Error::RandomBytesLengthInvalid {
653                min: 1,
654                max: MAX_SIZE,
655                requested: MAX_SIZE + 1,
656            }),
657            "oversized request"
658        );
659
660        // Test reasonable sizes
661        for size in [1, 16, 32, 64, 128, 256, 512, 1024] {
662            let bytes = Utils::random_bytes(size).expect("Should generate random bytes");
663            assert_eq!(bytes.len(), size, "Should generate exactly {} bytes", size);
664        }
665    }
666
667    #[test]
668    #[cfg(feature = "alloc")]
669    fn test_hex_to_bytes_decode_errors() {
670        use crate::error::{
671            Error,
672            HexDecodeError,
673        };
674
675        assert_eq!(
676            Utils::hex_to_bytes("123").unwrap_err(),
677            Error::HexDecode(HexDecodeError::OddLength { char_count: 3 })
678        );
679        assert_eq!(
680            Utils::hex_to_bytes("12g3").unwrap_err(),
681            Error::HexDecode(HexDecodeError::InvalidDigit {
682                pair_start: 2,
683                char_count: 4,
684            })
685        );
686    }
687}