Skip to main content

lib_q_core/security/
entropy.rs

1//! Entropy validation utilities
2//!
3//! This module provides utilities to validate the entropy quality of cryptographic
4//! inputs such as keys and randomness.
5
6#[cfg(feature = "alloc")]
7use alloc::string::ToString;
8
9use crate::error::Result;
10
11/// Entropy validator for cryptographic inputs
12///
13/// This validator provides utilities to validate the entropy quality of
14/// cryptographic inputs to ensure they meet security requirements.
15#[cfg(feature = "alloc")]
16#[derive(Clone)]
17pub struct EntropyValidator {
18    min_entropy_bits: usize,
19    enable_entropy_validation: bool,
20}
21
22#[cfg(feature = "alloc")]
23impl EntropyValidator {
24    /// Create a new entropy validator
25    ///
26    /// # Returns
27    ///
28    /// A new instance of EntropyValidator with default entropy requirements.
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if the validator fails to initialize.
33    pub fn new() -> Result<Self> {
34        Ok(Self {
35            min_entropy_bits: 128, // Minimum 128 bits of entropy
36            enable_entropy_validation: true,
37        })
38    }
39
40    /// Validate key entropy
41    ///
42    /// This function validates that a key has sufficient entropy to be
43    /// cryptographically secure.
44    ///
45    /// # Arguments
46    ///
47    /// * `key_data` - The key data to validate
48    ///
49    /// # Returns
50    ///
51    /// Returns `Ok(())` if the key has sufficient entropy, or an error if it doesn't.
52    pub fn validate_key_entropy(&self, key_data: &[u8]) -> Result<()> {
53        if !self.enable_entropy_validation {
54            return Ok(());
55        }
56
57        // Allow relaxed validation in testing environments
58        #[cfg(feature = "relaxed_entropy_validation")]
59        {
60            self.validate_key_entropy_relaxed(key_data)
61        }
62
63        // Strict validation for production
64        #[cfg(not(feature = "relaxed_entropy_validation"))]
65        {
66            self.validate_key_entropy_strict(key_data)
67        }
68    }
69
70    /// Strict entropy validation for production environments.
71    ///
72    /// Pattern detection thresholds scale with key size so that legitimate
73    /// NIST post-quantum keys (ML-DSA, SLH-DSA, FN-DSA) — which are structured
74    /// algebraic objects, not uniformly random bytes — are never rejected by
75    /// naive byte-level heuristics.
76    #[cfg(not(feature = "relaxed_entropy_validation"))]
77    fn validate_key_entropy_strict(&self, key_data: &[u8]) -> Result<()> {
78        let min_key_length = self.min_entropy_bits / 8;
79        if key_data.len() < min_key_length {
80            return Err(crate::error::Error::InvalidKeySize {
81                expected: min_key_length,
82                actual: key_data.len(),
83            });
84        }
85
86        if self.has_repeated_pattern(key_data) {
87            return Err(crate::error::Error::InvalidKey {
88                key_type: "key".to_string(),
89                reason: "Key contains repeated patterns indicating low entropy".to_string(),
90            });
91        }
92
93        if self.has_sequential_pattern(key_data) {
94            return Err(crate::error::Error::InvalidKey {
95                key_type: "key".to_string(),
96                reason: "Key contains sequential patterns indicating low entropy".to_string(),
97            });
98        }
99
100        if !self.has_sufficient_entropy(key_data) {
101            return Err(crate::error::Error::InvalidKey {
102                key_type: "key".to_string(),
103                reason: "Key does not have sufficient entropy".to_string(),
104            });
105        }
106
107        Ok(())
108    }
109
110    /// Relaxed entropy validation for testing environments
111    ///
112    /// This method implements relaxed entropy validation suitable for testing
113    /// scenarios with deterministic randomness. It only performs basic checks
114    /// to prevent obviously invalid keys while allowing deterministic patterns.
115    #[cfg(feature = "relaxed_entropy_validation")]
116    fn validate_key_entropy_relaxed(&self, key_data: &[u8]) -> Result<()> {
117        // Check minimum key length (relaxed requirement)
118        let min_key_length = 16; // Reduced from 128 bits to 16 bytes for testing
119        if key_data.len() < min_key_length {
120            return Err(crate::error::Error::InvalidKeySize {
121                expected: min_key_length,
122                actual: key_data.len(),
123            });
124        }
125
126        // Only check for obviously invalid patterns (all zeros, all ones)
127        if key_data.iter().all(|&b| b == 0) {
128            return Err(crate::error::Error::InvalidKey {
129                key_type: "key".to_string(),
130                reason: "Key cannot be all zeros".to_string(),
131            });
132        }
133
134        if key_data.iter().all(|&b| b == 0xFF) {
135            return Err(crate::error::Error::InvalidKey {
136                key_type: "key".to_string(),
137                reason: "Key cannot be all ones".to_string(),
138            });
139        }
140
141        // Skip pattern detection and entropy checks for testing
142        Ok(())
143    }
144
145    /// Check if data is dominated by repeated 4-byte patterns.
146    ///
147    /// Isolated 4-byte repeats are statistically expected in keys longer than a
148    /// few hundred bytes (birthday paradox on 2^32 patterns).  The threshold
149    /// therefore scales: for keys ≤256 bytes any single repeat is suspicious; for
150    /// larger keys we require that >12.5 % of 4-byte windows match some earlier
151    /// window — which only occurs for truly degenerate inputs.
152    #[cfg(not(feature = "relaxed_entropy_validation"))]
153    fn has_repeated_pattern(&self, data: &[u8]) -> bool {
154        if data.len() < 8 {
155            return false;
156        }
157
158        let windows = data.len() - 3;
159        let threshold = if data.len() <= 256 {
160            2usize
161        } else {
162            windows / 8
163        };
164
165        let mut hits = 0usize;
166        for i in 0..windows {
167            let pattern = &data[i..i + 4];
168            for j in i + 4..windows {
169                if &data[j..j + 4] == pattern {
170                    hits += 1;
171                    if hits >= threshold {
172                        return true;
173                    }
174                }
175            }
176        }
177
178        false
179    }
180
181    /// Check if data is dominated by sequential (ascending / descending) bytes.
182    ///
183    /// A single 4-byte ascending run (e.g. `[0xA0, 0xA1, 0xA2, 0xA3]`) is
184    /// expected with ≈6 % probability in an ML-DSA-65 secret key (4032 bytes).
185    /// Rejecting on a single hit therefore produces unacceptable false-positive
186    /// rates for legitimate PQ keys.
187    ///
188    /// Threshold: for keys ≤64 bytes, a single sequential run is flagged; for
189    /// larger keys, the number of sequential windows must exceed 5 % of total
190    /// windows, which only fires for data that is genuinely sequential.
191    #[cfg(not(feature = "relaxed_entropy_validation"))]
192    fn has_sequential_pattern(&self, data: &[u8]) -> bool {
193        if data.len() < 4 {
194            return false;
195        }
196
197        let windows = data.len() - 3;
198        let threshold = if data.len() <= 64 {
199            1usize
200        } else {
201            // 5 % of windows, minimum 4
202            (windows / 20).max(4)
203        };
204
205        let mut hits = 0usize;
206
207        for i in 0..windows {
208            let ascending = data[i].wrapping_add(1) == data[i + 1] &&
209                data[i + 1].wrapping_add(1) == data[i + 2] &&
210                data[i + 2].wrapping_add(1) == data[i + 3];
211            let descending = data[i] == data[i + 1].wrapping_add(1) &&
212                data[i + 1] == data[i + 2].wrapping_add(1) &&
213                data[i + 2] == data[i + 3].wrapping_add(1);
214            if ascending || descending {
215                hits += 1;
216                if hits >= threshold {
217                    return true;
218                }
219            }
220        }
221
222        false
223    }
224
225    /// Check if data has sufficient byte-value diversity.
226    ///
227    /// Counts distinct byte values and compares against a size-aware threshold.
228    /// Small keys (SLH-DSA 32–128 B) need ≥40 % unique values; medium and large
229    /// keys use a capped diversity floor so structured PQ material (ML-DSA,
230    /// FN-DSA, etc.) is not rejected for failing to hit all 256 byte values.
231    #[cfg(not(feature = "relaxed_entropy_validation"))]
232    fn has_sufficient_entropy(&self, data: &[u8]) -> bool {
233        if data.len() < 16 {
234            return false;
235        }
236
237        let mut byte_counts = [0u32; 256];
238        for &byte in data {
239            byte_counts[byte as usize] += 1;
240        }
241
242        let unique_bytes = byte_counts.iter().filter(|&&c| c > 0).count();
243
244        if data.len() > 10240 {
245            unique_bytes >= 64
246        } else if data.len() <= 128 {
247            // Small PQ keys (e.g. SLH-DSA 32–128 B): ≥40 % unique values,
248            // minimum 4 unique bytes.
249            unique_bytes >= (data.len() * 2 / 5).max(4)
250        } else {
251            // Medium keys: aim for ~10 % distinct values, but cap at 64. Uncapped
252            // `min(len/10, 256)` becomes 256 for keys ≥2560 B, i.e. it requires
253            // every byte value 0..=255 — a false positive for legitimate ML-DSA
254            // and similar encodings.
255            let required = (data.len() / 10).clamp(1, 64);
256            unique_bytes >= required
257        }
258    }
259
260    /// Set minimum entropy requirements
261    ///
262    /// # Arguments
263    ///
264    /// * `min_entropy_bits` - Minimum entropy in bits
265    pub fn set_min_entropy_bits(&mut self, min_entropy_bits: usize) {
266        self.min_entropy_bits = min_entropy_bits;
267    }
268
269    /// Get minimum entropy requirements
270    ///
271    /// # Returns
272    ///
273    /// Returns the minimum entropy requirement in bits.
274    pub fn min_entropy_bits(&self) -> usize {
275        self.min_entropy_bits
276    }
277
278    /// Enable or disable entropy validation
279    ///
280    /// # Arguments
281    ///
282    /// * `enabled` - Whether to enable entropy validation
283    pub fn set_entropy_validation(&mut self, enabled: bool) {
284        self.enable_entropy_validation = enabled;
285    }
286
287    /// Check if entropy validation is enabled
288    ///
289    /// # Returns
290    ///
291    /// Returns `true` if entropy validation is enabled, `false` otherwise.
292    pub fn is_entropy_validation_enabled(&self) -> bool {
293        self.enable_entropy_validation
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_entropy_validator_creation() {
303        let validator = EntropyValidator::new();
304        assert!(
305            validator.is_ok(),
306            "EntropyValidator should be created successfully"
307        );
308    }
309
310    #[test]
311    fn test_validate_key_entropy_valid() {
312        let validator = EntropyValidator::new().unwrap();
313
314        // 32-byte key with no repeated blocks and no sequential runs.
315        let high_entropy_key = vec![
316            0xA3, 0x17, 0x5B, 0xE2, 0x94, 0x0D, 0x68, 0xF1, 0x3C, 0x86, 0xD5, 0x4A, 0x72, 0xBE,
317            0x09, 0xC7, 0x58, 0xE4, 0x1F, 0x8B, 0xA0, 0x63, 0xD9, 0x2E, 0x7D, 0x45, 0xFB, 0x16,
318            0xCA, 0x30, 0x9E, 0x54,
319        ];
320        let result = validator.validate_key_entropy(&high_entropy_key);
321        assert!(result.is_ok(), "Should accept high-entropy key");
322    }
323
324    #[test]
325    fn test_validate_key_entropy_too_short() {
326        let validator = EntropyValidator::new().unwrap();
327
328        let short_key = vec![1, 2, 3, 4];
329        let result = validator.validate_key_entropy(&short_key);
330        assert!(result.is_err(), "Should reject too short key");
331    }
332
333    #[test]
334    fn test_validate_key_entropy_repeated_pattern() {
335        let validator = EntropyValidator::new().unwrap();
336
337        let repeated_key = vec![1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4];
338        let result = validator.validate_key_entropy(&repeated_key);
339        assert!(result.is_err(), "Should reject key with repeated patterns");
340    }
341
342    #[test]
343    fn test_validate_key_entropy_sequential_pattern() {
344        let validator = EntropyValidator::new().unwrap();
345
346        // 16-byte key (≤64 B) — a single ascending run triggers rejection.
347        let sequential_key: Vec<u8> = (1..=16).collect();
348        let result = validator.validate_key_entropy(&sequential_key);
349        assert!(
350            result.is_err(),
351            "Should reject short key that is entirely sequential"
352        );
353    }
354
355    #[cfg(not(feature = "relaxed_entropy_validation"))]
356    #[test]
357    fn test_isolated_sequential_run_in_large_key_is_accepted() {
358        let validator = EntropyValidator::new().unwrap();
359
360        // Build a 4032-byte key (ML-DSA-65 secret key size) using a 32-bit
361        // xorshift PRNG so the sequence does not cycle within 4032 bytes.
362        let mut key = Vec::with_capacity(4032);
363        let mut state: u32 = 0xDEAD_BEEF;
364        while key.len() < 4032 {
365            state ^= state << 13;
366            state ^= state >> 17;
367            state ^= state << 5;
368            key.extend_from_slice(&state.to_le_bytes());
369        }
370        key.truncate(4032);
371
372        // Plant one ascending run at offset 100.
373        key[100] = 0x50;
374        key[101] = 0x51;
375        key[102] = 0x52;
376        key[103] = 0x53;
377
378        let result = validator.validate_key_entropy(&key);
379        assert!(
380            result.is_ok(),
381            "A single 4-byte ascending run in a 4032-byte key must not trigger rejection"
382        );
383    }
384
385    #[cfg(not(feature = "relaxed_entropy_validation"))]
386    #[test]
387    fn test_ml_dsa_sized_key_does_not_require_full_byte_alphabet() {
388        let validator = EntropyValidator::new().unwrap();
389
390        // ML-DSA-65 signing keys are ~4 KiB of structured data; they need not use
391        // all 256 byte values. Old logic required min(len/10, 256) = 256 unique
392        // bytes and falsely rejected real keys.
393        let pool: Vec<u8> = (0u8..80).collect();
394        let mut key = Vec::with_capacity(4032);
395        let mut state: u32 = 0xC0FFEE42;
396        while key.len() < 4032 {
397            state ^= state << 13;
398            state ^= state >> 17;
399            state ^= state << 5;
400            let b = state.to_le_bytes()[0];
401            key.push(pool[(b as usize) % pool.len()]);
402        }
403
404        let mut seen = [false; 256];
405        let mut unique = 0usize;
406        for &b in &key {
407            if !seen[b as usize] {
408                seen[b as usize] = true;
409                unique += 1;
410            }
411        }
412        assert!(
413            unique < 256,
414            "fixture should use fewer than 256 byte values (got {unique})"
415        );
416
417        let result = validator.validate_key_entropy(&key);
418        assert!(
419            result.is_ok(),
420            "structured PQ-sized key with modest byte diversity must be accepted: {result:?}"
421        );
422    }
423
424    #[cfg(not(feature = "relaxed_entropy_validation"))]
425    #[test]
426    fn test_ml_kem_1024_sized_key_does_not_require_full_byte_alphabet() {
427        let validator = EntropyValidator::new().unwrap();
428
429        // ML-KEM-1024 decapsulation keys are 3168 B. The same `min(len/10, 256)` bug
430        // required 256 distinct byte values and broke HPKE/KEM tests on CI.
431        let pool: Vec<u8> = (0u8..200).collect();
432        let mut key = Vec::with_capacity(3168);
433        let mut state: u32 = 0x514B_3000;
434        while key.len() < 3168 {
435            state ^= state << 13;
436            state ^= state >> 17;
437            state ^= state << 5;
438            let b = state.to_le_bytes()[0];
439            key.push(pool[(b as usize) % pool.len()]);
440        }
441
442        let mut seen = [false; 256];
443        let mut unique = 0usize;
444        for &b in &key {
445            if !seen[b as usize] {
446                seen[b as usize] = true;
447                unique += 1;
448            }
449        }
450        assert!(
451            unique < 256,
452            "fixture should use fewer than 256 byte values (got {unique})"
453        );
454
455        let result = validator.validate_key_entropy(&key);
456        assert!(
457            result.is_ok(),
458            "ML-KEM-1024-sized structured key must be accepted: {result:?}"
459        );
460    }
461
462    #[cfg(not(feature = "relaxed_entropy_validation"))]
463    #[test]
464    fn test_massively_sequential_key_rejected() {
465        let validator = EntropyValidator::new().unwrap();
466
467        // 256-byte key that is entirely ascending (wrapping).
468        let key: Vec<u8> = (0..=255).collect();
469        let result = validator.validate_key_entropy(&key);
470        assert!(
471            result.is_err(),
472            "Key that is entirely sequential should be rejected"
473        );
474    }
475
476    #[test]
477    fn test_entropy_validation_control() {
478        let mut validator = EntropyValidator::new().unwrap();
479
480        assert!(
481            validator.is_entropy_validation_enabled(),
482            "Entropy validation should be enabled by default"
483        );
484        assert_eq!(
485            validator.min_entropy_bits(),
486            128,
487            "Default minimum entropy should be 128 bits"
488        );
489
490        validator.set_entropy_validation(false);
491        assert!(
492            !validator.is_entropy_validation_enabled(),
493            "Entropy validation should be disabled"
494        );
495
496        validator.set_entropy_validation(true);
497        assert!(
498            validator.is_entropy_validation_enabled(),
499            "Entropy validation should be enabled"
500        );
501
502        validator.set_min_entropy_bits(256);
503        assert_eq!(
504            validator.min_entropy_bits(),
505            256,
506            "Minimum entropy should be updated"
507        );
508    }
509}