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

[files]
extend-exclude = ["/tests/examples/pkcs1v15_cfrg_rsa_guidance/**"]
3 changes: 3 additions & 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.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, features = ["zeroize"] }
rand_core = { version = "0.10", default-features = false }
sha2 = { version = "0.11", default-features = false, features = ["oid", "zeroize"] }
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
56 changes: 56 additions & 0 deletions checkct/Cargo.lock

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

4 changes: 4 additions & 0 deletions checkct/driver/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ extern crate alloc;

mod allocator;
mod driver;
mod pkcs1v15;
#[allow(dead_code)]
#[path = "../../../src/algorithms/pkcs1v15/selection.rs"]
mod selection;
// BINSEC 0.11.3 does not decode the RISC-V amoadd.w used by Arc::clone.
#[cfg(not(target_arch = "riscv32"))]
mod raw_rsa;
Expand Down
34 changes: 34 additions & 0 deletions checkct/driver/src/pkcs1v15.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! Verify the actual §7.2 decoding and selection helpers up to the output boundary.
//!
//! EM, AM, CL, and the intermediate lengths are secret. We deliberately do not slice at the final
//! returned length: §7.2 step 5 permits exposing that length, but BINSEC has no declassification
//! annotation here. The fixed-size selection itself must have no secret-dependent control flow or
//! memory accesses.

use crate::rng::{PrivateRng, PublicRng};
use checkct_macros::checkct;
use core::hint::black_box;

#[checkct]
pub fn pkcs1v15_al() {
black_box(PublicRng.next_u32());
let mut cl = [0u8; 256];
PrivateRng.fill_bytes(&mut cl);
// `k=32` gives a non-power-of-two bound and exercises candidate rejection.
black_box(crate::selection::select_al(&cl, 21));
}

#[checkct]
pub fn pkcs1v15_decode_and_select() {
black_box(PublicRng.next_u32());
let mut em = [0u8; 32];
let mut am = [0u8; 32];
PrivateRng.fill_bytes(&mut em);
PrivateRng.fill_bytes(&mut am);
// An independent secret AL covering every valid length `(0..=21)` for `k=32`.
// Masking also admits `22..=31`, still within the selector's buffer bound.
let al = (PrivateRng.next_u32() & 31) as u16;
let (check, l) = crate::selection::eme_pkcs1v15_decode(&em, em.len());
black_box(crate::selection::select_message(&mut em, &am, al, l, check));
black_box(em);
}
148 changes: 6 additions & 142 deletions src/algorithms/pkcs1v15.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@

use alloc::vec::Vec;
use const_oid::AssociatedOid;
use crypto_bigint::{Choice, CtAssign, CtEq, CtGt, CtOption};
use crypto_bigint::CtEq;
use digest::Digest;
use rand_core::TryCryptoRng;
use zeroize::Zeroizing;

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

pub(crate) mod implicit_rejection;
mod selection;

pub(crate) use selection::{eme_pkcs1v15_decode, select_message};

/// Fills the provided slice with random values, which are guaranteed
/// to not be zero.
#[inline]
Expand Down Expand Up @@ -59,71 +64,6 @@ where
Ok(em)
}

/// Removes the encryption padding scheme from PKCS#1 v1.5.
///
/// 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 {
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)?;

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

/// Checks EME-PKCS1-v1_5 encoding as described in [draft-irtf-cfrg-rsa-guidance-10 §7.2, step 4].
///
/// Returns the message's starting index in a [`CtOption`], present when the padding is valid.
/// Scans all of `em` regardless of padding validity, leaving the encoded message available to
/// the caller for subsequent message selection. Both the validity and index remain secret.
///
/// Callers must establish `k >= 11`, `em.len() == k`, and `k <= u32::MAX` before calling.
/// These public length preconditions are checked by debug assertions.
///
/// [draft-irtf-cfrg-rsa-guidance-10 §7.2, step 4]: https://www.ietf.org/archive/id/draft-irtf-cfrg-rsa-guidance-10.html#section-7.2
#[inline]
#[allow(
clippy::cast_possible_truncation,
reason = "indices fit in u32 by the public length precondition"
)]
fn decrypt_inner(em: &[u8], k: usize) -> CtOption<u32> {
debug_assert!(k >= 11);
debug_assert_eq!(em.len(), k);
debug_assert!(u32::try_from(k).is_ok());

let first_byte_is_zero = em[0].ct_eq(&0u8);
let second_byte_is_two = em[1].ct_eq(&2u8);

// The remainder of the plaintext must be a string of non-zero random
// octets, followed by a 0, followed by the message.
// looking_for_index: 1 iff we are still looking for the zero.
// index: the offset of the first zero byte.
let mut looking_for_index = Choice::TRUE;
let mut index = 0u32;

for (i, el) in em.iter().enumerate().skip(2) {
let equals0 = el.ct_eq(&0u8);
index.ct_assign(&(i as u32), looking_for_index & equals0);
looking_for_index &= !equals0;
}

// PS starts at offset 2 and must contain at least 8 nonzero octets,
// so the first zero separator must occur at offset 10 or later.
let valid_ps = index.ct_gt(&9);
let valid = first_byte_is_zero & second_byte_is_two & !looking_for_index & valid_ps;

CtOption::new(index + 1, valid)
}

#[inline]
pub(crate) fn pkcs1v15_sign_pad(prefix: &[u8], hashed: &[u8], k: usize) -> Result<Vec<u8>> {
let hash_len = hashed.len();
Expand Down Expand Up @@ -219,80 +159,4 @@ mod tests {
let res = pkcs1v15_encrypt_pad(&mut rng, &message, k);
assert_eq!(res, Err(Error::MessageTooLong));
}

#[test]
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]
fn test_decrypt_inner_invalid_header() {
for header in [[1, 2], [0, 1], [1, 1], [0, 0]] {
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)
);
}
}

#[test]
fn test_decrypt_inner_first_delimiter() {
// 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());
assert_eq!(
pkcs1v15_encrypt_unpad(&em, em.len()),
Err(Error::Decryption)
);
}

#[test]
fn test_unpad_invalid_lengths() {
for k in 0..11 {
assert_eq!(
pkcs1v15_encrypt_unpad(&vec![0; k], 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));
}
}
}
Loading
Loading