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    // `relaxed_entropy_validation` deliberately accepts these patterns, so the assertion
334    // below only holds with the strict validator -- same gate as every other
335    // rejection test in this module.
336    #[cfg(not(feature = "relaxed_entropy_validation"))]
337    #[test]
338    fn test_validate_key_entropy_repeated_pattern() {
339        let validator = EntropyValidator::new().unwrap();
340
341        let repeated_key = vec![1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4];
342        let result = validator.validate_key_entropy(&repeated_key);
343        assert!(result.is_err(), "Should reject key with repeated patterns");
344    }
345
346    // `relaxed_entropy_validation` deliberately accepts these patterns, so the assertion
347    // below only holds with the strict validator -- same gate as every other
348    // rejection test in this module.
349    #[cfg(not(feature = "relaxed_entropy_validation"))]
350    #[test]
351    fn test_validate_key_entropy_sequential_pattern() {
352        let validator = EntropyValidator::new().unwrap();
353
354        // 16-byte key (≤64 B) — a single ascending run triggers rejection.
355        let sequential_key: Vec<u8> = (1..=16).collect();
356        let result = validator.validate_key_entropy(&sequential_key);
357        assert!(
358            result.is_err(),
359            "Should reject short key that is entirely sequential"
360        );
361    }
362
363    #[cfg(not(feature = "relaxed_entropy_validation"))]
364    #[test]
365    fn test_isolated_sequential_run_in_large_key_is_accepted() {
366        let validator = EntropyValidator::new().unwrap();
367
368        // Build a 4032-byte key (ML-DSA-65 secret key size) using a 32-bit
369        // xorshift PRNG so the sequence does not cycle within 4032 bytes.
370        let mut key = Vec::with_capacity(4032);
371        let mut state: u32 = 0xDEAD_BEEF;
372        while key.len() < 4032 {
373            state ^= state << 13;
374            state ^= state >> 17;
375            state ^= state << 5;
376            key.extend_from_slice(&state.to_le_bytes());
377        }
378        key.truncate(4032);
379
380        // Plant one ascending run at offset 100.
381        key[100] = 0x50;
382        key[101] = 0x51;
383        key[102] = 0x52;
384        key[103] = 0x53;
385
386        let result = validator.validate_key_entropy(&key);
387        assert!(
388            result.is_ok(),
389            "A single 4-byte ascending run in a 4032-byte key must not trigger rejection"
390        );
391    }
392
393    #[cfg(not(feature = "relaxed_entropy_validation"))]
394    #[test]
395    fn test_ml_dsa_sized_key_does_not_require_full_byte_alphabet() {
396        let validator = EntropyValidator::new().unwrap();
397
398        // ML-DSA-65 signing keys are ~4 KiB of structured data; they need not use
399        // all 256 byte values. Old logic required min(len/10, 256) = 256 unique
400        // bytes and falsely rejected real keys.
401        let pool: Vec<u8> = (0u8..80).collect();
402        let mut key = Vec::with_capacity(4032);
403        let mut state: u32 = 0xC0FFEE42;
404        while key.len() < 4032 {
405            state ^= state << 13;
406            state ^= state >> 17;
407            state ^= state << 5;
408            let b = state.to_le_bytes()[0];
409            key.push(pool[(b as usize) % pool.len()]);
410        }
411
412        let mut seen = [false; 256];
413        let mut unique = 0usize;
414        for &b in &key {
415            if !seen[b as usize] {
416                seen[b as usize] = true;
417                unique += 1;
418            }
419        }
420        assert!(
421            unique < 256,
422            "fixture should use fewer than 256 byte values (got {unique})"
423        );
424
425        let result = validator.validate_key_entropy(&key);
426        assert!(
427            result.is_ok(),
428            "structured PQ-sized key with modest byte diversity must be accepted: {result:?}"
429        );
430    }
431
432    #[cfg(not(feature = "relaxed_entropy_validation"))]
433    #[test]
434    fn test_ml_kem_1024_sized_key_does_not_require_full_byte_alphabet() {
435        let validator = EntropyValidator::new().unwrap();
436
437        // ML-KEM-1024 decapsulation keys are 3168 B. The same `min(len/10, 256)` bug
438        // required 256 distinct byte values and broke HPKE/KEM tests on CI.
439        let pool: Vec<u8> = (0u8..200).collect();
440        let mut key = Vec::with_capacity(3168);
441        let mut state: u32 = 0x514B_3000;
442        while key.len() < 3168 {
443            state ^= state << 13;
444            state ^= state >> 17;
445            state ^= state << 5;
446            let b = state.to_le_bytes()[0];
447            key.push(pool[(b as usize) % pool.len()]);
448        }
449
450        let mut seen = [false; 256];
451        let mut unique = 0usize;
452        for &b in &key {
453            if !seen[b as usize] {
454                seen[b as usize] = true;
455                unique += 1;
456            }
457        }
458        assert!(
459            unique < 256,
460            "fixture should use fewer than 256 byte values (got {unique})"
461        );
462
463        let result = validator.validate_key_entropy(&key);
464        assert!(
465            result.is_ok(),
466            "ML-KEM-1024-sized structured key must be accepted: {result:?}"
467        );
468    }
469
470    #[cfg(not(feature = "relaxed_entropy_validation"))]
471    #[test]
472    fn test_massively_sequential_key_rejected() {
473        let validator = EntropyValidator::new().unwrap();
474
475        // 256-byte key that is entirely ascending (wrapping).
476        let key: Vec<u8> = (0..=255).collect();
477        let result = validator.validate_key_entropy(&key);
478        assert!(
479            result.is_err(),
480            "Key that is entirely sequential should be rejected"
481        );
482    }
483
484    #[test]
485    fn test_entropy_validation_control() {
486        let mut validator = EntropyValidator::new().unwrap();
487
488        assert!(
489            validator.is_entropy_validation_enabled(),
490            "Entropy validation should be enabled by default"
491        );
492        assert_eq!(
493            validator.min_entropy_bits(),
494            128,
495            "Default minimum entropy should be 128 bits"
496        );
497
498        validator.set_entropy_validation(false);
499        assert!(
500            !validator.is_entropy_validation_enabled(),
501            "Entropy validation should be disabled"
502        );
503
504        validator.set_entropy_validation(true);
505        assert!(
506            validator.is_entropy_validation_enabled(),
507            "Entropy validation should be enabled"
508        );
509
510        validator.set_min_entropy_bits(256);
511        assert_eq!(
512            validator.min_entropy_bits(),
513            256,
514            "Minimum entropy should be updated"
515        );
516    }
517}