Skip to content
Open
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
5 changes: 5 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
[default.extend-identifiers]
# typ stands for type, but it's a reserved identifier
typ = "typ"

[files]
extend-exclude = [
"/tests/examples/**",
]
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.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ const-oid = { version = "0.10", default-features = false }
crypto-bigint = { version = "0.7", default-features = false, features = ["zeroize", "alloc"] }
crypto-primes = { version = "0.7", default-features = false }
digest = { version = "0.11", default-features = false, features = ["alloc", "oid"] }
hmac = { version = "0.13", default-features = false }
rand_core = { version = "0.10", default-features = false }
sha2 = { version = "0.11", default-features = false, features = ["oid"] }
signature = { version = "3", default-features = false, features = ["alloc", "digest", "rand_core"] }
zeroize = { version = "1.8", features = ["alloc"] }

Expand All @@ -28,7 +30,6 @@ pkcs1 = { version = "0.8.0-rc.5", optional = true, default-features = false, fea
pkcs8 = { version = "0.11", optional = true, default-features = false, features = ["alloc", "pem"] }
serdect = { version = "0.4", optional = true }
sha1 = { version = "0.11", optional = true, default-features = false, features = ["oid"] }
sha2 = { version = "0.11", optional = true, default-features = false, features = ["oid"] }
spki = { version = "0.8", optional = true, default-features = false, features = ["alloc"] }
serde = { version = "1.0.184", optional = true, default-features = false, features = ["derive"] }

Expand All @@ -40,7 +41,6 @@ serde_test = "1.0.89"
rand = { version = "0.10", features = ["chacha"] }
rand_core = { version = "0.10", default-features = false }
sha1 = { version = "0.11", default-features = false, features = ["oid"] }
sha2 = { version = "0.11", default-features = false, features = ["oid"] }
sha3 = { version = "0.12", default-features = false, features = ["oid"] }
hex = { version = "0.4.3", features = ["serde"] }
serde_json = "1.0.138"
Expand Down Expand Up @@ -100,7 +100,7 @@ unused_lifetimes = "warn"
unused_qualifications = "warn"

[package.metadata.docs.rs]
features = ["std", "serde", "hazmat", "sha2"]
features = ["std", "serde", "hazmat"]

[profile.dev]
opt-level = 2
Expand Down
246 changes: 210 additions & 36 deletions src/algorithms/pkcs1v15.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@

use alloc::vec::Vec;
use const_oid::AssociatedOid;
use crypto_bigint::{Choice, CtAssign, CtEq, CtGt, CtOption};
use digest::Digest;
use crypto_bigint::{BoxedUint, Choice, CtAssign, CtEq, CtGt, CtOption, CtSelect};
use digest::{Digest, OutputSizeUser};
use hmac::{Hmac, KeyInit, Mac};
use rand_core::TryCryptoRng;
use sha2::Sha256;
use zeroize::Zeroizing;

use crate::errors::{Error, Result};
use crate::{
algorithms::pad::i2osp_zeroizing,
errors::{Error, Result},
};

/// Fills the provided slice with random values, which are guaranteed
/// to not be zero.
Expand Down Expand Up @@ -59,25 +64,171 @@ where
Ok(em)
}

/// Removes the encryption padding scheme from PKCS#1 v1.5.
/// Removes the PKCS#1 v1.5 encryption padding with implicit rejection.
///
/// Note that whether this function returns an error or not discloses secret
/// information. If an attacker can cause this function to run repeatedly and
/// learn whether each instance returned an error then they can decrypt and
/// forge signatures as if they had the private key. See
/// `decrypt_session_key` for a way of solving this problem.
#[inline]
pub(crate) fn pkcs1v15_encrypt_unpad(em: &[u8], k: usize) -> Result<Vec<u8>> {
if k < 11 || em.len() != k {
/// This function does not return an error if
/// the padding is invalid. Instead, it deterministically generates and returns
/// a replacement random message using a key-derivation function.
/// As a result, callers cannot distinguish between valid and
/// invalid padding based on the output, thus reducing the risk of side-channel attacks.
///
/// See
/// [draft-irtf-cfrg-rsa-guidance-10 § 7.2](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-rsa-guidance-10#section-7.2)
pub(crate) fn pkcs1v15_encrypt_unpad_implicit_rejection(
em: Zeroizing<Vec<u8>>,
k: usize,
d: &BoxedUint,
ciphertext: &[u8],
) -> Result<Vec<u8>> {
const LENGTH_LABEL: &[u8] = b"length";
const MESSAGE_LABEL: &[u8] = b"message";

if k < 11 || k != em.len() {
return Err(Error::Decryption);
}

// TODO(tarcieri): padding validity is exposed here until implicit rejection is implemented.
let index = decrypt_inner(em, k)
.into_option()
.ok_or(Error::Decryption)?;
// KDK = KeyDerivation(d, k, C).
let kdk = KeyDerivationKey::derive(d, k, ciphertext)?;

// The maximum allowed message size is the modulus size minus 2 bytes
// and a minimum of 8 bytes for padding.
let max_length = u16::try_from(k - 11).map_err(|_| Error::Decryption)?;

// CL = IRPRF (KDK, "length", 256).
let rejection_lengths = kdk.prf(LENGTH_LABEL, 256)?;

// AM = IRPRF (KDK, "message", k).
let rejection_message = kdk.prf(MESSAGE_LABEL, k)?;

// Mask with 1s up to the most significant bit set in max_length.
// This ensures the mask covers all bits up to the highest bit set.
let mut mask = max_length;
mask |= mask >> 1;
mask |= mask >> 2;
mask |= mask >> 4;
mask |= mask >> 8;

// Select the rejection length from the prf output.
let rejection_length = rejection_lengths.chunks_exact(2).fold(0u16, |acc, el| {
let candidate_length = ((u16::from(el[0]) << 8) | u16::from(el[1])) & mask;
let within_max_length = !candidate_length.ct_gt(&max_length);
acc.ct_select(&candidate_length, within_max_length)
});

let Some(rejection_msg_index) = k.checked_sub(usize::from(rejection_length)) else {
return Err(Error::Decryption);
};

// Check the padding and extract the real index.
let real_index_opt = decrypt_inner(&em, k);
let valid = real_index_opt.is_some();

// Select either the rejection or real message index depending on valid padding.
let message_index = real_index_opt.map_or(rejection_msg_index, |i| i as usize);
// At this stage, message_index does not directly reveal whether the padding check was successful,
// thus avoiding leaking information through the message length.
let mut output = vec![0u8; usize::from(max_length)];
for ((&em_byte, &syn_byte), out_byte) in em[message_index..]
.iter()
.zip(&rejection_message[message_index..])
.zip(output.iter_mut())
{
*out_byte = syn_byte.ct_select(&em_byte, valid);
}
output.truncate(em.len() - message_index);

Ok(em[index as usize..].to_vec())
Ok(output)
}

struct KeyDerivationKey(Zeroizing<[u8; 32]>);

impl KeyDerivationKey {
/// Derives a key derivation key from the private key, the ciphertext, and the key length.
///
/// ## Specifications
/// ```text
///
/// Input:
/// d - RSA private exponent
/// k - length in octets of the RSA modulus n
/// ciphertext - the ciphertext
/// Output:
/// KDK - the key derivation key
///
/// D = I2OSP (d, k).
/// DH = SHA256 (D)
/// KDK = HMAC (DH, C, SHA256).
/// ```
///
/// See:
/// [draft-irtf-cfrg-rsa-guidance-10 § 7.2.3](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-rsa-guidance-10#section-7.2)
#[inline]
fn derive(d: &BoxedUint, k: usize, ciphertext: &[u8]) -> Result<Self> {
if k < 11 {
return Err(Error::Decryption);
}

// D = I2OSP (d, k).
let d_padded = i2osp_zeroizing(d.clone(), k);

// DH = SHA256 (D).
let d_hash: Zeroizing<[u8; 32]> = Zeroizing::new(Sha256::digest(d_padded).into());

// KDK = HMAC-SHA256 (DH, C).
let mut mac =
Hmac::<Sha256>::new_from_slice(d_hash.as_ref()).map_err(|_| Error::Decryption)?;
if ciphertext.len() < k {
mac.update(&vec![0u8; k - ciphertext.len()]);
}
mac.update(ciphertext);
let kdk = mac.finalize();

Ok(Self(Zeroizing::new(kdk.into_bytes().into())))
}

/// Implements the pseudo-random function (PRF) to derive randomness for implicit rejection.
///
/// ## Specifications
///
/// ```text
/// IRPRF (KDK, label, length)
/// Input:
/// KDK - the key derivation key
/// label - a label making the output unique for a given KDK
/// length - requested length of output in octets
/// Output: derived key, an octet string
/// ```
/// See:
/// [draft-irtf-cfrg-rsa-guidance-10 § 7.1] (https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-rsa-guidance-10#section-7.1)
#[inline]
fn prf(&self, label: &[u8], output_len: usize) -> Result<Vec<u8>> {
// bitLength = 2 octets
// throw an error if the output length bits does not fit into 2 octets
let bitlen_bytes = u16::try_from(output_len * 8)
.map_err(|_| Error::Decryption)?
.to_be_bytes();

let mut prf_output = vec![0u8; output_len];
for (chunk_idx, chunk) in prf_output
.chunks_mut(Hmac::<Sha256>::output_size())
.enumerate()
{
// I
let index = u16::try_from(chunk_idx).map_err(|_| Error::Decryption)?;

// P_i = I (2 octets) || label || bitLength (2 octets)
let mut hmac =
Hmac::<Sha256>::new_from_slice(self.0.as_ref()).map_err(|_| Error::Decryption)?;
hmac.update(&index.to_be_bytes());
hmac.update(label);
hmac.update(&bitlen_bytes);

// chunk_i = HMAC(KDK, P_i).
let chunk_data = hmac.finalize();
chunk.copy_from_slice(&chunk_data.as_bytes()[..chunk.len()]);
}
Ok(prf_output)
}
}

/// Checks EME-PKCS1-v1_5 encoding as described in [draft-irtf-cfrg-rsa-guidance-10 §7.2, step 4].
Expand Down Expand Up @@ -224,32 +375,22 @@ mod tests {
fn test_decrypt_inner_minimum_padding() {
let em = [0, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0, 42];
assert_eq!(decrypt_inner(&em, em.len()).into_option(), Some(11));
assert_eq!(pkcs1v15_encrypt_unpad(&em, em.len()).unwrap(), [42]);

// Seven PS octets must fail even when the total length is at least 11.
let em = [0, 2, 1, 2, 3, 4, 5, 6, 7, 0, 42];
assert!(decrypt_inner(&em, em.len()).is_none().to_bool());
assert_eq!(
pkcs1v15_encrypt_unpad(&em, em.len()),
Err(Error::Decryption)
);
}

#[test]
fn test_decrypt_inner_empty_message() {
let em = [0, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0];
assert_eq!(decrypt_inner(&em, em.len()).into_option(), Some(11));
assert!(pkcs1v15_encrypt_unpad(&em, em.len()).unwrap().is_empty());
}

#[test]
fn test_decrypt_inner_missing_delimiter() {
let em = [0, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
assert!(decrypt_inner(&em, em.len()).is_none().to_bool());
assert_eq!(
pkcs1v15_encrypt_unpad(&em, em.len()),
Err(Error::Decryption)
);
}

#[test]
Expand All @@ -258,10 +399,6 @@ mod tests {
let mut em = [0, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0, 42];
em[..2].copy_from_slice(&header);
assert!(decrypt_inner(&em, em.len()).is_none().to_bool());
assert_eq!(
pkcs1v15_encrypt_unpad(&em, em.len()),
Err(Error::Decryption)
);
}
}

Expand All @@ -270,29 +407,66 @@ mod tests {
// Zeros in the message do not change its starting index.
let em = [0, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0, 42, 0, 43];
assert_eq!(decrypt_inner(&em, em.len()).into_option(), Some(11));
assert_eq!(pkcs1v15_encrypt_unpad(&em, em.len()).unwrap(), [42, 0, 43]);

// A later zero cannot rescue a PS that ended too early.
let em = [0, 2, 1, 2, 3, 4, 5, 6, 7, 0, 8, 0, 42];
assert!(decrypt_inner(&em, em.len()).is_none().to_bool());
}

#[test]
fn test_unpad_implicit_rejection() {
let d = BoxedUint::one();
let unpad = |em: &[u8]| {
pkcs1v15_encrypt_unpad_implicit_rejection(Zeroizing::new(em.to_vec()), em.len(), &d, em)
};

// Valid padding returns the real message.
assert_eq!(unpad(&[0, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0, 42]), Ok(vec![42]));
assert_eq!(unpad(&[0, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0]), Ok(vec![]));
assert_eq!(
pkcs1v15_encrypt_unpad(&em, em.len()),
Err(Error::Decryption)
unpad(&[0, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0, 42, 0, 43]),
Ok(vec![42, 0, 43])
);

// Invalid padding does not error, but deterministically returns a synthetic message
// of at most `k - 11` bytes.
for em in [
&[0, 2, 1, 2, 3, 4, 5, 6, 7, 0, 42][..],
&[0, 2, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
&[1, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0, 42],
&[0, 2, 1, 2, 3, 4, 5, 6, 7, 0, 8, 0, 42],
] {
let msg = unpad(em).expect("invalid padding must be implicitly rejected");
assert!(msg.len() <= em.len() - 11);
assert_eq!(unpad(em), Ok(msg));
}
}

#[test]
fn test_unpad_invalid_lengths() {
for k in 0..11 {
assert_eq!(
pkcs1v15_encrypt_unpad(&vec![0; k], k),
pkcs1v15_encrypt_unpad_implicit_rejection(
Zeroizing::new(vec![0; k]),
k,
&BoxedUint::one(),
&vec![0; k]
),
Err(Error::Decryption)
);
}

let em = [0, 2, 1, 2, 3, 4, 5, 6, 7, 8, 0, 42];
for k in [em.len() - 1, em.len() + 1] {
assert_eq!(pkcs1v15_encrypt_unpad(&em, k), Err(Error::Decryption));
assert_eq!(
pkcs1v15_encrypt_unpad_implicit_rejection(
Zeroizing::new(em.to_vec()),
k,
&BoxedUint::one(),
&em
),
Err(Error::Decryption)
);
}
}
}
Loading
Loading