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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,6 +20,7 @@ bouncycastle-mlkem = { path = "./crypto/mlkem" }
bouncycastle-mlkem-lowmemory = { path = "./crypto/mlkem-lowmemory" }
bouncycastle-mldsa = { path = "./crypto/mldsa" }
bouncycastle-mldsa-lowmemory = { path = "./crypto/mldsa-lowmemory" }
bouncycastle-padding = { path = "./crypto/padding" }
bouncycastle-rng = { path = "./crypto/rng" }
bouncycastle-sha2 = { path = "./crypto/sha2" }
bouncycastle-sha3 = { path = "./crypto/sha3" }
Expand DownExpand Up@@ -51,6 +52,7 @@ bouncycastle-mldsa.workspace = true
bouncycastle-mldsa-lowmemory.workspace = true
bouncycastle-mlkem.workspace = true
bouncycastle-mlkem-lowmemory.workspace = true
bouncycastle-padding.workspace = true
bouncycastle-rng.workspace = true
bouncycastle-sha2.workspace = true
bouncycastle-sha3.workspace = true
39 changes: 25 additions & 14 deletions crypto/core-test-framework/src/symmetric_ciphers.rs
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
//! Generic behaviour tests for the symmetric cipher traits.

use crate::DUMMY_SEED;
use bouncycastle_core::errors::SymmetricCipherError;
use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError};
use bouncycastle_core::key_material::{
KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations,
};
use bouncycastle_core::traits::{
AEADCipher, BlockCipherDecryptor, BlockCipherEncryptor, SecurityStrength, StreamCipher, SymmetricCipher,
AEADCipher, BlockCipherDecryptor, BlockCipherEncryptor, SecurityStrength, StreamCipher,
SymmetricCipher,
};

/// Instance of the test framework.
Expand DownExpand Up@@ -85,9 +86,13 @@ impl TestFrameworkSymmetricCipher {
];
for ss in security_strengths.iter() {
// Tag the key at an arbitrary strength for the purpose of this test. Inside a
// do_hazardous_operations() closure, set_security_strength() raises the strength
// (and bypasses the key-length guard) without complaining.
do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap();
// do_hazardous_operations() closure, set_security_strength() may raise the strength,
// but it still refuses a strength the key length cannot support; skip those.
match do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())) {
Ok(()) => {}
Err(KeyMaterialError::SecurityStrength(_)) => continue,
Err(e) => panic!("unexpected error tagging key strength: {e:?}"),
}

match C::encrypt_out(&key, msg, &mut ct) {
Ok(_) => {
Expand DownExpand Up@@ -154,9 +159,7 @@ impl TestFrameworkBlockCipher {
let mut ct = [[0u8; BLOCK_LEN]; 1];
let mut pt = [[0u8; BLOCK_LEN]; 1];
for msg_chunk in DUMMY_SEED.as_chunks::<BLOCK_LEN>().0.iter() {
let ct_bytes_written = encryptor
.do_encrypt_blocks_out(&[*msg_chunk], &mut ct)
.unwrap();
let ct_bytes_written = encryptor.do_encrypt_blocks_out(&[*msg_chunk], &mut ct).unwrap();
assert_eq!(ct_bytes_written, BLOCK_LEN);

let pt_bytes_written = decryptor.do_decrypt_blocks_out(&ct, &mut pt).unwrap();
Expand DownExpand Up@@ -225,9 +228,13 @@ impl TestFrameworkBlockCipher {
];
for ss in security_strengths.iter() {
// Tag the key at an arbitrary strength for the purpose of this test. Inside a
// do_hazardous_operations() closure, set_security_strength() raises the strength
// (and bypasses the key-length guard) without complaining.
do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap();
// do_hazardous_operations() closure, set_security_strength() may raise the strength,
// but it still refuses a strength the key length cannot support; skip those.
match do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())) {
Ok(()) => {}
Err(KeyMaterialError::SecurityStrength(_)) => continue,
Err(e) => panic!("unexpected error tagging key strength: {e:?}"),
}

match E::do_encrypt_init(&key) {
Ok(_) => {
Expand DownExpand Up@@ -363,9 +370,13 @@ impl TestFrameworkAEADCipher {
];
for ss in security_strengths.iter() {
// Tag the key at an arbitrary strength for the purpose of this test. Inside a
// do_hazardous_operations() closure, set_security_strength() raises the strength
// (and bypasses the key-length guard) without complaining.
do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap();
// do_hazardous_operations() closure, set_security_strength() may raise the strength,
// but it still refuses a strength the key length cannot support; skip those.
match do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())) {
Ok(()) => {}
Err(KeyMaterialError::SecurityStrength(_)) => continue,
Err(e) => panic!("unexpected error tagging key strength: {e:?}"),
}

// The key-strength requirement must be enforced both by the AEAD one-shot and by the
// inherited SymmetricCipher one-shot (encrypt_out), so exercise both.
Expand Down
20 changes: 20 additions & 0 deletions crypto/core/src/errors.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -176,12 +176,32 @@ pub enum SymmetricCipherError {
///
KeyMaterialError(KeyMaterialError),
///
PaddingError(PaddingError),
///
RNGError(RNGError),
///
StateError(&'static str),
}

/// Errors from a [`crate::traits::Padding`] scheme.
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PaddingError {
/// `pad()` was asked to pad more data than fits in a block alongside at least one byte of padding.
/// The usize is the maximum permitted data length (`BLOCK_LEN - 1`).
DataLengthTooLong(usize),
/// `unpad()` found the block does not carry well-formed padding. Deliberately carries no detail
/// about *how* the padding was malformed.
InvalidPadding,
}

/*** Promotion functions ***/
impl From<PaddingError> for SymmetricCipherError {
fn from(e: PaddingError) -> SymmetricCipherError {
Self::PaddingError(e)
}
}

impl From<KeyMaterialError> for SymmetricCipherError {
fn from(e: KeyMaterialError) -> SymmetricCipherError {
Self::KeyMaterialError(e)
Expand Down
32 changes: 28 additions & 4 deletions crypto/core/src/traits.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -102,8 +102,11 @@ pub trait BlockCipher {
/// In order for these APIs to be usable securely in all contexts, the init data will be generated
/// securely by the block cipher implementation and returned along with the ciphertext, and there is no API for the
/// user to provide the init data. If you require this functionality, see the documentation for the underlying implementation.
pub trait BlockCipherEncryptor<const KEY_LEN: usize, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize>:
BlockCipher + Sized
pub trait BlockCipherEncryptor<
const KEY_LEN: usize,
const INIT_DATA_LEN: usize,
const BLOCK_LEN: usize,
>: BlockCipher + Sized
{
/// Begins a streaming encryption flow, returning the generated init data (e.g. IV).
/// Sources randomness from the library's default OS-backed RNG.
Expand All@@ -130,8 +133,11 @@ pub trait BlockCipherEncryptor<const KEY_LEN: usize, const INIT_DATA_LEN: usize,
}

/// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`].
pub trait BlockCipherDecryptor<const KEY_LEN: usize, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize>:
BlockCipher + Sized
pub trait BlockCipherDecryptor<
const KEY_LEN: usize,
const INIT_DATA_LEN: usize,
const BLOCK_LEN: usize,
>: BlockCipher + Sized
{
/// Begins a streaming decryption flow from the init data returned by [`BlockCipherEncryptor::do_encrypt_init`].
fn do_decrypt_init(
Expand All@@ -152,6 +158,24 @@ pub trait BlockCipherDecryptor<const KEY_LEN: usize, const INIT_DATA_LEN: usize,
) -> Result<usize, SymmetricCipherError>;
}

/// A block padding scheme, used to extend arbitrary-length data to a whole number of blocks so that it
/// can be processed by a [`BlockCipherEncryptor`]. Implementations are pure functions of the block
/// contents: no key, no state.
///
/// Only the final, partial block of a message is ever padded; the padding layer sitting between the
/// caller and the block cipher is responsible for routing whole blocks straight through.
pub trait Padding<const BLOCK_LEN: usize> {
/// Pads `block` in place: bytes `0..data_len` are data and are left untouched, bytes
/// `data_len..BLOCK_LEN` are overwritten with padding. `data_len` must be less than `BLOCK_LEN`
/// (a full block of data requires a whole additional block of padding, which the caller supplies
/// as `data_len = 0`).
fn pad(block: &mut [u8; BLOCK_LEN], data_len: usize) -> Result<(), PaddingError>;
/// Returns the number of data bytes in a padded `block`, or [`PaddingError::InvalidPadding`].
/// Implementations must run in constant time with respect to the block contents, so that a
/// decryptor built on them does not leak a padding oracle.
fn unpad(block: &[u8; BLOCK_LEN]) -> Result<usize, PaddingError>;
}

/// The basic functions of an Authenticated Encryption with Addititional Data cipher.
pub trait AEADCipher<const KEY_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize>:
SymmetricCipher<KEY_LEN, NONCE_LEN> + Sized
Expand Down
17 changes: 17 additions & 0 deletions crypto/padding/Cargo.toml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
[package]
name = "bouncycastle-padding"
version.workspace = true
edition.workspace = true

[dependencies]
bouncycastle-core.workspace = true
bouncycastle-utils.workspace = true

[dev-dependencies]
bouncycastle-core-test-framework.workspace = true
bouncycastle-rng.workspace = true
criterion.workspace = true

[[bench]]
name = "padding_benches"
harness = false
27 changes: 27 additions & 0 deletions crypto/padding/benches/padding_benches.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
use bouncycastle_core::traits::Padding;
use bouncycastle_padding::PKCS7;
use criterion::{Criterion, criterion_group, criterion_main};
use std::hint::black_box;

fn bench_pkcs7(c: &mut Criterion) {
let mut group = c.benchmark_group("padding::PKCS7");
group.bench_function("pad/16", |b| {
let mut block = [0u8; 16];
b.iter(|| {
<PKCS7 as Padding<16>>::pad(black_box(&mut block), black_box(5)).unwrap();
black_box(&block);
})
});
group.bench_function("unpad/16", |b| {
let mut block = [0u8; 16];
<PKCS7 as Padding<16>>::pad(&mut block, 5).unwrap();
b.iter(|| {
let n = <PKCS7 as Padding<16>>::unpad(black_box(&block)).unwrap();
black_box(n);
})
});
group.finish();
}

criterion_group!(benches, bench_pkcs7);
criterion_main!(benches);
115 changes: 115 additions & 0 deletions crypto/padding/src/lib.rs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
//! Block padding schemes implementing [`bouncycastle_core::traits::Padding`].
//!
//! * [`PKCS7`] — the padding scheme of RFC 5652 §6.3.
//! * [`PaddedEncryptor`] / [`PaddedDecryptor`] — adapt a block-aligned
//! [`BlockCipherEncryptor`](bouncycastle_core::traits::BlockCipherEncryptor) /
//! [`BlockCipherDecryptor`](bouncycastle_core::traits::BlockCipherDecryptor) to arbitrary-length
//! data, streaming or one-shot.
//!
//! # Usage Examples
//!
//! ```
//! use bouncycastle_core::traits::Padding;
//! use bouncycastle_padding::PKCS7;
//!
//! // 5 data bytes in a 16-byte block: pad with 11 bytes of value 0x0b.
//! let mut block = [0u8; 16];
//! block[..5].copy_from_slice(b"hello");
//! <PKCS7 as Padding<16>>::pad(&mut block, 5).unwrap();
//! assert_eq!(&block[..5], b"hello");
//! assert_eq!(&block[5..], &[0x0b; 11]);
//!
//! // Unpadding recovers the data length.
//! let data_len = <PKCS7 as Padding<16>>::unpad(&block).unwrap();
//! assert_eq!(data_len, 5);
//!
//! // A block that is not well-formed padding is rejected.
//! block[15] = 0x00;
//! assert!(<PKCS7 as Padding<16>>::unpad(&block).is_err());
//! ```
//!
//! # Memory Usage
//!
//! | Operation | Stack (excluding the caller's buffers and the inner cipher) |
//! |-----------------------|-------------------------------------------------------------|
//! | `PKCS7::pad` | O(1) |
//! | `PKCS7::unpad` | O(1) |
//! | `PaddedEncryptor` | one `BLOCK_LEN` buffer (in a `Secret`) + a length |
//! | `PaddedDecryptor` | two `BLOCK_LEN` buffers + a length |
//!
//! # Security Considerations
//!
//! `unpad` is the classic padding-oracle site: if timing or the error depends on *which* byte was
//! malformed, an attacker who can submit ciphertexts can decrypt them byte by byte. [`PKCS7::unpad`]
//! inspects every byte with constant-time masks and returns a single undifferentiated
//! [`PaddingError::InvalidPadding`]. This does not make unauthenticated encryption safe: still
//! authenticate the ciphertext (MAC or AEAD) so the error is never reachable by an attacker.

#![forbid(unsafe_code)]
#![forbid(missing_docs)]
#![no_std]

mod padded;
pub use padded::{PaddedDecryptor, PaddedEncryptor};

use bouncycastle_core::errors::PaddingError;
use bouncycastle_core::traits::Padding;
use bouncycastle_utils::ct::Condition;

/// RFC 5652 §6.3 padding (the CMS successor to PKCS #7): "the input shall be padded at the trailing
/// end with `k-(lth mod k)` octets all having value `k-(lth mod k)`". Defined only for block lengths
/// `0 < k < 256`, enforced at compile time.
pub struct PKCS7;

impl<const BLOCK_LEN: usize> Padding<BLOCK_LEN> for PKCS7 {
fn pad(block: &mut [u8; BLOCK_LEN], data_len: usize) -> Result<(), PaddingError> {
const {
assert!(
BLOCK_LEN > 0 && BLOCK_LEN < 256,
"PKCS7 padding is only defined for block lengths 1..=255 (RFC 5652 §6.3)"
)
}
if data_len >= BLOCK_LEN {
return Err(PaddingError::DataLengthTooLong(BLOCK_LEN - 1));
}
// RFC 5652 §6.3: pad with k - (lth mod k) octets of value k - (lth mod k). Here the caller
// has already reduced lth mod k to data_len, so the value is simply BLOCK_LEN - data_len.
// `data_len < BLOCK_LEN < 256` so this fits in a u8.
let pad_byte = (BLOCK_LEN - data_len) as u8;
// Constant-time in data_len: every byte is visited, and a mask selects data vs padding.
for (i, b) in block.iter_mut().enumerate() {
let is_padding = Condition::<i64>::is_gte(i as i64, data_len as i64);
*b = is_padding.select(pad_byte as i64, *b as i64) as u8;
}
Ok(())
}

fn unpad(block: &[u8; BLOCK_LEN]) -> Result<usize, PaddingError> {
const {
assert!(
BLOCK_LEN > 0 && BLOCK_LEN < 256,
"PKCS7 padding is only defined for block lengths 1..=255 (RFC 5652 §6.3)"
)
}
let k = BLOCK_LEN as i64;
// The last byte declares the padding length p; the block is valid iff 1 <= p <= k and the
// final p bytes all equal p. Every byte is examined regardless, so timing is independent of
// where (or whether) the padding is malformed.
let p = block[BLOCK_LEN - 1] as i64;
let mut valid = Condition::<i64>::is_within_range(p, 1, k);
for (i, b) in block.iter().enumerate() {
// Position i is a padding position iff i >= k - p. (If p is out of range this may select
// every position, but `valid` is already FALSE and cannot become TRUE again.)
let in_padding = Condition::<i64>::is_gte(i as i64, k - p);
let matches = Condition::<i64>::is_equal(*b as i64, p);
valid &= matches | !in_padding;
}
// Single public decision point: the caller learns only valid/invalid.
if valid.to_bool() {
// p is within 1..=k here, so k - p is in 0..k and the cast is lossless.
Ok((k - p) as usize)
} else {
Err(PaddingError::InvalidPadding)
}
}
}
Loading
Loading