Skip to content

Commit d1de0c3

Browse files
committed
feat: Implicit rejection api for PKCS#1 v1.5
1 parent 30560f5 commit d1de0c3

File tree

11 files changed

+751
-8
lines changed

11 files changed

+751
-8
lines changed

Cargo.lock

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pkcs8 = { version = "0.11.0-rc.10", optional = true, default-features = false, f
2929
serdect = { version = "0.4", optional = true }
3030
sha1 = { version = "0.11.0-rc.5", optional = true, default-features = false, features = ["oid"] }
3131
sha2 = { version = "0.11.0-rc.5", optional = true, default-features = false, features = ["oid"] }
32+
hmac = { version = "0.13.0", optional = true, default-features = false }
3233
spki = { version = "0.8.0-rc.4", optional = true, default-features = false, features = ["alloc"] }
3334
serde = { version = "1.0.184", optional = true, default-features = false, features = ["derive"] }
3435

@@ -58,6 +59,7 @@ getrandom = ["crypto-bigint/getrandom", "crypto-common"]
5859
serde = ["encoding", "dep:serde", "dep:serdect", "crypto-bigint/serde"]
5960
pkcs5 = ["pkcs8/encryption"]
6061
std = ["pkcs1?/std", "pkcs8?/std"]
62+
implicit_rejection = ["sha2", "dep:hmac"]
6163

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

src/algorithms/pkcs1v15.rs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ use zeroize::Zeroizing;
1515

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

18+
#[cfg(feature = "implicit_rejection")]
19+
use {
20+
crate::algorithms::pad::uint_to_zeroizing_be_pad,
21+
crypto_bigint::{BoxedUint, CtGt, CtLt},
22+
digest::OutputSizeUser,
23+
hmac::{Hmac, KeyInit, Mac},
24+
sha2::Sha256,
25+
};
26+
1827
/// Fills the provided slice with random values, which are guaranteed
1928
/// to not be zero.
2029
#[inline]
@@ -116,6 +125,190 @@ fn decrypt_inner(em: Vec<u8>, k: usize) -> Result<(u8, Vec<u8>, u32)> {
116125
Ok((valid.to_u8(), em, index))
117126
}
118127

128+
/// Removes PKCS#1 v1.5 encryption padding with implicit rejection.
129+
///
130+
/// Unlike [`pkcs1v15_encrypt_unpad`], this function does not return an error if
131+
/// the padding is invalid. Instead, it deterministically generates and returns
132+
/// a replacement random message using a key-derivation function.
133+
/// As a result, callers cannot distinguish between valid and
134+
/// invalid padding based on the output, thus preventing side-channel attacks.
135+
///
136+
/// See
137+
/// [draft-irtf-cfrg-rsa-guidance-08 § 7.2](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-rsa-guidance-08#section-7.2)
138+
#[cfg(feature = "implicit_rejection")]
139+
pub(crate) fn pkcs1v15_encrypt_unpad_implicit_rejection(
140+
em: Vec<u8>,
141+
k: usize,
142+
kdk: &KeyDerivationKey,
143+
) -> Result<Vec<u8>> {
144+
const LENGTH_LABEL: &[u8] = b"length";
145+
const MESSAGE_LABEL: &[u8] = b"message";
146+
147+
if k < 11 || k != em.len() {
148+
return Err(Error::Decryption);
149+
}
150+
151+
// The maximum allowed message size is the modulus size minus 2 bytes
152+
// and a minimum of 8 bytes for padding.
153+
let max_length = u16::try_from(k - 10).map_err(|_| Error::Decryption)?;
154+
155+
// CL = IRPRF (KDK, "length", 256).
156+
let rejection_lengths = kdk.prf(LENGTH_LABEL, 256)?;
157+
158+
// AM = IRPRF (KDK, "message", k).
159+
let rejection_message = kdk.prf(MESSAGE_LABEL, k)?;
160+
161+
// Mask with 1s up to the most significant bit set in max_length.
162+
// This ensures the mask covers all bits up to the highest bit set.
163+
let mut mask = max_length;
164+
mask |= mask >> 1;
165+
mask |= mask >> 2;
166+
mask |= mask >> 4;
167+
mask |= mask >> 8;
168+
169+
// Select the rejection length from the prf output.
170+
let rejection_length = rejection_lengths.chunks_exact(2).fold(0u16, |acc, el| {
171+
let candidate_length = (u16::from(el[0]) << 8 | u16::from(el[1])) & mask;
172+
let less_than_max_length = candidate_length.ct_lt(&max_length);
173+
acc.ct_select(&candidate_length, less_than_max_length)
174+
});
175+
176+
let Some(rejection_msg_index) = k.checked_sub(usize::from(rejection_length)) else {
177+
return Err(Error::Decryption);
178+
};
179+
180+
let first_byte_is_zero = em[0].ct_eq(&0u8);
181+
let second_byte_is_two = em[1].ct_eq(&2u8);
182+
183+
// Indicates whether the zero byte has been found.
184+
let mut found_zero_byte = Choice::FALSE;
185+
// Padding | message separation index.
186+
let mut zero_index: u32 = 0;
187+
188+
for (i, el) in em.iter().enumerate().skip(2) {
189+
let equals0 = el.ct_eq(&0u8);
190+
zero_index.ct_assign(&(i as u32), !found_zero_byte & equals0);
191+
found_zero_byte |= equals0;
192+
}
193+
194+
// Padding must be at least 8 bytes long, and it starts two bytes into the message.
195+
let index_is_greater_than_prefix = zero_index.ct_gt(&9);
196+
197+
let valid =
198+
first_byte_is_zero & second_byte_is_two & found_zero_byte & index_is_greater_than_prefix;
199+
200+
let real_message_index = zero_index.wrapping_add(1) as usize;
201+
202+
// Select either the rejection or real message depending on valid padding.
203+
let message_index = rejection_msg_index.ct_select(&real_message_index, valid);
204+
// At this stage, message_index does not directly reveal whether the padding check was successful,
205+
// thus avoiding leaking information through the message length.
206+
let mut output = vec![0u8; usize::from(max_length)];
207+
for ((&em_byte, &syn_byte), out_byte) in em[message_index..]
208+
.iter()
209+
.zip(&rejection_message[message_index..])
210+
.zip(output.iter_mut())
211+
{
212+
*out_byte = syn_byte.ct_select(&em_byte, valid);
213+
}
214+
output.truncate(em.len() - message_index);
215+
216+
Ok(output)
217+
}
218+
219+
#[cfg(feature = "implicit_rejection")]
220+
pub(crate) struct KeyDerivationKey(Zeroizing<[u8; 32]>);
221+
222+
#[cfg(feature = "implicit_rejection")]
223+
impl KeyDerivationKey {
224+
/// Derives a key derivation key from the private key, the ciphertext, and the key length.
225+
///
226+
/// ## Specifications
227+
/// ```text
228+
///
229+
/// Input:
230+
/// d - RSA private exponent
231+
/// k - length in octets of the RSA modulus n
232+
/// ciphertext - the ciphertext
233+
/// Output:
234+
/// KDK - the key derivation key
235+
///
236+
/// D = I2OSP (d, k).
237+
/// DH = SHA256 (D)
238+
/// KDK = HMAC (DH, C, SHA256).
239+
/// ```
240+
///
241+
/// See:
242+
/// [draft-irtf-cfrg-rsa-guidance-08 § 7.2.3](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-rsa-guidance-08#section-7.2)
243+
#[inline]
244+
pub fn derive(d: &BoxedUint, k: usize, ciphertext: &[u8]) -> Result<Self> {
245+
if k < 11 {
246+
return Err(Error::Decryption);
247+
}
248+
249+
// D = I2OSP (d, k).
250+
let d_padded = Zeroizing::new(uint_to_zeroizing_be_pad(d.clone(), k)?);
251+
252+
// DH = SHA256 (D).
253+
let d_hash: Zeroizing<[u8; 32]> = Zeroizing::new(Sha256::digest(d_padded).into());
254+
255+
// KDK = HMAC-SHA256 (DH, C).
256+
let mut mac =
257+
Hmac::<Sha256>::new_from_slice(d_hash.as_ref()).map_err(|_| Error::Decryption)?;
258+
if ciphertext.len() < k {
259+
mac.update(&vec![0u8; k - ciphertext.len()]);
260+
}
261+
mac.update(ciphertext);
262+
let kdk = mac.finalize();
263+
264+
Ok(Self(Zeroizing::new(kdk.into_bytes().into())))
265+
}
266+
267+
/// Implements the pseudo-random function (PRF) to derive randomness for implicit rejection.
268+
///
269+
/// ## Specifications
270+
///
271+
/// ```text
272+
/// IRPRF (KDK, label, length)
273+
/// Input:
274+
/// KDK - the key derivation key
275+
/// label - a label making the output unique for a given KDK
276+
/// length - requested length of output in octets
277+
/// Output: derived key, an octet string
278+
/// ```
279+
/// See:
280+
/// [draft-irtf-cfrg-rsa-guidance-08 § 7.1] (https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-rsa-guidance-08#section-7.1)
281+
#[inline]
282+
fn prf(&self, label: &[u8], output_len: usize) -> Result<Vec<u8>> {
283+
// bitLength = 2 octets
284+
// throw an error if the output length bits does not fit into 2 octets
285+
let bitlen_bytes = u16::try_from(output_len * 8)
286+
.map_err(|_| Error::Decryption)?
287+
.to_be_bytes();
288+
289+
let mut prf_output = vec![0u8; output_len];
290+
for (chunk_idx, chunk) in prf_output
291+
.chunks_mut(Hmac::<Sha256>::output_size())
292+
.enumerate()
293+
{
294+
// I
295+
let index = u16::try_from(chunk_idx).map_err(|_| Error::Decryption)?;
296+
297+
// P_i = I (2 octets) || label || bitLength (2 octets)
298+
let mut hmac =
299+
Hmac::<Sha256>::new_from_slice(self.0.as_ref()).map_err(|_| Error::Decryption)?;
300+
hmac.update(&index.to_be_bytes());
301+
hmac.update(label);
302+
hmac.update(&bitlen_bytes);
303+
304+
// chunk_i = HMAC(KDK, P_i).
305+
let chunk_data = hmac.finalize();
306+
chunk.copy_from_slice(&chunk_data.as_bytes()[..chunk.len()]);
307+
}
308+
Ok(prf_output)
309+
}
310+
}
311+
119312
#[inline]
120313
pub(crate) fn pkcs1v15_sign_pad(prefix: &[u8], hashed: &[u8], k: usize) -> Result<Vec<u8>> {
121314
let hash_len = hashed.len();

src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ mod key;
253253
pub use pkcs1;
254254
#[cfg(feature = "encoding")]
255255
pub use pkcs8;
256-
#[cfg(feature = "sha2")]
256+
#[cfg(any(feature = "sha2", feature = "implicit_rejection"))]
257257
pub use sha2;
258258

259259
pub use crate::{
@@ -265,5 +265,8 @@ pub use crate::{
265265
traits::keys::CrtValue,
266266
};
267267

268+
#[cfg(feature = "implicit_rejection")]
269+
pub use pkcs1v15::Pkcs1v15EncryptImplicitRejection;
270+
268271
#[cfg(feature = "hazmat")]
269272
pub mod hazmat;

src/pkcs1v15.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ pub use self::{
3737
signing_key::SigningKey, verifying_key::VerifyingKey,
3838
};
3939

40+
#[cfg(feature = "implicit_rejection")]
41+
use crate::algorithms::pkcs1v15::{pkcs1v15_encrypt_unpad_implicit_rejection, KeyDerivationKey};
42+
4043
use alloc::{boxed::Box, vec::Vec};
4144
use const_oid::AssociatedOid;
4245
use core::fmt::Debug;
@@ -49,6 +52,8 @@ use crate::algorithms::pkcs1v15::*;
4952
use crate::algorithms::rsa::{rsa_decrypt_and_check, rsa_encrypt};
5053
use crate::errors::{Error, Result};
5154
use crate::key::{self, RsaPrivateKey, RsaPublicKey};
55+
#[cfg(feature = "implicit_rejection")]
56+
use crate::traits::PrivateKeyParts;
5257
use crate::traits::{PaddingScheme, PublicKeyParts, SignatureScheme};
5358

5459
/// Encryption using PKCS#1 v1.5 padding.
@@ -75,6 +80,32 @@ impl PaddingScheme for Pkcs1v15Encrypt {
7580
}
7681
}
7782

83+
/// Encryption using PKCS#1 v1.5 padding with implicit rejection.
84+
#[cfg(feature = "implicit_rejection")]
85+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
86+
pub struct Pkcs1v15EncryptImplicitRejection;
87+
88+
#[cfg(feature = "implicit_rejection")]
89+
impl PaddingScheme for Pkcs1v15EncryptImplicitRejection {
90+
fn decrypt<Rng: TryCryptoRng + ?Sized>(
91+
self,
92+
rng: Option<&mut Rng>,
93+
priv_key: &RsaPrivateKey,
94+
ciphertext: &[u8],
95+
) -> Result<Vec<u8>> {
96+
decrypt_implicit_rejection(rng, priv_key, ciphertext)
97+
}
98+
99+
fn encrypt<Rng: TryCryptoRng + ?Sized>(
100+
self,
101+
rng: &mut Rng,
102+
pub_key: &RsaPublicKey,
103+
msg: &[u8],
104+
) -> Result<Vec<u8>> {
105+
encrypt(rng, pub_key, msg)
106+
}
107+
}
108+
78109
/// `RSASSA-PKCS1-v1_5`: digital signatures using PKCS#1 v1.5 padding.
79110
#[derive(Clone, Debug, Eq, PartialEq)]
80111
pub struct Pkcs1v15Sign {
@@ -183,6 +214,37 @@ fn decrypt<R: TryCryptoRng + ?Sized>(
183214
pkcs1v15_encrypt_unpad(em, priv_key.size())
184215
}
185216

217+
/// Decrypts plaintext using RSA and the PKCS#1 v1.5 padding scheme with implicit rejection.
218+
///
219+
/// If an `rng` is provided, RSA blinding is used to avoid timing side-channel attacks.
220+
///
221+
/// Unlike [`decrypt`], this function does not return an error if
222+
/// the padding is invalid. Instead, it deterministically generates and returns
223+
/// a replacement random message using a key-derivation function.
224+
/// As a result, callers cannot distinguish between valid and
225+
/// invalid paddings based on the output, thus reducing the risk of side-channel attacks.
226+
///
227+
/// See
228+
/// [draft-irtf-cfrg-rsa-guidance-08](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-rsa-guidance-08)
229+
#[cfg(feature = "implicit_rejection")]
230+
#[inline]
231+
fn decrypt_implicit_rejection<R: TryCryptoRng + ?Sized>(
232+
rng: Option<&mut R>,
233+
priv_key: &RsaPrivateKey,
234+
ciphertext: &[u8],
235+
) -> Result<Vec<u8>> {
236+
key::check_public(priv_key)?;
237+
238+
let k = priv_key.size();
239+
let ct_boxed = BoxedUint::from_be_slice(ciphertext, priv_key.n_bits_precision())?;
240+
let em = rsa_decrypt_and_check(priv_key, rng, &ct_boxed)?;
241+
// TODO: Check the timing leakage in this function.
242+
let em = uint_to_zeroizing_be_pad(em, k)?;
243+
244+
let kdk = KeyDerivationKey::derive(priv_key.d(), k, ciphertext)?;
245+
pkcs1v15_encrypt_unpad_implicit_rejection(em, k, &kdk)
246+
}
247+
186248
/// Calculates the signature of hashed using
187249
/// RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5. Note that `hashed` must
188250
/// be the result of hashing the input message using the given hash

0 commit comments

Comments
 (0)