Skip to content

Commit 2d2bf43

Browse files
authored
Add RsaPrivateKey::from_components_with_large_exponent (#648)
Adds a `hazmat`-gated method to `RsaPrivateKey` to allow construction of keys with large public exponents, which was removed in #459. These are a potential DoS vector ("RSADoS") hence gating on `hazmat`: https://www.imperialviolet.org/2012/03/17/rsados.html
1 parent 4f23607 commit 2d2bf43

1 file changed

Lines changed: 229 additions & 47 deletions

File tree

src/key.rs

Lines changed: 229 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -339,20 +339,16 @@ impl RsaPrivateKey {
339339
)
340340
}
341341

342-
/// Constructs an RSA key pair from individual components:
342+
/// Private helper function that constructs an RSA key pair from components
343+
/// WITHOUT performing any validation or precomputation.
343344
///
344-
/// - `n`: RSA modulus
345-
/// - `e`: public exponent (i.e. encrypting exponent)
346-
/// - `d`: private exponent (i.e. decrypting exponent)
347-
/// - `primes`: prime factors of `n`: typically two primes `p` and `q`. More than two primes can
348-
/// be provided for multiprime RSA, however this is generally not recommended. If no `primes`
349-
/// are provided, a prime factor recovery algorithm will be employed to attempt to recover the
350-
/// factors (as described in [NIST SP 800-56B Revision 2] Appendix C.2). This algorithm only
351-
/// works if there are just two prime factors `p` and `q` (as opposed to multiprime), and `e`
352-
/// is between 2^16 and 2^256.
345+
/// This is the shared implementation used by `from_components` and
346+
/// `from_components_with_large_exponent`.
353347
///
354-
/// [NIST SP 800-56B Revision 2]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Br2.pdf
355-
pub fn from_components(
348+
/// Callers are responsible for:
349+
/// 1. Validating the key (to ensure precomputation won't fail)
350+
/// 2. Calling precompute() after validation
351+
fn from_components_inner(
356352
n: BoxedUint,
357353
e: BoxedUint,
358354
d: BoxedUint,
@@ -398,7 +394,7 @@ impl RsaPrivateKey {
398394
})
399395
.collect();
400396

401-
let mut k = RsaPrivateKey {
397+
let k = RsaPrivateKey {
402398
pubkey_components: RsaPublicKey {
403399
n: n_c,
404400
e,
@@ -409,6 +405,65 @@ impl RsaPrivateKey {
409405
precomputed: None,
410406
};
411407

408+
Ok(k)
409+
}
410+
411+
/// Constructs an RSA key pair from individual components, accepting exponents outside
412+
/// the normal size bounds.
413+
///
414+
/// See [`RsaPrivateKey::from_components`] for an explanation on the parameters.
415+
///
416+
/// # ⚠️ Warning: Hazmat!
417+
///
418+
/// This method accepts public exponents outside the standard bounds (2 ≤ e ≤ 2^33-1),
419+
/// but still performs full cryptographic validation to ensure the key is mathematically
420+
/// correct (i.e., verifies that de ≡ 1 mod λ(n)).
421+
///
422+
/// **Note:** This method is dangerous as it can be used as a DOS vector if used with
423+
/// untrusted input https://www.imperialviolet.org/2012/03/17/rsados.html
424+
///
425+
/// This is intended for interoperating with systems that use non-standard exponents
426+
/// or loading legacy keys. Use [`RsaPrivateKey::from_components`] for standard key
427+
/// construction.
428+
#[cfg(feature = "hazmat")]
429+
pub fn from_components_with_large_exponent(
430+
n: BoxedUint,
431+
e: BoxedUint,
432+
d: BoxedUint,
433+
primes: Vec<BoxedUint>,
434+
) -> Result<RsaPrivateKey> {
435+
let mut k = Self::from_components_inner(n, e, d, primes)?;
436+
437+
// Validate everything except exponent size bounds (to ensure precompute can't fail)
438+
validate_skip_exponent_size(&k)?;
439+
440+
// Precompute when possible, ignore error otherwise.
441+
k.precompute().ok();
442+
443+
Ok(k)
444+
}
445+
446+
/// Constructs an RSA key pair from individual components:
447+
///
448+
/// - `n`: RSA modulus
449+
/// - `e`: public exponent (i.e. encrypting exponent)
450+
/// - `d`: private exponent (i.e. decrypting exponent)
451+
/// - `primes`: prime factors of `n`: typically two primes `p` and `q`. More than two primes can
452+
/// be provided for multiprime RSA, however this is generally not recommended. If no `primes`
453+
/// are provided, a prime factor recovery algorithm will be employed to attempt to recover the
454+
/// factors (as described in [NIST SP 800-56B Revision 2] Appendix C.2). This algorithm only
455+
/// works if there are just two prime factors `p` and `q` (as opposed to multiprime), and `e`
456+
/// is between 2^16 and 2^256.
457+
///
458+
/// [NIST SP 800-56B Revision 2]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-56Br2.pdf
459+
pub fn from_components(
460+
n: BoxedUint,
461+
e: BoxedUint,
462+
d: BoxedUint,
463+
primes: Vec<BoxedUint>,
464+
) -> Result<RsaPrivateKey> {
465+
let mut k = Self::from_components_inner(n, e, d, primes)?;
466+
412467
// Always validate the key, to ensure precompute can't fail
413468
k.validate()?;
414469

@@ -562,36 +617,7 @@ impl RsaPrivateKey {
562617
/// Returns `Ok(())` if everything is good, otherwise an appropriate error.
563618
pub fn validate(&self) -> Result<()> {
564619
check_public(self)?;
565-
566-
// Check that Πprimes == n.
567-
let mut m = BoxedUint::one_with_precision(self.pubkey_components.n.bits_precision());
568-
let one = BoxedUint::one();
569-
for prime in &self.primes {
570-
// Any primes ≤ 1 will cause divide-by-zero panics later.
571-
if prime < &one {
572-
return Err(Error::InvalidPrime);
573-
}
574-
m = m.wrapping_mul(prime);
575-
}
576-
if m != *self.pubkey_components.n {
577-
return Err(Error::InvalidModulus);
578-
}
579-
580-
// Check that de ≡ 1 mod p-1, for each prime.
581-
// This implies that e is coprime to each p-1 as e has a multiplicative
582-
// inverse. Therefore e is coprime to lcm(p-1,q-1,r-1,...) =
583-
// exponent(ℤ/nℤ). It also implies that a^de ≡ a mod p as a^(p-1) ≡ 1
584-
// mod p. Thus a^de ≡ a mod n for all a coprime to n, as required.
585-
let de = self.d.mul(&self.pubkey_components.e);
586-
587-
for prime in &self.primes {
588-
let x = NonZero::new(prime.wrapping_sub(&BoxedUint::one())).unwrap();
589-
let congruence = de.rem_vartime(&x);
590-
if !bool::from(congruence.is_one()) {
591-
return Err(Error::InvalidExponent);
592-
}
593-
}
594-
620+
validate_private_key_parts(self)?;
595621
Ok(())
596622
}
597623

@@ -686,6 +712,24 @@ fn check_public_with_max_size(n: &BoxedUint, e: &BoxedUint, max_size: Option<usi
686712
}
687713
}
688714

715+
check_public_skip_exponent_size(n, e)?;
716+
717+
if e < &BoxedUint::from(RsaPublicKey::MIN_PUB_EXPONENT) {
718+
return Err(Error::PublicExponentTooSmall);
719+
}
720+
721+
if e > &BoxedUint::from(RsaPublicKey::MAX_PUB_EXPONENT) {
722+
return Err(Error::PublicExponentTooLarge);
723+
}
724+
725+
Ok(())
726+
}
727+
728+
/// Check that the public key is well formed, skipping exponent size bounds checks.
729+
///
730+
/// This is used internally by both public validation functions and hazmat APIs.
731+
#[inline]
732+
fn check_public_skip_exponent_size(n: &BoxedUint, e: &BoxedUint) -> Result<()> {
689733
if e >= n || n.is_even().into() || n.is_zero().into() {
690734
return Err(Error::InvalidModulus);
691735
}
@@ -694,17 +738,63 @@ fn check_public_with_max_size(n: &BoxedUint, e: &BoxedUint, max_size: Option<usi
694738
return Err(Error::InvalidExponent);
695739
}
696740

697-
if e < &BoxedUint::from(RsaPublicKey::MIN_PUB_EXPONENT) {
698-
return Err(Error::PublicExponentTooSmall);
741+
// Skip exponent size bounds checks
742+
Ok(())
743+
}
744+
745+
/// Helper function that validates the private key structure and cryptographic correctness.
746+
///
747+
/// This performs the structural and mathematical validation checks that are common to both
748+
/// `validate()` and `validate_skip_exponent_size()`.
749+
fn validate_private_key_parts(key: &RsaPrivateKey) -> Result<()> {
750+
// Check that Πprimes == n.
751+
let mut m = BoxedUint::one_with_precision(key.pubkey_components.n.bits_precision());
752+
let one = BoxedUint::one();
753+
for prime in &key.primes {
754+
// Any primes ≤ 1 will cause divide-by-zero panics later.
755+
if prime < &one {
756+
return Err(Error::InvalidPrime);
757+
}
758+
m = m.wrapping_mul(prime);
759+
}
760+
if m != *key.pubkey_components.n {
761+
return Err(Error::InvalidModulus);
699762
}
700763

701-
if e > &BoxedUint::from(RsaPublicKey::MAX_PUB_EXPONENT) {
702-
return Err(Error::PublicExponentTooLarge);
764+
// Check that de ≡ 1 mod p-1, for each prime.
765+
// This implies that e is coprime to each p-1 as e has a multiplicative
766+
// inverse. Therefore e is coprime to lcm(p-1,q-1,r-1,...) =
767+
// exponent(ℤ/nℤ). It also implies that a^de ≡ a mod p as a^(p-1) ≡ 1
768+
// mod p. Thus a^de ≡ a mod n for all a coprime to n, as required.
769+
let de = key.d.mul(&key.pubkey_components.e);
770+
771+
for prime in &key.primes {
772+
let x = NonZero::new(prime.wrapping_sub(&BoxedUint::one())).unwrap();
773+
let congruence = de.rem_vartime(&x);
774+
if !bool::from(congruence.is_one()) {
775+
return Err(Error::InvalidExponent);
776+
}
703777
}
704778

705779
Ok(())
706780
}
707781

782+
/// Validate the private key structure and cryptographic correctness,
783+
/// skipping only the exponent size bounds checks.
784+
///
785+
/// This performs all the same checks as `RsaPrivateKey::validate()` except
786+
/// it doesn't verify that the exponent is within the standard bounds.
787+
#[cfg(feature = "hazmat")]
788+
fn validate_skip_exponent_size(key: &RsaPrivateKey) -> Result<()> {
789+
// Check public key properties (without exponent size checks)
790+
check_public_skip_exponent_size(key.pubkey_components.n.as_ref(), &key.pubkey_components.e)?;
791+
792+
// Perform common private key validation
793+
validate_private_key_parts(key)?;
794+
795+
Ok(())
796+
}
797+
708798
#[cfg(feature = "serde")]
709799
impl Serialize for RsaPublicKey {
710800
fn serialize<S>(&self, serializer: S) -> core::prelude::v1::Result<S::Ok, S::Error>
@@ -1114,4 +1204,96 @@ mod tests {
11141204

11151205
assert_eq!(PrivateKeyParts::d(&key), PrivateKeyParts::d(&ref_key));
11161206
}
1207+
1208+
#[test]
1209+
#[cfg(feature = "hazmat")]
1210+
fn test_from_components_with_large_exponent() {
1211+
// Test that from_components_with_large_exponent accepts exponents outside normal bounds
1212+
// while from_components would reject them
1213+
1214+
use rand::rngs::ChaCha8Rng;
1215+
use rand_core::SeedableRng;
1216+
1217+
let mut rng = ChaCha8Rng::from_seed([42; 32]);
1218+
1219+
// Use an exponent larger than the normal maximum (2^33 - 1)
1220+
let large_e = BoxedUint::from((1u64 << 34) + 1); // 2^34 + 1 (odd number)
1221+
1222+
// Generate a key with this large exponent
1223+
let components =
1224+
generate_multi_prime_key_with_exp(&mut rng, 2, 1024, large_e.clone()).unwrap();
1225+
1226+
// Extract components
1227+
let n = components.n.get().clone();
1228+
let d = components.d;
1229+
let primes = components.primes;
1230+
1231+
// from_components should fail with PublicExponentTooLarge
1232+
let result =
1233+
RsaPrivateKey::from_components(n.clone(), large_e.clone(), d.clone(), primes.clone());
1234+
assert!(result.is_err());
1235+
assert_eq!(result.unwrap_err(), Error::PublicExponentTooLarge);
1236+
1237+
// from_components_with_large_exponent should succeed
1238+
let key_with_large_exp = RsaPrivateKey::from_components_with_large_exponent(
1239+
n.clone(),
1240+
large_e.clone(),
1241+
d.clone(),
1242+
primes.clone(),
1243+
);
1244+
assert!(key_with_large_exp.is_ok());
1245+
1246+
let key_with_large_exp = key_with_large_exp.unwrap();
1247+
assert_eq!(PublicKeyParts::e(&key_with_large_exp), &large_e);
1248+
assert_eq!(PublicKeyParts::n(&key_with_large_exp).as_ref(), &n);
1249+
assert_eq!(PrivateKeyParts::d(&key_with_large_exp), &d);
1250+
1251+
// Verify that the key is still cryptographically valid (de ≡ 1 mod λ(n))
1252+
// by checking that validation with skip_exponent_size passes
1253+
assert!(validate_skip_exponent_size(&key_with_large_exp).is_ok());
1254+
}
1255+
1256+
#[test]
1257+
#[cfg(feature = "hazmat")]
1258+
fn test_from_components_with_small_exponent() {
1259+
// Test that from_components_with_large_exponent accepts exponents below normal minimum
1260+
// (despite the name, it works for any non-standard exponent size)
1261+
1262+
use rand::rngs::ChaCha8Rng;
1263+
use rand_core::SeedableRng;
1264+
1265+
let mut rng = ChaCha8Rng::from_seed([43; 32]);
1266+
1267+
// Use an exponent smaller than the normal minimum (2)
1268+
let small_e = BoxedUint::from(1u64); // This is odd, which is required
1269+
1270+
// Generate a key with this small exponent
1271+
let components =
1272+
generate_multi_prime_key_with_exp(&mut rng, 2, 1024, small_e.clone()).unwrap();
1273+
1274+
// Extract components
1275+
let n = components.n.get().clone();
1276+
let d = components.d;
1277+
let primes = components.primes;
1278+
1279+
// from_components should fail
1280+
let result =
1281+
RsaPrivateKey::from_components(n.clone(), small_e.clone(), d.clone(), primes.clone());
1282+
assert!(result.is_err());
1283+
1284+
// from_components_with_large_exponent should succeed
1285+
let key_with_small_exp = RsaPrivateKey::from_components_with_large_exponent(
1286+
n.clone(),
1287+
small_e.clone(),
1288+
d.clone(),
1289+
primes,
1290+
);
1291+
assert!(key_with_small_exp.is_ok());
1292+
1293+
let key_with_small_exp = key_with_small_exp.unwrap();
1294+
assert_eq!(PublicKeyParts::e(&key_with_small_exp), &small_e);
1295+
1296+
// Verify that the key is cryptographically valid
1297+
assert!(validate_skip_exponent_size(&key_with_small_exp).is_ok());
1298+
}
11171299
}

0 commit comments

Comments
 (0)