From 5ab0043204df995359df43f02beceb36ae36f845 Mon Sep 17 00:00:00 2001 From: royb Date: Wed, 30 Sep 2026 05:01:38 -0400 Subject: [PATCH 01/18] Add KeyWrapper/KeyUnwrapper traits for SP 800-38F key wrap (#150) Adds the core API for the key-wrap algorithms of NIST SP 800-38F (KW, KWP, TKW; RFC 3394 / RFC 5649), following the discussion on #150 and the conventions of the cipher traits on this branch. Traits only: an AES-KW/KWP implementation over ElectronicCodeBook in bouncycastle-modes is to follow once the API is agreed. core/traits.rs: - KeyWrapper and KeyUnwrapper as separate traits, so a policy can allow unwrapping existing ciphertexts while forbidding new wrappings, as with the *Encryptor/*Decryptor pairs. - Fixed-length one-shots wrap_key/unwrap_key are provided over the required wrap_key_out/unwrap_key_out. Implementations must reject invalid (KEY_LEN, CT_LEN) pairs with an inline const assertion, so a wrong length is a compile error rather than a runtime Err. - Runtime-length wrap_out/unwrap_out cover KWP over arbitrary data, sized by wrap_out_len (exact) and unwrap_out_max_len (upper bound), mirroring encrypt_out_len/decrypt_out_max_len. OutputBufferTooSmall is checked before any work is done. - Wrap input is plain bytes, since wrapping strips KeyMaterial metadata. Unwrap output is a Secret, so it is zeroized on drop without claiming a KeyType or provenance. - Every unwrap authenticity failure (wrong KEK, tampering, ICV mismatch, bad padding, wrong recovered length) is the same DecryptionFailed, per SP 800-38F Algorithm 6 steps 4, 7 and 8; output buffers are zeroized on error. - The KEK must be a SymmetricCipherKey at least as strong as MAX_SECURITY_STRENGTH. App. A.2's advice that the KEK *should* be as strong as the wrapped key is documented as the caller's responsibility. core/errors.rs: - SymmetricCipherError::InvalidInputLength, for plaintext or ciphertext lengths the algorithm is not defined on (variable-length APIs only). core-test-framework: - New key_wrap module: TestFrameworkKeyWrap::test() exercises round trips, determinism, the length helpers, buffer-size and input-length errors, tamper and wrong-KEK rejection, output zeroization, and KEK type and strength checks. test_kat() is ready for the RFC 3394/5649 vectors. - Checked against an in-file mock (not cryptography) with KWP's length behaviour. Adds a bouncycastle-utils dependency for Secret. Assisted-by: Claude:claude-opus-5-5 --- crypto/core-test-framework/src/key_wrap.rs | 453 +++++++++++++++++++++ crypto/core-test-framework/src/lib.rs | 1 + crypto/core/src/errors.rs | 4 + crypto/core/src/traits.rs | 169 ++++++++ 4 files changed, 627 insertions(+) create mode 100644 crypto/core-test-framework/src/key_wrap.rs diff --git a/crypto/core-test-framework/src/key_wrap.rs b/crypto/core-test-framework/src/key_wrap.rs new file mode 100644 index 000000000..b3e5cb9ee --- /dev/null +++ b/crypto/core-test-framework/src/key_wrap.rs @@ -0,0 +1,453 @@ +//! Generic behaviour tests for the key-wrap traits [`KeyWrapper`] and [`KeyUnwrapper`]. + +use crate::DUMMY_SEED; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +use bouncycastle_utils::secret::Secret; + +/// Size of the scratch buffers used for the variable-length APIs. +const BUF_LEN: usize = 1024; + +/// Instance of the test framework. +pub struct TestFrameworkKeyWrap { + // Put any config options here +} + +impl TestFrameworkKeyWrap { + /// Create a new instance of the test framework. + pub fn new() -> Self { + Self {} + } + + /// Test all the members of traits [`KeyWrapper`] and [`KeyUnwrapper`] for one valid + /// `(KEY_LEN, CT_LEN)` pair, including the error conditions common to all key-wrap algorithms. + /// This gives good baseline test coverage, but is not a substitute for known-answer tests; see + /// [`TestFrameworkKeyWrap::test_kat`]. + pub fn test< + const KEK_LEN: usize, + const KEY_LEN: usize, + const CT_LEN: usize, + W: KeyWrapper + KeyUnwrapper, + >( + &self, + ) { + assert!(CT_LEN <= BUF_LEN, "test buffers are too small"); + + let kek = KeyMaterial::::from_bytes_as_type( + &DUMMY_SEED[..KEK_LEN], + KeyType::SymmetricCipherKey, + ) + .unwrap(); + let key: [u8; KEY_LEN] = DUMMY_SEED[100..100 + KEY_LEN].try_into().unwrap(); + + /* Length helpers */ + + assert_eq!(W::wrap_out_len(KEY_LEN), CT_LEN); + let max_pt_len = W::unwrap_out_max_len(CT_LEN); + assert!(max_pt_len >= KEY_LEN, "unwrap_out_max_len is not an upper bound"); + assert!(max_pt_len < CT_LEN, "a key-wrap ciphertext is longer than its plaintext"); + // documented as saturating, and as never panicking + assert_eq!(W::unwrap_out_max_len(0), 0); + let _ = W::unwrap_out_max_len(usize::MAX); + let _ = W::wrap_out_len(usize::MAX); + // one semiblock: half the block cipher's block length + let semiblock_len = CT_LEN - max_pt_len; + + /* Fixed-length API */ + + let ct = W::wrap_key::(&kek, &key).unwrap(); + + // wrap_key() and wrap_key_out() agree + let mut ct_out = [0u8; CT_LEN]; + let bytes_written = W::wrap_key_out(&kek, &key, &mut ct_out).unwrap(); + assert_eq!(bytes_written, CT_LEN); + assert_eq!(ct, ct_out); + + // key wrapping is deterministic + assert_eq!(ct, W::wrap_key::(&kek, &key).unwrap()); + + // round trip through unwrap_key() and unwrap_key_out() + let recovered: Secret<[u8; KEY_LEN]> = W::unwrap_key(&kek, &ct).unwrap(); + assert_eq!(*recovered, key); + + let mut recovered_out = Secret::<[u8; KEY_LEN]>::new(); + let bytes_written = W::unwrap_key_out(&kek, &ct, &mut recovered_out).unwrap(); + assert_eq!(bytes_written, KEY_LEN); + assert_eq!(*recovered_out, key); + + /* Variable-length API agrees with the fixed-length API */ + + let mut buf = [0u8; BUF_LEN]; + let bytes_written = W::wrap_out(&kek, &key, &mut buf[..CT_LEN]).unwrap(); + assert_eq!(bytes_written, CT_LEN); + assert_eq!(&buf[..CT_LEN], &ct); + + // a larger buffer than needed is fine + let bytes_written = W::wrap_out(&kek, &key, &mut buf).unwrap(); + assert_eq!(bytes_written, CT_LEN); + + let mut pt = [0u8; BUF_LEN]; + let bytes_written = W::unwrap_out(&kek, &ct, &mut pt[..max_pt_len]).unwrap(); + assert_eq!(bytes_written, KEY_LEN); + assert_eq!(&pt[..KEY_LEN], &key); + + /* Error case: output buffers too small */ + + match W::wrap_out(&kek, &key, &mut buf[..CT_LEN - 1]) { + Err(SymmetricCipherError::OutputBufferTooSmall(required)) => { + assert_eq!(required, CT_LEN) + } + _ => panic!("Should have rejected a ciphertext buffer that is too small"), + } + // checked before any work is done, so it is the upper bound that is enforced, even when + // the actual plaintext would have fit + match W::unwrap_out(&kek, &ct, &mut pt[..max_pt_len - 1]) { + Err(SymmetricCipherError::OutputBufferTooSmall(required)) => { + assert_eq!(required, max_pt_len) + } + _ => panic!("Should have rejected a plaintext buffer that is too small"), + } + + /* Error case: input lengths the algorithm is not defined on */ + + // Every key-wrap algorithm in SP 800-38F (Table 1) needs at least one octet of plaintext + // and a ciphertext of at least two semiblocks that is a whole number of semiblocks. + // Empty inputs also catch implementations that subtract before checking the length. + match W::wrap_out(&kek, &[], &mut buf) { + Err(SymmetricCipherError::InvalidInputLength(_)) => { /* good */ } + _ => panic!("Should have rejected an empty plaintext"), + } + for bad_ct_len in [0, semiblock_len, CT_LEN - 1] { + match W::unwrap_out(&kek, &ct[..bad_ct_len], &mut pt) { + Err(SymmetricCipherError::InvalidInputLength(_)) => { /* good */ } + _ => panic!("Should have rejected a ciphertext of {bad_ct_len} bytes"), + } + } + + /* Error case: modified ciphertext */ + + // Key wrap is authenticated, so unlike an unauthenticated cipher, a modified ciphertext + // must always fail to unwrap, and always with DecryptionFailed. + for i in [0, CT_LEN / 2, CT_LEN - 1] { + let mut bad_ct = ct; + bad_ct[i] ^= 0x01; + + match W::unwrap_key::(&kek, &bad_ct) { + Err(SymmetricCipherError::DecryptionFailed) => { /* good */ } + _ => panic!("Should have failed to unwrap a ciphertext modified at byte {i}"), + } + + // on failure the plaintext buffer is zeroized, not left holding partial output + pt.fill(0xFF); + match W::unwrap_out(&kek, &bad_ct, &mut pt) { + Err(SymmetricCipherError::DecryptionFailed) => { /* good */ } + _ => panic!("Should have failed to unwrap a ciphertext modified at byte {i}"), + } + assert!(pt.iter().all(|b| *b == 0), "plaintext buffer was not zeroized on error"); + } + + /* Error case: wrong KEK */ + + let wrong_kek = KeyMaterial::::from_bytes_as_type( + &DUMMY_SEED[1..KEK_LEN + 1], + KeyType::SymmetricCipherKey, + ) + .unwrap(); + match W::unwrap_key::(&wrong_kek, &ct) { + Err(SymmetricCipherError::DecryptionFailed) => { /* good */ } + _ => panic!("Should have failed to unwrap under the wrong KEK"), + } + + /* Error case: KEK of the wrong type */ + + let mac_key = + KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEK_LEN], KeyType::MACKey) + .unwrap(); + match W::wrap_key::(&mac_key, &key) { + Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } + _ => panic!("Should have rejected a KEK that is not a SymmetricCipherKey"), + } + match W::wrap_out(&mac_key, &key, &mut buf) { + Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } + _ => panic!("Should have rejected a KEK that is not a SymmetricCipherKey"), + } + match W::unwrap_key::(&mac_key, &ct) { + Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } + _ => panic!("Should have rejected a KEK that is not a SymmetricCipherKey"), + } + match W::unwrap_out(&mac_key, &ct, &mut pt) { + Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } + _ => panic!("Should have rejected a KEK that is not a SymmetricCipherKey"), + } + + /* Error case: KEK security strength too weak for the algorithm */ + + let mut kek = kek; + let security_strengths = [ + SecurityStrength::None, + SecurityStrength::_112bit, + SecurityStrength::_128bit, + SecurityStrength::_192bit, + SecurityStrength::_256bit, + ]; + for ss in security_strengths.iter() { + // `set_security_strength` enforces its key-length guard even inside a + // do_hazardous_operations() closure, so skip the strengths this KEK cannot carry, + // as the symmetric cipher frameworks do. + if ss > &SecurityStrength::from_bytes(KEK_LEN) { + continue; + } + do_hazardous_operations(&mut kek, |kek| kek.set_security_strength(*ss)).unwrap(); + + let strong_enough = ss >= &W::MAX_SECURITY_STRENGTH; + match W::wrap_key::(&kek, &key) { + Ok(_) => assert!( + strong_enough, + "Should not have accepted a KEK weaker than the algorithm" + ), + Err(SymmetricCipherError::KeyMaterialError(_)) => { + assert!(!strong_enough, "Should have accepted a strong enough KEK") + } + _ => panic!("Unexpected error"), + } + match W::unwrap_key::(&kek, &ct) { + Ok(_) => assert!( + strong_enough, + "Should not have accepted a KEK weaker than the algorithm" + ), + Err(SymmetricCipherError::KeyMaterialError(_)) => { + assert!(!strong_enough, "Should have accepted a strong enough KEK") + } + _ => panic!("Unexpected error"), + } + } + } + + /// Known-answer test: checks that wrapping `key` under `kek` gives `expected_ct` through both + /// the fixed-length and variable-length APIs, and that unwrapping `expected_ct` gives back `key`. + /// Test vectors must come from the specification or its official companion files, for example + /// RFC 3394 Section 4 or RFC 5649 Section 6. + pub fn test_kat< + const KEK_LEN: usize, + const KEY_LEN: usize, + const CT_LEN: usize, + W: KeyWrapper + KeyUnwrapper, + >( + &self, + kek: &[u8; KEK_LEN], + key: &[u8; KEY_LEN], + expected_ct: &[u8; CT_LEN], + ) { + let kek = + KeyMaterial::::from_bytes_as_type(kek, KeyType::SymmetricCipherKey).unwrap(); + + let ct = W::wrap_key::(&kek, key).unwrap(); + assert_eq!(&ct, expected_ct); + + let recovered: Secret<[u8; KEY_LEN]> = W::unwrap_key(&kek, expected_ct).unwrap(); + assert_eq!(&*recovered, key); + + let mut buf = [0u8; BUF_LEN]; + let bytes_written = W::wrap_out(&kek, key, &mut buf).unwrap(); + assert_eq!(&buf[..bytes_written], expected_ct); + + let bytes_written = W::unwrap_out(&kek, expected_ct, &mut buf).unwrap(); + assert_eq!(&buf[..bytes_written], key); + } +} + +#[cfg(test)] +mod tests { + //! Exercises the framework itself against a mock implementation, so that the framework is known + //! to compile and pass before any real key-wrap algorithm exists. + + use super::*; + use bouncycastle_core::errors::KeyMaterialError; + use bouncycastle_core::traits::Algorithm; + + /// NOT CRYPTOGRAPHY. A stand-in with the same length behaviour as KWP (SP 800-38F, Sec. 6.3), + /// which satisfies the documented contract of the key-wrap traits. It builds + /// `S = [len(P)]32 || checksum32(kek || P) || P || zero padding` to a multiple of 8 bytes, and + /// XORs `S` with the repeated KEK. + struct MockKeyWrap; + + const MOCK_KEK_LEN: usize = 16; + const MOCK_SEMIBLOCK_LEN: usize = 8; + + impl Algorithm for MockKeyWrap { + const ALG_NAME: &'static str = "MockKeyWrap-NOT-CRYPTOGRAPHY"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + } + + fn check_kek(kek: &KeyMaterial) -> Result<(), SymmetricCipherError> { + if kek.key_type() != KeyType::SymmetricCipherKey { + Err(KeyMaterialError::InvalidKeyType("KEK must be a SymmetricCipherKey"))? + } + if kek.security_strength() < MockKeyWrap::MAX_SECURITY_STRENGTH { + Err(KeyMaterialError::SecurityStrength("KEK is weaker than the algorithm"))? + } + Ok(()) + } + + /// 32-bit FNV-1a over `kek || pt`. + fn checksum(kek: &[u8], pt: &[u8]) -> [u8; 4] { + let mut hash: u32 = 0x811c9dc5; + for b in kek.iter().chain(pt.iter()) { + hash ^= *b as u32; + hash = hash.wrapping_mul(0x01000193); + } + hash.to_be_bytes() + } + + fn xor_with_kek(kek: &[u8], data: &mut [u8]) { + for (i, b) in data.iter_mut().enumerate() { + *b ^= kek[i % kek.len()]; + } + } + + impl KeyWrapper for MockKeyWrap { + fn wrap_key_out( + kek: &KeyMaterial, + key: &[u8; KEY_LEN], + ciphertext: &mut [u8; CT_LEN], + ) -> Result { + const { + assert!( + KEY_LEN >= 1 + && CT_LEN + == KEY_LEN.div_ceil(MOCK_SEMIBLOCK_LEN) * MOCK_SEMIBLOCK_LEN + + MOCK_SEMIBLOCK_LEN + ) + }; + Self::wrap_out(kek, key, ciphertext) + } + + fn wrap_out_len(plaintext_len: usize) -> usize { + plaintext_len + .div_ceil(MOCK_SEMIBLOCK_LEN) + .saturating_mul(MOCK_SEMIBLOCK_LEN) + .saturating_add(MOCK_SEMIBLOCK_LEN) + } + + fn wrap_out( + kek: &KeyMaterial, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + let ct_len = Self::wrap_out_len(plaintext.len()); + if ciphertext.len() < ct_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(ct_len)); + } + check_kek(kek)?; + if plaintext.is_empty() || plaintext.len() > u32::MAX as usize { + return Err(SymmetricCipherError::InvalidInputLength("plaintext length")); + } + let kek = kek.ref_to_bytes(); + let s = &mut ciphertext[..ct_len]; + s.fill(0); + s[..4].copy_from_slice(&(plaintext.len() as u32).to_be_bytes()); + s[4..8].copy_from_slice(&checksum(kek, plaintext)); + s[8..8 + plaintext.len()].copy_from_slice(plaintext); + xor_with_kek(kek, s); + Ok(ct_len) + } + } + + impl KeyUnwrapper for MockKeyWrap { + fn unwrap_key_out( + kek: &KeyMaterial, + ciphertext: &[u8; CT_LEN], + key: &mut Secret<[u8; KEY_LEN]>, + ) -> Result { + const { + assert!( + KEY_LEN >= 1 + && CT_LEN + == KEY_LEN.div_ceil(MOCK_SEMIBLOCK_LEN) * MOCK_SEMIBLOCK_LEN + + MOCK_SEMIBLOCK_LEN + ) + }; + // The padded plaintext is up to 7 bytes longer than KEY_LEN, so unwrap into a + // scratch Secret sized for it, then check that the recovered length is KEY_LEN. + let mut scratch = Secret::<[u8; BUF_LEN]>::new(); + let pt_len = Self::unwrap_out(kek, ciphertext, &mut scratch[..])?; + if pt_len != KEY_LEN { + return Err(SymmetricCipherError::DecryptionFailed); + } + key.copy_from_slice(&scratch[..KEY_LEN]); + Ok(KEY_LEN) + } + + fn unwrap_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(MOCK_SEMIBLOCK_LEN) + } + + fn unwrap_out( + kek: &KeyMaterial, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let max_pt_len = Self::unwrap_out_max_len(ciphertext.len()); + if plaintext.len() < max_pt_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(max_pt_len)); + } + plaintext.fill(0); + check_kek(kek)?; + if ciphertext.len() < 2 * MOCK_SEMIBLOCK_LEN + || !ciphertext.len().is_multiple_of(MOCK_SEMIBLOCK_LEN) + { + return Err(SymmetricCipherError::InvalidInputLength("ciphertext length")); + } + let kek = kek.ref_to_bytes(); + + // Decrypt the header, and the padded plaintext straight into the caller's buffer. + let mut header = [0u8; MOCK_SEMIBLOCK_LEN]; + header.copy_from_slice(&ciphertext[..MOCK_SEMIBLOCK_LEN]); + xor_with_kek(kek, &mut header); + let padded = &mut plaintext[..max_pt_len]; + padded.copy_from_slice(&ciphertext[MOCK_SEMIBLOCK_LEN..]); + for (i, b) in padded.iter_mut().enumerate() { + *b ^= kek[(i + MOCK_SEMIBLOCK_LEN) % kek.len()]; + } + + // As KWP-AD: every check fails with the same error. + let pt_len = u32::from_be_bytes(header[..4].try_into().unwrap()) as usize; + let ok = pt_len <= max_pt_len + && max_pt_len - pt_len < MOCK_SEMIBLOCK_LEN + && padded[pt_len..].iter().all(|b| *b == 0) + && checksum(kek, &padded[..pt_len]) == header[4..]; + if !ok { + plaintext.fill(0); + return Err(SymmetricCipherError::DecryptionFailed); + } + Ok(pt_len) + } + } + + #[test] + fn framework_passes_against_mock() { + let tf = TestFrameworkKeyWrap::new(); + // a whole number of semiblocks, one that needs padding, and the shortest possible + tf.test::(); + tf.test::(); + tf.test::(); + } + + /// Only checks the plumbing of test_kat(): the "expected" value is generated by the mock itself. + /// Real implementations must use vectors from the specification. + #[test] + fn kat_plumbing_against_mock() { + let kek: [u8; MOCK_KEK_LEN] = DUMMY_SEED[..MOCK_KEK_LEN].try_into().unwrap(); + let key = [0x42u8; 4]; + let kek_material = + KeyMaterial::::from_bytes_as_type(&kek, KeyType::SymmetricCipherKey) + .unwrap(); + let expected: [u8; 16] = MockKeyWrap::wrap_key(&kek_material, &key).unwrap(); + TestFrameworkKeyWrap::new() + .test_kat::(&kek, &key, &expected); + } +} diff --git a/crypto/core-test-framework/src/lib.rs b/crypto/core-test-framework/src/lib.rs index fb563a5bc..9d7a3e9fb 100644 --- a/crypto/core-test-framework/src/lib.rs +++ b/crypto/core-test-framework/src/lib.rs @@ -22,6 +22,7 @@ pub mod hash; pub mod kdf; pub mod kem; pub mod key_stream; +pub mod key_wrap; pub mod mac; pub mod signature; pub mod suspendable_state; diff --git a/crypto/core/src/errors.rs b/crypto/core/src/errors.rs index 485550db8..08c970728 100644 --- a/crypto/core/src/errors.rs +++ b/crypto/core/src/errors.rs @@ -184,6 +184,10 @@ pub enum SymmetricCipherError { /// The data is not a whole number of blocks, for a block cipher operation that takes only /// whole blocks: a misaligned block-cipher ciphertext is malformed. Nothing was processed. DataNotBlockAligned, + /// The length of the input is outside the range of lengths the algorithm is defined on, for + /// example a key-wrap plaintext or ciphertext that is too short. Only returned by APIs that + /// take variable-length input; lengths fixed at compile time are validated at compile time. + InvalidInputLength(&'static str), /// KeyMaterialError(KeyMaterialError), /// diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index f474bed1a..5b88e8b39 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -5,6 +5,7 @@ use crate::errors::*; use crate::key_material::KeyMaterialTrait; use crate::security_strength::SecurityStrength; +use bouncycastle_utils::secret::Secret; use core::fmt::{Debug, Display}; use core::marker::Sized; @@ -672,6 +673,174 @@ pub trait KEMPublicKey: fn from_bytes(bytes: &[u8]) -> Result; } +/// A keyed keystream generator: the raw primitive under a stream cipher, as +/// The unwrapping (authenticated-decryption) half of a key-wrap algorithm, such as KW, KWP or TKW +/// from NIST SP 800-38F (also specified as RFC 3394 and RFC 5649). +/// +/// Wrapping is performed by the corresponding [`KeyWrapper`] trait. The two are separate traits so +/// that a policy can permit unwrapping of existing ciphertexts while forbidding new wrappings, as +/// with the other `*Encryptor` / `*Decryptor` pairs. +/// +/// Key wrap is a one-shot operation over the whole input, so there is no streaming API: the +/// underlying wrapping function (SP 800-38F, Algorithm 2 (W⁻¹)) makes several passes over all +/// of the data. +/// +/// # The KEK +/// +/// The KEK (key encryption key) is a [`KeyMaterial`] and must be of type +/// [`KeyType::SymmetricCipherKey`], tagged at a security strength of at least +/// [`Algorithm::MAX_SECURITY_STRENGTH`]; otherwise a [`SymmetricCipherError::KeyMaterialError`] is +/// returned. +/// +/// # The recovered plaintext is a `Secret`, not a `KeyMaterial` +/// +/// Unwrapping says nothing about the type or provenance of the data, so the caller must decide +/// explicitly what it is, for example by constructing a [`KeyMaterial`] of the appropriate +/// [`KeyType`] from it. It is returned as a [`Secret`] so that it is still zeroized on drop and +/// redacted from debug output. +/// +/// # Every authenticity failure is the same error +/// +/// A wrong KEK, a modified ciphertext, an integrity-check value mismatch, invalid padding, and (for +/// [`KeyUnwrapper::unwrap_key`]) a recovered plaintext whose length is not `KEY_LEN` must all be +/// reported as [`SymmetricCipherError::DecryptionFailed`], so that the error does not tell an +/// attacker which check failed. SP 800-38F reports all of these as a single FAIL; see, for example, +/// Algorithm 6 (KWP-AD), steps 4, 7 and 8. On any error, the output buffer is zeroized. +pub trait KeyUnwrapper: Algorithm + Sized { + /// One-shot: unwraps a ciphertext whose length is fixed at compile time, returning the + /// recovered plaintext. + /// + /// Provided over [`KeyUnwrapper::unwrap_key_out`]. + fn unwrap_key( + kek: &KeyMaterial, + ciphertext: &[u8; CT_LEN], + ) -> Result, SymmetricCipherError> { + let mut key = Secret::<[u8; KEY_LEN]>::new(); + let bytes_written = Self::unwrap_key_out(kek, ciphertext, &mut key)?; + debug_assert_eq!(bytes_written, KEY_LEN); + Ok(key) + } + + /// One-shot: unwraps a ciphertext whose length is fixed at compile time, writing the recovered + /// plaintext directly into `key`. Returns the number of bytes written, which is always + /// `KEY_LEN`. + /// + /// Implementations must reject any `(KEY_LEN, CT_LEN)` pair the algorithm does not define with + /// an inline `const` assertion, so that it is a compile error at the call site; see + /// [`KeyWrapper::wrap_key_out`]. + /// + /// # Errors + /// [`SymmetricCipherError::KeyMaterialError`] for an unsuitable KEK, and + /// [`SymmetricCipherError::DecryptionFailed`] for any authenticity failure. `key` is zeroized + /// on error. + fn unwrap_key_out( + kek: &KeyMaterial, + ciphertext: &[u8; CT_LEN], + key: &mut Secret<[u8; KEY_LEN]>, + ) -> Result; + + /// An upper bound on the plaintext length for a ciphertext of `ciphertext_len` bytes: the + /// buffer [`KeyUnwrapper::unwrap_out`] requires. This is `ciphertext_len` minus one semiblock + /// (half the block cipher's block length), saturating at zero. It is exact for KW and TKW; for + /// KWP the exact length is only known after unwrapping, since up to 7 bytes are padding. + /// + /// Must not panic for any `ciphertext_len`. + fn unwrap_out_max_len(ciphertext_len: usize) -> usize; + + /// One-shot: unwraps a ciphertext whose length is only known at run time into `plaintext`, + /// which needs [`KeyUnwrapper::unwrap_out_max_len`] bytes. Returns the number of plaintext + /// bytes written. + /// + /// On success the caller is responsible for zeroizing `plaintext` once done with it; prefer + /// [`KeyUnwrapper::unwrap_key`] where the length is known at compile time. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, checked before + /// any work is done; [`SymmetricCipherError::InvalidInputLength`] for a ciphertext length the + /// algorithm is not defined on; [`SymmetricCipherError::KeyMaterialError`] for an unsuitable + /// KEK; and [`SymmetricCipherError::DecryptionFailed`] for any authenticity failure. The whole + /// `plaintext` buffer is zeroized on error. + fn unwrap_out( + kek: &KeyMaterial, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result; +} + +/// The wrapping (authenticated-encryption) half of a key-wrap algorithm, such as KW, KWP or TKW +/// from NIST SP 800-38F (also specified as RFC 3394 and RFC 5649). +/// +/// Unwrapping is performed by the corresponding [`KeyUnwrapper`] trait, whose notes on the KEK +/// apply here too. +/// +/// # The data being wrapped is plain bytes +/// +/// It is taken as bytes rather than as a [`KeyMaterial`] because wrapping strips the key type and +/// security strength anyway, and because key-wrap algorithms (particularly KWP) are also used to +/// protect data that is not a symmetric key, such as encoded private keys. A [`Secret`] can be +/// passed in by dereferencing it: `&*secret`. +/// +/// SP 800-38F, Appendix A.2 says the KEK *should* be at least as strong as any key it protects. +/// Since the wrapped data carries no security strength, this trait cannot enforce that; it is the +/// caller's responsibility. +/// +/// Key wrapping is deterministic: wrapping the same data under the same KEK always produces the +/// same ciphertext. +pub trait KeyWrapper: Algorithm + Sized { + /// One-shot: wraps data whose length is fixed at compile time, returning the ciphertext. + /// + /// Provided over [`KeyWrapper::wrap_key_out`]. + fn wrap_key( + kek: &KeyMaterial, + key: &[u8; KEY_LEN], + ) -> Result<[u8; CT_LEN], SymmetricCipherError> { + let mut ciphertext = [0u8; CT_LEN]; + let bytes_written = Self::wrap_key_out(kek, key, &mut ciphertext)?; + debug_assert_eq!(bytes_written, CT_LEN); + Ok(ciphertext) + } + + /// One-shot: wraps data whose length is fixed at compile time into `ciphertext`. Returns the + /// number of bytes written, which is always `CT_LEN`. + /// + /// `CT_LEN` is a function of `KEY_LEN` that depends on the algorithm (for example + /// `KEY_LEN + 8` for KW), which Rust cannot yet express in the signature. Implementations must + /// therefore reject any `(KEY_LEN, CT_LEN)` pair the algorithm does not define -- including + /// lengths outside the limits of SP 800-38F, Section 5.3.1 (Table 1) -- with an inline `const` + /// assertion, so that it is a compile error at the call site, not a runtime `Err`. + /// + /// # Errors + /// [`SymmetricCipherError::KeyMaterialError`] for an unsuitable KEK. + fn wrap_key_out( + kek: &KeyMaterial, + key: &[u8; KEY_LEN], + ciphertext: &mut [u8; CT_LEN], + ) -> Result; + + /// The exact ciphertext length for a plaintext of `plaintext_len` bytes: the buffer + /// [`KeyWrapper::wrap_out`] requires. For example `plaintext_len + 8` for KW, and + /// `plaintext_len` rounded up to a multiple of 8, plus 8, for KWP. + /// + /// Must not panic for any `plaintext_len`; for a length the algorithm is not defined on, the + /// value is unspecified and [`KeyWrapper::wrap_out`] returns + /// [`SymmetricCipherError::InvalidInputLength`]. + fn wrap_out_len(plaintext_len: usize) -> usize; + + /// One-shot: wraps data whose length is only known at run time into `ciphertext`, which needs + /// [`KeyWrapper::wrap_out_len`] bytes. Returns the number of bytes written. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked before + /// any work is done; [`SymmetricCipherError::InvalidInputLength`] for a plaintext length the + /// algorithm is not defined on; and [`SymmetricCipherError::KeyMaterialError`] for an + /// unsuitable KEK. + fn wrap_out( + kek: &KeyMaterial, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result; +} + /// A Message Authentication Code algorithm is a keyed hash function that behaves somewhat like a symmetric signature function. /// A MAC algorithm takes in a key and some data, and produces a MAC (message authentication code) that /// can be used to verify the integrity of data. From 95f67d9ead3929685aa601cae7a6cc7df4607eb2 Mon Sep 17 00:00:00 2001 From: royb Date: Wed, 30 Sep 2026 09:24:18 -0400 Subject: [PATCH 02/18] Add AES-KW and AES-KWP (SP 800-38F) over ElectronicCodeBook (#150) Implements the KeyWrapper / KeyUnwrapper traits from the previous commit for the two 128-bit-block key-wrap algorithms of NIST SP 800-38F, KW (Sec 6.2; RFC 3394) and KWP (Sec 6.3; RFC 5649), generically over any ElectronicCodeBook permutation, and publishes the AES instantiations. modes: - kw.rs: Kw. The wrapping function W and its inverse (Algorithms 1 and 2) run in place on the caller's buffer, visiting the registers in the order the spec's shift register would present them, which RFC 3394 Sec 2.2.1 gives as the equivalent index-based form; the one block of scratch is a Secret. KW-AE/AD are Algorithms 3 and 4. - kwp.rs: Kwp. KWP-AE/AD are Algorithms 5 and 6, including the single-block path for plaintexts of at most one semiblock. The ICV, length-field and padding checks of Algorithm 6 steps 4, 7 and 8 are all made, combined bitwise, and reported as the one DecryptionFailed. - The fixed-length API rejects invalid (KEY_LEN, CT_LEN) pairs at compile time via const assertions on the Table 1 limits; the run-time API returns InvalidInputLength outside them. Output buffers are zeroized on every failure. TKW (TDEA) is not included: it needs a 64-bit-block engine (PR #139). aes: - kw.rs: AES_KW_128/192/256 and AES_KWP_128/192/256 aliases, with the RFC 3394 Sec 4.1 and RFC 5649 7-octet vectors as doctests. No OIDs yet, in line with the other AES mode aliases. tests: - modes/tests/kw_tests.rs: all six RFC 3394 Sec 4 vectors and both RFC 5649 Sec 6 examples via TestFrameworkKeyWrap::test_kat, the shared framework for every AES key length at 26 payload lengths (every padding length of the single-block case included), and pins for the properties that distinguish KW, KWP and a raw block encryption. - modes/tests/acvp_kw_tests.rs: the NIST ACVP-AES-KW and ACVP-AES-KWP sets from bc-test-data, 1800 cases each, through both APIs, including the 171 + 178 forged ciphertexts that must be rejected. - aes/tests/kw_alias_tests.rs: the aliases name the right permutation and pass the framework. benches: KW/KWP wrap and unwrap of a 256-bit key, and KWP over 4 KiB. cli: aes{128,192,256}-kw and -kwp with wrap|unwrap, reading all of stdin (key wrap is one-shot), sharing key loading with the other AES commands; cli/tests/aes_kw_cli_tests.rs replays the RFC vectors in both directions and pins the length rules, the fail-closed unwrap and the help text. Assisted-by: Claude:claude-fable-5-1 --- alpha_0.1.4_release_notes.md | 2 + cli/src/aes_kw_cmd.rs | 199 ++++++++++++++ cli/src/main.rs | 170 ++++++++++++ cli/tests/aes_kw_cli_tests.rs | 293 ++++++++++++++++++++ crypto/aes/src/kw.rs | 129 +++++++++ crypto/aes/src/lib.rs | 3 + crypto/aes/tests/acvp_kw_tests.rs | 290 ++++++++++++++++++++ crypto/aes/tests/ecb_bc-test-data.rs | 2 +- crypto/aes/tests/kw_alias_tests.rs | 41 +++ crypto/aes/tests/kw_tests.rs | 372 ++++++++++++++++++++++++++ crypto/cipher/src/modes/kw.rs | 383 +++++++++++++++++++++++++++ crypto/cipher/src/modes/kwp.rs | 301 +++++++++++++++++++++ 12 files changed, 2184 insertions(+), 1 deletion(-) create mode 100644 cli/src/aes_kw_cmd.rs create mode 100644 cli/tests/aes_kw_cli_tests.rs create mode 100644 crypto/aes/src/kw.rs create mode 100644 crypto/aes/tests/acvp_kw_tests.rs create mode 100644 crypto/aes/tests/kw_alias_tests.rs create mode 100644 crypto/aes/tests/kw_tests.rs create mode 100644 crypto/cipher/src/modes/kw.rs create mode 100644 crypto/cipher/src/modes/kwp.rs diff --git a/alpha_0.1.4_release_notes.md b/alpha_0.1.4_release_notes.md index 92dcafd13..e5a5c24ef 100644 --- a/alpha_0.1.4_release_notes.md +++ b/alpha_0.1.4_release_notes.md @@ -3,6 +3,8 @@ ## Major features * CHACHA20 -- ChaCha20, Poly1305 and ChaCha20-Poly1305 (RFC 8439). +* AES Key Wrap -- AES_KW and AES_KWP (NIST SP 800-38F; RFC 3394 / RFC 5649) over any 128-bit block permutation, + with the `KeyWrapper` / `KeyUnwrapper` core traits and the `aes*-kw` / `aes*-kwp` CLI commands. ## Minor features / bug fixes diff --git a/cli/src/aes_kw_cmd.rs b/cli/src/aes_kw_cmd.rs new file mode 100644 index 000000000..c486c5018 --- /dev/null +++ b/cli/src/aes_kw_cmd.rs @@ -0,0 +1,199 @@ +//! AES key wrap (KW, NIST SP 800-38F Sec 6.2 / RFC 3394) and key wrap with padding (KWP, Sec 6.3 +//! / RFC 5649): `aes{128,192,256}-{kw,kwp} wrap|unwrap`, stdin to stdout. +//! +//! Key loading is shared with the other AES commands ([`crate::helpers::block_mode_helpers::load_key`]), +//! so `--key` / `--key-file` behave identically. Everything else differs from the streaming +//! commands, because key wrap is a one-shot algorithm: +//! +//! # Not streaming +//! +//! The wrapping function makes six passes over the whole input (SP 800-38F Algorithm 1), so there +//! is no way to emit output before all of stdin has been read. These commands read stdin to the +//! end and then write the result in one go. That is fine for what key wrap is for -- a key, or a +//! small encoded private key -- and the algorithm's own limits (below) keep it bounded. +//! +//! # No IV +//! +//! Key wrap is deterministic: the same input under the same KEK gives the same output, and there +//! is nothing to prepend or strip. The output is exactly `wrap_out_len` of the input: one 8-byte +//! semiblock longer for KW, the input rounded up to a multiple of 8 plus 8 for KWP. +//! +//! # Input lengths +//! +//! `aes*-kw` wraps 2 or more whole 8-byte semiblocks (16, 24, 32, ... bytes: the lengths of +//! AES, HMAC and other symmetric keys) and unwraps ciphertexts of 3 or more. `aes*-kwp` wraps +//! anything from 1 byte to 2^32 - 1 bytes and unwraps ciphertexts of 2 or more whole semiblocks. +//! Other lengths are rejected with an explanation rather than padded or truncated. +//! +//! # Unwrap fails closed +//! +//! Both algorithms are authenticated. A ciphertext that was tampered with, was wrapped under a +//! different KEK, or was produced by the other algorithm (KW vs KWP) makes `unwrap` exit non-zero +//! with nothing written to stdout. It does not say which of those it was; that is deliberate. +//! +//! # Binary in, binary out +//! +//! stdin is read as binary. `-x` renders the output as hex. For hex input, pipe through +//! `hex-decode` first: +//! +//! ```text +//! echo -n 00112233445566778899aabbccddeeff | bc-rust hex-decode \ +//! | bc-rust aes128-kw wrap --key 000102030405060708090a0b0c0d0e0f -x +//! ``` + +use crate::helpers::block_mode_helpers::load_key; +use crate::helpers::write_bytes_or_hex; +use bouncycastle::aes::{ + AES_KW_128, AES_KW_192, AES_KW_256, AES_KWP_128, AES_KWP_192, AES_KWP_256, +}; +use bouncycastle::core::errors::SymmetricCipherError; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::{KeyUnwrapper, KeyWrapper}; +use clap::ValueEnum; +use std::io::Read; +use std::process::exit; + +/// Which direction to run. +#[derive(ValueEnum, Clone, Debug)] +pub(crate) enum KeyWrapAction { + /// Wrap stdin under the KEK and write the ciphertext to stdout. + Wrap, + /// Unwrap stdin under the KEK and write the recovered data to stdout, or exit non-zero if + /// the ciphertext is not authentic. + Unwrap, +} + +pub(crate) fn aes128_kw_cmd( + action: &KeyWrapAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::( + action, + &load_key::<16>(key, key_file, "AES-128-KW"), + output_hex, + "AES-128-KW", + ); +} + +pub(crate) fn aes192_kw_cmd( + action: &KeyWrapAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::( + action, + &load_key::<24>(key, key_file, "AES-192-KW"), + output_hex, + "AES-192-KW", + ); +} + +pub(crate) fn aes256_kw_cmd( + action: &KeyWrapAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::( + action, + &load_key::<32>(key, key_file, "AES-256-KW"), + output_hex, + "AES-256-KW", + ); +} + +pub(crate) fn aes128_kwp_cmd( + action: &KeyWrapAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::( + action, + &load_key::<16>(key, key_file, "AES-128-KWP"), + output_hex, + "AES-128-KWP", + ); +} + +pub(crate) fn aes192_kwp_cmd( + action: &KeyWrapAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::( + action, + &load_key::<24>(key, key_file, "AES-192-KWP"), + output_hex, + "AES-192-KWP", + ); +} + +pub(crate) fn aes256_kwp_cmd( + action: &KeyWrapAction, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::( + action, + &load_key::<32>(key, key_file, "AES-256-KWP"), + output_hex, + "AES-256-KWP", + ); +} + +/// Reads all of stdin, wraps or unwraps it under `kek` with the algorithm `W`, and writes the +/// result. `alg` names the algorithm in error messages. +fn run( + action: &KeyWrapAction, + kek: &KeyMaterial, + output_hex: bool, + alg: &str, +) where + W: KeyWrapper + KeyUnwrapper, +{ + let mut input = Vec::new(); + if let Err(e) = std::io::stdin().read_to_end(&mut input) { + eprintln!("Error: couldn't read stdin: {e}"); + exit(-1); + } + + let (result, mut output) = match action { + KeyWrapAction::Wrap => { + let mut output = vec![0u8; W::wrap_out_len(input.len())]; + (W::wrap_out(kek, &input, &mut output), output) + } + KeyWrapAction::Unwrap => { + let mut output = vec![0u8; W::unwrap_out_max_len(input.len())]; + (W::unwrap_out(kek, &input, &mut output), output) + } + }; + + match result { + Ok(n) => { + output.truncate(n); + write_bytes_or_hex(&output, output_hex); + } + Err(SymmetricCipherError::DecryptionFailed) => { + eprintln!( + "Error: {alg} unwrap failed: the input is not an authentic {alg} ciphertext under \ + this key (it was tampered with, wrapped under a different key, or produced by a \ + different algorithm)." + ); + exit(-1); + } + Err(SymmetricCipherError::InvalidInputLength(why)) => { + eprintln!("Error: {alg}: input of {} bytes rejected: {why}.", input.len()); + exit(-1); + } + Err(e) => { + eprintln!("Error: {alg} failed: {e:?}"); + exit(-1); + } + } +} diff --git a/cli/src/main.rs b/cli/src/main.rs index bc8ee86ee..055d898d6 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -7,6 +7,7 @@ mod aes_cfb_cmd; mod aes_ctr_cmd; mod aes_ecb_cmd; mod aes_gcm_cmd; +mod aes_kw_cmd; mod ascon_cmd; mod chacha20_cmd; mod chacha20_poly1305_cmd; @@ -2182,6 +2183,157 @@ enum Subcommands { x: bool, }, + /// AES-128 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `wrap` or `unwrap` stdin to stdout. + /// + /// Wraps a key -- or any data that is a whole number of 8-byte blocks, at least 16 bytes -- + /// under the given key-encryption key (KEK). The output is 8 bytes longer than the input. + /// There is NO IV: wrapping is deterministic, and nothing is prepended on `wrap` or consumed + /// on `unwrap`. + /// + /// `unwrap` is authenticated. A ciphertext that was tampered with, wrapped under a different + /// KEK, or produced by aes*-kwp makes the command exit non-zero with nothing written. Input to + /// `unwrap` must be a whole number of 8-byte blocks, at least 24 bytes. + /// + /// This command is not streaming: all of stdin is read before anything is written, because + /// the algorithm makes six passes over the whole input. For data that is not a multiple of + /// 8 bytes, use aes128-kwp. + /// + /// Note: in production uses, secrets should not be passed on the command-line because they get + /// logged in shell history. Use the file-based input instead. + AES128_KW { + action: aes_kw_cmd::KeyWrapAction, + + /// The 16-byte AES key-encryption key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte AES key-encryption key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-192 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `wrap` or `unwrap` stdin to stdout. + /// + /// See `aes128-kw` for the input-length rules, the absence of an IV and the non-streaming + /// note; only the key length differs. + AES192_KW { + action: aes_kw_cmd::KeyWrapAction, + + /// The 24-byte AES key-encryption key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 24-byte AES key-encryption key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-256 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `wrap` or `unwrap` stdin to stdout. + /// + /// See `aes128-kw` for the input-length rules, the absence of an IV and the non-streaming + /// note; only the key length differs. + AES256_KW { + action: aes_kw_cmd::KeyWrapAction, + + /// The 32-byte AES key-encryption key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte AES key-encryption key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-128 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `wrap` or `unwrap` + /// stdin to stdout. + /// + /// As aes128-kw, but for data of any length from 1 byte up: the input is padded to a whole + /// number of 8-byte blocks and the output is that length plus 8. Its ciphertexts are not + /// interchangeable with aes128-kw's. Input to `unwrap` must be a whole number of 8-byte + /// blocks, at least 16 bytes. + /// + /// See `aes128-kw` for the absence of an IV, the authenticated `unwrap` and the non-streaming + /// note. + AES128_KWP { + action: aes_kw_cmd::KeyWrapAction, + + /// The 16-byte AES key-encryption key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte AES key-encryption key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-192 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `wrap` or `unwrap` + /// stdin to stdout. + /// + /// See `aes128-kwp`; only the key length differs. + AES192_KWP { + action: aes_kw_cmd::KeyWrapAction, + + /// The 24-byte AES key-encryption key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 24-byte AES key-encryption key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-256 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `wrap` or `unwrap` + /// stdin to stdout. + /// + /// See `aes128-kwp`; only the key length differs. + AES256_KWP { + action: aes_kw_cmd::KeyWrapAction, + + /// The 32-byte AES key-encryption key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte AES key-encryption key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// The ML-KEM-512 key encapsulation algorithm. MLKEM512 { action: mlkem_cmd::MLKEMAction, @@ -3008,6 +3160,24 @@ fn run() { mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; mldsa_cmd::mldsa_cmd(&mldsa_cmd::MLDSA87_SET, action, &files, None, *deterministic, *x); } + Some(Subcommands::AES128_KW { action, key, key_file, x }) => { + aes_kw_cmd::aes128_kw_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES192_KW { action, key, key_file, x }) => { + aes_kw_cmd::aes192_kw_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES256_KW { action, key, key_file, x }) => { + aes_kw_cmd::aes256_kw_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES128_KWP { action, key, key_file, x }) => { + aes_kw_cmd::aes128_kwp_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES192_KWP { action, key, key_file, x }) => { + aes_kw_cmd::aes192_kwp_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES256_KWP { action, key, key_file, x }) => { + aes_kw_cmd::aes256_kwp_cmd(action, key, key_file, *x); + } Some(Subcommands::MLKEM512 { action, skfile, pkfile, ctfile, x }) => { mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM512_SET, action, skfile, pkfile, ctfile, *x); } diff --git a/cli/tests/aes_kw_cli_tests.rs b/cli/tests/aes_kw_cli_tests.rs new file mode 100644 index 000000000..64f4c0268 --- /dev/null +++ b/cli/tests/aes_kw_cli_tests.rs @@ -0,0 +1,293 @@ +//! Tests for the `aes{128,192,256}-kw` and `aes{128,192,256}-kwp` subcommands. +//! +//! These drive the built `bc-rust` binary as a subprocess, because what is worth testing is the +//! command-line contract: the `wrap` / `unwrap` spelling, no IV framing, whole-input reading, the +//! input-length rules, exit codes on a rejected ciphertext, and key loading. The algorithms are +//! tested in `bouncycastle-modes` against the RFC and ACVP vectors; the same RFC vectors are +//! replayed here in both directions, which key wrap's determinism makes possible. +//! +//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the +//! current profile, so there is nothing to build or locate by hand. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; +use std::thread; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// RFC 3394 Sec 4: every example uses these KEK bytes (truncated to the key length) and this key +/// data (truncated to the data length). +const RFC3394_KEK: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; +const RFC3394_KEY_DATA: &str = "00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f"; +/// Sec 4.1: 128 bits of key data, 128-bit KEK. +const RFC3394_4_1_CT: &str = "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5"; +/// Sec 4.4: 192 bits of key data, 192-bit KEK. +const RFC3394_4_4_CT: &str = "031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2"; +/// Sec 4.6: 256 bits of key data, 256-bit KEK. +const RFC3394_4_6_CT: &str = + "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21"; + +/// RFC 5649 Sec 6: both examples use a 192-bit KEK. +const RFC5649_KEK: &str = "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"; +const RFC5649_KEY_20: &str = "c37b7e6492584340bed12207808941155068f738"; +const RFC5649_CT_20: &str = "138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a"; +const RFC5649_KEY_7: &str = "466f7250617369"; +const RFC5649_CT_7: &str = "afbeb0f07dfbf5419200f2ccb50bb24f"; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +/// +/// stdin is written from a separate thread so that a large payload cannot deadlock against a +/// full stdout pipe; see the same helper in `aes_ecb_cli_tests.rs` for the full reasoning. A +/// broken pipe on write is expected when the child exits early on an error path. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || { + match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + // `stdin` drops here, closing the pipe so the child sees EOF and can exit. + }); + + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string and asserting that +/// nothing was written to stdout. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + assert!(out.stdout.is_empty(), "a failed command must write nothing to stdout"); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +// ---- the RFC vectors, both directions ------------------------------------------------------ + +#[test] +fn aes128_kw_matches_rfc3394_4_1_in_both_directions() { + let kek = &RFC3394_KEK[..32]; + let key_data = unhex(&RFC3394_KEY_DATA[..32]); + + let wrapped = run_ok(&["aes128-kw", "wrap", "--key", kek], &key_data); + assert_eq!(tohex(&wrapped), RFC3394_4_1_CT); + + let unwrapped = run_ok(&["aes128-kw", "unwrap", "--key", kek], &wrapped); + assert_eq!(unwrapped, key_data); +} + +#[test] +fn aes192_kw_matches_rfc3394_4_4_in_both_directions() { + let kek = &RFC3394_KEK[..48]; + let key_data = unhex(&RFC3394_KEY_DATA[..48]); + + let wrapped = run_ok(&["aes192-kw", "wrap", "--key", kek], &key_data); + assert_eq!(tohex(&wrapped), RFC3394_4_4_CT); + assert_eq!(run_ok(&["aes192-kw", "unwrap", "--key", kek], &wrapped), key_data); +} + +#[test] +fn aes256_kw_matches_rfc3394_4_6_in_both_directions() { + let key_data = unhex(RFC3394_KEY_DATA); + + let wrapped = run_ok(&["aes256-kw", "wrap", "--key", RFC3394_KEK], &key_data); + assert_eq!(tohex(&wrapped), RFC3394_4_6_CT); + assert_eq!(run_ok(&["aes256-kw", "unwrap", "--key", RFC3394_KEK], &wrapped), key_data); +} + +#[test] +fn aes192_kwp_matches_rfc5649_in_both_directions() { + // 20 octets: padded to 24, through the wrapping function. + let key_20 = unhex(RFC5649_KEY_20); + let wrapped = run_ok(&["aes192-kwp", "wrap", "--key", RFC5649_KEK], &key_20); + assert_eq!(tohex(&wrapped), RFC5649_CT_20); + assert_eq!(run_ok(&["aes192-kwp", "unwrap", "--key", RFC5649_KEK], &wrapped), key_20); + + // 7 octets: a single block. The unwrapped output is exactly 7 bytes, not the padded 8. + let key_7 = unhex(RFC5649_KEY_7); + let wrapped = run_ok(&["aes192-kwp", "wrap", "--key", RFC5649_KEK], &key_7); + assert_eq!(tohex(&wrapped), RFC5649_CT_7); + assert_eq!(run_ok(&["aes192-kwp", "unwrap", "--key", RFC5649_KEK], &wrapped), key_7); +} + +// ---- the command-line contract ------------------------------------------------------------- + +#[test] +fn hex_output_matches_binary_output() { + let key_data = unhex(&RFC3394_KEY_DATA[..32]); + let binary = run_ok(&["aes128-kw", "wrap", "--key", &RFC3394_KEK[..32]], &key_data); + let hex = run_ok(&["aes128-kw", "wrap", "--key", &RFC3394_KEK[..32], "-x"], &key_data); + assert_eq!(String::from_utf8(hex).unwrap(), tohex(&binary)); +} + +#[test] +fn wrapping_is_deterministic_and_adds_exactly_one_semiblock() { + let kek = &RFC3394_KEK[..32]; + let key_data = [0x5Au8; 32]; + let a = run_ok(&["aes128-kw", "wrap", "--key", kek], &key_data); + let b = run_ok(&["aes128-kw", "wrap", "--key", kek], &key_data); + assert_eq!(a, b, "no IV, so the same input always gives the same output"); + assert_eq!(a.len(), key_data.len() + 8); + + let a = run_ok(&["aes128-kwp", "wrap", "--key", kek], &key_data); + assert_eq!(a.len(), key_data.len() + 8, "aligned data costs KWP nothing extra"); + let a = run_ok(&["aes128-kwp", "wrap", "--key", kek], &key_data[..13]); + assert_eq!(a.len(), 16 + 8, "13 bytes pad to 16"); +} + +#[test] +fn a_tampered_ciphertext_is_rejected_with_no_output() { + let kek = &RFC3394_KEK[..32]; + let mut wrapped = unhex(RFC3394_4_1_CT); + wrapped[11] ^= 0x01; + let err = run_err(&["aes128-kw", "unwrap", "--key", kek], &wrapped); + assert!(err.contains("not an authentic"), "stderr should explain: {err}"); + + let mut wrapped = unhex(RFC5649_CT_7); + wrapped[0] ^= 0x80; + let err = run_err(&["aes192-kwp", "unwrap", "--key", RFC5649_KEK], &wrapped); + assert!(err.contains("not an authentic"), "stderr should explain: {err}"); +} + +#[test] +fn the_wrong_kek_is_rejected() { + let wrapped = unhex(RFC3394_4_1_CT); + let wrong_kek = "0f0e0d0c0b0a09080706050403020100"; + let err = run_err(&["aes128-kw", "unwrap", "--key", wrong_kek], &wrapped); + assert!(err.contains("not an authentic"), "stderr should explain: {err}"); +} + +#[test] +fn kw_and_kwp_are_not_interchangeable() { + let kek = &RFC3394_KEK[..32]; + let key_data = [0x5Au8; 16]; + let kw = run_ok(&["aes128-kw", "wrap", "--key", kek], &key_data); + let kwp = run_ok(&["aes128-kwp", "wrap", "--key", kek], &key_data); + assert_ne!(kw, kwp); + run_err(&["aes128-kwp", "unwrap", "--key", kek], &kw); + run_err(&["aes128-kw", "unwrap", "--key", kek], &kwp); +} + +#[test] +fn kw_rejects_inputs_that_are_not_whole_semiblocks() { + let kek = &RFC3394_KEK[..32]; + // wrap: 8 bytes is one semiblock (two are needed); 20 is not a whole number of them. + let err = run_err(&["aes128-kw", "wrap", "--key", kek], &[0u8; 8]); + assert!(err.contains("8 bytes rejected"), "stderr should name the length: {err}"); + let err = run_err(&["aes128-kw", "wrap", "--key", kek], &[0u8; 20]); + assert!(err.contains("20 bytes rejected"), "stderr should name the length: {err}"); + // unwrap: 16 bytes is two semiblocks (three are needed). + let err = run_err(&["aes128-kw", "unwrap", "--key", kek], &[0u8; 16]); + assert!(err.contains("16 bytes rejected"), "stderr should name the length: {err}"); + // KWP takes the unaligned length KW refused. + assert_eq!(run_ok(&["aes128-kwp", "wrap", "--key", kek], &[0u8; 20]).len(), 32); +} + +#[test] +fn empty_input_is_rejected_by_both() { + let kek = &RFC3394_KEK[..32]; + let err = run_err(&["aes128-kw", "wrap", "--key", kek], &[]); + assert!(err.contains("0 bytes rejected"), "stderr should name the length: {err}"); + let err = run_err(&["aes128-kwp", "wrap", "--key", kek], &[]); + assert!(err.contains("0 bytes rejected"), "stderr should name the length: {err}"); + let err = run_err(&["aes128-kwp", "unwrap", "--key", kek], &[]); + assert!(err.contains("0 bytes rejected"), "stderr should name the length: {err}"); +} + +#[test] +fn a_larger_payload_round_trips_through_kwp() { + let kek = &RFC3394_KEK[..32]; + // An "encoded private key" sized payload that is not a multiple of 8. + let payload: Vec = + (0..3001u32).map(|i| (i.wrapping_mul(2_654_435_761) >> 24) as u8).collect(); + let wrapped = run_ok(&["aes256-kwp", "wrap", "--key", RFC3394_KEK], &payload); + assert_eq!(wrapped.len(), 3008 + 8); + assert_eq!(run_ok(&["aes256-kwp", "unwrap", "--key", RFC3394_KEK], &wrapped), payload); + // ... and KW refuses it, because it is not aligned. + run_err(&["aes256-kw", "wrap", "--key", kek], &payload); +} + +#[test] +fn key_file_accepts_hex_and_binary() { + let dir = std::env::temp_dir().join(format!("bc-rust-kw-cli-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let hex_path = dir.join("kek.hex"); + let bin_path = dir.join("kek.bin"); + std::fs::write(&hex_path, &RFC3394_KEK[..32]).unwrap(); + std::fs::write(&bin_path, unhex(&RFC3394_KEK[..32])).unwrap(); + + let key_data = unhex(&RFC3394_KEY_DATA[..32]); + let from_hex = + run_ok(&["aes128-kw", "wrap", "--key-file", hex_path.to_str().unwrap()], &key_data); + let from_bin = + run_ok(&["aes128-kw", "wrap", "--key-file", bin_path.to_str().unwrap()], &key_data); + assert_eq!(tohex(&from_hex), RFC3394_4_1_CT); + assert_eq!(from_hex, from_bin); + + std::fs::remove_dir_all(&dir).unwrap(); +} + +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let err = run_err(&["aes128-kw", "wrap", "--key", &RFC3394_KEK[..48]], &[0u8; 16]); + assert!(err.contains("16-byte key"), "stderr should say what was expected: {err}"); + let err = run_err(&["aes256-kwp", "wrap", "--key", &RFC3394_KEK[..32]], &[0u8; 16]); + assert!(err.contains("32-byte key"), "stderr should say what was expected: {err}"); +} + +#[test] +fn a_missing_key_is_rejected() { + let err = run_err(&["aes128-kw", "wrap"], &[0u8; 16]); + assert!(err.contains("--key"), "stderr should point at the option: {err}"); +} + +#[test] +fn the_subcommands_are_listed_in_help() { + let help = String::from_utf8(run_ok(&["--help"], &[])).unwrap(); + for name in ["aes128-kw", "aes192-kw", "aes256-kw", "aes128-kwp", "aes192-kwp", "aes256-kwp"] { + assert!(help.contains(name), "{name} should be listed in --help"); + } + let help = String::from_utf8(run_ok(&["aes128-kw", "--help"], &[])).unwrap(); + assert!(help.contains("wrap") && help.contains("unwrap")); + assert!(help.contains("RFC 3394")); +} diff --git a/crypto/aes/src/kw.rs b/crypto/aes/src/kw.rs new file mode 100644 index 000000000..06cba4aa5 --- /dev/null +++ b/crypto/aes/src/kw.rs @@ -0,0 +1,129 @@ +//! Type aliases for AES Key Wrap, KW (NIST SP 800-38F Sec 6.2; RFC 3394), and AES Key Wrap with +//! Padding, KWP (Sec 6.3; RFC 5649). +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Kw` and `Kwp` take the permutation +//! and its key length. These aliases pin both: +//! +//! ```text +//! AES_KW_128 // AES-128 KW: whole 8-byte semiblocks in, one semiblock more out +//! AES_KWP_256 // AES-256 KWP: any length in, rounded up to 8 plus 8 out +//! ``` +//! +//! There is no direction parameter: wrapping and unwrapping are the separate [`KeyWrapper`] and +//! [`KeyUnwrapper`] traits, both implemented by each alias, and both consisting of associated +//! functions. See [`bouncycastle_modes::kw`] for the API, the in-place wrapping function and the +//! security notes, and [`bouncycastle_modes::kwp`] for what padding adds. +//! +//! # Which one +//! +//! **KW** is what RFC 3394, CMS (RFC 3565) and JOSE's `A128KW` family mean by "AES key wrap": it +//! wraps a symmetric key that is a whole number of 8 bytes, which every AES, HMAC and ChaCha key +//! is. **KWP** wraps anything from 1 byte up, at the cost of one extra semiblock when the input +//! was already aligned; it is what RFC 5649 and PKCS#11's `CKM_AES_KEY_WRAP_KWP` mean, and the +//! one to use for encoded private keys and other non-aligned data. Their ciphertexts are not +//! interchangeable: the integrity check values differ, so unwrapping one with the other fails. +//! +//! The RFC 3394 and RFC 5649 OIDs (`id-aes128-wrap` `{ aes 5 }`, `id-aes128-wrap-pad` `{ aes 8 }`, +//! and the 192- and 256-bit siblings at 25/28 and 45/48) are not attached to these aliases yet: +//! none of the AES mode aliases carries an [`AlgorithmOID`] on this branch, and a mode alias is not +//! a local type this crate could implement one for. + +use crate::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_modes::{Kw, Kwp}; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::traits::{AlgorithmOID, KeyUnwrapper, KeyWrapper}; +// end of imports needed for docs + +/// AES-128 Key Wrap (SP 800-38F KW; RFC 3394 with a 128-bit KEK). +/// +/// This is RFC 3394 Sec 4.1, "Wrap 128 bits of Key Data with a 128-bit KEK", as a doctest: +/// +/// ``` +/// use bouncycastle_aes::AES_KW_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +/// +/// // KEK 000102030405060708090A0B0C0D0E0F, key data 00112233445566778899AABBCCDDEEFF. +/// let kek_bytes: [u8; 16] = core::array::from_fn(|i| i as u8); +/// let key_data: [u8; 16] = core::array::from_fn(|i| (i as u8) * 0x11); +/// let kek = KeyMaterial::<16>::from_bytes_as_type(&kek_bytes, KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// +/// let wrapped: [u8; 24] = AES_KW_128::wrap_key(&kek, &key_data).expect("wrapping"); +/// assert_eq!( +/// wrapped, +/// [ +/// 0x1F, 0xA6, 0x8B, 0x0A, 0x81, 0x12, 0xB4, 0x47, 0xAE, 0xF3, 0x4B, 0xD8, 0xFB, 0x5A, +/// 0x7B, 0x82, 0x9D, 0x3E, 0x86, 0x23, 0x71, 0xD2, 0xCF, 0xE5, +/// ] +/// ); +/// let recovered = AES_KW_128::unwrap_key::<16, 24>(&kek, &wrapped).expect("unwrapping"); +/// assert_eq!(*recovered, key_data); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_KW_128 = Kw; + +/// AES-192 Key Wrap (SP 800-38F KW; RFC 3394 with a 192-bit KEK). See [`AES_KW_128`]. +#[allow(non_camel_case_types)] +pub type AES_KW_192 = Kw; + +/// AES-256 Key Wrap (SP 800-38F KW; RFC 3394 with a 256-bit KEK). See [`AES_KW_128`]. +#[allow(non_camel_case_types)] +pub type AES_KW_256 = Kw; + +/// AES-128 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 128-bit KEK). +/// +/// ``` +/// use bouncycastle_aes::AES_KWP_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +/// +/// let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// +/// // Any length: 13 bytes pad to 16, plus the 8-byte header. +/// let data = *b"thirteen bytes"; // 14, actually -- still 16 + 8. +/// let wrapped: [u8; 24] = AES_KWP_128::wrap_key(&kek, &data).expect("wrapping"); +/// let recovered = AES_KWP_128::unwrap_key::<14, 24>(&kek, &wrapped).expect("unwrapping"); +/// assert_eq!(*recovered, data); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_KWP_128 = Kwp; + +/// AES-192 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 192-bit KEK). +/// +/// This is the second example of RFC 5649 Sec 6, which wraps 7 octets -- short enough that the +/// header and data fit one AES block, the case Algorithm 5 step 5 enciphers directly: +/// +/// ``` +/// use bouncycastle_aes::AES_KWP_192; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +/// +/// let kek_bytes = [ +/// 0x58, 0x40, 0xdf, 0x6e, 0x29, 0xb0, 0x2a, 0xf1, 0xab, 0x49, 0x3b, 0x70, 0x5b, 0xf1, 0x6e, +/// 0xa1, 0xae, 0x83, 0x38, 0xf4, 0xdc, 0xc1, 0x76, 0xa8, +/// ]; +/// let kek = KeyMaterial::<24>::from_bytes_as_type(&kek_bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// let key_data = [0x46, 0x6f, 0x72, 0x50, 0x61, 0x73, 0x69]; +/// +/// let wrapped: [u8; 16] = AES_KWP_192::wrap_key(&kek, &key_data).expect("wrapping"); +/// assert_eq!( +/// wrapped, +/// [ +/// 0xaf, 0xbe, 0xb0, 0xf0, 0x7d, 0xfb, 0xf5, 0x41, 0x92, 0x00, 0xf2, 0xcc, 0xb5, 0x0b, +/// 0xb2, 0x4f, +/// ] +/// ); +/// assert_eq!(*AES_KWP_192::unwrap_key::<7, 16>(&kek, &wrapped).expect("unwrapping"), key_data); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_KWP_192 = Kwp; + +/// AES-256 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 256-bit KEK). See +/// [`AES_KWP_128`]. +#[allow(non_camel_case_types)] +pub type AES_KWP_256 = Kwp; diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs index 52388f51d..251d9d8d0 100644 --- a/crypto/aes/src/lib.rs +++ b/crypto/aes/src/lib.rs @@ -23,6 +23,7 @@ //! * [`AES_CTR_128`] //! * [`AES_GCM_128`] //! * [`AES_GMAC_128`], a nonce-based MAC rather than a cipher +//! * [`AES_KW_128`] and [`AES_KWP_128`], key wraps rather than ciphers //! //! Each mode is used through a mode-bound key object such as [`AES256Key`](AES256Key), //! which validates and expands the material once and hands out the mode's encryptors and @@ -193,6 +194,7 @@ mod gcm; mod gmac; pub mod hazmat; mod key; +mod kw; mod round; mod sbox; mod schedule; @@ -282,3 +284,4 @@ pub use ctr::{AES_CTR_128, AES_CTR_192, AES_CTR_256, CTR_NONCE_LEN}; pub use gcm::{AES_GCM_128, AES_GCM_192, AES_GCM_256, GCM_NONCE_LEN}; pub use gmac::{AES_GMAC_128, AES_GMAC_192, AES_GMAC_256}; pub use key::{AES128Key, AES192Key, AES256Key, AESKey}; +pub use kw::{AES_KW_128, AES_KW_192, AES_KW_256, AES_KWP_128, AES_KWP_192, AES_KWP_256}; diff --git a/crypto/aes/tests/acvp_kw_tests.rs b/crypto/aes/tests/acvp_kw_tests.rs new file mode 100644 index 000000000..8e8642cf0 --- /dev/null +++ b/crypto/aes/tests/acvp_kw_tests.rs @@ -0,0 +1,290 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-KW` and `ACVP-AES-KWP` vectors from the +//! `bc-test-data` repo. +//! +//! Requires `bc-test-data` to be cloned alongside this repository, i.e. at `../bc-test-data` +//! relative to the root of this git project. If it is absent the tests print a warning and pass, +//! matching the convention of the other ACVP suites -- `cargo test` must stay green for someone +//! who has only cloned this repository. +//! +//! # Joining the request and response files +//! +//! As for the CBC set, the response file carries the answer against a `tcId` and the request file +//! carries the inputs and the group metadata (`direction`, `keyLen`, `payloadLen`, `kwCipher`), so +//! both are read and joined on `tcId`. +//! +//! # Coverage +//! +//! Each set has 1800 AFT cases: 300 per direction per AES key length, over payloads of 128, 192 +//! and 512 bits. The decrypt groups include cases whose ciphertext has been corrupted, marked +//! `testPassed: false` in the response; those must be rejected with `DecryptionFailed` and nothing +//! else, and the test counts them so the coverage stays visible. Every case is run through the +//! run-time (`wrap_out` / `unwrap_out`) API, and since all three payload lengths are fixed, through +//! the compile-time (`wrap_key` / `unwrap_key`) API as well. +//! +//! `kwCipher` is `"cipher"` throughout: the forward cipher is the designated cipher function +//! (SP 800-38F Sec 5.1). The "inverse" variant, where decryption wraps, is not implemented and +//! would be skipped and counted if a group asked for it. + +use bouncycastle_aes::{AES_KW_128, AES_KW_192, AES_KW_256, AES_KWP_128, AES_KWP_192, AES_KWP_256}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +use bouncycastle_hex as hex; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = [ + "../../../bc-test-data/crypto/aes_tdes_vectors/AES", + "../bc-test-data/crypto/aes_tdes_vectors/AES", +]; + +const KW_REQUEST_FILE: &str = "ACVP-AES-KW.4014525.req.json"; +const KW_RESPONSE_FILE: &str = "ACVP-AES-KW.4014525.rsp.json"; +const KWP_REQUEST_FILE: &str = "ACVP-AES-KWP.4014526.req.json"; +const KWP_RESPONSE_FILE: &str = "ACVP-AES-KWP.4014526.rsp.json"; + +fn test_data_dir(request_file: &str, response_file: &str) -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate); + if path.join(request_file).exists() && path.join(response_file).exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); \ + ACVP {request_file} tests will be skipped" + ); + None +} + +/// The ACVP files are a two-element array: a version header, then the payload. +fn load(path: &Path) -> Value { + let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("reading {path:?}: {e}")); + let value: Value = + serde_json::from_str(&text).unwrap_or_else(|e| panic!("parsing {path:?}: {e}")); + value + .as_array() + .and_then(|a| a.get(1)) + .cloned() + .unwrap_or_else(|| panic!("{path:?}: not a two-element ACVP array")) +} + +/// Builds a `KeyMaterial` from raw ACVP key bytes, including the all-zero keys the set contains. +fn cipher_key(bytes: &[u8]) -> KeyMaterial { + assert_eq!(bytes.len(), N, "key length should match the group's keyLen"); + let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("ACVP key bytes fit the buffer"); + if key.key_type() != KeyType::SymmetricCipherKey { + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::from_bytes(N)) + }) + .expect("promoting a NIST all-zero test key"); + } + key +} + +fn hex_field(test: &Value, name: &str) -> Vec { + let tc_id = test["tcId"].as_u64().unwrap_or(0); + let s = test[name].as_str().unwrap_or_else(|| panic!("tcId {tc_id}: missing field {name}")); + hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc_id}: bad hex in {name}")) +} + +/// What one case expects. +enum Expected { + /// The wrap or unwrap must succeed and produce these bytes. + Bytes(Vec), + /// The unwrap must fail: the vector's ciphertext is a forgery. + Failure, +} + +#[derive(Default)] +struct Stats { + encrypt: usize, + decrypt: usize, + forgeries: usize, + skipped: usize, +} + +/// Runs one case through both APIs. `W` is the alias for the group's key length. +fn run_case( + key_bytes: &[u8], + input: &[u8], + encrypt: bool, + expected: &Expected, + tc_id: u64, +) where + W: KeyWrapper + KeyUnwrapper, +{ + let kek = cipher_key::(key_bytes); + + if encrypt { + let Expected::Bytes(ct) = expected else { + panic!("tcId {tc_id}: an encrypt case cannot expect failure"); + }; + let mut out = vec![0u8; W::wrap_out_len(input.len())]; + let n = W::wrap_out(&kek, input, &mut out) + .unwrap_or_else(|e| panic!("tcId {tc_id}: wrap_out: {e:?}")); + assert_eq!(&out[..n], ct, "tcId {tc_id}: wrap_out ciphertext"); + + // The fixed-length API, for the payload lengths the set uses. + let fixed = match input.len() { + 16 => W::wrap_key::<16, 24>(&kek, input.try_into().unwrap()).map(|c| c.to_vec()), + 24 => W::wrap_key::<24, 32>(&kek, input.try_into().unwrap()).map(|c| c.to_vec()), + 64 => W::wrap_key::<64, 72>(&kek, input.try_into().unwrap()).map(|c| c.to_vec()), + other => panic!("tcId {tc_id}: unexpected payload length {other}"), + }; + assert_eq!(fixed.unwrap().as_slice(), ct, "tcId {tc_id}: wrap_key ciphertext"); + } else { + let mut out = vec![0u8; W::unwrap_out_max_len(input.len())]; + let result = W::unwrap_out(&kek, input, &mut out); + match expected { + Expected::Bytes(pt) => { + let n = result.unwrap_or_else(|e| panic!("tcId {tc_id}: unwrap_out: {e:?}")); + assert_eq!(&out[..n], pt, "tcId {tc_id}: unwrap_out plaintext"); + let fixed = + match pt.len() { + 16 => W::unwrap_key::<16, 24>(&kek, input.try_into().unwrap()) + .map(|k| k.to_vec()), + 24 => W::unwrap_key::<24, 32>(&kek, input.try_into().unwrap()) + .map(|k| k.to_vec()), + 64 => W::unwrap_key::<64, 72>(&kek, input.try_into().unwrap()) + .map(|k| k.to_vec()), + other => panic!("tcId {tc_id}: unexpected payload length {other}"), + }; + assert_eq!(fixed.unwrap().as_slice(), pt, "tcId {tc_id}: unwrap_key plaintext"); + } + Expected::Failure => { + assert!( + matches!(result, Err(SymmetricCipherError::DecryptionFailed)), + "tcId {tc_id}: a forged ciphertext must fail with DecryptionFailed, got {result:?}" + ); + assert!( + out.iter().all(|b| *b == 0), + "tcId {tc_id}: the buffer is scrubbed on failure" + ); + let fixed: Result, _> = + match input.len() { + 24 => W::unwrap_key::<16, 24>(&kek, input.try_into().unwrap()) + .map(|k| k.to_vec()), + 32 => W::unwrap_key::<24, 32>(&kek, input.try_into().unwrap()) + .map(|k| k.to_vec()), + 72 => W::unwrap_key::<64, 72>(&kek, input.try_into().unwrap()) + .map(|k| k.to_vec()), + other => panic!("tcId {tc_id}: unexpected ciphertext length {other}"), + }; + assert!( + matches!(fixed, Err(SymmetricCipherError::DecryptionFailed)), + "tcId {tc_id}: unwrap_key must reject the forgery too" + ); + } + } + } +} + +/// Walks one ACVP set, dispatching each group to the alias for its key length. +fn run_set( + algorithm: &str, + request_file: &str, + response_file: &str, +) -> Option +where + W128: KeyWrapper<16> + KeyUnwrapper<16>, + W192: KeyWrapper<24> + KeyUnwrapper<24>, + W256: KeyWrapper<32> + KeyUnwrapper<32>, +{ + let dir = test_data_dir(request_file, response_file)?; + let request = load(&dir.join(request_file)); + let response = load(&dir.join(response_file)); + assert_eq!(request["algorithm"].as_str(), Some(algorithm), "wrong vector file"); + + // Index the answers by tcId across every group. + let mut answers: BTreeMap = BTreeMap::new(); + for group in response["testGroups"].as_array().expect("response testGroups") { + for test in group["tests"].as_array().expect("response tests") { + answers.insert(test["tcId"].as_u64().expect("tcId"), test); + } + } + + let mut stats = Stats::default(); + for group in request["testGroups"].as_array().expect("request testGroups") { + assert_eq!(group["testType"].as_str(), Some("AFT"), "only AFT groups are expected"); + if group["kwCipher"].as_str() != Some("cipher") { + stats.skipped += group["tests"].as_array().map_or(0, Vec::len); + continue; + } + let encrypt = match group["direction"].as_str() { + Some("encrypt") => true, + Some("decrypt") => false, + other => panic!("unexpected direction {other:?}"), + }; + let key_len = group["keyLen"].as_u64().expect("keyLen"); + + for test in group["tests"].as_array().expect("request tests") { + let tc_id = test["tcId"].as_u64().expect("tcId"); + let answer = answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + let key = hex_field(test, "key"); + let (input, expected) = if encrypt { + (hex_field(test, "pt"), Expected::Bytes(hex_field(answer, "ct"))) + } else if answer["testPassed"] == Value::Bool(false) { + (hex_field(test, "ct"), Expected::Failure) + } else { + (hex_field(test, "ct"), Expected::Bytes(hex_field(answer, "pt"))) + }; + + match key_len { + 128 => run_case::(&key, &input, encrypt, &expected, tc_id), + 192 => run_case::(&key, &input, encrypt, &expected, tc_id), + 256 => run_case::(&key, &input, encrypt, &expected, tc_id), + other => panic!("unexpected keyLen {other}"), + } + + match (encrypt, &expected) { + (true, _) => stats.encrypt += 1, + (false, Expected::Bytes(_)) => stats.decrypt += 1, + (false, Expected::Failure) => stats.forgeries += 1, + } + } + } + Some(stats) +} + +#[test] +fn acvp_aes_kw() { + let Some(stats) = run_set::( + "ACVP-AES-KW", KW_REQUEST_FILE, KW_RESPONSE_FILE, + ) else { + return; + }; + println!( + "ACVP-AES-KW: {} wrap, {} unwrap, {} rejected forgeries, {} skipped", + stats.encrypt, stats.decrypt, stats.forgeries, stats.skipped + ); + assert_eq!(stats.encrypt, 900); + assert_eq!(stats.decrypt + stats.forgeries, 900); + assert!(stats.forgeries > 0, "the set should contain forged ciphertexts"); + assert_eq!(stats.skipped, 0); +} + +#[test] +fn acvp_aes_kwp() { + let Some(stats) = run_set::( + "ACVP-AES-KWP", KWP_REQUEST_FILE, KWP_RESPONSE_FILE, + ) else { + return; + }; + println!( + "ACVP-AES-KWP: {} wrap, {} unwrap, {} rejected forgeries, {} skipped", + stats.encrypt, stats.decrypt, stats.forgeries, stats.skipped + ); + assert_eq!(stats.encrypt, 900); + assert_eq!(stats.decrypt + stats.forgeries, 900); + assert!(stats.forgeries > 0, "the set should contain forged ciphertexts"); + assert_eq!(stats.skipped, 0); +} diff --git a/crypto/aes/tests/ecb_bc-test-data.rs b/crypto/aes/tests/ecb_bc-test-data.rs index f46341bd3..62da285fa 100644 --- a/crypto/aes/tests/ecb_bc-test-data.rs +++ b/crypto/aes/tests/ecb_bc-test-data.rs @@ -26,7 +26,7 @@ //! | `ACVP-AES-OFB` | nothing yet (OFB is unimplemented) | //! | `ACVP-AES-CTR` | `ctr_bc-test-data.rs` | //! | `ACVP-AES-GCM` / `-GMAC` | `gcm_bc-test-data.rs` / `gmac_bc-test-data.rs` | -//! | `ACVP-AES-KW` / `-KWP` | nothing yet (key wrap is unimplemented) | +//! | `ACVP-AES-KW` / `-KWP` | `crypto/modes/tests/acvp_kw_tests.rs` | //! | `ACVP-AES-FF1` / `-FF3-1` | nothing yet (format-preserving encryption is unimplemented) | //! //! So an unused vector set here means an unimplemented mode, not an untested one. Adding a mode diff --git a/crypto/aes/tests/kw_alias_tests.rs b/crypto/aes/tests/kw_alias_tests.rs new file mode 100644 index 000000000..083ad6e3d --- /dev/null +++ b/crypto/aes/tests/kw_alias_tests.rs @@ -0,0 +1,41 @@ +//! Tests for the AES-KW and AES-KWP aliases. +//! +//! These are only type aliases, so what is worth testing here is that each names the right +//! permutation and key length -- a 16-byte KEK for `AES_KW_128` and so on -- and that they run +//! the shared key-wrap framework. The algorithms themselves, and the RFC 3394 / RFC 5649 / ACVP +//! vectors, are tested in `bouncycastle-modes`. + +use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_aes::{AES_KW_128, AES_KW_192, AES_KW_256, AES_KWP_128, AES_KWP_192, AES_KWP_256}; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_core_test_framework::key_wrap::TestFrameworkKeyWrap; + +/// The alias carries its permutation's name and strength, as the other mode aliases do. +#[test] +fn the_aliases_name_the_expected_permutation() { + assert_eq!(AES_KW_128::ALG_NAME, AES128Internal::ALG_NAME); + assert_eq!(AES_KW_192::ALG_NAME, AES192Internal::ALG_NAME); + assert_eq!(AES_KW_256::ALG_NAME, AES256Internal::ALG_NAME); + assert_eq!(AES_KWP_128::ALG_NAME, AES128Internal::ALG_NAME); + assert_eq!(AES_KWP_192::ALG_NAME, AES192Internal::ALG_NAME); + assert_eq!(AES_KWP_256::ALG_NAME, AES256Internal::ALG_NAME); + + assert_eq!(AES_KW_128::MAX_SECURITY_STRENGTH, AES128Internal::MAX_SECURITY_STRENGTH); + assert_eq!(AES_KW_256::MAX_SECURITY_STRENGTH, AES256Internal::MAX_SECURITY_STRENGTH); + assert_eq!(AES_KWP_192::MAX_SECURITY_STRENGTH, AES192Internal::MAX_SECURITY_STRENGTH); +} + +/// Each alias conforms to the shared framework under its own KEK length, wrapping a 256-bit key. +#[test] +fn the_aliases_conform_to_the_key_wrap_framework() { + let tf = TestFrameworkKeyWrap::new(); + tf.test::<16, 32, 40, AES_KW_128>(); + tf.test::<24, 32, 40, AES_KW_192>(); + tf.test::<32, 32, 40, AES_KW_256>(); + tf.test::<16, 32, 40, AES_KWP_128>(); + tf.test::<24, 32, 40, AES_KWP_192>(); + tf.test::<32, 32, 40, AES_KWP_256>(); + // ... and KWP something that is not a whole number of semiblocks. + tf.test::<16, 33, 48, AES_KWP_128>(); + tf.test::<32, 7, 16, AES_KWP_256>(); +} diff --git a/crypto/aes/tests/kw_tests.rs b/crypto/aes/tests/kw_tests.rs new file mode 100644 index 000000000..4455fba19 --- /dev/null +++ b/crypto/aes/tests/kw_tests.rs @@ -0,0 +1,372 @@ +//! Known-answer and behaviour tests for KW and KWP over AES. +//! +//! The known answers are the published ones: RFC 3394 Sec 4 (all six KW vectors, covering every +//! AES key length and 128-, 192- and 256-bit key data) and RFC 5649 Sec 6 (both KWP examples, the +//! second of which is the 7-octet case that takes the single-block path of SP 800-38F Algorithm 5 +//! step 5 and Algorithm 6 step 3). SP 800-38F itself publishes no vectors; the NIST ACVP sets are +//! in `acvp_kw_tests.rs`. +//! +//! The behaviour tests run the shared `TestFrameworkKeyWrap` -- round trips, determinism, the +//! length helpers, every error condition -- for each AES key length at several payload lengths, +//! and then pin the properties that distinguish the two algorithms from each other and from a +//! plain block encryption. + +use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ElectronicCodeBook, KeyUnwrapper, KeyWrapper}; +use bouncycastle_core_test_framework::key_wrap::TestFrameworkKeyWrap; +use bouncycastle_hex as hex; +use bouncycastle_modes::kw::{ + kw_ciphertext_len_is_valid, kw_plaintext_len_is_valid, kw_wrapped_len, +}; +use bouncycastle_modes::kwp::{ + kwp_ciphertext_len_is_valid, kwp_plaintext_len_is_valid, kwp_wrapped_len, +}; +use bouncycastle_modes::{Kw, Kwp}; + +type Aes128Kw = Kw; +type Aes192Kw = Kw; +type Aes256Kw = Kw; +type Aes128Kwp = Kwp; +type Aes192Kwp = Kwp; +type Aes256Kwp = Kwp; + +fn bytes(hex_str: &str) -> [u8; N] { + hex::decode(hex_str).expect("valid hex").try_into().expect("the expected length") +} + +fn kek(hex_str: &str) -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&bytes::(hex_str), KeyType::SymmetricCipherKey) + .expect("a symmetric cipher key") +} + +// ---- RFC 3394 Sec 4: the KW vectors -------------------------------------------------------- + +/// RFC 3394 Sec 4.1-4.6 all use the same KEK bytes, truncated to the key length, and the same +/// key data, truncated to the data length. +const RFC3394_KEK: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; +const RFC3394_KEY_DATA: &str = "00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f"; + +/// Sec 4.1, "Wrap 128 bits of Key Data with a 128-bit KEK". +#[test] +fn rfc3394_4_1_wrap_128_bits_with_a_128_bit_kek() { + TestFrameworkKeyWrap::new().test_kat::<16, 16, 24, Aes128Kw>( + &bytes(&RFC3394_KEK[..32]), + &bytes(&RFC3394_KEY_DATA[..32]), + &bytes("1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5"), + ); +} + +/// Sec 4.2, "Wrap 128 bits of Key Data with a 192-bit KEK". +#[test] +fn rfc3394_4_2_wrap_128_bits_with_a_192_bit_kek() { + TestFrameworkKeyWrap::new().test_kat::<24, 16, 24, Aes192Kw>( + &bytes(&RFC3394_KEK[..48]), + &bytes(&RFC3394_KEY_DATA[..32]), + &bytes("96778b25ae6ca435f92b5b97c050aed2468ab8a17ad84e5d"), + ); +} + +/// Sec 4.3, "Wrap 128 bits of Key Data with a 256-bit KEK". +#[test] +fn rfc3394_4_3_wrap_128_bits_with_a_256_bit_kek() { + TestFrameworkKeyWrap::new().test_kat::<32, 16, 24, Aes256Kw>( + &bytes(RFC3394_KEK), + &bytes(&RFC3394_KEY_DATA[..32]), + &bytes("64e8c3f9ce0f5ba263e9777905818a2a93c8191e7d6e8ae7"), + ); +} + +/// Sec 4.4, "Wrap 192 bits of Key Data with a 192-bit KEK". +#[test] +fn rfc3394_4_4_wrap_192_bits_with_a_192_bit_kek() { + TestFrameworkKeyWrap::new().test_kat::<24, 24, 32, Aes192Kw>( + &bytes(&RFC3394_KEK[..48]), + &bytes(&RFC3394_KEY_DATA[..48]), + &bytes("031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2"), + ); +} + +/// Sec 4.5, "Wrap 192 bits of Key Data with a 256-bit KEK". +#[test] +fn rfc3394_4_5_wrap_192_bits_with_a_256_bit_kek() { + TestFrameworkKeyWrap::new().test_kat::<32, 24, 32, Aes256Kw>( + &bytes(RFC3394_KEK), + &bytes(&RFC3394_KEY_DATA[..48]), + &bytes("a8f9bc1612c68b3ff6e6f4fbe30e71e4769c8b80a32cb8958cd5d17d6b254da1"), + ); +} + +/// Sec 4.6, "Wrap 256 bits of Key Data with a 256-bit KEK". +#[test] +fn rfc3394_4_6_wrap_256_bits_with_a_256_bit_kek() { + TestFrameworkKeyWrap::new().test_kat::<32, 32, 40, Aes256Kw>( + &bytes(RFC3394_KEK), + &bytes(RFC3394_KEY_DATA), + &bytes("28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21"), + ); +} + +// ---- RFC 5649 Sec 6: the KWP vectors ------------------------------------------------------- + +const RFC5649_KEK: &str = "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"; + +/// "The first example wraps 20 octets of key data with a 192-bit KEK": 20 bytes pad to 24, and +/// the 32-byte result goes through the wrapping function proper. +#[test] +fn rfc5649_wrap_20_octets_with_a_192_bit_kek() { + TestFrameworkKeyWrap::new().test_kat::<24, 20, 32, Aes192Kwp>( + &bytes(RFC5649_KEK), + &bytes("c37b7e6492584340bed12207808941155068f738"), + &bytes("138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a"), + ); +} + +/// "The second example wraps 7 octets of key data with a 192-bit KEK": header plus data fit one +/// block, so this is the single-block path (SP 800-38F Algorithm 5 step 5 / Algorithm 6 step 3). +#[test] +fn rfc5649_wrap_7_octets_with_a_192_bit_kek() { + TestFrameworkKeyWrap::new().test_kat::<24, 7, 16, Aes192Kwp>( + &bytes(RFC5649_KEK), + &bytes("466f7250617369"), + &bytes("afbeb0f07dfbf5419200f2ccb50bb24f"), + ); +} + +// ---- the shared framework, at several payload lengths --------------------------------------- + +#[test] +fn kw_conforms_to_the_framework() { + let tf = TestFrameworkKeyWrap::new(); + // AES-128 KEK: 128-, 192-, 256-bit keys and a 64-byte payload (n = 9 semiblocks). + tf.test::<16, 16, 24, Aes128Kw>(); + tf.test::<16, 24, 32, Aes128Kw>(); + tf.test::<16, 32, 40, Aes128Kw>(); + tf.test::<16, 64, 72, Aes128Kw>(); + tf.test::<24, 16, 24, Aes192Kw>(); + tf.test::<24, 32, 40, Aes192Kw>(); + tf.test::<32, 16, 24, Aes256Kw>(); + tf.test::<32, 32, 40, Aes256Kw>(); +} + +#[test] +fn kwp_conforms_to_the_framework() { + let tf = TestFrameworkKeyWrap::new(); + // Every padding length from 0 to 7 in the single-block case (1..=8 bytes -> 16), then the + // boundary into the wrapping function (9 -> 24), unaligned and aligned longer inputs. + tf.test::<16, 1, 16, Aes128Kwp>(); + tf.test::<16, 2, 16, Aes128Kwp>(); + tf.test::<16, 3, 16, Aes128Kwp>(); + tf.test::<16, 4, 16, Aes128Kwp>(); + tf.test::<16, 5, 16, Aes128Kwp>(); + tf.test::<16, 6, 16, Aes128Kwp>(); + tf.test::<16, 7, 16, Aes128Kwp>(); + tf.test::<16, 8, 16, Aes128Kwp>(); + tf.test::<16, 9, 24, Aes128Kwp>(); + tf.test::<16, 13, 24, Aes128Kwp>(); + tf.test::<16, 16, 24, Aes128Kwp>(); + tf.test::<16, 20, 32, Aes128Kwp>(); + tf.test::<16, 32, 40, Aes128Kwp>(); + tf.test::<16, 100, 112, Aes128Kwp>(); + tf.test::<24, 7, 16, Aes192Kwp>(); + tf.test::<24, 32, 40, Aes192Kwp>(); + tf.test::<32, 7, 16, Aes256Kwp>(); + tf.test::<32, 32, 40, Aes256Kwp>(); +} + +// ---- properties specific to KW and KWP ------------------------------------------------------ + +fn test_kek() -> KeyMaterial<16> { + kek::<16>("000102030405060708090a0b0c0d0e0f") +} + +/// The two integrity check values differ, so a ciphertext produced by one algorithm is rejected +/// by the other even though the lengths line up. +#[test] +fn kw_and_kwp_ciphertexts_are_not_interchangeable() { + let kek = test_kek(); + let data = [0x5Au8; 16]; + + let kw_ct: [u8; 24] = Aes128Kw::wrap_key(&kek, &data).unwrap(); + let kwp_ct: [u8; 24] = Aes128Kwp::wrap_key(&kek, &data).unwrap(); + assert_ne!(kw_ct, kwp_ct); + + assert!(matches!( + Aes128Kwp::unwrap_key::<16, 24>(&kek, &kw_ct), + Err(SymmetricCipherError::DecryptionFailed) + )); + assert!(matches!( + Aes128Kw::unwrap_key::<16, 24>(&kek, &kwp_ct), + Err(SymmetricCipherError::DecryptionFailed) + )); +} + +/// SP 800-38F Algorithm 5 step 5: for `len(P) <= 64` bits the ciphertext is `CIPH_K(S)` with +/// `S = ICV2 || [len]32 || P || PAD`, one raw block encryption and nothing else. Pinning that +/// against the permutation directly ties the single-block path to the spec rather than to the +/// RFC vector alone. +#[test] +fn kwp_single_block_path_is_one_raw_block_encryption() { + let kek = test_kek(); + let data = *b"12345"; + + let ct: [u8; 16] = Aes128Kwp::wrap_key(&kek, &data).unwrap(); + + let mut s = [0u8; 16]; + s[..4].copy_from_slice(&[0xA6, 0x59, 0x59, 0xA6]); + s[4..8].copy_from_slice(&(data.len() as u32).to_be_bytes()); + s[8..8 + data.len()].copy_from_slice(&data); + AES128Internal::new(&kek).unwrap().encrypt_block(&mut s); + assert_eq!(ct, s); +} + +/// A KWP ciphertext length fixes the plaintext length only to within 7 bytes; the exact length is +/// the header field. So a 12-byte wrap unwraps fine as 12, and as any other length that shares +/// its ciphertext length is an authenticity failure, as the trait requires. +#[test] +fn kwp_unwrap_key_rejects_a_recovered_length_other_than_key_len() { + let kek = test_kek(); + let ct: [u8; 24] = Aes128Kwp::wrap_key(&kek, &[0x33u8; 12]).unwrap(); + + assert_eq!(*Aes128Kwp::unwrap_key::<12, 24>(&kek, &ct).unwrap(), [0x33u8; 12]); + assert!(matches!( + Aes128Kwp::unwrap_key::<13, 24>(&kek, &ct), + Err(SymmetricCipherError::DecryptionFailed) + )); + assert!(matches!( + Aes128Kwp::unwrap_key::<9, 24>(&kek, &ct), + Err(SymmetricCipherError::DecryptionFailed) + )); + // ... and the run-time API reports the real length. + let mut pt = [0u8; 16]; + assert_eq!(Aes128Kwp::unwrap_out(&kek, &ct, &mut pt).unwrap(), 12); + assert_eq!(pt[..12], [0x33u8; 12]); + assert_eq!(pt[12..], [0u8; 4], "the padding is left as zeros"); +} + +/// Algorithm 6 step 8: padding must be zero. A ciphertext whose padding bytes were anything else +/// is a forgery. Constructed by wrapping through the raw block cipher with non-zero padding, since +/// the wrapper itself never produces one. +#[test] +fn kwp_rejects_non_zero_padding() { + let kek = test_kek(); + let perm = AES128Internal::new(&kek).unwrap(); + + let mut s = [0u8; 16]; + s[..4].copy_from_slice(&[0xA6, 0x59, 0x59, 0xA6]); + s[4..8].copy_from_slice(&5u32.to_be_bytes()); + s[8..13].copy_from_slice(b"12345"); + s[15] = 0x01; // the last pad byte + perm.encrypt_block(&mut s); + + assert!(matches!( + Aes128Kwp::unwrap_key::<5, 16>(&kek, &s), + Err(SymmetricCipherError::DecryptionFailed) + )); +} + +/// Algorithm 6 step 7: `padlen` must be 0 to 7, so a length field that does not fit the +/// ciphertext is a forgery even when the ICV and padding check out. Two cases: a length larger +/// than the padded data, and one that would leave 8 or more bytes of padding. +#[test] +fn kwp_rejects_a_length_field_that_does_not_fit() { + let kek = test_kek(); + let perm = AES128Internal::new(&kek).unwrap(); + + for bad_len in [9u32, 0u32, u32::MAX] { + let mut s = [0u8; 16]; + s[..4].copy_from_slice(&[0xA6, 0x59, 0x59, 0xA6]); + s[4..8].copy_from_slice(&bad_len.to_be_bytes()); + perm.encrypt_block(&mut s); + + let mut pt = [0xFFu8; 8]; + assert!( + matches!( + Aes128Kwp::unwrap_out(&kek, &s, &mut pt), + Err(SymmetricCipherError::DecryptionFailed) + ), + "a length field of {bad_len} should not fit an 8-byte padded plaintext" + ); + assert_eq!(pt, [0u8; 8], "the buffer is scrubbed on failure"); + } +} + +/// The length predicates and helpers are SP 800-38F Table 1 exactly. +#[test] +fn the_length_helpers_match_table_1() { + // KW: 2 to 2^54 - 1 semiblocks of plaintext, one more of ciphertext. + assert!(!kw_plaintext_len_is_valid(0)); + assert!(!kw_plaintext_len_is_valid(8)); + assert!(!kw_plaintext_len_is_valid(12)); + assert!(kw_plaintext_len_is_valid(16)); + assert!(kw_plaintext_len_is_valid(24)); + assert!(!kw_ciphertext_len_is_valid(16)); + assert!(kw_ciphertext_len_is_valid(24)); + assert!(!kw_ciphertext_len_is_valid(25)); + assert_eq!(kw_wrapped_len(16), 24); + assert_eq!(kw_wrapped_len(usize::MAX), usize::MAX, "saturates rather than overflowing"); + assert_eq!(Aes128Kw::wrap_out_len(32), 40); + assert_eq!(Aes128Kw::unwrap_out_max_len(40), 32); + assert_eq!(Aes128Kw::unwrap_out_max_len(0), 0); + + // KWP: 1 to 2^32 - 1 octets of plaintext; 2 to 2^29 semiblocks of ciphertext. + assert!(!kwp_plaintext_len_is_valid(0)); + assert!(kwp_plaintext_len_is_valid(1)); + assert!(kwp_plaintext_len_is_valid(u32::MAX as usize)); + assert!(!kwp_plaintext_len_is_valid(u32::MAX as usize + 1)); + assert!(!kwp_ciphertext_len_is_valid(8)); + assert!(kwp_ciphertext_len_is_valid(16)); + assert!(!kwp_ciphertext_len_is_valid(20)); + assert!(kwp_ciphertext_len_is_valid(8 << 29)); + assert!(!kwp_ciphertext_len_is_valid((8 << 29) + 8)); + assert_eq!(kwp_wrapped_len(1), 16); + assert_eq!(kwp_wrapped_len(8), 16); + assert_eq!(kwp_wrapped_len(9), 24); + assert_eq!(kwp_wrapped_len(20), 32); + assert_eq!(kwp_wrapped_len(usize::MAX), usize::MAX, "saturates rather than overflowing"); + assert_eq!(Aes128Kwp::unwrap_out_max_len(32), 24); + + // ... and the run-time API enforces them. + let kek = test_kek(); + let mut buf = [0u8; 64]; + assert!(matches!( + Aes128Kw::wrap_out(&kek, &[0u8; 8], &mut buf), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + assert!(matches!( + Aes128Kw::wrap_out(&kek, &[0u8; 12], &mut buf), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + assert!(matches!( + Aes128Kw::unwrap_out(&kek, &[0u8; 16], &mut buf), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + assert!(matches!( + Aes128Kwp::unwrap_out(&kek, &[0u8; 20], &mut buf), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); +} + +/// KW and KWP wrap the same aligned data to different lengths only when it is short enough to +/// fit KWP's single block; otherwise KWP's ciphertext is KW's length, not longer, since the +/// header replaces ICV1 rather than adding to it. +#[test] +fn kwp_costs_nothing_extra_for_aligned_data() { + assert_eq!(Aes128Kw::wrap_out_len(16), Aes128Kwp::wrap_out_len(16)); + assert_eq!(Aes128Kw::wrap_out_len(32), Aes128Kwp::wrap_out_len(32)); + assert_eq!(Aes128Kwp::wrap_out_len(8), 16, "and 8 bytes need only one block"); +} + +/// The three key lengths are distinct algorithms: the same data under the "same" KEK bytes gives +/// three different ciphertexts, and each unwraps only under its own. +#[test] +fn the_three_key_lengths_are_not_interchangeable() { + let data = [0x5Au8; 16]; + let ct128: [u8; 24] = Aes128Kw::wrap_key(&kek::<16>(&RFC3394_KEK[..32]), &data).unwrap(); + let ct192: [u8; 24] = Aes192Kw::wrap_key(&kek::<24>(&RFC3394_KEK[..48]), &data).unwrap(); + let ct256: [u8; 24] = Aes256Kw::wrap_key(&kek::<32>(RFC3394_KEK), &data).unwrap(); + assert_ne!(ct128, ct192); + assert_ne!(ct192, ct256); + assert_ne!(ct128, ct256); +} diff --git a/crypto/cipher/src/modes/kw.rs b/crypto/cipher/src/modes/kw.rs new file mode 100644 index 000000000..84851cc10 --- /dev/null +++ b/crypto/cipher/src/modes/kw.rs @@ -0,0 +1,383 @@ +//! The Key Wrap algorithm, KW (NIST SP 800-38F Sec 6.2; RFC 3394), over any 128-bit block +//! permutation. +//! +//! KW is a deterministic, authenticated, one-shot encryption of a *key* -- or any other data that +//! is a whole number of 8-byte semiblocks -- under a key encryption key (KEK). There is no IV: the +//! integrity check value ICV1 stands in for one, and wrapping the same data under the same KEK +//! always gives the same ciphertext, which is one semiblock longer than the plaintext. For data +//! of any other length, use [`Kwp`](crate::kwp::Kwp). +//! +//! The mode is cipher-agnostic in the same way as the rest of this crate: [`Kw`] takes any +//! [`ElectronicCodeBook`] whose block is 128 bits (SP 800-38F Sec 5.1 requires that for KW and +//! KWP, and AES is currently the only *approved* cipher that fits) and implements the +//! [`KeyWrapper`] and [`KeyUnwrapper`] traits from `bouncycastle-core`. The ready-made +//! `AES_KW_128` / `AES_KW_192` / `AES_KW_256` aliases are in `bouncycastle-aes`. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_aes::aes_internal::AES128Internal; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +//! use bouncycastle_modes::Kw; +//! +//! type Aes128Kw = Kw; +//! +//! let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let key_to_wrap = [0x5Au8; 32]; +//! +//! // Lengths fixed at compile time: a 32-byte key wraps to 40 bytes, and any other pair of +//! // lengths is a compile error, not a runtime one. +//! let wrapped: [u8; 40] = Aes128Kw::wrap_key(&kek, &key_to_wrap).expect("wrapping"); +//! let recovered = Aes128Kw::unwrap_key::<32, 40>(&kek, &wrapped).expect("unwrapping"); +//! assert_eq!(*recovered, key_to_wrap); +//! +//! // Lengths known only at run time: the caller sizes the buffers with the length helpers. +//! let mut wrapped_out = [0u8; 64]; +//! let n = Aes128Kw::wrap_out(&kek, &key_to_wrap, &mut wrapped_out).expect("wrapping"); +//! assert_eq!(n, Aes128Kw::wrap_out_len(key_to_wrap.len())); +//! assert_eq!(wrapped_out[..n], wrapped); +//! ``` +//! +//! A modified ciphertext, or the wrong KEK, is rejected rather than unwrapped to garbage: +//! +//! ``` +//! # use bouncycastle_aes::aes_internal::AES128Internal; +//! # use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! # use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +//! # use bouncycastle_modes::Kw; +//! use bouncycastle_core::errors::SymmetricCipherError; +//! # type Aes128Kw = Kw; +//! # let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! # .expect("a 16-byte symmetric cipher key"); +//! # let mut wrapped: [u8; 40] = Aes128Kw::wrap_key(&kek, &[0x5Au8; 32]).expect("wrapping"); +//! wrapped[17] ^= 0x01; +//! assert!(matches!( +//! Aes128Kw::unwrap_key::<32, 40>(&kek, &wrapped), +//! Err(SymmetricCipherError::DecryptionFailed) +//! )); +//! ``` +//! +//! # No direction marker +//! +//! Unlike [`Ecb`](crate::Ecb) and the other modes, [`Kw`] has no `Dir` parameter. The wrapping +//! and unwrapping halves are already separate traits ([`KeyWrapper`] / [`KeyUnwrapper`]) whose +//! methods are associated functions, so a policy can import one trait and not the other, and there +//! is no per-direction state for a marker type to select. +//! +//! # The wrapping function runs in place, without the shift register +//! +//! Step 2 of Algorithm 1 (and of Algorithm 2) is written as a shift register: at each step the +//! block cipher takes `A || R2`, the upper half of the output (XORed with the step counter) +//! becomes the new `A`, every `R` moves up one place, and the lower half of the output becomes the +//! new last register. Each step therefore consumes the register at the front and pushes a value +//! onto the back, so after `n - 1` steps every register has been through the cipher exactly once, +//! in the order `R2, ..., Rn`, and holds its replacement in its original position. The private +//! `w` and `w_inv` leave the registers where they are and visit them in that order instead -- the +//! index-based form of RFC 3394 Sec 2.2.1, which the RFC states is equivalent -- so the only +//! difference from the literal steps is that no semiblocks are copied between them. The step +//! counter `t` and the values of `A` and every `R` at every step are identical. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! ## KW is for keys, and for data that behaves like one +//! +//! KW is deterministic and has no IV, which is fine for wrapping a key -- a fresh random key never +//! repeats -- but means that wrapping the same data twice under the same KEK is visible as two +//! equal ciphertexts. Use an AEAD for data that may repeat, and use KW where the format calls for +//! it (CMS, JOSE, PKCS#11 and the like). +//! +//! ## The KEK should be at least as strong as what it protects +//! +//! SP 800-38F Appendix A.2: "the generation and management of the KEK should be at least as +//! strong cryptographically as any key that it protects". The wrapped data is plain bytes with no +//! security strength attached, so this mode cannot check it; it is the caller's responsibility. +//! The KEK itself must be a `SymmetricCipherKey` at least as strong as the permutation's +//! `MAX_SECURITY_STRENGTH`, which the permutation enforces. +//! +//! ## The length limits are part of the security argument +//! +//! Table 1 limits a KW plaintext to fewer than 2^54 semiblocks so that the forgery probability +//! stays below 2^-64 (Appendix A.4). The fixed-length API checks the limits at compile time and +//! the run-time API returns [`SymmetricCipherError::InvalidInputLength`] outside them. + +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, KeyUnwrapper, KeyWrapper}; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; + +/// The block length, in bytes, of the permutation under KW and KWP. SP 800-38F Sec 5.1: "For KW +/// and KWP, the underlying block cipher shall be approved, and the block size shall be 128 bits." +pub const KW_BLOCK_LEN: usize = 16; + +/// A semiblock (SP 800-38F Sec 4.1): "a bit string whose length is half of the block size". +pub const SEMIBLOCK_LEN: usize = KW_BLOCK_LEN / 2; + +/// ICV1, the integrity check value of KW (Algorithm 3, step 1): `0xA6A6A6A6A6A6A6A6`. +const ICV1: [u8; SEMIBLOCK_LEN] = [0xA6; SEMIBLOCK_LEN]; + +/// The most semiblocks of plaintext KW-AE accepts. Table 1: 2 to 2^54 - 1 semiblocks, the upper +/// bound being a requirement (Sec 5.3.1) motivated in Appendix A.4. Held as a `u64` because a +/// 32-bit `usize` cannot represent it. +const KW_MAX_PLAINTEXT_SEMIBLOCKS: u64 = (1u64 << 54) - 1; + +/// Whether `len` bytes is a plaintext length KW is defined on: 2 to 2^54 - 1 whole semiblocks +/// (SP 800-38F Table 1). +pub const fn kw_plaintext_len_is_valid(len: usize) -> bool { + len.is_multiple_of(SEMIBLOCK_LEN) + && len >= 2 * SEMIBLOCK_LEN + && (len / SEMIBLOCK_LEN) as u64 <= KW_MAX_PLAINTEXT_SEMIBLOCKS +} + +/// Whether `len` bytes is a ciphertext length KW-AD is defined on: 3 to 2^54 whole semiblocks +/// (SP 800-38F Table 1). +pub const fn kw_ciphertext_len_is_valid(len: usize) -> bool { + len.is_multiple_of(SEMIBLOCK_LEN) + && len >= 3 * SEMIBLOCK_LEN + && (len / SEMIBLOCK_LEN) as u64 <= KW_MAX_PLAINTEXT_SEMIBLOCKS + 1 +} + +/// The KW ciphertext length for a plaintext of `len` bytes: one semiblock longer. Algorithm 3 +/// computes `C = W(ICV1 || P)`, and W preserves the length of its input. +pub const fn kw_wrapped_len(len: usize) -> usize { + len.saturating_add(SEMIBLOCK_LEN) +} + +/// The wrapping function W (SP 800-38F Algorithm 1), in place over `S = a || r`. +/// +/// `a` holds `S1` and `r` holds `S2 .. Sn`, so `r.len() == n - 1`, which must be at least 2 +/// (Algorithm 1 is defined for `n >= 3`). On return `a` holds `C1` and `r` holds `C2 .. Cn`. +/// +/// Runs in place rather than through the shift register of the spec's step 2; see the module +/// docs for why the two are the same computation. +pub(crate) fn w( + perm: &P, + a: &mut [u8; SEMIBLOCK_LEN], + r: &mut [[u8; SEMIBLOCK_LEN]], +) where + P: ElectronicCodeBook, +{ + debug_assert!(r.len() >= 2, "Algorithm 1 is defined on n >= 3 semiblocks"); + // Everything that passes through `block` is derived from the key being wrapped, so it is + // scrubbed when this function returns. + let mut block: Secret<[u8; KW_BLOCK_LEN]> = Secret::new(); + // Step 1a: s = 6(n-1) steps, indexed by t = 1, ..., s. + let mut t: u64 = 0; + for _ in 0..6 { + for r_i in r.iter_mut() { + t += 1; + // Step 2: CIPH_K(A_{t-1} || R2_{t-1}), where R2_{t-1} is this register (module docs). + let (halves, _) = block.as_chunks_mut::(); + halves[0] = *a; + halves[1] = *r_i; + perm.encrypt_block(&mut block); + let (halves, _) = block.as_chunks::(); + // Step 2a: A_t = MSB64(CIPH_K(...)) xor [t]64. + for (a_byte, (c_byte, t_byte)) in + a.iter_mut().zip(halves[0].iter().zip(t.to_be_bytes())) + { + *a_byte = c_byte ^ t_byte; + } + // Step 2c: R_n_t = LSB64(CIPH_K(...)), which lands back in this register. + *r_i = halves[1]; + } + } + debug_assert_eq!(t as usize, 6 * r.len(), "Algorithm 1 step 1a: s = 6(n-1)"); +} + +/// The unwrapping function W⁻¹ (SP 800-38F Algorithm 2), in place over `C = a || r`. +/// +/// `a` holds `C1` and `r` holds `C2 .. Cn`, with `r.len() == n - 1 >= 2`. On return `a` holds +/// `S1` and `r` holds `S2 .. Sn`. The registers are visited in the reverse of the order [`w`] +/// visits them, so that step `t` here undoes step `t` there. +pub(crate) fn w_inv( + perm: &P, + a: &mut [u8; SEMIBLOCK_LEN], + r: &mut [[u8; SEMIBLOCK_LEN]], +) where + P: ElectronicCodeBook, +{ + debug_assert!(r.len() >= 2, "Algorithm 2 is defined on n >= 3 semiblocks"); + let mut block: Secret<[u8; KW_BLOCK_LEN]> = Secret::new(); + // Step 1a: s = 6(n-1); step 2 runs t = s, s-1, ..., 1. + let mut t: u64 = 6 * r.len() as u64; + for _ in 0..6 { + for r_i in r.iter_mut().rev() { + // Steps 2a/2b: CIPH⁻¹_K((A_t xor [t]64) || R_n_t), where R_n_t is this register. + let (halves, _) = block.as_chunks_mut::(); + for (b, (a_byte, t_byte)) in halves[0].iter_mut().zip(a.iter().zip(t.to_be_bytes())) { + *b = a_byte ^ t_byte; + } + halves[1] = *r_i; + perm.decrypt_block(&mut block); + let (halves, _) = block.as_chunks::(); + // Step 2a: A_{t-1} = MSB64(...). + *a = halves[0]; + // Step 2b: R2_{t-1} = LSB64(...), which lands back in this register. + *r_i = halves[1]; + t -= 1; + } + } + debug_assert_eq!(t, 0, "Algorithm 2 step 2 ends at t = 1"); +} + +/// KW over any 128-bit permutation that impls [`ElectronicCodeBook`]. +/// +/// `KEK_LEN` is the permutation's key length. There is no state and no direction marker: both +/// halves of the algorithm are associated functions of the [`KeyWrapper`] and [`KeyUnwrapper`] +/// traits, and the key schedule lives only for the duration of one call, in the permutation, which +/// is responsible for keeping it in a zeroize-on-drop wrapper. +/// +/// See the module docs for usage and the security notes. +pub struct Kw +where + P: ElectronicCodeBook, +{ + _perm: PhantomData

, +} + +impl Algorithm for Kw +where + P: ElectronicCodeBook, +{ + /// The underlying permutation's name; the mode is in the type, as for [`Ecb`](crate::Ecb). + const ALG_NAME: &'static str = P::ALG_NAME; + /// KW does not change the strength of the underlying cipher. + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +impl KeyWrapper for Kw +where + P: ElectronicCodeBook, +{ + /// KW-AE with the lengths fixed at compile time. `KEY_LEN` must be a valid KW plaintext + /// length and `CT_LEN` one semiblock more; anything else fails to compile. + fn wrap_key_out( + kek: &KeyMaterial, + key: &[u8; KEY_LEN], + ciphertext: &mut [u8; CT_LEN], + ) -> Result { + const { + assert!( + kw_plaintext_len_is_valid(KEY_LEN), + "KW wraps 2 to 2^54 - 1 whole 8-byte semiblocks (SP 800-38F Table 1); use KWP for other lengths" + ); + assert!( + CT_LEN == kw_wrapped_len(KEY_LEN), + "a KW ciphertext is exactly one 8-byte semiblock longer than its plaintext" + ); + } + Self::wrap_out(kek, key, ciphertext) + } + + /// One semiblock more than the plaintext. + fn wrap_out_len(plaintext_len: usize) -> usize { + kw_wrapped_len(plaintext_len) + } + + /// KW-AE (SP 800-38F Algorithm 3). + fn wrap_out( + kek: &KeyMaterial, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + let ct_len = kw_wrapped_len(plaintext.len()); + if ciphertext.len() < ct_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(ct_len)); + } + if !kw_plaintext_len_is_valid(plaintext.len()) { + return Err(SymmetricCipherError::InvalidInputLength( + "KW wraps 2 to 2^54 - 1 whole 8-byte semiblocks (SP 800-38F Table 1); use KWP for other lengths", + )); + } + let perm = P::new(kek)?; + + // Steps 1-2: S = ICV1 || P, assembled in the caller's buffer so that the plaintext is + // copied exactly once and W can run on it in place. + let (a, r) = ciphertext[..ct_len].split_at_mut(SEMIBLOCK_LEN); + let (a, _) = a.as_chunks_mut::(); + let a = &mut a[0]; + *a = ICV1; + r.copy_from_slice(plaintext); + let (r, _) = r.as_chunks_mut::(); + // Step 3: C = W(S). + w(&perm, a, r); + Ok(ct_len) + } +} + +impl KeyUnwrapper for Kw +where + P: ElectronicCodeBook, +{ + /// KW-AD with the lengths fixed at compile time; the same pairs as + /// [`wrap_key_out`](KeyWrapper::wrap_key_out) are accepted. + fn unwrap_key_out( + kek: &KeyMaterial, + ciphertext: &[u8; CT_LEN], + key: &mut Secret<[u8; KEY_LEN]>, + ) -> Result { + const { + assert!( + kw_plaintext_len_is_valid(KEY_LEN), + "KW unwraps to 2 to 2^54 - 1 whole 8-byte semiblocks (SP 800-38F Table 1); use KWP for other lengths" + ); + assert!( + CT_LEN == kw_wrapped_len(KEY_LEN), + "a KW ciphertext is exactly one 8-byte semiblock longer than its plaintext" + ); + } + // `key` is exactly the n-1 semiblocks the run-time method writes into, and that method + // scrubs the buffer on every failure it can reach from here. + Self::unwrap_out(kek, ciphertext, &mut key[..]) + } + + /// One semiblock less than the ciphertext: exact for KW. + fn unwrap_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(SEMIBLOCK_LEN) + } + + /// KW-AD (SP 800-38F Algorithm 4). + fn unwrap_out( + kek: &KeyMaterial, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let pt_len = Self::unwrap_out_max_len(ciphertext.len()); + if plaintext.len() < pt_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(pt_len)); + } + // Nothing this function writes may survive a failure, so start from zero and only ever + // return `Ok` with the buffer fully written. + plaintext.fill(0); + if !kw_ciphertext_len_is_valid(ciphertext.len()) { + return Err(SymmetricCipherError::InvalidInputLength( + "a KW ciphertext is 3 to 2^54 whole 8-byte semiblocks (SP 800-38F Table 1)", + )); + } + let perm = P::new(kek)?; + + // Step 2: S = W⁻¹(C). C1 goes to a scratch semiblock that is scrubbed on return; C2..Cn + // go straight into the caller's buffer, which is exactly n-1 semiblocks, so W⁻¹ runs in + // place and the recovered plaintext is never copied. + let mut a: Secret<[u8; SEMIBLOCK_LEN]> = Secret::new(); + a.copy_from_slice(&ciphertext[..SEMIBLOCK_LEN]); + let r = &mut plaintext[..pt_len]; + r.copy_from_slice(&ciphertext[SEMIBLOCK_LEN..]); + let (r, _) = r.as_chunks_mut::(); + w_inv(&perm, &mut a, r); + + // Step 3: if MSB64(S) != ICV1, FAIL. Compared in constant time, and the buffer is scrubbed + // so that a caller who ignores the `Result` is not left holding the unauthenticated bytes. + if !ct_eq_bytes(&*a, &ICV1) { + plaintext.fill(0); + return Err(SymmetricCipherError::DecryptionFailed); + } + // Step 4: P = LSB64(n-1)(S), which is already in place. + Ok(pt_len) + } +} diff --git a/crypto/cipher/src/modes/kwp.rs b/crypto/cipher/src/modes/kwp.rs new file mode 100644 index 000000000..f914a4bf8 --- /dev/null +++ b/crypto/cipher/src/modes/kwp.rs @@ -0,0 +1,301 @@ +//! The Key Wrap with Padding algorithm, KWP (NIST SP 800-38F Sec 6.3; RFC 5649), over any +//! 128-bit block permutation. +//! +//! KWP is [`Kw`](crate::kw::Kw) for data of any length from 1 byte up to 2^32 - 1 bytes: the +//! plaintext is prefixed with a 4-byte integrity check value and its 4-byte length, padded with +//! zeros to a whole number of 8-byte semiblocks, and then put through the same wrapping function. +//! The ciphertext is the plaintext rounded up to a multiple of 8, plus 8. Everything in the +//! [`kw`](crate::kw) module docs -- the trait shape, the in-place wrapping function, the security +//! notes -- applies here too; this module only documents what differs. +//! +//! The `AES_KWP_128` / `AES_KWP_192` / `AES_KWP_256` aliases are in `bouncycastle-aes`. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_aes::aes_internal::AES128Internal; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +//! use bouncycastle_modes::Kwp; +//! +//! type Aes128Kwp = Kwp; +//! +//! let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // 20 bytes pad to 24, plus the 8-byte header: 32 bytes of ciphertext. +//! let data = [0x5Au8; 20]; +//! let wrapped: [u8; 32] = Aes128Kwp::wrap_key(&kek, &data).expect("wrapping"); +//! let recovered = Aes128Kwp::unwrap_key::<20, 32>(&kek, &wrapped).expect("unwrapping"); +//! assert_eq!(*recovered, data); +//! +//! // Up to 8 bytes fit in a single block with the header, which is enciphered directly +//! // (Algorithm 5, step 5) rather than through the wrapping function. +//! let short = *b"short"; +//! let wrapped: [u8; 16] = Aes128Kwp::wrap_key(&kek, &short).expect("wrapping"); +//! assert_eq!(*Aes128Kwp::unwrap_key::<5, 16>(&kek, &wrapped).expect("unwrapping"), short); +//! ``` +//! +//! # The recovered length is inside the ciphertext +//! +//! A KW ciphertext's length says exactly how long its plaintext is. A KWP ciphertext's length +//! only bounds it: the exact length is the 32-bit field the wrapper put in the header, and is +//! known only after unwrapping. So [`KeyUnwrapper::unwrap_out_max_len`] is an upper bound, +//! [`KeyUnwrapper::unwrap_out`] returns the real length, and the fixed-length +//! [`KeyUnwrapper::unwrap_key`] treats a recovered length other than the `KEY_LEN` it was asked +//! for as an authenticity failure, indistinguishable from a bad ICV. +//! +//! # Every check is the same FAIL +//! +//! Algorithm 6 returns FAIL for a wrong ICV (step 4), a length field that does not fit the +//! ciphertext (step 7) and non-zero padding (step 8). All three are made unconditionally, combined +//! bitwise, and reported as one [`SymmetricCipherError::DecryptionFailed`], so that neither the +//! error nor the control flow says which check failed. (The number of padding bytes compared does +//! depend on the length field, but that field is public once unwrapping succeeds and never more +//! than 7 bytes; there is no padding oracle to protect because the failure is the same either way.) + +use crate::kw::{KW_BLOCK_LEN, SEMIBLOCK_LEN, w, w_inv}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, KeyUnwrapper, KeyWrapper}; +use bouncycastle_utils::ct::{ct_eq_bytes, ct_eq_zero_bytes}; +use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; + +/// ICV2, the integrity check value of KWP (Algorithm 5, step 1): `0xA65959A6`. +const ICV2: [u8; 4] = [0xA6, 0x59, 0x59, 0xA6]; + +/// The longest plaintext KWP-AE accepts: Table 1 gives 1 to 2^32 - 1 octets, the bound being the +/// range of the 32-bit length field in the header (Algorithm 5, step 4). +const KWP_MAX_PLAINTEXT_LEN: u64 = (1u64 << 32) - 1; + +/// The most semiblocks in a KWP ciphertext: Table 1 gives 2 to 2^29 semiblocks. +const KWP_MAX_CIPHERTEXT_SEMIBLOCKS: u64 = 1u64 << 29; + +/// Whether `len` bytes is a plaintext length KWP is defined on: 1 to 2^32 - 1 octets (SP 800-38F +/// Table 1). +pub const fn kwp_plaintext_len_is_valid(len: usize) -> bool { + len >= 1 && len as u64 <= KWP_MAX_PLAINTEXT_LEN +} + +/// Whether `len` bytes is a ciphertext length KWP-AD is defined on: 2 to 2^29 whole semiblocks +/// (SP 800-38F Table 1). +pub const fn kwp_ciphertext_len_is_valid(len: usize) -> bool { + len.is_multiple_of(SEMIBLOCK_LEN) + && len >= 2 * SEMIBLOCK_LEN + && (len / SEMIBLOCK_LEN) as u64 <= KWP_MAX_CIPHERTEXT_SEMIBLOCKS +} + +/// The KWP ciphertext length for a plaintext of `len` bytes: `len` rounded up to a whole number +/// of semiblocks (Algorithm 5, steps 2-3: `padlen = 8 * ceil(len / 8) - len` zero bytes), plus the +/// one-semiblock header of step 4. +pub const fn kwp_wrapped_len(len: usize) -> usize { + len.div_ceil(SEMIBLOCK_LEN).saturating_mul(SEMIBLOCK_LEN).saturating_add(SEMIBLOCK_LEN) +} + +/// KWP over any 128-bit permutation that impls [`ElectronicCodeBook`]. +/// +/// As [`Kw`](crate::kw::Kw): no state, no direction marker, `KEK_LEN` is the permutation's key +/// length. See the module docs. +pub struct Kwp +where + P: ElectronicCodeBook, +{ + _perm: PhantomData

, +} + +impl Algorithm for Kwp +where + P: ElectronicCodeBook, +{ + /// The underlying permutation's name; the mode is in the type, as for [`Ecb`](crate::Ecb). + const ALG_NAME: &'static str = P::ALG_NAME; + /// KWP does not change the strength of the underlying cipher. + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +impl KeyWrapper for Kwp +where + P: ElectronicCodeBook, +{ + /// KWP-AE with the lengths fixed at compile time. `KEY_LEN` may be anything from 1 to + /// 2^32 - 1 and `CT_LEN` must be [`kwp_wrapped_len`] of it; anything else fails to compile. + fn wrap_key_out( + kek: &KeyMaterial, + key: &[u8; KEY_LEN], + ciphertext: &mut [u8; CT_LEN], + ) -> Result { + const { + assert!( + kwp_plaintext_len_is_valid(KEY_LEN), + "KWP wraps 1 to 2^32 - 1 bytes (SP 800-38F Table 1)" + ); + assert!( + CT_LEN == kwp_wrapped_len(KEY_LEN), + "a KWP ciphertext is the plaintext rounded up to a multiple of 8 bytes, plus 8" + ); + } + Self::wrap_out(kek, key, ciphertext) + } + + /// The plaintext rounded up to a whole number of semiblocks, plus one. + fn wrap_out_len(plaintext_len: usize) -> usize { + kwp_wrapped_len(plaintext_len) + } + + /// KWP-AE (SP 800-38F Algorithm 5). + fn wrap_out( + kek: &KeyMaterial, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + let ct_len = kwp_wrapped_len(plaintext.len()); + if ciphertext.len() < ct_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(ct_len)); + } + if !kwp_plaintext_len_is_valid(plaintext.len()) { + return Err(SymmetricCipherError::InvalidInputLength( + "KWP wraps 1 to 2^32 - 1 bytes (SP 800-38F Table 1)", + )); + } + let perm = P::new(kek)?; + + // Steps 1-4: S = ICV2 || [len(P)/8]32 || P || PAD, assembled in the caller's buffer so the + // plaintext is copied exactly once. The length is in octets and fits 32 bits: checked above. + let s = &mut ciphertext[..ct_len]; + s[..ICV2.len()].copy_from_slice(&ICV2); + s[ICV2.len()..SEMIBLOCK_LEN].copy_from_slice(&(plaintext.len() as u32).to_be_bytes()); + s[SEMIBLOCK_LEN..SEMIBLOCK_LEN + plaintext.len()].copy_from_slice(plaintext); + // Step 3: PAD is padlen zero bytes, padlen being whatever rounds P up to a whole number of + // semiblocks (step 2). + s[SEMIBLOCK_LEN + plaintext.len()..].fill(0); + + // Step 5: a plaintext of at most one semiblock makes S a single block, which is enciphered + // directly; a longer one goes through W. + if plaintext.len() <= SEMIBLOCK_LEN { + let (blocks, _) = s.as_chunks_mut::(); + perm.encrypt_block(&mut blocks[0]); + } else { + let (a, r) = s.split_at_mut(SEMIBLOCK_LEN); + let (a, _) = a.as_chunks_mut::(); + let (r, _) = r.as_chunks_mut::(); + w(&perm, &mut a[0], r); + } + Ok(ct_len) + } +} + +impl KeyUnwrapper for Kwp +where + P: ElectronicCodeBook, +{ + /// KWP-AD with the lengths fixed at compile time; the same pairs as + /// [`wrap_key_out`](KeyWrapper::wrap_key_out) are accepted. + /// + /// A ciphertext that unwraps to any length other than `KEY_LEN` -- which a valid `CT_LEN` + /// allows, since up to eight plaintext lengths share one ciphertext length -- is reported as + /// [`SymmetricCipherError::DecryptionFailed`], exactly as a forgery would be. + fn unwrap_key_out( + kek: &KeyMaterial, + ciphertext: &[u8; CT_LEN], + key: &mut Secret<[u8; KEY_LEN]>, + ) -> Result { + const { + assert!( + kwp_plaintext_len_is_valid(KEY_LEN), + "KWP unwraps to 1 to 2^32 - 1 bytes (SP 800-38F Table 1)" + ); + assert!( + CT_LEN == kwp_wrapped_len(KEY_LEN), + "a KWP ciphertext is the plaintext rounded up to a multiple of 8 bytes, plus 8" + ); + } + // The padded plaintext can be up to 7 bytes longer than `key`, so unwrap into scratch + // that is scrubbed on drop, and only copy out once the recovered length is the expected one. + let mut padded: Secret<[u8; CT_LEN]> = Secret::new(); + match Self::unwrap_out(kek, ciphertext, &mut padded[..CT_LEN - SEMIBLOCK_LEN]) { + Ok(recovered_len) if recovered_len == KEY_LEN => { + key.copy_from_slice(&padded[..KEY_LEN]); + Ok(KEY_LEN) + } + Ok(_) => { + key.zeroize(); + Err(SymmetricCipherError::DecryptionFailed) + } + Err(e) => { + key.zeroize(); + Err(e) + } + } + } + + /// One semiblock less than the ciphertext: the padded plaintext, of which up to 7 bytes are + /// padding, so this is an upper bound (module docs). + fn unwrap_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(SEMIBLOCK_LEN) + } + + /// KWP-AD (SP 800-38F Algorithm 6). + fn unwrap_out( + kek: &KeyMaterial, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + // 8(n-1) bytes: the padded plaintext. + let padded_len = Self::unwrap_out_max_len(ciphertext.len()); + if plaintext.len() < padded_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(padded_len)); + } + // Nothing this function writes may survive a failure, so start from zero. + plaintext.fill(0); + if !kwp_ciphertext_len_is_valid(ciphertext.len()) { + return Err(SymmetricCipherError::InvalidInputLength( + "a KWP ciphertext is 2 to 2^29 whole 8-byte semiblocks (SP 800-38F Table 1)", + )); + } + let perm = P::new(kek)?; + + // Step 1: n, the number of semiblocks in C. + let n = ciphertext.len() / SEMIBLOCK_LEN; + // The header semiblock S1 goes to scratch that is scrubbed on return; S2..Sn -- the padded + // plaintext -- go straight into the caller's buffer. + let mut a: Secret<[u8; SEMIBLOCK_LEN]> = Secret::new(); + let r = &mut plaintext[..padded_len]; + // Step 3: a two-semiblock ciphertext is a single block, deciphered directly; a longer one + // goes through W⁻¹. + if n == 2 { + let mut block: Secret<[u8; KW_BLOCK_LEN]> = Secret::new(); + block.copy_from_slice(ciphertext); + perm.decrypt_block(&mut block); + let (halves, _) = block.as_chunks::(); + *a = halves[0]; + r.copy_from_slice(&halves[1]); + } else { + a.copy_from_slice(&ciphertext[..SEMIBLOCK_LEN]); + r.copy_from_slice(&ciphertext[SEMIBLOCK_LEN..]); + let (r, _) = r.as_chunks_mut::(); + w_inv(&perm, &mut a, r); + } + + // Steps 4-8: every check is made, and the verdicts combined with `&` rather than `&&`, so + // that which one failed shows in neither the error nor the control flow (module docs). + let (header, _) = a.as_chunks::<4>(); + // Step 4: MSB32(S) = ICV2, in constant time. + let icv_ok = ct_eq_bytes(&header[0], &ICV2); + // Step 5: Plen = int(LSB32(MSB64(S))). + let plen = u32::from_be_bytes(header[1]) as usize; + // Steps 6-7: padlen = 8(n-1) - Plen must be 0 to 7. Written so that no Plen can underflow. + let len_ok = (plen <= padded_len) & (padded_len.wrapping_sub(plen) < SEMIBLOCK_LEN); + // Step 8: LSB_{8 padlen}(S) = 0^{8 padlen}. Plen is clamped so an out-of-range value still + // gives a well-defined (and, via `len_ok`, failing) check rather than a panic. + let pad_ok = ct_eq_zero_bytes(&plaintext[plen.min(padded_len)..padded_len]); + if !(icv_ok & len_ok & pad_ok) { + plaintext.fill(0); + return Err(SymmetricCipherError::DecryptionFailed); + } + // Step 9: P = MSB_{8 Plen}(LSB_{64(n-1)}(S)), already in place, followed by padding that + // step 8 has just confirmed is zero. + Ok(plen) + } +} From 1362d0a6f93fe5fd96ef2d020b739df96cfe4b9c Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 27 Oct 2026 03:36:24 +1100 Subject: [PATCH 03/18] core: KeyWrapper/KeyUnwrapper are workers over a borrowed key state; KeyWrapMode/KeyUnwrapMode and the KeyWrappingKey/KeyUnwrappingKey roles The traits from PR #160 took the KEK as a KeyMaterial on every call. On the key path the KEK is validated once, when the key object is made, so the pair becomes worker traits on the shape of MAC: `&self` methods over a worker that borrows the keyed permutation, with no key parameter and no KeyMaterialError. The mode traits (hazmat) hand the worker out from a key state, and the role traits forward to them with the one-shots provided, as the MAC roles do. The length helpers take `&self` so the role can forward them through a worker. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core/src/hazmat/mod.rs | 4 +- crypto/core/src/hazmat/mode.rs | 35 ++++++++- crypto/core/src/symmetric_key.rs | 128 ++++++++++++++++++++++++++++++- crypto/core/src/traits.rs | 70 ++++++++--------- 4 files changed, 188 insertions(+), 49 deletions(-) diff --git a/crypto/core/src/hazmat/mod.rs b/crypto/core/src/hazmat/mod.rs index 0190b8f0c..20186475e 100644 --- a/crypto/core/src/hazmat/mod.rs +++ b/crypto/core/src/hazmat/mod.rs @@ -23,6 +23,6 @@ pub use hazardous_operations::do_hazardous_operations; pub use key_stream::KeyStream; pub use mode::{ AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, - BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbEncryptMode, MacMode, Mode, - NonceMacMode, SymmetricDecryptMode, SymmetricEncryptMode, + BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbEncryptMode, KeyUnwrapMode, KeyWrapMode, + MacMode, Mode, NonceMacMode, SymmetricDecryptMode, SymmetricEncryptMode, }; diff --git a/crypto/core/src/hazmat/mode.rs b/crypto/core/src/hazmat/mode.rs index d8d0f77f2..78191f6e4 100644 --- a/crypto/core/src/hazmat/mode.rs +++ b/crypto/core/src/hazmat/mode.rs @@ -15,7 +15,8 @@ //! [`EcbDecryptMode`] for ECB, which has no init data and so takes no RNG. A MAC mode -- CMAC -- //! implements [`MacMode`], whose worker takes no nonce and serves both roles; a nonce-based MAC //! -- GMAC -- implements [`NonceMacMode`], whose generator and verifier are made as an AEAD -//! mode's workers are. +//! mode's workers are. A key-wrap mode -- KW, KWP -- implements [`KeyWrapMode`] / +//! [`KeyUnwrapMode`], whose workers are one-shot and take no nonce either. //! //! Every worker borrows the key state it is made from, for the lifetime `'k` on the mode's //! associated worker type: the schedule stays in the key and is read through the reference, so @@ -25,8 +26,8 @@ use crate::errors::SymmetricCipherError; use crate::key_material::KeyType; use crate::traits::{ - AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, MAC, RNG, - SymmetricCipherDecryptor, SymmetricCipherEncryptor, + AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, + KeyUnwrapper, KeyWrapper, MAC, RNG, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; /// A mode's per-key state over the permutation `P`. @@ -407,6 +408,34 @@ pub trait MacMode

: Mode

{ fn mac_init<'k>(state: &'k Self::KeyState) -> Self::Worker<'k>; } +/// The wrapping side of a key-wrap mode over `P`: the mode's own constructor for its +/// [`KeyWrapper`], which the role trait [`crate::symmetric_key::KeyWrappingKey`] forwards to. +/// `KEK_LEN` ties the key's length to the permutation's, as `KEY_LEN` does on the cipher modes. +pub trait KeyWrapMode: Mode

{ + /// The one-shot wrapper this mode produces over `P`. It borrows the key state it was made + /// from for `'k`: the schedule is read through the reference, never copied into the worker, + /// and the worker cannot outlive the state. + type Wrapper<'k>: KeyWrapper + where + P: 'k; + + /// A wrapper over `state`. Infallible: the state is keyed and there is nothing to draw. + fn wrap_init<'k>(state: &'k Self::KeyState) -> Self::Wrapper<'k>; +} + +/// The unwrapping side of a key-wrap mode over `P`; see [`KeyWrapMode`]. The role trait +/// [`crate::symmetric_key::KeyUnwrappingKey`] forwards to it. +pub trait KeyUnwrapMode: Mode

{ + /// The one-shot unwrapper this mode produces over `P`, borrowing the key state for `'k` as + /// [`KeyWrapMode::Wrapper`] does. + type Unwrapper<'k>: KeyUnwrapper + where + P: 'k; + + /// An unwrapper over `state`. Infallible. + fn unwrap_init<'k>(state: &'k Self::KeyState) -> Self::Unwrapper<'k>; +} + /// A nonce-based MAC mode over `P` -- GMAC -- whose tag is bound to a nonce, so its two sides are /// made as an AEAD mode's workers are: the generator under a nonce drawn from an RNG or supplied /// by the caller, the verifier under the nonce the tag was made under. There is no role trait diff --git a/crypto/core/src/symmetric_key.rs b/crypto/core/src/symmetric_key.rs index 401b5b319..f9cdc09b9 100644 --- a/crypto/core/src/symmetric_key.rs +++ b/crypto/core/src/symmetric_key.rs @@ -17,7 +17,8 @@ //! know the lengths up front, [`SymmetricEncryptionKey`] / [`SymmetricDecryptionKey`] for the //! unauthenticated modes that take data of any length (padded CBC, CFB, CFB8, CTR), //! [`BlockEncryptionKey`] / [`BlockDecryptionKey`] for the whole-block modes, the ECB pair in -//! [`crate::hazmat`], and [`MacGenerationKey`] / [`MacVerificationKey`]. A key type +//! [`crate::hazmat`], [`MacGenerationKey`] / [`MacVerificationKey`], and +//! [`KeyWrappingKey`] / [`KeyUnwrappingKey`] for the key-wrap modes. A key type //! implements the sides it is allowed to use, so a decrypt-only or verify-only key refuses the //! other side at compile time. The one-shots are provided methods over `create_*` and the //! worker's streaming methods, the cipher traits in [`crate::traits`]. @@ -49,10 +50,11 @@ use crate::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; #[cfg(feature = "std")] use crate::traits::AEADEncryptedTuple; use crate::traits::{ - AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, MAC, RNG, - StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, + AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, + KeyUnwrapper, KeyWrapper, MAC, RNG, StreamCipherDecryptor, StreamCipherEncryptor, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; +use bouncycastle_utils::secret::Secret; /// Construction of a key object from exactly `KEY_LEN` bytes of key material. /// @@ -1163,3 +1165,121 @@ pub trait MacVerificationKey { ver.do_verify_final(mac) } } + +/// A key that can wrap keys: a key encryption key (KEK) bound to a key-wrap mode such as KW or +/// KWP (NIST SP 800-38F). The worker is a [`KeyWrapper`] borrowing the key for `'k`. Key wrap is +/// one-shot, so the provided methods here are the API in practice; the worker is there for the +/// trait pair's sake and for a caller that wants to hold the borrow. A key may implement +/// [`KeyUnwrappingKey`] and not this, which is what an unwrap-only key is. +/// +/// The KEK's type and strength were checked when the key was made, so the only errors left are +/// the lengths and, on unwrapping, authenticity; see [`KeyWrapper`] and [`KeyUnwrapper`]. +pub trait KeyWrappingKey { + /// The one-shot worker this key produces for wrapping, borrowing the key for `'k`. + type Wrapper<'k>: KeyWrapper + where + Self: 'k; + + /// A wrapper over this key. Infallible: there is nothing to draw and nothing left to check. + fn create_wrapper<'k>(&'k self) -> Self::Wrapper<'k>; + + /// One-shot: wraps `key`, whose length is fixed at compile time, returning the ciphertext. + /// `(KEY_LEN, CT_LEN)` must be a pair the mode defines; see [`KeyWrapper::wrap_key_out`]. + /// + /// # Errors + /// None for a valid pair of lengths. + fn wrap_key( + &self, + key: &[u8; KEY_LEN], + ) -> Result<[u8; CT_LEN], SymmetricCipherError> { + self.create_wrapper().wrap_key(key) + } + + /// One-shot: as [`wrap_key`](Self::wrap_key), into `ciphertext`; returns the number of bytes + /// written, always `CT_LEN`. + /// + /// # Errors + /// None for a valid pair of lengths. + fn wrap_key_out( + &self, + key: &[u8; KEY_LEN], + ciphertext: &mut [u8; CT_LEN], + ) -> Result { + self.create_wrapper().wrap_key_out(key, ciphertext) + } + + /// The exact ciphertext length for a plaintext of `plaintext_len` bytes: what + /// [`wrap_out`](Self::wrap_out) needs. See [`KeyWrapper::wrap_out_len`]. + fn wrap_out_len(&self, plaintext_len: usize) -> usize { + self.create_wrapper().wrap_out_len(plaintext_len) + } + + /// One-shot: wraps `plaintext`, whose length is only known at run time, into `ciphertext`; + /// returns the number of bytes written. + /// + /// # Errors + /// As [`KeyWrapper::wrap_out`]. + fn wrap_out( + &self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + self.create_wrapper().wrap_out(plaintext, ciphertext) + } +} + +/// A key that can unwrap keys; see [`KeyWrappingKey`] for the shape and the lifetime of the +/// worker, and [`KeyUnwrapper`] for the `Secret` output and the single authenticity error. +pub trait KeyUnwrappingKey { + /// The one-shot worker this key produces for unwrapping, borrowing the key for `'k`. + type Unwrapper<'k>: KeyUnwrapper + where + Self: 'k; + + /// An unwrapper over this key. Infallible. + fn create_unwrapper<'k>(&'k self) -> Self::Unwrapper<'k>; + + /// One-shot: unwraps `ciphertext`, whose length is fixed at compile time, into a fresh + /// `Secret`. `(KEY_LEN, CT_LEN)` must be a pair the mode defines. + /// + /// # Errors + /// [`SymmetricCipherError::DecryptionFailed`] for any authenticity failure. + fn unwrap_key( + &self, + ciphertext: &[u8; CT_LEN], + ) -> Result, SymmetricCipherError> { + self.create_unwrapper().unwrap_key(ciphertext) + } + + /// One-shot: as [`unwrap_key`](Self::unwrap_key), into `key`; returns the number of bytes + /// written, always `KEY_LEN`. `key` is zeroized on error. + /// + /// # Errors + /// [`SymmetricCipherError::DecryptionFailed`] for any authenticity failure. + fn unwrap_key_out( + &self, + ciphertext: &[u8; CT_LEN], + key: &mut Secret<[u8; KEY_LEN]>, + ) -> Result { + self.create_unwrapper().unwrap_key_out(ciphertext, key) + } + + /// An upper bound on the plaintext length of a `ciphertext_len`-byte ciphertext: what + /// [`unwrap_out`](Self::unwrap_out) needs. See [`KeyUnwrapper::unwrap_out_max_len`]. + fn unwrap_out_max_len(&self, ciphertext_len: usize) -> usize { + self.create_unwrapper().unwrap_out_max_len(ciphertext_len) + } + + /// One-shot: unwraps `ciphertext`, whose length is only known at run time, into `plaintext`; + /// returns the number of plaintext bytes written. The whole buffer is zeroized on error. + /// + /// # Errors + /// As [`KeyUnwrapper::unwrap_out`]. + fn unwrap_out( + &self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + self.create_unwrapper().unwrap_out(ciphertext, plaintext) + } +} diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 5b88e8b39..cff67580f 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -674,23 +674,16 @@ pub trait KEMPublicKey: } /// A keyed keystream generator: the raw primitive under a stream cipher, as -/// The unwrapping (authenticated-decryption) half of a key-wrap algorithm, such as KW, KWP or TKW -/// from NIST SP 800-38F (also specified as RFC 3394 and RFC 5649). -/// -/// Wrapping is performed by the corresponding [`KeyWrapper`] trait. The two are separate traits so -/// that a policy can permit unwrapping of existing ciphertexts while forbidding new wrappings, as -/// with the other `*Encryptor` / `*Decryptor` pairs. +/// The unwrapping half of a key-wrap algorithm -- KW, KWP or TKW of NIST SP 800-38F (RFC 3394 and +/// RFC 5649) -- as the worker a key object hands out through +/// [`KeyUnwrappingKey`](crate::symmetric_key::KeyUnwrappingKey). Wrapping is [`KeyWrapper`]. The +/// two are separate traits so that a policy can permit unwrapping existing ciphertexts while +/// forbidding new wrappings, as with the other `*Encryptor` / `*Decryptor` pairs. /// /// Key wrap is a one-shot operation over the whole input, so there is no streaming API: the -/// underlying wrapping function (SP 800-38F, Algorithm 2 (W⁻¹)) makes several passes over all -/// of the data. -/// -/// # The KEK -/// -/// The KEK (key encryption key) is a [`KeyMaterial`] and must be of type -/// [`KeyType::SymmetricCipherKey`], tagged at a security strength of at least -/// [`Algorithm::MAX_SECURITY_STRENGTH`]; otherwise a [`SymmetricCipherError::KeyMaterialError`] is -/// returned. +/// underlying wrapping function (SP 800-38F Algorithm 2, W⁻¹) makes several passes over all of +/// the data. The KEK (key encryption key) belongs to the key object, which validated it once; the +/// worker borrows the keyed permutation and never sees the material. /// /// # The recovered plaintext is a `Secret`, not a `KeyMaterial` /// @@ -706,17 +699,17 @@ pub trait KEMPublicKey: /// reported as [`SymmetricCipherError::DecryptionFailed`], so that the error does not tell an /// attacker which check failed. SP 800-38F reports all of these as a single FAIL; see, for example, /// Algorithm 6 (KWP-AD), steps 4, 7 and 8. On any error, the output buffer is zeroized. -pub trait KeyUnwrapper: Algorithm + Sized { +pub trait KeyUnwrapper: Algorithm { /// One-shot: unwraps a ciphertext whose length is fixed at compile time, returning the /// recovered plaintext. /// /// Provided over [`KeyUnwrapper::unwrap_key_out`]. fn unwrap_key( - kek: &KeyMaterial, + &self, ciphertext: &[u8; CT_LEN], ) -> Result, SymmetricCipherError> { let mut key = Secret::<[u8; KEY_LEN]>::new(); - let bytes_written = Self::unwrap_key_out(kek, ciphertext, &mut key)?; + let bytes_written = self.unwrap_key_out(ciphertext, &mut key)?; debug_assert_eq!(bytes_written, KEY_LEN); Ok(key) } @@ -730,11 +723,10 @@ pub trait KeyUnwrapper: Algorithm + Sized { /// [`KeyWrapper::wrap_key_out`]. /// /// # Errors - /// [`SymmetricCipherError::KeyMaterialError`] for an unsuitable KEK, and /// [`SymmetricCipherError::DecryptionFailed`] for any authenticity failure. `key` is zeroized /// on error. fn unwrap_key_out( - kek: &KeyMaterial, + &self, ciphertext: &[u8; CT_LEN], key: &mut Secret<[u8; KEY_LEN]>, ) -> Result; @@ -745,7 +737,7 @@ pub trait KeyUnwrapper: Algorithm + Sized { /// KWP the exact length is only known after unwrapping, since up to 7 bytes are padding. /// /// Must not panic for any `ciphertext_len`. - fn unwrap_out_max_len(ciphertext_len: usize) -> usize; + fn unwrap_out_max_len(&self, ciphertext_len: usize) -> usize; /// One-shot: unwraps a ciphertext whose length is only known at run time into `plaintext`, /// which needs [`KeyUnwrapper::unwrap_out_max_len`] bytes. Returns the number of plaintext @@ -757,21 +749,19 @@ pub trait KeyUnwrapper: Algorithm + Sized { /// # Errors /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, checked before /// any work is done; [`SymmetricCipherError::InvalidInputLength`] for a ciphertext length the - /// algorithm is not defined on; [`SymmetricCipherError::KeyMaterialError`] for an unsuitable - /// KEK; and [`SymmetricCipherError::DecryptionFailed`] for any authenticity failure. The whole - /// `plaintext` buffer is zeroized on error. + /// algorithm is not defined on; and [`SymmetricCipherError::DecryptionFailed`] for any + /// authenticity failure. The whole `plaintext` buffer is zeroized on error. fn unwrap_out( - kek: &KeyMaterial, + &self, ciphertext: &[u8], plaintext: &mut [u8], ) -> Result; } -/// The wrapping (authenticated-encryption) half of a key-wrap algorithm, such as KW, KWP or TKW -/// from NIST SP 800-38F (also specified as RFC 3394 and RFC 5649). -/// -/// Unwrapping is performed by the corresponding [`KeyUnwrapper`] trait, whose notes on the KEK -/// apply here too. +/// The wrapping half of a key-wrap algorithm -- KW, KWP or TKW of NIST SP 800-38F (RFC 3394 and +/// RFC 5649) -- as the worker a key object hands out through +/// [`KeyWrappingKey`](crate::symmetric_key::KeyWrappingKey). Unwrapping is [`KeyUnwrapper`], +/// whose notes on the KEK and the one-shot shape apply here too. /// /// # The data being wrapped is plain bytes /// @@ -786,16 +776,16 @@ pub trait KeyUnwrapper: Algorithm + Sized { /// /// Key wrapping is deterministic: wrapping the same data under the same KEK always produces the /// same ciphertext. -pub trait KeyWrapper: Algorithm + Sized { +pub trait KeyWrapper: Algorithm { /// One-shot: wraps data whose length is fixed at compile time, returning the ciphertext. /// /// Provided over [`KeyWrapper::wrap_key_out`]. fn wrap_key( - kek: &KeyMaterial, + &self, key: &[u8; KEY_LEN], ) -> Result<[u8; CT_LEN], SymmetricCipherError> { let mut ciphertext = [0u8; CT_LEN]; - let bytes_written = Self::wrap_key_out(kek, key, &mut ciphertext)?; + let bytes_written = self.wrap_key_out(key, &mut ciphertext)?; debug_assert_eq!(bytes_written, CT_LEN); Ok(ciphertext) } @@ -810,9 +800,10 @@ pub trait KeyWrapper: Algorithm + Sized { /// assertion, so that it is a compile error at the call site, not a runtime `Err`. /// /// # Errors - /// [`SymmetricCipherError::KeyMaterialError`] for an unsuitable KEK. + /// None for a valid pair of lengths; the signature carries the `Result` for the run-time + /// form's sake. fn wrap_key_out( - kek: &KeyMaterial, + &self, key: &[u8; KEY_LEN], ciphertext: &mut [u8; CT_LEN], ) -> Result; @@ -824,18 +815,17 @@ pub trait KeyWrapper: Algorithm + Sized { /// Must not panic for any `plaintext_len`; for a length the algorithm is not defined on, the /// value is unspecified and [`KeyWrapper::wrap_out`] returns /// [`SymmetricCipherError::InvalidInputLength`]. - fn wrap_out_len(plaintext_len: usize) -> usize; + fn wrap_out_len(&self, plaintext_len: usize) -> usize; /// One-shot: wraps data whose length is only known at run time into `ciphertext`, which needs /// [`KeyWrapper::wrap_out_len`] bytes. Returns the number of bytes written. /// /// # Errors /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked before - /// any work is done; [`SymmetricCipherError::InvalidInputLength`] for a plaintext length the - /// algorithm is not defined on; and [`SymmetricCipherError::KeyMaterialError`] for an - /// unsuitable KEK. + /// any work is done, and [`SymmetricCipherError::InvalidInputLength`] for a plaintext length + /// the algorithm is not defined on. fn wrap_out( - kek: &KeyMaterial, + &self, plaintext: &[u8], ciphertext: &mut [u8], ) -> Result; From 02e8652926809858e896cc6dc8ef241ea7c2caf8 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 27 Oct 2026 03:37:56 +1100 Subject: [PATCH 04/18] core-test-framework: the key-wrap suite runs a key object through the roles and its workers `test` and `test_kat` take the key type and build it from the seed through FromKeyMaterial; the KEK type and strength checks leave, since the construction suite covers them, and the wrong-KEK case is a second key. The mock becomes a key with a borrowed worker. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core-test-framework/src/key_wrap.rs | 337 +++++++++++---------- 1 file changed, 174 insertions(+), 163 deletions(-) diff --git a/crypto/core-test-framework/src/key_wrap.rs b/crypto/core-test-framework/src/key_wrap.rs index b3e5cb9ee..c6be3e187 100644 --- a/crypto/core-test-framework/src/key_wrap.rs +++ b/crypto/core-test-framework/src/key_wrap.rs @@ -1,11 +1,11 @@ -//! Generic behaviour tests for the key-wrap traits [`KeyWrapper`] and [`KeyUnwrapper`]. +//! Generic behaviour tests for a key-wrap key object, through the [`KeyWrappingKey`] / +//! [`KeyUnwrappingKey`] roles and the [`KeyWrapper`] / [`KeyUnwrapper`] workers it hands out. +//! The key-type and strength policy of the construction is `TestFrameworkFromKeyMaterial`'s. use crate::DUMMY_SEED; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::key_material::{ - KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; -use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; use bouncycastle_utils::secret::Secret; @@ -17,87 +17,121 @@ pub struct TestFrameworkKeyWrap { // Put any config options here } +impl Default for TestFrameworkKeyWrap { + fn default() -> Self { + Self::new() + } +} + impl TestFrameworkKeyWrap { /// Create a new instance of the test framework. pub fn new() -> Self { Self {} } - /// Test all the members of traits [`KeyWrapper`] and [`KeyUnwrapper`] for one valid - /// `(KEY_LEN, CT_LEN)` pair, including the error conditions common to all key-wrap algorithms. - /// This gives good baseline test coverage, but is not a substitute for known-answer tests; see - /// [`TestFrameworkKeyWrap::test_kat`]. - pub fn test< - const KEK_LEN: usize, - const KEY_LEN: usize, - const CT_LEN: usize, - W: KeyWrapper + KeyUnwrapper, - >( - &self, - ) { - assert!(CT_LEN <= BUF_LEN, "test buffers are too small"); - + /// The key from `KEK_LEN` bytes of the dummy seed starting at `offset`, tagged as a cipher key. + fn key>(offset: usize) -> K { let kek = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEK_LEN], + &DUMMY_SEED[offset..offset + KEK_LEN], KeyType::SymmetricCipherKey, ) .unwrap(); + K::from_key_material(&kek).expect("a cipher key of the key's own length") + } + + /// Every member of the two roles and the two worker traits for one valid `(KEY_LEN, CT_LEN)` + /// pair, including the error conditions common to all key-wrap algorithms. This gives good + /// baseline test coverage, but is not a substitute for known-answer tests; see + /// [`TestFrameworkKeyWrap::test_kat`]. + pub fn test(&self) + where + K: FromKeyMaterial + KeyWrappingKey + KeyUnwrappingKey, + { + assert!(CT_LEN <= BUF_LEN, "test buffers are too small"); + + let kek: K = Self::key(0); let key: [u8; KEY_LEN] = DUMMY_SEED[100..100 + KEY_LEN].try_into().unwrap(); - /* Length helpers */ + /* Length helpers, on the key and on the workers */ - assert_eq!(W::wrap_out_len(KEY_LEN), CT_LEN); - let max_pt_len = W::unwrap_out_max_len(CT_LEN); + assert_eq!(kek.wrap_out_len(KEY_LEN), CT_LEN); + assert_eq!(kek.create_wrapper().wrap_out_len(KEY_LEN), CT_LEN); + let max_pt_len = kek.unwrap_out_max_len(CT_LEN); + assert_eq!(kek.create_unwrapper().unwrap_out_max_len(CT_LEN), max_pt_len); assert!(max_pt_len >= KEY_LEN, "unwrap_out_max_len is not an upper bound"); assert!(max_pt_len < CT_LEN, "a key-wrap ciphertext is longer than its plaintext"); // documented as saturating, and as never panicking - assert_eq!(W::unwrap_out_max_len(0), 0); - let _ = W::unwrap_out_max_len(usize::MAX); - let _ = W::wrap_out_len(usize::MAX); + assert_eq!(kek.unwrap_out_max_len(0), 0); + let _ = kek.unwrap_out_max_len(usize::MAX); + let _ = kek.wrap_out_len(usize::MAX); // one semiblock: half the block cipher's block length let semiblock_len = CT_LEN - max_pt_len; /* Fixed-length API */ - let ct = W::wrap_key::(&kek, &key).unwrap(); + let ct = kek.wrap_key::(&key).unwrap(); - // wrap_key() and wrap_key_out() agree + // wrap_key() and wrap_key_out() agree, on the key and on the worker let mut ct_out = [0u8; CT_LEN]; - let bytes_written = W::wrap_key_out(&kek, &key, &mut ct_out).unwrap(); + let bytes_written = kek.wrap_key_out(&key, &mut ct_out).unwrap(); assert_eq!(bytes_written, CT_LEN); assert_eq!(ct, ct_out); + assert_eq!(kek.create_wrapper().wrap_key::(&key).unwrap(), ct); + ct_out.fill(0); + assert_eq!(kek.create_wrapper().wrap_key_out(&key, &mut ct_out).unwrap(), CT_LEN); + assert_eq!(ct, ct_out); // key wrapping is deterministic - assert_eq!(ct, W::wrap_key::(&kek, &key).unwrap()); + assert_eq!(ct, kek.wrap_key::(&key).unwrap()); - // round trip through unwrap_key() and unwrap_key_out() - let recovered: Secret<[u8; KEY_LEN]> = W::unwrap_key(&kek, &ct).unwrap(); + // round trip through unwrap_key() and unwrap_key_out(), on the key and on the worker + let recovered: Secret<[u8; KEY_LEN]> = kek.unwrap_key(&ct).unwrap(); + assert_eq!(*recovered, key); + let recovered: Secret<[u8; KEY_LEN]> = kek.create_unwrapper().unwrap_key(&ct).unwrap(); assert_eq!(*recovered, key); let mut recovered_out = Secret::<[u8; KEY_LEN]>::new(); - let bytes_written = W::unwrap_key_out(&kek, &ct, &mut recovered_out).unwrap(); + let bytes_written = kek.unwrap_key_out(&ct, &mut recovered_out).unwrap(); + assert_eq!(bytes_written, KEY_LEN); + assert_eq!(*recovered_out, key); + recovered_out.zeroize(); + let bytes_written = kek.create_unwrapper().unwrap_key_out(&ct, &mut recovered_out).unwrap(); assert_eq!(bytes_written, KEY_LEN); assert_eq!(*recovered_out, key); /* Variable-length API agrees with the fixed-length API */ let mut buf = [0u8; BUF_LEN]; - let bytes_written = W::wrap_out(&kek, &key, &mut buf[..CT_LEN]).unwrap(); + let bytes_written = kek.wrap_out(&key, &mut buf[..CT_LEN]).unwrap(); assert_eq!(bytes_written, CT_LEN); assert_eq!(&buf[..CT_LEN], &ct); + buf.fill(0); + assert_eq!(kek.create_wrapper().wrap_out(&key, &mut buf[..CT_LEN]).unwrap(), CT_LEN); + assert_eq!(&buf[..CT_LEN], &ct); // a larger buffer than needed is fine - let bytes_written = W::wrap_out(&kek, &key, &mut buf).unwrap(); + let bytes_written = kek.wrap_out(&key, &mut buf).unwrap(); assert_eq!(bytes_written, CT_LEN); let mut pt = [0u8; BUF_LEN]; - let bytes_written = W::unwrap_out(&kek, &ct, &mut pt[..max_pt_len]).unwrap(); + let bytes_written = kek.unwrap_out(&ct, &mut pt[..max_pt_len]).unwrap(); assert_eq!(bytes_written, KEY_LEN); assert_eq!(&pt[..KEY_LEN], &key); + pt.fill(0); + assert_eq!(kek.create_unwrapper().unwrap_out(&ct, &mut pt[..max_pt_len]).unwrap(), KEY_LEN); + assert_eq!(&pt[..KEY_LEN], &key); + + // several workers in flight from one key, and the key unchanged by use + let a = kek.create_wrapper(); + let b = kek.create_unwrapper(); + assert_eq!(a.wrap_key::(&key).unwrap(), ct); + assert_eq!(*b.unwrap_key::(&ct).unwrap(), key); + assert_eq!(a.wrap_key::(&key).unwrap(), ct); + assert_eq!(kek.wrap_key::(&key).unwrap(), ct); /* Error case: output buffers too small */ - match W::wrap_out(&kek, &key, &mut buf[..CT_LEN - 1]) { + match kek.wrap_out(&key, &mut buf[..CT_LEN - 1]) { Err(SymmetricCipherError::OutputBufferTooSmall(required)) => { assert_eq!(required, CT_LEN) } @@ -105,7 +139,7 @@ impl TestFrameworkKeyWrap { } // checked before any work is done, so it is the upper bound that is enforced, even when // the actual plaintext would have fit - match W::unwrap_out(&kek, &ct, &mut pt[..max_pt_len - 1]) { + match kek.unwrap_out(&ct, &mut pt[..max_pt_len - 1]) { Err(SymmetricCipherError::OutputBufferTooSmall(required)) => { assert_eq!(required, max_pt_len) } @@ -117,12 +151,12 @@ impl TestFrameworkKeyWrap { // Every key-wrap algorithm in SP 800-38F (Table 1) needs at least one octet of plaintext // and a ciphertext of at least two semiblocks that is a whole number of semiblocks. // Empty inputs also catch implementations that subtract before checking the length. - match W::wrap_out(&kek, &[], &mut buf) { + match kek.wrap_out(&[], &mut buf) { Err(SymmetricCipherError::InvalidInputLength(_)) => { /* good */ } _ => panic!("Should have rejected an empty plaintext"), } for bad_ct_len in [0, semiblock_len, CT_LEN - 1] { - match W::unwrap_out(&kek, &ct[..bad_ct_len], &mut pt) { + match kek.unwrap_out(&ct[..bad_ct_len], &mut pt) { Err(SymmetricCipherError::InvalidInputLength(_)) => { /* good */ } _ => panic!("Should have rejected a ciphertext of {bad_ct_len} bytes"), } @@ -136,161 +170,131 @@ impl TestFrameworkKeyWrap { let mut bad_ct = ct; bad_ct[i] ^= 0x01; - match W::unwrap_key::(&kek, &bad_ct) { + match kek.unwrap_key::(&bad_ct) { Err(SymmetricCipherError::DecryptionFailed) => { /* good */ } _ => panic!("Should have failed to unwrap a ciphertext modified at byte {i}"), } // on failure the plaintext buffer is zeroized, not left holding partial output pt.fill(0xFF); - match W::unwrap_out(&kek, &bad_ct, &mut pt) { + match kek.unwrap_out(&bad_ct, &mut pt) { Err(SymmetricCipherError::DecryptionFailed) => { /* good */ } _ => panic!("Should have failed to unwrap a ciphertext modified at byte {i}"), } assert!(pt.iter().all(|b| *b == 0), "plaintext buffer was not zeroized on error"); + let mut out = Secret::<[u8; KEY_LEN]>::new(); + out.fill(0xFF); + assert!(kek.unwrap_key_out(&bad_ct, &mut out).is_err()); + assert!(out.iter().all(|b| *b == 0), "the Secret was not zeroized on error"); } /* Error case: wrong KEK */ - let wrong_kek = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[1..KEK_LEN + 1], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - match W::unwrap_key::(&wrong_kek, &ct) { + let wrong_kek: K = Self::key(1); + match wrong_kek.unwrap_key::(&ct) { Err(SymmetricCipherError::DecryptionFailed) => { /* good */ } _ => panic!("Should have failed to unwrap under the wrong KEK"), } - - /* Error case: KEK of the wrong type */ - - let mac_key = - KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEK_LEN], KeyType::MACKey) - .unwrap(); - match W::wrap_key::(&mac_key, &key) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("Should have rejected a KEK that is not a SymmetricCipherKey"), - } - match W::wrap_out(&mac_key, &key, &mut buf) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("Should have rejected a KEK that is not a SymmetricCipherKey"), - } - match W::unwrap_key::(&mac_key, &ct) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("Should have rejected a KEK that is not a SymmetricCipherKey"), - } - match W::unwrap_out(&mac_key, &ct, &mut pt) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("Should have rejected a KEK that is not a SymmetricCipherKey"), - } - - /* Error case: KEK security strength too weak for the algorithm */ - - let mut kek = kek; - let security_strengths = [ - SecurityStrength::None, - SecurityStrength::_112bit, - SecurityStrength::_128bit, - SecurityStrength::_192bit, - SecurityStrength::_256bit, - ]; - for ss in security_strengths.iter() { - // `set_security_strength` enforces its key-length guard even inside a - // do_hazardous_operations() closure, so skip the strengths this KEK cannot carry, - // as the symmetric cipher frameworks do. - if ss > &SecurityStrength::from_bytes(KEK_LEN) { - continue; - } - do_hazardous_operations(&mut kek, |kek| kek.set_security_strength(*ss)).unwrap(); - - let strong_enough = ss >= &W::MAX_SECURITY_STRENGTH; - match W::wrap_key::(&kek, &key) { - Ok(_) => assert!( - strong_enough, - "Should not have accepted a KEK weaker than the algorithm" - ), - Err(SymmetricCipherError::KeyMaterialError(_)) => { - assert!(!strong_enough, "Should have accepted a strong enough KEK") - } - _ => panic!("Unexpected error"), - } - match W::unwrap_key::(&kek, &ct) { - Ok(_) => assert!( - strong_enough, - "Should not have accepted a KEK weaker than the algorithm" - ), - Err(SymmetricCipherError::KeyMaterialError(_)) => { - assert!(!strong_enough, "Should have accepted a strong enough KEK") - } - _ => panic!("Unexpected error"), - } - } + assert_ne!( + wrong_kek.wrap_key::(&key).unwrap(), + ct, + "two KEKs must not wrap to the same ciphertext" + ); } /// Known-answer test: checks that wrapping `key` under `kek` gives `expected_ct` through both /// the fixed-length and variable-length APIs, and that unwrapping `expected_ct` gives back `key`. /// Test vectors must come from the specification or its official companion files, for example /// RFC 3394 Section 4 or RFC 5649 Section 6. - pub fn test_kat< - const KEK_LEN: usize, - const KEY_LEN: usize, - const CT_LEN: usize, - W: KeyWrapper + KeyUnwrapper, - >( + pub fn test_kat( &self, kek: &[u8; KEK_LEN], key: &[u8; KEY_LEN], expected_ct: &[u8; CT_LEN], - ) { + ) where + K: FromKeyMaterial + KeyWrappingKey + KeyUnwrappingKey, + { let kek = KeyMaterial::::from_bytes_as_type(kek, KeyType::SymmetricCipherKey).unwrap(); + let kek = K::from_key_material(&kek).expect("the vector's KEK"); - let ct = W::wrap_key::(&kek, key).unwrap(); + let ct = kek.wrap_key::(key).unwrap(); assert_eq!(&ct, expected_ct); - let recovered: Secret<[u8; KEY_LEN]> = W::unwrap_key(&kek, expected_ct).unwrap(); + let recovered: Secret<[u8; KEY_LEN]> = kek.unwrap_key(expected_ct).unwrap(); assert_eq!(&*recovered, key); let mut buf = [0u8; BUF_LEN]; - let bytes_written = W::wrap_out(&kek, key, &mut buf).unwrap(); + let bytes_written = kek.wrap_out(key, &mut buf).unwrap(); assert_eq!(&buf[..bytes_written], expected_ct); - let bytes_written = W::unwrap_out(&kek, expected_ct, &mut buf).unwrap(); + let bytes_written = kek.unwrap_out(expected_ct, &mut buf).unwrap(); assert_eq!(&buf[..bytes_written], key); } } #[cfg(test)] mod tests { - //! Exercises the framework itself against a mock implementation, so that the framework is known + //! Exercises the framework itself against a mock key and worker, so that the framework is known //! to compile and pass before any real key-wrap algorithm exists. use super::*; use bouncycastle_core::errors::KeyMaterialError; + use bouncycastle_core::key_material::KeyMaterialTrait; + use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::Algorithm; - /// NOT CRYPTOGRAPHY. A stand-in with the same length behaviour as KWP (SP 800-38F, Sec. 6.3), - /// which satisfies the documented contract of the key-wrap traits. It builds + const MOCK_KEK_LEN: usize = 16; + const MOCK_SEMIBLOCK_LEN: usize = 8; + + /// NOT CRYPTOGRAPHY. A key whose worker has the same length behaviour as KWP (SP 800-38F, + /// Sec. 6.3) and satisfies the documented contract of the key-wrap traits. It builds /// `S = [len(P)]32 || checksum32(kek || P) || P || zero padding` to a multiple of 8 bytes, and /// XORs `S` with the repeated KEK. - struct MockKeyWrap; + struct MockKeyWrapKey { + kek: [u8; MOCK_KEK_LEN], + } - const MOCK_KEK_LEN: usize = 16; - const MOCK_SEMIBLOCK_LEN: usize = 8; + /// The worker: the KEK bytes, borrowed. + struct MockKeyWrap<'k> { + kek: &'k [u8; MOCK_KEK_LEN], + } - impl Algorithm for MockKeyWrap { + impl Algorithm for MockKeyWrap<'_> { const ALG_NAME: &'static str = "MockKeyWrap-NOT-CRYPTOGRAPHY"; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; } - fn check_kek(kek: &KeyMaterial) -> Result<(), SymmetricCipherError> { - if kek.key_type() != KeyType::SymmetricCipherKey { - Err(KeyMaterialError::InvalidKeyType("KEK must be a SymmetricCipherKey"))? + impl FromKeyMaterial for MockKeyWrapKey { + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn from_key_material( + kek: &KeyMaterial, + ) -> Result { + if kek.key_type() != KeyType::SymmetricCipherKey { + Err(KeyMaterialError::InvalidKeyType("KEK must be a SymmetricCipherKey"))? + } + if kek.security_strength() < MockKeyWrap::MAX_SECURITY_STRENGTH { + Err(KeyMaterialError::SecurityStrength("KEK is weaker than the algorithm"))? + } + Ok(Self { kek: kek.ref_to_bytes().try_into().unwrap() }) } - if kek.security_strength() < MockKeyWrap::MAX_SECURITY_STRENGTH { - Err(KeyMaterialError::SecurityStrength("KEK is weaker than the algorithm"))? + } + + impl KeyWrappingKey for MockKeyWrapKey { + type Wrapper<'k> = MockKeyWrap<'k>; + + fn create_wrapper<'k>(&'k self) -> Self::Wrapper<'k> { + MockKeyWrap { kek: &self.kek } + } + } + + impl KeyUnwrappingKey for MockKeyWrapKey { + type Unwrapper<'k> = MockKeyWrap<'k>; + + fn create_unwrapper<'k>(&'k self) -> Self::Unwrapper<'k> { + MockKeyWrap { kek: &self.kek } } - Ok(()) } /// 32-bit FNV-1a over `kek || pt`. @@ -309,9 +313,9 @@ mod tests { } } - impl KeyWrapper for MockKeyWrap { + impl KeyWrapper for MockKeyWrap<'_> { fn wrap_key_out( - kek: &KeyMaterial, + &self, key: &[u8; KEY_LEN], ciphertext: &mut [u8; CT_LEN], ) -> Result { @@ -323,10 +327,10 @@ mod tests { + MOCK_SEMIBLOCK_LEN ) }; - Self::wrap_out(kek, key, ciphertext) + self.wrap_out(key, ciphertext) } - fn wrap_out_len(plaintext_len: usize) -> usize { + fn wrap_out_len(&self, plaintext_len: usize) -> usize { plaintext_len .div_ceil(MOCK_SEMIBLOCK_LEN) .saturating_mul(MOCK_SEMIBLOCK_LEN) @@ -334,32 +338,30 @@ mod tests { } fn wrap_out( - kek: &KeyMaterial, + &self, plaintext: &[u8], ciphertext: &mut [u8], ) -> Result { - let ct_len = Self::wrap_out_len(plaintext.len()); + let ct_len = self.wrap_out_len(plaintext.len()); if ciphertext.len() < ct_len { return Err(SymmetricCipherError::OutputBufferTooSmall(ct_len)); } - check_kek(kek)?; if plaintext.is_empty() || plaintext.len() > u32::MAX as usize { return Err(SymmetricCipherError::InvalidInputLength("plaintext length")); } - let kek = kek.ref_to_bytes(); let s = &mut ciphertext[..ct_len]; s.fill(0); s[..4].copy_from_slice(&(plaintext.len() as u32).to_be_bytes()); - s[4..8].copy_from_slice(&checksum(kek, plaintext)); + s[4..8].copy_from_slice(&checksum(self.kek, plaintext)); s[8..8 + plaintext.len()].copy_from_slice(plaintext); - xor_with_kek(kek, s); + xor_with_kek(self.kek, s); Ok(ct_len) } } - impl KeyUnwrapper for MockKeyWrap { + impl KeyUnwrapper for MockKeyWrap<'_> { fn unwrap_key_out( - kek: &KeyMaterial, + &self, ciphertext: &[u8; CT_LEN], key: &mut Secret<[u8; KEY_LEN]>, ) -> Result { @@ -374,35 +376,43 @@ mod tests { // The padded plaintext is up to 7 bytes longer than KEY_LEN, so unwrap into a // scratch Secret sized for it, then check that the recovered length is KEY_LEN. let mut scratch = Secret::<[u8; BUF_LEN]>::new(); - let pt_len = Self::unwrap_out(kek, ciphertext, &mut scratch[..])?; - if pt_len != KEY_LEN { - return Err(SymmetricCipherError::DecryptionFailed); + let result = self.unwrap_out(ciphertext, &mut scratch[..]); + match result { + Ok(pt_len) if pt_len == KEY_LEN => { + key.copy_from_slice(&scratch[..KEY_LEN]); + Ok(KEY_LEN) + } + Ok(_) => { + key.zeroize(); + Err(SymmetricCipherError::DecryptionFailed) + } + Err(e) => { + key.zeroize(); + Err(e) + } } - key.copy_from_slice(&scratch[..KEY_LEN]); - Ok(KEY_LEN) } - fn unwrap_out_max_len(ciphertext_len: usize) -> usize { + fn unwrap_out_max_len(&self, ciphertext_len: usize) -> usize { ciphertext_len.saturating_sub(MOCK_SEMIBLOCK_LEN) } fn unwrap_out( - kek: &KeyMaterial, + &self, ciphertext: &[u8], plaintext: &mut [u8], ) -> Result { - let max_pt_len = Self::unwrap_out_max_len(ciphertext.len()); + let max_pt_len = self.unwrap_out_max_len(ciphertext.len()); if plaintext.len() < max_pt_len { return Err(SymmetricCipherError::OutputBufferTooSmall(max_pt_len)); } plaintext.fill(0); - check_kek(kek)?; if ciphertext.len() < 2 * MOCK_SEMIBLOCK_LEN || !ciphertext.len().is_multiple_of(MOCK_SEMIBLOCK_LEN) { return Err(SymmetricCipherError::InvalidInputLength("ciphertext length")); } - let kek = kek.ref_to_bytes(); + let kek = self.kek; // Decrypt the header, and the padded plaintext straight into the caller's buffer. let mut header = [0u8; MOCK_SEMIBLOCK_LEN]; @@ -432,9 +442,9 @@ mod tests { fn framework_passes_against_mock() { let tf = TestFrameworkKeyWrap::new(); // a whole number of semiblocks, one that needs padding, and the shortest possible - tf.test::(); - tf.test::(); - tf.test::(); + tf.test::(); + tf.test::(); + tf.test::(); } /// Only checks the plumbing of test_kat(): the "expected" value is generated by the mock itself. @@ -446,8 +456,9 @@ mod tests { let kek_material = KeyMaterial::::from_bytes_as_type(&kek, KeyType::SymmetricCipherKey) .unwrap(); - let expected: [u8; 16] = MockKeyWrap::wrap_key(&kek_material, &key).unwrap(); + let expected: [u8; 16] = + MockKeyWrapKey::from_key_material(&kek_material).unwrap().wrap_key(&key).unwrap(); TestFrameworkKeyWrap::new() - .test_kat::(&kek, &key, &expected); + .test_kat::(&kek, &key, &expected); } } From 60d41f711b7a31de3174012ee5e77a9260c6babb Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 27 Oct 2026 03:46:16 +1100 Subject: [PATCH 05/18] cipher: KW and KWP as mode types over the key path; Kw and Kwp borrow the keyed permutation The two structs from PR #160 held a PhantomData and keyed the permutation on every call. They become workers on the shape of Cmac: `Kw<'k, P>` over a `&'k P`, with `KW` / `KWP` as the mode markers a key is bound to, their key state the permutation alone, and KeyWrapMode / KeyUnwrapMode handing the worker out. ModeNames gains KW_ALG_NAME and KWP_ALG_NAME. The toy key gets the two roles, and the structural tests run over it; the framework's tamper checks need diffusion, so they run in `aes`. Assisted-by: Claude:claude-mythos-5-1 --- crypto/aes/tests/kw_alias_tests.rs | 41 --- .../{acvp_kw_tests.rs => kw_bc-test-data.rs} | 0 crypto/cipher/Cargo.toml | 4 + crypto/cipher/src/modes/ccm.rs | 2 + crypto/cipher/src/modes/kw.rs | 209 +++++++++----- crypto/cipher/src/modes/kwp.rs | 155 +++++++---- crypto/cipher/src/modes/mod.rs | 23 +- .../cipher/tests/common/toy_block_cipher.rs | 2 + crypto/cipher/tests/common/toy_key.rs | 36 ++- crypto/cipher/tests/modes/alg_name_tests.rs | 6 +- crypto/cipher/tests/modes/common/mod.rs | 8 + crypto/cipher/tests/modes/kw_tests.rs | 258 ++++++++++++++++++ 12 files changed, 574 insertions(+), 170 deletions(-) delete mode 100644 crypto/aes/tests/kw_alias_tests.rs rename crypto/aes/tests/{acvp_kw_tests.rs => kw_bc-test-data.rs} (100%) create mode 100644 crypto/cipher/tests/modes/kw_tests.rs diff --git a/crypto/aes/tests/kw_alias_tests.rs b/crypto/aes/tests/kw_alias_tests.rs deleted file mode 100644 index 083ad6e3d..000000000 --- a/crypto/aes/tests/kw_alias_tests.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Tests for the AES-KW and AES-KWP aliases. -//! -//! These are only type aliases, so what is worth testing here is that each names the right -//! permutation and key length -- a 16-byte KEK for `AES_KW_128` and so on -- and that they run -//! the shared key-wrap framework. The algorithms themselves, and the RFC 3394 / RFC 5649 / ACVP -//! vectors, are tested in `bouncycastle-modes`. - -use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_aes::{AES_KW_128, AES_KW_192, AES_KW_256, AES_KWP_128, AES_KWP_192, AES_KWP_256}; -use bouncycastle_core::traits::Algorithm; -use bouncycastle_core_test_framework::key_wrap::TestFrameworkKeyWrap; - -/// The alias carries its permutation's name and strength, as the other mode aliases do. -#[test] -fn the_aliases_name_the_expected_permutation() { - assert_eq!(AES_KW_128::ALG_NAME, AES128Internal::ALG_NAME); - assert_eq!(AES_KW_192::ALG_NAME, AES192Internal::ALG_NAME); - assert_eq!(AES_KW_256::ALG_NAME, AES256Internal::ALG_NAME); - assert_eq!(AES_KWP_128::ALG_NAME, AES128Internal::ALG_NAME); - assert_eq!(AES_KWP_192::ALG_NAME, AES192Internal::ALG_NAME); - assert_eq!(AES_KWP_256::ALG_NAME, AES256Internal::ALG_NAME); - - assert_eq!(AES_KW_128::MAX_SECURITY_STRENGTH, AES128Internal::MAX_SECURITY_STRENGTH); - assert_eq!(AES_KW_256::MAX_SECURITY_STRENGTH, AES256Internal::MAX_SECURITY_STRENGTH); - assert_eq!(AES_KWP_192::MAX_SECURITY_STRENGTH, AES192Internal::MAX_SECURITY_STRENGTH); -} - -/// Each alias conforms to the shared framework under its own KEK length, wrapping a 256-bit key. -#[test] -fn the_aliases_conform_to_the_key_wrap_framework() { - let tf = TestFrameworkKeyWrap::new(); - tf.test::<16, 32, 40, AES_KW_128>(); - tf.test::<24, 32, 40, AES_KW_192>(); - tf.test::<32, 32, 40, AES_KW_256>(); - tf.test::<16, 32, 40, AES_KWP_128>(); - tf.test::<24, 32, 40, AES_KWP_192>(); - tf.test::<32, 32, 40, AES_KWP_256>(); - // ... and KWP something that is not a whole number of semiblocks. - tf.test::<16, 33, 48, AES_KWP_128>(); - tf.test::<32, 7, 16, AES_KWP_256>(); -} diff --git a/crypto/aes/tests/acvp_kw_tests.rs b/crypto/aes/tests/kw_bc-test-data.rs similarity index 100% rename from crypto/aes/tests/acvp_kw_tests.rs rename to crypto/aes/tests/kw_bc-test-data.rs diff --git a/crypto/cipher/Cargo.toml b/crypto/cipher/Cargo.toml index 5e6261e50..40568cb29 100644 --- a/crypto/cipher/Cargo.toml +++ b/crypto/cipher/Cargo.toml @@ -66,6 +66,10 @@ path = "tests/modes/gcm_tests.rs" name = "gmac_tests" path = "tests/modes/gmac_tests.rs" +[[test]] +name = "kw_tests" +path = "tests/modes/kw_tests.rs" + [[test]] name = "symmetric_cipher_api_tests" path = "tests/modes/symmetric_cipher_api_tests.rs" diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs index 8bf658bf8..373f57cb9 100644 --- a/crypto/cipher/src/modes/ccm.rs +++ b/crypto/cipher/src/modes/ccm.rs @@ -1856,6 +1856,8 @@ mod tests { const ECB_ALG_NAME: &'static str = "identity_ECB"; const GCM_ALG_NAME: &'static str = "identity_GCM"; const GMAC_ALG_NAME: &'static str = "identity_GMAC"; + const KW_ALG_NAME: &'static str = "identity_KW"; + const KWP_ALG_NAME: &'static str = "identity_KWP"; } impl ElectronicCodeBook<16, 16> for Identity { diff --git a/crypto/cipher/src/modes/kw.rs b/crypto/cipher/src/modes/kw.rs index 84851cc10..4a7797a85 100644 --- a/crypto/cipher/src/modes/kw.rs +++ b/crypto/cipher/src/modes/kw.rs @@ -5,66 +5,79 @@ //! is a whole number of 8-byte semiblocks -- under a key encryption key (KEK). There is no IV: the //! integrity check value ICV1 stands in for one, and wrapping the same data under the same KEK //! always gives the same ciphertext, which is one semiblock longer than the plaintext. For data -//! of any other length, use [`Kwp`](crate::kwp::Kwp). +//! of any other length, use [`Kwp`](crate::modes::kwp::Kwp). //! -//! The mode is cipher-agnostic in the same way as the rest of this crate: [`Kw`] takes any -//! [`ElectronicCodeBook`] whose block is 128 bits (SP 800-38F Sec 5.1 requires that for KW and -//! KWP, and AES is currently the only *approved* cipher that fits) and implements the -//! [`KeyWrapper`] and [`KeyUnwrapper`] traits from `bouncycastle-core`. The ready-made -//! `AES_KW_128` / `AES_KW_192` / `AES_KW_256` aliases are in `bouncycastle-aes`. +//! The way onto it is a mode-bound key, built with `from_key_material`, whose state is the keyed +//! permutation and which hands out the one-shot worker [`Kw`] through [`KW`], the marker a key type +//! such as `bouncycastle_aes::AES128Key` is bound to. The key's roles, +//! [`KeyWrappingKey`] / [`KeyUnwrappingKey`], carry the one-shots; the worker implements +//! [`KeyWrapper`] / [`KeyUnwrapper`] over any [`ElectronicCodeBook`] whose block is 128 bits +//! (SP 800-38F Sec 5.1 requires that for KW and KWP, and AES is currently the only *approved* +//! cipher that fits). The ready-made `AES_KW_128` / `AES_KW_192` / `AES_KW_256` aliases are in +//! `bouncycastle-aes`. //! //! # Usage Examples //! +//! From a key state -- what a mode-bound key holds -- over `ToyBlockCipher` (see the +//! [module docs](super) for why): +//! //! ``` -//! use bouncycastle_aes::aes_internal::AES128Internal; +//! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # use toy::ToyBlockCipher; +//! use bouncycastle_cipher::modes::Kw; +//! use bouncycastle_core::hazmat::ElectronicCodeBook; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; -//! use bouncycastle_modes::Kw; -//! -//! type Aes128Kw = Kw; //! //! let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let state = ToyBlockCipher::new(&kek).unwrap(); +//! let kw = Kw::from_perm(&state); //! let key_to_wrap = [0x5Au8; 32]; //! //! // Lengths fixed at compile time: a 32-byte key wraps to 40 bytes, and any other pair of //! // lengths is a compile error, not a runtime one. -//! let wrapped: [u8; 40] = Aes128Kw::wrap_key(&kek, &key_to_wrap).expect("wrapping"); -//! let recovered = Aes128Kw::unwrap_key::<32, 40>(&kek, &wrapped).expect("unwrapping"); +//! let wrapped: [u8; 40] = kw.wrap_key(&key_to_wrap).expect("wrapping"); +//! let recovered = kw.unwrap_key::<32, 40>(&wrapped).expect("unwrapping"); //! assert_eq!(*recovered, key_to_wrap); //! //! // Lengths known only at run time: the caller sizes the buffers with the length helpers. //! let mut wrapped_out = [0u8; 64]; -//! let n = Aes128Kw::wrap_out(&kek, &key_to_wrap, &mut wrapped_out).expect("wrapping"); -//! assert_eq!(n, Aes128Kw::wrap_out_len(key_to_wrap.len())); +//! let n = kw.wrap_out(&key_to_wrap, &mut wrapped_out).expect("wrapping"); +//! assert_eq!(n, kw.wrap_out_len(key_to_wrap.len())); //! assert_eq!(wrapped_out[..n], wrapped); //! ``` //! -//! A modified ciphertext, or the wrong KEK, is rejected rather than unwrapped to garbage: +//! A modified ciphertext, or the wrong KEK, is rejected rather than unwrapped to garbage. (The +//! toy has no diffusion, so only a change to the first semiblock reaches its integrity check; +//! under a real cipher any change does.) //! //! ``` -//! # use bouncycastle_aes::aes_internal::AES128Internal; +//! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # use toy::ToyBlockCipher; +//! # use bouncycastle_cipher::modes::Kw; +//! # use bouncycastle_core::hazmat::ElectronicCodeBook; //! # use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! # use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; -//! # use bouncycastle_modes::Kw; //! use bouncycastle_core::errors::SymmetricCipherError; -//! # type Aes128Kw = Kw; //! # let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! # .expect("a 16-byte symmetric cipher key"); -//! # let mut wrapped: [u8; 40] = Aes128Kw::wrap_key(&kek, &[0x5Au8; 32]).expect("wrapping"); -//! wrapped[17] ^= 0x01; +//! # let state = ToyBlockCipher::new(&kek).unwrap(); +//! # let kw = Kw::from_perm(&state); +//! # let mut wrapped: [u8; 40] = kw.wrap_key(&[0x5Au8; 32]).expect("wrapping"); +//! wrapped[3] ^= 0x01; //! assert!(matches!( -//! Aes128Kw::unwrap_key::<32, 40>(&kek, &wrapped), +//! kw.unwrap_key::<32, 40>(&wrapped), //! Err(SymmetricCipherError::DecryptionFailed) //! )); //! ``` //! //! # No direction marker //! -//! Unlike [`Ecb`](crate::Ecb) and the other modes, [`Kw`] has no `Dir` parameter. The wrapping -//! and unwrapping halves are already separate traits ([`KeyWrapper`] / [`KeyUnwrapper`]) whose -//! methods are associated functions, so a policy can import one trait and not the other, and there -//! is no per-direction state for a marker type to select. +//! Unlike [`Ecb`](crate::modes::hazmat::ecb::Ecb) and the other modes, [`Kw`] has no `Dir` +//! parameter. The wrapping and unwrapping halves are already separate traits ([`KeyWrapper`] / +//! [`KeyUnwrapper`]), so a policy can import one trait and not the other, and there is no +//! per-direction state for a marker type to select. //! //! # The wrapping function runs in place, without the shift register //! @@ -94,21 +107,27 @@ //! strong cryptographically as any key that it protects". The wrapped data is plain bytes with no //! security strength attached, so this mode cannot check it; it is the caller's responsibility. //! The KEK itself must be a `SymmetricCipherKey` at least as strong as the permutation's -//! `MAX_SECURITY_STRENGTH`, which the permutation enforces. +//! `MAX_SECURITY_STRENGTH`, which the key's constructor enforces. //! //! ## The length limits are part of the security argument //! //! Table 1 limits a KW plaintext to fewer than 2^54 semiblocks so that the forgery probability //! stays below 2^-64 (Appendix A.4). The fixed-length API checks the limits at compile time and //! the run-time API returns [`SymmetricCipherError::InvalidInputLength`] outside them. +//! +//! [`KeyWrappingKey`]: bouncycastle_core::symmetric_key::KeyWrappingKey +//! [`KeyUnwrappingKey`]: bouncycastle_core::symmetric_key::KeyUnwrappingKey +use super::ModeNames; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::hazmat::{ + BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, +}; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, KeyUnwrapper, KeyWrapper}; +use bouncycastle_core::traits::{Algorithm, KeyUnwrapper, KeyWrapper}; use bouncycastle_utils::ct::ct_eq_bytes; use bouncycastle_utils::secret::Secret; -use core::marker::PhantomData; /// The block length, in bytes, of the permutation under KW and KWP. SP 800-38F Sec 5.1: "For KW /// and KWP, the underlying block cipher shall be approved, and the block size shall be 128 bits." @@ -154,12 +173,9 @@ pub const fn kw_wrapped_len(len: usize) -> usize { /// /// Runs in place rather than through the shift register of the spec's step 2; see the module /// docs for why the two are the same computation. -pub(crate) fn w( - perm: &P, - a: &mut [u8; SEMIBLOCK_LEN], - r: &mut [[u8; SEMIBLOCK_LEN]], -) where - P: ElectronicCodeBook, +pub(crate) fn w

(perm: &P, a: &mut [u8; SEMIBLOCK_LEN], r: &mut [[u8; SEMIBLOCK_LEN]]) +where + P: BlockPermutation, { debug_assert!(r.len() >= 2, "Algorithm 1 is defined on n >= 3 semiblocks"); // Everything that passes through `block` is derived from the key being wrapped, so it is @@ -194,12 +210,9 @@ pub(crate) fn w( /// `a` holds `C1` and `r` holds `C2 .. Cn`, with `r.len() == n - 1 >= 2`. On return `a` holds /// `S1` and `r` holds `S2 .. Sn`. The registers are visited in the reverse of the order [`w`] /// visits them, so that step `t` here undoes step `t` there. -pub(crate) fn w_inv( - perm: &P, - a: &mut [u8; SEMIBLOCK_LEN], - r: &mut [[u8; SEMIBLOCK_LEN]], -) where - P: ElectronicCodeBook, +pub(crate) fn w_inv

(perm: &P, a: &mut [u8; SEMIBLOCK_LEN], r: &mut [[u8; SEMIBLOCK_LEN]]) +where + P: BlockPermutation, { debug_assert!(r.len() >= 2, "Algorithm 2 is defined on n >= 3 semiblocks"); let mut block: Secret<[u8; KW_BLOCK_LEN]> = Secret::new(); @@ -225,39 +238,93 @@ pub(crate) fn w_inv( debug_assert_eq!(t, 0, "Algorithm 2 step 2 ends at t = 1"); } -/// KW over any 128-bit permutation that impls [`ElectronicCodeBook`]. -/// -/// `KEK_LEN` is the permutation's key length. There is no state and no direction marker: both -/// halves of the algorithm are associated functions of the [`KeyWrapper`] and [`KeyUnwrapper`] -/// traits, and the key schedule lives only for the duration of one call, in the permutation, which -/// is responsible for keeping it in a zeroize-on-drop wrapper. +/// The KW mode type, as a key type names it: `AES128Key`. Its key state is the keyed +/// permutation alone, and its workers are [`Kw`] on both sides. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KW; + +impl

Mode

for KW +where + P: BlockPermutation + ModeNames + Clone, +{ + type KeyState = P; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + perm + } +} + +impl KeyWrapMode for KW +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Wrapper<'k> + = Kw<'k, P> + where + P: 'k; + + fn wrap_init<'k>(state: &'k Self::KeyState) -> Self::Wrapper<'k> { + Kw::from_perm(state) + } +} + +impl KeyUnwrapMode for KW +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Unwrapper<'k> + = Kw<'k, P> + where + P: 'k; + + fn unwrap_init<'k>(state: &'k Self::KeyState) -> Self::Unwrapper<'k> { + Kw::from_perm(state) + } +} + +/// KW over any 128-bit permutation that impls [`BlockPermutation`]: the one-shot worker a +/// [`KW`]-bound key hands out, borrowing the keyed permutation for `'k`. /// -/// See the module docs for usage and the security notes. -pub struct Kw +/// There is no state beyond the borrow and no direction marker: both halves of the algorithm are +/// the [`KeyWrapper`] and [`KeyUnwrapper`] traits, which this type implements together. See the +/// module docs for usage and the security notes. +pub struct Kw<'k, P> where - P: ElectronicCodeBook, + P: BlockPermutation + ModeNames, { - _perm: PhantomData

, + perm: &'k P, +} + +impl<'k, P> Kw<'k, P> +where + P: BlockPermutation + ModeNames, +{ + /// Over an already-keyed permutation, which the worker borrows: what a [`KW`]-bound key's + /// roles build. + pub fn from_perm(perm: &'k P) -> Self { + Self { perm } + } } -impl Algorithm for Kw +impl<'k, P> Algorithm for Kw<'k, P> where - P: ElectronicCodeBook, + P: BlockPermutation + ModeNames, { - /// The underlying permutation's name; the mode is in the type, as for [`Ecb`](crate::Ecb). - const ALG_NAME: &'static str = P::ALG_NAME; + /// The permutation's name for this mode, [`ModeNames::KW_ALG_NAME`]. + const ALG_NAME: &'static str = P::KW_ALG_NAME; /// KW does not change the strength of the underlying cipher. const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl KeyWrapper for Kw +impl<'k, P> KeyWrapper for Kw<'k, P> where - P: ElectronicCodeBook, + P: BlockPermutation + ModeNames, { /// KW-AE with the lengths fixed at compile time. `KEY_LEN` must be a valid KW plaintext /// length and `CT_LEN` one semiblock more; anything else fails to compile. fn wrap_key_out( - kek: &KeyMaterial, + &self, key: &[u8; KEY_LEN], ciphertext: &mut [u8; CT_LEN], ) -> Result { @@ -271,17 +338,17 @@ where "a KW ciphertext is exactly one 8-byte semiblock longer than its plaintext" ); } - Self::wrap_out(kek, key, ciphertext) + self.wrap_out(key, ciphertext) } /// One semiblock more than the plaintext. - fn wrap_out_len(plaintext_len: usize) -> usize { + fn wrap_out_len(&self, plaintext_len: usize) -> usize { kw_wrapped_len(plaintext_len) } /// KW-AE (SP 800-38F Algorithm 3). fn wrap_out( - kek: &KeyMaterial, + &self, plaintext: &[u8], ciphertext: &mut [u8], ) -> Result { @@ -294,7 +361,6 @@ where "KW wraps 2 to 2^54 - 1 whole 8-byte semiblocks (SP 800-38F Table 1); use KWP for other lengths", )); } - let perm = P::new(kek)?; // Steps 1-2: S = ICV1 || P, assembled in the caller's buffer so that the plaintext is // copied exactly once and W can run on it in place. @@ -305,19 +371,19 @@ where r.copy_from_slice(plaintext); let (r, _) = r.as_chunks_mut::(); // Step 3: C = W(S). - w(&perm, a, r); + w(self.perm, a, r); Ok(ct_len) } } -impl KeyUnwrapper for Kw +impl<'k, P> KeyUnwrapper for Kw<'k, P> where - P: ElectronicCodeBook, + P: BlockPermutation + ModeNames, { /// KW-AD with the lengths fixed at compile time; the same pairs as /// [`wrap_key_out`](KeyWrapper::wrap_key_out) are accepted. fn unwrap_key_out( - kek: &KeyMaterial, + &self, ciphertext: &[u8; CT_LEN], key: &mut Secret<[u8; KEY_LEN]>, ) -> Result { @@ -333,21 +399,21 @@ where } // `key` is exactly the n-1 semiblocks the run-time method writes into, and that method // scrubs the buffer on every failure it can reach from here. - Self::unwrap_out(kek, ciphertext, &mut key[..]) + self.unwrap_out(ciphertext, &mut key[..]) } /// One semiblock less than the ciphertext: exact for KW. - fn unwrap_out_max_len(ciphertext_len: usize) -> usize { + fn unwrap_out_max_len(&self, ciphertext_len: usize) -> usize { ciphertext_len.saturating_sub(SEMIBLOCK_LEN) } /// KW-AD (SP 800-38F Algorithm 4). fn unwrap_out( - kek: &KeyMaterial, + &self, ciphertext: &[u8], plaintext: &mut [u8], ) -> Result { - let pt_len = Self::unwrap_out_max_len(ciphertext.len()); + let pt_len = self.unwrap_out_max_len(ciphertext.len()); if plaintext.len() < pt_len { return Err(SymmetricCipherError::OutputBufferTooSmall(pt_len)); } @@ -359,7 +425,6 @@ where "a KW ciphertext is 3 to 2^54 whole 8-byte semiblocks (SP 800-38F Table 1)", )); } - let perm = P::new(kek)?; // Step 2: S = W⁻¹(C). C1 goes to a scratch semiblock that is scrubbed on return; C2..Cn // go straight into the caller's buffer, which is exactly n-1 semiblocks, so W⁻¹ runs in @@ -369,7 +434,7 @@ where let r = &mut plaintext[..pt_len]; r.copy_from_slice(&ciphertext[SEMIBLOCK_LEN..]); let (r, _) = r.as_chunks_mut::(); - w_inv(&perm, &mut a, r); + w_inv(self.perm, &mut a, r); // Step 3: if MSB64(S) != ICV1, FAIL. Compared in constant time, and the buffer is scrubbed // so that a caller who ignores the `Result` is not left holding the unauthenticated bytes. diff --git a/crypto/cipher/src/modes/kwp.rs b/crypto/cipher/src/modes/kwp.rs index f914a4bf8..892184e27 100644 --- a/crypto/cipher/src/modes/kwp.rs +++ b/crypto/cipher/src/modes/kwp.rs @@ -1,39 +1,42 @@ //! The Key Wrap with Padding algorithm, KWP (NIST SP 800-38F Sec 6.3; RFC 5649), over any //! 128-bit block permutation. //! -//! KWP is [`Kw`](crate::kw::Kw) for data of any length from 1 byte up to 2^32 - 1 bytes: the -//! plaintext is prefixed with a 4-byte integrity check value and its 4-byte length, padded with -//! zeros to a whole number of 8-byte semiblocks, and then put through the same wrapping function. -//! The ciphertext is the plaintext rounded up to a multiple of 8, plus 8. Everything in the -//! [`kw`](crate::kw) module docs -- the trait shape, the in-place wrapping function, the security -//! notes -- applies here too; this module only documents what differs. +//! KWP is [`Kw`](crate::modes::kw::Kw) for data of any length from 1 byte up to 2^32 - 1 bytes: +//! the plaintext is prefixed with a 4-byte integrity check value and its 4-byte length, padded +//! with zeros to a whole number of 8-byte semiblocks, and then put through the same wrapping +//! function. The ciphertext is the plaintext rounded up to a multiple of 8, plus 8. Everything in +//! the [`kw`](crate::modes::kw) module docs -- the key path, the trait shape, the in-place +//! wrapping function, the security notes -- applies here too; this module only documents what +//! differs. A key is bound to it through [`KWP`]: `AES128Key`. //! //! The `AES_KWP_128` / `AES_KWP_192` / `AES_KWP_256` aliases are in `bouncycastle-aes`. //! //! # Usage Examples //! //! ``` -//! use bouncycastle_aes::aes_internal::AES128Internal; +//! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # use toy::ToyBlockCipher; +//! use bouncycastle_cipher::modes::Kwp; +//! use bouncycastle_core::hazmat::ElectronicCodeBook; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; //! use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; -//! use bouncycastle_modes::Kwp; -//! -//! type Aes128Kwp = Kwp; //! //! let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); +//! let state = ToyBlockCipher::new(&kek).unwrap(); +//! let kwp = Kwp::from_perm(&state); //! //! // 20 bytes pad to 24, plus the 8-byte header: 32 bytes of ciphertext. //! let data = [0x5Au8; 20]; -//! let wrapped: [u8; 32] = Aes128Kwp::wrap_key(&kek, &data).expect("wrapping"); -//! let recovered = Aes128Kwp::unwrap_key::<20, 32>(&kek, &wrapped).expect("unwrapping"); +//! let wrapped: [u8; 32] = kwp.wrap_key(&data).expect("wrapping"); +//! let recovered = kwp.unwrap_key::<20, 32>(&wrapped).expect("unwrapping"); //! assert_eq!(*recovered, data); //! //! // Up to 8 bytes fit in a single block with the header, which is enciphered directly //! // (Algorithm 5, step 5) rather than through the wrapping function. //! let short = *b"short"; -//! let wrapped: [u8; 16] = Aes128Kwp::wrap_key(&kek, &short).expect("wrapping"); -//! assert_eq!(*Aes128Kwp::unwrap_key::<5, 16>(&kek, &wrapped).expect("unwrapping"), short); +//! let wrapped: [u8; 16] = kwp.wrap_key(&short).expect("wrapping"); +//! assert_eq!(*kwp.unwrap_key::<5, 16>(&wrapped).expect("unwrapping"), short); //! ``` //! //! # The recovered length is inside the ciphertext @@ -54,14 +57,17 @@ //! depend on the length field, but that field is public once unwrapping succeeds and never more //! than 7 bytes; there is no padding oracle to protect because the failure is the same either way.) -use crate::kw::{KW_BLOCK_LEN, SEMIBLOCK_LEN, w, w_inv}; +use super::ModeNames; +use super::kw::{KW_BLOCK_LEN, SEMIBLOCK_LEN, w, w_inv}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::hazmat::{ + BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, +}; +use bouncycastle_core::key_material::KeyType; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, KeyUnwrapper, KeyWrapper}; +use bouncycastle_core::traits::{Algorithm, KeyUnwrapper, KeyWrapper}; use bouncycastle_utils::ct::{ct_eq_bytes, ct_eq_zero_bytes}; use bouncycastle_utils::secret::Secret; -use core::marker::PhantomData; /// ICV2, the integrity check value of KWP (Algorithm 5, step 1): `0xA65959A6`. const ICV2: [u8; 4] = [0xA6, 0x59, 0x59, 0xA6]; @@ -94,35 +100,90 @@ pub const fn kwp_wrapped_len(len: usize) -> usize { len.div_ceil(SEMIBLOCK_LEN).saturating_mul(SEMIBLOCK_LEN).saturating_add(SEMIBLOCK_LEN) } -/// KWP over any 128-bit permutation that impls [`ElectronicCodeBook`]. -/// -/// As [`Kw`](crate::kw::Kw): no state, no direction marker, `KEK_LEN` is the permutation's key -/// length. See the module docs. -pub struct Kwp +/// The KWP mode type, as a key type names it: `AES128Key`. As [`KW`](super::kw::KW): its +/// key state is the keyed permutation, and its workers are [`Kwp`] on both sides. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KWP; + +impl

Mode

for KWP where - P: ElectronicCodeBook, + P: BlockPermutation + ModeNames + Clone, { - _perm: PhantomData

, + type KeyState = P; + const KEY_TYPE: KeyType = KeyType::SymmetricCipherKey; + + fn key_state(perm: P) -> Self::KeyState { + perm + } +} + +impl KeyWrapMode for KWP +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Wrapper<'k> + = Kwp<'k, P> + where + P: 'k; + + fn wrap_init<'k>(state: &'k Self::KeyState) -> Self::Wrapper<'k> { + Kwp::from_perm(state) + } +} + +impl KeyUnwrapMode for KWP +where + P: ElectronicCodeBook + ModeNames + Clone, +{ + type Unwrapper<'k> + = Kwp<'k, P> + where + P: 'k; + + fn unwrap_init<'k>(state: &'k Self::KeyState) -> Self::Unwrapper<'k> { + Kwp::from_perm(state) + } +} + +/// KWP over any 128-bit permutation that impls [`BlockPermutation`]: the one-shot worker a +/// [`KWP`]-bound key hands out, borrowing the keyed permutation for `'k`, as +/// [`Kw`](super::kw::Kw) does. See the module docs. +pub struct Kwp<'k, P> +where + P: BlockPermutation + ModeNames, +{ + perm: &'k P, +} + +impl<'k, P> Kwp<'k, P> +where + P: BlockPermutation + ModeNames, +{ + /// Over an already-keyed permutation, which the worker borrows: what a [`KWP`]-bound key's + /// roles build. + pub fn from_perm(perm: &'k P) -> Self { + Self { perm } + } } -impl Algorithm for Kwp +impl<'k, P> Algorithm for Kwp<'k, P> where - P: ElectronicCodeBook, + P: BlockPermutation + ModeNames, { - /// The underlying permutation's name; the mode is in the type, as for [`Ecb`](crate::Ecb). - const ALG_NAME: &'static str = P::ALG_NAME; + /// The permutation's name for this mode, [`ModeNames::KWP_ALG_NAME`]. + const ALG_NAME: &'static str = P::KWP_ALG_NAME; /// KWP does not change the strength of the underlying cipher. const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -impl KeyWrapper for Kwp +impl<'k, P> KeyWrapper for Kwp<'k, P> where - P: ElectronicCodeBook, + P: BlockPermutation + ModeNames, { /// KWP-AE with the lengths fixed at compile time. `KEY_LEN` may be anything from 1 to /// 2^32 - 1 and `CT_LEN` must be [`kwp_wrapped_len`] of it; anything else fails to compile. fn wrap_key_out( - kek: &KeyMaterial, + &self, key: &[u8; KEY_LEN], ciphertext: &mut [u8; CT_LEN], ) -> Result { @@ -136,17 +197,17 @@ where "a KWP ciphertext is the plaintext rounded up to a multiple of 8 bytes, plus 8" ); } - Self::wrap_out(kek, key, ciphertext) + self.wrap_out(key, ciphertext) } /// The plaintext rounded up to a whole number of semiblocks, plus one. - fn wrap_out_len(plaintext_len: usize) -> usize { + fn wrap_out_len(&self, plaintext_len: usize) -> usize { kwp_wrapped_len(plaintext_len) } /// KWP-AE (SP 800-38F Algorithm 5). fn wrap_out( - kek: &KeyMaterial, + &self, plaintext: &[u8], ciphertext: &mut [u8], ) -> Result { @@ -159,7 +220,6 @@ where "KWP wraps 1 to 2^32 - 1 bytes (SP 800-38F Table 1)", )); } - let perm = P::new(kek)?; // Steps 1-4: S = ICV2 || [len(P)/8]32 || P || PAD, assembled in the caller's buffer so the // plaintext is copied exactly once. The length is in octets and fits 32 bits: checked above. @@ -175,20 +235,20 @@ where // directly; a longer one goes through W. if plaintext.len() <= SEMIBLOCK_LEN { let (blocks, _) = s.as_chunks_mut::(); - perm.encrypt_block(&mut blocks[0]); + self.perm.encrypt_block(&mut blocks[0]); } else { let (a, r) = s.split_at_mut(SEMIBLOCK_LEN); let (a, _) = a.as_chunks_mut::(); let (r, _) = r.as_chunks_mut::(); - w(&perm, &mut a[0], r); + w(self.perm, &mut a[0], r); } Ok(ct_len) } } -impl KeyUnwrapper for Kwp +impl<'k, P> KeyUnwrapper for Kwp<'k, P> where - P: ElectronicCodeBook, + P: BlockPermutation + ModeNames, { /// KWP-AD with the lengths fixed at compile time; the same pairs as /// [`wrap_key_out`](KeyWrapper::wrap_key_out) are accepted. @@ -197,7 +257,7 @@ where /// allows, since up to eight plaintext lengths share one ciphertext length -- is reported as /// [`SymmetricCipherError::DecryptionFailed`], exactly as a forgery would be. fn unwrap_key_out( - kek: &KeyMaterial, + &self, ciphertext: &[u8; CT_LEN], key: &mut Secret<[u8; KEY_LEN]>, ) -> Result { @@ -214,7 +274,7 @@ where // The padded plaintext can be up to 7 bytes longer than `key`, so unwrap into scratch // that is scrubbed on drop, and only copy out once the recovered length is the expected one. let mut padded: Secret<[u8; CT_LEN]> = Secret::new(); - match Self::unwrap_out(kek, ciphertext, &mut padded[..CT_LEN - SEMIBLOCK_LEN]) { + match self.unwrap_out(ciphertext, &mut padded[..CT_LEN - SEMIBLOCK_LEN]) { Ok(recovered_len) if recovered_len == KEY_LEN => { key.copy_from_slice(&padded[..KEY_LEN]); Ok(KEY_LEN) @@ -232,18 +292,18 @@ where /// One semiblock less than the ciphertext: the padded plaintext, of which up to 7 bytes are /// padding, so this is an upper bound (module docs). - fn unwrap_out_max_len(ciphertext_len: usize) -> usize { + fn unwrap_out_max_len(&self, ciphertext_len: usize) -> usize { ciphertext_len.saturating_sub(SEMIBLOCK_LEN) } /// KWP-AD (SP 800-38F Algorithm 6). fn unwrap_out( - kek: &KeyMaterial, + &self, ciphertext: &[u8], plaintext: &mut [u8], ) -> Result { // 8(n-1) bytes: the padded plaintext. - let padded_len = Self::unwrap_out_max_len(ciphertext.len()); + let padded_len = self.unwrap_out_max_len(ciphertext.len()); if plaintext.len() < padded_len { return Err(SymmetricCipherError::OutputBufferTooSmall(padded_len)); } @@ -254,7 +314,6 @@ where "a KWP ciphertext is 2 to 2^29 whole 8-byte semiblocks (SP 800-38F Table 1)", )); } - let perm = P::new(kek)?; // Step 1: n, the number of semiblocks in C. let n = ciphertext.len() / SEMIBLOCK_LEN; @@ -267,7 +326,7 @@ where if n == 2 { let mut block: Secret<[u8; KW_BLOCK_LEN]> = Secret::new(); block.copy_from_slice(ciphertext); - perm.decrypt_block(&mut block); + self.perm.decrypt_block(&mut block); let (halves, _) = block.as_chunks::(); *a = halves[0]; r.copy_from_slice(&halves[1]); @@ -275,7 +334,7 @@ where a.copy_from_slice(&ciphertext[..SEMIBLOCK_LEN]); r.copy_from_slice(&ciphertext[SEMIBLOCK_LEN..]); let (r, _) = r.as_chunks_mut::(); - w_inv(&perm, &mut a, r); + w_inv(self.perm, &mut a, r); } // Steps 4-8: every check is made, and the verdicts combined with `&` rather than `&&`, so diff --git a/crypto/cipher/src/modes/mod.rs b/crypto/cipher/src/modes/mod.rs index b6f4335cb..167d1b5cd 100644 --- a/crypto/cipher/src/modes/mod.rs +++ b/crypto/cipher/src/modes/mod.rs @@ -20,8 +20,10 @@ //! | ECB | [`hazmat`] | SP 800-38A Sec 6.1 | Electronic Codebook. **Not confidential for data**; interoperability and test vectors only | //! | GCM | [`gcm`] | SP 800-38D | **Authenticated**: 96-bit nonce, 96-128-bit tag, no padding; AAD before data | //! | GMAC | [`gmac`] | SP 800-38D Sec 5.2 | A MAC, not a cipher: GCM with no plaintext, so the tag is bound to a 96-bit nonce | +//! | KW | [`kw`] | SP 800-38F Sec 6.2 | Key Wrap (RFC 3394). **Authenticated**, deterministic, no IV; whole 8-byte semiblocks only | +//! | KWP | [`kwp`] | SP 800-38F Sec 6.3 | Key Wrap with Padding (RFC 5649). As KW, for data of any length | //! -//! They divide three ways. +//! They divide three ways, plus the key-wrap pair. //! //! **ECB and CBC are block ciphers** ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]): whole //! blocks in, whole blocks out, and arbitrary-length data needs the padding layer. @@ -37,6 +39,10 @@ //! [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] with no option to provide AAD, and //! the tag inline. //! +//! **KW and KWP are key-wrap algorithms** ([`KeyWrapper`] / [`KeyUnwrapper`]): one-shot, +//! deterministic and authenticated, with no IV and no streaming API, for wrapping a key (or a +//! small, non-repeating secret) under a key encryption key. They are not a mode for data. +//! //! CBC, CFB, CFB8 and CTR all generate their own init data: an IV for the first three, a nonce for //! CTR, which is shorter than a block because the rest of the counter block is the counter. ECB has //! none at all (`INIT_DATA_LEN = 0`) and is the raw permutation applied block by block, which is @@ -44,7 +50,8 @@ //! [Choosing between the modes](#choosing-between-the-modes). //! //! Each mode is also a **mode type** -- [`CBC`], [`CCM`], [`CCM_Packet`], [`CFB`], [`CFB8`], -//! [`CTR`], [`GCM`], [`hazmat::ecb::ECB`], and the MACs [`CMAC`] and [`GMAC`] -- the `M` a mode-bound +//! [`CTR`], [`GCM`], [`hazmat::ecb::ECB`], the MACs [`CMAC`] and [`GMAC`], and the key wraps +//! [`KW`] and [`KWP`] -- the `M` a mode-bound //! key such as `bouncycastle_aes::AES128Key` is parameterised by. The mode type implements the //! hazmat mode traits of [`bouncycastle_core::hazmat`], which is how a key hands out the mode's //! encryptors and decryptors through the role traits of `bouncycastle_core::symmetric_key`; a @@ -167,6 +174,8 @@ pub mod gcm; pub mod gmac; pub mod hazmat; mod iv; +pub mod kw; +pub mod kwp; pub use cbc::{CBC, Cbc}; pub use ccm::{ @@ -178,6 +187,8 @@ pub use cmac::{CMAC, CMAC_MIN_TAG_LEN, Cmac, CmacKeyState}; pub use ctr::{CTR, Ctr}; pub use gcm::{GCM, GCM_NONCE_LEN, Gcm, GcmKeyState}; pub use gmac::{GMAC, Gmac}; +pub use kw::{KW, KW_BLOCK_LEN, Kw, SEMIBLOCK_LEN}; +pub use kwp::{KWP, Kwp}; /// Selects the whole-block form of a mode that also has a padded form: `CBC` is a key /// for the block-aligned [`Cbc`] itself, on the block key path, where `CBC` is one for @@ -209,6 +220,10 @@ pub trait ModeNames { const GCM_ALG_NAME: &'static str; /// Reported by [`Gmac`]. const GMAC_ALG_NAME: &'static str; + /// Reported by [`Kw`]. + const KW_ALG_NAME: &'static str; + /// Reported by [`Kwp`]. + const KWP_ALG_NAME: &'static str; } // Imports needed for docs @@ -217,7 +232,7 @@ use bouncycastle_core::hazmat::ElectronicCodeBook; #[allow(unused_imports)] use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, BlockCipherDecryptor, - BlockCipherEncryptor, MAC, StreamCipherDecryptor, StreamCipherEncryptor, - SymmetricCipherDecryptor, SymmetricCipherEncryptor, + BlockCipherEncryptor, KeyUnwrapper, KeyWrapper, MAC, StreamCipherDecryptor, + StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, }; // end of imports needed for docs diff --git a/crypto/cipher/tests/common/toy_block_cipher.rs b/crypto/cipher/tests/common/toy_block_cipher.rs index 857f3a65b..801fef682 100644 --- a/crypto/cipher/tests/common/toy_block_cipher.rs +++ b/crypto/cipher/tests/common/toy_block_cipher.rs @@ -47,6 +47,8 @@ impl ModeNames for ToyBlockCipher { const ECB_ALG_NAME: &'static str = "ToyBlockCipher_ECB"; const GCM_ALG_NAME: &'static str = "ToyBlockCipher_GCM"; const GMAC_ALG_NAME: &'static str = "ToyBlockCipher_GMAC"; + const KW_ALG_NAME: &'static str = "ToyBlockCipher_KW"; + const KWP_ALG_NAME: &'static str = "ToyBlockCipher_KWP"; } impl ElectronicCodeBook for ToyBlockCipher { diff --git a/crypto/cipher/tests/common/toy_key.rs b/crypto/cipher/tests/common/toy_key.rs index 197d7932a..2625947dc 100644 --- a/crypto/cipher/tests/common/toy_key.rs +++ b/crypto/cipher/tests/common/toy_key.rs @@ -11,14 +11,14 @@ use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, BlockDecryptMode, BlockEncryptMode, BlockPermutation, EcbDecryptMode, EcbDecryptionKey, - EcbEncryptMode, EcbEncryptionKey, ElectronicCodeBook, MacMode, Mode, NonceMacMode, - SymmetricDecryptMode, SymmetricEncryptMode, do_hazardous_operations, + EcbEncryptMode, EcbEncryptionKey, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, MacMode, + Mode, NonceMacMode, SymmetricDecryptMode, SymmetricEncryptMode, do_hazardous_operations, }; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::symmetric_key::{ AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, AeadDecryptionKey, AeadEncryptionKey, - BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, MacGenerationKey, MacVerificationKey, - SymmetricDecryptionKey, SymmetricEncryptionKey, + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey, + MacGenerationKey, MacVerificationKey, SymmetricDecryptionKey, SymmetricEncryptionKey, }; use bouncycastle_core::traits::RNG; @@ -372,6 +372,34 @@ impl> MacVerificationKey for ToyKey { } } +impl KeyWrappingKey for ToyKey +where + M: KeyWrapMode, +{ + type Wrapper<'k> + = M::Wrapper<'k> + where + Self: 'k; + + fn create_wrapper<'k>(&'k self) -> Self::Wrapper<'k> { + M::wrap_init(&self.state) + } +} + +impl KeyUnwrappingKey for ToyKey +where + M: KeyUnwrapMode, +{ + type Unwrapper<'k> + = M::Unwrapper<'k> + where + Self: 'k; + + fn create_unwrapper<'k>(&'k self) -> Self::Unwrapper<'k> { + M::unwrap_init(&self.state) + } +} + // ---- GMAC's pair, inherent on the key as it is on the AES key ---------------------------------- impl ToyKey> diff --git a/crypto/cipher/tests/modes/alg_name_tests.rs b/crypto/cipher/tests/modes/alg_name_tests.rs index 03f5b32a6..ec8319835 100644 --- a/crypto/cipher/tests/modes/alg_name_tests.rs +++ b/crypto/cipher/tests/modes/alg_name_tests.rs @@ -9,7 +9,7 @@ mod common; use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; use bouncycastle_cipher::modes::{ Blocks, CBC, CCM, CCM_Packet, CFB, CFB8, CTR, Cbc, CcmPacketDecryptor, CcmPacketEncryptor, Cfb, - Cfb8, Cmac, Ctr, Gcm, Gmac, ModeNames, + Cfb8, Cmac, Ctr, Gcm, Gmac, Kw, Kwp, ModeNames, }; use bouncycastle_cipher::padding::{ NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, @@ -38,6 +38,8 @@ fn the_toy_names_every_mode_differently() { Toy::ECB_ALG_NAME, Toy::GCM_ALG_NAME, Toy::GMAC_ALG_NAME, + Toy::KW_ALG_NAME, + Toy::KWP_ALG_NAME, ]; for (i, a) in names.iter().enumerate() { for b in &names[i + 1..] { @@ -63,6 +65,8 @@ fn each_mode_reports_its_own_name() { check::>(Toy::CMAC_ALG_NAME); check::>(Toy::GMAC_ALG_NAME); check::>(Toy::GMAC_ALG_NAME); + check::>(Toy::KW_ALG_NAME); + check::>(Toy::KWP_ALG_NAME); } /// The padding layer and the stream-cipher wrapper add no name of their own. diff --git a/crypto/cipher/tests/modes/common/mod.rs b/crypto/cipher/tests/modes/common/mod.rs index fae2f7476..2c94fb5b8 100644 --- a/crypto/cipher/tests/modes/common/mod.rs +++ b/crypto/cipher/tests/modes/common/mod.rs @@ -70,6 +70,8 @@ impl ModeNames for Toy { const ECB_ALG_NAME: &'static str = "Toy_ECB"; const GCM_ALG_NAME: &'static str = "Toy_GCM"; const GMAC_ALG_NAME: &'static str = "Toy_GMAC"; + const KW_ALG_NAME: &'static str = "Toy_KW"; + const KWP_ALG_NAME: &'static str = "Toy_KWP"; } impl ElectronicCodeBook for Toy { @@ -148,6 +150,8 @@ impl ModeNames for SwappedPairToy { const ECB_ALG_NAME: &'static str = "SwappedPairToy_ECB"; const GCM_ALG_NAME: &'static str = "SwappedPairToy_GCM"; const GMAC_ALG_NAME: &'static str = "SwappedPairToy_GMAC"; + const KW_ALG_NAME: &'static str = "SwappedPairToy_KW"; + const KWP_ALG_NAME: &'static str = "SwappedPairToy_KWP"; } impl ElectronicCodeBook for SwappedPairToy { @@ -226,6 +230,8 @@ impl ModeNames for ForwardOnlyToy { const ECB_ALG_NAME: &'static str = "ForwardOnlyToy_ECB"; const GCM_ALG_NAME: &'static str = "ForwardOnlyToy_GCM"; const GMAC_ALG_NAME: &'static str = "ForwardOnlyToy_GMAC"; + const KW_ALG_NAME: &'static str = "ForwardOnlyToy_KW"; + const KWP_ALG_NAME: &'static str = "ForwardOnlyToy_KWP"; } impl ElectronicCodeBook for ForwardOnlyToy { @@ -286,6 +292,8 @@ impl ModeNames for SwappedFourToy { const ECB_ALG_NAME: &'static str = "SwappedFourToy_ECB"; const GCM_ALG_NAME: &'static str = "SwappedFourToy_GCM"; const GMAC_ALG_NAME: &'static str = "SwappedFourToy_GMAC"; + const KW_ALG_NAME: &'static str = "SwappedFourToy_KW"; + const KWP_ALG_NAME: &'static str = "SwappedFourToy_KWP"; } impl ElectronicCodeBook for SwappedFourToy { diff --git a/crypto/cipher/tests/modes/kw_tests.rs b/crypto/cipher/tests/modes/kw_tests.rs new file mode 100644 index 000000000..ae3ae2dba --- /dev/null +++ b/crypto/cipher/tests/modes/kw_tests.rs @@ -0,0 +1,258 @@ +//! Structural tests for KW and KWP, driven by a toy permutation: the key path and the bare +//! worker agree, the length rules of SP 800-38F Table 1, and the properties that tell the two +//! algorithms apart from each other and from a plain block encryption. The shared key-wrap +//! framework and the known-answer tests (RFC 3394, RFC 5649, ACVP) are in the `aes` crate, +//! `crypto/aes/tests/kw_*.rs`: the framework's tamper checks need a permutation with diffusion, +//! which the per-byte toy deliberately lacks, so a flipped register byte never reaches the ICV. + +mod common; +#[path = "../common/toy_key.rs"] +mod toy_key; + +use bouncycastle_cipher::modes::kw::{ + kw_ciphertext_len_is_valid, kw_plaintext_len_is_valid, kw_wrapped_len, +}; +use bouncycastle_cipher::modes::kwp::{ + kwp_ciphertext_len_is_valid, kwp_plaintext_len_is_valid, kwp_wrapped_len, +}; +use bouncycastle_cipher::modes::{KW, KWP, Kw, Kwp, ModeNames}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; +use common::{TOY_LEN, Toy, toy_key}; +use toy_key::ToyKey; + +type ToyKwKey = ToyKey; +type ToyKwpKey = ToyKey; + +fn kw_key() -> ToyKwKey { + ToyKwKey::from_key_material(&toy_key()).expect("the toy KEK") +} + +fn kwp_key() -> ToyKwpKey { + ToyKwpKey::from_key_material(&toy_key()).expect("the toy KEK") +} + +/// The construction suite -- key type, strength and `generate` -- for both modes. +#[test] +fn the_keys_enforce_the_material_policy() { + let framework = TestFrameworkFromKeyMaterial::new(); + framework.test::(Toy::KW_ALG_NAME, SecurityStrength::_128bit); + framework.test::(Toy::KWP_ALG_NAME, SecurityStrength::_128bit); +} + +/// The key's one-shots and a worker built straight over the permutation agree. +#[test] +fn the_key_and_the_bare_worker_agree() { + let perm = Toy::new(&toy_key()).unwrap(); + let data = [0x5Au8; 32]; + + let key = kw_key(); + let from_key: [u8; 40] = key.wrap_key(&data).unwrap(); + let from_worker: [u8; 40] = Kw::from_perm(&perm).wrap_key(&data).unwrap(); + assert_eq!(from_key, from_worker); + assert_eq!(*Kw::from_perm(&perm).unwrap_key::<32, 40>(&from_key).unwrap(), data); + assert_eq!(*key.unwrap_key::<32, 40>(&from_worker).unwrap(), data); + assert_eq!(*Kw::from_perm(key.state()).unwrap_key::<32, 40>(&from_key).unwrap(), data); + + let key = kwp_key(); + let short: [u8; 20] = data[..20].try_into().unwrap(); + let from_key: [u8; 32] = key.wrap_key(&short).unwrap(); + let from_worker: [u8; 32] = Kwp::from_perm(&perm).wrap_key(&short).unwrap(); + assert_eq!(from_key, from_worker); + assert_eq!(*key.unwrap_key::<20, 32>(&from_worker).unwrap(), data[..20]); +} + +// ---- properties specific to KW and KWP ------------------------------------------------------ + +/// The two integrity check values differ, so a ciphertext produced by one algorithm is rejected +/// by the other even though the lengths line up. +#[test] +fn kw_and_kwp_ciphertexts_are_not_interchangeable() { + let kw = kw_key(); + let kwp = kwp_key(); + let data = [0x5Au8; 16]; + + let kw_ct: [u8; 24] = kw.wrap_key(&data).unwrap(); + let kwp_ct: [u8; 24] = kwp.wrap_key(&data).unwrap(); + assert_ne!(kw_ct, kwp_ct); + + assert!(matches!( + kwp.unwrap_key::<16, 24>(&kw_ct), + Err(SymmetricCipherError::DecryptionFailed) + )); + assert!(matches!( + kw.unwrap_key::<16, 24>(&kwp_ct), + Err(SymmetricCipherError::DecryptionFailed) + )); +} + +/// SP 800-38F Algorithm 5 step 5: for `len(P) <= 64` bits the ciphertext is `CIPH_K(S)` with +/// `S = ICV2 || [len]32 || P || PAD`, one raw block encryption and nothing else. Pinning that +/// against the permutation directly ties the single-block path to the spec. +#[test] +fn kwp_single_block_path_is_one_raw_block_encryption() { + let key = kwp_key(); + let data = *b"12345"; + + let ct: [u8; 16] = key.wrap_key(&data).unwrap(); + + let mut s = [0u8; 16]; + s[..4].copy_from_slice(&[0xA6, 0x59, 0x59, 0xA6]); + s[4..8].copy_from_slice(&(data.len() as u32).to_be_bytes()); + s[8..8 + data.len()].copy_from_slice(&data); + Toy::new(&toy_key()).unwrap().encrypt_block(&mut s); + assert_eq!(ct, s); +} + +/// A KWP ciphertext length fixes the plaintext length only to within 7 bytes; the exact length is +/// the header field. So a 12-byte wrap unwraps fine as 12, and as any other length that shares +/// its ciphertext length is an authenticity failure, as the trait requires. +#[test] +fn kwp_unwrap_key_rejects_a_recovered_length_other_than_key_len() { + let key = kwp_key(); + let ct: [u8; 24] = key.wrap_key(&[0x33u8; 12]).unwrap(); + + assert_eq!(*key.unwrap_key::<12, 24>(&ct).unwrap(), [0x33u8; 12]); + assert!(matches!(key.unwrap_key::<13, 24>(&ct), Err(SymmetricCipherError::DecryptionFailed))); + assert!(matches!(key.unwrap_key::<9, 24>(&ct), Err(SymmetricCipherError::DecryptionFailed))); + // ... and the run-time API reports the real length. + let mut pt = [0u8; 16]; + assert_eq!(key.unwrap_out(&ct, &mut pt).unwrap(), 12); + assert_eq!(pt[..12], [0x33u8; 12]); + assert_eq!(pt[12..], [0u8; 4], "the padding is left as zeros"); +} + +/// Algorithm 6 step 8: padding must be zero. A ciphertext whose padding bytes were anything else +/// is a forgery. Constructed by wrapping through the raw block cipher with non-zero padding, since +/// the wrapper itself never produces one. +#[test] +fn kwp_rejects_non_zero_padding() { + let key = kwp_key(); + let perm = Toy::new(&toy_key()).unwrap(); + + let mut s = [0u8; 16]; + s[..4].copy_from_slice(&[0xA6, 0x59, 0x59, 0xA6]); + s[4..8].copy_from_slice(&5u32.to_be_bytes()); + s[8..13].copy_from_slice(b"12345"); + s[15] = 0x01; // the last pad byte + perm.encrypt_block(&mut s); + + assert!(matches!(key.unwrap_key::<5, 16>(&s), Err(SymmetricCipherError::DecryptionFailed))); +} + +/// Algorithm 6 step 7: `padlen` must be 0 to 7, so a length field that does not fit the +/// ciphertext is a forgery even when the ICV and padding check out. Two cases: a length larger +/// than the padded data, and one that would leave 8 or more bytes of padding. +#[test] +fn kwp_rejects_a_length_field_that_does_not_fit() { + let key = kwp_key(); + let perm = Toy::new(&toy_key()).unwrap(); + + for bad_len in [9u32, 0u32, u32::MAX] { + let mut s = [0u8; 16]; + s[..4].copy_from_slice(&[0xA6, 0x59, 0x59, 0xA6]); + s[4..8].copy_from_slice(&bad_len.to_be_bytes()); + perm.encrypt_block(&mut s); + + let mut pt = [0xFFu8; 8]; + assert!( + matches!(key.unwrap_out(&s, &mut pt), Err(SymmetricCipherError::DecryptionFailed)), + "a length field of {bad_len} should not fit an 8-byte padded plaintext" + ); + assert_eq!(pt, [0u8; 8], "the buffer is scrubbed on failure"); + } +} + +/// The length predicates and helpers are SP 800-38F Table 1 exactly. +#[test] +fn the_length_helpers_match_table_1() { + let kw = kw_key(); + let kwp = kwp_key(); + + // KW: 2 to 2^54 - 1 semiblocks of plaintext, one more of ciphertext. + assert!(!kw_plaintext_len_is_valid(0)); + assert!(!kw_plaintext_len_is_valid(8)); + assert!(!kw_plaintext_len_is_valid(12)); + assert!(kw_plaintext_len_is_valid(16)); + assert!(kw_plaintext_len_is_valid(24)); + assert!(!kw_ciphertext_len_is_valid(16)); + assert!(kw_ciphertext_len_is_valid(24)); + assert!(!kw_ciphertext_len_is_valid(25)); + assert_eq!(kw_wrapped_len(16), 24); + assert_eq!(kw_wrapped_len(usize::MAX), usize::MAX, "saturates rather than overflowing"); + // The 2^54 upper bounds, reachable as lengths only on a 64-bit target: the last valid + // plaintext is 2^54 - 1 semiblocks, the last valid ciphertext 2^54. + #[cfg(target_pointer_width = "64")] + { + const MAX_PT: usize = ((1usize << 54) - 1) * 8; + assert!(kw_plaintext_len_is_valid(MAX_PT)); + assert!(!kw_plaintext_len_is_valid(MAX_PT + 4)); + assert!(!kw_plaintext_len_is_valid(MAX_PT + 8)); + assert!(kw_ciphertext_len_is_valid(MAX_PT + 8)); + assert!(!kw_ciphertext_len_is_valid(MAX_PT + 16)); + } + assert_eq!(kw.wrap_out_len(32), 40); + assert_eq!(kw.unwrap_out_max_len(40), 32); + assert_eq!(kw.unwrap_out_max_len(0), 0); + + // KWP: 1 to 2^32 - 1 octets of plaintext; 2 to 2^29 semiblocks of ciphertext. + assert!(!kwp_plaintext_len_is_valid(0)); + assert!(kwp_plaintext_len_is_valid(1)); + assert!(kwp_plaintext_len_is_valid(u32::MAX as usize)); + assert!(!kwp_plaintext_len_is_valid(u32::MAX as usize + 1)); + assert!(!kwp_ciphertext_len_is_valid(8)); + assert!(kwp_ciphertext_len_is_valid(16)); + assert!(!kwp_ciphertext_len_is_valid(20)); + assert!(kwp_ciphertext_len_is_valid(8 << 29)); + assert!(!kwp_ciphertext_len_is_valid((8 << 29) + 8)); + assert_eq!(kwp_wrapped_len(1), 16); + assert_eq!(kwp_wrapped_len(8), 16); + assert_eq!(kwp_wrapped_len(9), 24); + assert_eq!(kwp_wrapped_len(20), 32); + assert_eq!(kwp_wrapped_len(usize::MAX), usize::MAX, "saturates rather than overflowing"); + assert_eq!(kwp.unwrap_out_max_len(32), 24); + + // ... and the run-time API enforces them. + let mut buf = [0u8; 64]; + assert!(matches!( + kw.wrap_out(&[0u8; 8], &mut buf), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + assert!(matches!( + kw.wrap_out(&[0u8; 12], &mut buf), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + assert!(matches!( + kw.unwrap_out(&[0u8; 16], &mut buf), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + assert!(matches!( + kwp.unwrap_out(&[0u8; 20], &mut buf), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); +} + +/// KW and KWP wrap the same aligned data to different lengths only when it is short enough to +/// fit KWP's single block; otherwise KWP's ciphertext is KW's length, not longer, since the +/// header replaces ICV1 rather than adding to it. +#[test] +fn kwp_costs_nothing_extra_for_aligned_data() { + let kw = kw_key(); + let kwp = kwp_key(); + assert_eq!(kw.wrap_out_len(16), kwp.wrap_out_len(16)); + assert_eq!(kw.wrap_out_len(32), kwp.wrap_out_len(32)); + assert_eq!(kwp.wrap_out_len(8), 16, "and 8 bytes need only one block"); +} + +/// The worker is the borrow and nothing else, and the key is the permutation and nothing else. +#[test] +fn sizes() { + use core::mem::size_of; + assert_eq!(size_of::>(), size_of::<&Toy>()); + assert_eq!(size_of::>(), size_of::<&Toy>()); + assert_eq!(size_of::(), size_of::()); +} From 56e43eb5a4a732caa623985c3b0d9e4403098a21 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 27 Oct 2026 03:48:30 +1100 Subject: [PATCH 06/18] aes: AES*Key and AES*Key wrap keys; the AES_KW_* / AES_KWP_* aliases name the workers The key gets the two key-wrap roles, forwarded to the mode as the others are, and the internal permutations get their KW and KWP names. The aliases become `Kw<'k, AES128Internal>` and friends, the shape of AES_CMAC_128. Tests: the RFC 3394 and RFC 5649 vectors and the shared key-wrap framework through the keys, the ACVP KW and KWP sets from bc-test-data (1800 cases each, with the forgeries counted), the construction suite, and the alias names alongside the other aliases. The modes bench gains the KW/KWP measurements the old modes crate's bench had. Assisted-by: Claude:claude-mythos-5-1 --- crypto/aes/benches/aes_modes_benches.rs | 65 +++- crypto/aes/src/hazmat/aes_internal.rs | 6 + crypto/aes/src/key.rs | 43 ++- crypto/aes/src/kw.rs | 198 ++++++------- crypto/aes/tests/alg_name_tests.rs | 24 +- crypto/aes/tests/ecb_bc-test-data.rs | 2 +- crypto/aes/tests/kw_bc-test-data.rs | 194 +++++------- crypto/aes/tests/kw_tests.rs | 374 ++++++++---------------- 8 files changed, 420 insertions(+), 486 deletions(-) diff --git a/crypto/aes/benches/aes_modes_benches.rs b/crypto/aes/benches/aes_modes_benches.rs index ec0cd1ad0..55b2fbf78 100644 --- a/crypto/aes/benches/aes_modes_benches.rs +++ b/crypto/aes/benches/aes_modes_benches.rs @@ -28,13 +28,14 @@ use bouncycastle_aes::{AES128Key, AES256Key}; use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; use bouncycastle_cipher::modes::{ Blocks, CBC, CCM, CCM_Packet, CFB, CFB8, CMAC, CTR, Cfb, Cfb8, Ctr, GCM, GCM_NONCE_LEN, GMAC, + KW, KWP, }; use bouncycastle_cipher::{Decrypting, Encrypting}; use bouncycastle_core::hazmat::{AeadDeclaredEncryptMode, EcbEncryptionKey, ElectronicCodeBook}; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::symmetric_key::{ - AeadEncryptionKey, BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, MacGenerationKey, - SymmetricDecryptionKey, SymmetricEncryptionKey, + AeadEncryptionKey, BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, KeyUnwrappingKey, + KeyWrappingKey, MacGenerationKey, SymmetricDecryptionKey, SymmetricEncryptionKey, }; use bouncycastle_core::traits::{ BlockCipherDecryptor, BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, @@ -597,9 +598,67 @@ fn bench_gmac(c: &mut Criterion) { group.finish(); } +/// KW and KWP wrapping and unwrapping a 256-bit key -- the common case, where the cost is the +/// six passes of the wrapping function over five semiblocks -- and KWP over 4 KiB, the +/// encoded-private-key case, where it is per byte. Throughput is per byte of plaintext. Only the +/// one-shots are measured, since key wrap has no streaming API, and the key is built outside the +/// loop as it would be in use. +fn bench_kw_aes128(c: &mut Criterion) { + let kw = AES128Key::::from_key_material(&key::<16>()).unwrap(); + let kwp = AES128Key::::from_key_material(&key::<16>()).unwrap(); + let key_to_wrap = [0x5Au8; 32]; + let kw_ct: [u8; 40] = kw.wrap_key(&key_to_wrap).unwrap(); + let kwp_ct: [u8; 40] = kwp.wrap_key(&key_to_wrap).unwrap(); + + let mut group = c.benchmark_group("modes::kw::AES_128"); + group.throughput(Throughput::Bytes(key_to_wrap.len() as u64)); + group.bench_function("KW wrap 32-byte key", |b| { + b.iter(|| black_box(kw.wrap_key::<32, 40>(black_box(&key_to_wrap)).unwrap())) + }); + group.bench_function("KW unwrap 32-byte key", |b| { + b.iter(|| black_box(kw.unwrap_key::<32, 40>(black_box(&kw_ct)).unwrap())) + }); + group.bench_function("KWP wrap 32-byte key", |b| { + b.iter(|| black_box(kwp.wrap_key::<32, 40>(black_box(&key_to_wrap)).unwrap())) + }); + group.bench_function("KWP unwrap 32-byte key", |b| { + b.iter(|| black_box(kwp.unwrap_key::<32, 40>(black_box(&kwp_ct)).unwrap())) + }); + group.finish(); + + let payload: Vec = data().as_flattened()[..4096].to_vec(); + let mut wrapped = vec![0u8; kwp.wrap_out_len(payload.len())]; + let wrapped_len = kwp.wrap_out(&payload, &mut wrapped).unwrap(); + wrapped.truncate(wrapped_len); + + let mut group = c.benchmark_group("modes::kwp::AES_128"); + group.throughput(Throughput::Bytes(payload.len() as u64)); + group.bench_function("KWP wrap 4KiB", |b| { + b.iter_batched( + || vec![0u8; wrapped.len()], + |mut out| { + kwp.wrap_out(black_box(&payload), &mut out).unwrap(); + black_box(&out); + }, + BatchSize::SmallInput, + ) + }); + group.bench_function("KWP unwrap 4KiB", |b| { + b.iter_batched( + || vec![0u8; payload.len()], + |mut out| { + kwp.unwrap_out(black_box(&wrapped), &mut out).unwrap(); + black_box(&out); + }, + BatchSize::SmallInput, + ) + }); + group.finish(); +} + criterion_group!( benches, bench_aes128cbc, bench_cfb_aes128, bench_cfb8_aes128, bench_ctr_aes128, bench_ecb_aes128, bench_ccm_aes128, bench_ccm_one_shot_pair, bench_gcm_aes128, - bench_gcm_aes256, bench_init, bench_cmac, bench_gmac + bench_gcm_aes256, bench_init, bench_cmac, bench_gmac, bench_kw_aes128 ); criterion_main!(benches); diff --git a/crypto/aes/src/hazmat/aes_internal.rs b/crypto/aes/src/hazmat/aes_internal.rs index 561128d22..897e03723 100644 --- a/crypto/aes/src/hazmat/aes_internal.rs +++ b/crypto/aes/src/hazmat/aes_internal.rs @@ -350,6 +350,8 @@ impl ModeNames for AES128Internal { const ECB_ALG_NAME: &'static str = "AES_ECB_128"; const GCM_ALG_NAME: &'static str = "AES_GCM_128"; const GMAC_ALG_NAME: &'static str = "AES_GMAC_128"; + const KW_ALG_NAME: &'static str = "AES_KW_128"; + const KWP_ALG_NAME: &'static str = "AES_KWP_128"; } impl ModeNames for AES192Internal { @@ -362,6 +364,8 @@ impl ModeNames for AES192Internal { const ECB_ALG_NAME: &'static str = "AES_ECB_192"; const GCM_ALG_NAME: &'static str = "AES_GCM_192"; const GMAC_ALG_NAME: &'static str = "AES_GMAC_192"; + const KW_ALG_NAME: &'static str = "AES_KW_192"; + const KWP_ALG_NAME: &'static str = "AES_KWP_192"; } impl ModeNames for AES256Internal { @@ -374,4 +378,6 @@ impl ModeNames for AES256Internal { const ECB_ALG_NAME: &'static str = "AES_ECB_256"; const GCM_ALG_NAME: &'static str = "AES_GCM_256"; const GMAC_ALG_NAME: &'static str = "AES_GMAC_256"; + const KW_ALG_NAME: &'static str = "AES_KW_256"; + const KWP_ALG_NAME: &'static str = "AES_KWP_256"; } diff --git a/crypto/aes/src/key.rs b/crypto/aes/src/key.rs index 0125b5e6f..1f37131c6 100644 --- a/crypto/aes/src/key.rs +++ b/crypto/aes/src/key.rs @@ -8,14 +8,14 @@ use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; use bouncycastle_core::hazmat::{ AeadDeclaredDecryptMode, AeadDeclaredEncryptMode, AeadDecryptMode, AeadEncryptMode, BlockDecryptMode, BlockEncryptMode, EcbDecryptMode, EcbDecryptionKey, EcbEncryptMode, - EcbEncryptionKey, MacMode, Mode, SymmetricDecryptMode, SymmetricEncryptMode, - do_hazardous_operations, + EcbEncryptionKey, KeyUnwrapMode, KeyWrapMode, MacMode, Mode, SymmetricDecryptMode, + SymmetricEncryptMode, do_hazardous_operations, }; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::symmetric_key::{ AeadDeclaredDecryptionKey, AeadDeclaredEncryptionKey, AeadDecryptionKey, AeadEncryptionKey, - BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, MacGenerationKey, MacVerificationKey, - SymmetricDecryptionKey, SymmetricEncryptionKey, + BlockDecryptionKey, BlockEncryptionKey, FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey, + MacGenerationKey, MacVerificationKey, SymmetricDecryptionKey, SymmetricEncryptionKey, }; use bouncycastle_core::traits::RNG; @@ -36,8 +36,9 @@ use bouncycastle_core::traits::RNG; /// `KeyType::SymmetricCipherKey`. Which workers a key hands out is decided by its mode: the /// symmetric pair for the stream modes and the padded block modes (`CFB`, `CFB8`, `CTR`, /// `CBC`, `ECB`), the block pair for `CBC`, the `hazmat` ECB pair for -/// `ECB`, the AEAD pairs for `GCM`, `CCM` and `CCM_Packet`, the MAC pair for `CMAC`, and -/// for `GMAC`, whose tag is nonce-bound, an inherent pair (see [`AES_GMAC_128`](crate::AES_GMAC_128)). +/// `ECB`, the AEAD pairs for `GCM`, `CCM` and `CCM_Packet`, the MAC pair for `CMAC`, the +/// key-wrap pair for `KW` and `KWP`, and for `GMAC`, whose tag is nonce-bound, an inherent pair +/// (see [`AES_GMAC_128`](crate::AES_GMAC_128)). /// /// # Usage Examples /// @@ -512,3 +513,33 @@ impl>> MacVerificationKey for AESKey KeyWrappingKey for AESKey +where + P: AESParams, + M: KeyWrapMode, KEK_LEN>, +{ + type Wrapper<'k> + = M::Wrapper<'k> + where + Self: 'k; + + fn create_wrapper<'k>(&'k self) -> Self::Wrapper<'k> { + M::wrap_init(&self.state) + } +} + +impl KeyUnwrappingKey for AESKey +where + P: AESParams, + M: KeyUnwrapMode, KEK_LEN>, +{ + type Unwrapper<'k> + = M::Unwrapper<'k> + where + Self: 'k; + + fn create_unwrapper<'k>(&'k self) -> Self::Unwrapper<'k> { + M::unwrap_init(&self.state) + } +} diff --git a/crypto/aes/src/kw.rs b/crypto/aes/src/kw.rs index 06cba4aa5..61837f788 100644 --- a/crypto/aes/src/kw.rs +++ b/crypto/aes/src/kw.rs @@ -1,18 +1,12 @@ //! Type aliases for AES Key Wrap, KW (NIST SP 800-38F Sec 6.2; RFC 3394), and AES Key Wrap with //! Padding, KWP (Sec 6.3; RFC 5649). //! -//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Kw` and `Kwp` take the permutation -//! and its key length. These aliases pin both: -//! -//! ```text -//! AES_KW_128 // AES-128 KW: whole 8-byte semiblocks in, one semiblock more out -//! AES_KWP_256 // AES-256 KWP: any length in, rounded up to 8 plus 8 out -//! ``` -//! -//! There is no direction parameter: wrapping and unwrapping are the separate [`KeyWrapper`] and -//! [`KeyUnwrapper`] traits, both implemented by each alias, and both consisting of associated -//! functions. See [`bouncycastle_modes::kw`] for the API, the in-place wrapping function and the -//! security notes, and [`bouncycastle_modes::kwp`] for what padding adds. +//! See [`bouncycastle_cipher::modes::kw`] for the construction, the in-place wrapping function and +//! the security notes, and [`bouncycastle_cipher::modes::kwp`] for what padding adds. +//! [`AES_KW_128`] and friends name the worker, which implements [`KeyWrapper`] and +//! [`KeyUnwrapper`]; it comes from a key, [`AES128Key`] or [`AES128Key`], which takes +//! [`KeyType::SymmetricCipherKey`] material and hands the worker out through [`KeyWrappingKey`] +//! and [`KeyUnwrappingKey`], whose one-shots are the API in practice. //! //! # Which one //! @@ -23,107 +17,113 @@ //! one to use for encoded private keys and other non-aligned data. Their ciphertexts are not //! interchangeable: the integrity check values differ, so unwrapping one with the other fails. //! -//! The RFC 3394 and RFC 5649 OIDs (`id-aes128-wrap` `{ aes 5 }`, `id-aes128-wrap-pad` `{ aes 8 }`, -//! and the 192- and 256-bit siblings at 25/28 and 45/48) are not attached to these aliases yet: -//! none of the AES mode aliases carries an [`AlgorithmOID`] on this branch, and a mode alias is not -//! a local type this crate could implement one for. - -use crate::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_modes::{Kw, Kwp}; +//! # Usage Examples +//! +//! RFC 3394 Sec 4.1, "Wrap 128 bits of Key Data with a 128-bit KEK", through the key: +//! +//! ``` +//! use bouncycastle_aes::AES128Key; +//! use bouncycastle_cipher::modes::KW; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +//! +//! // KEK 000102030405060708090A0B0C0D0E0F, key data 00112233445566778899AABBCCDDEEFF. +//! let kek_bytes: [u8; 16] = core::array::from_fn(|i| i as u8); +//! let key_data: [u8; 16] = core::array::from_fn(|i| (i as u8) * 0x11); +//! let material = KeyMaterial::<16>::from_bytes_as_type(&kek_bytes, KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let kek = AES128Key::::from_key_material(&material).expect("a valid AES-128 KEK"); +//! +//! let wrapped: [u8; 24] = kek.wrap_key(&key_data).expect("wrapping"); +//! assert_eq!( +//! wrapped, +//! [ +//! 0x1F, 0xA6, 0x8B, 0x0A, 0x81, 0x12, 0xB4, 0x47, 0xAE, 0xF3, 0x4B, 0xD8, 0xFB, 0x5A, +//! 0x7B, 0x82, 0x9D, 0x3E, 0x86, 0x23, 0x71, 0xD2, 0xCF, 0xE5, +//! ] +//! ); +//! let recovered = kek.unwrap_key::<16, 24>(&wrapped).expect("unwrapping"); +//! assert_eq!(*recovered, key_data); +//! ``` +//! +//! The second example of RFC 5649 Sec 6, which wraps 7 octets -- short enough that the header +//! and data fit one AES block, the case Algorithm 5 step 5 enciphers directly -- and a payload +//! whose length is only known at run time: +//! +//! ``` +//! use bouncycastle_aes::AES192Key; +//! use bouncycastle_cipher::modes::KWP; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +//! +//! let kek_bytes = [ +//! 0x58, 0x40, 0xdf, 0x6e, 0x29, 0xb0, 0x2a, 0xf1, 0xab, 0x49, 0x3b, 0x70, 0x5b, 0xf1, 0x6e, +//! 0xa1, 0xae, 0x83, 0x38, 0xf4, 0xdc, 0xc1, 0x76, 0xa8, +//! ]; +//! let material = KeyMaterial::<24>::from_bytes_as_type(&kek_bytes, KeyType::SymmetricCipherKey) +//! .expect("a 24-byte symmetric cipher key"); +//! let kek = AES192Key::::from_key_material(&material).expect("a valid AES-192 KEK"); +//! let key_data = [0x46, 0x6f, 0x72, 0x50, 0x61, 0x73, 0x69]; +//! +//! let wrapped: [u8; 16] = kek.wrap_key(&key_data).expect("wrapping"); +//! assert_eq!( +//! wrapped, +//! [ +//! 0xaf, 0xbe, 0xb0, 0xf0, 0x7d, 0xfb, 0xf5, 0x41, 0x92, 0x00, 0xf2, 0xcc, 0xb5, 0x0b, +//! 0xb2, 0x4f, +//! ] +//! ); +//! assert_eq!(*kek.unwrap_key::<7, 16>(&wrapped).expect("unwrapping"), key_data); +//! +//! let payload = b"an encoded private key, of whatever length it happens to be"; +//! let mut wrapped = vec![0u8; kek.wrap_out_len(payload.len())]; +//! let n = kek.wrap_out(payload, &mut wrapped).expect("wrapping"); +//! let mut recovered = vec![0u8; kek.unwrap_out_max_len(n)]; +//! let m = kek.unwrap_out(&wrapped[..n], &mut recovered).expect("unwrapping"); +//! assert_eq!(&recovered[..m], payload); +//! ``` +//! +//! # Memory Usage +//! +//! A key is the expanded schedule: 176, 208 and 240 bytes for AES-128, -192 and -256. A worker +//! is the borrow of it, one pointer, and the wrapping function's scratch is one block on the +//! stack; the data is wrapped and unwrapped in the caller's buffers. +//! +//! [`AES128Key`]: crate::AES128Key +//! [`AES128Key`]: crate::AES128Key +//! [`KeyWrapper`]: bouncycastle_core::traits::KeyWrapper +//! [`KeyUnwrapper`]: bouncycastle_core::traits::KeyUnwrapper +//! [`KeyWrappingKey`]: bouncycastle_core::symmetric_key::KeyWrappingKey +//! [`KeyUnwrappingKey`]: bouncycastle_core::symmetric_key::KeyUnwrappingKey +//! [`KeyType::SymmetricCipherKey`]: bouncycastle_core::key_material::KeyType::SymmetricCipherKey -// Imports needed for docs -#[allow(unused_imports)] -use bouncycastle_core::traits::{AlgorithmOID, KeyUnwrapper, KeyWrapper}; -// end of imports needed for docs +use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::{Kw, Kwp}; -/// AES-128 Key Wrap (SP 800-38F KW; RFC 3394 with a 128-bit KEK). -/// -/// This is RFC 3394 Sec 4.1, "Wrap 128 bits of Key Data with a 128-bit KEK", as a doctest: -/// -/// ``` -/// use bouncycastle_aes::AES_KW_128; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; -/// -/// // KEK 000102030405060708090A0B0C0D0E0F, key data 00112233445566778899AABBCCDDEEFF. -/// let kek_bytes: [u8; 16] = core::array::from_fn(|i| i as u8); -/// let key_data: [u8; 16] = core::array::from_fn(|i| (i as u8) * 0x11); -/// let kek = KeyMaterial::<16>::from_bytes_as_type(&kek_bytes, KeyType::SymmetricCipherKey) -/// .expect("a 16-byte symmetric cipher key"); -/// -/// let wrapped: [u8; 24] = AES_KW_128::wrap_key(&kek, &key_data).expect("wrapping"); -/// assert_eq!( -/// wrapped, -/// [ -/// 0x1F, 0xA6, 0x8B, 0x0A, 0x81, 0x12, 0xB4, 0x47, 0xAE, 0xF3, 0x4B, 0xD8, 0xFB, 0x5A, -/// 0x7B, 0x82, 0x9D, 0x3E, 0x86, 0x23, 0x71, 0xD2, 0xCF, 0xE5, -/// ] -/// ); -/// let recovered = AES_KW_128::unwrap_key::<16, 24>(&kek, &wrapped).expect("unwrapping"); -/// assert_eq!(*recovered, key_data); -/// ``` +/// AES-128 Key Wrap (SP 800-38F KW; RFC 3394 with a 128-bit KEK): the worker an +/// `AES128Key` hands out. #[allow(non_camel_case_types)] -pub type AES_KW_128 = Kw; +pub type AES_KW_128<'k> = Kw<'k, AES128Internal>; /// AES-192 Key Wrap (SP 800-38F KW; RFC 3394 with a 192-bit KEK). See [`AES_KW_128`]. #[allow(non_camel_case_types)] -pub type AES_KW_192 = Kw; +pub type AES_KW_192<'k> = Kw<'k, AES192Internal>; /// AES-256 Key Wrap (SP 800-38F KW; RFC 3394 with a 256-bit KEK). See [`AES_KW_128`]. #[allow(non_camel_case_types)] -pub type AES_KW_256 = Kw; +pub type AES_KW_256<'k> = Kw<'k, AES256Internal>; -/// AES-128 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 128-bit KEK). -/// -/// ``` -/// use bouncycastle_aes::AES_KWP_128; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; -/// -/// let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) -/// .expect("a 16-byte symmetric cipher key"); -/// -/// // Any length: 13 bytes pad to 16, plus the 8-byte header. -/// let data = *b"thirteen bytes"; // 14, actually -- still 16 + 8. -/// let wrapped: [u8; 24] = AES_KWP_128::wrap_key(&kek, &data).expect("wrapping"); -/// let recovered = AES_KWP_128::unwrap_key::<14, 24>(&kek, &wrapped).expect("unwrapping"); -/// assert_eq!(*recovered, data); -/// ``` +/// AES-128 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 128-bit KEK): the worker an +/// `AES128Key` hands out. #[allow(non_camel_case_types)] -pub type AES_KWP_128 = Kwp; +pub type AES_KWP_128<'k> = Kwp<'k, AES128Internal>; -/// AES-192 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 192-bit KEK). -/// -/// This is the second example of RFC 5649 Sec 6, which wraps 7 octets -- short enough that the -/// header and data fit one AES block, the case Algorithm 5 step 5 enciphers directly: -/// -/// ``` -/// use bouncycastle_aes::AES_KWP_192; -/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -/// use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; -/// -/// let kek_bytes = [ -/// 0x58, 0x40, 0xdf, 0x6e, 0x29, 0xb0, 0x2a, 0xf1, 0xab, 0x49, 0x3b, 0x70, 0x5b, 0xf1, 0x6e, -/// 0xa1, 0xae, 0x83, 0x38, 0xf4, 0xdc, 0xc1, 0x76, 0xa8, -/// ]; -/// let kek = KeyMaterial::<24>::from_bytes_as_type(&kek_bytes, KeyType::SymmetricCipherKey) -/// .expect("a 24-byte symmetric cipher key"); -/// let key_data = [0x46, 0x6f, 0x72, 0x50, 0x61, 0x73, 0x69]; -/// -/// let wrapped: [u8; 16] = AES_KWP_192::wrap_key(&kek, &key_data).expect("wrapping"); -/// assert_eq!( -/// wrapped, -/// [ -/// 0xaf, 0xbe, 0xb0, 0xf0, 0x7d, 0xfb, 0xf5, 0x41, 0x92, 0x00, 0xf2, 0xcc, 0xb5, 0x0b, -/// 0xb2, 0x4f, -/// ] -/// ); -/// assert_eq!(*AES_KWP_192::unwrap_key::<7, 16>(&kek, &wrapped).expect("unwrapping"), key_data); -/// ``` +/// AES-192 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 192-bit KEK). See +/// [`AES_KWP_128`]. #[allow(non_camel_case_types)] -pub type AES_KWP_192 = Kwp; +pub type AES_KWP_192<'k> = Kwp<'k, AES192Internal>; /// AES-256 Key Wrap with Padding (SP 800-38F KWP; RFC 5649 with a 256-bit KEK). See /// [`AES_KWP_128`]. #[allow(non_camel_case_types)] -pub type AES_KWP_256 = Kwp; +pub type AES_KWP_256<'k> = Kwp<'k, AES256Internal>; diff --git a/crypto/aes/tests/alg_name_tests.rs b/crypto/aes/tests/alg_name_tests.rs index 481a4bd3a..f9676a50e 100644 --- a/crypto/aes/tests/alg_name_tests.rs +++ b/crypto/aes/tests/alg_name_tests.rs @@ -8,7 +8,8 @@ use bouncycastle_aes::{ AES_CBC_128, AES_CBC_192, AES_CBC_256, AES_CCM_128, AES_CCM_192, AES_CCM_256, AES_CFB_128, AES_CFB_192, AES_CFB_256, AES_CFB8_128, AES_CFB8_192, AES_CFB8_256, AES_CMAC_128, AES_CMAC_192, AES_CMAC_256, AES_CTR_128, AES_CTR_192, AES_CTR_256, AES_GCM_128, AES_GCM_192, AES_GCM_256, - AES_GMAC_128, AES_GMAC_192, AES_GMAC_256, AES128Key, CCM_NONCE_LEN, CCM_TAG_LEN, + AES_GMAC_128, AES_GMAC_192, AES_GMAC_256, AES_KW_128, AES_KW_192, AES_KW_256, AES_KWP_128, + AES_KWP_192, AES_KWP_256, AES128Key, CCM_NONCE_LEN, CCM_TAG_LEN, }; use bouncycastle_cipher::modes::hazmat::ecb::{ECB, Ecb}; use bouncycastle_cipher::modes::{CBC, Cbc, Gcm}; @@ -157,6 +158,27 @@ fn cmac() { ); } +/// One worker serves both halves of a key wrap, so there is one name per alias to check. +#[test] +fn kw_kwp() { + use SecurityStrength::*; + assert_eq!((AES_KW_128::ALG_NAME, AES_KW_128::MAX_SECURITY_STRENGTH), ("AES_KW_128", _128bit)); + assert_eq!((AES_KW_192::ALG_NAME, AES_KW_192::MAX_SECURITY_STRENGTH), ("AES_KW_192", _192bit)); + assert_eq!((AES_KW_256::ALG_NAME, AES_KW_256::MAX_SECURITY_STRENGTH), ("AES_KW_256", _256bit)); + assert_eq!( + (AES_KWP_128::ALG_NAME, AES_KWP_128::MAX_SECURITY_STRENGTH), + ("AES_KWP_128", _128bit) + ); + assert_eq!( + (AES_KWP_192::ALG_NAME, AES_KWP_192::MAX_SECURITY_STRENGTH), + ("AES_KWP_192", _192bit) + ); + assert_eq!( + (AES_KWP_256::ALG_NAME, AES_KWP_256::MAX_SECURITY_STRENGTH), + ("AES_KWP_256", _256bit) + ); +} + fn key_for>() -> AES128Key { let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) .expect("a 16-byte symmetric cipher key"); diff --git a/crypto/aes/tests/ecb_bc-test-data.rs b/crypto/aes/tests/ecb_bc-test-data.rs index 62da285fa..c08dd1105 100644 --- a/crypto/aes/tests/ecb_bc-test-data.rs +++ b/crypto/aes/tests/ecb_bc-test-data.rs @@ -26,7 +26,7 @@ //! | `ACVP-AES-OFB` | nothing yet (OFB is unimplemented) | //! | `ACVP-AES-CTR` | `ctr_bc-test-data.rs` | //! | `ACVP-AES-GCM` / `-GMAC` | `gcm_bc-test-data.rs` / `gmac_bc-test-data.rs` | -//! | `ACVP-AES-KW` / `-KWP` | `crypto/modes/tests/acvp_kw_tests.rs` | +//! | `ACVP-AES-KW` / `-KWP` | `kw_bc-test-data.rs` | //! | `ACVP-AES-FF1` / `-FF3-1` | nothing yet (format-preserving encryption is unimplemented) | //! //! So an unused vector set here means an unimplemented mode, not an untested one. Adding a mode diff --git a/crypto/aes/tests/kw_bc-test-data.rs b/crypto/aes/tests/kw_bc-test-data.rs index 8e8642cf0..674b0a070 100644 --- a/crypto/aes/tests/kw_bc-test-data.rs +++ b/crypto/aes/tests/kw_bc-test-data.rs @@ -3,14 +3,11 @@ //! //! Requires `bc-test-data` to be cloned alongside this repository, i.e. at `../bc-test-data` //! relative to the root of this git project. If it is absent the tests print a warning and pass, -//! matching the convention of the other ACVP suites -- `cargo test` must stay green for someone -//! who has only cloned this repository. +//! matching the convention of the other ACVP suites. //! -//! # Joining the request and response files -//! -//! As for the CBC set, the response file carries the answer against a `tcId` and the request file -//! carries the inputs and the group metadata (`direction`, `keyLen`, `payloadLen`, `kwCipher`), so -//! both are read and joined on `tcId`. +//! The response file carries the answer against a `tcId` and the request file carries the inputs +//! and the group metadata (`direction`, `keyLen`, `payloadLen`, `kwCipher`), so both are read and +//! joined on `tcId`. //! //! # Coverage //! @@ -25,77 +22,24 @@ //! (SP 800-38F Sec 5.1). The "inverse" variant, where decryption wraps, is not implemented and //! would be skipped and counted if a group asked for it. -use bouncycastle_aes::{AES_KW_128, AES_KW_192, AES_KW_256, AES_KWP_128, AES_KWP_192, AES_KWP_256}; +use bouncycastle_aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::{KW, KWP}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::key_material::{ - KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; -use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; -use bouncycastle_hex as hex; -use serde_json::Value; +use bouncycastle_core::hazmat::Mode; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; use std::collections::BTreeMap; -use std::fs; -use std::path::{Path, PathBuf}; -/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. -const TEST_DATA_PATHS: [&str; 2] = [ - "../../../bc-test-data/crypto/aes_tdes_vectors/AES", - "../bc-test-data/crypto/aes_tdes_vectors/AES", -]; +#[path = "common/acvp_helpers.rs"] +mod acvp_helpers; +use acvp_helpers::cipher_key; +const TEST_DATA_DIR: &str = "crypto/aes_tdes_vectors/AES"; const KW_REQUEST_FILE: &str = "ACVP-AES-KW.4014525.req.json"; const KW_RESPONSE_FILE: &str = "ACVP-AES-KW.4014525.rsp.json"; const KWP_REQUEST_FILE: &str = "ACVP-AES-KWP.4014526.req.json"; const KWP_RESPONSE_FILE: &str = "ACVP-AES-KWP.4014526.rsp.json"; -fn test_data_dir(request_file: &str, response_file: &str) -> Option { - for candidate in TEST_DATA_PATHS { - let path = Path::new(candidate); - if path.join(request_file).exists() && path.join(response_file).exists() { - return Some(path.to_path_buf()); - } - } - println!( - "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); \ - ACVP {request_file} tests will be skipped" - ); - None -} - -/// The ACVP files are a two-element array: a version header, then the payload. -fn load(path: &Path) -> Value { - let text = fs::read_to_string(path).unwrap_or_else(|e| panic!("reading {path:?}: {e}")); - let value: Value = - serde_json::from_str(&text).unwrap_or_else(|e| panic!("parsing {path:?}: {e}")); - value - .as_array() - .and_then(|a| a.get(1)) - .cloned() - .unwrap_or_else(|| panic!("{path:?}: not a two-element ACVP array")) -} - -/// Builds a `KeyMaterial` from raw ACVP key bytes, including the all-zero keys the set contains. -fn cipher_key(bytes: &[u8]) -> KeyMaterial { - assert_eq!(bytes.len(), N, "key length should match the group's keyLen"); - let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) - .expect("ACVP key bytes fit the buffer"); - if key.key_type() != KeyType::SymmetricCipherKey { - do_hazardous_operations(&mut key, |k| { - k.set_key_type(KeyType::SymmetricCipherKey)?; - k.set_security_strength(SecurityStrength::from_bytes(N)) - }) - .expect("promoting a NIST all-zero test key"); - } - key -} - -fn hex_field(test: &Value, name: &str) -> Vec { - let tc_id = test["tcId"].as_u64().unwrap_or(0); - let s = test[name].as_str().unwrap_or_else(|| panic!("tcId {tc_id}: missing field {name}")); - hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc_id}: bad hex in {name}")) -} - /// What one case expects. enum Expected { /// The wrap or unwrap must succeed and produce these bytes. @@ -112,52 +56,47 @@ struct Stats { skipped: usize, } -/// Runs one case through both APIs. `W` is the alias for the group's key length. -fn run_case( - key_bytes: &[u8], +/// Runs one case through both APIs of the key `kek`. +fn run_case( + kek: &K, input: &[u8], encrypt: bool, expected: &Expected, tc_id: u64, ) where - W: KeyWrapper + KeyUnwrapper, + K: KeyWrappingKey + KeyUnwrappingKey, { - let kek = cipher_key::(key_bytes); - if encrypt { let Expected::Bytes(ct) = expected else { panic!("tcId {tc_id}: an encrypt case cannot expect failure"); }; - let mut out = vec![0u8; W::wrap_out_len(input.len())]; - let n = W::wrap_out(&kek, input, &mut out) + let mut out = vec![0u8; kek.wrap_out_len(input.len())]; + let n = kek + .wrap_out(input, &mut out) .unwrap_or_else(|e| panic!("tcId {tc_id}: wrap_out: {e:?}")); assert_eq!(&out[..n], ct, "tcId {tc_id}: wrap_out ciphertext"); // The fixed-length API, for the payload lengths the set uses. let fixed = match input.len() { - 16 => W::wrap_key::<16, 24>(&kek, input.try_into().unwrap()).map(|c| c.to_vec()), - 24 => W::wrap_key::<24, 32>(&kek, input.try_into().unwrap()).map(|c| c.to_vec()), - 64 => W::wrap_key::<64, 72>(&kek, input.try_into().unwrap()).map(|c| c.to_vec()), + 16 => kek.wrap_key::<16, 24>(input.try_into().unwrap()).map(|c| c.to_vec()), + 24 => kek.wrap_key::<24, 32>(input.try_into().unwrap()).map(|c| c.to_vec()), + 64 => kek.wrap_key::<64, 72>(input.try_into().unwrap()).map(|c| c.to_vec()), other => panic!("tcId {tc_id}: unexpected payload length {other}"), }; assert_eq!(fixed.unwrap().as_slice(), ct, "tcId {tc_id}: wrap_key ciphertext"); } else { - let mut out = vec![0u8; W::unwrap_out_max_len(input.len())]; - let result = W::unwrap_out(&kek, input, &mut out); + let mut out = vec![0u8; kek.unwrap_out_max_len(input.len())]; + let result = kek.unwrap_out(input, &mut out); match expected { Expected::Bytes(pt) => { let n = result.unwrap_or_else(|e| panic!("tcId {tc_id}: unwrap_out: {e:?}")); assert_eq!(&out[..n], pt, "tcId {tc_id}: unwrap_out plaintext"); - let fixed = - match pt.len() { - 16 => W::unwrap_key::<16, 24>(&kek, input.try_into().unwrap()) - .map(|k| k.to_vec()), - 24 => W::unwrap_key::<24, 32>(&kek, input.try_into().unwrap()) - .map(|k| k.to_vec()), - 64 => W::unwrap_key::<64, 72>(&kek, input.try_into().unwrap()) - .map(|k| k.to_vec()), - other => panic!("tcId {tc_id}: unexpected payload length {other}"), - }; + let fixed = match pt.len() { + 16 => kek.unwrap_key::<16, 24>(input.try_into().unwrap()).map(|k| k.to_vec()), + 24 => kek.unwrap_key::<24, 32>(input.try_into().unwrap()).map(|k| k.to_vec()), + 64 => kek.unwrap_key::<64, 72>(input.try_into().unwrap()).map(|k| k.to_vec()), + other => panic!("tcId {tc_id}: unexpected payload length {other}"), + }; assert_eq!(fixed.unwrap().as_slice(), pt, "tcId {tc_id}: unwrap_key plaintext"); } Expected::Failure => { @@ -169,16 +108,12 @@ fn run_case( out.iter().all(|b| *b == 0), "tcId {tc_id}: the buffer is scrubbed on failure" ); - let fixed: Result, _> = - match input.len() { - 24 => W::unwrap_key::<16, 24>(&kek, input.try_into().unwrap()) - .map(|k| k.to_vec()), - 32 => W::unwrap_key::<24, 32>(&kek, input.try_into().unwrap()) - .map(|k| k.to_vec()), - 72 => W::unwrap_key::<64, 72>(&kek, input.try_into().unwrap()) - .map(|k| k.to_vec()), - other => panic!("tcId {tc_id}: unexpected ciphertext length {other}"), - }; + let fixed: Result, _> = match input.len() { + 24 => kek.unwrap_key::<16, 24>(input.try_into().unwrap()).map(|k| k.to_vec()), + 32 => kek.unwrap_key::<24, 32>(input.try_into().unwrap()).map(|k| k.to_vec()), + 72 => kek.unwrap_key::<64, 72>(input.try_into().unwrap()).map(|k| k.to_vec()), + other => panic!("tcId {tc_id}: unexpected ciphertext length {other}"), + }; assert!( matches!(fixed, Err(SymmetricCipherError::DecryptionFailed)), "tcId {tc_id}: unwrap_key must reject the forgery too" @@ -188,20 +123,20 @@ fn run_case( } } -/// Walks one ACVP set, dispatching each group to the alias for its key length. -fn run_set( - algorithm: &str, - request_file: &str, - response_file: &str, -) -> Option +/// Walks one ACVP set under the mode `M`, dispatching each group to the key type for its length. +fn run_set(algorithm: &str, request_file: &str, response_file: &str) -> Option where - W128: KeyWrapper<16> + KeyUnwrapper<16>, - W192: KeyWrapper<24> + KeyUnwrapper<24>, - W256: KeyWrapper<32> + KeyUnwrapper<32>, + M: Mode + + Mode + + Mode, + AES128Key: KeyWrappingKey<16> + KeyUnwrappingKey<16>, + AES192Key: KeyWrappingKey<24> + KeyUnwrappingKey<24>, + AES256Key: KeyWrappingKey<32> + KeyUnwrappingKey<32>, { - let dir = test_data_dir(request_file, response_file)?; - let request = load(&dir.join(request_file)); - let response = load(&dir.join(response_file)); + let req = bc_test_data_json(TEST_DATA_DIR, request_file)?; + let rsp = bc_test_data_json(TEST_DATA_DIR, response_file)?; + let request = &req[1]; + let response = &rsp[1]; assert_eq!(request["algorithm"].as_str(), Some(algorithm), "wrong vector file"); // Index the answers by tcId across every group. @@ -229,19 +164,28 @@ where for test in group["tests"].as_array().expect("request tests") { let tc_id = test["tcId"].as_u64().expect("tcId"); let answer = answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); - let key = hex_field(test, "key"); + let key = hex_field(test, "key", tc_id); let (input, expected) = if encrypt { - (hex_field(test, "pt"), Expected::Bytes(hex_field(answer, "ct"))) + (hex_field(test, "pt", tc_id), Expected::Bytes(hex_field(answer, "ct", tc_id))) } else if answer["testPassed"] == Value::Bool(false) { - (hex_field(test, "ct"), Expected::Failure) + (hex_field(test, "ct", tc_id), Expected::Failure) } else { - (hex_field(test, "ct"), Expected::Bytes(hex_field(answer, "pt"))) + (hex_field(test, "ct", tc_id), Expected::Bytes(hex_field(answer, "pt", tc_id))) }; match key_len { - 128 => run_case::(&key, &input, encrypt, &expected, tc_id), - 192 => run_case::(&key, &input, encrypt, &expected, tc_id), - 256 => run_case::(&key, &input, encrypt, &expected, tc_id), + 128 => { + let kek = AES128Key::::from_key_material(&cipher_key::<16>(&key)).unwrap(); + run_case::<_, 16>(&kek, &input, encrypt, &expected, tc_id) + } + 192 => { + let kek = AES192Key::::from_key_material(&cipher_key::<24>(&key)).unwrap(); + run_case::<_, 24>(&kek, &input, encrypt, &expected, tc_id) + } + 256 => { + let kek = AES256Key::::from_key_material(&cipher_key::<32>(&key)).unwrap(); + run_case::<_, 32>(&kek, &input, encrypt, &expected, tc_id) + } other => panic!("unexpected keyLen {other}"), } @@ -257,9 +201,7 @@ where #[test] fn acvp_aes_kw() { - let Some(stats) = run_set::( - "ACVP-AES-KW", KW_REQUEST_FILE, KW_RESPONSE_FILE, - ) else { + let Some(stats) = run_set::("ACVP-AES-KW", KW_REQUEST_FILE, KW_RESPONSE_FILE) else { return; }; println!( @@ -274,9 +216,7 @@ fn acvp_aes_kw() { #[test] fn acvp_aes_kwp() { - let Some(stats) = run_set::( - "ACVP-AES-KWP", KWP_REQUEST_FILE, KWP_RESPONSE_FILE, - ) else { + let Some(stats) = run_set::("ACVP-AES-KWP", KWP_REQUEST_FILE, KWP_RESPONSE_FILE) else { return; }; println!( diff --git a/crypto/aes/tests/kw_tests.rs b/crypto/aes/tests/kw_tests.rs index 4455fba19..032c64602 100644 --- a/crypto/aes/tests/kw_tests.rs +++ b/crypto/aes/tests/kw_tests.rs @@ -1,46 +1,49 @@ -//! Known-answer and behaviour tests for KW and KWP over AES. +//! `AES*Key` and `AES*Key`: the published known answers, the shared key-wrap framework +//! at several payload lengths, and what the aliases resolve to. //! -//! The known answers are the published ones: RFC 3394 Sec 4 (all six KW vectors, covering every -//! AES key length and 128-, 192- and 256-bit key data) and RFC 5649 Sec 6 (both KWP examples, the -//! second of which is the 7-octet case that takes the single-block path of SP 800-38F Algorithm 5 -//! step 5 and Algorithm 6 step 3). SP 800-38F itself publishes no vectors; the NIST ACVP sets are -//! in `acvp_kw_tests.rs`. -//! -//! The behaviour tests run the shared `TestFrameworkKeyWrap` -- round trips, determinism, the -//! length helpers, every error condition -- for each AES key length at several payload lengths, -//! and then pin the properties that distinguish the two algorithms from each other and from a -//! plain block encryption. - -use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; -use bouncycastle_core::errors::SymmetricCipherError; +//! The known answers are RFC 3394 Sec 4 (all six KW vectors, covering every AES key length and +//! 128-, 192- and 256-bit key data) and RFC 5649 Sec 6 (both KWP examples, the second of which is +//! the 7-octet case that takes the single-block path of SP 800-38F Algorithm 5 step 5 and +//! Algorithm 6 step 3). SP 800-38F itself publishes no vectors; the NIST ACVP sets are in +//! `kw_bc-test-data.rs`. The structural tests of the two algorithms are toy-driven, in +//! `crypto/cipher/tests/modes/kw_tests.rs`; the framework runs here because its tamper checks +//! need a permutation with diffusion. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_aes::{ + AES_KW_128, AES_KW_192, AES_KW_256, AES_KWP_128, AES_KWP_192, AES_KWP_256, AES128Key, + AES192Key, AES256Key, +}; +use bouncycastle_cipher::modes::{KW, KWP, ModeNames}; +use bouncycastle_core::hazmat::Mode; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -use bouncycastle_core::traits::{ElectronicCodeBook, KeyUnwrapper, KeyWrapper}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +use bouncycastle_core_test_framework::from_key_material::TestFrameworkFromKeyMaterial; use bouncycastle_core_test_framework::key_wrap::TestFrameworkKeyWrap; use bouncycastle_hex as hex; -use bouncycastle_modes::kw::{ - kw_ciphertext_len_is_valid, kw_plaintext_len_is_valid, kw_wrapped_len, -}; -use bouncycastle_modes::kwp::{ - kwp_ciphertext_len_is_valid, kwp_plaintext_len_is_valid, kwp_wrapped_len, -}; -use bouncycastle_modes::{Kw, Kwp}; - -type Aes128Kw = Kw; -type Aes192Kw = Kw; -type Aes256Kw = Kw; -type Aes128Kwp = Kwp; -type Aes192Kwp = Kwp; -type Aes256Kwp = Kwp; fn bytes(hex_str: &str) -> [u8; N] { hex::decode(hex_str).expect("valid hex").try_into().expect("the expected length") } -fn kek(hex_str: &str) -> KeyMaterial { +fn material(hex_str: &str) -> KeyMaterial { KeyMaterial::::from_bytes_as_type(&bytes::(hex_str), KeyType::SymmetricCipherKey) .expect("a symmetric cipher key") } +fn key128>(hex_str: &str) -> AES128Key { + AES128Key::from_key_material(&material::<16>(hex_str)).expect("a valid key") +} + +fn key192>(hex_str: &str) -> AES192Key { + AES192Key::from_key_material(&material::<24>(hex_str)).expect("a valid key") +} + +fn key256>(hex_str: &str) -> AES256Key { + AES256Key::from_key_material(&material::<32>(hex_str)).expect("a valid key") +} + // ---- RFC 3394 Sec 4: the KW vectors -------------------------------------------------------- /// RFC 3394 Sec 4.1-4.6 all use the same KEK bytes, truncated to the key length, and the same @@ -51,7 +54,7 @@ const RFC3394_KEY_DATA: &str = "00112233445566778899aabbccddeeff0001020304050607 /// Sec 4.1, "Wrap 128 bits of Key Data with a 128-bit KEK". #[test] fn rfc3394_4_1_wrap_128_bits_with_a_128_bit_kek() { - TestFrameworkKeyWrap::new().test_kat::<16, 16, 24, Aes128Kw>( + TestFrameworkKeyWrap::new().test_kat::<16, 16, 24, AES128Key>( &bytes(&RFC3394_KEK[..32]), &bytes(&RFC3394_KEY_DATA[..32]), &bytes("1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5"), @@ -61,7 +64,7 @@ fn rfc3394_4_1_wrap_128_bits_with_a_128_bit_kek() { /// Sec 4.2, "Wrap 128 bits of Key Data with a 192-bit KEK". #[test] fn rfc3394_4_2_wrap_128_bits_with_a_192_bit_kek() { - TestFrameworkKeyWrap::new().test_kat::<24, 16, 24, Aes192Kw>( + TestFrameworkKeyWrap::new().test_kat::<24, 16, 24, AES192Key>( &bytes(&RFC3394_KEK[..48]), &bytes(&RFC3394_KEY_DATA[..32]), &bytes("96778b25ae6ca435f92b5b97c050aed2468ab8a17ad84e5d"), @@ -71,7 +74,7 @@ fn rfc3394_4_2_wrap_128_bits_with_a_192_bit_kek() { /// Sec 4.3, "Wrap 128 bits of Key Data with a 256-bit KEK". #[test] fn rfc3394_4_3_wrap_128_bits_with_a_256_bit_kek() { - TestFrameworkKeyWrap::new().test_kat::<32, 16, 24, Aes256Kw>( + TestFrameworkKeyWrap::new().test_kat::<32, 16, 24, AES256Key>( &bytes(RFC3394_KEK), &bytes(&RFC3394_KEY_DATA[..32]), &bytes("64e8c3f9ce0f5ba263e9777905818a2a93c8191e7d6e8ae7"), @@ -81,7 +84,7 @@ fn rfc3394_4_3_wrap_128_bits_with_a_256_bit_kek() { /// Sec 4.4, "Wrap 192 bits of Key Data with a 192-bit KEK". #[test] fn rfc3394_4_4_wrap_192_bits_with_a_192_bit_kek() { - TestFrameworkKeyWrap::new().test_kat::<24, 24, 32, Aes192Kw>( + TestFrameworkKeyWrap::new().test_kat::<24, 24, 32, AES192Key>( &bytes(&RFC3394_KEK[..48]), &bytes(&RFC3394_KEY_DATA[..48]), &bytes("031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2"), @@ -91,7 +94,7 @@ fn rfc3394_4_4_wrap_192_bits_with_a_192_bit_kek() { /// Sec 4.5, "Wrap 192 bits of Key Data with a 256-bit KEK". #[test] fn rfc3394_4_5_wrap_192_bits_with_a_256_bit_kek() { - TestFrameworkKeyWrap::new().test_kat::<32, 24, 32, Aes256Kw>( + TestFrameworkKeyWrap::new().test_kat::<32, 24, 32, AES256Key>( &bytes(RFC3394_KEK), &bytes(&RFC3394_KEY_DATA[..48]), &bytes("a8f9bc1612c68b3ff6e6f4fbe30e71e4769c8b80a32cb8958cd5d17d6b254da1"), @@ -101,7 +104,7 @@ fn rfc3394_4_5_wrap_192_bits_with_a_256_bit_kek() { /// Sec 4.6, "Wrap 256 bits of Key Data with a 256-bit KEK". #[test] fn rfc3394_4_6_wrap_256_bits_with_a_256_bit_kek() { - TestFrameworkKeyWrap::new().test_kat::<32, 32, 40, Aes256Kw>( + TestFrameworkKeyWrap::new().test_kat::<32, 32, 40, AES256Key>( &bytes(RFC3394_KEK), &bytes(RFC3394_KEY_DATA), &bytes("28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21"), @@ -116,7 +119,7 @@ const RFC5649_KEK: &str = "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"; /// the 32-byte result goes through the wrapping function proper. #[test] fn rfc5649_wrap_20_octets_with_a_192_bit_kek() { - TestFrameworkKeyWrap::new().test_kat::<24, 20, 32, Aes192Kwp>( + TestFrameworkKeyWrap::new().test_kat::<24, 20, 32, AES192Key>( &bytes(RFC5649_KEK), &bytes("c37b7e6492584340bed12207808941155068f738"), &bytes("138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a"), @@ -127,27 +130,40 @@ fn rfc5649_wrap_20_octets_with_a_192_bit_kek() { /// block, so this is the single-block path (SP 800-38F Algorithm 5 step 5 / Algorithm 6 step 3). #[test] fn rfc5649_wrap_7_octets_with_a_192_bit_kek() { - TestFrameworkKeyWrap::new().test_kat::<24, 7, 16, Aes192Kwp>( + TestFrameworkKeyWrap::new().test_kat::<24, 7, 16, AES192Key>( &bytes(RFC5649_KEK), &bytes("466f7250617369"), &bytes("afbeb0f07dfbf5419200f2ccb50bb24f"), ); } -// ---- the shared framework, at several payload lengths --------------------------------------- +// ---- the shared frameworks ------------------------------------------------------------------ + +/// The construction suite -- key type, strength and `generate` -- for both modes at every key +/// length. +#[test] +fn the_keys_enforce_the_material_policy() { + let framework = TestFrameworkFromKeyMaterial::new(); + framework.test::<16, AES128Key>(AES128Internal::KW_ALG_NAME, SecurityStrength::_128bit); + framework.test::<24, AES192Key>(AES192Internal::KW_ALG_NAME, SecurityStrength::_192bit); + framework.test::<32, AES256Key>(AES256Internal::KW_ALG_NAME, SecurityStrength::_256bit); + framework.test::<16, AES128Key>(AES128Internal::KWP_ALG_NAME, SecurityStrength::_128bit); + framework.test::<24, AES192Key>(AES192Internal::KWP_ALG_NAME, SecurityStrength::_192bit); + framework.test::<32, AES256Key>(AES256Internal::KWP_ALG_NAME, SecurityStrength::_256bit); +} #[test] fn kw_conforms_to_the_framework() { let tf = TestFrameworkKeyWrap::new(); // AES-128 KEK: 128-, 192-, 256-bit keys and a 64-byte payload (n = 9 semiblocks). - tf.test::<16, 16, 24, Aes128Kw>(); - tf.test::<16, 24, 32, Aes128Kw>(); - tf.test::<16, 32, 40, Aes128Kw>(); - tf.test::<16, 64, 72, Aes128Kw>(); - tf.test::<24, 16, 24, Aes192Kw>(); - tf.test::<24, 32, 40, Aes192Kw>(); - tf.test::<32, 16, 24, Aes256Kw>(); - tf.test::<32, 32, 40, Aes256Kw>(); + tf.test::<16, 16, 24, AES128Key>(); + tf.test::<16, 24, 32, AES128Key>(); + tf.test::<16, 32, 40, AES128Key>(); + tf.test::<16, 64, 72, AES128Key>(); + tf.test::<24, 16, 24, AES192Key>(); + tf.test::<24, 32, 40, AES192Key>(); + tf.test::<32, 16, 24, AES256Key>(); + tf.test::<32, 32, 40, AES256Key>(); } #[test] @@ -155,218 +171,78 @@ fn kwp_conforms_to_the_framework() { let tf = TestFrameworkKeyWrap::new(); // Every padding length from 0 to 7 in the single-block case (1..=8 bytes -> 16), then the // boundary into the wrapping function (9 -> 24), unaligned and aligned longer inputs. - tf.test::<16, 1, 16, Aes128Kwp>(); - tf.test::<16, 2, 16, Aes128Kwp>(); - tf.test::<16, 3, 16, Aes128Kwp>(); - tf.test::<16, 4, 16, Aes128Kwp>(); - tf.test::<16, 5, 16, Aes128Kwp>(); - tf.test::<16, 6, 16, Aes128Kwp>(); - tf.test::<16, 7, 16, Aes128Kwp>(); - tf.test::<16, 8, 16, Aes128Kwp>(); - tf.test::<16, 9, 24, Aes128Kwp>(); - tf.test::<16, 13, 24, Aes128Kwp>(); - tf.test::<16, 16, 24, Aes128Kwp>(); - tf.test::<16, 20, 32, Aes128Kwp>(); - tf.test::<16, 32, 40, Aes128Kwp>(); - tf.test::<16, 100, 112, Aes128Kwp>(); - tf.test::<24, 7, 16, Aes192Kwp>(); - tf.test::<24, 32, 40, Aes192Kwp>(); - tf.test::<32, 7, 16, Aes256Kwp>(); - tf.test::<32, 32, 40, Aes256Kwp>(); -} + tf.test::<16, 1, 16, AES128Key>(); + tf.test::<16, 2, 16, AES128Key>(); + tf.test::<16, 3, 16, AES128Key>(); + tf.test::<16, 4, 16, AES128Key>(); + tf.test::<16, 5, 16, AES128Key>(); + tf.test::<16, 6, 16, AES128Key>(); + tf.test::<16, 7, 16, AES128Key>(); + tf.test::<16, 8, 16, AES128Key>(); + tf.test::<16, 9, 24, AES128Key>(); + tf.test::<16, 13, 24, AES128Key>(); + tf.test::<16, 16, 24, AES128Key>(); + tf.test::<16, 20, 32, AES128Key>(); + tf.test::<16, 32, 40, AES128Key>(); + tf.test::<16, 100, 112, AES128Key>(); + tf.test::<24, 7, 16, AES192Key>(); + tf.test::<24, 32, 40, AES192Key>(); + tf.test::<32, 7, 16, AES256Key>(); + tf.test::<32, 32, 40, AES256Key>(); +} + +// ---- the keys and the aliases --------------------------------------------------------------- -// ---- properties specific to KW and KWP ------------------------------------------------------ - -fn test_kek() -> KeyMaterial<16> { - kek::<16>("000102030405060708090a0b0c0d0e0f") -} - -/// The two integrity check values differ, so a ciphertext produced by one algorithm is rejected -/// by the other even though the lengths line up. +/// The three key lengths are distinct algorithms: the same data under the "same" KEK bytes gives +/// three different ciphertexts. #[test] -fn kw_and_kwp_ciphertexts_are_not_interchangeable() { - let kek = test_kek(); +fn the_three_key_lengths_are_not_interchangeable() { let data = [0x5Au8; 16]; - - let kw_ct: [u8; 24] = Aes128Kw::wrap_key(&kek, &data).unwrap(); - let kwp_ct: [u8; 24] = Aes128Kwp::wrap_key(&kek, &data).unwrap(); - assert_ne!(kw_ct, kwp_ct); - - assert!(matches!( - Aes128Kwp::unwrap_key::<16, 24>(&kek, &kw_ct), - Err(SymmetricCipherError::DecryptionFailed) - )); - assert!(matches!( - Aes128Kw::unwrap_key::<16, 24>(&kek, &kwp_ct), - Err(SymmetricCipherError::DecryptionFailed) - )); -} - -/// SP 800-38F Algorithm 5 step 5: for `len(P) <= 64` bits the ciphertext is `CIPH_K(S)` with -/// `S = ICV2 || [len]32 || P || PAD`, one raw block encryption and nothing else. Pinning that -/// against the permutation directly ties the single-block path to the spec rather than to the -/// RFC vector alone. -#[test] -fn kwp_single_block_path_is_one_raw_block_encryption() { - let kek = test_kek(); - let data = *b"12345"; - - let ct: [u8; 16] = Aes128Kwp::wrap_key(&kek, &data).unwrap(); - - let mut s = [0u8; 16]; - s[..4].copy_from_slice(&[0xA6, 0x59, 0x59, 0xA6]); - s[4..8].copy_from_slice(&(data.len() as u32).to_be_bytes()); - s[8..8 + data.len()].copy_from_slice(&data); - AES128Internal::new(&kek).unwrap().encrypt_block(&mut s); - assert_eq!(ct, s); -} - -/// A KWP ciphertext length fixes the plaintext length only to within 7 bytes; the exact length is -/// the header field. So a 12-byte wrap unwraps fine as 12, and as any other length that shares -/// its ciphertext length is an authenticity failure, as the trait requires. -#[test] -fn kwp_unwrap_key_rejects_a_recovered_length_other_than_key_len() { - let kek = test_kek(); - let ct: [u8; 24] = Aes128Kwp::wrap_key(&kek, &[0x33u8; 12]).unwrap(); - - assert_eq!(*Aes128Kwp::unwrap_key::<12, 24>(&kek, &ct).unwrap(), [0x33u8; 12]); - assert!(matches!( - Aes128Kwp::unwrap_key::<13, 24>(&kek, &ct), - Err(SymmetricCipherError::DecryptionFailed) - )); - assert!(matches!( - Aes128Kwp::unwrap_key::<9, 24>(&kek, &ct), - Err(SymmetricCipherError::DecryptionFailed) - )); - // ... and the run-time API reports the real length. - let mut pt = [0u8; 16]; - assert_eq!(Aes128Kwp::unwrap_out(&kek, &ct, &mut pt).unwrap(), 12); - assert_eq!(pt[..12], [0x33u8; 12]); - assert_eq!(pt[12..], [0u8; 4], "the padding is left as zeros"); -} - -/// Algorithm 6 step 8: padding must be zero. A ciphertext whose padding bytes were anything else -/// is a forgery. Constructed by wrapping through the raw block cipher with non-zero padding, since -/// the wrapper itself never produces one. -#[test] -fn kwp_rejects_non_zero_padding() { - let kek = test_kek(); - let perm = AES128Internal::new(&kek).unwrap(); - - let mut s = [0u8; 16]; - s[..4].copy_from_slice(&[0xA6, 0x59, 0x59, 0xA6]); - s[4..8].copy_from_slice(&5u32.to_be_bytes()); - s[8..13].copy_from_slice(b"12345"); - s[15] = 0x01; // the last pad byte - perm.encrypt_block(&mut s); - - assert!(matches!( - Aes128Kwp::unwrap_key::<5, 16>(&kek, &s), - Err(SymmetricCipherError::DecryptionFailed) - )); -} - -/// Algorithm 6 step 7: `padlen` must be 0 to 7, so a length field that does not fit the -/// ciphertext is a forgery even when the ICV and padding check out. Two cases: a length larger -/// than the padded data, and one that would leave 8 or more bytes of padding. -#[test] -fn kwp_rejects_a_length_field_that_does_not_fit() { - let kek = test_kek(); - let perm = AES128Internal::new(&kek).unwrap(); - - for bad_len in [9u32, 0u32, u32::MAX] { - let mut s = [0u8; 16]; - s[..4].copy_from_slice(&[0xA6, 0x59, 0x59, 0xA6]); - s[4..8].copy_from_slice(&bad_len.to_be_bytes()); - perm.encrypt_block(&mut s); - - let mut pt = [0xFFu8; 8]; - assert!( - matches!( - Aes128Kwp::unwrap_out(&kek, &s, &mut pt), - Err(SymmetricCipherError::DecryptionFailed) - ), - "a length field of {bad_len} should not fit an 8-byte padded plaintext" - ); - assert_eq!(pt, [0u8; 8], "the buffer is scrubbed on failure"); - } + let ct128: [u8; 24] = key128::(&RFC3394_KEK[..32]).wrap_key(&data).unwrap(); + let ct192: [u8; 24] = key192::(&RFC3394_KEK[..48]).wrap_key(&data).unwrap(); + let ct256: [u8; 24] = key256::(RFC3394_KEK).wrap_key(&data).unwrap(); + assert_ne!(ct128, ct192); + assert_ne!(ct192, ct256); + assert_ne!(ct128, ct256); } -/// The length predicates and helpers are SP 800-38F Table 1 exactly. +/// The keys hand out exactly the aliased workers (whose names are pinned in `alg_name_tests`). #[test] -fn the_length_helpers_match_table_1() { - // KW: 2 to 2^54 - 1 semiblocks of plaintext, one more of ciphertext. - assert!(!kw_plaintext_len_is_valid(0)); - assert!(!kw_plaintext_len_is_valid(8)); - assert!(!kw_plaintext_len_is_valid(12)); - assert!(kw_plaintext_len_is_valid(16)); - assert!(kw_plaintext_len_is_valid(24)); - assert!(!kw_ciphertext_len_is_valid(16)); - assert!(kw_ciphertext_len_is_valid(24)); - assert!(!kw_ciphertext_len_is_valid(25)); - assert_eq!(kw_wrapped_len(16), 24); - assert_eq!(kw_wrapped_len(usize::MAX), usize::MAX, "saturates rather than overflowing"); - assert_eq!(Aes128Kw::wrap_out_len(32), 40); - assert_eq!(Aes128Kw::unwrap_out_max_len(40), 32); - assert_eq!(Aes128Kw::unwrap_out_max_len(0), 0); - - // KWP: 1 to 2^32 - 1 octets of plaintext; 2 to 2^29 semiblocks of ciphertext. - assert!(!kwp_plaintext_len_is_valid(0)); - assert!(kwp_plaintext_len_is_valid(1)); - assert!(kwp_plaintext_len_is_valid(u32::MAX as usize)); - assert!(!kwp_plaintext_len_is_valid(u32::MAX as usize + 1)); - assert!(!kwp_ciphertext_len_is_valid(8)); - assert!(kwp_ciphertext_len_is_valid(16)); - assert!(!kwp_ciphertext_len_is_valid(20)); - assert!(kwp_ciphertext_len_is_valid(8 << 29)); - assert!(!kwp_ciphertext_len_is_valid((8 << 29) + 8)); - assert_eq!(kwp_wrapped_len(1), 16); - assert_eq!(kwp_wrapped_len(8), 16); - assert_eq!(kwp_wrapped_len(9), 24); - assert_eq!(kwp_wrapped_len(20), 32); - assert_eq!(kwp_wrapped_len(usize::MAX), usize::MAX, "saturates rather than overflowing"); - assert_eq!(Aes128Kwp::unwrap_out_max_len(32), 24); - - // ... and the run-time API enforces them. - let kek = test_kek(); - let mut buf = [0u8; 64]; - assert!(matches!( - Aes128Kw::wrap_out(&kek, &[0u8; 8], &mut buf), - Err(SymmetricCipherError::InvalidInputLength(_)) - )); - assert!(matches!( - Aes128Kw::wrap_out(&kek, &[0u8; 12], &mut buf), - Err(SymmetricCipherError::InvalidInputLength(_)) - )); - assert!(matches!( - Aes128Kw::unwrap_out(&kek, &[0u8; 16], &mut buf), - Err(SymmetricCipherError::InvalidInputLength(_)) - )); - assert!(matches!( - Aes128Kwp::unwrap_out(&kek, &[0u8; 20], &mut buf), - Err(SymmetricCipherError::InvalidInputLength(_)) - )); +fn the_keys_hand_out_the_aliased_workers() { + let kek = key128::(&RFC3394_KEK[..32]); + let _: AES_KW_128<'_> = kek.create_wrapper(); + let _: AES_KW_128<'_> = kek.create_unwrapper(); + let kek = key192::(&RFC3394_KEK[..48]); + let _: AES_KW_192<'_> = kek.create_wrapper(); + let kek = key256::(RFC3394_KEK); + let _: AES_KW_256<'_> = kek.create_unwrapper(); + let kek = key128::(&RFC3394_KEK[..32]); + let _: AES_KWP_128<'_> = kek.create_wrapper(); + let kek = key192::(&RFC3394_KEK[..48]); + let _: AES_KWP_192<'_> = kek.create_unwrapper(); + let kek = key256::(RFC3394_KEK); + let _: AES_KWP_256<'_> = kek.create_wrapper(); } -/// KW and KWP wrap the same aligned data to different lengths only when it is short enough to -/// fit KWP's single block; otherwise KWP's ciphertext is KW's length, not longer, since the -/// header replaces ICV1 rather than adding to it. +/// The sizes the docs quote: the key is the schedule, the worker is the borrow. #[test] -fn kwp_costs_nothing_extra_for_aligned_data() { - assert_eq!(Aes128Kw::wrap_out_len(16), Aes128Kwp::wrap_out_len(16)); - assert_eq!(Aes128Kw::wrap_out_len(32), Aes128Kwp::wrap_out_len(32)); - assert_eq!(Aes128Kwp::wrap_out_len(8), 16, "and 8 bytes need only one block"); +fn sizes() { + use core::mem::size_of; + assert_eq!( + [size_of::>(), size_of::>(), size_of::>()], + [size_of::(), size_of::(), size_of::()] + ); + assert_eq!(size_of::>(), size_of::>()); + assert_eq!(size_of::(), size_of::<&AES128Internal>()); + assert_eq!(size_of::(), size_of::<&AES256Internal>()); } -/// The three key lengths are distinct algorithms: the same data under the "same" KEK bytes gives -/// three different ciphertexts, and each unwraps only under its own. +/// Equality follows the material: the same bytes compare equal, as does a clone, and different +/// bytes do not. #[test] -fn the_three_key_lengths_are_not_interchangeable() { - let data = [0x5Au8; 16]; - let ct128: [u8; 24] = Aes128Kw::wrap_key(&kek::<16>(&RFC3394_KEK[..32]), &data).unwrap(); - let ct192: [u8; 24] = Aes192Kw::wrap_key(&kek::<24>(&RFC3394_KEK[..48]), &data).unwrap(); - let ct256: [u8; 24] = Aes256Kw::wrap_key(&kek::<32>(RFC3394_KEK), &data).unwrap(); - assert_ne!(ct128, ct192); - assert_ne!(ct192, ct256); - assert_ne!(ct128, ct256); +fn keys_compare_equal_exactly_when_their_material_does() { + let kek = key128::(&RFC3394_KEK[..32]); + assert!(kek == kek.clone()); + assert!(kek == key128::(&RFC3394_KEK[..32])); + assert!(kek != key128::(&RFC3394_KEK[2..34])); } From dc47bf5c10207a4a3c5d3bfeae499c7d67e05d9c Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 27 Oct 2026 03:49:01 +1100 Subject: [PATCH 07/18] cli: the aes*-kw and aes*-kwp commands take -d encrypt|decrypt and build key objects The six commands load `AES*Key` / `AES*Key` through load_key_object and call the key's one-shots. The direction takes the CipherDirection flag every other cipher command takes, so `-d encrypt` wraps and `-d decrypt` unwraps, instead of PR #160's positional wrap|unwrap. The Rust subprocess test becomes cli/tests/test_aes_kw.sh on the shell harness the other commands use, with the same cases: the RFC vectors both ways, framing, the length rules, rejection with no output, and key loading. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/aes_kw_cmd.rs | 110 ++++++------- cli/src/main.rs | 75 +++++---- cli/tests/aes_kw_cli_tests.rs | 293 ---------------------------------- cli/tests/test_aes_kw.sh | 203 +++++++++++++++++++++++ 4 files changed, 293 insertions(+), 388 deletions(-) delete mode 100644 cli/tests/aes_kw_cli_tests.rs create mode 100755 cli/tests/test_aes_kw.sh diff --git a/cli/src/aes_kw_cmd.rs b/cli/src/aes_kw_cmd.rs index c486c5018..44d769322 100644 --- a/cli/src/aes_kw_cmd.rs +++ b/cli/src/aes_kw_cmd.rs @@ -1,7 +1,9 @@ //! AES key wrap (KW, NIST SP 800-38F Sec 6.2 / RFC 3394) and key wrap with padding (KWP, Sec 6.3 -//! / RFC 5649): `aes{128,192,256}-{kw,kwp} wrap|unwrap`, stdin to stdout. +//! / RFC 5649): `aes{128,192,256}-{kw,kwp} -d encrypt|decrypt`, stdin to stdout. `encrypt` wraps +//! and `decrypt` unwraps: the spelling is shared with every other cipher command, and SP 800-38F +//! itself calls the two halves authenticated encryption and decryption (KW-AE, KW-AD). //! -//! Key loading is shared with the other AES commands ([`crate::helpers::block_mode_helpers::load_key`]), +//! Key loading is shared with the other AES commands ([`crate::helpers::block_mode_helpers::load_key_object`]), //! so `--key` / `--key-file` behave identically. Everything else differs from the streaming //! commands, because key wrap is a one-shot algorithm: //! @@ -28,8 +30,9 @@ //! # Unwrap fails closed //! //! Both algorithms are authenticated. A ciphertext that was tampered with, was wrapped under a -//! different KEK, or was produced by the other algorithm (KW vs KWP) makes `unwrap` exit non-zero -//! with nothing written to stdout. It does not say which of those it was; that is deliberate. +//! different KEK, or was produced by the other algorithm (KW vs KWP) makes `decrypt` exit +//! non-zero with nothing written to stdout. It does not say which of those it was; that is +//! deliberate. //! //! # Binary in, binary out //! @@ -38,124 +41,107 @@ //! //! ```text //! echo -n 00112233445566778899aabbccddeeff | bc-rust hex-decode \ -//! | bc-rust aes128-kw wrap --key 000102030405060708090a0b0c0d0e0f -x +//! | bc-rust aes128-kw -d encrypt --key 000102030405060708090a0b0c0d0e0f -x //! ``` -use crate::helpers::block_mode_helpers::load_key; +use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object}; use crate::helpers::write_bytes_or_hex; -use bouncycastle::aes::{ - AES_KW_128, AES_KW_192, AES_KW_256, AES_KWP_128, AES_KWP_192, AES_KWP_256, -}; +use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::cipher::modes::{KW, KWP}; use bouncycastle::core::errors::SymmetricCipherError; -use bouncycastle::core::key_material::KeyMaterial; -use bouncycastle::core::traits::{KeyUnwrapper, KeyWrapper}; -use clap::ValueEnum; +use bouncycastle::core::symmetric_key::{KeyUnwrappingKey, KeyWrappingKey}; use std::io::Read; use std::process::exit; -/// Which direction to run. -#[derive(ValueEnum, Clone, Debug)] -pub(crate) enum KeyWrapAction { - /// Wrap stdin under the KEK and write the ciphertext to stdout. - Wrap, - /// Unwrap stdin under the KEK and write the recovered data to stdout, or exit non-zero if - /// the ciphertext is not authentic. - Unwrap, -} - pub(crate) fn aes128_kw_cmd( - action: &KeyWrapAction, + direction: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, ) { - run::( - action, - &load_key::<16>(key, key_file, "AES-128-KW"), + run::<_, 16>( + direction, + &load_key_object::, 16>(key, key_file, "AES-128-KW"), output_hex, "AES-128-KW", ); } pub(crate) fn aes192_kw_cmd( - action: &KeyWrapAction, + direction: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, ) { - run::( - action, - &load_key::<24>(key, key_file, "AES-192-KW"), + run::<_, 24>( + direction, + &load_key_object::, 24>(key, key_file, "AES-192-KW"), output_hex, "AES-192-KW", ); } pub(crate) fn aes256_kw_cmd( - action: &KeyWrapAction, + direction: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, ) { - run::( - action, - &load_key::<32>(key, key_file, "AES-256-KW"), + run::<_, 32>( + direction, + &load_key_object::, 32>(key, key_file, "AES-256-KW"), output_hex, "AES-256-KW", ); } pub(crate) fn aes128_kwp_cmd( - action: &KeyWrapAction, + direction: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, ) { - run::( - action, - &load_key::<16>(key, key_file, "AES-128-KWP"), + run::<_, 16>( + direction, + &load_key_object::, 16>(key, key_file, "AES-128-KWP"), output_hex, "AES-128-KWP", ); } pub(crate) fn aes192_kwp_cmd( - action: &KeyWrapAction, + direction: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, ) { - run::( - action, - &load_key::<24>(key, key_file, "AES-192-KWP"), + run::<_, 24>( + direction, + &load_key_object::, 24>(key, key_file, "AES-192-KWP"), output_hex, "AES-192-KWP", ); } pub(crate) fn aes256_kwp_cmd( - action: &KeyWrapAction, + direction: &CipherDirection, key: &Option, key_file: &Option, output_hex: bool, ) { - run::( - action, - &load_key::<32>(key, key_file, "AES-256-KWP"), + run::<_, 32>( + direction, + &load_key_object::, 32>(key, key_file, "AES-256-KWP"), output_hex, "AES-256-KWP", ); } -/// Reads all of stdin, wraps or unwraps it under `kek` with the algorithm `W`, and writes the -/// result. `alg` names the algorithm in error messages. -fn run( - action: &KeyWrapAction, - kek: &KeyMaterial, - output_hex: bool, - alg: &str, -) where - W: KeyWrapper + KeyUnwrapper, +/// Reads all of stdin, wraps or unwraps it under `kek`, and writes the result. `alg` names the +/// algorithm in error messages. +fn run(direction: &CipherDirection, kek: &K, output_hex: bool, alg: &str) +where + K: KeyWrappingKey + KeyUnwrappingKey, { let mut input = Vec::new(); if let Err(e) = std::io::stdin().read_to_end(&mut input) { @@ -163,14 +149,14 @@ fn run( exit(-1); } - let (result, mut output) = match action { - KeyWrapAction::Wrap => { - let mut output = vec![0u8; W::wrap_out_len(input.len())]; - (W::wrap_out(kek, &input, &mut output), output) + let (result, mut output) = match direction { + CipherDirection::Encrypt => { + let mut output = vec![0u8; kek.wrap_out_len(input.len())]; + (kek.wrap_out(&input, &mut output), output) } - KeyWrapAction::Unwrap => { - let mut output = vec![0u8; W::unwrap_out_max_len(input.len())]; - (W::unwrap_out(kek, &input, &mut output), output) + CipherDirection::Decrypt => { + let mut output = vec![0u8; kek.unwrap_out_max_len(input.len())]; + (kek.unwrap_out(&input, &mut output), output) } }; diff --git a/cli/src/main.rs b/cli/src/main.rs index 055d898d6..44a9bfab4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2183,16 +2183,17 @@ enum Subcommands { x: bool, }, - /// AES-128 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `wrap` or `unwrap` stdin to stdout. + /// AES-128 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `-d encrypt` wraps stdin to stdout, + /// `-d decrypt` unwraps it. /// /// Wraps a key -- or any data that is a whole number of 8-byte blocks, at least 16 bytes -- /// under the given key-encryption key (KEK). The output is 8 bytes longer than the input. - /// There is NO IV: wrapping is deterministic, and nothing is prepended on `wrap` or consumed - /// on `unwrap`. + /// There is NO IV: wrapping is deterministic, and nothing is prepended on `encrypt` or + /// consumed on `decrypt`. /// - /// `unwrap` is authenticated. A ciphertext that was tampered with, wrapped under a different + /// Unwrapping is authenticated. A ciphertext that was tampered with, wrapped under a different /// KEK, or produced by aes*-kwp makes the command exit non-zero with nothing written. Input to - /// `unwrap` must be a whole number of 8-byte blocks, at least 24 bytes. + /// `decrypt` must be a whole number of 8-byte blocks, at least 24 bytes. /// /// This command is not streaming: all of stdin is read before anything is written, because /// the algorithm makes six passes over the whole input. For data that is not a multiple of @@ -2201,7 +2202,8 @@ enum Subcommands { /// Note: in production uses, secrets should not be passed on the command-line because they get /// logged in shell history. Use the file-based input instead. AES128_KW { - action: aes_kw_cmd::KeyWrapAction, + #[arg(short, long)] + direction: CipherDirection, /// The 16-byte AES key-encryption key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. @@ -2218,12 +2220,14 @@ enum Subcommands { x: bool, }, - /// AES-192 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `wrap` or `unwrap` stdin to stdout. + /// AES-192 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `-d encrypt` wraps stdin to stdout, + /// `-d decrypt` unwraps it. /// /// See `aes128-kw` for the input-length rules, the absence of an IV and the non-streaming /// note; only the key length differs. AES192_KW { - action: aes_kw_cmd::KeyWrapAction, + #[arg(short, long)] + direction: CipherDirection, /// The 24-byte AES key-encryption key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. @@ -2240,12 +2244,14 @@ enum Subcommands { x: bool, }, - /// AES-256 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `wrap` or `unwrap` stdin to stdout. + /// AES-256 Key Wrap (NIST SP 800-38F Sec 6.2, RFC 3394): `-d encrypt` wraps stdin to stdout, + /// `-d decrypt` unwraps it. /// /// See `aes128-kw` for the input-length rules, the absence of an IV and the non-streaming /// note; only the key length differs. AES256_KW { - action: aes_kw_cmd::KeyWrapAction, + #[arg(short, long)] + direction: CipherDirection, /// The 32-byte AES key-encryption key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. @@ -2262,18 +2268,19 @@ enum Subcommands { x: bool, }, - /// AES-128 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `wrap` or `unwrap` - /// stdin to stdout. + /// AES-128 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `-d encrypt` wraps + /// stdin to stdout, `-d decrypt` unwraps it. /// /// As aes128-kw, but for data of any length from 1 byte up: the input is padded to a whole /// number of 8-byte blocks and the output is that length plus 8. Its ciphertexts are not - /// interchangeable with aes128-kw's. Input to `unwrap` must be a whole number of 8-byte + /// interchangeable with aes128-kw's. Input to `decrypt` must be a whole number of 8-byte /// blocks, at least 16 bytes. /// - /// See `aes128-kw` for the absence of an IV, the authenticated `unwrap` and the non-streaming + /// See `aes128-kw` for the absence of an IV, the authenticated `decrypt` and the non-streaming /// note. AES128_KWP { - action: aes_kw_cmd::KeyWrapAction, + #[arg(short, long)] + direction: CipherDirection, /// The 16-byte AES key-encryption key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. @@ -2290,12 +2297,13 @@ enum Subcommands { x: bool, }, - /// AES-192 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `wrap` or `unwrap` - /// stdin to stdout. + /// AES-192 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `-d encrypt` wraps + /// stdin to stdout, `-d decrypt` unwraps it. /// /// See `aes128-kwp`; only the key length differs. AES192_KWP { - action: aes_kw_cmd::KeyWrapAction, + #[arg(short, long)] + direction: CipherDirection, /// The 24-byte AES key-encryption key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. @@ -2312,12 +2320,13 @@ enum Subcommands { x: bool, }, - /// AES-256 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `wrap` or `unwrap` - /// stdin to stdout. + /// AES-256 Key Wrap with Padding (NIST SP 800-38F Sec 6.3, RFC 5649): `-d encrypt` wraps + /// stdin to stdout, `-d decrypt` unwraps it. /// /// See `aes128-kwp`; only the key length differs. AES256_KWP { - action: aes_kw_cmd::KeyWrapAction, + #[arg(short, long)] + direction: CipherDirection, /// The 32-byte AES key-encryption key in hex. /// The `key_file` option is preferred to avoid leaving key material in command history. @@ -3160,23 +3169,23 @@ fn run() { mldsa_cmd::Files { ctx: ctxfile, sk: skfile, pk: pkfile, sig: sigfile, mu: mufile }; mldsa_cmd::mldsa_cmd(&mldsa_cmd::MLDSA87_SET, action, &files, None, *deterministic, *x); } - Some(Subcommands::AES128_KW { action, key, key_file, x }) => { - aes_kw_cmd::aes128_kw_cmd(action, key, key_file, *x); + Some(Subcommands::AES128_KW { direction, key, key_file, x }) => { + aes_kw_cmd::aes128_kw_cmd(direction, key, key_file, *x); } - Some(Subcommands::AES192_KW { action, key, key_file, x }) => { - aes_kw_cmd::aes192_kw_cmd(action, key, key_file, *x); + Some(Subcommands::AES192_KW { direction, key, key_file, x }) => { + aes_kw_cmd::aes192_kw_cmd(direction, key, key_file, *x); } - Some(Subcommands::AES256_KW { action, key, key_file, x }) => { - aes_kw_cmd::aes256_kw_cmd(action, key, key_file, *x); + Some(Subcommands::AES256_KW { direction, key, key_file, x }) => { + aes_kw_cmd::aes256_kw_cmd(direction, key, key_file, *x); } - Some(Subcommands::AES128_KWP { action, key, key_file, x }) => { - aes_kw_cmd::aes128_kwp_cmd(action, key, key_file, *x); + Some(Subcommands::AES128_KWP { direction, key, key_file, x }) => { + aes_kw_cmd::aes128_kwp_cmd(direction, key, key_file, *x); } - Some(Subcommands::AES192_KWP { action, key, key_file, x }) => { - aes_kw_cmd::aes192_kwp_cmd(action, key, key_file, *x); + Some(Subcommands::AES192_KWP { direction, key, key_file, x }) => { + aes_kw_cmd::aes192_kwp_cmd(direction, key, key_file, *x); } - Some(Subcommands::AES256_KWP { action, key, key_file, x }) => { - aes_kw_cmd::aes256_kwp_cmd(action, key, key_file, *x); + Some(Subcommands::AES256_KWP { direction, key, key_file, x }) => { + aes_kw_cmd::aes256_kwp_cmd(direction, key, key_file, *x); } Some(Subcommands::MLKEM512 { action, skfile, pkfile, ctfile, x }) => { mlkem_cmd::mlkem_cmd(&mlkem_cmd::MLKEM512_SET, action, skfile, pkfile, ctfile, *x); diff --git a/cli/tests/aes_kw_cli_tests.rs b/cli/tests/aes_kw_cli_tests.rs deleted file mode 100644 index 64f4c0268..000000000 --- a/cli/tests/aes_kw_cli_tests.rs +++ /dev/null @@ -1,293 +0,0 @@ -//! Tests for the `aes{128,192,256}-kw` and `aes{128,192,256}-kwp` subcommands. -//! -//! These drive the built `bc-rust` binary as a subprocess, because what is worth testing is the -//! command-line contract: the `wrap` / `unwrap` spelling, no IV framing, whole-input reading, the -//! input-length rules, exit codes on a rejected ciphertext, and key loading. The algorithms are -//! tested in `bouncycastle-modes` against the RFC and ACVP vectors; the same RFC vectors are -//! replayed here in both directions, which key wrap's determinism makes possible. -//! -//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the -//! current profile, so there is nothing to build or locate by hand. - -use std::io::{ErrorKind, Write}; -use std::process::{Command, Output, Stdio}; -use std::thread; - -/// The path to the binary under test, resolved by cargo. -const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); - -/// RFC 3394 Sec 4: every example uses these KEK bytes (truncated to the key length) and this key -/// data (truncated to the data length). -const RFC3394_KEK: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; -const RFC3394_KEY_DATA: &str = "00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f"; -/// Sec 4.1: 128 bits of key data, 128-bit KEK. -const RFC3394_4_1_CT: &str = "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5"; -/// Sec 4.4: 192 bits of key data, 192-bit KEK. -const RFC3394_4_4_CT: &str = "031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2"; -/// Sec 4.6: 256 bits of key data, 256-bit KEK. -const RFC3394_4_6_CT: &str = - "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21"; - -/// RFC 5649 Sec 6: both examples use a 192-bit KEK. -const RFC5649_KEK: &str = "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"; -const RFC5649_KEY_20: &str = "c37b7e6492584340bed12207808941155068f738"; -const RFC5649_CT_20: &str = "138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a"; -const RFC5649_KEY_7: &str = "466f7250617369"; -const RFC5649_CT_7: &str = "afbeb0f07dfbf5419200f2ccb50bb24f"; - -/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. -/// -/// stdin is written from a separate thread so that a large payload cannot deadlock against a -/// full stdout pipe; see the same helper in `aes_ecb_cli_tests.rs` for the full reasoning. A -/// broken pipe on write is expected when the child exits early on an error path. -fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { - let mut child = Command::new(BC_RUST) - .args(args) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("failed to spawn bc-rust"); - - let mut stdin = child.stdin.take().expect("stdin piped"); - let payload = stdin_bytes.to_vec(); - let writer = thread::spawn(move || { - match stdin.write_all(&payload) { - Ok(()) => {} - Err(e) if e.kind() == ErrorKind::BrokenPipe => {} - Err(e) => panic!("failed to write to stdin: {e}"), - } - // `stdin` drops here, closing the pipe so the child sees EOF and can exit. - }); - - let output = child.wait_with_output().expect("failed to wait for bc-rust"); - writer.join().expect("the stdin writer thread panicked"); - output -} - -/// Runs a command that is expected to succeed, returning stdout. -fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { - let out = run(args, stdin_bytes); - assert!( - out.status.success(), - "expected success from {args:?}, got {:?}\nstderr: {}", - out.status, - String::from_utf8_lossy(&out.stderr) - ); - out.stdout -} - -/// Runs a command that is expected to fail, returning stderr as a string and asserting that -/// nothing was written to stdout. -fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { - let out = run(args, stdin_bytes); - assert!( - !out.status.success(), - "expected failure from {args:?}, but it succeeded\nstdout: {:?}", - String::from_utf8_lossy(&out.stdout) - ); - assert!(out.stdout.is_empty(), "a failed command must write nothing to stdout"); - String::from_utf8_lossy(&out.stderr).into_owned() -} - -fn unhex(s: &str) -> Vec { - assert!(s.len().is_multiple_of(2), "hex string must have even length"); - (0..s.len()) - .step_by(2) - .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) - .collect() -} - -fn tohex(bytes: &[u8]) -> String { - bytes.iter().map(|b| format!("{b:02x}")).collect() -} - -// ---- the RFC vectors, both directions ------------------------------------------------------ - -#[test] -fn aes128_kw_matches_rfc3394_4_1_in_both_directions() { - let kek = &RFC3394_KEK[..32]; - let key_data = unhex(&RFC3394_KEY_DATA[..32]); - - let wrapped = run_ok(&["aes128-kw", "wrap", "--key", kek], &key_data); - assert_eq!(tohex(&wrapped), RFC3394_4_1_CT); - - let unwrapped = run_ok(&["aes128-kw", "unwrap", "--key", kek], &wrapped); - assert_eq!(unwrapped, key_data); -} - -#[test] -fn aes192_kw_matches_rfc3394_4_4_in_both_directions() { - let kek = &RFC3394_KEK[..48]; - let key_data = unhex(&RFC3394_KEY_DATA[..48]); - - let wrapped = run_ok(&["aes192-kw", "wrap", "--key", kek], &key_data); - assert_eq!(tohex(&wrapped), RFC3394_4_4_CT); - assert_eq!(run_ok(&["aes192-kw", "unwrap", "--key", kek], &wrapped), key_data); -} - -#[test] -fn aes256_kw_matches_rfc3394_4_6_in_both_directions() { - let key_data = unhex(RFC3394_KEY_DATA); - - let wrapped = run_ok(&["aes256-kw", "wrap", "--key", RFC3394_KEK], &key_data); - assert_eq!(tohex(&wrapped), RFC3394_4_6_CT); - assert_eq!(run_ok(&["aes256-kw", "unwrap", "--key", RFC3394_KEK], &wrapped), key_data); -} - -#[test] -fn aes192_kwp_matches_rfc5649_in_both_directions() { - // 20 octets: padded to 24, through the wrapping function. - let key_20 = unhex(RFC5649_KEY_20); - let wrapped = run_ok(&["aes192-kwp", "wrap", "--key", RFC5649_KEK], &key_20); - assert_eq!(tohex(&wrapped), RFC5649_CT_20); - assert_eq!(run_ok(&["aes192-kwp", "unwrap", "--key", RFC5649_KEK], &wrapped), key_20); - - // 7 octets: a single block. The unwrapped output is exactly 7 bytes, not the padded 8. - let key_7 = unhex(RFC5649_KEY_7); - let wrapped = run_ok(&["aes192-kwp", "wrap", "--key", RFC5649_KEK], &key_7); - assert_eq!(tohex(&wrapped), RFC5649_CT_7); - assert_eq!(run_ok(&["aes192-kwp", "unwrap", "--key", RFC5649_KEK], &wrapped), key_7); -} - -// ---- the command-line contract ------------------------------------------------------------- - -#[test] -fn hex_output_matches_binary_output() { - let key_data = unhex(&RFC3394_KEY_DATA[..32]); - let binary = run_ok(&["aes128-kw", "wrap", "--key", &RFC3394_KEK[..32]], &key_data); - let hex = run_ok(&["aes128-kw", "wrap", "--key", &RFC3394_KEK[..32], "-x"], &key_data); - assert_eq!(String::from_utf8(hex).unwrap(), tohex(&binary)); -} - -#[test] -fn wrapping_is_deterministic_and_adds_exactly_one_semiblock() { - let kek = &RFC3394_KEK[..32]; - let key_data = [0x5Au8; 32]; - let a = run_ok(&["aes128-kw", "wrap", "--key", kek], &key_data); - let b = run_ok(&["aes128-kw", "wrap", "--key", kek], &key_data); - assert_eq!(a, b, "no IV, so the same input always gives the same output"); - assert_eq!(a.len(), key_data.len() + 8); - - let a = run_ok(&["aes128-kwp", "wrap", "--key", kek], &key_data); - assert_eq!(a.len(), key_data.len() + 8, "aligned data costs KWP nothing extra"); - let a = run_ok(&["aes128-kwp", "wrap", "--key", kek], &key_data[..13]); - assert_eq!(a.len(), 16 + 8, "13 bytes pad to 16"); -} - -#[test] -fn a_tampered_ciphertext_is_rejected_with_no_output() { - let kek = &RFC3394_KEK[..32]; - let mut wrapped = unhex(RFC3394_4_1_CT); - wrapped[11] ^= 0x01; - let err = run_err(&["aes128-kw", "unwrap", "--key", kek], &wrapped); - assert!(err.contains("not an authentic"), "stderr should explain: {err}"); - - let mut wrapped = unhex(RFC5649_CT_7); - wrapped[0] ^= 0x80; - let err = run_err(&["aes192-kwp", "unwrap", "--key", RFC5649_KEK], &wrapped); - assert!(err.contains("not an authentic"), "stderr should explain: {err}"); -} - -#[test] -fn the_wrong_kek_is_rejected() { - let wrapped = unhex(RFC3394_4_1_CT); - let wrong_kek = "0f0e0d0c0b0a09080706050403020100"; - let err = run_err(&["aes128-kw", "unwrap", "--key", wrong_kek], &wrapped); - assert!(err.contains("not an authentic"), "stderr should explain: {err}"); -} - -#[test] -fn kw_and_kwp_are_not_interchangeable() { - let kek = &RFC3394_KEK[..32]; - let key_data = [0x5Au8; 16]; - let kw = run_ok(&["aes128-kw", "wrap", "--key", kek], &key_data); - let kwp = run_ok(&["aes128-kwp", "wrap", "--key", kek], &key_data); - assert_ne!(kw, kwp); - run_err(&["aes128-kwp", "unwrap", "--key", kek], &kw); - run_err(&["aes128-kw", "unwrap", "--key", kek], &kwp); -} - -#[test] -fn kw_rejects_inputs_that_are_not_whole_semiblocks() { - let kek = &RFC3394_KEK[..32]; - // wrap: 8 bytes is one semiblock (two are needed); 20 is not a whole number of them. - let err = run_err(&["aes128-kw", "wrap", "--key", kek], &[0u8; 8]); - assert!(err.contains("8 bytes rejected"), "stderr should name the length: {err}"); - let err = run_err(&["aes128-kw", "wrap", "--key", kek], &[0u8; 20]); - assert!(err.contains("20 bytes rejected"), "stderr should name the length: {err}"); - // unwrap: 16 bytes is two semiblocks (three are needed). - let err = run_err(&["aes128-kw", "unwrap", "--key", kek], &[0u8; 16]); - assert!(err.contains("16 bytes rejected"), "stderr should name the length: {err}"); - // KWP takes the unaligned length KW refused. - assert_eq!(run_ok(&["aes128-kwp", "wrap", "--key", kek], &[0u8; 20]).len(), 32); -} - -#[test] -fn empty_input_is_rejected_by_both() { - let kek = &RFC3394_KEK[..32]; - let err = run_err(&["aes128-kw", "wrap", "--key", kek], &[]); - assert!(err.contains("0 bytes rejected"), "stderr should name the length: {err}"); - let err = run_err(&["aes128-kwp", "wrap", "--key", kek], &[]); - assert!(err.contains("0 bytes rejected"), "stderr should name the length: {err}"); - let err = run_err(&["aes128-kwp", "unwrap", "--key", kek], &[]); - assert!(err.contains("0 bytes rejected"), "stderr should name the length: {err}"); -} - -#[test] -fn a_larger_payload_round_trips_through_kwp() { - let kek = &RFC3394_KEK[..32]; - // An "encoded private key" sized payload that is not a multiple of 8. - let payload: Vec = - (0..3001u32).map(|i| (i.wrapping_mul(2_654_435_761) >> 24) as u8).collect(); - let wrapped = run_ok(&["aes256-kwp", "wrap", "--key", RFC3394_KEK], &payload); - assert_eq!(wrapped.len(), 3008 + 8); - assert_eq!(run_ok(&["aes256-kwp", "unwrap", "--key", RFC3394_KEK], &wrapped), payload); - // ... and KW refuses it, because it is not aligned. - run_err(&["aes256-kw", "wrap", "--key", kek], &payload); -} - -#[test] -fn key_file_accepts_hex_and_binary() { - let dir = std::env::temp_dir().join(format!("bc-rust-kw-cli-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let hex_path = dir.join("kek.hex"); - let bin_path = dir.join("kek.bin"); - std::fs::write(&hex_path, &RFC3394_KEK[..32]).unwrap(); - std::fs::write(&bin_path, unhex(&RFC3394_KEK[..32])).unwrap(); - - let key_data = unhex(&RFC3394_KEY_DATA[..32]); - let from_hex = - run_ok(&["aes128-kw", "wrap", "--key-file", hex_path.to_str().unwrap()], &key_data); - let from_bin = - run_ok(&["aes128-kw", "wrap", "--key-file", bin_path.to_str().unwrap()], &key_data); - assert_eq!(tohex(&from_hex), RFC3394_4_1_CT); - assert_eq!(from_hex, from_bin); - - std::fs::remove_dir_all(&dir).unwrap(); -} - -#[test] -fn a_key_of_the_wrong_length_is_rejected() { - let err = run_err(&["aes128-kw", "wrap", "--key", &RFC3394_KEK[..48]], &[0u8; 16]); - assert!(err.contains("16-byte key"), "stderr should say what was expected: {err}"); - let err = run_err(&["aes256-kwp", "wrap", "--key", &RFC3394_KEK[..32]], &[0u8; 16]); - assert!(err.contains("32-byte key"), "stderr should say what was expected: {err}"); -} - -#[test] -fn a_missing_key_is_rejected() { - let err = run_err(&["aes128-kw", "wrap"], &[0u8; 16]); - assert!(err.contains("--key"), "stderr should point at the option: {err}"); -} - -#[test] -fn the_subcommands_are_listed_in_help() { - let help = String::from_utf8(run_ok(&["--help"], &[])).unwrap(); - for name in ["aes128-kw", "aes192-kw", "aes256-kw", "aes128-kwp", "aes192-kwp", "aes256-kwp"] { - assert!(help.contains(name), "{name} should be listed in --help"); - } - let help = String::from_utf8(run_ok(&["aes128-kw", "--help"], &[])).unwrap(); - assert!(help.contains("wrap") && help.contains("unwrap")); - assert!(help.contains("RFC 3394")); -} diff --git a/cli/tests/test_aes_kw.sh b/cli/tests/test_aes_kw.sh new file mode 100755 index 000000000..d03cc7e39 --- /dev/null +++ b/cli/tests/test_aes_kw.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +# The aes{128,192,256}-kw and aes{128,192,256}-kwp subcommands, end to end through the binary. +# +# What is worth testing here is the command-line contract: `-d encrypt` wraps and `-d decrypt` +# unwraps, no IV framing, whole-input reading, the input-length rules, exit codes on a rejected +# ciphertext, and key loading. The algorithms are tested in the library against the RFC and ACVP +# vectors; the RFC vectors are replayed here in both directions, which key wrap's determinism +# makes possible. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# RFC 3394 Sec 4: every example uses these KEK bytes (truncated to the key length) and this key +# data (truncated to the data length). +RFC3394_KEK=000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f +RFC3394_KEY_DATA=00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f +# Sec 4.1: 128 bits of key data, 128-bit KEK. +RFC3394_4_1_CT=1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5 +# Sec 4.4: 192 bits of key data, 192-bit KEK. +RFC3394_4_4_CT=031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2 +# Sec 4.6: 256 bits of key data, 256-bit KEK. +RFC3394_4_6_CT=28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21 + +# RFC 5649 Sec 6: both examples use a 192-bit KEK. +RFC5649_KEK=5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8 +RFC5649_KEY_20=c37b7e6492584340bed12207808941155068f738 +RFC5649_CT_20=138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a +RFC5649_KEY_7=466f7250617369 +RFC5649_CT_7=afbeb0f07dfbf5419200f2ccb50bb24f + +KEK128=${RFC3394_KEK:0:32} +KEK192=${RFC3394_KEK:0:48} + +# `wrap SUB KEK` / `unwrap SUB KEK` run one direction of SUB with the hex KEK, stdin to stdout. +wrap() { "$BC_RUST" "$1" -d encrypt --key "$2"; } +unwrap() { "$BC_RUST" "$1" -d decrypt --key "$2"; } + +# ---- the RFC vectors, both directions ------------------------------------------------------ + +test_aes128_kw_matches_rfc3394_4_1_in_both_directions() { + local got + got=$(unhex "${RFC3394_KEY_DATA:0:32}" | wrap aes128-kw "$KEK128" | "$BC_RUST" hex-encode) + assert_eq "$got" "$RFC3394_4_1_CT" "4.1 wrap" + got=$(unhex "$RFC3394_4_1_CT" | unwrap aes128-kw "$KEK128" | "$BC_RUST" hex-encode) + assert_eq "$got" "${RFC3394_KEY_DATA:0:32}" "4.1 unwrap" +} + +test_aes192_kw_matches_rfc3394_4_4_in_both_directions() { + local got + got=$(unhex "${RFC3394_KEY_DATA:0:48}" | wrap aes192-kw "$KEK192" | "$BC_RUST" hex-encode) + assert_eq "$got" "$RFC3394_4_4_CT" "4.4 wrap" + got=$(unhex "$RFC3394_4_4_CT" | unwrap aes192-kw "$KEK192" | "$BC_RUST" hex-encode) + assert_eq "$got" "${RFC3394_KEY_DATA:0:48}" "4.4 unwrap" +} + +test_aes256_kw_matches_rfc3394_4_6_in_both_directions() { + local got + got=$(unhex "$RFC3394_KEY_DATA" | wrap aes256-kw "$RFC3394_KEK" | "$BC_RUST" hex-encode) + assert_eq "$got" "$RFC3394_4_6_CT" "4.6 wrap" + got=$(unhex "$RFC3394_4_6_CT" | unwrap aes256-kw "$RFC3394_KEK" | "$BC_RUST" hex-encode) + assert_eq "$got" "$RFC3394_KEY_DATA" "4.6 unwrap" +} + +test_aes192_kwp_matches_rfc5649_in_both_directions() { + local got + # 20 octets: padded to 24, through the wrapping function. + got=$(unhex "$RFC5649_KEY_20" | wrap aes192-kwp "$RFC5649_KEK" | "$BC_RUST" hex-encode) + assert_eq "$got" "$RFC5649_CT_20" "20-octet wrap" + got=$(unhex "$RFC5649_CT_20" | unwrap aes192-kwp "$RFC5649_KEK" | "$BC_RUST" hex-encode) + assert_eq "$got" "$RFC5649_KEY_20" "20-octet unwrap" + # 7 octets: a single block. The unwrapped output is exactly 7 bytes, not the padded 8. + got=$(unhex "$RFC5649_KEY_7" | wrap aes192-kwp "$RFC5649_KEK" | "$BC_RUST" hex-encode) + assert_eq "$got" "$RFC5649_CT_7" "7-octet wrap" + got=$(unhex "$RFC5649_CT_7" | unwrap aes192-kwp "$RFC5649_KEK" | "$BC_RUST" hex-encode) + assert_eq "$got" "$RFC5649_KEY_7" "7-octet unwrap" +} + +# ---- the command-line contract ------------------------------------------------------------- + +test_hex_output_matches_binary_output() { + local hex_out + unhex "${RFC3394_KEY_DATA:0:32}" >"$TMP/pt" + wrap aes128-kw "$KEK128" <"$TMP/pt" >"$TMP/ct" + hex_out=$("$BC_RUST" aes128-kw -d encrypt --key "$KEK128" -x <"$TMP/pt") + assert_eq "$hex_out" "$(hex "$TMP/ct")" "-x must be the hex of the binary output" + assert_eq "$hex_out" "$RFC3394_4_1_CT" "and both are the 4.1 ciphertext" +} + +test_wrapping_is_deterministic_and_adds_exactly_one_semiblock() { + rng 32 >"$TMP/key_data" + wrap aes128-kw "$KEK128" <"$TMP/key_data" >"$TMP/a" + wrap aes128-kw "$KEK128" <"$TMP/key_data" >"$TMP/b" + assert_same "$TMP/a" "$TMP/b" "no IV, so the same input always gives the same output" + assert_size "$TMP/a" 40 "KW adds one semiblock" + + wrap aes128-kwp "$KEK128" <"$TMP/key_data" >"$TMP/a" + assert_size "$TMP/a" 40 "aligned data costs KWP nothing extra" + head -c 13 "$TMP/key_data" | wrap aes128-kwp "$KEK128" >"$TMP/a" + assert_size "$TMP/a" 24 "13 bytes pad to 16, plus the header" +} + +test_a_tampered_ciphertext_is_rejected_with_no_output() { + unhex "$RFC3394_4_1_CT" >"$TMP/ct" + flip_byte "$TMP/ct" 11 + expect_fail "a modified KW ciphertext" unwrap aes128-kw "$KEK128" <"$TMP/ct" + assert_stderr_has "not an authentic" + + unhex "$RFC5649_CT_7" >"$TMP/ct" + flip_byte "$TMP/ct" 0 128 + expect_fail "a modified KWP ciphertext" unwrap aes192-kwp "$RFC5649_KEK" <"$TMP/ct" + assert_stderr_has "not an authentic" +} + +test_the_wrong_kek_is_rejected() { + unhex "$RFC3394_4_1_CT" >"$TMP/ct" + expect_fail "the wrong KEK" unwrap aes128-kw 0f0e0d0c0b0a09080706050403020100 <"$TMP/ct" + assert_stderr_has "not an authentic" +} + +test_kw_and_kwp_are_not_interchangeable() { + rng 16 >"$TMP/key_data" + wrap aes128-kw "$KEK128" <"$TMP/key_data" >"$TMP/kw" + wrap aes128-kwp "$KEK128" <"$TMP/key_data" >"$TMP/kwp" + assert_differs "$TMP/kw" "$TMP/kwp" "the integrity check values differ" + expect_fail "KWP must not unwrap a KW ciphertext" unwrap aes128-kwp "$KEK128" <"$TMP/kw" + expect_fail "KW must not unwrap a KWP ciphertext" unwrap aes128-kw "$KEK128" <"$TMP/kwp" +} + +test_kw_rejects_inputs_that_are_not_whole_semiblocks() { + # wrap: 8 bytes is one semiblock (two are needed); 20 is not a whole number of them. + rng 8 >"$TMP/short" + expect_fail "8 bytes" wrap aes128-kw "$KEK128" <"$TMP/short" + assert_stderr_has "8 bytes rejected" + rng 20 >"$TMP/unaligned" + expect_fail "20 bytes" wrap aes128-kw "$KEK128" <"$TMP/unaligned" + assert_stderr_has "20 bytes rejected" + # unwrap: 16 bytes is two semiblocks (three are needed). + rng 16 >"$TMP/two" + expect_fail "16 bytes" unwrap aes128-kw "$KEK128" <"$TMP/two" + assert_stderr_has "16 bytes rejected" + # KWP takes the unaligned length KW refused. + wrap aes128-kwp "$KEK128" <"$TMP/unaligned" >"$TMP/ct" + assert_size "$TMP/ct" 32 "20 bytes pad to 24, plus the header" +} + +test_empty_input_is_rejected_by_both() { + : >"$TMP/empty" + expect_fail "KW wrap of nothing" wrap aes128-kw "$KEK128" <"$TMP/empty" + assert_stderr_has "0 bytes rejected" + expect_fail "KWP wrap of nothing" wrap aes128-kwp "$KEK128" <"$TMP/empty" + assert_stderr_has "0 bytes rejected" + expect_fail "KWP unwrap of nothing" unwrap aes128-kwp "$KEK128" <"$TMP/empty" + assert_stderr_has "0 bytes rejected" +} + +test_a_larger_payload_round_trips_through_kwp() { + # An "encoded private key" sized payload that is not a multiple of 8. + rng 3001 >"$TMP/payload" + wrap aes256-kwp "$RFC3394_KEK" <"$TMP/payload" >"$TMP/ct" + assert_size "$TMP/ct" $((3008 + 8)) "3001 bytes pad to 3008, plus the header" + unwrap aes256-kwp "$RFC3394_KEK" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/payload" "$TMP/rec" "KWP must recover the payload" + # ... and KW refuses it, because it is not aligned. + expect_fail "KW on unaligned data" wrap aes256-kw "$RFC3394_KEK" <"$TMP/payload" +} + +test_key_file_accepts_hex_and_binary() { + local form + printf '%s' "$KEK128" >"$TMP/kek.hex" + unhex "$KEK128" >"$TMP/kek.bin" + unhex "${RFC3394_KEY_DATA:0:32}" >"$TMP/pt" + for form in kek.hex kek.bin; do + "$BC_RUST" aes128-kw -d encrypt --key-file "$TMP/$form" <"$TMP/pt" >"$TMP/ct" + assert_eq "$(hex "$TMP/ct")" "$RFC3394_4_1_CT" "--key-file $form must give the 4.1 ciphertext" + done +} + +test_a_key_of_the_wrong_length_is_rejected() { + rng 16 >"$TMP/pt" + expect_fail "a 24-byte key is not an AES-128 key" wrap aes128-kw "$KEK192" <"$TMP/pt" + assert_stderr_has "16-byte key" + expect_fail "a 16-byte key is not an AES-256 key" wrap aes256-kwp "$KEK128" <"$TMP/pt" + assert_stderr_has "32-byte key" +} + +test_a_missing_key_is_rejected() { + rng 16 >"$TMP/pt" + expect_fail "neither --key nor --key-file" "$BC_RUST" aes128-kw -d encrypt <"$TMP/pt" + assert_stderr_has "\-\-key" +} + +test_the_subcommands_are_listed_in_help() { + local cmd word + "$BC_RUST" --help >"$TMP/help" + for cmd in aes128-kw aes192-kw aes256-kw aes128-kwp aes192-kwp aes256-kwp; do + grep -q "$cmd" "$TMP/help" || fail "--help should list $cmd" + done + "$BC_RUST" aes128-kw --help >"$TMP/help" + for word in encrypt decrypt "RFC 3394" "NO IV"; do + grep -q "$word" "$TMP/help" || fail "aes128-kw --help should mention '$word'" + done +} + +run_all From c0da939a756d316746d4784f4bdf74cc6fbeb4cb Mon Sep 17 00:00:00 2001 From: David Hook Date: Sat, 31 Oct 2026 16:55:12 +1100 Subject: [PATCH 08/18] cipher: the key-wrap workers' bare constructors leave the public API Kw::from_perm and Kwp::from_perm were pub, which the key-rework-memory merge just made the odd ones out: 534d1820 took every other worker's bare constructor to pub(crate) under QUALITY_AND_STYLE's hazmat rule, since the key object is the route and the mode impls that call them live in this crate. The two follow. The doc examples build their KEK through the test suite's toy key, as the other modes' now do, and kw_tests compares the key route against the hazmat key-wrap mode traits rather than the constructors. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/kw.rs | 43 ++++++++++++++------------- crypto/cipher/src/modes/kwp.rs | 27 +++++++++-------- crypto/cipher/tests/modes/kw_tests.rs | 28 ++++++++++------- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/crypto/cipher/src/modes/kw.rs b/crypto/cipher/src/modes/kw.rs index 4a7797a85..a4eaf6bd5 100644 --- a/crypto/cipher/src/modes/kw.rs +++ b/crypto/cipher/src/modes/kw.rs @@ -18,30 +18,33 @@ //! //! # Usage Examples //! -//! From a key state -- what a mode-bound key holds -- over `ToyBlockCipher` (see the -//! [module docs](super) for why): +//! From a mode-bound key over `ToyBlockCipher` (see the [module docs](super) for why); over the +//! toy permutation the key is the test suite's `ToyKey`, the shape of the AES one: //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # #[allow(dead_code)] mod toy_key { include!("../../tests/common/toy_key.rs"); } //! # use toy::ToyBlockCipher; -//! use bouncycastle_cipher::modes::Kw; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! # use toy_key::ToyKey; +//! use bouncycastle_cipher::modes::KW; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +//! use bouncycastle_core::traits::KeyWrapper; //! //! let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let state = ToyBlockCipher::new(&kek).unwrap(); -//! let kw = Kw::from_perm(&state); +//! let key = ToyKey::::from_key_material(&kek).expect("a valid KEK"); //! let key_to_wrap = [0x5Au8; 32]; //! //! // Lengths fixed at compile time: a 32-byte key wraps to 40 bytes, and any other pair of //! // lengths is a compile error, not a runtime one. -//! let wrapped: [u8; 40] = kw.wrap_key(&key_to_wrap).expect("wrapping"); -//! let recovered = kw.unwrap_key::<32, 40>(&wrapped).expect("unwrapping"); +//! let wrapped: [u8; 40] = key.wrap_key(&key_to_wrap).expect("wrapping"); +//! let recovered = key.unwrap_key::<32, 40>(&wrapped).expect("unwrapping"); //! assert_eq!(*recovered, key_to_wrap); //! -//! // Lengths known only at run time: the caller sizes the buffers with the length helpers. +//! // Lengths known only at run time, on the worker the key hands out: the caller sizes the +//! // buffers with the length helpers. +//! let kw = key.create_wrapper(); //! let mut wrapped_out = [0u8; 64]; //! let n = kw.wrap_out(&key_to_wrap, &mut wrapped_out).expect("wrapping"); //! assert_eq!(n, kw.wrap_out_len(key_to_wrap.len())); @@ -54,20 +57,20 @@ //! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # #[allow(dead_code)] mod toy_key { include!("../../tests/common/toy_key.rs"); } //! # use toy::ToyBlockCipher; -//! # use bouncycastle_cipher::modes::Kw; -//! # use bouncycastle_core::hazmat::ElectronicCodeBook; +//! # use toy_key::ToyKey; +//! # use bouncycastle_cipher::modes::KW; //! # use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! # use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +//! # use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; //! use bouncycastle_core::errors::SymmetricCipherError; //! # let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! # .expect("a 16-byte symmetric cipher key"); -//! # let state = ToyBlockCipher::new(&kek).unwrap(); -//! # let kw = Kw::from_perm(&state); -//! # let mut wrapped: [u8; 40] = kw.wrap_key(&[0x5Au8; 32]).expect("wrapping"); +//! # let key = ToyKey::::from_key_material(&kek).expect("a valid KEK"); +//! # let mut wrapped: [u8; 40] = key.wrap_key(&[0x5Au8; 32]).expect("wrapping"); //! wrapped[3] ^= 0x01; //! assert!(matches!( -//! kw.unwrap_key::<32, 40>(&wrapped), +//! key.unwrap_key::<32, 40>(&wrapped), //! Err(SymmetricCipherError::DecryptionFailed) //! )); //! ``` @@ -300,9 +303,9 @@ impl<'k, P> Kw<'k, P> where P: BlockPermutation + ModeNames, { - /// Over an already-keyed permutation, which the worker borrows: what a [`KW`]-bound key's - /// roles build. - pub fn from_perm(perm: &'k P) -> Self { + /// Over an already-keyed permutation, which the worker borrows: what [`KW`]'s mode impls + /// build for a key's roles. + pub(crate) fn from_perm(perm: &'k P) -> Self { Self { perm } } } diff --git a/crypto/cipher/src/modes/kwp.rs b/crypto/cipher/src/modes/kwp.rs index 892184e27..282fecac1 100644 --- a/crypto/cipher/src/modes/kwp.rs +++ b/crypto/cipher/src/modes/kwp.rs @@ -13,30 +13,33 @@ //! //! # Usage Examples //! +//! From a mode-bound key over `ToyBlockCipher`; over the toy permutation the key is the test +//! suite's `ToyKey`, the shape of the AES one: +//! //! ``` //! # mod toy { include!("../../tests/common/toy_block_cipher.rs"); } +//! # #[allow(dead_code)] mod toy_key { include!("../../tests/common/toy_key.rs"); } //! # use toy::ToyBlockCipher; -//! use bouncycastle_cipher::modes::Kwp; -//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! # use toy_key::ToyKey; +//! use bouncycastle_cipher::modes::KWP; //! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; +//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; //! //! let kek = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) //! .expect("a 16-byte symmetric cipher key"); -//! let state = ToyBlockCipher::new(&kek).unwrap(); -//! let kwp = Kwp::from_perm(&state); +//! let key = ToyKey::::from_key_material(&kek).expect("a valid KEK"); //! //! // 20 bytes pad to 24, plus the 8-byte header: 32 bytes of ciphertext. //! let data = [0x5Au8; 20]; -//! let wrapped: [u8; 32] = kwp.wrap_key(&data).expect("wrapping"); -//! let recovered = kwp.unwrap_key::<20, 32>(&wrapped).expect("unwrapping"); +//! let wrapped: [u8; 32] = key.wrap_key(&data).expect("wrapping"); +//! let recovered = key.unwrap_key::<20, 32>(&wrapped).expect("unwrapping"); //! assert_eq!(*recovered, data); //! //! // Up to 8 bytes fit in a single block with the header, which is enciphered directly //! // (Algorithm 5, step 5) rather than through the wrapping function. //! let short = *b"short"; -//! let wrapped: [u8; 16] = kwp.wrap_key(&short).expect("wrapping"); -//! assert_eq!(*kwp.unwrap_key::<5, 16>(&wrapped).expect("unwrapping"), short); +//! let wrapped: [u8; 16] = key.wrap_key(&short).expect("wrapping"); +//! assert_eq!(*key.unwrap_key::<5, 16>(&wrapped).expect("unwrapping"), short); //! ``` //! //! # The recovered length is inside the ciphertext @@ -159,9 +162,9 @@ impl<'k, P> Kwp<'k, P> where P: BlockPermutation + ModeNames, { - /// Over an already-keyed permutation, which the worker borrows: what a [`KWP`]-bound key's - /// roles build. - pub fn from_perm(perm: &'k P) -> Self { + /// Over an already-keyed permutation, which the worker borrows: what [`KWP`]'s mode impls + /// build for a key's roles. + pub(crate) fn from_perm(perm: &'k P) -> Self { Self { perm } } } diff --git a/crypto/cipher/tests/modes/kw_tests.rs b/crypto/cipher/tests/modes/kw_tests.rs index ae3ae2dba..7e90dad53 100644 --- a/crypto/cipher/tests/modes/kw_tests.rs +++ b/crypto/cipher/tests/modes/kw_tests.rs @@ -1,5 +1,5 @@ -//! Structural tests for KW and KWP, driven by a toy permutation: the key path and the bare -//! worker agree, the length rules of SP 800-38F Table 1, and the properties that tell the two +//! Structural tests for KW and KWP, driven by a toy permutation: the key path and the mode's +//! own route agree, the length rules of SP 800-38F Table 1, and the properties that tell the two //! algorithms apart from each other and from a plain block encryption. The shared key-wrap //! framework and the known-answer tests (RFC 3394, RFC 5649, ACVP) are in the `aes` crate, //! `crypto/aes/tests/kw_*.rs`: the framework's tamper checks need a permutation with diffusion, @@ -17,7 +17,9 @@ use bouncycastle_cipher::modes::kwp::{ }; use bouncycastle_cipher::modes::{KW, KWP, Kw, Kwp, ModeNames}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::hazmat::{BlockPermutation, ElectronicCodeBook}; +use bouncycastle_core::hazmat::{ + BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, +}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; use bouncycastle_core::traits::{KeyUnwrapper, KeyWrapper}; @@ -44,24 +46,30 @@ fn the_keys_enforce_the_material_policy() { framework.test::(Toy::KWP_ALG_NAME, SecurityStrength::_128bit); } -/// The key's one-shots and a worker built straight over the permutation agree. +/// The key's one-shots and the workers of the mode's own route -- the hazmat mode traits the +/// key forwards to -- agree. #[test] -fn the_key_and_the_bare_worker_agree() { - let perm = Toy::new(&toy_key()).unwrap(); +fn the_key_and_the_mode_route_agree() { let data = [0x5Au8; 32]; let key = kw_key(); + let state = >::key_state(Toy::new(&toy_key()).unwrap()); let from_key: [u8; 40] = key.wrap_key(&data).unwrap(); - let from_worker: [u8; 40] = Kw::from_perm(&perm).wrap_key(&data).unwrap(); + let from_worker: [u8; 40] = + >::wrap_init(&state).wrap_key(&data).unwrap(); assert_eq!(from_key, from_worker); - assert_eq!(*Kw::from_perm(&perm).unwrap_key::<32, 40>(&from_key).unwrap(), data); + let unwrapper = >::unwrap_init(&state); + assert_eq!(*unwrapper.unwrap_key::<32, 40>(&from_key).unwrap(), data); assert_eq!(*key.unwrap_key::<32, 40>(&from_worker).unwrap(), data); - assert_eq!(*Kw::from_perm(key.state()).unwrap_key::<32, 40>(&from_key).unwrap(), data); + let over_the_key_state = >::unwrap_init(key.state()); + assert_eq!(*over_the_key_state.unwrap_key::<32, 40>(&from_key).unwrap(), data); let key = kwp_key(); + let state = >::key_state(Toy::new(&toy_key()).unwrap()); let short: [u8; 20] = data[..20].try_into().unwrap(); let from_key: [u8; 32] = key.wrap_key(&short).unwrap(); - let from_worker: [u8; 32] = Kwp::from_perm(&perm).wrap_key(&short).unwrap(); + let from_worker: [u8; 32] = + >::wrap_init(&state).wrap_key(&short).unwrap(); assert_eq!(from_key, from_worker); assert_eq!(*key.unwrap_key::<20, 32>(&from_worker).unwrap(), data[..20]); } From 1c45b85f130e2b77c3ee515b77e7623baf3fe063 Mon Sep 17 00:00:00 2001 From: David Hook Date: Tue, 3 Nov 2026 13:35:03 +1100 Subject: [PATCH 09/18] 150-key-wrap: the merge-time adaptations to feature/key-rework-memory, carried over the rebase onto its release-based history This branch took feature/key-rework-memory by merge, and the merge commits carried the adaptations to what arrived. The rebase onto the branch's new history (key-rework-memory rebased onto release/0.1.3alpha, which carries feature/key-rework as one squash commit) dropped those merges; this commit restores the affected files to the branch's pre-rebase tip, so the tree is identical to it. Assisted-by: Claude:claude-mythos-5-1 --- crypto/aes/src/kw.rs | 195 ++++++++++++++++++++++--------------------- 1 file changed, 98 insertions(+), 97 deletions(-) diff --git a/crypto/aes/src/kw.rs b/crypto/aes/src/kw.rs index 61837f788..c89333a17 100644 --- a/crypto/aes/src/kw.rs +++ b/crypto/aes/src/kw.rs @@ -1,107 +1,108 @@ //! Type aliases for AES Key Wrap, KW (NIST SP 800-38F Sec 6.2; RFC 3394), and AES Key Wrap with -//! Padding, KWP (Sec 6.3; RFC 5649). -//! -//! See [`bouncycastle_cipher::modes::kw`] for the construction, the in-place wrapping function and -//! the security notes, and [`bouncycastle_cipher::modes::kwp`] for what padding adds. -//! [`AES_KW_128`] and friends name the worker, which implements [`KeyWrapper`] and -//! [`KeyUnwrapper`]; it comes from a key, [`AES128Key`] or [`AES128Key`], which takes -//! [`KeyType::SymmetricCipherKey`] material and hands the worker out through [`KeyWrappingKey`] -//! and [`KeyUnwrappingKey`], whose one-shots are the API in practice. -//! -//! # Which one -//! -//! **KW** is what RFC 3394, CMS (RFC 3565) and JOSE's `A128KW` family mean by "AES key wrap": it -//! wraps a symmetric key that is a whole number of 8 bytes, which every AES, HMAC and ChaCha key -//! is. **KWP** wraps anything from 1 byte up, at the cost of one extra semiblock when the input -//! was already aligned; it is what RFC 5649 and PKCS#11's `CKM_AES_KEY_WRAP_KWP` mean, and the -//! one to use for encoded private keys and other non-aligned data. Their ciphertexts are not -//! interchangeable: the integrity check values differ, so unwrapping one with the other fails. -//! -//! # Usage Examples -//! -//! RFC 3394 Sec 4.1, "Wrap 128 bits of Key Data with a 128-bit KEK", through the key: -//! -//! ``` -//! use bouncycastle_aes::AES128Key; -//! use bouncycastle_cipher::modes::KW; -//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; -//! -//! // KEK 000102030405060708090A0B0C0D0E0F, key data 00112233445566778899AABBCCDDEEFF. -//! let kek_bytes: [u8; 16] = core::array::from_fn(|i| i as u8); -//! let key_data: [u8; 16] = core::array::from_fn(|i| (i as u8) * 0x11); -//! let material = KeyMaterial::<16>::from_bytes_as_type(&kek_bytes, KeyType::SymmetricCipherKey) -//! .expect("a 16-byte symmetric cipher key"); -//! let kek = AES128Key::::from_key_material(&material).expect("a valid AES-128 KEK"); -//! -//! let wrapped: [u8; 24] = kek.wrap_key(&key_data).expect("wrapping"); -//! assert_eq!( -//! wrapped, -//! [ -//! 0x1F, 0xA6, 0x8B, 0x0A, 0x81, 0x12, 0xB4, 0x47, 0xAE, 0xF3, 0x4B, 0xD8, 0xFB, 0x5A, -//! 0x7B, 0x82, 0x9D, 0x3E, 0x86, 0x23, 0x71, 0xD2, 0xCF, 0xE5, -//! ] -//! ); -//! let recovered = kek.unwrap_key::<16, 24>(&wrapped).expect("unwrapping"); -//! assert_eq!(*recovered, key_data); -//! ``` -//! -//! The second example of RFC 5649 Sec 6, which wraps 7 octets -- short enough that the header -//! and data fit one AES block, the case Algorithm 5 step 5 enciphers directly -- and a payload -//! whose length is only known at run time: -//! -//! ``` -//! use bouncycastle_aes::AES192Key; -//! use bouncycastle_cipher::modes::KWP; -//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; -//! use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; -//! -//! let kek_bytes = [ -//! 0x58, 0x40, 0xdf, 0x6e, 0x29, 0xb0, 0x2a, 0xf1, 0xab, 0x49, 0x3b, 0x70, 0x5b, 0xf1, 0x6e, -//! 0xa1, 0xae, 0x83, 0x38, 0xf4, 0xdc, 0xc1, 0x76, 0xa8, -//! ]; -//! let material = KeyMaterial::<24>::from_bytes_as_type(&kek_bytes, KeyType::SymmetricCipherKey) -//! .expect("a 24-byte symmetric cipher key"); -//! let kek = AES192Key::::from_key_material(&material).expect("a valid AES-192 KEK"); -//! let key_data = [0x46, 0x6f, 0x72, 0x50, 0x61, 0x73, 0x69]; -//! -//! let wrapped: [u8; 16] = kek.wrap_key(&key_data).expect("wrapping"); -//! assert_eq!( -//! wrapped, -//! [ -//! 0xaf, 0xbe, 0xb0, 0xf0, 0x7d, 0xfb, 0xf5, 0x41, 0x92, 0x00, 0xf2, 0xcc, 0xb5, 0x0b, -//! 0xb2, 0x4f, -//! ] -//! ); -//! assert_eq!(*kek.unwrap_key::<7, 16>(&wrapped).expect("unwrapping"), key_data); -//! -//! let payload = b"an encoded private key, of whatever length it happens to be"; -//! let mut wrapped = vec![0u8; kek.wrap_out_len(payload.len())]; -//! let n = kek.wrap_out(payload, &mut wrapped).expect("wrapping"); -//! let mut recovered = vec![0u8; kek.unwrap_out_max_len(n)]; -//! let m = kek.unwrap_out(&wrapped[..n], &mut recovered).expect("unwrapping"); -//! assert_eq!(&recovered[..m], payload); -//! ``` -//! -//! # Memory Usage -//! -//! A key is the expanded schedule: 176, 208 and 240 bytes for AES-128, -192 and -256. A worker -//! is the borrow of it, one pointer, and the wrapping function's scratch is one block on the -//! stack; the data is wrapped and unwrapped in the caller's buffers. -//! -//! [`AES128Key`]: crate::AES128Key -//! [`AES128Key`]: crate::AES128Key -//! [`KeyWrapper`]: bouncycastle_core::traits::KeyWrapper -//! [`KeyUnwrapper`]: bouncycastle_core::traits::KeyUnwrapper -//! [`KeyWrappingKey`]: bouncycastle_core::symmetric_key::KeyWrappingKey -//! [`KeyUnwrappingKey`]: bouncycastle_core::symmetric_key::KeyUnwrappingKey -//! [`KeyType::SymmetricCipherKey`]: bouncycastle_core::key_material::KeyType::SymmetricCipherKey +//! Padding, KWP (Sec 6.3; RFC 5649). The docs are on [`AES_KW_128`], which the crate root +//! re-exports. use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; use bouncycastle_cipher::modes::{Kw, Kwp}; /// AES-128 Key Wrap (SP 800-38F KW; RFC 3394 with a 128-bit KEK): the worker an /// `AES128Key` hands out. +/// +/// See [`bouncycastle_cipher::modes::kw`] for the construction, the in-place wrapping function and +/// the security notes, and [`bouncycastle_cipher::modes::kwp`] for what padding adds. +/// [`AES_KW_128`] and friends name the worker, which implements [`KeyWrapper`] and +/// [`KeyUnwrapper`]; it comes from a key, [`AES128Key`] or [`AES128Key`], which takes +/// [`KeyType::SymmetricCipherKey`] material and hands the worker out through [`KeyWrappingKey`] +/// and [`KeyUnwrappingKey`], whose one-shots are the API in practice. +/// +/// # Which one +/// +/// **KW** is what RFC 3394, CMS (RFC 3565) and JOSE's `A128KW` family mean by "AES key wrap": it +/// wraps a symmetric key that is a whole number of 8 bytes, which every AES, HMAC and ChaCha key +/// is. **KWP** wraps anything from 1 byte up, at the cost of one extra semiblock when the input +/// was already aligned; it is what RFC 5649 and PKCS#11's `CKM_AES_KEY_WRAP_KWP` mean, and the +/// one to use for encoded private keys and other non-aligned data. Their ciphertexts are not +/// interchangeable: the integrity check values differ, so unwrapping one with the other fails. +/// +/// # Usage Examples +/// +/// RFC 3394 Sec 4.1, "Wrap 128 bits of Key Data with a 128-bit KEK", through the key: +/// +/// ``` +/// use bouncycastle_aes::AES128Key; +/// use bouncycastle_cipher::modes::KW; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +/// +/// // KEK 000102030405060708090A0B0C0D0E0F, key data 00112233445566778899AABBCCDDEEFF. +/// let kek_bytes: [u8; 16] = core::array::from_fn(|i| i as u8); +/// let key_data: [u8; 16] = core::array::from_fn(|i| (i as u8) * 0x11); +/// let material = KeyMaterial::<16>::from_bytes_as_type(&kek_bytes, KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// let kek = AES128Key::::from_key_material(&material).expect("a valid AES-128 KEK"); +/// +/// let wrapped: [u8; 24] = kek.wrap_key(&key_data).expect("wrapping"); +/// assert_eq!( +/// wrapped, +/// [ +/// 0x1F, 0xA6, 0x8B, 0x0A, 0x81, 0x12, 0xB4, 0x47, 0xAE, 0xF3, 0x4B, 0xD8, 0xFB, 0x5A, +/// 0x7B, 0x82, 0x9D, 0x3E, 0x86, 0x23, 0x71, 0xD2, 0xCF, 0xE5, +/// ] +/// ); +/// let recovered = kek.unwrap_key::<16, 24>(&wrapped).expect("unwrapping"); +/// assert_eq!(*recovered, key_data); +/// ``` +/// +/// The second example of RFC 5649 Sec 6, which wraps 7 octets -- short enough that the header +/// and data fit one AES block, the case Algorithm 5 step 5 enciphers directly -- and a payload +/// whose length is only known at run time: +/// +/// ``` +/// use bouncycastle_aes::AES192Key; +/// use bouncycastle_cipher::modes::KWP; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +/// +/// let kek_bytes = [ +/// 0x58, 0x40, 0xdf, 0x6e, 0x29, 0xb0, 0x2a, 0xf1, 0xab, 0x49, 0x3b, 0x70, 0x5b, 0xf1, 0x6e, +/// 0xa1, 0xae, 0x83, 0x38, 0xf4, 0xdc, 0xc1, 0x76, 0xa8, +/// ]; +/// let material = KeyMaterial::<24>::from_bytes_as_type(&kek_bytes, KeyType::SymmetricCipherKey) +/// .expect("a 24-byte symmetric cipher key"); +/// let kek = AES192Key::::from_key_material(&material).expect("a valid AES-192 KEK"); +/// let key_data = [0x46, 0x6f, 0x72, 0x50, 0x61, 0x73, 0x69]; +/// +/// let wrapped: [u8; 16] = kek.wrap_key(&key_data).expect("wrapping"); +/// assert_eq!( +/// wrapped, +/// [ +/// 0xaf, 0xbe, 0xb0, 0xf0, 0x7d, 0xfb, 0xf5, 0x41, 0x92, 0x00, 0xf2, 0xcc, 0xb5, 0x0b, +/// 0xb2, 0x4f, +/// ] +/// ); +/// assert_eq!(*kek.unwrap_key::<7, 16>(&wrapped).expect("unwrapping"), key_data); +/// +/// let payload = b"an encoded private key, of whatever length it happens to be"; +/// let mut wrapped = vec![0u8; kek.wrap_out_len(payload.len())]; +/// let n = kek.wrap_out(payload, &mut wrapped).expect("wrapping"); +/// let mut recovered = vec![0u8; kek.unwrap_out_max_len(n)]; +/// let m = kek.unwrap_out(&wrapped[..n], &mut recovered).expect("unwrapping"); +/// assert_eq!(&recovered[..m], payload); +/// ``` +/// +/// # Memory Usage +/// +/// A key is the expanded schedule: 176, 208 and 240 bytes for AES-128, -192 and -256. A worker +/// is the borrow of it, one pointer, and the wrapping function's scratch is one block on the +/// stack; the data is wrapped and unwrapped in the caller's buffers. +/// +/// [`AES128Key`]: crate::AES128Key +/// [`AES128Key`]: crate::AES128Key +/// [`KeyWrapper`]: bouncycastle_core::traits::KeyWrapper +/// [`KeyUnwrapper`]: bouncycastle_core::traits::KeyUnwrapper +/// [`KeyWrappingKey`]: bouncycastle_core::symmetric_key::KeyWrappingKey +/// [`KeyUnwrappingKey`]: bouncycastle_core::symmetric_key::KeyUnwrappingKey +/// [`KeyType::SymmetricCipherKey`]: bouncycastle_core::key_material::KeyType::SymmetricCipherKey #[allow(non_camel_case_types)] pub type AES_KW_128<'k> = Kw<'k, AES128Internal>; From 38427f5f026d409e03235640210fda7c6afb3e2a Mon Sep 17 00:00:00 2001 From: David Hook Date: Thu, 5 Nov 2026 12:00:41 +1100 Subject: [PATCH 10/18] 150-key-wrap: the adaptations to release/0.1.4alpha, carried over the rebase from feature/key-rework-memory The release reads a key file as raw bytes unless --hex-input is given, so the aes*-kw and aes*-kwp help says so and the key-file test passes --hex-input for its hex form; kw_tests allows the shared toy key's unused helpers, as the other mode tests do. The KW bullet moves to the 0.1.4 release notes in the first commit. Assisted-by: Claude:claude-opus-5-5 --- cli/src/main.rs | 12 ++++++------ cli/tests/test_aes_kw.sh | 9 +++++---- crypto/cipher/tests/modes/kw_tests.rs | 1 + 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index 44a9bfab4..ff4c0e98f 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2210,7 +2210,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key-encryption key, in binary or hex. + /// A file containing the 16-byte AES key-encryption key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -2234,7 +2234,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key-encryption key, in binary or hex. + /// A file containing the 24-byte AES key-encryption key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -2258,7 +2258,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key-encryption key, in binary or hex. + /// A file containing the 32-byte AES key-encryption key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -2287,7 +2287,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 16-byte AES key-encryption key, in binary or hex. + /// A file containing the 16-byte AES key-encryption key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -2310,7 +2310,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 24-byte AES key-encryption key, in binary or hex. + /// A file containing the 24-byte AES key-encryption key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, @@ -2333,7 +2333,7 @@ enum Subcommands { #[arg(long)] key: Option, - /// A file containing the 32-byte AES key-encryption key, in binary or hex. + /// A file containing the 32-byte AES key-encryption key (raw bytes; hex with --hex-input). /// If both key and key_file options are provided, the file will be used. #[arg(short, long)] key_file: Option, diff --git a/cli/tests/test_aes_kw.sh b/cli/tests/test_aes_kw.sh index d03cc7e39..aa679c9e0 100755 --- a/cli/tests/test_aes_kw.sh +++ b/cli/tests/test_aes_kw.sh @@ -168,10 +168,11 @@ test_key_file_accepts_hex_and_binary() { printf '%s' "$KEK128" >"$TMP/kek.hex" unhex "$KEK128" >"$TMP/kek.bin" unhex "${RFC3394_KEY_DATA:0:32}" >"$TMP/pt" - for form in kek.hex kek.bin; do - "$BC_RUST" aes128-kw -d encrypt --key-file "$TMP/$form" <"$TMP/pt" >"$TMP/ct" - assert_eq "$(hex "$TMP/ct")" "$RFC3394_4_1_CT" "--key-file $form must give the 4.1 ciphertext" - done + # The hex form needs --hex-input: a key file is raw bytes unless the flag says otherwise. + "$BC_RUST" --hex-input aes128-kw -d encrypt --key-file "$TMP/kek.hex" <"$TMP/pt" >"$TMP/ct" + assert_eq "$(hex "$TMP/ct")" "$RFC3394_4_1_CT" "--key-file kek.hex must give the 4.1 ciphertext" + "$BC_RUST" aes128-kw -d encrypt --key-file "$TMP/kek.bin" <"$TMP/pt" >"$TMP/ct" + assert_eq "$(hex "$TMP/ct")" "$RFC3394_4_1_CT" "--key-file kek.bin must give the 4.1 ciphertext" } test_a_key_of_the_wrong_length_is_rejected() { diff --git a/crypto/cipher/tests/modes/kw_tests.rs b/crypto/cipher/tests/modes/kw_tests.rs index 7e90dad53..1a95e0f8e 100644 --- a/crypto/cipher/tests/modes/kw_tests.rs +++ b/crypto/cipher/tests/modes/kw_tests.rs @@ -7,6 +7,7 @@ mod common; #[path = "../common/toy_key.rs"] +#[allow(dead_code)] mod toy_key; use bouncycastle_cipher::modes::kw::{ From 98575030742fb56be2f58c9d4d7095bd419a0719 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 6 Nov 2026 03:04:54 +1100 Subject: [PATCH 11/18] core: KeyWrapper and KeyUnwrapper are dyn-compatible, as the other worker traits are The pair had `Algorithm` as its supertrait and no `Self: Sized` fence on the four const-generic one-shots, so `&dyn KeyWrapper` was rejected (E0038) where every other cipher and MAC worker trait can be held as a trait object. The supertrait becomes `AlgorithmInfo` and the const-generic methods are fenced, leaving `wrap_out`, `wrap_out_len`, `unwrap_out` and `unwrap_out_max_len` dispatchable; dyn_compatible_tests pins the pair. The `_out` docs now say the buffer is zeroized before anything else, which the cipher crate's implementations do from the next commit. Also drops a stray first line on KeyUnwrapper's docs, the KeyStream header a merge had glued on. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core/src/traits.rs | 33 ++++++++++++++++------- crypto/core/tests/dyn_compatible_tests.rs | 8 +++--- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index cff67580f..dffe262cf 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -673,7 +673,6 @@ pub trait KEMPublicKey: fn from_bytes(bytes: &[u8]) -> Result; } -/// A keyed keystream generator: the raw primitive under a stream cipher, as /// The unwrapping half of a key-wrap algorithm -- KW, KWP or TKW of NIST SP 800-38F (RFC 3394 and /// RFC 5649) -- as the worker a key object hands out through /// [`KeyUnwrappingKey`](crate::symmetric_key::KeyUnwrappingKey). Wrapping is [`KeyWrapper`]. The @@ -699,15 +698,19 @@ pub trait KEMPublicKey: /// reported as [`SymmetricCipherError::DecryptionFailed`], so that the error does not tell an /// attacker which check failed. SP 800-38F reports all of these as a single FAIL; see, for example, /// Algorithm 6 (KWP-AD), steps 4, 7 and 8. On any error, the output buffer is zeroized. -pub trait KeyUnwrapper: Algorithm { +pub trait KeyUnwrapper: AlgorithmInfo { /// One-shot: unwraps a ciphertext whose length is fixed at compile time, returning the /// recovered plaintext. /// /// Provided over [`KeyUnwrapper::unwrap_key_out`]. + /// Not callable on a trait object: the lengths are properties of the call site. fn unwrap_key( &self, ciphertext: &[u8; CT_LEN], - ) -> Result, SymmetricCipherError> { + ) -> Result, SymmetricCipherError> + where + Self: Sized, + { let mut key = Secret::<[u8; KEY_LEN]>::new(); let bytes_written = self.unwrap_key_out(ciphertext, &mut key)?; debug_assert_eq!(bytes_written, KEY_LEN); @@ -725,11 +728,14 @@ pub trait KeyUnwrapper: Algorithm { /// # Errors /// [`SymmetricCipherError::DecryptionFailed`] for any authenticity failure. `key` is zeroized /// on error. + /// Not callable on a trait object: the lengths are properties of the call site. fn unwrap_key_out( &self, ciphertext: &[u8; CT_LEN], key: &mut Secret<[u8; KEY_LEN]>, - ) -> Result; + ) -> Result + where + Self: Sized; /// An upper bound on the plaintext length for a ciphertext of `ciphertext_len` bytes: the /// buffer [`KeyUnwrapper::unwrap_out`] requires. This is `ciphertext_len` minus one semiblock @@ -750,7 +756,8 @@ pub trait KeyUnwrapper: Algorithm { /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, checked before /// any work is done; [`SymmetricCipherError::InvalidInputLength`] for a ciphertext length the /// algorithm is not defined on; and [`SymmetricCipherError::DecryptionFailed`] for any - /// authenticity failure. The whole `plaintext` buffer is zeroized on error. + /// authenticity failure. The whole `plaintext` buffer is zeroized before anything else, so + /// nothing stale survives an error. fn unwrap_out( &self, ciphertext: &[u8], @@ -776,14 +783,18 @@ pub trait KeyUnwrapper: Algorithm { /// /// Key wrapping is deterministic: wrapping the same data under the same KEK always produces the /// same ciphertext. -pub trait KeyWrapper: Algorithm { +pub trait KeyWrapper: AlgorithmInfo { /// One-shot: wraps data whose length is fixed at compile time, returning the ciphertext. /// /// Provided over [`KeyWrapper::wrap_key_out`]. + /// Not callable on a trait object: the lengths are properties of the call site. fn wrap_key( &self, key: &[u8; KEY_LEN], - ) -> Result<[u8; CT_LEN], SymmetricCipherError> { + ) -> Result<[u8; CT_LEN], SymmetricCipherError> + where + Self: Sized, + { let mut ciphertext = [0u8; CT_LEN]; let bytes_written = self.wrap_key_out(key, &mut ciphertext)?; debug_assert_eq!(bytes_written, CT_LEN); @@ -802,11 +813,14 @@ pub trait KeyWrapper: Algorithm { /// # Errors /// None for a valid pair of lengths; the signature carries the `Result` for the run-time /// form's sake. + /// Not callable on a trait object: the lengths are properties of the call site. fn wrap_key_out( &self, key: &[u8; KEY_LEN], ciphertext: &mut [u8; CT_LEN], - ) -> Result; + ) -> Result + where + Self: Sized; /// The exact ciphertext length for a plaintext of `plaintext_len` bytes: the buffer /// [`KeyWrapper::wrap_out`] requires. For example `plaintext_len + 8` for KW, and @@ -823,7 +837,8 @@ pub trait KeyWrapper: Algorithm { /// # Errors /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked before /// any work is done, and [`SymmetricCipherError::InvalidInputLength`] for a plaintext length - /// the algorithm is not defined on. + /// the algorithm is not defined on. `ciphertext` is zeroized before anything else, so nothing + /// stale survives an error. fn wrap_out( &self, plaintext: &[u8], diff --git a/crypto/core/tests/dyn_compatible_tests.rs b/crypto/core/tests/dyn_compatible_tests.rs index 58e3228ff..ab7ffcb9e 100644 --- a/crypto/core/tests/dyn_compatible_tests.rs +++ b/crypto/core/tests/dyn_compatible_tests.rs @@ -5,8 +5,8 @@ use bouncycastle_core::hazmat::KeyStream; use bouncycastle_core::traits::{ AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, Hash, - MAC, StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, - SymmetricCipherEncryptor, XOF, XOFSqueezer, + KeyUnwrapper, KeyWrapper, MAC, StreamCipherDecryptor, StreamCipherEncryptor, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, XOF, XOFSqueezer, }; fn symmetric( @@ -18,6 +18,8 @@ fn aead(_: &mut dyn AEADCipherEncryptor<12, 16, 16>, _: &mut dyn AEADCipherDecry fn stream(_: &mut dyn StreamCipherEncryptor<16>, _: &mut dyn StreamCipherDecryptor<16>) {} fn block(_: &mut dyn BlockCipherEncryptor<16, 16>, _: &mut dyn BlockCipherDecryptor<16, 16>) {} fn mac(_: &mut dyn MAC) {} +/// The run-time-length half of key wrap; the const-generic one-shots are `Self: Sized`. +fn key_wrap(_: &dyn KeyWrapper, _: &dyn KeyUnwrapper) {} fn key_stream(_: &mut dyn KeyStream<12, 16>) {} fn hash(_: &mut dyn Hash) {} /// A XOF names its squeezer type, so the object type is `dyn XOF`. @@ -28,7 +30,7 @@ fn the_worker_traits_are_dyn_compatible() { // Referenced so the functions are not dead code; the compile is the assertion. let _ = ( symmetric as fn(_, _), aead as fn(_, _), stream as fn(_, _), block as fn(_, _), - mac as fn(_), key_stream as fn(_), hash as fn(_), + mac as fn(_), key_wrap as fn(_, _), key_stream as fn(_), hash as fn(_), ); fn reference_xof() { let _ = xof:: as fn(_, _); From e09776d44e49afaf4347649cd6ec83be026a6d78 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 6 Nov 2026 03:04:54 +1100 Subject: [PATCH 12/18] cipher: the key-wrap `_out` methods zeroize the whole output buffer before any check `wrap_out` never zeroized its buffer and `unwrap_out` did so only after the OutputBufferTooSmall check, so a refused call could leave stale bytes behind. Both now `fill(0)` first, before any error checking, as the `_out` rule in QUALITY_AND_STYLE and GCM's do_encrypt_out have it. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/kw.rs | 8 +++++--- crypto/cipher/src/modes/kwp.rs | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crypto/cipher/src/modes/kw.rs b/crypto/cipher/src/modes/kw.rs index a4eaf6bd5..ee0efd078 100644 --- a/crypto/cipher/src/modes/kw.rs +++ b/crypto/cipher/src/modes/kw.rs @@ -355,6 +355,8 @@ where plaintext: &[u8], ciphertext: &mut [u8], ) -> Result { + // Nothing stale may survive a failure, so the whole buffer starts from zero. + ciphertext.fill(0); let ct_len = kw_wrapped_len(plaintext.len()); if ciphertext.len() < ct_len { return Err(SymmetricCipherError::OutputBufferTooSmall(ct_len)); @@ -416,13 +418,13 @@ where ciphertext: &[u8], plaintext: &mut [u8], ) -> Result { + // Nothing this function writes may survive a failure, so start from zero and only ever + // return `Ok` with the buffer fully written. + plaintext.fill(0); let pt_len = self.unwrap_out_max_len(ciphertext.len()); if plaintext.len() < pt_len { return Err(SymmetricCipherError::OutputBufferTooSmall(pt_len)); } - // Nothing this function writes may survive a failure, so start from zero and only ever - // return `Ok` with the buffer fully written. - plaintext.fill(0); if !kw_ciphertext_len_is_valid(ciphertext.len()) { return Err(SymmetricCipherError::InvalidInputLength( "a KW ciphertext is 3 to 2^54 whole 8-byte semiblocks (SP 800-38F Table 1)", diff --git a/crypto/cipher/src/modes/kwp.rs b/crypto/cipher/src/modes/kwp.rs index 282fecac1..add9ab3ca 100644 --- a/crypto/cipher/src/modes/kwp.rs +++ b/crypto/cipher/src/modes/kwp.rs @@ -214,6 +214,8 @@ where plaintext: &[u8], ciphertext: &mut [u8], ) -> Result { + // Nothing stale may survive a failure, so the whole buffer starts from zero. + ciphertext.fill(0); let ct_len = kwp_wrapped_len(plaintext.len()); if ciphertext.len() < ct_len { return Err(SymmetricCipherError::OutputBufferTooSmall(ct_len)); @@ -305,13 +307,13 @@ where ciphertext: &[u8], plaintext: &mut [u8], ) -> Result { + // Nothing this function writes may survive a failure, so start from zero. + plaintext.fill(0); // 8(n-1) bytes: the padded plaintext. let padded_len = self.unwrap_out_max_len(ciphertext.len()); if plaintext.len() < padded_len { return Err(SymmetricCipherError::OutputBufferTooSmall(padded_len)); } - // Nothing this function writes may survive a failure, so start from zero. - plaintext.fill(0); if !kwp_ciphertext_len_is_valid(ciphertext.len()) { return Err(SymmetricCipherError::InvalidInputLength( "a KWP ciphertext is 2 to 2^29 whole 8-byte semiblocks (SP 800-38F Table 1)", From 1a5de48f1ed49997c81f0ffb54a2cdf281ec9fe2 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 6 Nov 2026 03:04:54 +1100 Subject: [PATCH 13/18] core-test-framework: the key-wrap suite checks that a refused `_out` call left the buffer zeroed The too-small-buffer and invalid-length cases pre-fill the output with 0xFF and assert it is all zeros after the error; the mock zeroizes first, as the implementations must. Run against the previous cipher commit's predecessor, the new asserts fail on both KW and KWP. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core-test-framework/src/key_wrap.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crypto/core-test-framework/src/key_wrap.rs b/crypto/core-test-framework/src/key_wrap.rs index c6be3e187..8ba3c9c3f 100644 --- a/crypto/core-test-framework/src/key_wrap.rs +++ b/crypto/core-test-framework/src/key_wrap.rs @@ -131,35 +131,45 @@ impl TestFrameworkKeyWrap { /* Error case: output buffers too small */ + // An `_out` method zeroizes its whole output buffer before any check, so a refused + // call leaves nothing stale behind either. + buf.fill(0xFF); match kek.wrap_out(&key, &mut buf[..CT_LEN - 1]) { Err(SymmetricCipherError::OutputBufferTooSmall(required)) => { assert_eq!(required, CT_LEN) } _ => panic!("Should have rejected a ciphertext buffer that is too small"), } + assert!(buf[..CT_LEN - 1].iter().all(|b| *b == 0), "ciphertext buffer not zeroized"); // checked before any work is done, so it is the upper bound that is enforced, even when // the actual plaintext would have fit + pt.fill(0xFF); match kek.unwrap_out(&ct, &mut pt[..max_pt_len - 1]) { Err(SymmetricCipherError::OutputBufferTooSmall(required)) => { assert_eq!(required, max_pt_len) } _ => panic!("Should have rejected a plaintext buffer that is too small"), } + assert!(pt[..max_pt_len - 1].iter().all(|b| *b == 0), "plaintext buffer not zeroized"); /* Error case: input lengths the algorithm is not defined on */ // Every key-wrap algorithm in SP 800-38F (Table 1) needs at least one octet of plaintext // and a ciphertext of at least two semiblocks that is a whole number of semiblocks. // Empty inputs also catch implementations that subtract before checking the length. + buf.fill(0xFF); match kek.wrap_out(&[], &mut buf) { Err(SymmetricCipherError::InvalidInputLength(_)) => { /* good */ } _ => panic!("Should have rejected an empty plaintext"), } + assert!(buf.iter().all(|b| *b == 0), "ciphertext buffer not zeroized"); for bad_ct_len in [0, semiblock_len, CT_LEN - 1] { + pt.fill(0xFF); match kek.unwrap_out(&ct[..bad_ct_len], &mut pt) { Err(SymmetricCipherError::InvalidInputLength(_)) => { /* good */ } _ => panic!("Should have rejected a ciphertext of {bad_ct_len} bytes"), } + assert!(pt.iter().all(|b| *b == 0), "plaintext buffer not zeroized"); } /* Error case: modified ciphertext */ @@ -342,6 +352,7 @@ mod tests { plaintext: &[u8], ciphertext: &mut [u8], ) -> Result { + ciphertext.fill(0); let ct_len = self.wrap_out_len(plaintext.len()); if ciphertext.len() < ct_len { return Err(SymmetricCipherError::OutputBufferTooSmall(ct_len)); @@ -350,7 +361,6 @@ mod tests { return Err(SymmetricCipherError::InvalidInputLength("plaintext length")); } let s = &mut ciphertext[..ct_len]; - s.fill(0); s[..4].copy_from_slice(&(plaintext.len() as u32).to_be_bytes()); s[4..8].copy_from_slice(&checksum(self.kek, plaintext)); s[8..8 + plaintext.len()].copy_from_slice(plaintext); @@ -402,11 +412,11 @@ mod tests { ciphertext: &[u8], plaintext: &mut [u8], ) -> Result { + plaintext.fill(0); let max_pt_len = self.unwrap_out_max_len(ciphertext.len()); if plaintext.len() < max_pt_len { return Err(SymmetricCipherError::OutputBufferTooSmall(max_pt_len)); } - plaintext.fill(0); if ciphertext.len() < 2 * MOCK_SEMIBLOCK_LEN || !ciphertext.len().is_multiple_of(MOCK_SEMIBLOCK_LEN) { From 667235221030ac7b095095f4ef90188e95ddef30 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 6 Nov 2026 03:08:22 +1100 Subject: [PATCH 14/18] cipher: the key-wrap length helpers leave the public API; the modes doc matches SP 800-38F Sec 3.1 The six `kw_*`/`kwp_*` length predicates and helpers were public and used only by this crate's own tests. They become pub(crate): the lower bounds, the alignment rule and the length helpers are now pinned through a key's run-time API in kw_tests, and the 2^54, 2^32 and 2^29 upper bounds of Table 1, which no buffer can reach, are a unit test at the bottom of each mode file. The modes overview said KW and KWP "are not a mode for data"; SP 800-38F Sec 3.1 approves them for general data as well as keys. It now says so, and points at the determinism note in `kw` for why an AEAD is the better fit for data that may repeat. Assisted-by: Claude:claude-mythos-5-1 --- crypto/cipher/src/modes/kw.rs | 26 +++++++- crypto/cipher/src/modes/kwp.rs | 27 ++++++-- crypto/cipher/src/modes/mod.rs | 4 +- crypto/cipher/tests/modes/kw_tests.rs | 96 +++++++++------------------ 4 files changed, 80 insertions(+), 73 deletions(-) diff --git a/crypto/cipher/src/modes/kw.rs b/crypto/cipher/src/modes/kw.rs index ee0efd078..e7f8ad1c6 100644 --- a/crypto/cipher/src/modes/kw.rs +++ b/crypto/cipher/src/modes/kw.rs @@ -149,7 +149,7 @@ const KW_MAX_PLAINTEXT_SEMIBLOCKS: u64 = (1u64 << 54) - 1; /// Whether `len` bytes is a plaintext length KW is defined on: 2 to 2^54 - 1 whole semiblocks /// (SP 800-38F Table 1). -pub const fn kw_plaintext_len_is_valid(len: usize) -> bool { +pub(crate) const fn kw_plaintext_len_is_valid(len: usize) -> bool { len.is_multiple_of(SEMIBLOCK_LEN) && len >= 2 * SEMIBLOCK_LEN && (len / SEMIBLOCK_LEN) as u64 <= KW_MAX_PLAINTEXT_SEMIBLOCKS @@ -157,7 +157,7 @@ pub const fn kw_plaintext_len_is_valid(len: usize) -> bool { /// Whether `len` bytes is a ciphertext length KW-AD is defined on: 3 to 2^54 whole semiblocks /// (SP 800-38F Table 1). -pub const fn kw_ciphertext_len_is_valid(len: usize) -> bool { +pub(crate) const fn kw_ciphertext_len_is_valid(len: usize) -> bool { len.is_multiple_of(SEMIBLOCK_LEN) && len >= 3 * SEMIBLOCK_LEN && (len / SEMIBLOCK_LEN) as u64 <= KW_MAX_PLAINTEXT_SEMIBLOCKS + 1 @@ -165,7 +165,7 @@ pub const fn kw_ciphertext_len_is_valid(len: usize) -> bool { /// The KW ciphertext length for a plaintext of `len` bytes: one semiblock longer. Algorithm 3 /// computes `C = W(ICV1 || P)`, and W preserves the length of its input. -pub const fn kw_wrapped_len(len: usize) -> usize { +pub(crate) const fn kw_wrapped_len(len: usize) -> usize { len.saturating_add(SEMIBLOCK_LEN) } @@ -451,3 +451,23 @@ where Ok(pt_len) } } + +#[cfg(test)] +mod tests { + //! The 2^54 upper bounds of SP 800-38F Table 1, which no buffer can reach: the last valid + //! plaintext is 2^54 - 1 semiblocks, the last valid ciphertext 2^54. The lower bounds and the + //! alignment rule are pinned through the key in `tests/modes/kw_tests.rs`. + + use super::*; + + #[cfg(target_pointer_width = "64")] + #[test] + fn the_table_1_upper_bounds() { + const MAX_PT: usize = ((1usize << 54) - 1) * SEMIBLOCK_LEN; + assert!(kw_plaintext_len_is_valid(MAX_PT)); + assert!(!kw_plaintext_len_is_valid(MAX_PT + SEMIBLOCK_LEN / 2)); + assert!(!kw_plaintext_len_is_valid(MAX_PT + SEMIBLOCK_LEN)); + assert!(kw_ciphertext_len_is_valid(MAX_PT + SEMIBLOCK_LEN)); + assert!(!kw_ciphertext_len_is_valid(MAX_PT + 2 * SEMIBLOCK_LEN)); + } +} diff --git a/crypto/cipher/src/modes/kwp.rs b/crypto/cipher/src/modes/kwp.rs index add9ab3ca..9c26d69e7 100644 --- a/crypto/cipher/src/modes/kwp.rs +++ b/crypto/cipher/src/modes/kwp.rs @@ -84,13 +84,13 @@ const KWP_MAX_CIPHERTEXT_SEMIBLOCKS: u64 = 1u64 << 29; /// Whether `len` bytes is a plaintext length KWP is defined on: 1 to 2^32 - 1 octets (SP 800-38F /// Table 1). -pub const fn kwp_plaintext_len_is_valid(len: usize) -> bool { +pub(crate) const fn kwp_plaintext_len_is_valid(len: usize) -> bool { len >= 1 && len as u64 <= KWP_MAX_PLAINTEXT_LEN } /// Whether `len` bytes is a ciphertext length KWP-AD is defined on: 2 to 2^29 whole semiblocks /// (SP 800-38F Table 1). -pub const fn kwp_ciphertext_len_is_valid(len: usize) -> bool { +pub(crate) const fn kwp_ciphertext_len_is_valid(len: usize) -> bool { len.is_multiple_of(SEMIBLOCK_LEN) && len >= 2 * SEMIBLOCK_LEN && (len / SEMIBLOCK_LEN) as u64 <= KWP_MAX_CIPHERTEXT_SEMIBLOCKS @@ -99,7 +99,7 @@ pub const fn kwp_ciphertext_len_is_valid(len: usize) -> bool { /// The KWP ciphertext length for a plaintext of `len` bytes: `len` rounded up to a whole number /// of semiblocks (Algorithm 5, steps 2-3: `padlen = 8 * ceil(len / 8) - len` zero bytes), plus the /// one-semiblock header of step 4. -pub const fn kwp_wrapped_len(len: usize) -> usize { +pub(crate) const fn kwp_wrapped_len(len: usize) -> usize { len.div_ceil(SEMIBLOCK_LEN).saturating_mul(SEMIBLOCK_LEN).saturating_add(SEMIBLOCK_LEN) } @@ -184,7 +184,8 @@ where P: BlockPermutation + ModeNames, { /// KWP-AE with the lengths fixed at compile time. `KEY_LEN` may be anything from 1 to - /// 2^32 - 1 and `CT_LEN` must be [`kwp_wrapped_len`] of it; anything else fails to compile. + /// 2^32 - 1 and `CT_LEN` must be `KEY_LEN` rounded up to a multiple of 8, plus 8; anything + /// else fails to compile. fn wrap_key_out( &self, key: &[u8; KEY_LEN], @@ -363,3 +364,21 @@ where Ok(plen) } } + +#[cfg(test)] +mod tests { + //! The upper bounds of SP 800-38F Table 1, which no buffer can reach: 2^32 - 1 octets of + //! plaintext and 2^29 semiblocks of ciphertext. The lower bounds and the alignment rule are + //! pinned through the key in `tests/modes/kw_tests.rs`. + + use super::*; + + #[cfg(target_pointer_width = "64")] + #[test] + fn the_table_1_upper_bounds() { + assert!(kwp_plaintext_len_is_valid(u32::MAX as usize)); + assert!(!kwp_plaintext_len_is_valid(u32::MAX as usize + 1)); + assert!(kwp_ciphertext_len_is_valid(SEMIBLOCK_LEN << 29)); + assert!(!kwp_ciphertext_len_is_valid((SEMIBLOCK_LEN << 29) + SEMIBLOCK_LEN)); + } +} diff --git a/crypto/cipher/src/modes/mod.rs b/crypto/cipher/src/modes/mod.rs index 167d1b5cd..3f8821c1c 100644 --- a/crypto/cipher/src/modes/mod.rs +++ b/crypto/cipher/src/modes/mod.rs @@ -41,7 +41,9 @@ //! //! **KW and KWP are key-wrap algorithms** ([`KeyWrapper`] / [`KeyUnwrapper`]): one-shot, //! deterministic and authenticated, with no IV and no streaming API, for wrapping a key (or a -//! small, non-repeating secret) under a key encryption key. They are not a mode for data. +//! small, non-repeating secret) under a key encryption key. SP 800-38F Sec 3.1 approves them for +//! general data as well, but they are deterministic, so for data that may repeat an AEAD is the +//! better fit (see the [`kw`] security notes). //! //! CBC, CFB, CFB8 and CTR all generate their own init data: an IV for the first three, a nonce for //! CTR, which is shorter than a block because the rest of the counter block is the counter. ECB has diff --git a/crypto/cipher/tests/modes/kw_tests.rs b/crypto/cipher/tests/modes/kw_tests.rs index 1a95e0f8e..893277551 100644 --- a/crypto/cipher/tests/modes/kw_tests.rs +++ b/crypto/cipher/tests/modes/kw_tests.rs @@ -10,12 +10,6 @@ mod common; #[allow(dead_code)] mod toy_key; -use bouncycastle_cipher::modes::kw::{ - kw_ciphertext_len_is_valid, kw_plaintext_len_is_valid, kw_wrapped_len, -}; -use bouncycastle_cipher::modes::kwp::{ - kwp_ciphertext_len_is_valid, kwp_plaintext_len_is_valid, kwp_wrapped_len, -}; use bouncycastle_cipher::modes::{KW, KWP, Kw, Kwp, ModeNames}; use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::hazmat::{ @@ -176,73 +170,45 @@ fn kwp_rejects_a_length_field_that_does_not_fit() { } } -/// The length predicates and helpers are SP 800-38F Table 1 exactly. +/// The length rules of SP 800-38F Table 1, through the key's run-time API: the lower bounds, the +/// alignment rule and the length helpers. The 2^54, 2^32 and 2^29 upper bounds, which no buffer +/// can reach, are unit tests in the mode files. #[test] -fn the_length_helpers_match_table_1() { +fn the_length_rules_match_table_1() { let kw = kw_key(); let kwp = kwp_key(); - - // KW: 2 to 2^54 - 1 semiblocks of plaintext, one more of ciphertext. - assert!(!kw_plaintext_len_is_valid(0)); - assert!(!kw_plaintext_len_is_valid(8)); - assert!(!kw_plaintext_len_is_valid(12)); - assert!(kw_plaintext_len_is_valid(16)); - assert!(kw_plaintext_len_is_valid(24)); - assert!(!kw_ciphertext_len_is_valid(16)); - assert!(kw_ciphertext_len_is_valid(24)); - assert!(!kw_ciphertext_len_is_valid(25)); - assert_eq!(kw_wrapped_len(16), 24); - assert_eq!(kw_wrapped_len(usize::MAX), usize::MAX, "saturates rather than overflowing"); - // The 2^54 upper bounds, reachable as lengths only on a 64-bit target: the last valid - // plaintext is 2^54 - 1 semiblocks, the last valid ciphertext 2^54. - #[cfg(target_pointer_width = "64")] - { - const MAX_PT: usize = ((1usize << 54) - 1) * 8; - assert!(kw_plaintext_len_is_valid(MAX_PT)); - assert!(!kw_plaintext_len_is_valid(MAX_PT + 4)); - assert!(!kw_plaintext_len_is_valid(MAX_PT + 8)); - assert!(kw_ciphertext_len_is_valid(MAX_PT + 8)); - assert!(!kw_ciphertext_len_is_valid(MAX_PT + 16)); - } + let mut buf = [0u8; 64]; + let invalid = |r: Result| { + matches!(r, Err(SymmetricCipherError::InvalidInputLength(_))) + }; + + // KW: 2 or more whole semiblocks of plaintext, one more of ciphertext. + assert!(invalid(kw.wrap_out(&[0u8; 0], &mut buf))); + assert!(invalid(kw.wrap_out(&[0u8; 8], &mut buf))); + assert!(invalid(kw.wrap_out(&[0u8; 12], &mut buf))); + assert_eq!(kw.wrap_out(&[0u8; 16], &mut buf).unwrap(), 24); + assert_eq!(kw.wrap_out(&[0u8; 24], &mut buf).unwrap(), 32); + assert!(invalid(kw.unwrap_out(&[0u8; 16], &mut buf))); + assert!(invalid(kw.unwrap_out(&[0u8; 25], &mut buf))); + assert!(!invalid(kw.unwrap_out(&[0u8; 24], &mut buf))); + assert_eq!(kw.wrap_out_len(16), 24); assert_eq!(kw.wrap_out_len(32), 40); + assert_eq!(kw.wrap_out_len(usize::MAX), usize::MAX, "saturates rather than overflowing"); assert_eq!(kw.unwrap_out_max_len(40), 32); assert_eq!(kw.unwrap_out_max_len(0), 0); - // KWP: 1 to 2^32 - 1 octets of plaintext; 2 to 2^29 semiblocks of ciphertext. - assert!(!kwp_plaintext_len_is_valid(0)); - assert!(kwp_plaintext_len_is_valid(1)); - assert!(kwp_plaintext_len_is_valid(u32::MAX as usize)); - assert!(!kwp_plaintext_len_is_valid(u32::MAX as usize + 1)); - assert!(!kwp_ciphertext_len_is_valid(8)); - assert!(kwp_ciphertext_len_is_valid(16)); - assert!(!kwp_ciphertext_len_is_valid(20)); - assert!(kwp_ciphertext_len_is_valid(8 << 29)); - assert!(!kwp_ciphertext_len_is_valid((8 << 29) + 8)); - assert_eq!(kwp_wrapped_len(1), 16); - assert_eq!(kwp_wrapped_len(8), 16); - assert_eq!(kwp_wrapped_len(9), 24); - assert_eq!(kwp_wrapped_len(20), 32); - assert_eq!(kwp_wrapped_len(usize::MAX), usize::MAX, "saturates rather than overflowing"); + // KWP: 1 or more octets of plaintext; 2 or more whole semiblocks of ciphertext. + assert!(invalid(kwp.wrap_out(&[0u8; 0], &mut buf))); + assert_eq!(kwp.wrap_out(&[0u8; 1], &mut buf).unwrap(), 16); + assert!(invalid(kwp.unwrap_out(&[0u8; 8], &mut buf))); + assert!(invalid(kwp.unwrap_out(&[0u8; 20], &mut buf))); + assert!(!invalid(kwp.unwrap_out(&[0u8; 16], &mut buf))); + assert_eq!(kwp.wrap_out_len(1), 16); + assert_eq!(kwp.wrap_out_len(8), 16); + assert_eq!(kwp.wrap_out_len(9), 24); + assert_eq!(kwp.wrap_out_len(20), 32); + assert_eq!(kwp.wrap_out_len(usize::MAX), usize::MAX, "saturates rather than overflowing"); assert_eq!(kwp.unwrap_out_max_len(32), 24); - - // ... and the run-time API enforces them. - let mut buf = [0u8; 64]; - assert!(matches!( - kw.wrap_out(&[0u8; 8], &mut buf), - Err(SymmetricCipherError::InvalidInputLength(_)) - )); - assert!(matches!( - kw.wrap_out(&[0u8; 12], &mut buf), - Err(SymmetricCipherError::InvalidInputLength(_)) - )); - assert!(matches!( - kw.unwrap_out(&[0u8; 16], &mut buf), - Err(SymmetricCipherError::InvalidInputLength(_)) - )); - assert!(matches!( - kwp.unwrap_out(&[0u8; 20], &mut buf), - Err(SymmetricCipherError::InvalidInputLength(_)) - )); } /// KW and KWP wrap the same aligned data to different lengths only when it is short enough to From 687c21e0d621457ad10d195688fb55ae795754e7 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 6 Nov 2026 07:19:11 +1100 Subject: [PATCH 15/18] core: the key-wrap roles gain the allocating `wrap` / `unwrap` one-shots under `std` The roles had the fixed-length `wrap_key` / `unwrap_key` and the buffer-taking `wrap_out` / `unwrap_out`, but not the allocating form every other role trait provides beside its `_out` method. `wrap` and `unwrap` are provided over the worker's length helper and `_out` method, gated on `std` as `encrypt_with_aad` is. `unwrap` returns a `Vec`, which `Secret` cannot wrap, so its doc points at `unwrap_key` for a known length and asks the caller to zeroize otherwise. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core/src/symmetric_key.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crypto/core/src/symmetric_key.rs b/crypto/core/src/symmetric_key.rs index f9cdc09b9..1e6d120e1 100644 --- a/crypto/core/src/symmetric_key.rs +++ b/crypto/core/src/symmetric_key.rs @@ -1226,6 +1226,19 @@ pub trait KeyWrappingKey { ) -> Result { self.create_wrapper().wrap_out(plaintext, ciphertext) } + + /// As [`wrap_out`](Self::wrap_out), allocating the ciphertext. + /// + /// # Errors + /// As [`wrap_out`](Self::wrap_out). + #[cfg(feature = "std")] + fn wrap(&self, plaintext: &[u8]) -> Result, SymmetricCipherError> { + let wrapper = self.create_wrapper(); + let mut ciphertext = vec![0u8; wrapper.wrap_out_len(plaintext.len())]; + let written = wrapper.wrap_out(plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok(ciphertext) + } } /// A key that can unwrap keys; see [`KeyWrappingKey`] for the shape and the lifetime of the @@ -1282,4 +1295,19 @@ pub trait KeyUnwrappingKey { ) -> Result { self.create_unwrapper().unwrap_out(ciphertext, plaintext) } + + /// As [`unwrap_out`](Self::unwrap_out), allocating the plaintext, sized to what was recovered. + /// A `Vec` is not zeroized on drop: prefer [`unwrap_key`](Self::unwrap_key) when the length is + /// known at compile time, and zeroize this one when done with it. + /// + /// # Errors + /// As [`unwrap_out`](Self::unwrap_out). + #[cfg(feature = "std")] + fn unwrap(&self, ciphertext: &[u8]) -> Result, SymmetricCipherError> { + let unwrapper = self.create_unwrapper(); + let mut plaintext = vec![0u8; unwrapper.unwrap_out_max_len(ciphertext.len())]; + let written = unwrapper.unwrap_out(ciphertext, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } } From 81d094967def74d7bf4066b8f368d56ecac666d0 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 6 Nov 2026 07:19:11 +1100 Subject: [PATCH 16/18] core-test-framework: the key-wrap suite covers the allocating `wrap` / `unwrap` They are checked against the fixed-length answers in `test` and `test_kat`, and for the invalid-length and modified-ciphertext errors. Assisted-by: Claude:claude-mythos-5-1 --- crypto/core-test-framework/src/key_wrap.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crypto/core-test-framework/src/key_wrap.rs b/crypto/core-test-framework/src/key_wrap.rs index 8ba3c9c3f..c802e55e7 100644 --- a/crypto/core-test-framework/src/key_wrap.rs +++ b/crypto/core-test-framework/src/key_wrap.rs @@ -121,6 +121,11 @@ impl TestFrameworkKeyWrap { assert_eq!(kek.create_unwrapper().unwrap_out(&ct, &mut pt[..max_pt_len]).unwrap(), KEY_LEN); assert_eq!(&pt[..KEY_LEN], &key); + /* Allocating one-shots agree with the fixed-length API */ + + assert_eq!(kek.wrap(&key).unwrap(), ct); + assert_eq!(kek.unwrap(&ct).unwrap(), key); + // several workers in flight from one key, and the key unchanged by use let a = kek.create_wrapper(); let b = kek.create_unwrapper(); @@ -163,6 +168,7 @@ impl TestFrameworkKeyWrap { _ => panic!("Should have rejected an empty plaintext"), } assert!(buf.iter().all(|b| *b == 0), "ciphertext buffer not zeroized"); + assert!(matches!(kek.wrap(&[]), Err(SymmetricCipherError::InvalidInputLength(_)))); for bad_ct_len in [0, semiblock_len, CT_LEN - 1] { pt.fill(0xFF); match kek.unwrap_out(&ct[..bad_ct_len], &mut pt) { @@ -170,6 +176,10 @@ impl TestFrameworkKeyWrap { _ => panic!("Should have rejected a ciphertext of {bad_ct_len} bytes"), } assert!(pt.iter().all(|b| *b == 0), "plaintext buffer not zeroized"); + assert!(matches!( + kek.unwrap(&ct[..bad_ct_len]), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); } /* Error case: modified ciphertext */ @@ -196,6 +206,7 @@ impl TestFrameworkKeyWrap { out.fill(0xFF); assert!(kek.unwrap_key_out(&bad_ct, &mut out).is_err()); assert!(out.iter().all(|b| *b == 0), "the Secret was not zeroized on error"); + assert!(matches!(kek.unwrap(&bad_ct), Err(SymmetricCipherError::DecryptionFailed))); } /* Error case: wrong KEK */ @@ -240,6 +251,9 @@ impl TestFrameworkKeyWrap { let bytes_written = kek.unwrap_out(expected_ct, &mut buf).unwrap(); assert_eq!(&buf[..bytes_written], key); + + assert_eq!(kek.wrap(key).unwrap(), expected_ct); + assert_eq!(kek.unwrap(expected_ct).unwrap(), key); } } From 6ffae4cb27c89cf7d2c59c74f4f0ca90e72b4b81 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 6 Nov 2026 07:19:11 +1100 Subject: [PATCH 17/18] aes: the AES_KW_128 run-time-length example uses the allocating pair Assisted-by: Claude:claude-mythos-5-1 --- crypto/aes/src/kw.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crypto/aes/src/kw.rs b/crypto/aes/src/kw.rs index c89333a17..d5036e85c 100644 --- a/crypto/aes/src/kw.rs +++ b/crypto/aes/src/kw.rs @@ -83,11 +83,9 @@ use bouncycastle_cipher::modes::{Kw, Kwp}; /// assert_eq!(*kek.unwrap_key::<7, 16>(&wrapped).expect("unwrapping"), key_data); /// /// let payload = b"an encoded private key, of whatever length it happens to be"; -/// let mut wrapped = vec![0u8; kek.wrap_out_len(payload.len())]; -/// let n = kek.wrap_out(payload, &mut wrapped).expect("wrapping"); -/// let mut recovered = vec![0u8; kek.unwrap_out_max_len(n)]; -/// let m = kek.unwrap_out(&wrapped[..n], &mut recovered).expect("unwrapping"); -/// assert_eq!(&recovered[..m], payload); +/// let wrapped = kek.wrap(payload).expect("wrapping"); +/// assert_eq!(wrapped.len(), kek.wrap_out_len(payload.len())); +/// assert_eq!(kek.unwrap(&wrapped).expect("unwrapping"), payload); /// ``` /// /// # Memory Usage From 198577a287f478013b52214dd3cdcf266994b447 Mon Sep 17 00:00:00 2001 From: David Hook Date: Fri, 6 Nov 2026 07:19:11 +1100 Subject: [PATCH 18/18] cli: the aes*-kw and aes*-kwp commands call the key's `wrap` / `unwrap` The command no longer sizes and truncates its own output buffer. Assisted-by: Claude:claude-mythos-5-1 --- cli/src/aes_kw_cmd.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/cli/src/aes_kw_cmd.rs b/cli/src/aes_kw_cmd.rs index 44d769322..56d5ec7b1 100644 --- a/cli/src/aes_kw_cmd.rs +++ b/cli/src/aes_kw_cmd.rs @@ -17,8 +17,8 @@ //! # No IV //! //! Key wrap is deterministic: the same input under the same KEK gives the same output, and there -//! is nothing to prepend or strip. The output is exactly `wrap_out_len` of the input: one 8-byte -//! semiblock longer for KW, the input rounded up to a multiple of 8 plus 8 for KWP. +//! is nothing to prepend or strip. The output is one 8-byte semiblock longer than the input for +//! KW, and the input rounded up to a multiple of 8 plus 8 for KWP. //! //! # Input lengths //! @@ -149,22 +149,13 @@ where exit(-1); } - let (result, mut output) = match direction { - CipherDirection::Encrypt => { - let mut output = vec![0u8; kek.wrap_out_len(input.len())]; - (kek.wrap_out(&input, &mut output), output) - } - CipherDirection::Decrypt => { - let mut output = vec![0u8; kek.unwrap_out_max_len(input.len())]; - (kek.unwrap_out(&input, &mut output), output) - } + let result = match direction { + CipherDirection::Encrypt => kek.wrap(&input), + CipherDirection::Decrypt => kek.unwrap(&input), }; match result { - Ok(n) => { - output.truncate(n); - write_bytes_or_hex(&output, output_hex); - } + Ok(output) => write_bytes_or_hex(&output, output_hex), Err(SymmetricCipherError::DecryptionFailed) => { eprintln!( "Error: {alg} unwrap failed: the input is not an authentic {alg} ciphertext under \