Skip to content

Commit cd4286f

Browse files
authored
Add BoxedUint: heap-allocated fixed-precision integers (#221)
Use cases like DSA and RSA benefit from having heap-allocated integers whose precision can be chosen at runtime rather than compile time. These algorithms are used in conjunction with varying key sizes, and when dealing with a large number of potential key sizes it's helpful not to have to monomorphize the algorithm implementation for each one. Ideally we can define traits so that code can be written generically but used with either `Uint` or `BoxedUint`, allowing heapless `no_std` users to use e.g. `U2048`, while users with a heap can use `BoxedUint`. It should also be possible to make some of the existing algorithm implementations generic over `Uint` vs `BoxedUint`. This initial implementation only provides addition and comparison support, but also the initial scaffolding for adding more operations.
1 parent b158ced commit cd4286f

File tree

7 files changed

+351
-4
lines changed

7 files changed

+351
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ using const generics.
1717

1818
## Goals
1919

20-
- No heap allocations. `no_std`-friendly.
20+
- Supports `no_std`-friendly stack-allocated big integers.
2121
- Constant-time by default. Variable-time functions are explicitly marked as such.
2222
- Leverage what is possible today with const generics on `stable` rust.
2323
- Support `const fn` as much as possible, including decoding big integers from

src/boxed.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//! Heap-allocated "boxed" types.
2+
3+
pub(crate) mod uint;

src/boxed/uint.rs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
//! Heap-allocated big unsigned integers.
2+
3+
mod add;
4+
mod cmp;
5+
6+
use crate::{Limb, Word};
7+
use alloc::{vec, vec::Vec};
8+
use core::fmt;
9+
10+
#[cfg(feature = "zeroize")]
11+
use zeroize::Zeroize;
12+
13+
/// Fixed-precision heap-allocated big unsigned integer.
14+
///
15+
/// Alternative to the stack-allocated [`Uint`][`crate::Uint`] but with a
16+
/// fixed precision chosen at runtime instead of compile time.
17+
///
18+
/// Unlike many other heap-allocated big integer libraries, this type is not
19+
/// arbitrary precision and will wrap at its fixed-precision rather than
20+
/// automatically growing.
21+
#[derive(Clone, Default)]
22+
pub struct BoxedUint {
23+
/// Inner limb vector. Stored from least significant to most significant.
24+
limbs: Vec<Limb>,
25+
}
26+
27+
impl BoxedUint {
28+
/// Get the value `0`, represented as succinctly as possible.
29+
pub fn zero() -> Self {
30+
Self::default()
31+
}
32+
33+
/// Get the value `1`, represented as succinctly as possible.
34+
pub fn one() -> Self {
35+
Self {
36+
limbs: vec![Limb::ONE; 1],
37+
}
38+
}
39+
40+
/// Create a new [`BoxedUint`] with the given number of bits of precision.
41+
///
42+
/// Returns `None` if the number of bits is not a multiple of the
43+
/// [`Limb`] size.
44+
pub fn new(bits_precision: usize) -> Option<Self> {
45+
if bits_precision == 0 || bits_precision % Limb::BITS != 0 {
46+
return None;
47+
}
48+
49+
let nlimbs = bits_precision / Limb::BITS;
50+
51+
Some(Self {
52+
limbs: vec![Limb::ZERO; nlimbs],
53+
})
54+
}
55+
56+
/// Get the maximum value for a given number of bits of precision.
57+
///
58+
/// Returns `None` if the number of bits is not a multiple of the
59+
/// [`Limb`] size.
60+
pub fn max(bits_precision: usize) -> Option<Self> {
61+
let mut ret = Self::new(bits_precision)?;
62+
63+
for limb in &mut ret.limbs {
64+
*limb = Limb::MAX;
65+
}
66+
67+
Some(ret)
68+
}
69+
70+
/// Create a [`BoxedUint`] from an array of [`Word`]s (i.e. word-sized unsigned
71+
/// integers).
72+
#[inline]
73+
pub fn from_words(words: &[Word]) -> Self {
74+
Self {
75+
limbs: words.iter().copied().map(Into::into).collect(),
76+
}
77+
}
78+
79+
/// Create an array of [`Word`]s (i.e. word-sized unsigned integers) from
80+
/// a [`BoxedUint`].
81+
#[inline]
82+
pub fn to_words(&self) -> Vec<Word> {
83+
self.limbs.iter().copied().map(Into::into).collect()
84+
}
85+
86+
/// Borrow the inner limbs as a slice of [`Word`]s.
87+
pub fn as_words(&self) -> &[Word] {
88+
// SAFETY: `Limb` is a `repr(transparent)` newtype for `Word`
89+
#[allow(trivial_casts, unsafe_code)]
90+
unsafe {
91+
&*((self.limbs.as_slice() as *const _) as *const [Word])
92+
}
93+
}
94+
95+
/// Borrow the inner limbs as a mutable array of [`Word`]s.
96+
pub fn as_words_mut(&mut self) -> &mut [Word] {
97+
// SAFETY: `Limb` is a `repr(transparent)` newtype for `Word`
98+
#[allow(trivial_casts, unsafe_code)]
99+
unsafe {
100+
&mut *((self.limbs.as_mut_slice() as *mut _) as *mut [Word])
101+
}
102+
}
103+
104+
/// Borrow the limbs of this [`BoxedUint`].
105+
pub fn as_limbs(&self) -> &[Limb] {
106+
self.limbs.as_ref()
107+
}
108+
109+
/// Borrow the limbs of this [`BoxedUint`] mutably.
110+
pub fn as_limbs_mut(&mut self) -> &mut [Limb] {
111+
self.limbs.as_mut()
112+
}
113+
114+
/// Convert this [`BoxedUint`] into its inner limbs.
115+
pub fn to_limbs(&self) -> Vec<Limb> {
116+
self.limbs.clone()
117+
}
118+
119+
/// Convert this [`BoxedUint`] into its inner limbs.
120+
pub fn into_limbs(self) -> Vec<Limb> {
121+
self.limbs
122+
}
123+
124+
/// Get the precision of this [`BoxedUint`] in bits.
125+
pub fn bits(&self) -> usize {
126+
self.limbs.len() * Limb::BITS
127+
}
128+
129+
/// Sort two [`BoxedUint`]s by precision, returning a tuple of the shorter
130+
/// followed by the longer, or the original order if their precision is
131+
/// equal.
132+
fn sort_by_precision<'a>(a: &'a Self, b: &'a Self) -> (&'a Self, &'a Self) {
133+
if a.limbs.len() <= b.limbs.len() {
134+
(a, b)
135+
} else {
136+
(b, a)
137+
}
138+
}
139+
140+
/// Perform a carry chain-like operation over the limbs of the inputs,
141+
/// constructing a result from the returned limbs and carry.
142+
///
143+
/// If one of the two values has fewer limbs than the other, passes
144+
/// [`Limb::ZERO`] as the value for that limb.
145+
fn chain<F>(a: &Self, b: &Self, mut carry: Limb, f: F) -> (Self, Limb)
146+
where
147+
F: Fn(Limb, Limb, Limb) -> (Limb, Limb),
148+
{
149+
let (shorter, longer) = Self::sort_by_precision(a, b);
150+
let mut limbs = Vec::with_capacity(longer.limbs.len());
151+
152+
for i in 0..longer.limbs.len() {
153+
let &a = shorter.limbs.get(i).unwrap_or(&Limb::ZERO);
154+
let &b = longer.limbs.get(i).unwrap_or(&Limb::ZERO);
155+
let (limb, c) = f(a, b, carry);
156+
limbs.push(limb);
157+
carry = c;
158+
}
159+
160+
(Self { limbs }, carry)
161+
}
162+
}
163+
164+
impl AsRef<[Word]> for BoxedUint {
165+
fn as_ref(&self) -> &[Word] {
166+
self.as_words()
167+
}
168+
}
169+
170+
impl AsMut<[Word]> for BoxedUint {
171+
fn as_mut(&mut self) -> &mut [Word] {
172+
self.as_words_mut()
173+
}
174+
}
175+
176+
impl AsRef<[Limb]> for BoxedUint {
177+
fn as_ref(&self) -> &[Limb] {
178+
self.as_limbs()
179+
}
180+
}
181+
182+
impl AsMut<[Limb]> for BoxedUint {
183+
fn as_mut(&mut self) -> &mut [Limb] {
184+
self.as_limbs_mut()
185+
}
186+
}
187+
188+
impl fmt::Debug for BoxedUint {
189+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190+
write!(f, "BoxedUint(0x{self:X})")
191+
}
192+
}
193+
194+
impl fmt::Display for BoxedUint {
195+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196+
fmt::UpperHex::fmt(self, f)
197+
}
198+
}
199+
200+
impl fmt::LowerHex for BoxedUint {
201+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202+
if self.limbs.is_empty() {
203+
return fmt::LowerHex::fmt(&Limb::ZERO, f);
204+
}
205+
206+
for limb in self.limbs.iter().rev() {
207+
fmt::LowerHex::fmt(limb, f)?;
208+
}
209+
Ok(())
210+
}
211+
}
212+
213+
impl fmt::UpperHex for BoxedUint {
214+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215+
if self.limbs.is_empty() {
216+
return fmt::LowerHex::fmt(&Limb::ZERO, f);
217+
}
218+
219+
for limb in self.limbs.iter().rev() {
220+
fmt::UpperHex::fmt(limb, f)?;
221+
}
222+
Ok(())
223+
}
224+
}
225+
226+
#[cfg(feature = "zeroize")]
227+
impl Zeroize for BoxedUint {
228+
fn zeroize(&mut self) {
229+
self.limbs.zeroize();
230+
}
231+
}

src/boxed/uint/add.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//! [`BoxedUint`] addition operations.
2+
3+
use crate::{BoxedUint, CheckedAdd, Limb, Zero};
4+
use subtle::CtOption;
5+
6+
impl BoxedUint {
7+
/// Computes `a + b + carry`, returning the result along with the new carry.
8+
#[inline(always)]
9+
pub fn adc(&self, rhs: &Self, carry: Limb) -> (Self, Limb) {
10+
Self::chain(self, rhs, carry, |a, b, c| a.adc(b, c))
11+
}
12+
13+
/// Perform wrapping addition, discarding overflow.
14+
pub fn wrapping_add(&self, rhs: &Self) -> Self {
15+
self.adc(rhs, Limb::ZERO).0
16+
}
17+
}
18+
19+
impl CheckedAdd<&BoxedUint> for BoxedUint {
20+
type Output = Self;
21+
22+
fn checked_add(&self, rhs: &Self) -> CtOption<Self> {
23+
let (result, carry) = self.adc(rhs, Limb::ZERO);
24+
CtOption::new(result, carry.is_zero())
25+
}
26+
}
27+
28+
#[cfg(test)]
29+
mod tests {
30+
use super::{BoxedUint, CheckedAdd, Limb};
31+
32+
#[test]
33+
fn adc_no_carry() {
34+
let (res, carry) = BoxedUint::zero().adc(&BoxedUint::one(), Limb::ZERO);
35+
assert_eq!(res, BoxedUint::one());
36+
assert_eq!(carry, Limb::ZERO);
37+
}
38+
39+
#[test]
40+
fn adc_with_carry() {
41+
let (res, carry) = BoxedUint::max(Limb::BITS)
42+
.unwrap()
43+
.adc(&BoxedUint::one(), Limb::ZERO);
44+
assert_eq!(res, BoxedUint::zero());
45+
assert_eq!(carry, Limb::ONE);
46+
}
47+
48+
#[test]
49+
fn checked_add_ok() {
50+
let result = BoxedUint::zero().checked_add(&BoxedUint::one());
51+
assert_eq!(result.unwrap(), BoxedUint::one());
52+
}
53+
54+
#[test]
55+
fn checked_add_overflow() {
56+
let result = BoxedUint::max(Limb::BITS)
57+
.unwrap()
58+
.checked_add(&BoxedUint::one());
59+
assert!(!bool::from(result.is_some()));
60+
}
61+
}

src/boxed/uint/cmp.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//! [`BoxedUint`] comparisons.
2+
//!
3+
//! By default these are all constant-time and use the `subtle` crate.
4+
5+
use super::BoxedUint;
6+
use crate::Limb;
7+
use subtle::{Choice, ConstantTimeEq};
8+
9+
impl ConstantTimeEq for BoxedUint {
10+
#[inline]
11+
fn ct_eq(&self, other: &Self) -> Choice {
12+
let (shorter, longer) = Self::sort_by_precision(self, other);
13+
let mut ret = Choice::from(1u8);
14+
15+
for i in 0..longer.limbs.len() {
16+
let a = shorter.limbs.get(i).unwrap_or(&Limb::ZERO);
17+
let b = longer.limbs.get(i).unwrap_or(&Limb::ZERO);
18+
ret &= a.ct_eq(b);
19+
}
20+
21+
ret
22+
}
23+
}
24+
25+
impl Eq for BoxedUint {}
26+
impl PartialEq for BoxedUint {
27+
fn eq(&self, other: &Self) -> bool {
28+
self.ct_eq(other).into()
29+
}
30+
}
31+
32+
#[cfg(test)]
33+
mod tests {
34+
use super::BoxedUint;
35+
use subtle::ConstantTimeEq;
36+
37+
#[test]
38+
fn ct_eq() {
39+
let a = BoxedUint::zero();
40+
let b = BoxedUint::one();
41+
42+
assert!(bool::from(a.ct_eq(&a)));
43+
assert!(!bool::from(a.ct_eq(&b)));
44+
assert!(!bool::from(b.ct_eq(&a)));
45+
assert!(bool::from(b.ct_eq(&b)));
46+
}
47+
}

src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,16 @@
151151
//! [`Rem`]: core::ops::Rem
152152
//! [`Sub`]: core::ops::Sub
153153
154-
#[cfg(all(feature = "alloc", test))]
154+
#[cfg(feature = "alloc")]
155155
extern crate alloc;
156156

157157
#[macro_use]
158158
mod nlimbs;
159159

160160
#[cfg(feature = "generic-array")]
161161
mod array;
162+
#[cfg(feature = "alloc")]
163+
mod boxed;
162164
mod checked;
163165
mod ct_choice;
164166
mod limb;
@@ -179,6 +181,9 @@ pub use crate::{
179181
};
180182
pub use subtle;
181183

184+
#[cfg(feature = "alloc")]
185+
pub use crate::boxed::uint::BoxedUint;
186+
182187
#[cfg(feature = "generic-array")]
183188
pub use {
184189
crate::array::{ArrayDecoding, ArrayEncoding, ByteArray},

src/uint.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Big unsigned integers.
1+
//! Stack-allocated big unsigned integers.
22
33
#![allow(
44
clippy::needless_range_loop,
@@ -54,7 +54,7 @@ use serdect::serde::{Deserialize, Deserializer, Serialize, Serializer};
5454
#[cfg(feature = "zeroize")]
5555
use zeroize::DefaultIsZeroes;
5656

57-
/// Big unsigned integer.
57+
/// Stack-allocated big unsigned integer.
5858
///
5959
/// Generic over the given number of `LIMBS`
6060
///

0 commit comments

Comments
 (0)