From 044a284267a08687517435120bc9c5f4d461a8af Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Tue, 12 May 2026 16:41:26 -0600 Subject: [PATCH] [WIP] Add `BoxedUint::{from_be_slice_truncated, from_le_slice_truncated}` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds infallible constructors for `BoxedUint` that handle the error case for the current `from_be_slice`/`from_le_slice` by truncating the input. My real goal is to be able to add some slice constructors to the `Encoding` trait that can actually provide equivalent semantics between `Uint` and `BoxedUint` and also ensure construction of modulus-sized `BoxedUint`s. A truncating behavior like this eliminates the error case and allows for an infallible constructor. As it were, it's largely mimicking the `bits2int` function described in RFC6979 ยง 2.3.2 which is what I'd like to implement using `Uint` for ECDSA an `BoxedUint` for DSA. An infallible constructor lets us compose both `from_*e_slice_vartime` and `from_*e_slice` in terms of `from_*e_slice_truncated`, whee the former is also infallible and picks its capacity/precision on-the-fly based on the input data (e.g. we need that to decode the public modulus and figure out what precision we're working with). --- src/limb/encoding.rs | 28 +++++++-------- src/uint/boxed/encoding.rs | 71 +++++++++++++++++++++++++------------- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/src/limb/encoding.rs b/src/limb/encoding.rs index a076c8b32..b026784ca 100644 --- a/src/limb/encoding.rs +++ b/src/limb/encoding.rs @@ -4,10 +4,7 @@ use super::{Limb, Word}; use crate::Encoding; impl Encoding for Limb { - cpubits::cpubits! { - 32 => { type Repr = [u8; 4]; } - 64 => { type Repr = [u8; 8]; } - } + type Repr = [u8; Self::BYTES]; #[inline] fn from_be_bytes(bytes: Self::Repr) -> Self { @@ -32,32 +29,31 @@ impl Encoding for Limb { #[cfg(feature = "alloc")] impl Limb { - /// Decode limb from a big endian byte slice. + /// Decode limb from a big endian byte slice, which may be shorter than [`Limb::BYTES`]. /// /// # Panics /// - if the slice is larger than [`Limb::Repr`]. pub(crate) fn from_be_slice(bytes: &[u8]) -> Self { + let offset = Self::BYTES + .checked_sub(bytes.len()) + .expect("The given slice is larger than Limb::BYTES"); + let mut repr = Self::ZERO.to_be_bytes(); - let repr_len = repr.len(); - assert!( - bytes.len() <= repr_len, - "The given slice is larger than the limb size" - ); - repr[(repr_len - bytes.len())..].copy_from_slice(bytes); + repr[offset..].copy_from_slice(bytes); Self::from_be_bytes(repr) } - /// Decode limb from a little endian byte slice. + /// Decode limb from a little endian byte slice, which may be shorter than [`Limb::BYTES`]. /// /// # Panics /// - if the slice is not the same size as [`Limb::Repr`]. pub(crate) fn from_le_slice(bytes: &[u8]) -> Self { - let mut repr = Self::ZERO.to_le_bytes(); - let repr_len = repr.len(); assert!( - bytes.len() <= repr_len, - "The given slice is larger than the limb size" + bytes.len() <= Self::BYTES, + "The given slice is larger than Limb::BYTES" ); + + let mut repr = Self::ZERO.to_be_bytes(); repr[..bytes.len()].copy_from_slice(bytes); Self::from_le_bytes(repr) } diff --git a/src/uint/boxed/encoding.rs b/src/uint/boxed/encoding.rs index eb93f94a2..76d33945c 100644 --- a/src/uint/boxed/encoding.rs +++ b/src/uint/boxed/encoding.rs @@ -13,8 +13,8 @@ impl BoxedUint { /// The `bits_precision` argument represents the precision of the resulting integer, which is /// fixed as this type is not arbitrary-precision. /// - /// The new [`BoxedUint`] will be created with `bits_precision` - /// rounded up to a multiple of [`Limb::BITS`]. + /// The new [`BoxedUint`] will be created with `bits_precision` rounded up to a multiple of + /// [`Limb::BITS`]. /// /// # Errors /// - Returns [`DecodeError::InputSize`] if the length of `bytes` is larger than @@ -26,11 +26,7 @@ impl BoxedUint { return Err(DecodeError::InputSize); } - let mut ret = Self::zero_with_precision(bits_precision); - - for (chunk, limb) in bytes.rchunks(Limb::BYTES).zip(ret.limbs.iter_mut()) { - *limb = Limb::from_be_slice(chunk); - } + let ret = Self::from_be_slice_truncated(bytes, bits_precision); if bits_precision < ret.bits() { return Err(DecodeError::Precision); @@ -39,6 +35,27 @@ impl BoxedUint { Ok(ret) } + /// Create a new [`BoxedUint`] from the provided big endian bytes, zero padding if necessary, + /// and truncating to the least significant bytes in the event the given amount of data exceeds + /// `bits_precision`. + #[must_use] + pub fn from_be_slice_truncated(mut bytes: &[u8], bits_precision: u32) -> Self { + let bytes_precision = bitlen::to_bytes(bits_precision); + + // TODO(tarcieri): mask bits in the most significant byte if necessary + if bytes.len() > bytes_precision { + bytes = &bytes[..bytes_precision]; + } + + let mut ret = Self::zero_with_precision(bits_precision); + + for (chunk, limb) in bytes.rchunks(Limb::BYTES).zip(ret.limbs.iter_mut()) { + *limb = Limb::from_be_slice(chunk); + } + + ret + } + /// Create a new [`BoxedUint`] from the provided big endian bytes, automatically selecting its /// precision based on the size of the input. /// @@ -47,12 +64,8 @@ impl BoxedUint { /// /// When working with secret values, use [`BoxedUint::from_be_slice`]. #[must_use] - #[allow(clippy::cast_possible_truncation, clippy::missing_panics_doc)] pub fn from_be_slice_vartime(bytes: &[u8]) -> Self { - let bits_precision = bitlen::from_bytes(bytes.len()); - - // TODO(tarcieri): avoid panic - Self::from_be_slice(bytes, bits_precision).expect("precision should be large enough") + Self::from_be_slice_truncated(bytes, bitlen::from_bytes(bytes.len())) } /// Create a new [`BoxedUint`] from the provided little endian bytes. @@ -73,11 +86,7 @@ impl BoxedUint { return Err(DecodeError::InputSize); } - let mut ret = Self::zero_with_precision(bits_precision); - - for (chunk, limb) in bytes.chunks(Limb::BYTES).zip(ret.limbs.iter_mut()) { - *limb = Limb::from_le_slice(chunk); - } + let ret = Self::from_le_slice_truncated(bytes, bits_precision); if bits_precision < ret.bits() { return Err(DecodeError::Precision); @@ -86,6 +95,25 @@ impl BoxedUint { Ok(ret) } + /// Create a new [`BoxedUint`] from the provided little endian bytes, zero padding if necessary, + /// and truncating to the least significant bytes in the event the given amount of data exceeds + /// `bits_precision`. + #[must_use] + pub fn from_le_slice_truncated(mut bytes: &[u8], bits_precision: u32) -> Self { + let offset = bytes.len().saturating_sub(bitlen::to_bytes(bits_precision)); + + // TODO(tarcieri): mask bits in the most significant byte if necessary + bytes = &bytes[offset..]; + + let mut ret = Self::zero_with_precision(bits_precision); + + for (chunk, limb) in bytes.chunks(Limb::BYTES).zip(ret.limbs.iter_mut()) { + *limb = Limb::from_le_slice(chunk); + } + + ret + } + /// Create a new [`BoxedUint`] from the provided little endian bytes, automatically selecting /// its precision based on the size of the input. /// @@ -94,12 +122,8 @@ impl BoxedUint { /// /// When working with secret values, use [`BoxedUint::from_le_slice`]. #[must_use] - #[allow(clippy::cast_possible_truncation, clippy::missing_panics_doc)] pub fn from_le_slice_vartime(bytes: &[u8]) -> Self { - let bits_precision = bitlen::from_bytes(bytes.len()); - - // TODO(tarcieri): avoid panic - Self::from_le_slice(bytes, bits_precision).expect("precision should be large enough") + Self::from_le_slice_truncated(bytes, bitlen::from_bytes(bytes.len())) } /// Serialize this [`BoxedUint`] as big-endian. @@ -163,9 +187,8 @@ impl BoxedUint { /// # Panics /// - if hex string is not the expected size #[must_use] - #[allow(clippy::integer_division_remainder_used, reason = "public parameter")] pub fn from_be_hex(hex: &str, bits_precision: u32) -> CtOption { - let nlimbs = (bits_precision / Limb::BITS) as usize; + let nlimbs = bitlen::to_limbs(bits_precision); let bytes = hex.as_bytes(); assert_eq!(