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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion marvin-toolkit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ publish = false
anyhow = "1"
clap = { version = "4", features = ["derive"] }
rand = { workspace = true }
rsa = { path = ".." }
rsa = { path = "..", features = ["pkcs1v15"] }
4 changes: 3 additions & 1 deletion src/algorithms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
14 changes: 10 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
//! <code><a href="./pkcs1v15/index.html">pkcs1v15</a></code> module.
//! </div>
//!
//! ```
#![cfg_attr(feature = "pkcs1v15", doc = "```")]
#![cfg_attr(not(feature = "pkcs1v15"), doc = "```ignore")]
//! use rsa::{RsaPrivateKey, RsaPublicKey, Pkcs1v15Encrypt};
//!
//! let mut rng = rand::rng();
Expand All @@ -73,7 +74,8 @@
//! See security notes in the <code><a href="./pkcs1v15/index.html">pkcs1v15</a></code> module.
//! </div>
//!
//! ```
#![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};
Expand Down Expand Up @@ -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;
Expand All @@ -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;
8 changes: 7 additions & 1 deletion tests/key_size.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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::<sha1::Sha1>::new(), b"test")
.unwrap();
}
Expand Down
3 changes: 2 additions & 1 deletion tests/pkcs1v15.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! PKCS#1 v1.5 tests.
//! PKCS #1 v1.5 tests
#![cfg(feature = "pkcs1v15")]

#[cfg(feature = "encoding")]
#[test]
Expand Down
15 changes: 12 additions & 3 deletions tests/pkcs8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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();

Expand Down Expand Up @@ -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();

Expand Down
10 changes: 8 additions & 2 deletions tests/proptests.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -21,6 +26,7 @@ prop_compose! {

proptest! {
#[test]
#[cfg(feature = "pkcs1v15")]
fn pkcs1v15_sign_roundtrip(private_key in private_key(), msg in any::<Vec<u8>>()) {
let signing_key = pkcs1v15::SigningKey::<Sha256>::new(private_key);
let signature_bytes = signing_key.sign(&msg).to_bytes();
Expand Down
51 changes: 38 additions & 13 deletions tests/rfc8017.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@

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,
},
traits::PublicKeyParts,
};
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();
Expand Down Expand Up @@ -72,49 +76,70 @@ 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";
let mut rng = ChaCha8Rng::from_seed([7; 32]);
let pkcs = pkcs1v15::SigningKey::<Sha256>::new(key.clone())
.sign_with_rng(&mut rng, message)
.to_bytes();
let pss = pss::SigningKey::<Sha256>::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::<Sha256>::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);
}

Expand Down
Loading
Loading