Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ zeroize = { version = "1.8", features = ["alloc"] }

# optional dependencies
crypto-common = { version = "0.2.0-rc.8", optional = true, features = ["getrandom"] }
hmac = { version = "0.13.0-rc.0", optional = true, default-features = false }
pkcs1 = { version = "0.8.0-rc.3", optional = true, default-features = false, features = ["alloc", "pem"] }
pkcs8 = { version = "0.11.0-rc.8", optional = true, default-features = false, features = ["alloc", "pem"] }
serdect = { version = "0.4", optional = true }
Expand Down Expand Up @@ -58,6 +59,8 @@ getrandom = ["crypto-bigint/getrandom", "crypto-common"]
serde = ["encoding", "dep:serde", "dep:serdect", "crypto-bigint/serde"]
pkcs5 = ["pkcs8/encryption"]
std = ["pkcs1?/std", "pkcs8?/std"]
# Implicit rejection for PKCS#1 v1.5 decryption (Marvin attack mitigation)
implicit-rejection = ["dep:hmac", "dep:sha2"]

[package.metadata.docs.rs]
features = ["std", "serde", "hazmat", "sha2"]
Expand Down
127 changes: 127 additions & 0 deletions src/algorithms/pkcs1v15.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ use zeroize::Zeroizing;

use crate::errors::{Error, Result};

#[cfg(feature = "implicit-rejection")]
use digest::KeyInit;
#[cfg(feature = "implicit-rejection")]
use hmac::{Hmac, Mac};
#[cfg(feature = "implicit-rejection")]
use sha2::Sha256;

/// Fills the provided slice with random values, which are guaranteed
/// to not be zero.
#[inline]
Expand Down Expand Up @@ -116,6 +123,126 @@ fn decrypt_inner(em: Vec<u8>, k: usize) -> Result<(u8, Vec<u8>, u32)> {
Ok((valid.to_u8(), em, index))
}

/// Implicit Rejection PRF as specified in IETF draft-irtf-cfrg-rsa-guidance.
///
/// Generates a deterministic synthetic plaintext from the ciphertext and private key
/// when padding validation fails. This prevents timing side-channels.
///
/// PRF(key, label || ciphertext) where:
/// - key = HMAC-SHA256(d || p || q, "implicit rejection key")
/// - label = "implicit rejection PKCS#1 v1.5 ciphertext"
#[cfg(feature = "implicit-rejection")]
pub(crate) fn implicit_rejection_prf(
key_hash: &[u8; 32],
ciphertext: &[u8],
output_len: usize,
) -> Vec<u8> {
const LABEL: &[u8] = b"implicit rejection PKCS#1 v1.5 ciphertext";

// Use HMAC-SHA256 in counter mode to generate enough output bytes
let mut result = Vec::with_capacity(output_len);
let mut counter: u32 = 0;

while result.len() < output_len {
let mut mac =
Hmac::<Sha256>::new_from_slice(key_hash).expect("HMAC can accept any key length");
mac.update(&counter.to_be_bytes());
mac.update(LABEL);
mac.update(ciphertext);
let block = mac.finalize().into_bytes();

let remaining = output_len - result.len();
let take = core::cmp::min(remaining, block.len());
result.extend_from_slice(&block[..take]);
counter += 1;
}

result
}

/// Derive a key for implicit rejection PRF from the private key components.
///
/// key = HMAC-SHA256(d || p || q, "implicit rejection key")
#[cfg(feature = "implicit-rejection")]
pub(crate) fn derive_implicit_rejection_key(d: &[u8], primes: &[&[u8]]) -> [u8; 32] {
const KEY_LABEL: &[u8] = b"implicit rejection key";

// Concatenate d and all primes as the HMAC key material
let mut key_material =
Vec::with_capacity(d.len() + primes.iter().map(|p| p.len()).sum::<usize>());
key_material.extend_from_slice(d);
for prime in primes {
key_material.extend_from_slice(prime);
}

let mut mac =
Hmac::<Sha256>::new_from_slice(&key_material).expect("HMAC can accept any key length");
mac.update(KEY_LABEL);
let result = mac.finalize().into_bytes();

let mut key = [0u8; 32];
key.copy_from_slice(&result);
key
}

/// Removes the encryption padding scheme from PKCS#1 v1.5 with implicit rejection.
///
/// Instead of returning an error on invalid padding, this function returns a
/// deterministic synthetic message derived from the ciphertext. This prevents
/// Bleichenbacher/Marvin timing attacks.
///
/// # Arguments
/// * `em` - The decrypted (but still padded) message
/// * `k` - The key size in bytes
/// * `ciphertext` - The original ciphertext (used to derive synthetic message)
/// * `key_hash` - Pre-computed HMAC key derived from private key
/// * `expected_len` - The expected plaintext length
///
/// # Returns
/// Either the actual plaintext or a synthetic plaintext of `expected_len` bytes
#[cfg(feature = "implicit-rejection")]
#[inline]
pub(crate) fn pkcs1v15_encrypt_unpad_implicit_rejection(
em: Vec<u8>,
k: usize,
ciphertext: &[u8],
key_hash: &[u8; 32],
expected_len: usize,
) -> Vec<u8> {
// Generate synthetic message first (constant time - always computed)
let synthetic = implicit_rejection_prf(key_hash, ciphertext, expected_len);

// Validate padding in constant time
let (valid, decrypted, index) = match decrypt_inner(em, k) {
Ok(result) => result,
Err(_) => {
// If k < 11, return synthetic (this is a non-timing-sensitive check)
return synthetic;
}
};

// Check if the message length matches expected_len
let msg_len = k.saturating_sub(index as usize);
let len_matches = Choice::from_u8_lsb((msg_len == expected_len) as u8);

// Combine validity: padding must be valid AND length must match
let use_real = Choice::from_u8_lsb(valid) & len_matches;

// Constant-time selection between real and synthetic message
let mut result = vec![0u8; expected_len];
for (i, out_byte) in result.iter_mut().enumerate() {
let real_byte = if (index as usize + i) < decrypted.len() {
decrypted[index as usize + i]
} else {
0u8
};
let synthetic_byte = synthetic[i];
*out_byte = u8::ct_select(&synthetic_byte, &real_byte, use_real);
}

result
}

#[inline]
pub(crate) fn pkcs1v15_sign_pad(prefix: &[u8], hashed: &[u8], k: usize) -> Result<Vec<u8>> {
let hash_len = hashed.len();
Expand Down
Loading
Loading