From 8c8332361a604426f3bdc5168fbd1ea494a69a2e Mon Sep 17 00:00:00 2001 From: Tony Arcieri Date: Sun, 18 Oct 2026 10:50:46 -0700 Subject: [PATCH] Extract `os2ip(_zeroizing)` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Followup to #718 which extracts an Octet-String-to-Integer Primitive helper function(s) as described in RFC8017 §4.2. This makes it possible to write code that looks more like the algorithmic specifications, e.g. `draft-irtf-cfrg-rsa-guidance` --- src/algorithms/pad.rs | 77 ++++++++++++++++++++++++++++++++++++++++++- src/oaep.rs | 11 +++---- src/pkcs1v15.rs | 6 ++-- 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/algorithms/pad.rs b/src/algorithms/pad.rs index 3e35e772..3cdfad9d 100644 --- a/src/algorithms/pad.rs +++ b/src/algorithms/pad.rs @@ -1,5 +1,6 @@ -//! Special handling for converting the BigUint to u8 vectors +//! Conversion between integers and octet strings. +use crate::errors::Result; use alloc::vec::Vec; use crypto_bigint::{BoxedUint, Choice, CtEq}; use zeroize::Zeroizing; @@ -65,6 +66,45 @@ pub(crate) fn i2osp_zeroizing(input: BoxedUint, padded_len: usize) -> Zeroizing< out } +/// Octet-String-to-Integer Primitive (OS2IP) from [RFC8017 §4.2]. +/// +/// Interprets `input` as a big-endian nonnegative integer with `bits_precision` bits of storage +/// precision. `bits_precision` must be a multiple of [`Limb::BITS`]. This is a caller-established +/// precondition, checked by a debug assertion. Returns an error if `input` exceeds that storage +/// width, even if the excess bytes are zero. +/// +/// For RSA operations, use the public modulus's storage precision. Ciphertext length and +/// representative range checks remain the caller's responsibility. +/// +/// Use [`os2ip_zeroizing`] when decoding secret values. +/// +/// [RFC8017 §4.2]: https://www.rfc-editor.org/rfc/rfc8017.html#section-4.2 +#[inline] +pub(crate) fn os2ip(input: &[u8], bits_precision: u32) -> Result { + debug_assert_eq!(bits_precision % crypto_bigint::Limb::BITS, 0); + Ok(BoxedUint::from_be_slice(input, bits_precision)?) +} + +/// Computes [`os2ip`] (Octet-String-to-Integer Primitive) with zeroization of the input and output +/// buffers. +/// +/// Takes ownership of the input buffer, which is zeroized on drop, including on error. The +/// returned integer is also zeroized on drop. The same storage-width precondition and errors as +/// [`os2ip`] apply. +/// +/// [draft-irtf-cfrg-rsa-guidance-10 §6.1] requires operations on secret values to be constant-time. +/// Decoding in this function depends only on `input.len()` and `bits_precision`. To satisfy that +/// requirement, callers must ensure that both widths are independent of the input's value. +/// +/// [draft-irtf-cfrg-rsa-guidance-10 §6.1]: https://www.ietf.org/archive/id/draft-irtf-cfrg-rsa-guidance-10.html#section-6.1 +#[inline] +pub(crate) fn os2ip_zeroizing( + input: Zeroizing>, + bits_precision: u32, +) -> Result> { + os2ip(&input, bits_precision).map(Zeroizing::new) +} + #[cfg(test)] mod tests { use super::*; @@ -91,4 +131,39 @@ mod tests { fn be_pad_rejects_broken_precondition() { i2osp_zeroizing(BoxedUint::from(0x0102u64), 1); } + + #[test] + fn os2ip_round_trip() { + // A 2049-bit modulus uses 257 octets but has a larger, limb-aligned storage precision. + let bits_precision = BoxedUint::zero_with_precision(2049).bits_precision(); + for first_byte in [0, 1, 0xff] { + let mut input = vec![0xff; 257]; + input[0] = first_byte; + + let integer = os2ip(&input, bits_precision).unwrap(); + assert_eq!(integer.bits_precision(), bits_precision); + assert_eq!(i2osp(integer, input.len()), input); + + let integer = os2ip_zeroizing(Zeroizing::new(input.clone()), bits_precision).unwrap(); + assert_eq!(integer.bits_precision(), bits_precision); + let output = i2osp_zeroizing((*integer).clone(), input.len()); + assert_eq!(*output, input); + } + + let zero = os2ip(&[], 128).unwrap(); + assert_eq!(zero.bits_precision(), 128); + assert_eq!(i2osp(zero, 16), [0u8; 16]); + } + + #[test] + fn os2ip_rejects_oversized_input() { + // Leading zeros do not make an encoding fit the requested storage width. + let input = vec![0; 17]; + let expected = crate::errors::Error::Decode(crypto_bigint::DecodeError::InputSize); + assert_eq!(os2ip(&input, 128).unwrap_err(), expected); + assert_eq!( + os2ip_zeroizing(Zeroizing::new(input), 128).unwrap_err(), + expected + ); + } } diff --git a/src/oaep.rs b/src/oaep.rs index 39665805..73f51e18 100644 --- a/src/oaep.rs +++ b/src/oaep.rs @@ -12,13 +12,12 @@ pub use self::{decrypting_key::DecryptingKey, encrypting_key::EncryptingKey}; use alloc::boxed::Box; use alloc::vec::Vec; use core::fmt; -use crypto_bigint::BoxedUint; use digest::{Digest, FixedOutputReset}; use rand_core::TryCryptoRng; use crate::algorithms::oaep::*; -use crate::algorithms::pad::{i2osp, i2osp_zeroizing}; +use crate::algorithms::pad::{i2osp, i2osp_zeroizing, os2ip, os2ip_zeroizing}; use crate::algorithms::rsa::{rsa_decrypt_and_check, rsa_encrypt}; use crate::errors::{Error, Result}; use crate::key::{self, RsaPrivateKey, RsaPublicKey}; @@ -214,7 +213,7 @@ where let em = oaep_encrypt(rng, msg, digest, mgf_digest, label, pub_key.size())?; - let int = BoxedUint::from_be_slice(&em, pub_key.n_bits_precision())?; + let int = os2ip_zeroizing(em, pub_key.n_bits_precision())?; Ok(i2osp(rsa_encrypt(pub_key, &int)?, pub_key.size())) } @@ -240,7 +239,7 @@ where let em = oaep_encrypt_digest::<_, D, MGD>(rng, msg, label, pub_key.size())?; - let int = BoxedUint::from_be_slice(&em, pub_key.n_bits_precision())?; + let int = os2ip_zeroizing(em, pub_key.n_bits_precision())?; Ok(i2osp(rsa_encrypt(pub_key, &int)?, pub_key.size())) } @@ -274,7 +273,7 @@ where return Err(Error::Decryption); } - let ciphertext = BoxedUint::from_be_slice(ciphertext, priv_key.n_bits_precision())?; + let ciphertext = os2ip(ciphertext, priv_key.n_bits_precision())?; let em = rsa_decrypt_and_check(priv_key, rng, &ciphertext)?; let mut em = i2osp_zeroizing(em, priv_key.size()); @@ -312,7 +311,7 @@ where return Err(Error::Decryption); } - let ciphertext = BoxedUint::from_be_slice(ciphertext, priv_key.n_bits_precision())?; + let ciphertext = os2ip(ciphertext, priv_key.n_bits_precision())?; let em = rsa_decrypt_and_check(priv_key, rng, &ciphertext)?; let mut em = i2osp_zeroizing(em, priv_key.size()); diff --git a/src/pkcs1v15.rs b/src/pkcs1v15.rs index 3e564b09..59555362 100644 --- a/src/pkcs1v15.rs +++ b/src/pkcs1v15.rs @@ -44,7 +44,7 @@ use crypto_bigint::BoxedUint; use digest::Digest; use rand_core::TryCryptoRng; -use crate::algorithms::pad::{i2osp, i2osp_zeroizing}; +use crate::algorithms::pad::{i2osp, i2osp_zeroizing, os2ip, os2ip_zeroizing}; use crate::algorithms::pkcs1v15::*; use crate::algorithms::rsa::{rsa_decrypt_and_check, rsa_encrypt}; use crate::errors::{Error, Result}; @@ -155,7 +155,7 @@ fn encrypt( key::check_public(pub_key)?; let em = pkcs1v15_encrypt_pad(rng, msg, pub_key.size())?; - let int = BoxedUint::from_be_slice(&em, pub_key.n_bits_precision())?; + let int = os2ip_zeroizing(em, pub_key.n_bits_precision())?; Ok(i2osp(rsa_encrypt(pub_key, &int)?, pub_key.size())) } @@ -181,7 +181,7 @@ fn decrypt( return Err(Error::Decryption); } - let ciphertext = BoxedUint::from_be_slice(ciphertext, priv_key.n_bits_precision())?; + let ciphertext = os2ip(ciphertext, priv_key.n_bits_precision())?; let em = rsa_decrypt_and_check(priv_key, rng, &ciphertext)?; let em = i2osp_zeroizing(em, priv_key.size());