diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2ae3143..a1c7b4a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: with: toolchain: ${{ matrix.rust }} targets: ${{ matrix.target }} - - run: cargo build --no-default-features --target ${{ matrix.target }} + - run: cargo build --no-default-features --features pkcs1v15 --target ${{ matrix.target }} test: runs-on: ubuntu-latest @@ -83,7 +83,7 @@ jobs: toolchain: nightly-2026-11-01 - run: cargo update -Z minimal-versions - uses: dtolnay/rust-toolchain@stable - - run: cargo test --release --features getrandom,serde,pkcs5 + - run: cargo test --release --features getrandom,serde,pkcs5,pkcs1v15 nightly: runs-on: ubuntu-latest @@ -95,4 +95,4 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: nightly-2026-11-01 - - run: cargo build --benches + - run: cargo build --features pkcs1v15 --benches diff --git a/Cargo.toml b/Cargo.toml index d086d8c8..c50628af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +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"] serde = ["encoding", "dep:serde", "dep:serdect", "crypto-bigint/serde"] pkcs5 = ["pkcs8/encryption"] diff --git a/README.md b/README.md index c5a5c9c0..3a2da198 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ specified in [PKCS#1 v2.2 (RFC8017)][RFC8017]. | RSASSA-PSS | Signatures | [RFC8017 §8.1][RSASSA-PSS] | | RSASSA-PKCS1-v1_5 | Signatures | [RFC8017 §8.2][RSASSA-PKCS1-v1_5] | -PKCS#1 v1.5 encryption is provided for compatibility with legacy applications only. -Prefer OAEP and PSS for new encryption applications. +PKCS#1 v1.5 is provided as opt-in through feature `pkcs1v15` for compatibility + with legacy applications only. Prefer OAEP and PSS for new applications. See the security warning below for known implementation vulnerabilities. @@ -54,14 +54,25 @@ assert_eq!(&data[..], &dec_data[..]); Though this crate has been [previously audited][audit] the implementation has diverged significantly since then, and several security issues have been -discovered which we are still working to address. +discovered which have been resolved in the 0.10 release. See the [open security issues] on our issue tracker for other known problems. This crate was previously vulnerable to the [Marvin Attack] which could enable -private key recovery by a network attacker (see [RUSTSEC-2023-0071]). + private key recovery by a network attacker (see [RUSTSEC-2023-0071]). You can follow our work on constant-time hardening in [#711]. +Users are advised to migrate to 0.10 in order to avoid the discovered side channels. + +## ⚠️ PCKS#1 1.5 Warning + +Additionally legacy PKCS#1 1.5 implementations, including the implementation of +it within this crate, has seen a long running source of Bleichenbacher and +[Marvin Attacks](https://eprint.iacr.org/2023/1442) related vulnerabilities. + +For this reason, users who require the use of this legacy padding scheme i.e. + in legacy protocols must explicitly enable the `pkcs1v15` feature to use it. + ## Minimum Supported Rust Version (MSRV) Policy MSRV increases are not considered breaking changes and can happen in patch diff --git a/marvin-toolkit/Cargo.toml b/marvin-toolkit/Cargo.toml index 8a741d15..c18cda73 100644 --- a/marvin-toolkit/Cargo.toml +++ b/marvin-toolkit/Cargo.toml @@ -9,4 +9,4 @@ publish = false anyhow = "1" clap = { version = "4", features = ["derive"] } rand = { workspace = true } -rsa = { path = ".." } +rsa = { path = "..", features = ["pkcs1v15"] } diff --git a/src/algorithms.rs b/src/algorithms.rs index 61748ff8..f18d5808 100644 --- a/src/algorithms.rs +++ b/src/algorithms.rs @@ -18,7 +18,9 @@ pub(crate) mod blinding; pub(crate) mod generate; pub(crate) mod oaep; pub(crate) mod pad; -pub(crate) mod pkcs1v15; pub(crate) mod pss; pub(crate) mod reduction; pub(crate) mod rsa; + +#[cfg(feature = "pkcs1v15")] +pub(crate) mod pkcs1v15; diff --git a/src/lib.rs b/src/lib.rs index 926804c0..fd060be8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,7 +47,8 @@ //! pkcs1v15 module. //! //! -//! ``` +#![cfg_attr(feature = "pkcs1v15", doc = "```")] +#![cfg_attr(not(feature = "pkcs1v15"), doc = "```ignore")] //! use rsa::{RsaPrivateKey, RsaPublicKey, Pkcs1v15Encrypt}; //! //! let mut rng = rand::rng(); @@ -73,7 +74,8 @@ //! See security notes in the pkcs1v15 module. //! //! -//! ``` +#![cfg_attr(feature = "pkcs1v15", doc = "```")] +#![cfg_attr(not(feature = "pkcs1v15"), doc = "```ignore")] //! use rsa::RsaPrivateKey; //! use rsa::pkcs1v15::{SigningKey, VerifyingKey}; //! use rsa::signature::{Keypair, RandomizedSigner, SignatureEncoding, Verifier}; @@ -231,10 +233,12 @@ pub use signature; mod algorithms; pub mod errors; pub mod oaep; -pub mod pkcs1v15; pub mod pss; pub mod traits; +#[cfg(feature = "pkcs1v15")] +pub mod pkcs1v15; + #[cfg(not(feature = "getrandom"))] mod dummy_rng; mod encoding; @@ -250,12 +254,14 @@ pub use sha2; pub use crate::{ errors::{Error, Result}, oaep::Oaep, - pkcs1v15::{Pkcs1v15Encrypt, Pkcs1v15Sign}, private_key::RsaPrivateKey, pss::Pss, public_key::RsaPublicKey, traits::keys::CrtValue, }; +#[cfg(feature = "pkcs1v15")] +pub use crate::pkcs1v15::{Pkcs1v15Encrypt, Pkcs1v15Sign}; + #[cfg(feature = "hazmat")] pub mod hazmat; diff --git a/tests/key_size.rs b/tests/key_size.rs index 403c13a5..717753a7 100644 --- a/tests/key_size.rs +++ b/tests/key_size.rs @@ -3,7 +3,10 @@ use crypto_bigint::Resize; use rand::rngs::ChaCha8Rng; use rand_core::SeedableRng; -use rsa::{BoxedUint, Error, Oaep, Pkcs1v15Encrypt, RsaPublicKey}; +use rsa::{BoxedUint, Error, Oaep, RsaPublicKey}; + +#[cfg(feature = "pkcs1v15")] +use rsa::Pkcs1v15Encrypt; // Public-key construction checks the modulus size and parity, not its factorization. fn modulus(bits: u32) -> BoxedUint { @@ -51,7 +54,10 @@ fn explicit_public_key_size_limits() { // Operation-time checks must respect the constructor's explicit size override. let mut rng = ChaCha8Rng::from_seed([42; 32]); + + #[cfg(feature = "pkcs1v15")] key.encrypt(&mut rng, Pkcs1v15Encrypt, b"test").unwrap(); + key.encrypt(&mut rng, Oaep::::new(), b"test") .unwrap(); } diff --git a/tests/pkcs1v15.rs b/tests/pkcs1v15.rs index 8d8ca4c2..af414d5e 100644 --- a/tests/pkcs1v15.rs +++ b/tests/pkcs1v15.rs @@ -1,4 +1,5 @@ -//! PKCS#1 v1.5 tests. +//! PKCS #1 v1.5 tests +#![cfg(feature = "pkcs1v15")] #[cfg(feature = "encoding")] #[test] diff --git a/tests/pkcs8.rs b/tests/pkcs8.rs index f19b19c7..3becf5f4 100644 --- a/tests/pkcs8.rs +++ b/tests/pkcs8.rs @@ -5,20 +5,25 @@ use crypto_bigint::{BoxedUint, CtEq}; use hex_literal::hex; use rsa::{ - RsaPrivateKey, RsaPublicKey, pkcs1v15, + RsaPrivateKey, RsaPublicKey, pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey}, pss, traits::{PrivateKeyParts, PublicKeyParts}, }; use sha2::Sha256; +#[cfg(feature = "pkcs1v15")] +use rsa::pkcs1v15; + #[cfg(feature = "encoding")] use rsa::pkcs8::LineEnding; /// RSA-2048 PKCS#8 private key encoded as ASN.1 DER +#[cfg(feature = "pkcs1v15")] const RSA_2048_PRIV_DER: &[u8] = include_bytes!("examples/pkcs8/rsa2048-priv.der"); /// RSA-2048 `SubjectPublicKeyInfo` encoded as ASN.1 DER +#[cfg(feature = "pkcs1v15")] const RSA_2048_PUB_DER: &[u8] = include_bytes!("examples/pkcs8/rsa2048-pub.der"); /// RSA-2048 PKCS#8 private key encoded as PEM @@ -36,6 +41,7 @@ const RSA_2048_PSS_PRIV_DER: &[u8] = include_bytes!("examples/pkcs8/rsa2048-rfc9 const RSA_2048_PSS_PUB_DER: &[u8] = include_bytes!("examples/pkcs8/rsa2048-rfc9421-pub.der"); #[test] +#[cfg(feature = "pkcs1v15")] fn decode_rsa2048_priv_der() { let key = RsaPrivateKey::from_pkcs8_der(RSA_2048_PRIV_DER).unwrap(); @@ -96,6 +102,7 @@ fn decode_rsa2048_priv_der() { } #[test] +#[cfg(feature = "pkcs1v15")] fn decode_rsa2048_pub_der() { let key = RsaPublicKey::from_public_key_der(RSA_2048_PUB_DER).unwrap(); @@ -203,6 +210,7 @@ fn decode_rsa2048_pss_pub_der() { } #[test] +#[cfg(feature = "pkcs1v15")] fn encode_rsa2048_priv_der() { let key = RsaPrivateKey::from_pkcs8_der(RSA_2048_PRIV_DER).unwrap(); let der = key.to_pkcs8_der().unwrap(); @@ -214,6 +222,7 @@ fn encode_rsa2048_priv_der() { } #[test] +#[cfg(feature = "pkcs1v15")] fn encode_rsa2048_pub_der() { let key = RsaPublicKey::from_public_key_der(RSA_2048_PUB_DER).unwrap(); let der = key.to_public_key_der().unwrap(); @@ -226,7 +235,7 @@ fn encode_rsa2048_pub_der() { } #[test] -#[cfg(feature = "encoding")] +#[cfg(all(feature = "pkcs1v15", feature = "encoding"))] fn decode_rsa2048_priv_pem() { let key = RsaPrivateKey::from_pkcs8_pem(RSA_2048_PRIV_PEM).unwrap(); @@ -287,7 +296,7 @@ fn decode_rsa2048_priv_pem() { } #[test] -#[cfg(feature = "encoding")] +#[cfg(all(feature = "pkcs1v15", feature = "encoding"))] fn decode_rsa2048_pub_pem() { let key = RsaPublicKey::from_public_key_pem(RSA_2048_PUB_PEM).unwrap(); diff --git a/tests/proptests.rs b/tests/proptests.rs index 142a8839..db37ea73 100644 --- a/tests/proptests.rs +++ b/tests/proptests.rs @@ -1,16 +1,21 @@ //! Property-based tests. -#![cfg(feature = "hazmat")] +// TODO(tarcieri) item here for PSS +//#![cfg(feature = "hazmat")] +#![cfg(all(feature = "pkcs1v15", feature = "hazmat"))] use proptest::prelude::*; use rand::rngs::ChaCha8Rng; use rand_core::SeedableRng; use rsa::{ - RsaPrivateKey, pkcs1v15, + RsaPrivateKey, signature::{Keypair, SignatureEncoding, Signer, Verifier}, }; use sha2::Sha256; +#[cfg(feature = "pkcs1v15")] +use rsa::pkcs1v15; + prop_compose! { // WARNING: do *NOT* copy and paste this code. It's insecure and optimized for test speed. fn private_key()(seed in any::<[u8; 32]>()) -> RsaPrivateKey { @@ -21,6 +26,7 @@ prop_compose! { proptest! { #[test] + #[cfg(feature = "pkcs1v15")] fn pkcs1v15_sign_roundtrip(private_key in private_key(), msg in any::>()) { let signing_key = pkcs1v15::SigningKey::::new(private_key); let signature_bytes = signing_key.sign(&msg).to_bytes(); diff --git a/tests/rfc8017.rs b/tests/rfc8017.rs index 3853b191..51b1c732 100644 --- a/tests/rfc8017.rs +++ b/tests/rfc8017.rs @@ -2,7 +2,7 @@ use rand::{SeedableRng, rngs::ChaCha8Rng}; use rsa::{ - BoxedUint, Pkcs1v15Sign, Pss, RsaPrivateKey, RsaPublicKey, pkcs1v15, pss, + BoxedUint, Pss, RsaPrivateKey, RsaPublicKey, pss, signature::{ DigestVerifier, RandomizedSigner, SignatureEncoding, Verifier, hazmat::PrehashVerifier, }, @@ -10,12 +10,16 @@ use rsa::{ }; use sha2::{Digest, Sha256}; +#[cfg(feature = "pkcs1v15")] +use rsa::{Pkcs1v15Sign, pkcs1v15}; + fn private_key() -> RsaPrivateKey { // 1025 bits makes both k != storage width and PSS emLen != k. let mut rng = ChaCha8Rng::from_seed([42; 32]); RsaPrivateKey::new(&mut rng, 1025).unwrap() } +#[cfg(feature = "pkcs1v15")] fn check_pkcs1v15(key: &RsaPublicKey, message: &[u8], bytes: &[u8], valid: bool) { let digest = Sha256::digest(message); let signature = pkcs1v15::Signature::try_from(bytes).unwrap(); @@ -72,21 +76,29 @@ fn check_pss(key: &RsaPublicKey, message: &[u8], bytes: &[u8], valid: bool) { } #[test] -fn signature_octet_strings_round_trip() { +#[cfg(feature = "pkcs1v15")] +fn signature_octet_strings_round_trip_pkcs1v15() { for bytes in [&[][..], &[0][..], &[0, 0, 42][..], &[0u8; 129][..]] { let pkcs = pkcs1v15::Signature::try_from(bytes).unwrap(); - let pss = pss::Signature::try_from(bytes).unwrap(); assert_eq!(pkcs.to_bytes().as_ref(), bytes); - assert_eq!(pss.to_bytes().as_ref(), bytes); assert_eq!(format!("{pkcs:x}"), hex::encode(bytes)); - assert_eq!(format!("{pss:x}"), hex::encode(bytes)); assert_eq!(format!("{pkcs:X}"), hex::encode_upper(bytes)); + } +} + +#[test] +fn signature_octet_strings_round_trip_pss() { + for bytes in [&[][..], &[0][..], &[0, 0, 42][..], &[0u8; 129][..]] { + let pss = pss::Signature::try_from(bytes).unwrap(); + assert_eq!(pss.to_bytes().as_ref(), bytes); + assert_eq!(format!("{pss:x}"), hex::encode(bytes)); assert_eq!(format!("{pss:X}"), hex::encode_upper(bytes)); } } #[test] -fn signatures_require_exact_modulus_length() { +#[cfg(feature = "pkcs1v15")] +fn signatures_require_exact_modulus_length_pkcs1v15() { let key = private_key(); let public = key.to_public_key(); let message = b"RFC8017 octet-string lengths"; @@ -94,27 +106,40 @@ fn signatures_require_exact_modulus_length() { let pkcs = pkcs1v15::SigningKey::::new(key.clone()) .sign_with_rng(&mut rng, message) .to_bytes(); - let pss = pss::SigningKey::::new(key) - .sign_with_rng(&mut rng, message) - .to_bytes(); assert_eq!(pkcs.len(), public.size()); - assert_eq!(pss.len(), public.size()); check_pkcs1v15(&public, message, &pkcs, true); - check_pss(&public, message, &pss, true); // Adding zero octets changes S's length without changing OS2IP(S). for extra in [1, 7, 8] { let mut padded = vec![0; extra]; padded.extend_from_slice(&pkcs); check_pkcs1v15(&public, message, &padded, false); + } + check_pkcs1v15(&public, message, &pkcs[1..], false); + check_pkcs1v15(&public, message, &[], false); +} + +#[test] +fn signatures_require_exact_modulus_length_pss() { + let key = private_key(); + let public = key.to_public_key(); + let message = b"RFC8017 octet-string lengths"; + let mut rng = ChaCha8Rng::from_seed([7; 32]); + let pss = pss::SigningKey::::new(key) + .sign_with_rng(&mut rng, message) + .to_bytes(); + + assert_eq!(pss.len(), public.size()); + check_pss(&public, message, &pss, true); + + // Adding zero octets changes S's length without changing OS2IP(S). + for extra in [1, 7, 8] { let mut padded = vec![0; extra]; padded.extend_from_slice(&pss); check_pss(&public, message, &padded, false); } - check_pkcs1v15(&public, message, &pkcs[1..], false); check_pss(&public, message, &pss[1..], false); - check_pkcs1v15(&public, message, &[], false); check_pss(&public, message, &[], false); } diff --git a/tests/signature_size.rs b/tests/signature_size.rs index d2079b67..169f27c2 100644 --- a/tests/signature_size.rs +++ b/tests/signature_size.rs @@ -68,10 +68,13 @@ macro_rules! signature_tests { }; } -signature_tests!(pkcs1v15); signature_tests!(pss); +#[cfg(feature = "pkcs1v15")] +signature_tests!(pkcs1v15); + #[test] +#[cfg(feature = "pkcs1v15")] fn sign_with_large_key() { use crypto_bigint::{BoxedUint, ConcatenatingMul, NonZero, Resize}; use rand::{SeedableRng, rngs::ChaCha8Rng}; @@ -118,6 +121,8 @@ fn sign_with_large_key() { }}; } - check_signing!(pkcs1v15); check_signing!(pss); + + #[cfg(feature = "pkcs1v15")] + check_signing!(pkcs1v15); } diff --git a/tests/signing_blinding.rs b/tests/signing_blinding.rs index ea376107..2959ac0d 100644 --- a/tests/signing_blinding.rs +++ b/tests/signing_blinding.rs @@ -3,16 +3,20 @@ use pkcs8::DecodePrivateKey; use rand_core::{TryCryptoRng, TryRng}; -use rsa::{ - Error, Pkcs1v15Sign, Pss, RsaPrivateKey, RsaPublicKey, pkcs1v15, pss, traits::SignatureScheme, -}; +use rsa::{Error, Pss, RsaPrivateKey, RsaPublicKey, pss, traits::SignatureScheme}; use sha2::{Digest, Sha256}; use signature::{ - DigestSigner, MultipartSigner, RandomizedDigestSigner, RandomizedMultipartSigner, - RandomizedSigner, SignatureEncoding, Signer, - hazmat::{PrehashSigner, RandomizedPrehashSigner}, + RandomizedDigestSigner, RandomizedMultipartSigner, RandomizedSigner, + hazmat::RandomizedPrehashSigner, }; +#[cfg(feature = "pkcs1v15")] +use rsa::{Pkcs1v15Sign, pkcs1v15}; + +// These are only tested with pkcs1v15 +#[cfg(feature = "pkcs1v15")] +use signature::{DigestSigner, MultipartSigner, SignatureEncoding, Signer, hazmat::PrehashSigner}; + fn key() -> RsaPrivateKey { RsaPrivateKey::from_pkcs8_der(include_bytes!("examples/pkcs8/rsa2048-priv.der")) .expect("valid fixture") @@ -121,6 +125,7 @@ fn default_rng_reaches_signature_scheme() { } #[test] +#[cfg(feature = "pkcs1v15")] fn pkcs1v15_default_signatures_are_unchanged() { let key = key(); let digest = Sha256::digest(b"message"); diff --git a/tests/wycheproof.rs b/tests/wycheproof.rs index cb50727f..c550a923 100644 --- a/tests/wycheproof.rs +++ b/tests/wycheproof.rs @@ -9,7 +9,7 @@ use std::{fmt::Debug, fs::File}; use pkcs1::DecodeRsaPublicKey; use pkcs8::DecodePrivateKey; -use rsa::{Oaep, RsaPrivateKey, RsaPublicKey, pkcs1v15, pss, signature::Verifier}; +use rsa::{Oaep, RsaPrivateKey, RsaPublicKey, pss, signature::Verifier}; use rstest::rstest; use serde::Deserialize; use sha1::Sha1; @@ -18,6 +18,9 @@ use sha2::{ digest::{Digest, FixedOutputReset}, }; +#[cfg(feature = "pkcs1v15")] +use rsa::pkcs1v15; + #[derive(Deserialize, Debug)] struct TestFile { #[serde(rename(deserialize = "testGroups"))] @@ -154,6 +157,7 @@ impl Drop for Summary { // #[case("rsa_signature_8192_sha256_test.json")] TODO: needs disabling of maxsize // #[case("rsa_signature_8192_sha384_test.json")] TODO: needs disabling of maxsize // #[case("rsa_signature_8192_sha512_test.json")] TODO: needs disabling of maxsize +#[cfg(feature = "pkcs1v15")] fn test_rsa_pkcs1_verify(#[case] file: &str) { let path = format!("thirdparty/wycheproof/testvectors_v1/{file}"); let data_file = File::open(&path)