Skip to main content

ctutils/
primitives.rs

1//! Constant-time integer primitives for cryptographic arithmetic.
2//!
3//! Core integer methods can lower to branches or table lookups on targets without the corresponding
4//! instructions. These helpers provide alternatives if `value` is zero-bit counting  and carry propagation.
5//!
6//! Like [`Choice`], they use optimization barriers where needed. Their generated code should still
7//! be checked for the target and compiler in use.
8//!
9//! Native zero counts are used when supported and advertised by the target configuration.
10
11use crate::Choice;
12use core::hint::black_box;
13
14/// Bitwise less-than-or equal: returns `1` if `x <= y`, and otherwise returns `0`.
15///
16/// See "Hacker's Delight" 2nd edition, section 2-12 (Comparison predicates)
17macro_rules! bitle {
18    ($x:expr, $y:expr, $bits:expr) => {
19        (((!$x) | $y) & (($x ^ $y) | !($y.wrapping_sub($x)))) >> ($bits - 1)
20    };
21}
22
23/// Outgoing borrow bit for a wrapping `x - y - borrow_in` result.
24///
25/// See "Hacker's Delight", 2nd edition, section 2-13 (Overflow detection)
26macro_rules! bitborrow {
27    ($x:expr, $y:expr, $result:expr, $bits:expr) => {
28        (((!$x) & $y) | (((!$x) | $y) & $result)) >> ($bits - 1)
29    };
30}
31
32/// Bitwise less-than: returns `1` if `x < y`, and otherwise returns `0`.
33///
34/// See "Hacker's Delight" 2nd edition, section 2-12 (Comparison predicates)
35macro_rules! bitlt {
36    ($x:expr, $y:expr, $bits:expr) => {
37        $crate::primitives::bitborrow!($x, $y, ($x).wrapping_sub($y), $bits)
38    };
39}
40
41/// Bitwise non-zero: returns `1` if `x != 0`, and otherwise returns `0`.
42macro_rules! bitnz {
43    ($value:expr, $bits:expr) => {
44        ($value | $value.wrapping_neg()) >> ($bits - 1)
45    };
46}
47
48pub(crate) use {bitborrow, bitle, bitlt, bitnz};
49
50// Keep the comparison bit opaque before narrowing it to Choice's byte representation.
51// ARM targets without pointer-width atomics cover Thumb-1 on Rust 1.87, which does not
52// advertise `thumb2` as a target feature. The other targets retain the original lowering.
53macro_rules! comparison_primitives {
54    ($ty:ty, $le:ident, $lt:ident, $nz:ident, $from_lsb:ident) => {
55        #[inline(always)]
56        pub(crate) const fn $le(lhs: $ty, rhs: $ty) -> Choice {
57            let bit = bitle!(lhs, rhs, <$ty>::BITS);
58            #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))]
59            let bit = black_box(bit);
60            Choice::$from_lsb(bit)
61        }
62
63        #[cfg_attr(
64            all(target_arch = "arm", not(target_has_atomic = "ptr")),
65            inline(never)
66        )]
67        #[cfg_attr(
68            not(all(target_arch = "arm", not(target_has_atomic = "ptr"))),
69            inline(always)
70        )]
71        pub(crate) const fn $lt(lhs: $ty, rhs: $ty) -> Choice {
72            let bit = $crate::primitives::bitlt!(lhs, rhs, <$ty>::BITS);
73            #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))]
74            let bit = black_box(bit);
75            Choice::$from_lsb(bit)
76        }
77
78        #[inline(always)]
79        pub(crate) const fn $nz(value: $ty) -> Choice {
80            let bit = bitnz!(value, <$ty>::BITS);
81            #[cfg(all(target_arch = "arm", not(target_has_atomic = "ptr")))]
82            let bit = black_box(bit);
83            Choice::$from_lsb(bit)
84        }
85    };
86}
87
88comparison_primitives!(u32, le_u32, lt_u32, nz_u32, from_u32_lsb);
89comparison_primitives!(u64, le_u64, lt_u64, nz_u64, from_u64_lsb);
90
91macro_rules! borrowing_sub_primitives {
92    ($ty:ty, $sub:ident, $sub_mask:ident, $sub_inner:ident, $from_lsb:ident) => {
93        /// Subtract `rhs` and an incoming borrow from `lhs`.
94        ///
95        /// Returns the wrapping difference and [`Choice::TRUE`] if the subtraction underflows.
96        /// The incoming `borrow` subtracts one when true and zero when false.
97        #[inline]
98        #[must_use]
99        pub const fn $sub(lhs: $ty, rhs: $ty, borrow: Choice) -> ($ty, Choice) {
100            let (difference, borrow) = $sub_inner(lhs, rhs, borrow.to_u8() as $ty);
101            (difference, Choice::$from_lsb(borrow))
102        }
103
104        /// Subtract `rhs` and an incoming borrow from `lhs`, using word-sized borrow masks.
105        ///
106        /// The incoming borrow is the most significant bit of `borrow`; all other bits are ignored.
107        /// Returns the wrapping difference and an all-ones mask if the subtraction underflows,
108        /// or zero otherwise.
109        ///
110        /// The outgoing mask can be passed directly to the next limb's subtraction, avoiding
111        /// conversions to and from [`Choice`] at each step of a multi-limb subtraction.
112        #[inline]
113        #[must_use]
114        pub const fn $sub_mask(lhs: $ty, rhs: $ty, borrow: $ty) -> ($ty, $ty) {
115            let (difference, borrow) = $sub_inner(lhs, rhs, borrow >> (<$ty>::BITS - 1));
116            (difference, borrow.wrapping_neg())
117        }
118
119        // The incoming borrow must be zero or one; the outgoing borrow is also zero or one.
120        #[inline(always)]
121        const fn $sub_inner(lhs: $ty, rhs: $ty, borrow: $ty) -> ($ty, $ty) {
122            let difference = lhs.wrapping_sub(rhs).wrapping_sub(borrow);
123            // Keep all arithmetic in the operand type: widening can trigger incorrect
124            // optimization on older compilers: https://github.com/rust-lang/rust/issues/149522
125            // Keep the full-width bit opaque before forming a mask or narrowing it to Choice,
126            // so a caller's masked add-back cannot be folded into a secret-dependent branch.
127            let borrow = black_box(bitborrow!(lhs, rhs, difference, <$ty>::BITS));
128            (difference, borrow)
129        }
130    };
131}
132
133borrowing_sub_primitives!(
134    u32,
135    borrowing_sub_u32,
136    borrowing_sub_mask_u32,
137    borrowing_sub_inner_u32,
138    from_u32_lsb
139);
140borrowing_sub_primitives!(
141    u64,
142    borrowing_sub_u64,
143    borrowing_sub_mask_u64,
144    borrowing_sub_inner_u64,
145    from_u64_lsb
146);
147
148/// Count leading zero bits, returning 32 if `value` is zero.
149#[inline]
150#[must_use]
151pub const fn leading_zeros_u32(value: u32) -> u32 {
152    #[cfg(any(
153        target_arch = "aarch64",
154        target_arch = "x86",
155        target_arch = "x86_64",
156        target_feature = "thumb2",
157        target_feature = "zbb"
158    ))]
159    {
160        value.leading_zeros()
161    }
162    #[cfg(not(any(
163        target_arch = "aarch64",
164        target_arch = "x86",
165        target_arch = "x86_64",
166        target_feature = "thumb2",
167        target_feature = "zbb"
168    )))]
169    {
170        leading_zeros_u32_soft(value)
171    }
172}
173
174/// Count trailing zero bits, returning 32 if `value` is zero.
175#[inline]
176#[must_use]
177pub const fn trailing_zeros_u32(value: u32) -> u32 {
178    #[cfg(any(
179        target_arch = "aarch64",
180        target_arch = "x86",
181        target_arch = "x86_64",
182        target_feature = "thumb2",
183        target_feature = "zbb"
184    ))]
185    {
186        value.trailing_zeros()
187    }
188    #[cfg(not(any(
189        target_arch = "aarch64",
190        target_arch = "x86",
191        target_arch = "x86_64",
192        target_feature = "thumb2",
193        target_feature = "zbb"
194    )))]
195    {
196        // Reversing bits uses fixed shifts and masks when RBIT is unavailable.
197        leading_zeros_u32_soft(value.reverse_bits())
198    }
199}
200
201/// Count leading zero bits, returning 64 if `value` is zero.
202#[inline]
203#[must_use]
204#[allow(clippy::cast_possible_truncation)]
205pub const fn leading_zeros_u64(value: u64) -> u32 {
206    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
207    {
208        value.leading_zeros()
209    }
210    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
211    {
212        let hi = (value >> 32) as u32;
213        let lo = value as u32;
214        let mask = Choice::from_u32_nz(hi).not().to_u32_mask();
215        leading_zeros_u32(hi).wrapping_add(mask & leading_zeros_u32(lo))
216    }
217}
218
219/// Count trailing zero bits, returning 64 if `value` is zero.
220#[inline]
221#[must_use]
222#[allow(clippy::cast_possible_truncation)]
223pub const fn trailing_zeros_u64(value: u64) -> u32 {
224    #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
225    {
226        value.trailing_zeros()
227    }
228    #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
229    {
230        let hi = (value >> 32) as u32;
231        let lo = value as u32;
232        let mask = Choice::from_u32_nz(lo).not().to_u32_mask();
233        trailing_zeros_u32(lo).wrapping_add(mask & trailing_zeros_u32(hi))
234    }
235}
236
237/// Fixed-stage binary search for the highest set bit, including the zero case.
238#[cfg(any(
239    test,
240    not(any(
241        target_arch = "aarch64",
242        target_arch = "x86",
243        target_arch = "x86_64",
244        target_feature = "thumb2",
245        target_feature = "zbb"
246    ))
247))]
248#[inline]
249#[allow(clippy::arithmetic_side_effects, reason = "shift is between 1 and 16")]
250const fn leading_zeros_u32_soft(mut value: u32) -> u32 {
251    let mut count = 0u32;
252    let mut shift = 16;
253    while shift != 0 {
254        let mask = Choice::from_u32_nz(value >> (u32::BITS - shift))
255            .not()
256            .to_u32_mask();
257        value ^= mask & (value ^ (value << shift));
258        count = count.wrapping_add(mask & shift);
259        shift >>= 1;
260    }
261    count.wrapping_add(Choice::from_u32_nz(value).not().to_u32_mask() & 1)
262}
263
264/// Add two integers, returning the wrapping sum and [`Choice::TRUE`] on overflow.
265#[inline]
266#[must_use]
267pub const fn overflowing_add_u64(lhs: u64, rhs: u64) -> (u64, Choice) {
268    let sum = lhs.wrapping_add(rhs);
269    let carry = ((lhs & rhs) | ((lhs | rhs) & !sum)) >> (u64::BITS - 1);
270    (sum, Choice::from_u64_lsb(carry))
271}
272
273/// Add two integers, returning the wrapping sum and [`Choice::TRUE`] on overflow.
274#[inline]
275#[must_use]
276pub const fn overflowing_add_u128(lhs: u128, rhs: u128) -> (u128, Choice) {
277    #[cfg(target_arch = "riscv32")]
278    {
279        overflowing_add_u128_halves(lhs, rhs)
280    }
281    #[cfg(not(target_arch = "riscv32"))]
282    {
283        let sum = lhs.wrapping_add(rhs);
284        let carry = ((lhs & rhs) | ((lhs | rhs) & !sum)) >> (u128::BITS - 1);
285        (sum, Choice::from_u128_lsb(carry))
286    }
287}
288
289// RV32 can branch even in `u128::wrapping_add`, before computing the final carry.
290// Keep this workaround local to that target, preserving native addition elsewhere.
291#[cfg(any(target_arch = "riscv32", test))]
292#[inline]
293#[allow(clippy::cast_possible_truncation)]
294const fn overflowing_add_u128_halves(lhs: u128, rhs: u128) -> (u128, Choice) {
295    let (lo, lo_carry) = overflowing_add_u64(lhs as u64, rhs as u64);
296    let (hi, hi_carry) = overflowing_add_u64((lhs >> 64) as u64, (rhs >> 64) as u64);
297    let (hi, carry) = overflowing_add_u64(hi, lo_carry.to_u8() as u64);
298    (((hi as u128) << 64) | lo as u128, hi_carry.or(carry))
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    // Keep all public entrypoints usable by const callers such as crypto-bigint.
306    const _: () = {
307        assert!(borrowing_sub_u32(0, 0, Choice::TRUE).0 == u32::MAX);
308        assert!(borrowing_sub_u32(0, 0, Choice::TRUE).1.to_bool());
309        assert!(borrowing_sub_u64(0, 0, Choice::TRUE).0 == u64::MAX);
310        assert!(borrowing_sub_u64(0, 0, Choice::TRUE).1.to_bool());
311        assert!(!borrowing_sub_u32(1, 0, Choice::TRUE).1.to_bool());
312        assert!(!borrowing_sub_u64(1, 0, Choice::TRUE).1.to_bool());
313        assert!(borrowing_sub_mask_u32(0, 0, u32::MAX).0 == u32::MAX);
314        assert!(borrowing_sub_mask_u32(0, 0, u32::MAX).1 == u32::MAX);
315        assert!(borrowing_sub_mask_u64(0, 0, u64::MAX).0 == u64::MAX);
316        assert!(borrowing_sub_mask_u64(0, 0, u64::MAX).1 == u64::MAX);
317        assert!(borrowing_sub_mask_u32(1, 0, u32::MAX).1 == 0);
318        assert!(borrowing_sub_mask_u64(1, 0, u64::MAX).1 == 0);
319        assert!(leading_zeros_u32(0) == 32);
320        assert!(trailing_zeros_u32(0) == 32);
321        assert!(leading_zeros_u64(0) == 64);
322        assert!(trailing_zeros_u64(0) == 64);
323        assert!(overflowing_add_u64(u64::MAX, 1).1.to_bool());
324        assert!(overflowing_add_u128(u128::MAX, 1).1.to_bool());
325    };
326
327    #[test]
328    fn zero_counts_u32() {
329        for bit in 0..u32::BITS {
330            for value in [0, 1 << bit, !(1 << bit), u32::MAX >> bit] {
331                assert_eq!(leading_zeros_u32(value), value.leading_zeros());
332                assert_eq!(trailing_zeros_u32(value), value.trailing_zeros());
333                // Exercise the software fallback even on hosts with native instructions.
334                assert_eq!(leading_zeros_u32_soft(value), value.leading_zeros());
335                assert_eq!(
336                    leading_zeros_u32_soft(value.reverse_bits()),
337                    value.trailing_zeros()
338                );
339            }
340        }
341    }
342
343    #[test]
344    fn zero_counts_u64() {
345        for bit in 0..u64::BITS {
346            for value in [0, 1 << bit, !(1 << bit), u64::MAX >> bit] {
347                assert_eq!(leading_zeros_u64(value), value.leading_zeros());
348                assert_eq!(trailing_zeros_u64(value), value.trailing_zeros());
349            }
350        }
351    }
352
353    macro_rules! subtraction_tests {
354        ($test:ident, $ty:ty, $sub:ident, $sub_mask:ident) => {
355            #[test]
356            fn $test() {
357                let half = 1 << (<$ty>::BITS >> 1);
358                let high = 1 << (<$ty>::BITS - 1);
359                let values = [0, 1, half - 1, half, half + 1, high - 1, high, <$ty>::MAX];
360                for lhs in values {
361                    for rhs in values {
362                        // Include noncanonical masks: only the most significant bit matters.
363                        for mask in [0, 1, high - 1, high, <$ty>::MAX - 1, <$ty>::MAX] {
364                            let incoming = mask >> (<$ty>::BITS - 1) != 0;
365                            let (expected, first_borrow) = lhs.overflowing_sub(rhs);
366                            let (expected, second_borrow) =
367                                expected.overflowing_sub(<$ty>::from(incoming));
368                            let (difference, borrow) =
369                                $sub(lhs, rhs, Choice::from_u8_lsb(u8::from(incoming)));
370                            assert_eq!(
371                                (difference, borrow.to_bool()),
372                                (expected, first_borrow | second_borrow)
373                            );
374                            assert_eq!(
375                                $sub_mask(lhs, rhs, mask),
376                                (
377                                    expected,
378                                    <$ty>::from(first_borrow | second_borrow).wrapping_neg()
379                                )
380                            );
381                        }
382                    }
383                }
384            }
385        };
386    }
387
388    subtraction_tests!(sub_u32, u32, borrowing_sub_u32, borrowing_sub_mask_u32);
389    subtraction_tests!(sub_u64, u64, borrowing_sub_u64, borrowing_sub_mask_u64);
390
391    #[test]
392    fn sub_mask_propagates_borrow() {
393        let (lo, borrow) = borrowing_sub_mask_u32(0, 1, 0);
394        let (mid, borrow) = borrowing_sub_mask_u32(0, 0, borrow);
395        assert_eq!((lo, mid, borrow), (u32::MAX, u32::MAX, u32::MAX));
396        assert_eq!(borrowing_sub_mask_u32(1, 0, borrow), (0, 0));
397
398        let (lo, borrow) = borrowing_sub_mask_u64(0, 1, 0);
399        let (mid, borrow) = borrowing_sub_mask_u64(0, 0, borrow);
400        assert_eq!((lo, mid, borrow), (u64::MAX, u64::MAX, u64::MAX));
401        assert_eq!(borrowing_sub_mask_u64(1, 0, borrow), (0, 0));
402    }
403
404    macro_rules! addition_tests {
405        ($test:ident, $ty:ty, $add:ident) => {
406            #[test]
407            fn $test() {
408                // Include both full-width and half-width carry boundaries.
409                let half = 1 << (<$ty>::BITS >> 1);
410                let high = 1 << (<$ty>::BITS - 1);
411                let values = [0, 1, half - 1, half, half + 1, high - 1, high, <$ty>::MAX];
412                for lhs in values {
413                    for rhs in values {
414                        let (sum, carry) = $add(lhs, rhs);
415                        assert_eq!((sum, carry.to_bool()), lhs.overflowing_add(rhs));
416                    }
417                }
418            }
419        };
420    }
421
422    addition_tests!(add_u64, u64, overflowing_add_u64);
423    addition_tests!(add_u128, u128, overflowing_add_u128);
424    addition_tests!(add_u128_halves, u128, overflowing_add_u128_halves);
425}