Skip to main content

saorsa_pqc/pqc/
constant_time.rs

1//! Constant-time operations for cryptographic primitives
2//!
3//! This module provides constant-time comparison and conditional operations
4//! to prevent timing attacks on sensitive cryptographic data.
5
6use core::hint::black_box;
7use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption};
8use zeroize::Zeroize;
9
10/// Constant-time comparison for byte slices
11///
12/// Returns true if the slices are equal, false otherwise.
13/// The comparison runs in constant time regardless of where differences occur.
14///
15/// # Security Note
16/// This function is designed to be constant-time to prevent timing attacks.
17/// Uses the `subtle` crate's ConstantTimeEq implementation which is
18/// well-audited and optimized for constant-time behavior.
19///
20/// When slices have different lengths, this function returns false but
21/// still performs the comparison on the overlapping portion to minimize
22/// timing variations. Note that some timing difference is unavoidable
23/// when lengths differ significantly.
24#[must_use]
25pub fn ct_eq(a: &[u8], b: &[u8]) -> bool {
26    // Use subtle's ConstantTimeEq which handles same-length comparison
27    // in a well-tested constant-time manner
28    if a.len() != b.len() {
29        // Different lengths - still compare the overlapping portion
30        // to minimize timing leakage, but we know the result will be false
31        let min_len = a.len().min(b.len());
32        if min_len > 0 {
33            // Compare overlapping portion to do consistent work.
34            // Slicing is safe: min_len <= a.len() and min_len <= b.len() by construction.
35            #[allow(clippy::indexing_slicing)]
36            let _ = black_box(a[..min_len].ct_eq(&b[..min_len]));
37        }
38        return black_box(false);
39    }
40
41    // Same length - use subtle's constant-time comparison directly
42    // This is the critical path for security-sensitive comparisons
43    let result = a.ct_eq(b);
44    black_box(result.into())
45}
46
47/// Constant-time conditional selection
48///
49/// Selects `a` if `choice` is true, `b` otherwise.
50/// The selection happens in constant time.
51#[inline]
52pub fn ct_select<T: ConditionallySelectable>(a: &T, b: &T, choice: bool) -> T {
53    T::conditional_select(b, a, Choice::from(u8::from(choice)))
54}
55
56/// Constant-time conditional assignment
57///
58/// Assigns `new_val` to `dest` if `choice` is true.
59/// The assignment happens in constant time.
60#[inline]
61pub fn ct_assign<T: ConditionallySelectable>(dest: &mut T, new_val: &T, choice: bool) {
62    dest.conditional_assign(new_val, Choice::from(u8::from(choice)));
63}
64
65/// Constant-time option type for cryptographic operations
66///
67/// Similar to `Option<T>` but with constant-time operations.
68pub struct CtSecretOption<T> {
69    value: T,
70    is_some: Choice,
71}
72
73impl<T> CtSecretOption<T> {
74    /// Create a new Some variant
75    #[inline]
76    pub fn some(value: T) -> Self {
77        Self {
78            value,
79            is_some: Choice::from(1),
80        }
81    }
82
83    /// Create a new None variant
84    #[inline]
85    pub fn none(default: T) -> Self {
86        Self {
87            value: default,
88            is_some: Choice::from(0),
89        }
90    }
91
92    /// Check if the option contains a value (constant-time)
93    #[inline]
94    pub const fn is_some(&self) -> Choice {
95        self.is_some
96    }
97
98    /// Check if the option is None (constant-time)
99    #[inline]
100    pub fn is_none(&self) -> Choice {
101        !self.is_some
102    }
103
104    /// Unwrap the value with a default if None
105    #[inline]
106    pub fn unwrap_or(self, default: T) -> T
107    where
108        T: ConditionallySelectable,
109    {
110        T::conditional_select(&default, &self.value, self.is_some)
111    }
112
113    /// Map the value if Some
114    #[inline]
115    pub fn map<U, F>(self, f: F) -> CtSecretOption<U>
116    where
117        F: FnOnce(T) -> U,
118        U: ConditionallySelectable + Default,
119    {
120        let mapped = f(self.value);
121        let default = U::default();
122        CtSecretOption {
123            value: U::conditional_select(&default, &mapped, self.is_some),
124            is_some: self.is_some,
125        }
126    }
127}
128
129impl<T: Zeroize> Zeroize for CtSecretOption<T> {
130    fn zeroize(&mut self) {
131        self.value.zeroize();
132        self.is_some = Choice::from(0);
133    }
134}
135
136/// Trait for types that support constant-time equality comparison
137pub trait ConstantTimeEqExt: Sized {
138    /// Perform constant-time equality comparison
139    fn ct_eq(&self, other: &Self) -> Choice;
140
141    /// Perform constant-time inequality comparison
142    fn ct_ne(&self, other: &Self) -> Choice {
143        !self.ct_eq(other)
144    }
145}
146
147/// Implement constant-time comparison for secret key types
148macro_rules! impl_ct_eq_for_secret {
149    ($type:ty) => {
150        impl ConstantTimeEqExt for $type {
151            fn ct_eq(&self, other: &Self) -> Choice {
152                self.as_bytes().ct_eq(other.as_bytes())
153            }
154        }
155    };
156}
157
158// Import types that need constant-time operations
159use crate::pqc::ml_dsa_44::{MlDsa44SecretKey, MlDsa44Signature};
160use crate::pqc::ml_dsa_87::{MlDsa87SecretKey, MlDsa87Signature};
161use crate::pqc::ml_kem_1024::MlKem1024SecretKey;
162use crate::pqc::ml_kem_512::MlKem512SecretKey;
163use crate::pqc::types::{MlDsaSecretKey, MlDsaSignature, MlKemSecretKey, SharedSecret};
164
165// Implement constant-time comparison for all sensitive types
166impl_ct_eq_for_secret!(MlKemSecretKey);
167impl_ct_eq_for_secret!(MlDsaSecretKey);
168impl_ct_eq_for_secret!(SharedSecret);
169impl_ct_eq_for_secret!(MlKem512SecretKey);
170impl_ct_eq_for_secret!(MlKem1024SecretKey);
171impl_ct_eq_for_secret!(MlDsa44SecretKey);
172impl_ct_eq_for_secret!(MlDsa87SecretKey);
173
174// Implement for signatures
175impl ConstantTimeEqExt for MlDsaSignature {
176    fn ct_eq(&self, other: &Self) -> Choice {
177        self.as_bytes().ct_eq(other.as_bytes())
178    }
179}
180
181impl ConstantTimeEqExt for MlDsa44Signature {
182    fn ct_eq(&self, other: &Self) -> Choice {
183        self.as_bytes().ct_eq(other.as_bytes())
184    }
185}
186
187impl ConstantTimeEqExt for MlDsa87Signature {
188    fn ct_eq(&self, other: &Self) -> Choice {
189        self.as_bytes().ct_eq(other.as_bytes())
190    }
191}
192
193/// Perform constant-time verification of a boolean condition
194///
195/// Returns a `CtOption` that is Some(value) if condition is true, None otherwise.
196/// The operation runs in constant time.
197#[inline]
198pub fn ct_verify<T>(condition: bool, value: T) -> CtOption<T> {
199    CtOption::new(value, Choice::from(u8::from(condition)))
200}
201
202/// Constant-time byte array comparison
203///
204/// Compares two fixed-size byte arrays in constant time.
205#[must_use]
206pub fn ct_array_eq<const N: usize>(a: &[u8; N], b: &[u8; N]) -> bool {
207    // Use black_box to prevent optimization
208    let result = a.ct_eq(b);
209    black_box(result.into())
210}
211
212/// Clear sensitive data from memory in constant time
213///
214/// This ensures the compiler doesn't optimize away the clearing operation.
215#[inline]
216pub fn ct_clear<T: Zeroize>(data: &mut T) {
217    data.zeroize();
218}
219
220/// Constant-time conditional copy
221///
222/// Copies `src` to `dest` if `choice` is true AND lengths match.
223///
224/// # Returns
225/// `true` if lengths matched (copy may or may not have occurred based on `choice`),
226/// `false` if lengths did not match (no copy occurred).
227///
228/// # Security Properties
229/// - **Constant-time on `choice`**: Whether `choice` is true or false does not
230///   affect timing. This is the critical security property for FIPS 140-3.
231/// - **Constant-time on buffer contents**: The values in `src` and `dest` do not
232///   affect timing.
233/// - **Length is NOT secret**: Buffer lengths are public API parameters. Different
234///   lengths will have different timing, which is expected and not a vulnerability.
235///
236/// # Example
237/// ```
238/// use saorsa_pqc::pqc::constant_time::ct_copy_bytes;
239///
240/// let mut dest = [0u8; 4];
241/// let src = [1u8, 2, 3, 4];
242///
243/// // Copy happens (choice=true, lengths match)
244/// assert!(ct_copy_bytes(&mut dest, &src, true));
245/// assert_eq!(dest, [1, 2, 3, 4]);
246///
247/// // Reset and try with choice=false - no copy, but same timing
248/// dest = [0u8; 4];
249/// assert!(ct_copy_bytes(&mut dest, &src, false));
250/// assert_eq!(dest, [0, 0, 0, 0]); // Unchanged
251/// ```
252#[inline]
253#[must_use]
254pub fn ct_copy_bytes(dest: &mut [u8], src: &[u8], choice: bool) -> bool {
255    // Length mismatch is not a secret - early return is fine
256    if dest.len() != src.len() {
257        return false;
258    }
259
260    // The critical constant-time property: choice doesn't affect timing
261    let should_copy = Choice::from(u8::from(choice));
262
263    // Constant-time conditional copy for each byte
264    for (d, s) in dest.iter_mut().zip(src.iter()) {
265        d.conditional_assign(s, should_copy);
266    }
267
268    true
269}
270
271#[cfg(test)]
272#[allow(clippy::unwrap_used, clippy::expect_used)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_ct_eq() {
278        let a = [1u8, 2, 3, 4];
279        let b = [1u8, 2, 3, 4];
280        let c = [1u8, 2, 3, 5];
281
282        assert!(ct_eq(&a, &b));
283        assert!(!ct_eq(&a, &c));
284        assert!(!ct_eq(&a[..3], &b)); // Different lengths
285    }
286
287    #[test]
288    fn test_ct_select() {
289        let a = 42u32;
290        let b = 100u32;
291
292        assert_eq!(ct_select(&a, &b, true), a);
293        assert_eq!(ct_select(&a, &b, false), b);
294    }
295
296    #[test]
297    fn test_ct_option() {
298        let some_val = CtSecretOption::some(42u32);
299        let none_val = CtSecretOption::none(0u32);
300
301        assert_eq!(some_val.is_some().unwrap_u8(), 1);
302        assert_eq!(none_val.is_none().unwrap_u8(), 1);
303
304        assert_eq!(some_val.unwrap_or(100), 42);
305        assert_eq!(none_val.unwrap_or(100), 100);
306    }
307
308    #[test]
309    fn test_ct_copy_bytes() {
310        let src = [1u8, 2, 3, 4];
311        let mut dest1 = [0u8; 4];
312        let mut dest2 = [0u8; 4];
313
314        let success1 = ct_copy_bytes(&mut dest1, &src, true);
315        let success2 = ct_copy_bytes(&mut dest2, &src, false);
316
317        assert!(success1, "Copy with choice=true should succeed");
318        assert!(success2, "Copy with choice=false should succeed (no-op)");
319        assert_eq!(dest1, src);
320        assert_eq!(dest2, [0, 0, 0, 0]);
321    }
322
323    #[test]
324    fn test_ct_copy_bytes_mismatched_length() {
325        // Test that mismatched lengths are handled in constant time
326        // The function should still process in constant time but return false
327        let src_short = [1u8, 2];
328        let src_long = [1u8, 2, 3, 4, 5, 6];
329        let mut dest = [0u8; 4];
330
331        // Mismatched lengths should return false but still take constant time
332        let result1 = ct_copy_bytes(&mut dest, &src_short, true);
333        assert!(!result1, "Mismatched length should return false");
334        assert_eq!(
335            dest,
336            [0, 0, 0, 0],
337            "Dest should be unchanged on length mismatch"
338        );
339
340        let result2 = ct_copy_bytes(&mut dest, &src_long, true);
341        assert!(!result2, "Mismatched length should return false");
342        assert_eq!(
343            dest,
344            [0, 0, 0, 0],
345            "Dest should be unchanged on length mismatch"
346        );
347    }
348
349    #[test]
350    fn test_constant_time_property() {
351        // This test doesn't verify constant-time execution directly
352        // (that requires specialized tools), but ensures the API works correctly
353
354        let secret1 = vec![0u8; 1000];
355        let secret2 = vec![1u8; 1000];
356
357        // These operations should take the same time regardless of content
358        let _ = ct_eq(&secret1, &secret2);
359        let _ = ct_eq(&secret1, &secret1);
360
361        // The actual constant-time property would be verified with tools like
362        // valgrind, dudect, or specialized timing analysis
363    }
364}