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..56d5ec7b1 --- /dev/null +++ b/cli/src/aes_kw_cmd.rs @@ -0,0 +1,176 @@ +//! 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} -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_object`]), +//! 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 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 +//! +//! `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 `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 +//! +//! 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 -d encrypt --key 000102030405060708090a0b0c0d0e0f -x +//! ``` + +use crate::helpers::block_mode_helpers::{CipherDirection, load_key_object}; +use crate::helpers::write_bytes_or_hex; +use bouncycastle::aes::{AES128Key, AES192Key, AES256Key}; +use bouncycastle::cipher::modes::{KW, KWP}; +use bouncycastle::core::errors::SymmetricCipherError; +use bouncycastle::core::symmetric_key::{KeyUnwrappingKey, KeyWrappingKey}; +use std::io::Read; +use std::process::exit; + +pub(crate) fn aes128_kw_cmd( + direction: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 16>( + direction, + &load_key_object::, 16>(key, key_file, "AES-128-KW"), + output_hex, + "AES-128-KW", + ); +} + +pub(crate) fn aes192_kw_cmd( + direction: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 24>( + direction, + &load_key_object::, 24>(key, key_file, "AES-192-KW"), + output_hex, + "AES-192-KW", + ); +} + +pub(crate) fn aes256_kw_cmd( + direction: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 32>( + direction, + &load_key_object::, 32>(key, key_file, "AES-256-KW"), + output_hex, + "AES-256-KW", + ); +} + +pub(crate) fn aes128_kwp_cmd( + direction: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 16>( + direction, + &load_key_object::, 16>(key, key_file, "AES-128-KWP"), + output_hex, + "AES-128-KWP", + ); +} + +pub(crate) fn aes192_kwp_cmd( + direction: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::<_, 24>( + direction, + &load_key_object::, 24>(key, key_file, "AES-192-KWP"), + output_hex, + "AES-192-KWP", + ); +} + +pub(crate) fn aes256_kwp_cmd( + direction: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + 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`, 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) { + eprintln!("Error: couldn't read stdin: {e}"); + exit(-1); + } + + let result = match direction { + CipherDirection::Encrypt => kek.wrap(&input), + CipherDirection::Decrypt => kek.unwrap(&input), + }; + + match result { + 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 \ + 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..ff4c0e98f 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,166 @@ enum Subcommands { x: bool, }, + /// 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 `encrypt` or + /// consumed on `decrypt`. + /// + /// 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 + /// `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 + /// 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 { + #[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. + #[arg(long)] + key: Option, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 { + #[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. + #[arg(long)] + key: Option, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 { + #[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. + #[arg(long)] + key: Option, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 `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 `decrypt` and the non-streaming + /// note. + AES128_KWP { + #[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. + #[arg(long)] + key: Option, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 { + #[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. + #[arg(long)] + key: Option, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 { + #[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. + #[arg(long)] + key: Option, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// The ML-KEM-512 key encapsulation algorithm. MLKEM512 { action: mlkem_cmd::MLKEMAction, @@ -3008,6 +3169,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 { direction, key, key_file, x }) => { + aes_kw_cmd::aes128_kw_cmd(direction, 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 { direction, key, key_file, x }) => { + aes_kw_cmd::aes256_kw_cmd(direction, 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 { direction, key, key_file, x }) => { + aes_kw_cmd::aes192_kwp_cmd(direction, 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/test_aes_kw.sh b/cli/tests/test_aes_kw.sh new file mode 100755 index 000000000..aa679c9e0 --- /dev/null +++ b/cli/tests/test_aes_kw.sh @@ -0,0 +1,204 @@ +#!/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" + # 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() { + 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 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 new file mode 100644 index 000000000..d5036e85c --- /dev/null +++ b/crypto/aes/src/kw.rs @@ -0,0 +1,128 @@ +//! 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). 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 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 +/// +/// 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>; + +/// 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<'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<'k> = Kw<'k, AES256Internal>; + +/// 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<'k> = Kwp<'k, AES128Internal>; + +/// 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<'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<'k> = Kwp<'k, AES256Internal>; 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/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 f46341bd3..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` | nothing yet (key wrap is unimplemented) | +//! | `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 new file mode 100644 index 000000000..674b0a070 --- /dev/null +++ b/crypto/aes/tests/kw_bc-test-data.rs @@ -0,0 +1,230 @@ +//! 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. +//! +//! 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::{AES128Key, AES192Key, AES256Key}; +use bouncycastle_cipher::modes::{KW, KWP}; +use bouncycastle_core::errors::SymmetricCipherError; +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; + +#[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"; + +/// 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 of the key `kek`. +fn run_case( + kek: &K, + input: &[u8], + encrypt: bool, + expected: &Expected, + tc_id: u64, +) where + K: KeyWrappingKey + KeyUnwrappingKey, +{ + if encrypt { + let Expected::Bytes(ct) = expected else { + panic!("tcId {tc_id}: an encrypt case cannot expect failure"); + }; + 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 => 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; 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 => 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 => { + 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 => 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" + ); + } + } + } +} + +/// 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 + M: Mode + + Mode + + Mode, + AES128Key: KeyWrappingKey<16> + KeyUnwrappingKey<16>, + AES192Key: KeyWrappingKey<24> + KeyUnwrappingKey<24>, + AES256Key: KeyWrappingKey<32> + KeyUnwrappingKey<32>, +{ + 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. + 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", tc_id); + let (input, expected) = if encrypt { + (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", tc_id), Expected::Failure) + } else { + (hex_field(test, "ct", tc_id), Expected::Bytes(hex_field(answer, "pt", tc_id))) + }; + + match key_len { + 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}"), + } + + 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/kw_tests.rs b/crypto/aes/tests/kw_tests.rs new file mode 100644 index 000000000..032c64602 --- /dev/null +++ b/crypto/aes/tests/kw_tests.rs @@ -0,0 +1,248 @@ +//! `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 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::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; + +fn bytes(hex_str: &str) -> [u8; N] { + hex::decode(hex_str).expect("valid hex").try_into().expect("the expected length") +} + +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 +/// 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, AES128Key>( + &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, AES192Key>( + &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, AES256Key>( + &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, AES192Key>( + &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, AES256Key>( + &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, AES256Key>( + &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, AES192Key>( + &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, AES192Key>( + &bytes(RFC5649_KEK), + &bytes("466f7250617369"), + &bytes("afbeb0f07dfbf5419200f2ccb50bb24f"), + ); +} + +// ---- 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, 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] +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, 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 --------------------------------------------------------------- + +/// The three key lengths are distinct algorithms: the same data under the "same" KEK bytes gives +/// three different ciphertexts. +#[test] +fn the_three_key_lengths_are_not_interchangeable() { + let data = [0x5Au8; 16]; + 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 keys hand out exactly the aliased workers (whose names are pinned in `alg_name_tests`). +#[test] +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(); +} + +/// The sizes the docs quote: the key is the schedule, the worker is the borrow. +#[test] +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>()); +} + +/// Equality follows the material: the same bytes compare equal, as does a clone, and different +/// bytes do not. +#[test] +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])); +} 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 new file mode 100644 index 000000000..e7f8ad1c6 --- /dev/null +++ b/crypto/cipher/src/modes/kw.rs @@ -0,0 +1,473 @@ +//! 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::modes::kwp::Kwp). +//! +//! 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 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 toy_key::ToyKey; +//! use bouncycastle_cipher::modes::KW; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! 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 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] = 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, 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())); +//! assert_eq!(wrapped_out[..n], wrapped); +//! ``` +//! +//! 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.) +//! +//! ``` +//! # 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 toy_key::ToyKey; +//! # use bouncycastle_cipher::modes::KW; +//! # use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! # 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 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!( +//! key.unwrap_key::<32, 40>(&wrapped), +//! Err(SymmetricCipherError::DecryptionFailed) +//! )); +//! ``` +//! +//! # No direction marker +//! +//! 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 +//! +//! 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 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::hazmat::{ + BlockPermutation, ElectronicCodeBook, KeyUnwrapMode, KeyWrapMode, Mode, +}; +use bouncycastle_core::key_material::KeyType; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, KeyUnwrapper, KeyWrapper}; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; + +/// 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(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 +} + +/// Whether `len` bytes is a ciphertext length KW-AD is defined on: 3 to 2^54 whole semiblocks +/// (SP 800-38F Table 1). +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 +} + +/// 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(crate) 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: 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 + // 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: BlockPermutation, +{ + 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"); +} + +/// 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`. +/// +/// 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: BlockPermutation + ModeNames, +{ + perm: &'k P, +} + +impl<'k, P> Kw<'k, P> +where + P: BlockPermutation + ModeNames, +{ + /// 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 } + } +} + +impl<'k, P> Algorithm for Kw<'k, P> +where + P: BlockPermutation + ModeNames, +{ + /// 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<'k, P> KeyWrapper for Kw<'k, P> +where + 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( + &self, + 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(key, ciphertext) + } + + /// One semiblock more than the plaintext. + fn wrap_out_len(&self, plaintext_len: usize) -> usize { + kw_wrapped_len(plaintext_len) + } + + /// KW-AE (SP 800-38F Algorithm 3). + fn wrap_out( + &self, + 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)); + } + 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", + )); + } + + // 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(self.perm, a, r); + Ok(ct_len) + } +} + +impl<'k, P> KeyUnwrapper for Kw<'k, P> +where + 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( + &self, + 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(ciphertext, &mut key[..]) + } + + /// One semiblock less than the ciphertext: exact for KW. + 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( + &self, + 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)); + } + 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)", + )); + } + + // 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(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. + 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) + } +} + +#[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 new file mode 100644 index 000000000..9c26d69e7 --- /dev/null +++ b/crypto/cipher/src/modes/kwp.rs @@ -0,0 +1,384 @@ +//! 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::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 +//! +//! 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 toy_key::ToyKey; +//! use bouncycastle_cipher::modes::KWP; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! 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 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] = 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] = key.wrap_key(&short).expect("wrapping"); +//! assert_eq!(*key.unwrap_key::<5, 16>(&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 super::ModeNames; +use super::kw::{KW_BLOCK_LEN, SEMIBLOCK_LEN, w, w_inv}; +use bouncycastle_core::errors::SymmetricCipherError; +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, KeyUnwrapper, KeyWrapper}; +use bouncycastle_utils::ct::{ct_eq_bytes, ct_eq_zero_bytes}; +use bouncycastle_utils::secret::Secret; + +/// 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(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(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 +} + +/// 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(crate) const fn kwp_wrapped_len(len: usize) -> usize { + len.div_ceil(SEMIBLOCK_LEN).saturating_mul(SEMIBLOCK_LEN).saturating_add(SEMIBLOCK_LEN) +} + +/// 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: BlockPermutation + ModeNames + Clone, +{ + 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 [`KWP`]'s mode impls + /// build for a key's roles. + pub(crate) fn from_perm(perm: &'k P) -> Self { + Self { perm } + } +} + +impl<'k, P> Algorithm for Kwp<'k, P> +where + P: BlockPermutation + ModeNames, +{ + /// 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<'k, P> KeyWrapper for Kwp<'k, P> +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 `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], + 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(key, ciphertext) + } + + /// The plaintext rounded up to a whole number of semiblocks, plus one. + fn wrap_out_len(&self, plaintext_len: usize) -> usize { + kwp_wrapped_len(plaintext_len) + } + + /// KWP-AE (SP 800-38F Algorithm 5). + fn wrap_out( + &self, + 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)); + } + 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)", + )); + } + + // 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::(); + 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(self.perm, &mut a[0], r); + } + Ok(ct_len) + } +} + +impl<'k, P> KeyUnwrapper for Kwp<'k, P> +where + 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. + /// + /// 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( + &self, + 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(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(&self, ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(SEMIBLOCK_LEN) + } + + /// KWP-AD (SP 800-38F Algorithm 6). + fn unwrap_out( + &self, + 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)); + } + 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)", + )); + } + + // 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); + self.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(self.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) + } +} + +#[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 b6f4335cb..3f8821c1c 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,12 @@ //! [`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. 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 //! none at all (`INIT_DATA_LEN = 0`) and is the raw permutation applied block by block, which is @@ -44,7 +52,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 +176,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 +189,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 +222,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 +234,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..893277551 --- /dev/null +++ b/crypto/cipher/tests/modes/kw_tests.rs @@ -0,0 +1,233 @@ +//! 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, +//! which the per-byte toy deliberately lacks, so a flipped register byte never reaches the ICV. + +mod common; +#[path = "../common/toy_key.rs"] +#[allow(dead_code)] +mod toy_key; + +use bouncycastle_cipher::modes::{KW, KWP, Kw, Kwp, ModeNames}; +use bouncycastle_core::errors::SymmetricCipherError; +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}; +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 the workers of the mode's own route -- the hazmat mode traits the +/// key forwards to -- agree. +#[test] +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] = + >::wrap_init(&state).wrap_key(&data).unwrap(); + assert_eq!(from_key, from_worker); + 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); + 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] = + >::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]); +} + +// ---- 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 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_rules_match_table_1() { + let kw = kw_key(); + let kwp = kwp_key(); + 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 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); +} + +/// 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::()); +} 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..c802e55e7 --- /dev/null +++ b/crypto/core-test-framework/src/key_wrap.rs @@ -0,0 +1,488 @@ +//! 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, KeyType}; +use bouncycastle_core::symmetric_key::{FromKeyMaterial, KeyUnwrappingKey, KeyWrappingKey}; +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 Default for TestFrameworkKeyWrap { + fn default() -> Self { + Self::new() + } +} + +impl TestFrameworkKeyWrap { + /// Create a new instance of the test framework. + pub fn new() -> Self { + Self {} + } + + /// 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[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, on the key and on the workers */ + + 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!(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 = kek.wrap_key::(&key).unwrap(); + + // wrap_key() and wrap_key_out() agree, on the key and on the worker + let mut ct_out = [0u8; CT_LEN]; + 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, kek.wrap_key::(&key).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 = 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 = 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 = kek.wrap_out(&key, &mut buf).unwrap(); + assert_eq!(bytes_written, CT_LEN); + + let mut pt = [0u8; BUF_LEN]; + 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); + + /* 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(); + 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 */ + + // 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"); + 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) { + 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"); + assert!(matches!( + kek.unwrap(&ct[..bad_ct_len]), + Err(SymmetricCipherError::InvalidInputLength(_)) + )); + } + + /* 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 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 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"); + assert!(matches!(kek.unwrap(&bad_ct), Err(SymmetricCipherError::DecryptionFailed))); + } + + /* Error case: wrong KEK */ + + 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"), + } + 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( + &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 = kek.wrap_key::(key).unwrap(); + assert_eq!(&ct, expected_ct); + + 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 = kek.wrap_out(key, &mut buf).unwrap(); + assert_eq!(&buf[..bytes_written], expected_ct); + + 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); + } +} + +#[cfg(test)] +mod tests { + //! 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; + + 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 MockKeyWrapKey { + kek: [u8; MOCK_KEK_LEN], + } + + /// The worker: the KEK bytes, borrowed. + struct MockKeyWrap<'k> { + kek: &'k [u8; MOCK_KEK_LEN], + } + + impl Algorithm for MockKeyWrap<'_> { + const ALG_NAME: &'static str = "MockKeyWrap-NOT-CRYPTOGRAPHY"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + } + + 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() }) + } + } + + 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 } + } + } + + /// 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( + &self, + 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(key, ciphertext) + } + + fn wrap_out_len(&self, plaintext_len: usize) -> usize { + plaintext_len + .div_ceil(MOCK_SEMIBLOCK_LEN) + .saturating_mul(MOCK_SEMIBLOCK_LEN) + .saturating_add(MOCK_SEMIBLOCK_LEN) + } + + fn wrap_out( + &self, + 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)); + } + if plaintext.is_empty() || plaintext.len() > u32::MAX as usize { + return Err(SymmetricCipherError::InvalidInputLength("plaintext length")); + } + let s = &mut ciphertext[..ct_len]; + 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); + xor_with_kek(self.kek, s); + Ok(ct_len) + } + } + + impl KeyUnwrapper for MockKeyWrap<'_> { + fn unwrap_key_out( + &self, + 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 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) + } + } + } + + fn unwrap_out_max_len(&self, ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(MOCK_SEMIBLOCK_LEN) + } + + fn unwrap_out( + &self, + 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)); + } + if ciphertext.len() < 2 * MOCK_SEMIBLOCK_LEN + || !ciphertext.len().is_multiple_of(MOCK_SEMIBLOCK_LEN) + { + return Err(SymmetricCipherError::InvalidInputLength("ciphertext length")); + } + let kek = self.kek; + + // 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] = + MockKeyWrapKey::from_key_material(&kek_material).unwrap().wrap_key(&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/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..1e6d120e1 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,149 @@ 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) + } + + /// 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 +/// 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) + } + + /// 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) + } +} diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index f474bed1a..dffe262cf 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,179 @@ pub trait KEMPublicKey: fn from_bytes(bytes: &[u8]) -> Result; } +/// 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 (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` +/// +/// 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: 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> + 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); + 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::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 + 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 + /// (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(&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 + /// 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; and [`SymmetricCipherError::DecryptionFailed`] for any + /// authenticity failure. The whole `plaintext` buffer is zeroized before anything else, so + /// nothing stale survives an error. + fn unwrap_out( + &self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result; +} + +/// 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 +/// +/// 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: 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> + 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); + 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 + /// 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 + 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 + /// `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(&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, and [`SymmetricCipherError::InvalidInputLength`] for a plaintext length + /// the algorithm is not defined on. `ciphertext` is zeroized before anything else, so nothing + /// stale survives an error. + fn wrap_out( + &self, + 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. 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(_, _);