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        // NIST PQC security categories (FIPS 203 for ML-KEM, FIPS 204 for ML-DSA):
438        // ML-KEM-1024 and ML-DSA-87 are Category 5, not Category 4. This test was
439        // previously pinned at 4, which encoded a defect (card t_e3457ac8).
440        //
441        // The note that used to sit here said `Algorithm::security_level()` in
442        // lib-q-types was an unfixed second copy still returning 4. That was true
443        // when written and was resolved at b4119d9, which corrected seven algorithms
444        // across all five duplicated tables. `security_level()` is the lib-q-types
445        // implementation, so these assertions exercise that copy, not the registry.
446        assert_eq!(Algorithm::MlKem512.security_level(), 1);
447        assert_eq!(Algorithm::MlKem768.security_level(), 3);
448        assert_eq!(Algorithm::MlKem1024.security_level(), 5);
449        assert_eq!(Algorithm::MlDsa44.security_level(), 1);
450        assert_eq!(Algorithm::MlDsa65.security_level(), 3);
451        assert_eq!(Algorithm::MlDsa87.security_level(), 5);
452    }
453
454    #[test]
455    fn test_algorithm_categories() {
456        assert_eq!(Algorithm::MlKem512.category(), AlgorithmCategory::Kem);
457        assert_eq!(Algorithm::MlDsa44.category(), AlgorithmCategory::Signature);
458        assert_eq!(Algorithm::Shake256.category(), AlgorithmCategory::Hash);
459    }
460
461    #[test]
462    #[cfg(feature = "alloc")]
463    fn test_kem_context() {
464        let mut ctx = KemContext::new();
465        let result = ctx.generate_keypair(Algorithm::MlKem512, None);
466        assert!(result.is_err());
467        if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
468            assert_eq!(operation, "KEM");
469        } else {
470            panic!("Expected ProviderNotConfigured error");
471        }
472    }
473
474    #[test]
475    #[cfg(feature = "alloc")]
476    fn test_signature_context() {
477        let mut ctx = SignatureContext::new();
478        let result = ctx.generate_keypair(Algorithm::MlDsa65, None);
479        assert!(result.is_err());
480        if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
481            assert_eq!(operation, "signature");
482        } else {
483            panic!("Expected ProviderNotConfigured error");
484        }
485    }
486
487    #[test]
488    #[cfg(feature = "alloc")]
489    fn test_hash_context() {
490        let mut ctx = HashContext::new();
491        let result = ctx.hash(Algorithm::Shake256, b"test");
492        assert!(result.is_err());
493        if let Err(crate::error::Error::ProviderNotConfigured { operation }) = result {
494            assert_eq!(operation, "hash");
495        } else {
496            panic!("Expected ProviderNotConfigured error");
497        }
498    }
499
500    #[test]
501    fn test_utils() {
502        #[cfg(feature = "getrandom")]
503        {
504            let bytes = Utils::random_bytes(32).unwrap();
505            assert_eq!(bytes.len(), 32);
506        }
507
508        #[cfg(feature = "alloc")]
509        {
510            let hex = Utils::bytes_to_hex(&[0x01, 0x23, 0x45, 0x67]);
511            assert_eq!(hex, "01234567");
512
513            let decoded = Utils::hex_to_bytes(&hex).unwrap();
514            assert_eq!(decoded, alloc::vec![0x01, 0x23, 0x45, 0x67]);
515        }
516    }
517
518    #[test]
519    fn test_random_bytes_generation() {
520        // Test that random_bytes generates different values when available
521        match Utils::random_bytes(32) {
522            Ok(bytes1) => {
523                let bytes2 = Utils::random_bytes(32).expect("Should generate random bytes");
524                assert_eq!(bytes1.len(), 32);
525                assert_eq!(bytes2.len(), 32);
526
527                // Verify that we get different bytes on subsequent calls
528                // (This test has a very small probability of failure, but it's acceptable for testing)
529                assert_ne!(
530                    bytes1, bytes2,
531                    "Random bytes should be different on subsequent calls"
532                );
533
534                // Test that all bytes are not zero (very unlikely with proper RNG)
535                let all_zero1 = bytes1.iter().all(|&b| b == 0);
536                let all_zero2 = bytes2.iter().all(|&b| b == 0);
537
538                assert!(!all_zero1, "Random bytes should not all be zero");
539                assert!(!all_zero2, "Random bytes should not all be zero");
540            }
541            Err(crate::error::Error::RandomGenerationFailed { .. }) => {
542                // This is expected in no_std mode without getrandom feature
543                // The test passes by not panicking
544            }
545            Err(e) => {
546                panic!("Unexpected error: {:?}", e);
547            }
548        }
549    }
550
551    #[test]
552    fn test_constant_time_compare() {
553        assert!(Utils::constant_time_compare(b"hello", b"hello"));
554        assert!(!Utils::constant_time_compare(b"hello", b"world"));
555        assert!(!Utils::constant_time_compare(b"hello", b"hell"));
556    }
557
558    #[cfg(feature = "getrandom")]
559    #[test]
560    fn test_random_bytes_entropy_quality() {
561        // Test entropy quality by checking byte distribution
562        const NUM_SAMPLES: usize = 1000;
563        const BYTE_LENGTH: usize = 32;
564
565        let mut byte_counts = [0u32; 256];
566        let mut total_bytes = 0u32;
567
568        for _ in 0..NUM_SAMPLES {
569            let bytes = Utils::random_bytes(BYTE_LENGTH).expect("Should generate random bytes");
570            for &byte in &bytes {
571                byte_counts[byte as usize] += 1;
572                total_bytes += 1;
573            }
574        }
575
576        // Check that no byte value is completely absent (extremely unlikely with good RNG)
577        let zero_count = byte_counts.iter().filter(|&&count| count == 0).count();
578        assert!(
579            zero_count < 50,
580            "Too many byte values are missing from random generation"
581        );
582
583        // Chi-square goodness-of-fit test for uniform byte distribution (χ² with ν=255).
584        // Wilson-Hilferty approximation converts χ² to z; reject if z > 5 (false positive ~2.9e-7).
585        let expected_per_byte = total_bytes as f64 / 256.0;
586        let chi_sq: f64 = byte_counts
587            .iter()
588            .map(|&count| {
589                let d = count as f64 - expected_per_byte;
590                d * d / expected_per_byte
591            })
592            .sum();
593        const NU: f64 = 255.0;
594        let z =
595            ((chi_sq / NU).powf(1.0 / 3.0) - (1.0 - 2.0 / (9.0 * NU))) / (2.0 / (9.0 * NU)).sqrt();
596        assert!(
597            z <= 5.0,
598            "Random bytes show poor entropy distribution (chi-square z = {})",
599            z
600        );
601    }
602
603    #[cfg(feature = "getrandom")]
604    #[test]
605    fn test_random_bytes_uniformity() {
606        // Test that random bytes are uniformly distributed
607        const NUM_SAMPLES: usize = 10000;
608        const BYTE_LENGTH: usize = 16;
609
610        let mut all_bytes = alloc::vec![0u8; NUM_SAMPLES * BYTE_LENGTH];
611        let mut offset = 0;
612
613        for _ in 0..NUM_SAMPLES {
614            let bytes = Utils::random_bytes(BYTE_LENGTH).expect("Should generate random bytes");
615            all_bytes[offset..offset + BYTE_LENGTH].copy_from_slice(&bytes);
616            offset += BYTE_LENGTH;
617        }
618
619        // Test for patterns that would indicate poor randomness
620        // Check for runs of identical bytes (should be rare)
621        let mut max_run_length = 0;
622        let mut current_run_length = 1;
623
624        for i in 1..all_bytes.len() {
625            if all_bytes[i] == all_bytes[i - 1] {
626                current_run_length += 1;
627                max_run_length = max_run_length.max(current_run_length);
628            } else {
629                current_run_length = 1;
630            }
631        }
632
633        // Runs longer than 4 identical bytes are suspicious
634        assert!(
635            max_run_length <= 4,
636            "Random bytes show suspicious patterns (run length: {})",
637            max_run_length
638        );
639    }
640
641    #[cfg(any(feature = "rand", all(feature = "getrandom", feature = "alloc")))]
642    #[test]
643    fn test_random_bytes_size_limits() {
644        const MAX_SIZE: usize = 1024 * 1024; // 1MB
645        assert_eq!(
646            Utils::random_bytes(0),
647            Err(crate::error::Error::RandomBytesLengthInvalid {
648                min: 1,
649                max: MAX_SIZE,
650                requested: 0,
651            }),
652            "zero length"
653        );
654
655        assert!(
656            Utils::random_bytes(MAX_SIZE).is_ok(),
657            "Should accept maximum size"
658        );
659        assert_eq!(
660            Utils::random_bytes(MAX_SIZE + 1),
661            Err(crate::error::Error::RandomBytesLengthInvalid {
662                min: 1,
663                max: MAX_SIZE,
664                requested: MAX_SIZE + 1,
665            }),
666            "oversized request"
667        );
668
669        // Test reasonable sizes
670        for size in [1, 16, 32, 64, 128, 256, 512, 1024] {
671            let bytes = Utils::random_bytes(size).expect("Should generate random bytes");
672            assert_eq!(bytes.len(), size, "Should generate exactly {} bytes", size);
673        }
674    }
675
676    #[test]
677    #[cfg(feature = "alloc")]
678    fn test_hex_to_bytes_decode_errors() {
679        use crate::error::{
680            Error,
681            HexDecodeError,
682        };
683
684        assert_eq!(
685            Utils::hex_to_bytes("123").unwrap_err(),
686            Error::HexDecode(HexDecodeError::OddLength { char_count: 3 })
687        );
688        assert_eq!(
689            Utils::hex_to_bytes("12g3").unwrap_err(),
690            Error::HexDecode(HexDecodeError::InvalidDigit {
691                pair_start: 2,
692                char_count: 4,
693            })
694        );
695    }
696}