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)