Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ zeroize = { version = "1.8", default-features = false, features = ["alloc"] }
[dependencies]
const-oid = { version = "0.10", default-features = false }
crypto-bigint = { workspace = true }
crypto-common = { version = "0.2", default-features = false, features = ["rand_core"] }
crypto-primes = { version = "0.7", default-features = false }
digest = { version = "0.11", default-features = false, features = ["alloc", "oid"] }
hmac = { version = "0.13", default-features = false, features = ["zeroize"] }
Expand All @@ -36,7 +37,6 @@ signature = { version = "3", default-features = false, features = ["alloc", "dig
zeroize = { workspace = true, default-features = true }

# optional dependencies
crypto-common = { version = "0.2", optional = true, features = ["getrandom"] }
pkcs1 = { version = "0.8", optional = true, default-features = false, features = ["alloc", "pem"] }
pkcs8 = { version = "0.11", optional = true, default-features = false, features = ["alloc", "pem"] }
serdect = { version = "0.4", optional = true }
Expand Down Expand Up @@ -67,9 +67,8 @@ std = ["pkcs1?/std", "pkcs8?/std"]

encoding = ["dep:pkcs1", "dep:pkcs8", "dep:spki"]
hazmat = []
# PKCS #1 v1.5 is only supported as opt-in basis for legacy protocols
pkcs1v15 = []
getrandom = ["crypto-bigint/getrandom", "crypto-common"]
pkcs1v15 = [] # PKCS #1 v1.5 is only supported as opt-in basis for legacy protocols
getrandom = ["crypto-bigint/getrandom", "crypto-common/getrandom"]
serde = ["encoding", "dep:serde", "dep:serdect", "crypto-bigint/serde"]
pkcs5 = ["pkcs8/encryption"]

Expand Down
22 changes: 14 additions & 8 deletions src/algorithms/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pub(crate) fn generate_multi_prime_key_with_exp<R: TryCryptoRng + ?Sized>(
#[allow(clippy::cast_possible_truncation, reason = "TODO")]
for (i, prime) in primes.iter_mut().enumerate() {
let bits = (todo / (nprimes - i)) as u32;
*prime = generate_prime_with_rng(rng, bits)?;
*prime = generate_prime_with_rng(rng, bits).map_err(|_| Error::Rng)?;
todo -= prime.bits() as usize;
}

Expand Down Expand Up @@ -114,29 +114,35 @@ pub(crate) fn generate_multi_prime_key_with_exp<R: TryCryptoRng + ?Sized>(
})
}

fn generate_prime_with_rng<R: TryCryptoRng + ?Sized>(
/// Generate an odd prime with its top two bits set.
///
/// # Panics
/// Panics if `bit_length` is less than two.
pub(crate) fn generate_prime_with_rng<R: TryCryptoRng + ?Sized>(
rng: &mut R,
bit_length: u32,
) -> Result<BoxedUint> {
) -> core::result::Result<BoxedUint, R::Error> {
let bit_length = NonZeroU32::new(bit_length)
.filter(|bits| bits.get() >= 2)
.ok_or(Error::InvalidArguments)?;
.expect("prime bit length must be at least two");

loop {
// Sample directly because `SmallFactorsSieveFactory` requires an infallible RNG.
let mut start =
BoxedUint::try_random_bits(rng, bit_length.get()).map_err(|err| match err {
RandomBitsError::RandCore(_) => Error::Rng,
_ => Error::Internal, // The precision is determined by the requested bit length.
RandomBitsError::RandCore(err) => err,
// The precision is determined by the requested bit length.
_ => unreachable!("random integer has the requested precision"),
})?;

// Match `SetBits::TwoMsb`: make the candidate odd and set its top two bits.
start.set_bit_vartime(0, true);
start.set_bit_vartime(bit_length.get() - 1, true);
start.set_bit_vartime(bit_length.get() - 2, true);

let mut sieve =
SmallFactorsSieve::new(start, bit_length, false).map_err(|_| Error::Internal)?;
let Ok(mut sieve) = SmallFactorsSieve::new(start, bit_length, false) else {
unreachable!("candidate fits the bit length");
};
if let Some(prime) = sieve.find(|candidate| is_prime(Flavor::Any, candidate)) {
return Ok(prime);
}
Expand Down
16 changes: 16 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@
//!
//! # Usage
//!
//! ## Key generation
//!
//! [`Generate`] creates 3072-bit RSA keys with public exponent 65537. This targets an estimated
//! 128-bit security strength; see [`RsaPrivateKey::DEFAULT_BITS`] for the rationale. Use
//! [`RsaPrivateKey::new`] to select a different key size.
//!
//! ```
//! use rsa::{Generate, RsaPrivateKey};
//!
//! let private_key = RsaPrivateKey::generate_from_rng(&mut rand::rng());
//! ```
//!
//! With the `getrandom` feature (enabled by default), `Generate::generate` and
//! `Generate::try_generate` use the operating system RNG.
//!
//! ## OAEP encryption
//!
//! ```
Expand Down Expand Up @@ -227,6 +242,7 @@ extern crate alloc;
extern crate std;

pub use crypto_bigint::BoxedUint;
pub use crypto_common::Generate;
pub use rand_core;
pub use signature;

Expand Down
16 changes: 15 additions & 1 deletion src/pkcs1v15/signing_key.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{Signature, VerifyingKey, pkcs1v15_generate_prefix, sign, sign_with_default_rng};
use crate::{Result, RsaPrivateKey};
use crate::{Generate, Result, RsaPrivateKey};
use alloc::vec::Vec;
use const_oid::AssociatedOid;
use core::marker::PhantomData;
Expand Down Expand Up @@ -100,6 +100,20 @@ where
}
}

/// Generate a key with the default RSA parameters and a prefix for the digest `D`.
///
/// See [`RsaPrivateKey::DEFAULT_BITS`] for the default modulus size.
impl<D> Generate for SigningKey<D>
where
D: Digest + AssociatedOid,
{
fn try_generate_from_rng<R: TryCryptoRng + ?Sized>(
rng: &mut R,
) -> core::result::Result<Self, R::Error> {
RsaPrivateKey::try_generate_from_rng(rng).map(Self::new)
}
}

//
// `*Signer` trait impls
//
Expand Down
34 changes: 32 additions & 2 deletions src/private_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use alloc::vec::Vec;
use core::fmt;
use core::hash::{Hash, Hasher};

use crate::algorithms::generate::generate_multi_prime_key_with_exp;
use crate::algorithms::generate::{generate_multi_prime_key_with_exp, generate_prime_with_rng};
use crate::algorithms::rsa::{
compute_modulus, compute_private_exponent_carmicheal, compute_private_exponent_euler_totient,
recover_primes, to_monty,
Expand All @@ -14,7 +14,7 @@ use crate::dummy_rng::DummyRng;
use crate::errors::{Error, Result};
use crate::traits::keys::{CrtValue, PrivateKeyParts, PublicKeyParts};
use crate::traits::{PaddingScheme, SignatureScheme};
use crate::{RsaPublicKey, public_key};
use crate::{Generate, RsaPublicKey, public_key};

use crypto_bigint::{
BoxedUint, Choice, ConcatenatingMul, CtEq, CtLt, Integer, NonZero, Odd, Resize,
Expand Down Expand Up @@ -141,6 +141,12 @@ impl PublicKeyParts for RsaPrivateKey {
}

impl RsaPrivateKey {
/// Default modulus size in bits used by [`Generate`].
///
/// A 3072-bit RSA modulus provides an estimated 128-bit security strength according to
/// [NIST SP 800-57 Part 1 Rev. 5, Table 2](https://doi.org/10.6028/NIST.SP.800-57pt1r5).
pub const DEFAULT_BITS: usize = 3072;

/// Default exponent for RSA keys.
const EXP: u64 = 65537;

Expand Down Expand Up @@ -619,6 +625,30 @@ impl RsaPrivateKey {
}
}

/// Generate a key with a [`Self::DEFAULT_BITS`]-bit modulus and public exponent 65537.
impl Generate for RsaPrivateKey {
fn try_generate_from_rng<R: TryCryptoRng + ?Sized>(
rng: &mut R,
) -> core::result::Result<Self, R::Error> {
let Ok(prime_bits) = u32::try_from(Self::DEFAULT_BITS.div_euclid(2)) else {
unreachable!("default prime size fits in u32");
};

loop {
// Setting the top two bits of both primes ensures a full-size modulus.
let p = generate_prime_with_rng(rng, prime_bits)?;
let q = generate_prime_with_rng(rng, prime_bits)?;

match Self::from_primes(vec![p, q], Self::EXP.into()) {
Ok(key) => return Ok(key),
// Retry if the primes coincide or the public exponent is not invertible.
Err(Error::InvalidPrime) => continue,
Err(err) => unreachable!("invalid generated RSA key: {err}"),
}
}
}
}

impl PrivateKeyParts for RsaPrivateKey {
fn d(&self) -> &BoxedUint {
&self.d
Expand Down
16 changes: 15 additions & 1 deletion src/pss/signing_key.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{Signature, VerifyingKey, sign_digest};
use crate::{Result, RsaPrivateKey};
use crate::{Generate, Result, RsaPrivateKey};
use core::marker::PhantomData;
use digest::{Digest, FixedOutputReset, Update};
use rand_core::TryCryptoRng;
Expand Down Expand Up @@ -97,6 +97,20 @@ where
}
}

/// Generate a key with the default RSA parameters and a salt length equal to the digest output size.
///
/// See [`RsaPrivateKey::DEFAULT_BITS`] for the default modulus size.
impl<D> Generate for SigningKey<D>
where
D: Digest,
{
fn try_generate_from_rng<R: TryCryptoRng + ?Sized>(
rng: &mut R,
) -> core::result::Result<Self, R::Error> {
RsaPrivateKey::try_generate_from_rng(rng).map(Self::new)
}
}

//
// `*Signer` trait impls
//
Expand Down
137 changes: 137 additions & 0 deletions tests/generate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//! Default RSA key generation and fallible RNG support.

use rand::rngs::ChaCha8Rng;
use rand_core::{SeedableRng, TryCryptoRng, TryRng};
#[cfg(feature = "pkcs1v15")]
use rsa::pkcs1v15;
use rsa::{
BoxedUint, Error, Generate, RsaPrivateKey, pss,
traits::{PrivateKeyParts, PublicKeyParts},
};
use sha2::Sha256;
use signature::{Keypair, RandomizedSigner, Verifier};

fn rng() -> ChaCha8Rng {
ChaCha8Rng::from_seed([42; 32])
}

fn check_key(key: &RsaPrivateKey) {
assert_eq!(RsaPrivateKey::DEFAULT_BITS, 3072);
assert_eq!(key.n().bits(), 3072);
assert_eq!(key.e(), &BoxedUint::from(65537u64));
assert_eq!(key.primes().len(), 2);
assert_ne!(key.primes()[0], key.primes()[1]);
assert!(key.dp().is_some());
assert!(key.dq().is_some());
assert!(key.qinv().is_some());
key.validate().unwrap();
}

#[test]
fn generate_private_key() {
let key = RsaPrivateKey::generate_from_rng(&mut rng());
check_key(&key);
assert_eq!(
key,
RsaPrivateKey::new(&mut rng(), RsaPrivateKey::DEFAULT_BITS).unwrap()
);
}

#[test]
#[cfg(feature = "pkcs1v15")]
fn generate_pkcs1v15_signing_key() {
let key = pkcs1v15::SigningKey::<Sha256>::try_generate_from_rng(&mut rng()).unwrap();
check_key(key.as_ref());
let signature = key.try_sign_with_rng(&mut rng(), b"message").unwrap();
// A verifying key with the standard digest prefix rejects unprefixed signatures.
pkcs1v15::VerifyingKey::<Sha256>::new(key.as_ref().to_public_key())
.verify(b"message", &signature)
.unwrap();
}

#[test]
fn generate_pss_signing_key() {
let key = pss::SigningKey::<Sha256>::try_generate_from_rng(&mut rng()).unwrap();
check_key(key.as_ref());
assert_eq!(key.salt_len(), 32);
let signature = key.try_sign_with_rng(&mut rng(), b"message").unwrap();
key.verifying_key().verify(b"message", &signature).unwrap();
}

// A non-Copy, non-Clone error carrying data makes error erasure observable.
#[derive(Debug, PartialEq)]
struct Exhausted(usize);

impl core::fmt::Display for Exhausted {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "entropy exhausted after {} bytes", self.0)
}
}

impl core::error::Error for Exhausted {}

struct RepeatingRng {
budget: usize,
used: usize,
}

impl TryRng for RepeatingRng {
type Error = Exhausted;

fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
let mut bytes = [0; 4];
self.try_fill_bytes(&mut bytes)?;
Ok(u32::from_le_bytes(bytes))
}

fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
let mut bytes = [0; 8];
self.try_fill_bytes(&mut bytes)?;
Ok(u64::from_le_bytes(bytes))
}

fn try_fill_bytes(&mut self, bytes: &mut [u8]) -> Result<(), Self::Error> {
if bytes.len() > self.budget {
return Err(Exhausted(self.used));
}
self.budget -= bytes.len();
self.used += bytes.len();
bytes.fill(0xaa);
Ok(())
}
}

impl TryCryptoRng for RepeatingRng {}

fn check_rng_error<T: Generate + core::fmt::Debug>(budget: usize) {
let mut rng = RepeatingRng { budget, used: 0 };
let rng: &mut dyn TryCryptoRng<Error = Exhausted> = &mut rng;
assert_eq!(
T::try_generate_from_rng(rng).unwrap_err(),
Exhausted(budget)
);
}

#[test]
fn generate_preserves_rng_errors() {
// Fail before sampling, between primes, and after rejecting equal primes. The last case
// ensures `Generate` retries an invalid candidate instead of returning it or panicking.
for budget in [0, 192, 384] {
check_rng_error::<RsaPrivateKey>(budget);
}
#[cfg(feature = "pkcs1v15")]
check_rng_error::<pkcs1v15::SigningKey<Sha256>>(0);
check_rng_error::<pss::SigningKey<Sha256>>(0);

// Existing constructors continue mapping RNG failures to `rsa::Error::Rng`.
assert_eq!(
RsaPrivateKey::new(&mut RepeatingRng { budget: 0, used: 0 }, 3072),
Err(Error::Rng)
);
}

#[test]
#[cfg(feature = "getrandom")]
fn generate_with_system_rng() {
check_key(&RsaPrivateKey::try_generate().unwrap());
}
Loading