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