diff --git a/CLAUDE.md b/CLAUDE.md index 8e39e744..78d357f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,9 +69,15 @@ Quality / mutation testing: ``` ./dev_scripts/quality_stats.sh ./crypto # lines-of-code, docstring & fallibility metrics; CI publishes this -cargo mutants # config in .cargo/mutants.toml (output: custom_mutants_output/) +cargo mutants -p bouncycastle-sha3 # config in .cargo/mutants.toml (output: custom_mutants_output/) ``` +`-p` is as non-optional here as `--workspace` is for build and test, and for the same reason: a bare +`cargo mutants` examines only the root `bouncycastle` package, whose single `src/lib.rs` yields no +mutants, so it prints "No mutants found under the active filters" and exits **0**. See +[the mutation-testing mechanics](#notes-on-testing) for scoping a run to one file, for crates whose +tests live elsewhere, and for the test-data symlink. + Stack-memory benches are separate binaries under `mem_usage_benches/src/`, each declared as a `[[bin]]` in that crate's `Cargo.toml`: @@ -130,7 +136,11 @@ Repo mechanics behind those rules, which the documents don't spell out: - `./dev_scripts/quality_stats.sh` produces the fallibility metrics both documents ask you to check. Run it before and after a change and compare, rather than eyeballing the diff. - **CLI commands stream.** The `cli/` binary is stdin→stdout with ~1 KB buffers so commands compose in shell - pipelines; preserve that when adding subcommands. + pipelines; preserve that when adding subcommands. The exception is a construction that is not + itself streamable, such as CCM (SP 800-38C Sec 3: "CCM is not designed to support partial + processing or stream processing", because the payload length is inside the first block the MAC + covers) -- there, read the whole input once and process it in place, rather than adding a second + buffer the size of the input on top of it; see `aes_ccm_cmd.rs`. - Trait → factory → CLI is the wiring path for a new primitive; see [the workspace architecture](#the-core--core-test-framework--factory-spine) above for the crates involved. ## Scope of changes @@ -180,7 +190,11 @@ Rules when working from the downloaded copy: What a crate must be tested against — including the mutation-testing expectation, the trait test framework, and the external vector suites — is specified in QUALITY_AND_STYLE.md and CONTRIBUTING.md. Repo-specific mechanics: -- `cargo mutants` is expected to be run on each crate; surviving mutants must be investigated but not all need to die (e.g. XOR/OR equivalences in crypto code are acceptable). Config lives in `.cargo/mutants.toml` (output dir `custom_mutants_output/`). +- `cargo mutants` is expected to be run on each crate; surviving mutants must be investigated but not all need to die (e.g. XOR/OR equivalences in crypto code are acceptable). Config lives in `.cargo/mutants.toml` (output dir `custom_mutants_output/`). Four things about running it here: + - **Always pass `-p `.** Without it only the root package is examined, which has no mutants, and the run "passes" vacuously — see [Common commands](#common-commands). + - **`-f`/`--file` does nothing while the checked-in config is in play**, because its `examine_globs` wins over the CLI filter: `cargo mutants -p bouncycastle-sha3 -f '**/kmac.rs'` still examines all ~874 mutants in the crate. To scope a run to the files you changed, copy `.cargo/mutants.toml` somewhere outside the repo, delete its `examine_globs` block, and pass `--config `; `-f` then filters as documented. (`--config /dev/null` also works but throws away `skip_calls`, `error_values`, `cap_lints` and the timeout multipliers with it.) + - **Add `--test-workspace true` when a crate's mutants are killed by another crate's tests.** The `core` traits are the case that matters: their default method bodies are exercised from `sha3` and `factory`, so a `-p bouncycastle-core` run alone reports them all as missed. + - **Symlink the test data into `/tmp`.** `cargo mutants` copies the tree to `/tmp/cargo-mutants--XXXX.tmp/`, so the `../../../bc-test-data/...` paths the vector suites use resolve to `/tmp/bc-test-data`. Without `ln -s /bc-test-data /tmp/bc-test-data` those tests print their "not found" warning, pass vacuously, and every mutant they would have killed is reported as missed. Use `--jobs 3` and an explicit `--timeout`; note that a mutant which makes a squeeze return no bytes hangs a fill loop for real, so some timeouts are kills rather than false alarms. - Integration tests in `tests/` are preferred over in-file `#[cfg(test)] mod tests` blocks — see "Unit tests vs integration tests" in QUALITY_AND_STYLE.md for the reasoning and the exceptions. A unit test is justified for high-risk code that has known-answer values and cannot be reached through the public API; when you write one, all of its helpers go inside that `mod tests`. - A property that can be asserted at compile time (`const _: () = assert!(...)`) stays a compile-time assertion even when a test also covers it: `cargo mutants` cannot see a const assertion fail, so pair the two rather than trading the guarantee for the coverage. - For traits in `core`, the canonical tests live in `core-test-framework` and are invoked from each implementor's integration tests — don't duplicate them per-implementation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5f250d37..cf6575c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,10 +43,10 @@ Some specifics: * Public APIs of a library should be both ergonomic and expressive. When defining a new trait or public function, ask yourself whether a programmer who is new to cryptography is likely to use this in a way that will get them into trouble. -* Variables should be well-named, well-structured, and well-commented (a comment-to-code ration of 1:1 is a goal to be +* Variables should be well-named, well-structured, and well-commented (a comment-to-code ratio of 1:1 is a goal to be strived for!). Think about memory footprint and, where possible, use unnamed scopes to allow the compiler to pop intermediate value variables off the stack as soon as they are no longer needed. -* Always run your code through `cargo mutants` and get the issue count as low as your can. As a first pass, this forces +* Always run your code through `cargo mutants` and get the issue count as low as you can. As a first pass, this forces you to write thorough unit tests. As a second pass, this draws your attention to bits of your code that cannot be tested from the outside. Often this means that the code can be simplified without affecting functionality (as defined by your set of unit tests) -- "simpler code" usually means faster runtime and easier future maintenance. @@ -71,7 +71,7 @@ For minor updates, you can instead choose to create an issue with short snippets * For contributions touching multiple files try and split up the pull request, smaller changes are easier to review and test, as well as being less likely to run into merge issues. -* Create a test cases for your change, it may be a simple addition to an existing test. If you do not know how to do +* Create test cases for your change; it may be a simple addition to an existing test. If you do not know how to do this, ask us and we will help you. * If you run into any merge issues, check out this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues. diff --git a/Cargo.toml b/Cargo.toml index 63f0d999..7aa567d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ version = "0.1.3" # *** Internal Dependencies *** bouncycastle = { path = "./" } bouncycastle-aes = { path = "./crypto/aes" } +bouncycastle-ascon = { path = "./crypto/ascon" } bouncycastle-base64 = { path = "./crypto/base64" } bouncycastle-modes = { path = "./crypto/modes" } bouncycastle-core = { path = "crypto/core" } @@ -46,6 +47,7 @@ edition.workspace = true [dependencies] bouncycastle-aes.workspace = true +bouncycastle-ascon.workspace = true bouncycastle-base64.workspace = true bouncycastle-core.workspace = true bouncycastle-factory.workspace = true diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index d5185528..4ce74352 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -5,6 +5,46 @@ * New algorithms added to crypto/ : * SM3 -- the SM3 hash (GB/T 32905-2016 / ISO/IEC 10118-3:2018), ported from bc-java. * AES -- AES-128/192/256, along with its modes AES_ECB, AES_CBC, AES_GCM. + * ASCON -- Ascon-AEAD128, Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 (NIST SP 800-232). + `AsconAead128Encryptor` / `AsconAead128Decryptor` implement the generated-nonce + `AEADCipherEncryptor` / `AEADCipherDecryptor` pair; the inherent `AsconAead128` API keeps the + explicit-nonce, in-place streaming form (`new_encrypting` / `new_decrypting`). + `Ascon_AEAD128` names the pair by direction (`Ascon_AEAD128` / + `Ascon_AEAD128`). + * `bouncycastle-ascon` is re-exported as `bouncycastle::ascon`; `Ascon-Hash256` and + `Ascon-XOF128` are registered in the factories, and the CLI adds `ascon-hash256`, + `ascon-xof128`, `ascon-cxof128` and `ascon-aead128`. The AEAD command generates and prefixes + the nonce by default, with `--nonce`/`--nonce-file` retained for deterministic vectors. + Streaming decrypt releases plaintext before the final tag check, so callers must discard any + output if finalization or the CLI exit status reports authentication failure. +* `core` gains the streaming AEAD split: `AEADCipherEncryptor` and `AEADCipherDecryptor<...>`, which extend `SymmetricCipherEncryptor` / `SymmetricCipherDecryptor<...>`. The inherited methods are the AEAD + with no associated data and the tag inline (`ciphertext || tag`), so an AEAD can be held and + used as a plain symmetric cipher; `FINAL_LEN` is the tag plus anything the cipher holds back, + and every decryptor holds back the last `TAG_LEN` bytes it has seen, since it cannot know which + layout its final call will ask for. The AEAD traits add `do_update_aad`; the detached-tag + methods, each named for the base method it mirrors plus `_detached` (`do_final_detached` / + `do_final_out_detached`, `encrypt_out_detached`, `encrypt_out_rng_detached`, `encrypt_detached`, + `decrypt_out_detached`, `decrypt_detached` and the `*_len_detached` sizing helpers); and the + inline-tag one-shots with AAD, named for their base method plus `_with_aad` + (`encrypt_out_with_aad`, `encrypt_out_rng_with_aad`, `encrypt_with_aad`, `decrypt_out_with_aad`, + `decrypt_with_aad`). + `SymmetricCipherDecryptor::decrypt_out` now zeroizes what it wrote when `do_final` fails, as the + AEAD one-shots always have. + The older single-type `core::traits::AEADCipher`, which this splits and which had no + implementors, is removed, along with its `core-test-framework` suites + (`TestFrameworkAEADCipher::test` / `::test_plain_one_shots`). + Mutation testing of the pair's defaults (`traits.rs`, scoped to `AEADCipher{En,De}cryptor` and + `SymmetricCipherDecryptor::decrypt_out`, tested through `bouncycastle-core` + + `bouncycastle-ascon`) reports 134 mutants, 107 caught, 27 unviable and none missed; the + `AsconAead128Encryptor` / `AsconAead128Decryptor` adapters report 76 mutants, 49 caught, 27 + unviable and none missed. +* ASCON testing covers the NIST LWC KAT sweeps from `bc-test-data` (1089 AEAD128, 1025 Hash256, + 1025 XOF128 and 1089 CXOF128 cases when the data repository is present), plus embedded always-on + vectors. Mutation testing for `bouncycastle-ascon` reports 661 mutants, 558 caught, 97 unviable + and 6 missed; the six survivors are the sponge boundary and `set_state_byte` OR/XOR equivalences + documented at their sites. ## Minor features / bug fixes diff --git a/cli/src/aead_mode_cmd.rs b/cli/src/aead_mode_cmd.rs new file mode 100644 index 00000000..7438d57b --- /dev/null +++ b/cli/src/aead_mode_cmd.rs @@ -0,0 +1,184 @@ +//! Shared plumbing for the AEAD subcommands: `aes{128,192,256}-gcm`. +//! +//! Parallel to [`crate::stream_mode_cmd`], but for [`bouncycastle::modes::Gcm`] rather than a +//! [`StreamCipherEncryptor`](bouncycastle::core::traits::StreamCipherEncryptor) mode: GCM carries +//! additional authenticated data and a tag, neither of which that trait has room for, so this +//! module drives it through [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] instead, which add +//! `do_update_aad` to the symmetric-cipher streaming methods. +//! +//! # On-the-wire format: `nonce || ciphertext || tag` +//! +//! `encrypt` writes the generated 12-byte nonce first, then the ciphertext as it streams, then the +//! 16-byte tag once stdin is exhausted. `decrypt` reads the 12-byte nonce first, then streams the +//! rest of stdin through the inline decryptor -- which, per [`SymmetricCipherDecryptor`]'s contract, +//! holds back the last 16 bytes it has seen because they might be the tag -- and checks the tag on +//! `do_final`. +//! +//! # The exit code is the signal, not the output +//! +//! On a tag failure, `decrypt` has **already written plaintext to stdout**: the inline decryptor +//! releases bytes as they clear the tail hold-back, well before the tag at the very end of the +//! stream can be checked. This is the same trade-off `Gcm`'s streaming API documents; a script that +//! needs to know before acting on the output must use the one-shot instead (not exposed by this +//! CLI) or check the exit code before trusting anything already written. On failure this command +//! prints `Error: authentication failed` to stderr and exits non-zero. +//! +//! # AAD +//! +//! `--aad ` or `--aad-file `; if neither is given, AAD is empty. Fed to the engine in +//! one call before any ciphertext, matching SP 800-38D Algorithm 4's requirement that AAD precede +//! data. +//! +//! `--aad-file` is read as raw bytes ([`read_from_file_raw`]), never hex-decoded. The +//! hex-or-raw guess `--key-file` uses would change what is authenticated without any error: a +//! binary header that happens to parse as hex text (`cafe`, or sixteen zero bytes, which the hex +//! decoder skips) would be authenticated as its decoding, and the tag would not verify against any +//! other GCM implementation given the same file. + +use crate::helpers::{read_from_file_raw, write_bytes_or_hex}; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, ElectronicCodeBook, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle::hex; +use bouncycastle::modes::{Decrypting, Encrypting, Gcm}; +use std::io; +use std::io::{Read, Write}; +use std::process::exit; + +/// Bytes read from stdin per call. GCM has no batching advantage from a larger chunk the way CTR's +/// four-block path does, so this matches the other streaming commands' 1 KiB rather than needing +/// its own tuning. +const CHUNK_LEN: usize = 1024; + +/// Loads the additional authenticated data from `--aad` (hex) or `--aad-file` (raw bytes; see the +/// module docs for why not hex). Empty if neither is given: AAD is optional, unlike the key. +pub(crate) fn load_aad(aad: &Option, aad_file: &Option) -> Vec { + if let Some(path) = aad_file { + read_from_file_raw(path) + } else if let Some(hex_str) = aad { + hex::decode(hex_str).unwrap_or_else(|_| { + eprintln!("Error: `--aad` must be hex. Use `--aad-file` for raw bytes."); + exit(-1); + }) + } else { + Vec::new() + } +} + +/// Encrypts stdin to stdout under GCM: writes the generated nonce, then the ciphertext as it +/// streams, then the tag. +pub(crate) fn encrypt_gcm( + key: &KeyMaterial, + aad: &[u8], + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + let (mut enc, nonce) = Gcm::::do_encrypt_init(key) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't start encryption: {e:?}"); + exit(-1); + }); + write_bytes_or_hex(&nonce, output_hex); + + enc.do_update_aad(aad).unwrap_or_else(|e| { + eprintln!("Error: couldn't absorb the additional authenticated data: {e:?}"); + exit(-1); + }); + + let mut buf = [0u8; CHUNK_LEN]; + let mut out = [0u8; CHUNK_LEN]; + loop { + let n = io::stdin().read(&mut buf).unwrap_or_else(|e| { + eprintln!("Error: failed to read from stdin: {e}"); + exit(-1); + }); + if n == 0 { + break; + } + // GCM's encryptor holds nothing back, so `out` (as long as `buf`) always has room. + let written = enc.do_update_out(&buf[..n], &mut out).unwrap_or_else(|e| { + eprintln!("Error: encryption failed: {e:?}"); + exit(-1); + }); + write_bytes_or_hex(&out[..written], output_hex); + } + + // The detached final flushes nothing for GCM and returns the tag, written last. + let (_, _, tag) = enc.do_final_detached().unwrap_or_else(|e| { + eprintln!("Error: encryption failed: {e:?}"); + exit(-1); + }); + write_bytes_or_hex(&tag, output_hex); + finish(output_hex); +} + +/// Decrypts stdin to stdout under GCM: reads the 12-byte nonce, streams the rest through the +/// inline decryptor, and checks the tag on `do_final`. See the module docs for why plaintext may +/// already be written to stdout by the time a tag failure is reported. +pub(crate) fn decrypt_gcm( + key: &KeyMaterial, + aad: &[u8], + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + let mut nonce = [0u8; 12]; + if let Err(e) = io::stdin().read_exact(&mut nonce) { + eprintln!( + "Error: input too short to contain the 12-byte nonce that `encrypt` writes first ({e})." + ); + exit(-1); + } + + let mut dec = Gcm::::do_decrypt_init(key, &nonce) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't start decryption: {e:?}"); + exit(-1); + }); + dec.do_update_aad(aad).unwrap_or_else(|e| { + eprintln!("Error: couldn't absorb the additional authenticated data: {e:?}"); + exit(-1); + }); + + let mut buf = [0u8; CHUNK_LEN]; + loop { + let n = io::stdin().read(&mut buf).unwrap_or_else(|e| { + eprintln!("Error: failed to read from stdin: {e}"); + exit(-1); + }); + if n == 0 { + break; + } + let out_len = dec.update_out_len(n); + let mut out = vec![0u8; out_len]; + dec.do_update_out(&buf[..n], &mut out).unwrap_or_else(|e| { + eprintln!("Error: decryption failed: {e:?}"); + exit(-1); + }); + write_bytes_or_hex(&out, output_hex); + } + + if let Err(e) = dec.do_final() { + // Whatever plaintext was already written above stands; the exit code is the signal a + // script must check (see the module docs). + io::stdout().flush().ok(); + eprintln!("Error: authentication failed: {e:?}"); + exit(-1); + } + + finish(output_hex); +} + +/// Flushes stdout, and adds the trailing newline the hex-output commands all emit. +fn finish(output_hex: bool) { + if output_hex { + println!(); + } + io::stdout().flush().unwrap_or_else(|e| { + eprintln!("Error: failed to flush stdout: {e}"); + exit(-1); + }); +} diff --git a/cli/src/aes_ccm_cmd.rs b/cli/src/aes_ccm_cmd.rs new file mode 100644 index 00000000..c5806d02 --- /dev/null +++ b/cli/src/aes_ccm_cmd.rs @@ -0,0 +1,390 @@ +//! AES-CCM authenticated encryption and decryption (NIST SP 800-38C). +//! +//! # This command does not stream, and cannot +//! +//! Every other cipher command here streams stdin to stdout in 1 KiB chunks. This one reads stdin to +//! the end first, and that is a property of CCM rather than a shortcut. SP 800-38C Sec 3: +//! +//! > CCM is intended for use in a packet environment, i.e., when all of the data is available in +//! > storage before CCM is applied; CCM is not designed to support partial processing or stream +//! > processing. +//! +//! Appendix A.2.1 puts the payload's octet length inside `B0`, the first block the CBC-MAC absorbs, +//! so nothing can be authenticated until the whole payload length is known. Buffering the input is +//! therefore the correct behaviour, not a compromise -- and it has a real benefit on the decryption +//! side: unlike `ascon-aead128`, this command writes **no plaintext at all** until the tag has +//! verified, so a non-zero exit leaves nothing to discard. +//! +//! The practical consequence is that memory use is proportional to the input, so this is not the +//! command to point at a multi-gigabyte file. `aes256-ctr` piped through a separate MAC, or +//! `ascon-aead128`, are the streaming alternatives. +//! +//! # The nonce is supplied, not generated +//! +//! This is the one cipher command here with a `--nonce` flag. The other modes generate their IV or +//! nonce and prepend it to the output, because for them an unpredictable value is what is required. +//! CCM needs the nonce to be **unique**, not unpredictable -- Sec 5.3: "The nonce is not required +//! to be random" -- and a caller with a message counter can guarantee uniqueness better than a +//! DRBG draw can. Since a repeated nonce under one key is fatal for CCM (see the subcommand help), +//! the choice is the caller's to make explicitly. +//! +//! The nonce is not written to the output, so `encrypt` and `decrypt` both need the same +//! `--nonce`. +//! +//! # Lengths +//! +//! `--nonce` must be 7..=13 bytes and `--tag-len` one of 4, 6, 8, 10, 12, 14, 16, both from +//! Appendix A.1. The nonce length fixes the maximum payload at `2^(8 * (15 - n)) - 1` bytes, which +//! this command checks against the actual input length. Because those are const generic parameters +//! of the mode, the runtime value is dispatched to one of the seven nonce lengths and seven tag +//! lengths below. +//! +//! The output layout is Sec 6.1 step 8's own: `ciphertext || tag`. + +use std::io::{self, Read}; +use std::process::exit; + +use bouncycastle::aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::core::errors::SymmetricCipherError; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::ElectronicCodeBook; +use bouncycastle::hex; +use bouncycastle::modes::{Ccm, Decrypting, Encrypting}; + +use crate::block_mode_cmd::{BLOCK_LEN, BlockModeAction, load_key}; +use crate::helpers; + +/// AES-128 CCM. See the module docs and the subcommand help. +pub(crate) fn aes128_ccm_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + nonce: &Option, + nonce_file: &Option, + aad: &Option, + tag_len: usize, + output_hex: bool, +) { + run::( + action, + &load_key::<16>(key, key_file, "AES-128"), + nonce, + nonce_file, + aad, + tag_len, + output_hex, + ); +} + +/// AES-192 CCM. See [`aes128_ccm_cmd`]. +pub(crate) fn aes192_ccm_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + nonce: &Option, + nonce_file: &Option, + aad: &Option, + tag_len: usize, + output_hex: bool, +) { + run::( + action, + &load_key::<24>(key, key_file, "AES-192"), + nonce, + nonce_file, + aad, + tag_len, + output_hex, + ); +} + +/// AES-256 CCM. See [`aes128_ccm_cmd`]. +pub(crate) fn aes256_ccm_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + nonce: &Option, + nonce_file: &Option, + aad: &Option, + tag_len: usize, + output_hex: bool, +) { + run::( + action, + &load_key::<32>(key, key_file, "AES-256"), + nonce, + nonce_file, + aad, + tag_len, + output_hex, + ); +} + +/// Loads the nonce from `--nonce` (hex) or `--nonce-file` (raw bytes, exactly as they are). +/// +/// Unlike the key there is no entropy question here: Sec 5.3 asks for uniqueness, not randomness, +/// so an all-zero nonce is a perfectly valid *first* nonce and only a repeat is a problem. +/// +/// `--nonce-file` reads raw bytes ([`helpers::read_from_file_raw`]), not the hex-or-raw guess +/// [`helpers::read_from_file`] uses for keys: a repeated nonce under one key is fatal for CCM (see +/// the module docs), so two distinct binary nonce files that happen to look like hex text of the +/// same value must not silently collapse to the same nonce. +/// +/// For the same reason a trailing newline is **not** stripped: a 13-byte file ending in `0x0a` and +/// the 12-byte file without it are two different nonces, and silently treating them as one would +/// be exactly the collapse above. But every length from 7 to 13 is valid, so a 12-byte nonce +/// written with `echo` rather than `echo -n` is accepted as a *different*, 13-byte nonce, and the +/// only symptom is a failed tag check on the other side. That case is warned about on stderr so it +/// is not a silent one; the bytes are still used exactly as they are. +fn load_nonce(nonce: &Option, nonce_file: &Option) -> Vec { + let bytes = if let Some(file) = nonce_file { + let bytes = helpers::read_from_file_raw(file); + if bytes.last() == Some(&b'\n') { + eprintln!( + "Warning: nonce file '{file}' ends with a newline byte (0x0a), which is used as \ + part of the nonce." + ); + eprintln!( + " If that is not intended (for example the file was written by `echo`), \ + write it with `printf` or `echo -n`." + ); + } + bytes + } else if let Some(v) = nonce { + hex::decode(v).unwrap_or_else(|_| { + eprintln!("Error: nonce is not valid hex."); + exit(-1) + }) + } else { + eprintln!("Error: --nonce or --nonce-file must be supplied. CCM has no generated nonce;"); + eprintln!(" see the subcommand help for why, and for the uniqueness requirement."); + exit(-1) + }; + + // Appendix A.1: "n is an element of {7, 8, 9, 10, 11, 12, 13}". + if !(7..=13).contains(&bytes.len()) { + eprintln!( + "Error: nonce is {} bytes; CCM requires 7 to 13 (SP 800-38C Appendix A.1).", + bytes.len() + ); + exit(-1) + } + bytes +} + +fn load_aad(aad: &Option) -> Vec { + match aad { + Some(v) => hex::decode(v).unwrap_or_else(|_| { + eprintln!("Error: associated data is not valid hex."); + exit(-1) + }), + None => Vec::new(), + } +} + +/// Reads all of stdin. See the module docs on why this is not a streaming command. +fn read_all_stdin() -> Vec { + let mut input = Vec::new(); + io::stdin().read_to_end(&mut input).expect("Failed to read from stdin"); + input +} + +/// Turns the runtime nonce and tag lengths into the mode's const generic parameters. +/// +/// `NONCE_LEN` and `TAG_LEN` are const parameters of `Ccm` -- that is what makes A.1's length +/// conditions compile-time checks rather than runtime ones -- so a command-line value has to be +/// matched into one of the permitted instantiations. The two nested matches are the price of that, +/// and they are exhaustive over A.1's sets: 7 nonce lengths x 7 tag lengths. +fn run( + action: &BlockModeAction, + key: &KeyMaterial, + nonce: &Option, + nonce_file: &Option, + aad: &Option, + tag_len: usize, + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + // Reject this before opening nonce/AAD files or waiting for stdin. Appendix A.1: "t is an + // element of {4, 6, 8, 10, 12, 14, 16}". + if !matches!(tag_len, 4 | 6 | 8 | 10 | 12 | 14 | 16) { + eprintln!( + "Error: --tag-len is {tag_len}; CCM requires one of 4, 6, 8, 10, 12, 14, 16 \ + (SP 800-38C Appendix A.1)." + ); + exit(-1) + } + + let nonce_bytes = load_nonce(nonce, nonce_file); + let aad_bytes = load_aad(aad); + let input = read_all_stdin(); + let encrypt = matches!(action, BlockModeAction::Encrypt); + + macro_rules! with_tag_len { + ($n:literal) => { + match tag_len { + 4 => go::( + key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex, + ), + 6 => go::( + key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex, + ), + 8 => go::( + key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex, + ), + 10 => go::( + key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex, + ), + 12 => go::( + key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex, + ), + 14 => go::( + key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex, + ), + 16 => go::( + key, &nonce_bytes, &aad_bytes, input, encrypt, output_hex, + ), + _ => unreachable!("tag length was validated before stdin was read"), + } + }; + } + + // `load_nonce` has already rejected anything outside 7..=13, so the fall-through is unreachable; + // it is spelled out rather than `unreachable!()` so this cannot panic on a future edit. + match nonce_bytes.len() { + 7 => with_tag_len!(7), + 8 => with_tag_len!(8), + 9 => with_tag_len!(9), + 10 => with_tag_len!(10), + 11 => with_tag_len!(11), + 12 => with_tag_len!(12), + 13 => with_tag_len!(13), + other => { + eprintln!("Error: nonce is {other} bytes; CCM requires 7 to 13."); + exit(-1) + } + } +} + +/// Reports [`Ccm::new`]'s refusal of a payload past the `q` limit and exits. +/// +/// The only [`SymmetricCipherError::GenericError`] `new` can return is that limit: A.1's +/// `p < 2^8q`, where `q = 15 - n`. Both directions hit it -- the decrypt side on the input minus +/// its tag -- so both report it here, with the numbers, since the fix is a shorter nonce. +fn payload_past_the_q_limit( + msg: &str, + payload_len: usize, +) -> ! +where + P: ElectronicCodeBook, +{ + eprintln!("Error: {msg}"); + eprintln!( + " Payload is {payload_len} bytes; with a {NONCE_LEN}-byte nonce, q = {} and the \ + limit is {} bytes.", + 15 - NONCE_LEN, + Ccm::::MAX_PAYLOAD_LEN, + ); + eprintln!(" Use a shorter nonce for a larger payload."); + exit(-1) +} + +/// One fully-instantiated CCM run. +/// +/// `input` is processed in place through [`Ccm`]'s own streaming API rather than through the +/// one-shot [`Ccm::encrypt`]/[`Ccm::decrypt`], which each need a second, freshly allocated buffer +/// the size of `input`: the declared-length constructor already has everything a one-shot needs, +/// so there is no second buffer to allocate or copy into. +fn go( + key: &KeyMaterial, + nonce_bytes: &[u8], + aad: &[u8], + mut input: Vec, + encrypt: bool, + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + type Enc = + Ccm; + type Dec = + Ccm; + + // `run` dispatched on this exact length, so the conversion cannot fail. + let Ok(nonce) = <[u8; NONCE_LEN]>::try_from(nonce_bytes) else { + eprintln!("Error: internal nonce length mismatch."); + exit(-1) + }; + + if encrypt { + match Enc::::new(key, &nonce, aad, input.len()) { + Ok(mut ccm) => { + // `new` already accepted this exact length as `input.len()`, and this is the one + // and only call supplying it, so `take_owed` can never see too much and `owed` + // can never be left nonzero: neither of these can fail on the path that reaches + // them. + ccm.do_encrypt_update(&mut input).expect("declared length matches what was sent"); + let tag = ccm.do_encrypt_final().expect("declared length was fully supplied"); + helpers::write_bytes_or_hex(&input, output_hex); + helpers::write_bytes_or_hex(&tag, output_hex); + if output_hex { + println!(); + } + } + Err(SymmetricCipherError::GenericError(msg)) => { + payload_past_the_q_limit::(msg, input.len()) + } + Err(e) => { + eprintln!("Error: AES-CCM encryption failed: {e:?}"); + exit(-1) + } + } + } else { + // `split_last_chunk_mut` is `None` exactly when there is no room for a `TAG_LEN`-byte tag, + // which is the same octet-level test (and the same allowance for an empty payload plus its + // tag) that `Ccm::decrypt`'s own doc comment explains for Sec 6.2 step 1. + let Some((data, tag)) = input.split_last_chunk_mut::() else { + eprintln!( + "Error: input is {} bytes, shorter than the {TAG_LEN}-byte tag it must end with.", + input.len() + ); + exit(-1) + }; + match Dec::::new(key, &nonce, aad, data.len()) { + Ok(mut ccm) => { + // As the encrypt arm above: `data.len()` is exactly the length just declared, and + // it is supplied in this one call, so this cannot fail. + ccm.do_decrypt_update(data).expect("declared length matches what was sent"); + match ccm.do_decrypt_final(tag) { + Ok(()) => { + helpers::write_bytes_or_hex(data, output_hex); + if output_hex { + println!(); + } + } + Err(SymmetricCipherError::AEADTagCheckFailed) => { + // Nothing has been written to stdout at this point, which is what + // processing in place still buys here: Sec 6.2's "the payload P and the + // MAC T shall not be revealed" holds end to end. + eprintln!( + "Error: AES-CCM authentication failed; the input is not authentic." + ); + exit(-1) + } + Err(e) => { + eprintln!("Error: AES-CCM decryption failed: {e:?}"); + exit(-1) + } + } + } + Err(SymmetricCipherError::GenericError(msg)) => { + payload_past_the_q_limit::(msg, data.len()) + } + Err(e) => { + eprintln!("Error: AES-CCM decryption failed: {e:?}"); + exit(-1) + } + } + } +} diff --git a/cli/src/aes_gcm_cmd.rs b/cli/src/aes_gcm_cmd.rs new file mode 100644 index 00000000..e73ba5ba --- /dev/null +++ b/cli/src/aes_gcm_cmd.rs @@ -0,0 +1,82 @@ +//! AES-GCM authenticated encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the nonce/tag framing, AAD loading and stdin streaming are in +//! [`crate::aead_mode_cmd`], shared across all three key lengths. See that module for the +//! command-line contract (`nonce || ciphertext || tag`, the AAD flags, and why a tag failure may be +//! reported after plaintext has already reached stdout). +//! +//! GCM (NIST SP 800-38D) is authenticated: unlike `aes*-cbc`, `aes*-cfb`, `aes*-cfb8` and +//! `aes*-ctr`, tampering with the ciphertext, the AAD or the nonce is detected rather than merely +//! producing wrong plaintext. The nonce is 12 bytes and the tag 16 (128-bit, the maximum SP +//! 800-38D Sec 5.2.1.2 allows); a fresh nonce is generated per `encrypt` and there is no `--iv` +//! flag, for the same reason as the other modes -- and doubly so here, since a repeated GCM nonce +//! also lets an attacker recover the hash subkey (SP 800-38D Appendix A). + +use crate::aead_mode_cmd::{decrypt_gcm, encrypt_gcm, load_aad}; +use crate::block_mode_cmd::{BlockModeAction, load_key}; +use bouncycastle::aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::ElectronicCodeBook; + +pub(crate) fn aes128_gcm_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + aad: &Option, + aad_file: &Option, + output_hex: bool, +) { + run::( + action, + &load_key::<16>(key, key_file, "AES-128"), + &load_aad(aad, aad_file), + output_hex, + ); +} + +pub(crate) fn aes192_gcm_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + aad: &Option, + aad_file: &Option, + output_hex: bool, +) { + run::( + action, + &load_key::<24>(key, key_file, "AES-192"), + &load_aad(aad, aad_file), + output_hex, + ); +} + +pub(crate) fn aes256_gcm_cmd( + action: &BlockModeAction, + key: &Option, + key_file: &Option, + aad: &Option, + aad_file: &Option, + output_hex: bool, +) { + run::( + action, + &load_key::<32>(key, key_file, "AES-256"), + &load_aad(aad, aad_file), + output_hex, + ); +} + +/// Dispatches to the shared AEAD streaming loops with `Gcm`'s 128-bit tag. +fn run( + action: &BlockModeAction, + key: &KeyMaterial, + aad: &[u8], + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + match action { + BlockModeAction::Encrypt => encrypt_gcm::(key, aad, output_hex), + BlockModeAction::Decrypt => decrypt_gcm::(key, aad, output_hex), + } +} diff --git a/cli/src/ascon_cmd.rs b/cli/src/ascon_cmd.rs new file mode 100644 index 00000000..ad37f25e --- /dev/null +++ b/cli/src/ascon_cmd.rs @@ -0,0 +1,290 @@ +use std::io::{self, Read}; +use std::process::exit; + +use bouncycastle::ascon::ascon_aead128::{ + AsconAead128, AsconAead128Decryptor, AsconAead128Encryptor, +}; +use bouncycastle::ascon::ascon_cxof128::AsconCXof128; +use bouncycastle::ascon::ascon_hash256::AsconHash256; +use bouncycastle::ascon::ascon_xof128::AsconXof128; +use bouncycastle::core::errors::SymmetricCipherError; +use bouncycastle::core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle::core::security_strength::SecurityStrength; +use bouncycastle::core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle::hex; + +use crate::helpers; + +/// Load a hex string or a binary/hex file into bytes; exits with an error if neither is supplied. +fn load_bytes(value: &Option, value_file: &Option, label: &str) -> Vec { + if let Some(file) = value_file { + helpers::read_from_file(file) + } else if let Some(v) = value { + hex::decode(v).unwrap_or_else(|_| { + eprintln!("Error: {label} is not valid hex."); + exit(-1) + }) + } else { + eprintln!("Error: {label} must be supplied."); + exit(-1) + } +} + +fn load_optional_bytes( + value: &Option, + value_file: &Option, + label: &str, +) -> Option> { + if let Some(file) = value_file { + Some(helpers::read_from_file(file)) + } else { + value.as_ref().map(|v| { + hex::decode(v).unwrap_or_else(|_| { + eprintln!("Error: {label} is not valid hex."); + exit(-1) + }) + }) + } +} + +fn require_16(bytes: Vec, label: &str) -> [u8; 16] { + bytes.try_into().unwrap_or_else(|_: Vec| { + eprintln!("Error: {label} must be exactly 16 bytes."); + exit(-1) + }) +} + +/// Build a `KeyMaterial<16>` for the AEAD key, warning (and forcing usable metadata) only if the +/// key turns out to be low-entropy (e.g. all-zero), the same way `helpers::parse_seed` does. +fn load_key_material(key_bytes: &[u8; 16]) -> KeyMaterial<16> { + let mut key = + KeyMaterial::<16>::from_bytes_as_type(key_bytes, KeyType::SymmetricCipherKey).unwrap(); + if key.key_type() == KeyType::Zeroized || key.security_strength() < SecurityStrength::_128bit { + eprintln!( + "Warning: low entropy key provided. We'll still process it, but it may be insecure." + ); + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::_128bit) + }) + .unwrap(); + } + key +} + +/// Ascon-Hash256 of stdin. Streaming update; 256-bit digest. +pub(crate) fn hash256_cmd(output_hex: bool) { + helpers::stream_hash(AsconHash256::new(), output_hex); +} + +/// Ascon-XOF128 of stdin, producing `output_len` bytes. Streaming absorb. +pub(crate) fn xof128_cmd(output_len: usize, output_hex: bool) { + helpers::stream_xof(AsconXof128::new(), output_len, output_hex); +} + +/// Ascon-CXOF128 of stdin with a hex customization string, producing `output_len` bytes. +pub(crate) fn cxof128_cmd(customization: &Option, output_len: usize, output_hex: bool) { + let z = match customization { + Some(v) => hex::decode(v).unwrap_or_else(|_| { + eprintln!("Error: customization is not valid hex."); + exit(-1) + }), + None => Vec::new(), + }; + let x = AsconCXof128::with_customization(&z).unwrap_or_else(|_| { + eprintln!("Error: customization string exceeds 256 bytes."); + exit(-1) + }); + helpers::stream_xof(x, output_len, output_hex); +} + +/// Ascon-AEAD128 of stdin. Encrypts (stdin = plaintext, output = nonce||ciphertext||tag) or, with +/// `decrypt`, decrypts (stdin = nonce||ciphertext||tag, output = plaintext). Decryption exits with +/// a non-zero status if the authentication tag does not verify. +/// +/// The 16-byte nonce is generated by the library and travels at the head of the stream, the same +/// convention `block_mode_cmd`/`stream_mode_cmd` use for their IV, so an encrypt and a decrypt +/// compose in a pipeline with nothing but the key passed between them. A caller-supplied `nonce` +/// overrides that and is kept out of the stream in both directions; it is there for known-answer +/// vectors, and repeating one under a given key breaks Ascon-AEAD128 outright. +/// +/// Both directions stream stdin in fixed-size chunks (no full-buffer slurp). Encryption emits +/// ciphertext eagerly, before the tag is known; note that in the decryption direction, plaintext +/// is likewise emitted before the tag has been checked, so it should not be treated as +/// authentic until this command exits with status 0 (see the crate's "Security Considerations"). +pub(crate) fn aead128_cmd( + key: &Option, + key_file: &Option, + nonce: &Option, + nonce_file: &Option, + ad: &Option, + decrypt: bool, + output_hex: bool, +) { + let key = load_key_material(&require_16(load_bytes(key, key_file, "key"), "key")); + let nonce = + load_optional_bytes(nonce, nonce_file, "nonce").map(|bytes| require_16(bytes, "nonce")); + let ad_bytes = match ad { + Some(v) => hex::decode(v).unwrap_or_else(|_| { + eprintln!("Error: associated data is not valid hex."); + exit(-1) + }), + None => Vec::new(), + }; + let ad_opt = if ad_bytes.is_empty() { None } else { Some(ad_bytes.as_slice()) }; + + if decrypt { + aead128_decrypt_stream(&key, nonce.as_ref(), ad_opt, output_hex); + } else { + aead128_encrypt_stream(&key, nonce.as_ref(), ad_opt, output_hex); + } +} + +/// Generated-nonce encryption: drives [`AsconAead128Encryptor`] in the inline `ciphertext || tag` +/// layout (the inherited [`SymmetricCipherEncryptor::do_final_out`]), writing the nonce it +/// generated ahead of the stream. +/// With an explicit nonce there is no nonce to write, so that case goes to +/// [`aead128_encrypt_stream_with_explicit_nonce`] instead. +fn aead128_encrypt_stream( + key: &KeyMaterial<16>, + nonce: Option<&[u8; 16]>, + ad_opt: Option<&[u8]>, + output_hex: bool, +) { + if let Some(nonce) = nonce { + aead128_encrypt_stream_with_explicit_nonce(key, nonce, ad_opt, output_hex); + return; + } + + let (mut cipher, nonce) = AsconAead128Encryptor::do_encrypt_init(key).unwrap_or_else(|e| { + eprintln!("Error: couldn't start encryption: {e:?}"); + exit(-1); + }); + if let Some(ad) = ad_opt { + // infallible: `do_update_aad` only refuses AAD once plaintext has been fed in, and none + // has been yet. + cipher.do_update_aad(ad).unwrap(); + } + + helpers::write_bytes_or_hex(&nonce, output_hex); + + let mut buf = [0u8; 1024]; + loop { + let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + if n == 0 { + break; + } + let mut out = [0u8; 1024]; + // infallible: `out` is as long as `buf`, so it cannot be shorter than the `n` bytes read + // into it, which is the only length `OutputBufferTooSmall` could complain about. + let written = cipher.do_update_out(&buf[..n], &mut out).unwrap(); + helpers::write_bytes_or_hex(&out[..written], output_hex); + } + // infallible: Ascon-AEAD128 holds nothing back, so the inline final is only the 16-byte tag. + let mut tail = [0u8; 16]; + let tail_len = cipher.do_final_out(&mut tail).unwrap(); + helpers::write_bytes_or_hex(&tail[..tail_len], output_hex); + if output_hex { + println!(); + } +} + +/// Encryption under a caller-supplied nonce, which nothing is written to the stream for. This +/// drives the inherent [`AsconAead128`] API rather than the `AEADCipherEncryptor` pair because the +/// pair generates its own nonce by construction -- `do_encrypt_init` owns that choice, which is +/// the point of the trait -- and has no caller-supplied-nonce constructor to call here. +fn aead128_encrypt_stream_with_explicit_nonce( + key: &KeyMaterial<16>, + nonce: &[u8; 16], + ad_opt: Option<&[u8]>, + output_hex: bool, +) { + let mut cipher = AsconAead128::new_encrypting(key, nonce, ad_opt).unwrap_or_else(|e| { + eprintln!("Error: couldn't start encryption: {e:?}"); + exit(-1); + }); + let mut buf = [0u8; 1024]; + loop { + let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + if n == 0 { + break; + } + cipher.do_encrypt_update(&mut buf[..n]); + helpers::write_bytes_or_hex(&buf[..n], output_hex); + } + let tag = cipher.do_encrypt_final(); + helpers::write_bytes_or_hex(&tag, output_hex); + if output_hex { + println!(); + } +} + +/// Decrypts a stream whose final 16 bytes are the tag, which is only known once EOF is reached. +/// [`AsconAead128Decryptor`] holds the last 16 bytes it has seen back itself, releasing everything +/// before them as soon as it is known not to be part of the tag; at EOF +/// [`SymmetricCipherDecryptor::do_final`] checks what it held back as the tag. +fn aead128_decrypt_stream( + key: &KeyMaterial<16>, + nonce: Option<&[u8; 16]>, + ad_opt: Option<&[u8]>, + output_hex: bool, +) { + const CHUNK: usize = 1024; + let nonce = match nonce { + Some(nonce) => *nonce, + None => { + let mut nonce = [0u8; 16]; + if let Err(e) = io::stdin().read_exact(&mut nonce) { + if e.kind() == io::ErrorKind::UnexpectedEof { + eprintln!("Error: ciphertext is shorter than the 16-byte nonce."); + exit(-1); + } + panic!("Failed to read from stdin: {e}"); + } + nonce + } + }; + + let mut cipher = AsconAead128Decryptor::do_decrypt_init(key, &nonce).unwrap_or_else(|e| { + eprintln!("Error: couldn't start decryption: {e:?}"); + exit(-1); + }); + if let Some(ad) = ad_opt { + // infallible: as on the encrypt side, no ciphertext has been fed in yet. + cipher.do_update_aad(ad).unwrap(); + } + + let mut buf = [0u8; CHUNK]; + let mut out = [0u8; CHUNK]; + loop { + let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + if n == 0 { + break; + } + // infallible: the decryptor releases at most what it has held back (16 bytes) plus what + // it is given, less the 16 it keeps, so never more than the `n <= CHUNK` bytes read. + let written = cipher.do_update_out(&buf[..n], &mut out).unwrap(); + helpers::write_bytes_or_hex(&out[..written], output_hex); + } + + match cipher.do_final() { + Ok((last, last_len)) => { + helpers::write_bytes_or_hex(&last[..last_len], output_hex); + if output_hex { + println!(); + } + } + Err(SymmetricCipherError::DecryptionFailed) => { + eprintln!("Error: ciphertext is shorter than the 16-byte tag."); + exit(-1); + } + Err(_) => { + eprintln!("Error: Ascon-AEAD128 authentication failed."); + exit(-1); + } + } +} diff --git a/cli/src/block_mode_cmd.rs b/cli/src/block_mode_cmd.rs index 584d314d..0326ea6a 100644 --- a/cli/src/block_mode_cmd.rs +++ b/cli/src/block_mode_cmd.rs @@ -67,19 +67,16 @@ pub(crate) const BLOCK_LEN: usize = 16; /// block at a time; it is bounded, so its cost does not scale with the input. pub(crate) const CHUNK_LEN: usize = 64 * BLOCK_LEN; -/// Which direction to run. Shared by every mode subcommand. +/// Which direction to run. Shared by every mode subcommand, including CCM's, whose framing (a +/// caller-supplied `--nonce` that is never written to the output, plus AAD and a tag) is +/// different enough from the rest that it is not summarized here -- see the specific subcommand's +/// own `--help` (`bc-rust aes128-ccm --help` and friends) for what `encrypt`/`decrypt` actually do +/// for the mode you are running. #[derive(ValueEnum, Clone, Debug)] pub(crate) enum BlockModeAction { - /// Encrypt stdin to stdout. - /// For CBC, CFB and CFB8 a freshly generated IV is written as the first 16 bytes of the - /// output, and for CTR a 12-byte nonce, so that `decrypt` can read it back; ECB has neither and - /// writes none. The `-cbc` and `-ecb` commands need the input to be a multiple of 16 bytes; - /// `-cfb`, `-cfb8` and `-ctr` take any length. See the individual subcommand's help. + /// Encrypt stdin to stdout. See the subcommand's own help for this mode's exact framing. Encrypt, - /// Decrypt stdin to stdout. - /// For CBC, CFB and CFB8 the first 16 bytes of input are taken as the IV, and for CTR the - /// first 12 as the nonce, as written by `encrypt`; ECB has neither and reads none. See - /// `encrypt` for the input-length rule. + /// Decrypt stdin to stdout. See the subcommand's own help for this mode's exact framing. Decrypt, } diff --git a/cli/src/helpers.rs b/cli/src/helpers.rs index 329cda77..1a89fa43 100644 --- a/cli/src/helpers.rs +++ b/cli/src/helpers.rs @@ -2,12 +2,28 @@ use bouncycastle::core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; use bouncycastle::core::security_strength::SecurityStrength; +use bouncycastle::core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle::hex; use std::fs::File; use std::io; use std::io::{Read, Write}; use std::process::exit; +/// Reads a file's bytes exactly as they are, with no hex-or-raw guessing. +/// +/// Use this where a misread would silently change the *value* the caller asked for rather than +/// merely fail to match it -- a nonce is the reason this exists: two distinct binary nonce files +/// that happen to decode as hex to the same bytes must not collapse to one nonce (see +/// `aes_ccm_cmd::load_nonce`). [`read_from_file`]'s "try hex, fall back to raw" heuristic is fine +/// for a key, where a wrong guess only ever produces a mismatch, never a same-looking-different +/// value. +pub(crate) fn read_from_file_raw(filename: &str) -> Vec { + std::fs::read(filename).unwrap_or_else(|e| { + eprintln!("Error: couldn't read file '{filename}': {e}"); + exit(-1); + }) +} + /// Reads either bin or hex pub(crate) fn read_from_file(filename: &str) -> Vec { let file = File::open(&filename); @@ -58,6 +74,7 @@ pub(crate) fn read_from_file_or_stdin(filename: &Option) -> Vec { pub(crate) fn write_bytes_or_hex(bytes: &[u8], output_hex: bool) { // first flush stdout to ensure any buffered data is written io::stdout().flush().unwrap(); + if output_hex { for b in bytes.iter() { print!("{b:02x}"); @@ -69,6 +86,7 @@ pub(crate) fn write_bytes_or_hex(bytes: &[u8], output_hex: bool) { pub(crate) fn write_bytes_or_hex_to_file(bytes: &[u8], filename: &str, output_hex: bool) { let mut file = File::create(filename).expect("Failed to create file"); + if output_hex { for b in bytes.iter() { file.write_all(format!("{b:02x}").as_bytes()).unwrap(); @@ -89,13 +107,15 @@ pub(crate) fn parse_seed(bytes: &[u8]) -> Result { - // it's not hex, so take the fist SEED_LEN bytes of the raw binary + // it's not hex, so take the first SEED_LEN bytes of the raw binary if bytes.len() < SEED_LEN || bytes.len() > SEED_LEN + 1 { return Err(()); } + bytes[..SEED_LEN].try_into().unwrap() } }; @@ -108,11 +128,49 @@ pub(crate) fn parse_seed(bytes: &[u8]) -> Result, - key_file: &Option, - verify_val: &Option, - output_hex: bool, -) { - // load the key +/// Loads a MAC key from `--key` (hex) or `--key-file` (raw), tagged as a MAC key. +fn load_mac_key(key: &Option, key_file: &Option) -> KeyMaterial512 { let key_bytes: Vec = if key.is_some() { hex::decode(key.as_ref().unwrap()).unwrap() } else if key_file.is_some() { @@ -42,6 +37,17 @@ pub(crate) fn mac_cmd( } let mut key = KeyMaterial512::from_bytes(&key_bytes).unwrap(); do_hazardous_operations(&mut key, |key| key.set_key_type(KeyType::MACKey)).unwrap(); + key +} + +pub(crate) fn mac_cmd( + hmac_variant: HMACVariant, + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key = load_mac_key(key, key_file); // instantiate the MAC object and call do_mac() match hmac_variant { @@ -68,6 +74,36 @@ pub(crate) fn mac_cmd( } } +/// KMAC (NIST SP 800-185 Sec 4), which unlike HMAC takes a customization string and a caller- +/// chosen tag length -- both are bound into the computation, so the verifier must use the same. +pub(crate) fn kmac_cmd( + bit_len: usize, + length: usize, + customization: &Option, + key: &Option, + key_file: &Option, + verify_val: &Option, + output_hex: bool, +) { + let key = load_mac_key(key, key_file); + let s = customization.as_deref().unwrap_or("").as_bytes(); + // new_allow_weak_key, as the HMAC commands do: a CLI is used for test vectors and scripting, + // where a short or all-zero key is a legitimate thing to want. + match bit_len { + 128 => do_mac( + KMAC128::new_with_params(&key, s, length, true).expect("a valid MAC key"), + verify_val, + output_hex, + ), + 256 => do_mac( + KMAC256::new_with_params(&key, s, length, true).expect("a valid MAC key"), + verify_val, + output_hex, + ), + _ => panic!("Unsupported algorithm: KMAC-{bit_len}"), + } +} + fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) { // read the content to be MAC'd from stdin let mut buf: [u8; 1024] = [0u8; 1024]; diff --git a/cli/src/main.rs b/cli/src/main.rs index 2b26315b..6371a6e4 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,8 +1,12 @@ +mod aead_mode_cmd; mod aes_cbc_cmd; +mod aes_ccm_cmd; mod aes_cfb8_cmd; mod aes_cfb_cmd; mod aes_ctr_cmd; mod aes_ecb_cmd; +mod aes_gcm_cmd; +mod ascon_cmd; mod block_mode_cmd; mod encoders_cmd; mod helpers; @@ -158,6 +162,267 @@ enum Subcommands { x: bool, }, + /// Perform TupleHash128 (NIST SP 800-185 Sec 5) over a tuple of strings. The tuple is given + /// by repeated --element flags, each in hex; with none, stdin is hashed as a single element. + /// The boundaries between elements are part of the hash. + TUPLEHASH128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'e', long = "element")] + /// A tuple element, in hex. Repeat for each element, in order. + elements: Vec, + + #[arg(short = 's', long)] + /// Customization string. + customization: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform TupleHash256 (NIST SP 800-185 Sec 5). See tuplehash128. + TUPLEHASH256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'e', long = "element")] + /// A tuple element, in hex. Repeat for each element, in order. + elements: Vec, + + #[arg(short = 's', long)] + /// Customization string. + customization: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform ParallelHash128 (NIST SP 800-185 Sec 6) of the content provided on stdin. + /// The block size is part of the function: the same input under a different block size gives + /// an unrelated hash, so both sides must use the same value. + /// Supports streaming update for low memory footprint. + PARALLELHASH128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'b', long)] + /// Block size B in bytes, for the parallel split. + block_size: usize, + + #[arg(short = 's', long)] + /// Customization string. + customization: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform ParallelHash256 (NIST SP 800-185 Sec 6). See parallelhash128. + PARALLELHASH256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 'b', long)] + /// Block size B in bytes, for the parallel split. + block_size: usize, + + #[arg(short = 's', long)] + /// Customization string. + customization: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Compute or verify a KMAC128 (NIST SP 800-185 Sec 4) over the content provided on stdin. + /// The tag length and customization string are bound into the computation, so the verifier + /// must use the same values. + KMAC128 { + /// Length of the tag in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string, domain-separating this use of KMAC from another. + customization: Option, + + #[arg(short, long)] + /// The key, in hex. + key: Option, + + #[arg(long)] + /// File containing the key, as raw bytes. + key_file: Option, + + #[arg(short, long)] + /// Verify against this tag (hex) instead of computing one. + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Compute or verify a KMAC256 (NIST SP 800-185 Sec 4) over the content provided on stdin. + /// See kmac128. + KMAC256 { + /// Length of the tag in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string, domain-separating this use of KMAC from another. + customization: Option, + + #[arg(short, long)] + /// The key, in hex. + key: Option, + + #[arg(long)] + /// File containing the key, as raw bytes. + key_file: Option, + + #[arg(short, long)] + /// Verify against this tag (hex) instead of computing one. + verify: Option, + + #[arg(short)] + /// Output the tag in hex format. + x: bool, + }, + + /// Perform cSHAKE128 (NIST SP 800-185) of the content provided on stdin. Requires the output + /// length in bytes. With no customization string this is exactly SHAKE128. + /// Supports streaming update for low memory footprint. + CSHAKE128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string. Two cSHAKEs with different customization strings produce + /// unrelated output, so this domain-separates one use of the function from another. + customization: Option, + + #[arg(short = 'n', long)] + /// Function-name string. Reserved by NIST for functions it defines (SP 800-185 Sec 3.4); + /// use --customization for your own domain separation. + function_name: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform cSHAKE256 (NIST SP 800-185) of the content provided on stdin. Requires the output + /// length in bytes. With no customization string this is exactly SHAKE256. + /// Supports streaming update for low memory footprint. + CSHAKE256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short = 's', long)] + /// Customization string. See cshake128. + customization: Option, + + #[arg(short = 'n', long)] + /// Function-name string, reserved by NIST. See cshake128. + function_name: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform Ascon-Hash256 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + AsconHash256 { + #[arg(short)] + /// Output the digest in hex format. + x: bool, + }, + + /// Perform Ascon-XOF128 of the content provided on stdin. Requires the output length in bytes. + /// Supports streaming update for low memory footprint. + AsconXOF128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform Ascon-CXOF128 of the content provided on stdin. Requires the output length in bytes. + /// Supports streaming update for low memory footprint. + AsconCXOF128 { + /// Length of the output in bytes. + length: usize, + + /// Customization string in hex (optional). + #[arg(long)] + customization: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Ascon-AEAD128 authenticated encryption/decryption of the content provided on stdin + /// (NIST SP 800-232). + /// + /// On encrypt, a fresh nonce is generated and written as the FIRST 16 BYTES of the output, + /// followed by the ciphertext and then the 16-byte tag; on --decrypt the nonce is read back + /// from the first 16 bytes of the input, so the two compose directly in a pipeline. This is + /// the same convention the AES commands use for their IV. Decryption fails with a non-zero + /// exit status if the tag does not verify. + /// + /// --nonce/--nonce-file override that: the nonce is then neither written on encrypt nor read + /// on decrypt, and the stream is exactly ciphertext||tag in both directions. That override + /// exists for reproducing known-answer vectors; repeating a nonce under one key destroys both + /// the confidentiality and the authenticity of Ascon-AEAD128. + /// + /// 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. + /// + /// Security note: decryption streams its output, so plaintext bytes are written to stdout + /// before the authentication tag (the last 16 bytes of input) can be checked. Do not treat + /// the output as authentic until this command exits with status 0; a non-zero exit means the + /// input was tampered with and any plaintext already written must be discarded. + AsconAEAD128 { + /// The 128-bit 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 128-bit key in hex or binary. + #[arg(long)] + key_file: Option, + + /// The 128-bit nonce in hex. Hazardous override: supplying it keeps the nonce out of the + /// stream (see above), and reusing one under a given key breaks the cipher. + #[arg(long)] + nonce: Option, + + /// A file containing a 128-bit nonce in hex or binary; the same hazardous override. + #[arg(long)] + nonce_file: Option, + + /// Associated data in hex (authenticated but not encrypted). + #[arg(long)] + ad: Option, + + /// Decrypt instead of encrypt. + #[arg(short, long)] + decrypt: bool, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// Perform HMAC-SHA256 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get @@ -704,6 +969,267 @@ enum Subcommands { x: bool, }, + /// AES-128 in CCM mode (NIST SP 800-38C): authenticated encryption of stdin to stdout. + /// + /// CCM is an AEAD: it protects both confidentiality and authenticity, and `decrypt` either + /// writes the plaintext or fails, unlike aes*-cbc/-cfb/-ctr, which cannot detect tampering. + /// + /// The output of `encrypt` is `ciphertext || tag` -- SP 800-38C Sec 6.1 step 8's own layout -- + /// so it is `--tag-len` bytes longer than the input, and `decrypt` reads the tag back off the + /// end. Both directions authenticate `--aad` as well as the payload. + /// + /// THE NONCE IS SUPPLIED, NOT GENERATED, and this is the only cipher command here that takes + /// one. The other modes need an unpredictable IV, so they generate it; CCM needs the nonce to + /// be UNIQUE but not unpredictable (Sec 5.3: "The nonce is not required to be random"), and a + /// caller with a message counter can guarantee uniqueness better than a random draw. The nonce + /// is NOT written to the output, so `decrypt` needs the same `--nonce` as `encrypt`. + /// + /// WARNING: never reuse a nonce under one key. For CCM a repeat is worse than for CTR: it + /// reuses the keystream AND lets an attacker who can replay the nonce flip any chosen bit of + /// the payload (Appendix B.1). Use a counter, or a random value long enough that a collision is + /// negligible. + /// + /// Nonce length must be 7 to 13 bytes and `--tag-len` one of 4, 6, 8, 10, 12, 14, 16 + /// (Appendix A.1). The two are linked to the payload limit and the forgery bound respectively: + /// a nonce of n bytes caps the payload at 2^(8*(15-n)) - 1 bytes, so 13 bytes allows only + /// 64 KiB - 1 while 7 bytes is effectively unlimited; and Sec B.2 says a tag shorter than + /// 8 bytes "shall not be used without a careful analysis of the risks". A 12-byte nonce with a + /// 16-byte tag is the usual choice; `--tag-len` defaults to 16, while the nonce must be given. + /// + /// UNLIKE EVERY OTHER CIPHER COMMAND HERE, THIS ONE DOES NOT STREAM: it reads all of stdin + /// before doing any work, so memory use is proportional to the input. That is inherent to CCM, + /// not a limitation of this implementation -- Sec 3: "CCM is not designed to support partial + /// processing or stream processing", because Appendix A.2.1 puts the payload length inside the + /// first block the MAC covers. It does buy one thing: on `decrypt` NO plaintext is written + /// until the tag has verified, so unlike `ascon-aead128` a non-zero exit leaves nothing to + /// discard. For large inputs use `ascon-aead128`, which streams. + /// + /// Input may be any length: CCM pads internally and the payload is not block-aligned. + /// + /// 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_CCM { + action: BlockModeAction, + + /// The 16-byte AES 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The nonce in hex, 7 to 13 bytes. MUST be unique per encryption under a given key. + #[arg(long)] + nonce: Option, + + /// A file containing the nonce, as raw bytes exactly as they are (no hex decoding). + #[arg(long)] + nonce_file: Option, + + /// Associated data in hex: authenticated but not encrypted. Must match on decrypt. + #[arg(long)] + aad: Option, + + /// Tag length in bytes: one of 4, 6, 8, 10, 12, 14, 16. Must match on decrypt. + #[arg(long, default_value_t = 16)] + tag_len: usize, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-192 in CCM mode (NIST SP 800-38C), authenticated encryption of stdin to stdout. + /// + /// See `aes128-ccm` for the nonce convention, the length rules, the non-streaming note and the + /// warnings; only the key length differs. + AES192_CCM { + action: BlockModeAction, + + /// The 24-byte AES 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The nonce in hex, 7 to 13 bytes. MUST be unique per encryption under a given key. + #[arg(long)] + nonce: Option, + + /// A file containing the nonce, as raw bytes exactly as they are (no hex decoding). + #[arg(long)] + nonce_file: Option, + + /// Associated data in hex: authenticated but not encrypted. Must match on decrypt. + #[arg(long)] + aad: Option, + + /// Tag length in bytes: one of 4, 6, 8, 10, 12, 14, 16. Must match on decrypt. + #[arg(long, default_value_t = 16)] + tag_len: usize, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-256 in CCM mode (NIST SP 800-38C), authenticated encryption of stdin to stdout. + /// + /// See `aes128-ccm` for the nonce convention, the length rules, the non-streaming note and the + /// warnings; only the key length differs. + AES256_CCM { + action: BlockModeAction, + + /// The 32-byte AES 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The nonce in hex, 7 to 13 bytes. MUST be unique per encryption under a given key. + #[arg(long)] + nonce: Option, + + /// A file containing the nonce, as raw bytes exactly as they are (no hex decoding). + #[arg(long)] + nonce_file: Option, + + /// Associated data in hex: authenticated but not encrypted. Must match on decrypt. + #[arg(long)] + aad: Option, + + /// Tag length in bytes: one of 4, 6, 8, 10, 12, 14, 16. Must match on decrypt. + #[arg(long, default_value_t = 16)] + tag_len: usize, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-128 in GCM (NIST SP 800-38D), streaming stdin to stdout. + /// + /// AUTHENTICATED, unlike the other AES modes here: tampering with the ciphertext, the AAD or + /// the nonce is detected rather than merely producing wrong plaintext. + /// + /// On `encrypt`, a fresh nonce is generated and written as the FIRST 12 BYTES of the output, + /// the ciphertext follows, and the 16-byte tag is written last. `decrypt` reads the nonce back + /// from the first 12 bytes of input and streams the rest, checking the tag once input is + /// exhausted. There is deliberately no `--iv` flag: a repeated GCM nonce is worse than merely + /// unwise, since it lets an attacker recover the hash subkey (SP 800-38D Appendix A). + /// + /// `--aad` (hex) or `--aad-file` (raw bytes) supply the additional authenticated data, + /// which is covered by the tag but not encrypted; if neither is given, AAD is empty. + /// + /// Input may be ANY length: GCM needs no padding. + /// + /// WARNING: on `decrypt`, a tag failure may be reported only after plaintext has already been + /// written to stdout, because this command streams the inline decryptor. A script MUST check + /// the exit code before trusting anything already written; on failure this command prints + /// `Error: authentication failed` and exits non-zero. + /// + /// 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_GCM { + action: BlockModeAction, + + /// The 16-byte AES 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The additional authenticated data, in hex. Covered by the tag but not encrypted. + #[arg(long)] + aad: Option, + + /// A file containing the additional authenticated data, as raw bytes (never hex-decoded). + /// If both aad and aad_file options are provided, the file will be used. + #[arg(long)] + aad_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-192 in GCM (NIST SP 800-38D), streaming stdin to stdout. + /// + /// See `aes128-gcm` for the nonce/tag framing, the AAD flags and the warnings; only the key + /// length differs. + AES192_GCM { + action: BlockModeAction, + + /// The 24-byte AES 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The additional authenticated data, in hex. Covered by the tag but not encrypted. + #[arg(long)] + aad: Option, + + /// A file containing the additional authenticated data, as raw bytes (never hex-decoded). + /// If both aad and aad_file options are provided, the file will be used. + #[arg(long)] + aad_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-256 in GCM (NIST SP 800-38D), streaming stdin to stdout. + /// + /// See `aes128-gcm` for the nonce/tag framing, the AAD flags and the warnings; only the key + /// length differs. + AES256_GCM { + action: BlockModeAction, + + /// The 32-byte AES 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, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// The additional authenticated data, in hex. Covered by the tag but not encrypted. + #[arg(long)] + aad: Option, + + /// A file containing the additional authenticated data, as raw bytes (never hex-decoded). + /// If both aad and aad_file options are provided, the file will be used. + #[arg(long)] + aad_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// AES-128 in ECB mode (NIST SP 800-38A Sec 6.1), streaming stdin to stdout. /// /// WARNING: ECB is NOT a confidentiality mode for data. Under a given key every plaintext @@ -996,7 +1522,23 @@ enum Subcommands { }, } +// The CLI body runs on a spawned thread with an explicit 8 MiB stack rather than directly on the +// process's main thread, whose size this program does not control: on Linux it is `ulimit -s` +// (8 MiB by default), and it can be a good deal smaller elsewhere or under a tightened limit. With +// a 1 MiB main stack a debug build overflows during argument parsing -- in every subcommand, before +// any algorithm runs -- so this is a property of the command tree, not of one algorithm's state. +// 8 MiB is the usual Linux default; do not lower it without re-checking that case. fn main() { + std::thread::Builder::new() + .name("bc-rust-main".to_string()) + .stack_size(8 * 1024 * 1024) + .spawn(run) + .expect("failed to start CLI thread") + .join() + .expect("CLI thread panicked"); +} + +fn run() { let cli = Cli::parse(); match &cli.subcommands { @@ -1051,6 +1593,42 @@ fn main() { Some(Subcommands::SHAKE256 { length, x }) => { sha3_cmd::shake_cmd(256, *length, *x); } + Some(Subcommands::CSHAKE128 { length, customization, function_name, x }) => { + sha3_cmd::cshake_cmd(128, *length, function_name, customization, *x); + } + Some(Subcommands::TUPLEHASH128 { length, elements, customization, x }) => { + sha3_cmd::tuplehash_cmd(128, *length, elements, customization, *x); + } + Some(Subcommands::TUPLEHASH256 { length, elements, customization, x }) => { + sha3_cmd::tuplehash_cmd(256, *length, elements, customization, *x); + } + Some(Subcommands::PARALLELHASH128 { length, block_size, customization, x }) => { + sha3_cmd::parallelhash_cmd(128, *length, *block_size, customization, *x); + } + Some(Subcommands::PARALLELHASH256 { length, block_size, customization, x }) => { + sha3_cmd::parallelhash_cmd(256, *length, *block_size, customization, *x); + } + Some(Subcommands::KMAC128 { length, customization, key, key_file, verify, x }) => { + mac_cmd::kmac_cmd(128, *length, customization, key, key_file, verify, *x) + } + Some(Subcommands::KMAC256 { length, customization, key, key_file, verify, x }) => { + mac_cmd::kmac_cmd(256, *length, customization, key, key_file, verify, *x) + } + Some(Subcommands::CSHAKE256 { length, customization, function_name, x }) => { + sha3_cmd::cshake_cmd(256, *length, function_name, customization, *x); + } + Some(Subcommands::AsconHash256 { x }) => { + ascon_cmd::hash256_cmd(*x); + } + Some(Subcommands::AsconXOF128 { length, x }) => { + ascon_cmd::xof128_cmd(*length, *x); + } + Some(Subcommands::AsconCXOF128 { length, customization, x }) => { + ascon_cmd::cxof128_cmd(customization, *length, *x); + } + Some(Subcommands::AsconAEAD128 { key, key_file, nonce, nonce_file, ad, decrypt, x }) => { + ascon_cmd::aead128_cmd(key, key_file, nonce, nonce_file, ad, *decrypt, *x); + } Some(Subcommands::HMAC_SHA256 { key, key_file, verify, x }) => { mac_cmd::mac_cmd(HMACVariant::SHA256, key, key_file, verify, *x) } @@ -1129,6 +1707,57 @@ fn main() { Some(Subcommands::AES256_CTR { action, key, key_file, x }) => { aes_ctr_cmd::aes256_ctr_cmd(action, key, key_file, *x); } + Some(Subcommands::AES128_CCM { + action, + key, + key_file, + nonce, + nonce_file, + aad, + tag_len, + x, + }) => { + aes_ccm_cmd::aes128_ccm_cmd( + action, key, key_file, nonce, nonce_file, aad, *tag_len, *x, + ); + } + Some(Subcommands::AES192_CCM { + action, + key, + key_file, + nonce, + nonce_file, + aad, + tag_len, + x, + }) => { + aes_ccm_cmd::aes192_ccm_cmd( + action, key, key_file, nonce, nonce_file, aad, *tag_len, *x, + ); + } + Some(Subcommands::AES256_CCM { + action, + key, + key_file, + nonce, + nonce_file, + aad, + tag_len, + x, + }) => { + aes_ccm_cmd::aes256_ccm_cmd( + action, key, key_file, nonce, nonce_file, aad, *tag_len, *x, + ); + } + Some(Subcommands::AES128_GCM { action, key, key_file, aad, aad_file, x }) => { + aes_gcm_cmd::aes128_gcm_cmd(action, key, key_file, aad, aad_file, *x); + } + Some(Subcommands::AES192_GCM { action, key, key_file, aad, aad_file, x }) => { + aes_gcm_cmd::aes192_gcm_cmd(action, key, key_file, aad, aad_file, *x); + } + Some(Subcommands::AES256_GCM { action, key, key_file, aad, aad_file, x }) => { + aes_gcm_cmd::aes256_gcm_cmd(action, key, key_file, aad, aad_file, *x); + } Some(Subcommands::AES128_ECB { action, key, key_file, x }) => { aes_ecb_cmd::aes128_ecb_cmd(action, key, key_file, *x); } diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index b6107e0c..d7a8d7fc 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -1,59 +1,171 @@ -use bouncycastle::core::traits::{Hash, XOF}; +use bouncycastle::core::traits::{Hash, XOF, XOFSqueezer}; use std::io; use std::io::{Read, Write}; -use bouncycastle::sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; +use bouncycastle::hex; +use bouncycastle::sha3::{ + CSHAKE128, CSHAKE256, PARALLELHASH128, PARALLELHASH256, SHA3_224, SHA3_256, SHA3_384, SHA3_512, + SHAKE128, SHAKE256, TUPLEHASH128, TUPLEHASH256, +}; +use std::process::exit; + +use crate::helpers::{stream_hash, stream_xof}; pub(crate) fn sha3_cmd(bit_len: usize, output_hex: bool) { match bit_len { - 224 => do_sha3(SHA3_224::new(), output_hex), - 256 => do_sha3(SHA3_256::new(), output_hex), - 384 => do_sha3(SHA3_384::new(), output_hex), - 512 => do_sha3(SHA3_512::new(), output_hex), + 224 => stream_hash(SHA3_224::new(), output_hex), + 256 => stream_hash(SHA3_256::new(), output_hex), + 384 => stream_hash(SHA3_384::new(), output_hex), + 512 => stream_hash(SHA3_512::new(), output_hex), _ => panic!("Unsupported algorithm: SHA3-{}", bit_len), } } -fn do_sha3(mut sha3: impl Hash, output_hex: bool) { - let mut buf: [u8; 1024] = [0u8; 1024]; +pub(crate) fn shake_cmd(bit_len: usize, output_len: usize, output_hex: bool) { + match bit_len { + 128 => stream_xof(SHAKE128::new(), output_len, output_hex), + 256 => stream_xof(SHAKE256::new(), output_len, output_hex), + _ => panic!("Unsupported algorithm: SHAKE-{}", bit_len), + } +} - // read from stdin - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - sha3.do_update(&buf[..bytes_read]); - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); +/// cSHAKE (NIST SP 800-185 Sec 3): SHAKE bound to a function name and a customization string. +/// +/// Both strings default to empty, and with both empty cSHAKE is defined to be plain SHAKE +/// (Sec 3.3 step 1), so `cshake128 32` and `shake128 32` agree. +pub(crate) fn cshake_cmd( + bit_len: usize, + output_len: usize, + function_name: &Option, + customization: &Option, + output_hex: bool, +) { + let n = function_name.as_deref().unwrap_or("").as_bytes(); + let s = customization.as_deref().unwrap_or("").as_bytes(); + match bit_len { + 128 => do_shake(CSHAKE128::new(n, s), output_len, output_hex), + 256 => do_shake(CSHAKE256::new(n, s), output_len, output_hex), + _ => panic!("Unsupported algorithm: cSHAKE-{}", bit_len), + } +} + +/// TupleHash (NIST SP 800-185 Sec 5): hashes a *tuple* of strings unambiguously. +/// +/// The tuple comes from repeated `--element` flags, each a hex string. With none given, stdin is +/// hashed as a single-element tuple -- which is not the same as hashing those bytes with SHAKE, +/// because the element is length-prefixed. +pub(crate) fn tuplehash_cmd( + bit_len: usize, + output_len: usize, + elements: &[String], + customization: &Option, + output_hex: bool, +) { + let s = customization.as_deref().unwrap_or("").as_bytes(); + + // Either the tuple came from flags, or stdin is the single element. + let tuple: Vec> = if elements.is_empty() { + vec![read_stdin()] + } else { + elements + .iter() + .map(|e| { + hex::decode(e).unwrap_or_else(|_| { + eprintln!("Error: --element must be hex."); + exit(-1); + }) + }) + .collect() + }; + let refs: Vec<&[u8]> = tuple.iter().map(|v| v.as_slice()).collect(); + + let out = match bit_len { + 128 => TUPLEHASH128::new(s, output_len).hash_tuple(&refs), + 256 => TUPLEHASH256::new(s, output_len).hash_tuple(&refs), + _ => panic!("Unsupported algorithm: TupleHash-{bit_len}"), + }; + write_out(&out, output_hex); +} + +/// ParallelHash (NIST SP 800-185 Sec 6): hashes stdin in `block_size`-byte blocks. +/// +/// The block size is part of the function, not a tuning knob -- the same input under a different +/// block size gives an unrelated hash, so it must match on both sides. +pub(crate) fn parallelhash_cmd( + bit_len: usize, + output_len: usize, + block_size: usize, + customization: &Option, + output_hex: bool, +) { + if block_size == 0 { + eprintln!("Error: --block-size must be greater than zero (SP 800-185 Sec 6.2)."); + exit(-1); } + let s = customization.as_deref().unwrap_or("").as_bytes(); + match bit_len { + 128 => { + let mut p = PARALLELHASH128::new(block_size, s, output_len); + stream_stdin(|chunk| p.do_update(chunk)); + write_out(&p.do_final(), output_hex); + } + 256 => { + let mut p = PARALLELHASH256::new(block_size, s, output_len); + stream_stdin(|chunk| p.do_update(chunk)); + write_out(&p.do_final(), output_hex); + } + _ => panic!("Unsupported algorithm: ParallelHash-{bit_len}"), + } +} - let out = sha3.do_final(); +/// Reads all of stdin. Used where the whole input must be held anyway (a tuple element). +fn read_stdin() -> Vec { + let mut out = Vec::new(); + let mut buf = [0u8; 1024]; + loop { + let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + if n == 0 { + return out; + } + out.extend_from_slice(&buf[..n]); + } +} + +/// Feeds stdin to `sink` in 1 KiB pieces, so a long input is never held in memory. +fn stream_stdin(mut sink: impl FnMut(&[u8])) { + let mut buf = [0u8; 1024]; + loop { + let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + if n == 0 { + return; + } + sink(&buf[..n]); + } +} +/// Writes the digest as raw bytes or hex, with the trailing newline the other commands emit. +fn write_out(out: &[u8], output_hex: bool) { if output_hex { - for b in out.iter() { + for b in out { print!("{b:02x}"); } } else { - io::stdout().write(&out).unwrap(); + io::stdout().write_all(out).expect("Failed to write to stdout"); } println!(); } -pub(crate) fn shake_cmd(bit_len: usize, output_len: usize, output_hex: bool) { - match bit_len { - 128 => do_shake(SHAKE128::new(), output_len, output_hex), - 256 => do_shake(SHAKE256::new(), output_len, output_hex), - _ => panic!("Unsupported algorithm: SHAKE-{}", bit_len), - } -} - fn do_shake(mut shake: impl XOF, output_len: usize, output_hex: bool) { let mut buf: [u8; 1024] = [0u8; 1024]; // read from stdin let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); while bytes_read != 0 { - shake.absorb(&buf[..bytes_read]).expect("absorb before squeeze is infallible"); + shake.do_update(&buf[..bytes_read]); bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); } - let out = shake.squeeze(output_len); + let mut shake = shake.into_squeezer(); + let out = shake.do_output(output_len); if output_hex { for b in out.iter() { print!("{b:02x}"); diff --git a/cli/tests/aes_ccm_cli_tests.rs b/cli/tests/aes_ccm_cli_tests.rs new file mode 100644 index 00000000..48b2ccbb --- /dev/null +++ b/cli/tests/aes_ccm_cli_tests.rs @@ -0,0 +1,591 @@ +//! Tests for the `aes128-ccm` / `aes192-ccm` / `aes256-ccm` subcommands. +//! +//! These drive the built `bc-rust` binary as a subprocess, because the behaviour worth testing is +//! the command-line contract itself -- the supplied nonce, the AAD flag, the tag riding at the end +//! of the ciphertext, the exit code on a failed tag check -- none of which is reachable from the +//! library API. +//! +//! Key loading is shared with `aes*-cbc` (`cli/src/block_mode_cmd.rs`), so that coverage is +//! repeated here rather than assumed. What is tested only here is everything CCM does differently +//! from the other five modes: +//! +//! * the **nonce is a required flag** and is *not* written to the output, unlike every other mode's +//! generated IV; +//! * `--aad` is authenticated but not encrypted, and must match on both sides; +//! * `--tag-len` changes the output length, and must match on both sides; +//! * `decrypt` **fails with a non-zero exit and writes nothing** when the input is inauthentic; +//! * the nonce length and tag length are validated against SP 800-38C Appendix A.1, and the nonce +//! length caps the payload. +//! +//! The known-answer test is SP 800-38C Appendix C.1, run end to end through the pipe, so the CLI is +//! pinned against the specification and not merely against itself. +//! +//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the +//! current profile, so there is nothing to build or locate by hand. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + +/// A 12-byte nonce, the length these tests use unless they are about nonce length. +const NONCE: &str = "000102030405060708090a0b"; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin. See `aes_ctr_cli_tests.rs` for why stdin +/// is written from a separate thread and why `BrokenPipe` is ignored; the reasoning is identical. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + }); + + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output +} + +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {} bytes", + out.stdout.len() + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex must be an even number of characters"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +/// SP 800-38C Appendix C.1, end to end: `Klen = 128, Tlen = 32, Nlen = 56, Alen = 64, Plen = 32`. +/// +/// The appendix's `C` is `7162015b 4dac255d`, which is the 4-byte ciphertext followed by the 4-byte +/// tag -- exactly what this command writes. This is the one test here that pins the CLI against the +/// specification rather than against a round trip. +#[test] +fn encrypt_matches_sp800_38c_appendix_c1() { + let out = run_ok( + &[ + "aes128-ccm", + "encrypt", + "--key", + "404142434445464748494a4b4c4d4e4f", + "--nonce", + "10111213141516", + "--aad", + "0001020304050607", + "--tag-len", + "4", + ], + &unhex("20212223"), + ); + assert_eq!(hex(&out), "7162015b4dac255d", "Appendix C.1's C string"); + + // And back again. The appendix gives no decryption example, but says one is "straightforward to + // construct" from each. + let back = run_ok( + &[ + "aes128-ccm", + "decrypt", + "--key", + "404142434445464748494a4b4c4d4e4f", + "--nonce", + "10111213141516", + "--aad", + "0001020304050607", + "--tag-len", + "4", + ], + &out, + ); + assert_eq!(hex(&back), "20212223", "Appendix C.1's P"); +} + +/// A round trip at each key length, with AAD, over a payload that spans several blocks and does not +/// end on a block boundary. +#[test] +fn encrypt_then_decrypt_round_trips() { + let plaintext: Vec = (0..=200u8).collect(); + for (cmd, key) in [("aes128-ccm", KEY_128), ("aes192-ccm", KEY_192), ("aes256-ccm", KEY_256)] { + let sealed = run_ok( + &[cmd, "encrypt", "--key", key, "--nonce", NONCE, "--aad", "cafebabe"], + &plaintext, + ); + assert_eq!( + sealed.len(), + plaintext.len() + 16, + "{cmd}: the default tag length is 16, and the nonce is not written" + ); + let opened = + run_ok(&[cmd, "decrypt", "--key", key, "--nonce", NONCE, "--aad", "cafebabe"], &sealed); + assert_eq!(opened, plaintext, "{cmd}: round trip"); + } +} + +/// The three commands are not interchangeable: a ciphertext from one must not decrypt under +/// another, even with the right-length key, and the failure is the tag check rather than garbage. +#[test] +fn the_three_variants_are_not_interchangeable() { + let sealed = + run_ok(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE], b"a short message"); + let stderr = run_err(&["aes256-ccm", "decrypt", "--key", KEY_256, "--nonce", NONCE], &sealed); + assert!( + stderr.contains("authentication failed"), + "expected a tag-check failure, got: {stderr}" + ); +} + +/// The nonce is **not** written to the output, so `decrypt` needs the same `--nonce`. This is the +/// sharpest difference from the other five commands, all of which prepend their generated IV. +#[test] +fn the_nonce_is_not_written_to_the_output_and_is_required_to_decrypt() { + let plaintext = b"the nonce rides out of band"; + let sealed = run_ok(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE], plaintext); + assert_eq!( + sealed.len(), + plaintext.len() + 16, + "output is plaintext + tag only; no nonce prefix" + ); + + // A different nonce must fail: it changes both B0 and every counter block. + let mut other = unhex(NONCE); + other[0] ^= 1; + let stderr = + run_err(&["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", &hex(&other)], &sealed); + assert!(stderr.contains("authentication failed"), "got: {stderr}"); +} + +/// `--nonce-file` is raw bytes, not hex-or-raw guessed like `--key-file`: two different binary +/// nonces that happen to be valid hex *text* for the same value must not collapse to one nonce, +/// since a repeated nonce under one key breaks CCM's authentication (see the module docs). +#[test] +fn nonce_file_is_raw_bytes_not_hex_decoded() { + let dir = std::env::temp_dir().join(format!("bc_rust_ccm_cli_nonce_{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + + // 12 ASCII bytes that are also valid hex *text* -- decoding them halves the length to 6, which + // is out of CCM's 7..=13 range. A nonce-file that hex-decodes opportunistically would reject a + // perfectly good 12-byte nonce (or worse, silently accept a *different* file that decodes to + // the same 6 bytes); one that reads raw bytes only must accept these 12 bytes as-is. + let raw_path = dir.join("nonce_raw.bin"); + let raw_nonce = b"aabbccddeeff".to_vec(); + std::fs::write(&raw_path, &raw_nonce).expect("write raw nonce file"); + + let plaintext = b"the nonce file's bytes are used raw"; + let sealed = run_ok( + &["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce-file", raw_path.to_str().unwrap()], + plaintext, + ); + + // Decrypting with the 12 raw bytes, passed directly via --nonce, must agree: --nonce-file did + // not hex-decode them down to 6 bytes. + let recovered = + run_ok(&["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", &hex(&raw_nonce)], &sealed); + assert_eq!(recovered, plaintext); + + // The would-be hex decoding of those same 12 ASCII bytes is only 6 bytes, out of CCM's + // 7..=13 range -- if --nonce-file had decoded them, this file would already have been + // rejected as a bad nonce length instead of round-tripping above. + let stderr = + run_err(&["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", "aabbccddeeff"], &sealed); + assert!(stderr.contains("nonce is 6 bytes"), "got: {stderr}"); + + std::fs::remove_dir_all(&dir).ok(); +} + +/// Omitting the nonce is refused, and the message says why there is no generated one. +#[test] +fn a_missing_nonce_is_rejected_with_an_explanation() { + let stderr = run_err(&["aes128-ccm", "encrypt", "--key", KEY_128], b"data"); + assert!(stderr.contains("--nonce"), "stderr should name the flag: {stderr}"); + assert!( + stderr.contains("no generated nonce"), + "stderr should say why there is no generated nonce: {stderr}" + ); +} + +/// The AAD is authenticated but not encrypted: it does not change the ciphertext length, it does +/// change the tag, and a mismatch on decryption is caught. +#[test] +fn the_aad_is_authenticated_but_not_encrypted() { + let plaintext = b"payload"; + let with = run_ok( + &["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE, "--aad", "0011"], + plaintext, + ); + let without = run_ok(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE], plaintext); + + assert_eq!(with.len(), without.len(), "AAD does not change the output length"); + assert_eq!( + with[..plaintext.len()], + without[..plaintext.len()], + "AAD does not change the ciphertext, only the tag" + ); + assert_ne!(with[plaintext.len()..], without[plaintext.len()..], "AAD changes the tag"); + + // Wrong AAD, missing AAD and extra AAD must all be caught. + for args in [ + vec!["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", NONCE, "--aad", "0012"], + vec!["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", NONCE], + vec!["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", NONCE, "--aad", "001100"], + ] { + let stderr = run_err(&args, &with); + assert!(stderr.contains("authentication failed"), "{args:?} gave: {stderr}"); + } +} + +/// A failed tag check must exit non-zero **and write nothing**. This is what buffering the input +/// buys, and it is stronger than `ascon-aead128`'s contract; SP 800-38C Sec 6.2 requires that on +/// INVALID "the payload P and the MAC T shall not be revealed". +#[test] +fn a_tampered_ciphertext_produces_no_output_at_all() { + let plaintext: Vec = (0..=255u8).collect(); + let sealed = run_ok(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE], &plaintext); + + // Flip a bit in the ciphertext, then in the tag; both must be caught with empty stdout. + for pos in [0usize, plaintext.len() - 1, plaintext.len(), sealed.len() - 1] { + let mut bad = sealed.clone(); + bad[pos] ^= 0x01; + let out = run(&["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", NONCE], &bad); + assert!(!out.status.success(), "a flipped bit at {pos} must fail"); + assert!( + out.stdout.is_empty(), + "no plaintext may be written when the tag check fails (flipped byte {pos}), \ + got {} bytes", + out.stdout.len() + ); + assert!( + String::from_utf8_lossy(&out.stderr).contains("authentication failed"), + "flipped byte {pos}" + ); + } +} + +/// `--tag-len` changes the output length and must match on both sides, and only A.1's values are +/// accepted. +#[test] +fn tag_len_is_validated_and_must_match() { + let plaintext = b"tag length matters"; + + for t in [4usize, 6, 8, 10, 12, 14, 16] { + let t_str = t.to_string(); + let sealed = run_ok( + &["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE, "--tag-len", &t_str], + plaintext, + ); + assert_eq!(sealed.len(), plaintext.len() + t, "tag-len {t}"); + let opened = run_ok( + &["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", NONCE, "--tag-len", &t_str], + &sealed, + ); + assert_eq!(opened, plaintext, "tag-len {t} round trip"); + } + + // A.1: t is an element of {4, 6, 8, 10, 12, 14, 16}. Odd values and out-of-range are refused. + for bad in ["0", "2", "5", "15", "17", "32"] { + let stderr = run_err( + &["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE, "--tag-len", bad], + b"data", + ); + assert!(stderr.contains("tag-len"), "tag-len {bad} gave: {stderr}"); + assert!(stderr.contains("A.1"), "the message should cite A.1: {stderr}"); + } + + // A tag-len mismatch between the two sides is caught rather than silently truncating. + let sealed = run_ok( + &["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE, "--tag-len", "16"], + plaintext, + ); + let stderr = run_err( + &["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", NONCE, "--tag-len", "8"], + &sealed, + ); + assert!(stderr.contains("authentication failed"), "got: {stderr}"); +} + +/// An invalid tag length is a command-line error, so it must be rejected without waiting for EOF +/// on the payload pipe. +#[test] +fn invalid_tag_len_is_rejected_before_stdin_is_read() { + let mut child = Command::new(BC_RUST) + .args(["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE, "--tag-len", "5"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + // Keep `child.stdin` open: exiting while it is open proves the command did not call + // `read_all_stdin` before validating the option. + let deadline = Instant::now() + Duration::from_secs(2); + loop { + if child.try_wait().expect("failed to poll bc-rust").is_some() { + break; + } + if Instant::now() >= deadline { + child.kill().expect("failed to stop hung bc-rust"); + let _ = child.wait(); + panic!("invalid --tag-len waited for stdin EOF"); + } + thread::sleep(Duration::from_millis(10)); + } + + let output = child.wait_with_output().expect("failed to collect bc-rust output"); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("tag-len") && stderr.contains("A.1"), "got: {stderr}"); +} + +#[test] +fn nonce_file_read_errors_are_reported_as_read_errors() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock after Unix epoch") + .as_nanos(); + let missing = std::env::temp_dir() + .join(format!("bc_rust_ccm_missing_nonce_{}_{}", std::process::id(), unique)) + .join("nonce.bin"); + let stderr = run_err( + &[ + "aes128-ccm", + "encrypt", + "--key", + KEY_128, + "--nonce-file", + missing.to_str().expect("temporary path is UTF-8"), + ], + b"data", + ); + assert!(stderr.contains("couldn't read file"), "got: {stderr}"); + assert!(stderr.contains("nonce.bin"), "the error should name the file: {stderr}"); +} + +/// Every nonce length A.1 permits works, and nothing else does. The nonce length is not written +/// anywhere, so both sides must agree on it too. +#[test] +fn nonce_len_is_validated_across_a_1_s_whole_range() { + let plaintext = b"nonce lengths"; + + for n in 7usize..=13 { + let nonce = hex(&vec![0x5Au8; n]); + let sealed = + run_ok(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", &nonce], plaintext); + let opened = + run_ok(&["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", &nonce], &sealed); + assert_eq!(opened, plaintext, "nonce length {n}"); + } + + // A.1: n is an element of {7, ..., 13}. + for n in [0usize, 1, 6, 14, 16] { + let nonce = hex(&vec![0x5Au8; n]); + let stderr = + run_err(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", &nonce], b"data"); + assert!( + stderr.contains("7 to 13"), + "nonce length {n} should be refused with the range: {stderr}" + ); + } +} + +/// The nonce length caps the payload (A.1's `p < 2^8q`, `q = 15 - n`), and the error says so with +/// the numbers rather than just failing. +#[test] +fn a_payload_past_the_q_limit_is_rejected_with_the_numbers() { + // n = 13 gives q = 2, so the limit is 65535 bytes. + let nonce = hex(&[0x5Au8; 13]); + let too_big = vec![0u8; 65536]; + let stderr = run_err(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", &nonce], &too_big); + assert!(stderr.contains("65535"), "the message should give the limit: {stderr}"); + assert!(stderr.contains("65536"), "and the actual input length: {stderr}"); + + // One byte under the limit is fine, which pins the boundary rather than just the rejection. + let ok = vec![0u8; 65535]; + let sealed = run_ok(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", &nonce], &ok); + assert_eq!(sealed.len(), 65535 + 16); + + // The decrypt side hits the same limit on the input minus its tag, and must explain it the + // same way rather than dumping the raw error: 65536 bytes of ciphertext plus a 16-byte tag. + let too_big_sealed = vec![0u8; 65536 + 16]; + let stderr = + run_err(&["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", &nonce], &too_big_sealed); + assert!(stderr.contains("65535"), "the decrypt message should give the limit: {stderr}"); + assert!(stderr.contains("65536"), "and the actual payload length: {stderr}"); + assert!(stderr.contains("shorter nonce"), "and the remedy: {stderr}"); + assert!(!stderr.contains("GenericError"), "not the Debug form: {stderr}"); +} + +/// A nonce file ending in a newline -- the `echo` without `-n` mistake -- is used as it is, since +/// stripping it would collapse two different nonces into one (see `load_nonce`), but is warned +/// about, because every length in 7..=13 is valid and the only other symptom would be a failed tag +/// check on the far side. +#[test] +fn a_nonce_file_ending_in_a_newline_is_used_as_is_but_warned_about() { + let dir = std::env::temp_dir().join(format!("bc_rust_ccm_cli_nonce_nl_{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + let path = dir.join("nonce_with_newline.bin"); + let mut with_newline = unhex(NONCE); + with_newline.push(b'\n'); + std::fs::write(&path, &with_newline).expect("write nonce file"); + + let plaintext = b"thirteen bytes of nonce, the last one a newline"; + let output = run( + &["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce-file", path.to_str().unwrap()], + plaintext, + ); + assert!(output.status.success(), "the file is still a valid 13-byte nonce"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("newline"), "the trailing newline must be warned about: {stderr}"); + assert!(stderr.contains("echo -n"), "and the remedy given: {stderr}"); + + // The 13 bytes, newline included, are the nonce: decrypting with exactly those via --nonce + // succeeds, and with the 12-byte nonce the file was meant to hold, it does not. + let recovered = run_ok( + &["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", &hex(&with_newline)], + &output.stdout, + ); + assert_eq!(recovered, plaintext); + let stderr = + run_err(&["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", NONCE], &output.stdout); + assert!(stderr.contains("authentication failed"), "got: {stderr}"); + + // A file without the newline draws no warning. + let clean_path = dir.join("nonce_clean.bin"); + std::fs::write(&clean_path, unhex(NONCE)).expect("write nonce file"); + let output = run( + &["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce-file", clean_path.to_str().unwrap()], + plaintext, + ); + assert!(output.status.success()); + assert!( + output.stderr.is_empty(), + "no warning for a clean file: {}", + String::from_utf8_lossy(&output.stderr) + ); + + std::fs::remove_dir_all(&dir).ok(); +} + +/// Sec 6.2 step 1: a `C` too short to contain a tag is rejected before anything else. +#[test] +fn an_input_shorter_than_the_tag_is_rejected() { + for len in [0usize, 1, 15] { + let stderr = run_err( + &["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", NONCE], + &vec![0u8; len], + ); + assert!( + stderr.contains("shorter than"), + "a {len}-byte input should be refused as too short: {stderr}" + ); + } + + // Exactly the tag length is an empty payload plus its tag, which is valid (Sec 5.3 footnote). + let sealed = run_ok(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE], b""); + assert_eq!(sealed.len(), 16); + let opened = run_ok(&["aes128-ccm", "decrypt", "--key", KEY_128, "--nonce", NONCE], &sealed); + assert!(opened.is_empty(), "an empty payload round trips to nothing"); +} + +/// `-x` writes hex, and it must be the hex of what the binary form writes. +#[test] +fn hex_output_matches_binary_output() { + let plaintext = b"hex and binary"; + let binary = run_ok(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE], plaintext); + let as_hex = + run_ok(&["aes128-ccm", "encrypt", "--key", KEY_128, "--nonce", NONCE, "-x"], plaintext); + assert_eq!(String::from_utf8_lossy(&as_hex).trim(), hex(&binary)); +} + +/// Key loading errors are the shared `block_mode_cmd` ones, checked here so the CCM commands are +/// not assumed to inherit them. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let stderr = run_err(&["aes128-ccm", "encrypt", "--key", KEY_256, "--nonce", NONCE], b"data"); + assert!(!stderr.is_empty(), "a 32-byte key must be refused by aes128-ccm"); + + let stderr = run_err(&["aes128-ccm", "encrypt", "--nonce", NONCE], b"data"); + assert!(stderr.contains("key"), "stderr should mention the key options: {stderr}"); +} + +/// An input larger than a pipe buffer round trips, which also pins that the non-streaming +/// read-all-of-stdin loop does not deadlock against its own output. +#[test] +fn a_payload_larger_than_the_pipe_buffer_round_trips() { + // 256 KiB, comfortably past the usual 64 KiB pipe buffer. A 12-byte nonce gives q = 3, so the + // payload limit is 16 MiB and this is well inside it. + let plaintext: Vec = (0..256 * 1024).map(|i| (i % 251) as u8).collect(); + let sealed = run_ok(&["aes256-ccm", "encrypt", "--key", KEY_256, "--nonce", NONCE], &plaintext); + assert_eq!(sealed.len(), plaintext.len() + 16); + let opened = run_ok(&["aes256-ccm", "decrypt", "--key", KEY_256, "--nonce", NONCE], &sealed); + assert_eq!(opened, plaintext); +} + +/// The subcommands are listed in `--help`, and their own help documents the things that differ from +/// the other modes: the supplied nonce, the non-streaming behaviour, and the nonce-reuse hazard. +#[test] +fn the_subcommands_are_documented_in_help() { + let help = String::from_utf8_lossy(&run_ok(&["--help"], b"")).into_owned(); + for cmd in ["aes128-ccm", "aes192-ccm", "aes256-ccm"] { + assert!(help.contains(cmd), "{cmd} should be listed in --help"); + } + + let per_cmd = String::from_utf8_lossy(&run_ok(&["aes128-ccm", "--help"], b"")).into_owned(); + assert!( + per_cmd.contains("NOT GENERATED") || per_cmd.contains("SUPPLIED"), + "the help should say the nonce is supplied: {per_cmd}" + ); + assert!( + per_cmd.to_lowercase().contains("does not stream"), + "the help should say it does not stream: {per_cmd}" + ); + assert!( + per_cmd.contains("never reuse a nonce"), + "the help should warn about nonce reuse: {per_cmd}" + ); + assert!( + per_cmd.contains("--tag-len") && per_cmd.contains("defaults to 16"), + "the help should identify the option that has a default: {per_cmd}" + ); + assert!( + !per_cmd.contains("usual choice and the default"), + "the help must not claim the required nonce has a default: {per_cmd}" + ); +} diff --git a/cli/tests/aes_gcm_cli_tests.rs b/cli/tests/aes_gcm_cli_tests.rs new file mode 100644 index 00000000..753616ba --- /dev/null +++ b/cli/tests/aes_gcm_cli_tests.rs @@ -0,0 +1,398 @@ +//! Tests for the `aes128-gcm` / `aes192-gcm` / `aes256-gcm` subcommands. +//! +//! These drive the built `bc-rust` binary as a subprocess, exactly as `aes_ctr_cli_tests.rs` does +//! and for the same reason: the command-line contract -- `nonce || ciphertext || tag` framing, the +//! `--aad` flags, exit codes, key loading -- is not reachable from the library API. GCM's algorithm +//! correctness is pinned in `bouncycastle-modes`' ACVP, GMAC and bc-java known-answer suites; what +//! is worth testing here is the wiring: that AAD actually reaches the tag, that a tampered byte or +//! tag is rejected with a non-zero exit, and that decrypt still writes whatever plaintext it +//! recovered before the failure (the streaming trade-off `aead_mode_cmd.rs` documents). +//! +//! There is no OpenSSL cross-check here: `openssl enc` does not do AEAD, so unlike the CTR/CFB/CBC +//! suites there is no equivalent vector to play through the pipe. +//! +//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the +//! current profile. +//! +//! # A note on this environment +//! +//! In this session's environment, every subprocess invocation of the **debug** `bc-rust` binary -- +//! including a bare `--help`, and every existing `aes_ctr_cli_tests.rs` case -- crashes with a +//! stack overflow before reaching any command logic (`thread 'main' has overflowed its stack`). +//! `git stash` reproduced it on the unmodified `main.rs` too, so it predates this change and is +//! unrelated to GCM; a release build (`cargo test --release -p cli`) does not hit it, which points +//! at clap's derive-generated parser code being large enough, unoptimized, to need more than the +//! default debug-build stack on this toolchain -- plausibly worsened by how many subcommands and +//! doc-comment-derived help strings this binary now has. This file's tests were run and pass +//! against the release build; `cargo test -p cli` (debug) will need that issue investigated +//! separately. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; +use std::thread; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// GCM's nonce, like CTR's, is 12 bytes. +const NONCE_LEN: usize = 12; +/// The (only) tag length these commands support: 128 bits. +const TAG_LEN: usize = 16; + +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + +const AAD: &str = "deadbeef"; + +const PLAINTEXT: &str = concat!( + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", + "0011223344", +); + +/// See `aes_ctr_cli_tests.rs::run` for why stdin is written from a separate thread and why +/// `BrokenPipe` is not a harness failure. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + }); + + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output +} + +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> (String, Vec) { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + (String::from_utf8_lossy(&out.stderr).into_owned(), out.stdout) +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +// ---- round trips --------------------------------------------------------------------------- + +#[test] +fn encrypt_then_decrypt_round_trips_with_aad() { + for (cmd, key) in [("aes128-gcm", KEY_128), ("aes192-gcm", KEY_192), ("aes256-gcm", KEY_256)] { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&[cmd, "encrypt", "--key", key, "--aad", AAD], &plaintext); + assert_eq!( + ciphertext.len(), + plaintext.len() + NONCE_LEN + TAG_LEN, + "{cmd}: nonce, ciphertext and tag" + ); + let recovered = run_ok(&[cmd, "decrypt", "--key", key, "--aad", AAD], &ciphertext); + assert_eq!(recovered, plaintext, "{cmd}: round trip"); + } +} + +/// AAD is optional; omitting it on both sides round-trips too. +#[test] +fn round_trips_with_no_aad() { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128], &plaintext); + let recovered = run_ok(&["aes128-gcm", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext); +} + +/// Any length round-trips with the ciphertext plus a fixed 12+16-byte overhead. +#[test] +fn any_input_length_is_accepted_and_round_trips() { + for len in 0..=(2 * 16 + 1) { + let plaintext = pseudo_random(len, len as u32); + let ciphertext = + run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128, "--aad", AAD], &plaintext); + assert_eq!( + ciphertext.len(), + len + NONCE_LEN + TAG_LEN, + "len {len}: nonce, equal-length body, tag" + ); + let recovered = + run_ok(&["aes128-gcm", "decrypt", "--key", KEY_128, "--aad", AAD], &ciphertext); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + } +} + +/// Round trips at sizes that straddle the 1 KiB streaming chunk and the tag-hold-back boundary. +#[test] +fn round_trips_across_chunk_boundaries() { + for size in [0usize, 1, 15, 16, 17, 1023, 1024, 1025, 4096, 4099, 65536] { + let plaintext = pseudo_random(size, size as u32); + let ciphertext = + run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128, "--aad", AAD], &plaintext); + let recovered = + run_ok(&["aes128-gcm", "decrypt", "--key", KEY_128, "--aad", AAD], &ciphertext); + assert_eq!(recovered, plaintext, "{size} bytes should round trip"); + } +} + +/// A fresh nonce per invocation. +#[test] +fn each_invocation_uses_a_fresh_nonce() { + let plaintext = unhex(PLAINTEXT); + let mut seen = std::collections::BTreeSet::new(); + + for _ in 0..8 { + let ciphertext = run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128], &plaintext); + let nonce = ciphertext[..NONCE_LEN].to_vec(); + assert!(seen.insert(nonce), "the CLI reused a nonce across invocations"); + let recovered = run_ok(&["aes128-gcm", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext); + } +} + +#[test] +fn hex_output_matches_binary_output() { + let plaintext = unhex(PLAINTEXT); + let binary = run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128], &plaintext); + let hex_out = run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128, "-x"], &plaintext); + + let hex_str = String::from_utf8(hex_out).expect("hex output is text"); + // The nonce differs per run, so compare lengths and that the body decodes to something of the + // same shape rather than the exact bytes. + assert_eq!(hex_str.trim_end().len(), binary.len() * 2); + assert_eq!(unhex(hex_str.trim_end()).len(), binary.len()); +} + +// ---- AAD ------------------------------------------------------------------------------------- + +/// Decrypting with the wrong AAD must fail authentication. +#[test] +fn wrong_aad_fails_authentication() { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128, "--aad", AAD], &plaintext); + let (stderr, _stdout) = + run_err(&["aes128-gcm", "decrypt", "--key", KEY_128, "--aad", "00112233"], &ciphertext); + assert!( + stderr.contains("authentication failed"), + "stderr should report authentication failure: {stderr}" + ); +} + +/// Encrypting with AAD and decrypting with none (or vice versa) must fail authentication too. +#[test] +fn missing_aad_on_one_side_fails_authentication() { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128, "--aad", AAD], &plaintext); + let (stderr, _stdout) = run_err(&["aes128-gcm", "decrypt", "--key", KEY_128], &ciphertext); + assert!( + stderr.contains("authentication failed"), + "stderr should report authentication failure: {stderr}" + ); +} + +/// `--aad-file` is raw bytes, never hex-or-raw guessed like `--key-file`: a binary header that +/// happens to parse as hex text must be authenticated as the bytes in the file, or the tag will not +/// verify against any other GCM implementation given the same file. Each case encrypts with the +/// file and decrypts with `--aad` set to the hex of the file's exact bytes. +#[test] +fn aad_file_is_raw_bytes_not_hex_decoded() { + let dir = std::env::temp_dir().join(format!("bc_rust_gcm_cli_aad_{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + let plaintext = unhex(PLAINTEXT); + + let cases: [(&str, &[u8]); 3] = [ + // ASCII that is also valid hex text: hex-decoding would authenticate 2 bytes, not 4. + ("ascii_hex", b"cafe"), + // Sixteen zero bytes, which the hex decoder skips entirely: decoding would authenticate + // empty AAD. + ("zeros", &[0u8; 16]), + // A trailing backslash, which sent the hex decoder's `\x` handling past the end of the + // buffer. + ("trailing_backslash", b"header\\"), + ]; + for (name, aad) in cases { + let path = dir.join(name); + std::fs::write(&path, aad).expect("write AAD file"); + let aad_hex: String = aad.iter().map(|b| format!("{b:02x}")).collect(); + + let ciphertext = run_ok( + &["aes128-gcm", "encrypt", "--key", KEY_128, "--aad-file", path.to_str().unwrap()], + &plaintext, + ); + let recovered = + run_ok(&["aes128-gcm", "decrypt", "--key", KEY_128, "--aad", &aad_hex], &ciphertext); + assert_eq!(recovered, plaintext, "case {name}"); + } + + std::fs::remove_dir_all(&dir).ok(); +} + +// ---- tamper detection -------------------------------------------------------------------------- + +/// A tampered ciphertext byte must be rejected, non-zero exit. +#[test] +fn a_tampered_ciphertext_byte_is_rejected() { + let plaintext = unhex(PLAINTEXT); + let mut ciphertext = + run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128, "--aad", AAD], &plaintext); + let body_start = NONCE_LEN; + ciphertext[body_start] ^= 0x01; + + let (stderr, _stdout) = + run_err(&["aes128-gcm", "decrypt", "--key", KEY_128, "--aad", AAD], &ciphertext); + assert!( + stderr.contains("authentication failed"), + "stderr should report authentication failure: {stderr}" + ); +} + +/// A tampered tag byte must be rejected too. +#[test] +fn a_tampered_tag_byte_is_rejected() { + let plaintext = unhex(PLAINTEXT); + let mut ciphertext = + run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128, "--aad", AAD], &plaintext); + let last = ciphertext.len() - 1; + ciphertext[last] ^= 0x01; + + let (stderr, _stdout) = + run_err(&["aes128-gcm", "decrypt", "--key", KEY_128, "--aad", AAD], &ciphertext); + assert!( + stderr.contains("authentication failed"), + "stderr should report authentication failure: {stderr}" + ); +} + +/// The streaming trade-off `aead_mode_cmd.rs` documents: on a tag failure, whatever plaintext the +/// inline decryptor had already released before the tag check stands on stdout. For a message +/// longer than the tag, that is everything except (at most) the last `TAG_LEN` bytes. +#[test] +fn decrypt_still_writes_the_plaintext_it_had_already_released_on_forgery() { + let plaintext = pseudo_random(4096, 7); + let mut ciphertext = + run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128, "--aad", AAD], &plaintext); + let last = ciphertext.len() - 1; + ciphertext[last] ^= 0x01; // corrupt the tag only, leaving the ciphertext body intact + + let out = run(&["aes128-gcm", "decrypt", "--key", KEY_128, "--aad", AAD], &ciphertext); + assert!(!out.status.success(), "a corrupted tag must be rejected"); + assert!( + out.stdout.len() >= plaintext.len() - TAG_LEN, + "most of the plaintext should already have reached stdout: got {} of {} bytes", + out.stdout.len(), + plaintext.len() + ); + assert_eq!( + &out.stdout[..out.stdout.len().min(plaintext.len())], + &plaintext[..out.stdout.len().min(plaintext.len())], + "the released bytes must be the genuine plaintext, not garbage" + ); +} + +// ---- short input --------------------------------------------------------------------------- + +/// Input shorter than the 12-byte nonce is rejected. +#[test] +fn decrypt_input_shorter_than_the_nonce_is_rejected() { + for len in [0usize, 1, 11] { + let (stderr, _stdout) = + run_err(&["aes128-gcm", "decrypt", "--key", KEY_128], &pseudo_random(len, 1)); + assert!( + stderr.contains("12-byte nonce"), + "stderr should explain the missing nonce (len {len}): {stderr}" + ); + } +} + +/// Input that has a nonce but not a full tag is rejected as an authentication failure (there is +/// nothing to check the tag against). +#[test] +fn decrypt_input_with_a_nonce_but_no_full_tag_is_rejected() { + // `encrypt` on empty input yields exactly nonce || tag; drop the last tag byte. + let nonce_and_tag = run_ok(&["aes128-gcm", "encrypt", "--key", KEY_128], &[]); + let short = &nonce_and_tag[..nonce_and_tag.len() - 1]; + let (stderr, _stdout) = run_err(&["aes128-gcm", "decrypt", "--key", KEY_128], short); + assert!( + stderr.contains("authentication failed"), + "stderr should report authentication failure: {stderr}" + ); +} + +// ---- key handling --------------------------------------------------------------------------- + +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let (stderr, _stdout) = + run_err(&["aes256-gcm", "encrypt", "--key", KEY_128], &unhex(PLAINTEXT)); + assert!(stderr.contains("32-byte key"), "stderr should name the expected length: {stderr}"); + assert!(stderr.contains("16 bytes"), "stderr should name the supplied length: {stderr}"); +} + +#[test] +fn a_missing_key_is_rejected() { + let (stderr, _stdout) = run_err(&["aes128-gcm", "encrypt"], &unhex(PLAINTEXT)); + assert!(stderr.contains("--key"), "stderr should mention the key options: {stderr}"); +} + +// ---- discoverability -------------------------------------------------------------------------- + +#[test] +fn the_subcommands_are_listed_in_help() { + let out = run_ok(&["--help"], &[]); + let help = String::from_utf8_lossy(&out); + for cmd in ["aes128-gcm", "aes192-gcm", "aes256-gcm"] { + assert!(help.contains(cmd), "`--help` should list {cmd}"); + } +} + +/// The per-command help must document the AAD flags and the authenticated-but-streamed warning. +#[test] +fn per_command_help_documents_aad_and_the_streaming_warning() { + let out = run_ok(&["aes128-gcm", "--help"], &[]); + let help = String::from_utf8_lossy(&out); + assert!(help.contains("aad"), "help should mention AAD: {help}"); + assert!( + help.to_lowercase().contains("authenticat"), + "help should mention authentication: {help}" + ); +} diff --git a/cli/tests/ascon_cli_tests.rs b/cli/tests/ascon_cli_tests.rs new file mode 100644 index 00000000..f4e7dfda --- /dev/null +++ b/cli/tests/ascon_cli_tests.rs @@ -0,0 +1,324 @@ +//! Tests for the `ascon-hash256` / `ascon-xof128` / `ascon-cxof128` / `ascon-aead128` +//! subcommands. +//! +//! These drive the built `bc-rust` binary as a subprocess, because the behaviour worth testing is +//! the command-line contract itself -- KAT-level correctness through the pipe, the `ciphertext || +//! tag` layout, generated nonce prefixing, `--key-file`/`--nonce-file` loading, AAD, and exit codes -- none of which is +//! reachable from the library API, which `crypto/ascon/tests/*.rs` already covers directly. +//! +//! The KAT values below are taken from the embedded vectors already pinned in +//! `crypto/ascon/tests/{hash256,xof128,cxof128,aead128}_tests.rs` (themselves NIST LWC vectors), +//! not retyped from memory. +//! +//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the +//! current profile, so there is nothing to build or locate by hand. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; +use std::thread; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The NIST LWC AEAD KAT convention uses key == nonce for the embedded vectors (see +/// `crypto/ascon/tests/aead128_tests.rs`'s `aead128_embedded_kat`). +const KEY_HEX: &str = "000102030405060708090a0b0c0d0e0f"; +const NONCE_LEN: usize = 16; +const TAG_LEN: usize = 16; + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +/// +/// See `aes_ctr_cli_tests.rs::run` for why stdin is written from a separate thread (a pipe with a +/// bounded buffer deadlocks otherwise) and why a `BrokenPipe` write error is swallowed (an +/// error-path command may exit before draining stdin). +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || { + match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + // `stdin` drops here, closing the pipe so the child sees EOF and can exit. + }); + + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG or on `/dev/urandom`. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +fn hex_stdout(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run_ok(args, stdin_bytes); + String::from_utf8(out).expect("hex output is text").trim_end().to_string() +} + +// ---- ascon-hash256 ------------------------------------------------------------------------ + +/// LWC_HASH_KAT_256.txt Count 1: the digest of the empty message. +#[test] +fn ascon_hash256_matches_the_embedded_kat_for_the_empty_message() { + let out = hex_stdout(&["ascon-hash256", "-x"], &[]); + assert_eq!(out, "0b3be5850f2f6b98caf29f8fdea89b64a1fa70aa249b8f839bd53baa304d92b2"); +} + +/// A non-empty message, matching LWC_HASH_KAT_256.txt Count 9. +#[test] +fn ascon_hash256_matches_the_embedded_kat_for_a_multi_byte_message() { + let out = hex_stdout(&["ascon-hash256", "-x"], &unhex("0001020304050607")); + assert_eq!(out, "b88e497ae8e6fb641b87ef622eb8f2fca0ed95383f7ffebe167acf1099ba764f"); +} + +// ---- ascon-xof128 -------------------------------------------------------------------------- + +/// LWC_XOF_KAT_128_512.txt Count 1: 64 bytes squeezed after absorbing the empty message. +#[test] +fn ascon_xof128_matches_the_embedded_kat_for_the_empty_message() { + let out = hex_stdout(&["ascon-xof128", "64", "-x"], &[]); + assert_eq!( + out, + "473d5e6164f58b39dfd84aacdb8ae42ec2d91fed33388ee0d960d9b3993295c\ + 6ad77855a5d3b13fe6ad9e6098988373af7d0956d05a8f1665d2c67d1a3ad10ff" + ); +} + +/// The output length is the caller's choice, and shorter output is a prefix of longer output +/// (every XOF's defining property) -- pinned here through the CLI specifically, since the CLI is +/// what turns the length into a positional argument. +#[test] +fn ascon_xof128_output_length_is_a_prefix_of_a_longer_squeeze() { + let full = hex_stdout(&["ascon-xof128", "64", "-x"], &[]); + let short = hex_stdout(&["ascon-xof128", "16", "-x"], &[]); + assert_eq!(short.len(), 32, "16 bytes is 32 hex characters"); + assert!(full.starts_with(&short)); +} + +// ---- ascon-cxof128 ------------------------------------------------------------------------- + +/// LWC_CXOF_KAT_128_512.txt Count 4: message `00`, customization `10`. +#[test] +fn ascon_cxof128_matches_the_embedded_kat() { + let out = hex_stdout(&["ascon-cxof128", "64", "--customization", "10", "-x"], &unhex("00")); + assert_eq!( + out, + "63fa8ba86382f2d544580f51322d080424b42c556eb74503cd73cf052bb993\ + bd6f5210984c71c9c445f43ccc5b158226e509bd339cd634414377f79411aa8d5c" + ); +} + +/// No `--customization` at all must give the same output as an empty one: `AsconCXof128::new()` +/// versus `with_customization(&[])`, both reachable only through the library elsewhere -- here we +/// pin that the CLI's `Option` plumbing treats "absent" and "empty" identically. +#[test] +fn ascon_cxof128_with_no_customization_matches_an_empty_one() { + let without = hex_stdout(&["ascon-cxof128", "64", "-x"], &[]); + let with_empty = hex_stdout(&["ascon-cxof128", "64", "--customization", "", "-x"], &[]); + assert_eq!(without, with_empty); + // LWC_CXOF_KAT_128_512.txt Count 1: message and customization both empty. + assert_eq!( + without, + "4f50159ef70bb3dad8807e034eaebd44c4fa2cbbc8cf1f05511ab66cdcc5299\ + 05ca12083fc186ad899b270b1473dc5f7ec88d1052082dcdfe69fb75d269e7b74" + ); +} + +// ---- ascon-aead128 ------------------------------------------------------------------------- + +/// LWC_AEAD_KAT_128_128.txt Count 1: the tag over an empty message with no AAD (key == nonce). +#[test] +fn ascon_aead128_matches_the_embedded_kat_for_an_empty_message() { + let out = hex_stdout(&["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "-x"], &[]); + assert_eq!(out, "4427d64b8e1e1451fc445960f0839bb0"); +} + +/// Encrypt then `--decrypt` round-trips a multi-KB payload, byte for byte, and the ciphertext is +/// exactly the generated nonce plus the plaintext plus the 16-byte tag. +#[test] +fn ascon_aead128_encrypt_then_decrypt_round_trips() { + let plaintext = pseudo_random(4096, 0xC0FFEE); + let ciphertext = run_ok(&["ascon-aead128", "--key", KEY_HEX], &plaintext); + assert_eq!( + ciphertext.len(), + plaintext.len() + NONCE_LEN + TAG_LEN, + "ciphertext is nonce plus plaintext plus the tag" + ); + + let recovered = run_ok(&["ascon-aead128", "--key", KEY_HEX, "--decrypt"], &ciphertext); + assert_eq!(recovered, plaintext); +} + +/// Associated data is authenticated on both sides of a round trip. +#[test] +fn ascon_aead128_associated_data_round_trips() { + let plaintext = pseudo_random(256, 7); + let ciphertext = run_ok(&["ascon-aead128", "--key", KEY_HEX, "--ad", "deadbeef"], &plaintext); + let recovered = + run_ok(&["ascon-aead128", "--key", KEY_HEX, "--ad", "deadbeef", "--decrypt"], &ciphertext); + assert_eq!(recovered, plaintext); +} + +/// Decrypting with the wrong associated data must fail the tag check, the same as tampering with +/// the ciphertext itself. +#[test] +fn ascon_aead128_wrong_associated_data_is_rejected() { + let plaintext = pseudo_random(64, 11); + let ciphertext = run_ok(&["ascon-aead128", "--key", KEY_HEX, "--ad", "deadbeef"], &plaintext); + let stderr = + run_err(&["ascon-aead128", "--key", KEY_HEX, "--ad", "cafebabe", "--decrypt"], &ciphertext); + assert!(stderr.contains("authentication failed"), "stderr: {stderr}"); +} + +/// A single flipped ciphertext byte must fail the tag check on decrypt, with a non-zero exit and +/// an explanatory stderr message -- the security-relevant contract the streaming decrypt path +/// (`ascon_cmd.rs::aead128_decrypt_stream`) exists to uphold. +#[test] +fn ascon_aead128_a_flipped_ciphertext_byte_is_rejected() { + let plaintext = pseudo_random(64, 1); + let mut ciphertext = run_ok(&["ascon-aead128", "--key", KEY_HEX], &plaintext); + ciphertext[NONCE_LEN] ^= 0x01; + + let stderr = run_err(&["ascon-aead128", "--key", KEY_HEX, "--decrypt"], &ciphertext); + assert!(stderr.contains("authentication failed"), "stderr: {stderr}"); +} + +/// A flipped tag byte (the last byte of the stream) must be rejected the same way. +#[test] +fn ascon_aead128_a_flipped_tag_byte_is_rejected() { + let plaintext = pseudo_random(64, 2); + let mut ciphertext = run_ok(&["ascon-aead128", "--key", KEY_HEX], &plaintext); + let last = ciphertext.len() - 1; + ciphertext[last] ^= 0x01; + + let stderr = run_err(&["ascon-aead128", "--key", KEY_HEX, "--decrypt"], &ciphertext); + assert!(stderr.contains("authentication failed"), "stderr: {stderr}"); +} + +/// Decrypt input shorter than the generated 16-byte nonce is rejected before any tag check is +/// attempted, including the empty-input case. +#[test] +fn ascon_aead128_decrypt_input_shorter_than_the_nonce_is_rejected() { + for len in [0usize, 1, 15] { + let stderr = run_err( + &["ascon-aead128", "--key", KEY_HEX, "--decrypt"], + &pseudo_random(len, len as u32 + 1), + ); + assert!( + stderr.contains("shorter than the 16-byte nonce"), + "len {len}: stderr should explain the missing nonce: {stderr}" + ); + } +} + +#[test] +fn ascon_aead128_explicit_nonce_decrypt_input_shorter_than_the_tag_is_rejected() { + for len in [0usize, 1, 15] { + let stderr = run_err( + &["ascon-aead128", "--key", KEY_HEX, "--nonce", KEY_HEX, "--decrypt"], + &pseudo_random(len, len as u32 + 1), + ); + assert!( + stderr.contains("shorter than the 16-byte tag"), + "len {len}: stderr should explain the missing tag: {stderr}" + ); + } +} + +#[test] +fn ascon_aead128_each_invocation_uses_a_fresh_nonce() { + let plaintext = pseudo_random(32, 19); + let a = run_ok(&["ascon-aead128", "--key", KEY_HEX], &plaintext); + let b = run_ok(&["ascon-aead128", "--key", KEY_HEX], &plaintext); + + assert_eq!(a.len(), plaintext.len() + NONCE_LEN + TAG_LEN); + assert_eq!(b.len(), plaintext.len() + NONCE_LEN + TAG_LEN); + assert_ne!(&a[..NONCE_LEN], &b[..NONCE_LEN], "the CLI reused a nonce"); +} + +/// `--key-file`/`--nonce-file` accept binary content, not just hex, the same as the AES commands' +/// `--key-file` (see `key_file_accepts_hex_and_binary` in `aes_ctr_cli_tests.rs`). +#[test] +fn ascon_aead128_key_file_and_nonce_file_accept_binary_content() { + let dir = std::env::temp_dir().join(format!("ascon_cli_test_{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + let key_path = dir.join("key.bin"); + let nonce_path = dir.join("nonce.bin"); + std::fs::write(&key_path, unhex(KEY_HEX)).expect("write key file"); + std::fs::write(&nonce_path, unhex(KEY_HEX)).expect("write nonce file"); + + let out = hex_stdout( + &[ + "ascon-aead128", + "--key-file", + key_path.to_str().unwrap(), + "--nonce-file", + nonce_path.to_str().unwrap(), + "-x", + ], + &[], + ); + assert_eq!(out, "4427d64b8e1e1451fc445960f0839bb0"); + + let _ = std::fs::remove_dir_all(&dir); +} + +/// The subcommands are listed in top-level help. +#[test] +fn the_subcommands_are_listed_in_help() { + let out = run_ok(&["--help"], &[]); + let text = String::from_utf8_lossy(&out); + for name in ["ascon-hash256", "ascon-xof128", "ascon-cxof128", "ascon-aead128"] { + assert!(text.contains(name), "--help should list {name}"); + } +} diff --git a/crypto/aes/src/ccm.rs b/crypto/aes/src/ccm.rs new file mode 100644 index 00000000..d879552c --- /dev/null +++ b/crypto/aes/src/ccm.rs @@ -0,0 +1,255 @@ +//! Type aliases for AES in CCM mode (NIST SP 800-38C). +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Ccm` takes the permutation and the +//! `KEY_LEN` / `BLOCK_LEN` / `NONCE_LEN` / `TAG_LEN` const parameters. These aliases pin the AES +//! values so callers never spell them out. They add nothing to the engine: the permutation still +//! implements none of the data-encryption traits itself (see the crate docs), the mode does. +//! +//! AES is the *only* cipher CCM can use. SP 800-38C Sec 3: "CCM is based on an approved symmetric +//! key block cipher algorithm whose block size is 128 bits ... thus, CCM cannot be used with the +//! Triple Data Encryption Algorithm, whose block size is 64 bits", and Sec 5.1 adds that +//! "currently, the AES algorithm is the only approved block cipher algorithm with this block size". +//! +//! # The nonce length and the tag length stay parameters +//! +//! `Dir` is [`Encrypting`](bouncycastle_modes::Encrypting) or +//! [`Decrypting`](bouncycastle_modes::Decrypting), as for the other modes. Beyond that, and unlike +//! the other aliases in this crate, these do not pin everything: `NONCE_LEN` and `TAG_LEN` +//! are real cryptographic choices, and CCM ties them to the payload limit and to the strength of +//! the authentication respectively, so hiding them behind a default would hide the decision: +//! +//! * **`NONCE_LEN` (the spec's `n`) fixes the maximum payload.** A.1 requires `n + q = 15`, and +//! `q` bounds the payload at `2^8q - 1` bytes. So a 13-byte nonce caps a message at 64 KiB - 1, +//! and a 7-byte nonce lifts the cap entirely at the cost of nonce space. See +//! [`Ccm`] for the table. +//! * **`TAG_LEN` (the spec's `t`) is the forgery bound.** Sec B.2: "a value of Tlen that is less +//! than 64 shall not be used without a careful analysis of the risks of accepting inauthentic +//! data as authentic". +//! +//! Both are still checked at compile time against A.1's permitted sets, so a wrong value is a +//! compile error rather than a runtime `Err`. +//! +//! [`CCM_NONCE_LEN`] and [`CCM_TAG_LEN`] name the sensible default pair -- a 12-byte nonce and a +//! 16-byte tag, which is what the NIST ACVP vectors and most protocols use -- for callers who have +//! no reason to choose otherwise: +//! +//! ```text +//! AES_CCM_128 // 12-byte nonce, 16-byte tag, < 16 MiB +//! AES_CCM_128 // IEEE 802.11 CCMP's pair +//! ``` +//! +//! # Generic streaming needs the buffering pair +//! +//! These aliases are for [`Ccm`] itself: its one-shots and its +//! length-declared streaming API, neither of which buffers. Code written against +//! [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] wants +//! [`AES_CCM_128_Encryptor`] / [`AES_CCM_128_Decryptor`] instead, which carry the extra +//! `FINAL_LEN` their streaming methods require; their one-shots bypass it. See +//! [`CcmEncryptor`] for why. + +use crate::aes_internal::{AES128Internal, AES192Internal, AES256Internal, BLOCK_LEN}; +use bouncycastle_modes::{Ccm, CcmDecryptor, CcmEncryptor}; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +// end of imports needed for docs + +/// The nonce length to use unless there is a reason not to: 12 bytes, which is what the NIST ACVP +/// `ACVP-AES-CCM` vectors use in every group. It leaves `q = 3`, so a payload of up to +/// 16 MiB - 1 bytes. +pub const CCM_NONCE_LEN: usize = 12; + +/// The tag length to use unless there is a reason not to: the full 16 bytes, the largest A.1 +/// permits. See the module docs on Sec B.2. +pub const CCM_TAG_LEN: usize = 16; + +/// AES-128 in CCM mode (SP 800-38C). +/// +/// `NONCE_LEN` must be 7..=13 and `TAG_LEN` one of 4, 6, 8, 10, 12, 14, 16 (A.1); anything else is +/// a compile error. Use [`CCM_NONCE_LEN`] and [`CCM_TAG_LEN`] if you have no reason to choose. +/// +/// The nonce is **supplied**, not generated, because CCM requires it to be unique but not random +/// (Sec 5.3), so a caller with a counter can do better than a draw from a DRBG. It must never +/// repeat under one key; see [`Ccm`]'s security considerations. +/// +/// ``` +/// use bouncycastle_aes::{AES_CCM_128, CCM_NONCE_LEN, CCM_TAG_LEN}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// type Ccm128 = AES_CCM_128; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// let nonce = [0x01u8; CCM_NONCE_LEN]; +/// let header = b"authenticated but not encrypted"; +/// let message = b"authenticated and encrypted"; +/// +/// // The spec's own layout: ciphertext with the tag appended (Sec 6.1 step 8). +/// let mut sealed = vec![0u8; message.len() + CCM_TAG_LEN]; +/// let n = Ccm128::::encrypt(&key, &nonce, header, message, &mut sealed).expect("encryption"); +/// assert_eq!(n, sealed.len()); +/// +/// let mut opened = vec![0u8; message.len()]; +/// let n = Ccm128::::decrypt(&key, &nonce, header, &sealed, &mut opened).expect("decryption"); +/// assert_eq!(&opened[..n], message); +/// +/// // Tampering with either the ciphertext or the header is caught. +/// let mut tampered = sealed.clone(); +/// tampered[0] ^= 1; +/// assert!(Ccm128::::decrypt(&key, &nonce, header, &tampered, &mut opened).is_err()); +/// assert!(Ccm128::::decrypt(&key, &nonce, b"other header", &sealed, &mut opened).is_err()); +/// ``` +/// +/// A detached tag, for a wire format that carries it separately: +/// +/// ``` +/// use bouncycastle_aes::{AES_CCM_128, CCM_NONCE_LEN, CCM_TAG_LEN}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// type Ccm128 = AES_CCM_128; +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let nonce = [0x02u8; CCM_NONCE_LEN]; +/// let message = b"a short packet"; +/// +/// let mut ct = vec![0u8; message.len()]; +/// let (n, tag) = Ccm128::::encrypt_detached(&key, &nonce, &[], message, &mut ct).unwrap(); +/// assert_eq!(n, message.len(), "CCM never expands the payload"); +/// +/// let mut pt = vec![0u8; message.len()]; +/// Ccm128::::decrypt_detached(&key, &nonce, &[], &ct, &tag, &mut pt).unwrap(); +/// assert_eq!(&pt[..], message); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CCM_128 = + Ccm; + +/// AES-192 in CCM mode. See [`AES_CCM_128`]. +/// +/// ``` +/// use bouncycastle_aes::{AES_CCM_192, CCM_NONCE_LEN, CCM_TAG_LEN}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// type Ccm192 = AES_CCM_192; +/// let key = KeyMaterial::<24>::from_bytes_as_type(&[0x42; 24], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let nonce = [0x03u8; CCM_NONCE_LEN]; +/// let message = [0u8; 30]; +/// +/// let mut sealed = vec![0u8; message.len() + CCM_TAG_LEN]; +/// Ccm192::::encrypt(&key, &nonce, &[], &message, &mut sealed).unwrap(); +/// let mut opened = vec![0u8; message.len()]; +/// Ccm192::::decrypt(&key, &nonce, &[], &sealed, &mut opened).unwrap(); +/// assert_eq!(opened, message); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CCM_192 = + Ccm; + +/// AES-256 in CCM mode. See [`AES_CCM_128`]. +/// +/// ``` +/// use bouncycastle_aes::{AES_CCM_256, CCM_NONCE_LEN, CCM_TAG_LEN}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// type Ccm256 = AES_CCM_256; +/// let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let nonce = [0x04u8; CCM_NONCE_LEN]; +/// let message = [0u8; 30]; +/// +/// let mut sealed = vec![0u8; message.len() + CCM_TAG_LEN]; +/// Ccm256::::encrypt(&key, &nonce, &[], &message, &mut sealed).unwrap(); +/// let mut opened = vec![0u8; message.len()]; +/// Ccm256::::decrypt(&key, &nonce, &[], &sealed, &mut opened).unwrap(); +/// assert_eq!(opened, message); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CCM_256 = + Ccm; + +/// AES-128 CCM as an [`AEADCipherEncryptor`], for code written against the generic AEAD trait. +/// +/// `FINAL_LEN` is the trait's: the size of the inline `ciphertext || tag` the final call returns, +/// so the largest message and the largest AAD the streaming `do_*` methods accept is +/// `FINAL_LEN - TAG_LEN`. It exists because `do_encrypt_init` is handed no length and CCM needs +/// one; see [`CcmEncryptor`]. The one-shot methods bypass that buffer and accept data up to CCM's +/// nonce-dependent payload limit. +/// +/// Note that the AAD shares that one bound although it is never part of the output: pick +/// `FINAL_LEN >= max(largest payload, largest AAD) + TAG_LEN`. A protocol whose authenticated +/// header can outgrow its payload pays for the header in every `[u8; FINAL_LEN]` the trait puts on +/// the stack, and is better served by [`AES_CCM_128`], which takes the AAD by reference. +/// +/// The nonce is generated here, unlike [`AES_CCM_128`]'s caller-supplied nonce. Consequently this +/// adapter pair requires `NONCE_LEN >= 12` -- the decryptor too, so that a parameter set which +/// compiles for one side compiles for the other; use [`AES_CCM_128`] with a caller-managed unique +/// nonce for shorter A.1 nonce lengths. +/// +/// ``` +/// use bouncycastle_aes::{AES_CCM_128_Decryptor, AES_CCM_128_Encryptor}; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +/// +/// // 2 KiB of message plus the 16-byte tag: comfortably above an 802.11 frame, the packet size +/// // CCM was designed for. +/// type Enc = AES_CCM_128_Encryptor<12, 16, { 2048 + 16 }>; +/// type Dec = AES_CCM_128_Decryptor<12, 16, { 2048 + 16 }>; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let (nonce, ciphertext, tag) = Enc::encrypt_detached(&key, b"header", b"message").unwrap(); +/// let plaintext = Dec::decrypt_detached(&key, &nonce, b"header", &ciphertext, &tag).unwrap(); +/// assert_eq!(plaintext, b"message"); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CCM_128_Encryptor< + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> = CcmEncryptor; + +/// AES-128 CCM as an [`AEADCipherDecryptor`]. See [`AES_CCM_128_Encryptor`]. +#[allow(non_camel_case_types)] +pub type AES_CCM_128_Decryptor< + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> = CcmDecryptor; + +/// AES-192 CCM as an [`AEADCipherEncryptor`]. See [`AES_CCM_128_Encryptor`]. +#[allow(non_camel_case_types)] +pub type AES_CCM_192_Encryptor< + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> = CcmEncryptor; + +/// AES-192 CCM as an [`AEADCipherDecryptor`]. See [`AES_CCM_128_Encryptor`]. +#[allow(non_camel_case_types)] +pub type AES_CCM_192_Decryptor< + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> = CcmDecryptor; + +/// AES-256 CCM as an [`AEADCipherEncryptor`]. See [`AES_CCM_128_Encryptor`]. +#[allow(non_camel_case_types)] +pub type AES_CCM_256_Encryptor< + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> = CcmEncryptor; + +/// AES-256 CCM as an [`AEADCipherDecryptor`]. See [`AES_CCM_128_Encryptor`]. +#[allow(non_camel_case_types)] +pub type AES_CCM_256_Decryptor< + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> = CcmDecryptor; diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs new file mode 100644 index 00000000..c5d7f2ff --- /dev/null +++ b/crypto/aes/src/gcm.rs @@ -0,0 +1,133 @@ +//! Type aliases for AES in GCM (NIST SP 800-38D). +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Gcm` takes the permutation, the +//! direction, and the `KEY_LEN` / `TAG_LEN` const parameters. These aliases pin the AES values and +//! fix the tag length at 128 bits, the maximum SP 800-38D Sec 5.2.1.2 allows. For a shorter tag +//! (96, 104, 112 or 120 bits), name `bouncycastle_modes::Gcm` directly with the desired `TAG_LEN`. +//! +//! The nonce is always [`bouncycastle_modes::GCM_NONCE_LEN`] (12 bytes / 96 bits): `Gcm` has no +//! nonce-length parameter at all, unlike `Ctr`'s aliases, because SP 800-38D's `len(IV) != 96` +//! branch (deriving `J0` from a GHASH of the IV) is not implemented -- see the `gcm` module docs in +//! `bouncycastle-modes`. + +use crate::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_modes::Gcm; + +/// AES-128 in GCM with a 128-bit tag. `Dir` is [`bouncycastle_modes::Encrypting`] or +/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// +/// The nonce is generated by the encryptor and returned; it is never supplied. Both directions +/// implement [`AEADCipherEncryptor`](bouncycastle_core::traits::AEADCipherEncryptor) / +/// [`AEADCipherDecryptor`](bouncycastle_core::traits::AEADCipherDecryptor); see the `gcm` module +/// docs in `bouncycastle-modes` for the detached-tag and inline `ciphertext || tag` layouts, and for +/// the security considerations (nonce uniqueness above all). +/// +/// ``` +/// use bouncycastle_aes::AES_GCM_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// let aad = b"header, sent in the clear"; +/// let message = *b"attack at dawn!!"; +/// +/// // Detached tag, one-shot. +/// let mut ciphertext = [0u8; 16]; +/// let (nonce, _, tag) = +/// AES_GCM_128::::encrypt_out_detached(&key, aad, &message, &mut ciphertext).unwrap(); +/// let mut plaintext = [0u8; 16]; +/// AES_GCM_128::::decrypt_out_detached(&key, &nonce, aad, &ciphertext, &tag, &mut plaintext) +/// .unwrap(); +/// assert_eq!(plaintext, message); +/// ``` +/// +/// Inline `ciphertext || tag`, through [`SymmetricCipherEncryptor`](bouncycastle_core::traits::SymmetricCipherEncryptor) / [`SymmetricCipherDecryptor`](bouncycastle_core::traits::SymmetricCipherDecryptor): +/// +/// ``` +/// use bouncycastle_aes::AES_GCM_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// let aad = b"associated data"; +/// let message = b"a message of no particular length at all"; +/// +/// let mut ciphertext = vec![0u8; AES_GCM_128::::encrypt_out_len(message.len())]; +/// let (nonce, written) = +/// AES_GCM_128::::encrypt_out(&key, message, &mut ciphertext).unwrap(); +/// assert_eq!(written, ciphertext.len()); +/// +/// let mut plaintext = vec![0u8; AES_GCM_128::::decrypt_out_max_len(ciphertext.len())]; +/// let n = AES_GCM_128::::decrypt_out(&key, &nonce, &ciphertext, &mut plaintext).unwrap(); +/// assert_eq!(&plaintext[..n], &message[..]); +/// ``` +/// +/// Streaming, with AAD fed via `do_update_aad` before any data: +/// +/// ``` +/// use bouncycastle_aes::AES_GCM_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{ +/// AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +/// }; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x99; 16], KeyType::SymmetricCipherKey).unwrap(); +/// let (mut enc, nonce) = AES_GCM_128::::do_encrypt_init(&key).unwrap(); +/// enc.do_update_aad(b"header").unwrap(); +/// let mut ct = [0u8; 5]; +/// enc.do_update_out(b"hello", &mut ct).unwrap(); +/// let (tag, tag_len) = enc.do_final().unwrap(); +/// +/// let mut dec = AES_GCM_128::::do_decrypt_init(&key, &nonce).unwrap(); +/// dec.do_update_aad(b"header").unwrap(); +/// let mut full_ct = ct.to_vec(); +/// full_ct.extend_from_slice(&tag[..tag_len]); +/// let mut pt = vec![0u8; full_ct.len()]; +/// let n = dec.do_update_out(&full_ct, &mut pt).unwrap(); +/// let (_last, last_len) = dec.do_final().unwrap(); +/// assert_eq!(&pt[..n + last_len], b"hello"); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_GCM_128 = Gcm; + +/// AES-192 in GCM with a 128-bit tag. See [`AES_GCM_128`]. +/// +/// ``` +/// use bouncycastle_aes::AES_GCM_192; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<24>::from_bytes_as_type(&[0x24; 24], KeyType::SymmetricCipherKey).unwrap(); +/// let message = b"a 192-bit key message!!"; +/// let (nonce, ciphertext, tag) = +/// AES_GCM_192::::encrypt_detached(&key, b"aad", message).unwrap(); +/// let plaintext = +/// AES_GCM_192::::decrypt_detached(&key, &nonce, b"aad", &ciphertext, &tag).unwrap(); +/// assert_eq!(&plaintext, message); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_GCM_192 = Gcm; + +/// AES-256 in GCM with a 128-bit tag. See [`AES_GCM_128`]. +/// +/// ``` +/// use bouncycastle_aes::AES_GCM_256; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<32>::from_bytes_as_type(&[0x32; 32], KeyType::SymmetricCipherKey).unwrap(); +/// let message = b"a 256-bit key message!!"; +/// let (nonce, ciphertext, tag) = +/// AES_GCM_256::::encrypt_detached(&key, b"aad", message).unwrap(); +/// let plaintext = +/// AES_GCM_256::::decrypt_detached(&key, &nonce, b"aad", &ciphertext, &tag).unwrap(); +/// assert_eq!(&plaintext, message); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_GCM_256 = Gcm; diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs index 9ae03115..5e1c11c7 100644 --- a/crypto/aes/src/lib.rs +++ b/crypto/aes/src/lib.rs @@ -14,10 +14,12 @@ //! For ready-to-use primitives, see the documentation for one of the provided modes of operation: //! //! * [AES_CBC](crate::cbc) +//! * [AES_CCM](crate::ccm) //! * [AES_CFB](crate::cfb) //! * [AES_CFB8](crate::cfb8) //! * [AES_CTR](crate::ctr) //! * [AES_ECB](crate::ecb) +//! * [AES_GCM](crate::gcm) //! //! # Design //! @@ -157,10 +159,12 @@ pub mod aes_internal; mod bitslice; pub mod cbc; +pub mod ccm; pub mod cfb; pub mod cfb8; pub mod ctr; pub mod ecb; +pub mod gcm; mod padded_mode; mod round; mod sbox; @@ -168,7 +172,13 @@ mod schedule; pub use aes_internal::BLOCK_LEN; pub use cbc::{AES_CBC_128, AES_CBC_192, AES_CBC_256}; +pub use ccm::{ + AES_CCM_128, AES_CCM_128_Decryptor, AES_CCM_128_Encryptor, AES_CCM_192, AES_CCM_192_Decryptor, + AES_CCM_192_Encryptor, AES_CCM_256, AES_CCM_256_Decryptor, AES_CCM_256_Encryptor, + CCM_NONCE_LEN, CCM_TAG_LEN, +}; pub use cfb::{AES_CFB_128, AES_CFB_192, AES_CFB_256}; pub use cfb8::{AES_CFB8_128, AES_CFB8_192, AES_CFB8_256}; pub use ctr::{AES_CTR_128, AES_CTR_192, AES_CTR_256, CTR_NONCE_LEN}; pub use ecb::{AES_ECB_128, AES_ECB_192, AES_ECB_256}; +pub use gcm::{AES_GCM_128, AES_GCM_192, AES_GCM_256}; diff --git a/crypto/aes/tests/bc-test-data.rs b/crypto/aes/tests/bc-test-data.rs index 92f1afae..49526413 100644 --- a/crypto/aes/tests/bc-test-data.rs +++ b/crypto/aes/tests/bc-test-data.rs @@ -11,7 +11,7 @@ //! block-permutation test vector -- which is the only reason ECB is mentioned in this crate. See //! the crate docs on why you must never use ECB to encrypt data. //! -//! `bc-test-data` ships thirteen ACVP AES vector sets, one per mode. This file deliberately +//! `bc-test-data` ships sixteen ACVP AES vector sets, one per mode. This file deliberately //! consumes only `ACVP-AES-ECB`, because that is the one that tests the permutation rather than a //! mode. The others belong with whatever implements the mode: //! @@ -20,10 +20,12 @@ //! | `ACVP-AES-ECB` | this file (the permutation) and `crypto/modes/tests/acvp_ecb_tests.rs` (the `Ecb` mode) | //! | `ACVP-AES-CBC` | `crypto/modes/tests/acvp_tests.rs` | //! | `ACVP-AES-CBC-CS1` / `-CS2` / `-CS3` | nothing yet (ciphertext stealing is unimplemented) | +//! | `ACVP-AES-CCM` | `crypto/modes/tests/acvp_ccm_tests.rs` | //! | `ACVP-AES-CFB128` | `crypto/modes/tests/acvp_cfb_tests.rs` | //! | `ACVP-AES-CFB8` | `crypto/modes/tests/acvp_cfb8_tests.rs` | //! | `ACVP-AES-OFB` | nothing yet (OFB is unimplemented) | //! | `ACVP-AES-CTR` | `crypto/modes/tests/acvp_ctr_tests.rs` | +//! | `ACVP-AES-GCM` / `-GMAC` | nothing yet (GCM is unimplemented; it needs GF(2^128) arithmetic) | //! | `ACVP-AES-KW` / `-KWP` | nothing yet (key wrap is unimplemented) | //! | `ACVP-AES-FF1` / `-FF3-1` | nothing yet (format-preserving encryption is unimplemented) | //! diff --git a/crypto/aes/tests/gcm_alias_tests.rs b/crypto/aes/tests/gcm_alias_tests.rs new file mode 100644 index 00000000..1e240e2b --- /dev/null +++ b/crypto/aes/tests/gcm_alias_tests.rs @@ -0,0 +1,92 @@ +//! Tests for the AES-GCM aliases. +//! +//! The aliases are only type aliases, so what is worth testing is that they name the *right* type +//! at both directions, that all three key lengths reach the shared `AEADCipherEncryptor` / +//! `AEADCipherDecryptor` conformance suite (`TestFrameworkAEADCipher`, which runs the +//! `SymmetricCipherEncryptor` / `SymmetricCipherDecryptor` suite first), and that a fresh nonce is +//! generated per encryption. Algorithm correctness itself is pinned by `bouncycastle-modes`' +//! ACVP and bc-java known-answer suites. + +use bouncycastle_aes::aes_internal::AES128Internal; +use bouncycastle_aes::{AES_GCM_128, AES_GCM_192, AES_GCM_256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkAEADCipher; +use bouncycastle_modes::{Decrypting, Encrypting, Gcm}; + +fn key() -> KeyMaterial { + let bytes: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(7).wrapping_add(1)); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).expect("a valid key") +} + +/// The alias must resolve to exactly the type it claims to, at both directions. +#[test] +fn the_alias_names_the_expected_type() { + use core::mem::size_of; + + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); +} + +/// All three key lengths satisfy the shared AEAD conformance suite, which includes the +/// symmetric-cipher suite the padding adapters and the stream modes run. +#[test] +fn all_three_key_lengths_conform_to_the_aead_suite() { + let framework = TestFrameworkAEADCipher::new(); + framework.test_encryptor_decryptor::< + 16, + 12, + 16, + 16, + AES_GCM_128, + AES_GCM_128, + >(); + framework.test_encryptor_decryptor::< + 24, + 12, + 16, + 16, + AES_GCM_192, + AES_GCM_192, + >(); + framework.test_encryptor_decryptor::< + 32, + 12, + 16, + 16, + AES_GCM_256, + AES_GCM_256, + >(); +} + +/// The nonce is generated per encryption, so the same plaintext gives different ciphertext, and +/// each still round-trips. +#[test] +fn each_encryption_gets_a_fresh_nonce() { + let data = *b"the quick brown fox jumps over the lazy dog!!!"; + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..16 { + let mut ct = [0u8; 46]; + let (nonce, _, tag) = + AES_GCM_128::::encrypt_out_detached(&key::<16>(), b"aad", &data, &mut ct) + .unwrap(); + assert!(seen.insert(nonce), "nonce repeated across encryptions"); + let mut pt = [0u8; 46]; + AES_GCM_128::::decrypt_out_detached( + &key::<16>(), + &nonce, + b"aad", + &ct, + &tag, + &mut pt, + ) + .unwrap(); + assert_eq!(pt, data); + } +} diff --git a/crypto/ascon/Cargo.toml b/crypto/ascon/Cargo.toml new file mode 100644 index 00000000..1ee94e04 --- /dev/null +++ b/crypto/ascon/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "bouncycastle-ascon" +version.workspace = true +edition.workspace = true + +[features] +# `std` gates the ergonomic, allocating (`Vec`-returning) one-shot cipher APIs, mirroring the +# `std` feature of `bouncycastle-core`. On by default; a future `--no-default-features` build is +# what will let the crate move toward `#![no_std]`. +default = ["std"] +std = ["bouncycastle-core/std"] + +[dependencies] +bouncycastle-core.workspace = true +# Only for the `Encrypting` / `Decrypting` markers `Ascon_AEAD128` takes; nothing else from it. +bouncycastle-modes.workspace = true +bouncycastle-rng.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +criterion.workspace = true + +[[bench]] +name = "ascon_benches" +harness = false diff --git a/crypto/ascon/benches/ascon_benches.rs b/crypto/ascon/benches/ascon_benches.rs new file mode 100644 index 00000000..2238302c --- /dev/null +++ b/crypto/ascon/benches/ascon_benches.rs @@ -0,0 +1,102 @@ +use bouncycastle_rng as rng; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_ascon::ascon_aead128::AsconAead128; +use bouncycastle_ascon::ascon_cxof128::AsconCXof128; +use bouncycastle_ascon::ascon_hash256::AsconHash256; +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{Hash, RNG, XOF}; + +const DATA_LEN: usize = 16 * 1024; + +fn random_data(len: usize) -> Vec { + let mut data = vec![0u8; len]; + rng::DefaultRNG::default().next_bytes_out(&mut data).unwrap(); + data +} + +fn bench_aead128_encrypt(c: &mut Criterion) { + let key = + KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); + let nonce = [0x24u8; 16]; + let data = random_data(DATA_LEN); + let mut out = vec![0u8; DATA_LEN + 16]; + + let mut group = c.benchmark_group("ascon::AsconAead128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function(format!("{DATA_LEN} bytes -- ::encrypt()"), |b| { + b.iter(|| { + AsconAead128::encrypt(&key, &nonce, None, black_box(&data), &mut out).unwrap(); + black_box(&out); + }) + }); + + group.finish(); +} + +fn bench_hash256(c: &mut Criterion) { + let data = random_data(DATA_LEN); + let mut digest = [0u8; 32]; + + let mut group = c.benchmark_group("ascon::AsconHash256"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function(format!("{DATA_LEN} bytes -- ::hash_out()"), |b| { + b.iter(|| { + AsconHash256::new().hash_out(black_box(&data), &mut digest); + black_box(&digest); + }) + }); + + group.finish(); +} + +fn bench_xof128(c: &mut Criterion) { + let data = random_data(DATA_LEN); + let mut out = [0u8; 64]; + + let mut group = c.benchmark_group("ascon::AsconXof128"); + group.throughput(Throughput::Bytes((DATA_LEN + out.len()) as u64)); + + group.bench_function( + format!("input: {DATA_LEN} bytes, output: 64 bytes -- ::xof_out()"), + |b| { + b.iter(|| { + AsconXof128::new().xof_out(black_box(&data), &mut out); + black_box(&out); + }) + }, + ); + + group.finish(); +} + +fn bench_cxof128(c: &mut Criterion) { + let data = random_data(DATA_LEN); + let customization = b"bench-customization"; + let mut out = [0u8; 64]; + + let mut group = c.benchmark_group("ascon::AsconCXof128"); + group.throughput(Throughput::Bytes((DATA_LEN + out.len()) as u64)); + + group.bench_function( + format!("input: {DATA_LEN} bytes, output: 64 bytes -- ::xof_out()"), + |b| { + b.iter(|| { + AsconCXof128::with_customization(customization) + .unwrap() + .xof_out(black_box(&data), &mut out); + + black_box(&out); + }) + }, + ); + + group.finish(); +} + +criterion_group!(benches, bench_aead128_encrypt, bench_hash256, bench_xof128, bench_cxof128); +criterion_main!(benches); diff --git a/crypto/ascon/src/ascon_aead128.rs b/crypto/ascon/src/ascon_aead128.rs new file mode 100644 index 00000000..2343b393 --- /dev/null +++ b/crypto/ascon/src/ascon_aead128.rs @@ -0,0 +1,852 @@ +//! Ascon-AEAD128 authenticated encryption, as specified in NIST SP 800-232 §4. +//! +//! Rate = 128 bits, capacity = 192 bits, 128-bit key/nonce/tag. Initialization and finalization use +//! `Ascon-p[12]`; associated-data and plaintext/ciphertext blocks use `Ascon-p[8]`. +//! +//! Every byte of plaintext/ciphertext is transformed and emitted as soon as it is seen (no +//! held-back buffering across `do_encrypt_update`/`do_decrypt_update` calls); this is what lets the +//! finalizers be plain `self -> tag` / `self -> Result<(), _>` calls with nothing left to flush. +//! Ascon-AEAD128 permits this because within a 128-bit rate block each plaintext/ciphertext byte +//! is transformed independently of the others in that block; the permutation only runs once a +//! full 16-byte block has been absorbed, or at finalization. +//! +//! [`AsconAead128Encryptor`] / [`AsconAead128Decryptor`] adapt this type's direction-agnostic +//! streaming API (a single [`AsconAead128`] value serves either direction, fixed at construction +//! by [`AsconAead128::new_encrypting`] / [`AsconAead128::new_decrypting`]) to +//! [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], whose +//! direction is fixed by the type: each newtype wraps an [`AsconAead128`] already constructed for +//! its own direction and only ever calls that direction's inherent methods, so the wrong-direction +//! panics inside [`AsconAead128::do_encrypt_update`] and friends are unreachable through them. See +//! their docs for why a thin newtype pair rather than encoding the direction into `AsconAead128` +//! itself: the inherent API is deliberately one type serving both directions, which is what the +//! in-place streaming and the explicit-nonce one-shots are built on. + +use core::fmt::{self, Debug, Display, Formatter}; + +use bouncycastle_core::errors::{KeyMaterialError, SuspendableError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, SuspendableKeyed, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_modes::{Decrypting, Encrypting}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; + +use crate::permutation::{AsconState, load_u64_le, p8, p12, store_u64_le}; + +/// Length in bytes of the Ascon-AEAD128 key. +pub const KEY_LEN: usize = 16; +/// Length in bytes of the Ascon-AEAD128 nonce. +pub const NONCE_LEN: usize = 16; +/// Length in bytes of the Ascon-AEAD128 authentication tag. +pub const TAG_LEN: usize = 16; +const RATE: usize = 16; + +/// Ascon-AEAD128 initial value (SP 800-232 Table 14). +const ASCON_IV: u64 = 0x00001000808C0001; + +/// State machine for enforcing the call order and remembering the direction (encrypt/decrypt). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum StateMachine { + EncInit, + EncAad, + EncData, + DecInit, + DecAad, + DecData, +} + +impl StateMachine { + // Stable u8 encoding used when suspending/resuming the AEAD state machine. + fn to_u8(self) -> u8 { + match self { + StateMachine::EncInit => 0, + StateMachine::EncAad => 1, + StateMachine::EncData => 2, + StateMachine::DecInit => 4, + StateMachine::DecAad => 5, + StateMachine::DecData => 6, + } + } + + fn from_u8(v: u8) -> Option { + Some(match v { + 0 => StateMachine::EncInit, + 1 => StateMachine::EncAad, + 2 => StateMachine::EncData, + 4 => StateMachine::DecInit, + 5 => StateMachine::DecAad, + 6 => StateMachine::DecData, + _ => return None, + }) + } + + fn is_encrypt(self) -> bool { + matches!(self, StateMachine::EncInit | StateMachine::EncAad | StateMachine::EncData) + } + + fn is_init(self) -> bool { + matches!(self, StateMachine::EncInit | StateMachine::DecInit) + } +} + +/// An implementation of the Ascon-AEAD128 algorithm (NIST SP 800-232). +/// +/// A single instance performs one operation (encryption or decryption) under one (key, nonce) pair. +/// See [`AsconAead128::new_encrypting`] for the streaming workflow and +/// [`AsconAead128::encrypt`] / +/// [`AsconAead128::decrypt`] for the one-shot APIs. +#[derive(Clone)] +pub struct AsconAead128 { + // 128-bit secret key (two 64-bit words). It is re-added to the state at finalization, so it must + // be retained; wrapped in `Secret` for volatile-write zeroization on drop. + key: Secret<[u64; 2]>, + // 320-bit internal state (five 64-bit words). Carries keystream/plaintext-derived material, so + // it is likewise wrapped in `Secret`. + state: Secret, + // Byte position (0..RATE) within the current rate block. + pos: usize, + // State machine for enforcing the call order and remembering the direction. + state_machine: StateMachine, +} + +impl AsconAead128 { + /// Validate a [`KeyMaterial`] for use with Ascon-AEAD128 and return its key words. + /// The key must be tagged as a [`KeyType::SymmetricCipherKey`] and carry at least the + /// algorithm's 128-bit security strength (SP 800-232 R1/R2). + fn checked_key(key: &KeyMaterial) -> Result<[u64; 2], SymmetricCipherError> { + if key.key_type() != KeyType::SymmetricCipherKey { + return Err(KeyMaterialError::InvalidKeyType( + "Ascon-AEAD128 requires a SymmetricCipherKey", + ) + .into()); + } + if key.security_strength() < SecurityStrength::_128bit { + return Err(KeyMaterialError::SecurityStrength( + "Ascon-AEAD128 requires a key with at least 128-bit security strength", + ) + .into()); + } + let bytes = key.ref_to_bytes(); + if bytes.len() != KEY_LEN { + return Err(KeyMaterialError::InvalidLength.into()); + } + Ok([load_u64_le(bytes, 0), load_u64_le(bytes, 8)]) + } + + /// Draw a fresh, unique 128-bit nonce from the library's default OS-seeded DRBG. + /// + /// The one-shot APIs of main's cipher framework generate the init data / nonce internally, so + /// Ascon's per-encryption nonce-uniqueness requirement (SP 800-232 R3) is satisfied by sourcing + /// each nonce from a CSPRNG. Callers who need deterministic, caller-supplied nonces should use + /// the inherent streaming API ([`AsconAead128::new_encrypting`]). + fn fresh_nonce() -> Result<[u8; NONCE_LEN], SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + let mut nonce = [0u8; NONCE_LEN]; + rng.next_bytes_out(&mut nonce)?; + Ok(nonce) + } + + /// Creates a streaming instance for **encryption** under a caller-supplied nonce. + /// * `key` is validated as a [`KeyType::SymmetricCipherKey`] with at least 128-bit strength. + /// * `nonce` is the 128-bit nonce. It **must** be unique per encryption under a given key; + /// [`AsconAead128Encryptor`] generates one instead, which is the safer default. + /// * `ad` is optional associated data (authenticated, not encrypted); processed immediately. + /// + /// Only the `do_encrypt_*` methods may be called on the result; the decrypting ones panic. + pub fn new_encrypting( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ad: Option<&[u8]>, + ) -> Result { + Self::new(key, nonce, ad, true) + } + + /// Creates a streaming instance for **decryption** under the nonce the ciphertext was produced + /// with; see [`new_encrypting`](Self::new_encrypting) for the arguments. + /// + /// Only the `do_decrypt_*` methods may be called on the result; the encrypting ones panic. + pub fn new_decrypting( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ad: Option<&[u8]>, + ) -> Result { + Self::new(key, nonce, ad, false) + } + + /// The body of [`new_encrypting`](Self::new_encrypting) / [`new_decrypting`](Self::new_decrypting). + /// Private because a `bool` for the direction is not something the public API should ask a + /// caller to get right: every public entry point fixes it, either by name here or by type on + /// [`AsconAead128Encryptor`] / [`AsconAead128Decryptor`]. + fn new( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ad: Option<&[u8]>, + for_encryption: bool, + ) -> Result { + let key_words = Self::checked_key(key)?; + let mut key_secret: Secret<[u64; 2]> = Secret::new(); + *key_secret = key_words; + + let mut state: Secret = Secret::new(); + // Initialization (SP 800-232 §4.1.1 step 1 / Eq. 15-17): S = IV||K||N, then Ascon-p[12], + // then XOR K into the last 128 bits. + state[0] = ASCON_IV; + state[1] = key_words[0]; + state[2] = key_words[1]; + state[3] = load_u64_le(nonce, 0); + state[4] = load_u64_le(nonce, 8); + p12(&mut state); + state[3] ^= key_words[0]; + state[4] ^= key_words[1]; + + let mut aead = AsconAead128 { + key: key_secret, + state, + pos: 0, + state_machine: if for_encryption { + StateMachine::EncInit + } else { + StateMachine::DecInit + }, + }; + if let Some(ad_bytes) = ad { + // infallible: a freshly constructed instance has processed no data yet, so + // `check_aad` cannot return `StateError`. + aead.do_update_aad(ad_bytes).unwrap(); + } + Ok(aead) + } + + /// One-shot authenticated encryption with a caller-supplied nonce (SP 800-232 Algorithm 3). + /// Writes ciphertext followed by the 128-bit tag into `out`, which must be at least + /// `plaintext.len() + 16` bytes. Returns the number of bytes written. + pub fn encrypt( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ad: Option<&[u8]>, + plaintext: &[u8], + out: &mut [u8], + ) -> Result { + let needed = plaintext.len() + TAG_LEN; + if out.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let mut cipher = Self::new(key, nonce, ad, true)?; + out[..plaintext.len()].copy_from_slice(plaintext); + cipher.do_encrypt_update(&mut out[..plaintext.len()]); + let tag = cipher.do_encrypt_final(); + out[plaintext.len()..needed].copy_from_slice(&tag); + Ok(needed) + } + + /// One-shot authenticated decryption with a caller-supplied nonce (SP 800-232 Algorithm 4). + /// `ciphertext` is the ciphertext followed by the 128-bit tag. Writes the recovered plaintext + /// into `out`, which must be at least `ciphertext.len() - 16` bytes. Returns the number of + /// bytes written, or [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify -- + /// in which case `out` is zeroized before returning. + pub fn decrypt( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ad: Option<&[u8]>, + ciphertext: &[u8], + out: &mut [u8], + ) -> Result { + if ciphertext.len() < TAG_LEN { + return Err(SymmetricCipherError::GenericError( + "Ascon-AEAD128 ciphertext shorter than tag", + )); + } + let pt_len = ciphertext.len() - TAG_LEN; + if out.len() < pt_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(pt_len)); + } + let mut cipher = Self::new(key, nonce, ad, false)?; + out[..pt_len].copy_from_slice(&ciphertext[..pt_len]); + cipher.do_decrypt_update(&mut out[..pt_len]); + // infallible: ciphertext.len() - pt_len == TAG_LEN by construction above. + let tag: &[u8; TAG_LEN] = ciphertext[pt_len..].try_into().unwrap(); + match cipher.do_decrypt_final(tag) { + Ok(()) => Ok(pt_len), + Err(e) => { + out[..pt_len].fill(0); + Err(e) + } + } + } + + /// Read the value of state byte `pos` (0 = LSB of word 0, ..., 15 = MSB of word 1). + fn state_byte(&self, pos: usize) -> u8 { + let word = if pos < 8 { self.state[0] } else { self.state[1] }; + (word >> ((pos % 8) * 8)) as u8 + } + + /// XOR `b` into state byte `pos`. + fn xor_state_byte(&mut self, pos: usize, b: u8) { + let shifted = (b as u64) << ((pos % 8) * 8); + if pos < 8 { self.state[0] ^= shifted } else { self.state[1] ^= shifted } + } + + /// Overwrite state byte `pos` with `b`. + fn set_state_byte(&mut self, pos: usize, b: u8) { + let shift = (pos % 8) * 8; + let mask = !(0xFFu64 << shift); + let shifted = (b as u64) << shift; + if pos < 8 { + self.state[0] = (self.state[0] & mask) | shifted; + } else { + self.state[1] = (self.state[1] & mask) | shifted; + } + } + + /// Advance to the next byte position, running `Ascon-p[8]` and wrapping back to 0 once a full + /// rate block (16 bytes) has been absorbed. + fn advance(&mut self) { + self.pos += 1; + if self.pos == RATE { + p8(&mut self.state); + self.pos = 0; + } + } + + fn absorb_aad_byte(&mut self, b: u8) { + self.xor_state_byte(self.pos, b); + self.advance(); + } + + fn encrypt_byte(&mut self, p: u8) -> u8 { + self.xor_state_byte(self.pos, p); + let c = self.state_byte(self.pos); + self.advance(); + c + } + + fn decrypt_byte(&mut self, c: u8) -> u8 { + let prev = self.state_byte(self.pos); + self.set_state_byte(self.pos, c); + self.advance(); + prev ^ c + } + + fn check_aad(&mut self) -> Result<(), SymmetricCipherError> { + match self.state_machine { + StateMachine::EncInit => self.state_machine = StateMachine::EncAad, + StateMachine::DecInit => self.state_machine = StateMachine::DecAad, + StateMachine::EncAad | StateMachine::DecAad => {} + StateMachine::EncData | StateMachine::DecData => { + return Err(SymmetricCipherError::StateError( + "Ascon-AEAD128: associated data must be processed before plaintext/ciphertext", + )); + } + } + Ok(()) + } + + // Ends the associated-data phase (SP 800-232 §4.1.1/§4.1.2 step 2): pads and absorbs the + // final (possibly empty) AAD block only if any AAD was actually supplied, then applies the + // domain-separation bit unconditionally. + fn finish_aad(&mut self) { + if matches!(self.state_machine, StateMachine::EncAad | StateMachine::DecAad) { + self.xor_state_byte(self.pos, 0x01); + p8(&mut self.state); + self.pos = 0; + } + // Domain separation (Eq. 22/40: S ^= (0^319 || 1)). + self.state[4] ^= 0x8000000000000000; + self.state_machine = match self.state_machine { + StateMachine::EncInit | StateMachine::EncAad => StateMachine::EncData, + StateMachine::DecInit | StateMachine::DecAad => StateMachine::DecData, + StateMachine::EncData | StateMachine::DecData => unreachable!(), + }; + } + + fn check_data(&mut self) { + if !matches!(self.state_machine, StateMachine::EncData | StateMachine::DecData) { + self.finish_aad(); + } + } + + // Finalization (SP 800-232 §4.1.1 step 4 / §4.1.2 step 4, Eq. 30-32 / 49-51): re-add the key, + // permute with Ascon-p[12], and add the key again; the tag is the resulting last 128 bits. + fn finish_data(&mut self) -> [u8; TAG_LEN] { + self.state[2] ^= self.key[0]; + self.state[3] ^= self.key[1]; + p12(&mut self.state); + self.state[3] ^= self.key[0]; + self.state[4] ^= self.key[1]; + + let mut tag = [0u8; TAG_LEN]; + store_u64_le(&mut tag, 0, self.state[3]); + store_u64_le(&mut tag, 8, self.state[4]); + tag + } + + /// Process associated data (AAD) bytes. May be called multiple times, but only before any + /// plaintext/ciphertext is processed; an empty `input` is always a no-op, even after data. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if `input` is non-empty and plaintext/ciphertext has + /// already been processed. + pub fn do_update_aad(&mut self, input: &[u8]) -> Result<(), SymmetricCipherError> { + if input.is_empty() { + return Ok(()); + } + self.check_aad()?; + + let mut input = input; + while !input.is_empty() { + if self.pos == 0 && input.len() >= RATE { + self.state[0] ^= load_u64_le(input, 0); + self.state[1] ^= load_u64_le(input, 8); + p8(&mut self.state); + input = &input[RATE..]; + } else { + self.absorb_aad_byte(input[0]); + input = &input[1..]; + } + } + Ok(()) + } + + /// Encrypt `data` in place (SP 800-232 §4.1.1 step 3). Every byte is transformed and emitted + /// immediately; nothing is buffered across calls. + pub fn do_encrypt_update(&mut self, data: &mut [u8]) { + if !self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_encrypt_update called on a decryptor"); + } + self.check_data(); + + let mut data = data; + while !data.is_empty() { + if self.pos == 0 && data.len() >= RATE { + let c0 = self.state[0] ^ load_u64_le(data, 0); + let c1 = self.state[1] ^ load_u64_le(data, 8); + store_u64_le(data, 0, c0); + store_u64_le(data, 8, c1); + self.state[0] = c0; + self.state[1] = c1; + p8(&mut self.state); + data = &mut data[RATE..]; + } else { + data[0] = self.encrypt_byte(data[0]); + data = &mut data[1..]; + } + } + } + + /// Finish encryption; returns the 128-bit tag (SP 800-232 §4.1.1 steps 3-4). Pads the final + /// (possibly empty) plaintext block; no further bytes are emitted here since every + /// plaintext/ciphertext byte was already written by `do_encrypt_update`. + pub fn do_encrypt_final(mut self) -> [u8; TAG_LEN] { + if !self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_encrypt_final called on a decryptor"); + } + self.check_data(); + // Padding of the final (possibly empty) plaintext block (Eq. 27). + self.xor_state_byte(self.pos, 0x01); + self.finish_data() + } + + /// Decrypt `data` in place (SP 800-232 §4.1.2 step 3). Every byte is transformed and emitted + /// immediately; the plaintext is **not** authenticated until [`AsconAead128::do_decrypt_final`] + /// returns `Ok`. + pub fn do_decrypt_update(&mut self, data: &mut [u8]) { + if self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_decrypt_update called on an encryptor"); + } + self.check_data(); + + let mut data = data; + while !data.is_empty() { + if self.pos == 0 && data.len() >= RATE { + let t0 = load_u64_le(data, 0); + let t1 = load_u64_le(data, 8); + store_u64_le(data, 0, self.state[0] ^ t0); + store_u64_le(data, 8, self.state[1] ^ t1); + self.state[0] = t0; + self.state[1] = t1; + p8(&mut self.state); + data = &mut data[RATE..]; + } else { + data[0] = self.decrypt_byte(data[0]); + data = &mut data[1..]; + } + } + } + + /// Finish decryption, checking `tag` in constant time (SP 800-232 §4.1.2 steps 3-4). + pub fn do_decrypt_final(mut self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + if self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_decrypt_final called on an encryptor"); + } + self.check_data(); + // Padding of the final (possibly empty) ciphertext block (Eq. 47). + self.xor_state_byte(self.pos, 0x01); + let computed = self.finish_data(); + + if !ct_eq_bytes(&computed, tag) { + return Err(SymmetricCipherError::AEADTagCheckFailed); + } + Ok(()) + } +} + +impl Algorithm for AsconAead128 { + const ALG_NAME: &'static str = "Ascon-AEAD128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +/// Adapts [`AsconAead128`]'s encrypting direction to [`AEADCipherEncryptor`] and, through it, +/// [`SymmetricCipherEncryptor`]; see the module docs for why this is a thin wrapper rather than a +/// change to `AsconAead128` itself. +/// +/// `FINAL_LEN` is `TAG_LEN`: Ascon-AEAD128 holds nothing back, so the inline +/// [`SymmetricCipherEncryptor::do_final`] writes only the tag, and the detached +/// [`AEADCipherEncryptor::do_final_out_detached`] writes nothing. +pub struct AsconAead128Encryptor(AsconAead128); + +impl Algorithm for AsconAead128Encryptor { + const ALG_NAME: &'static str = AsconAead128::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = AsconAead128::MAX_SECURITY_STRENGTH; +} + +impl SymmetricCipherEncryptor for AsconAead128Encryptor { + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + let nonce = AsconAead128::fresh_nonce()?; + Ok((Self(AsconAead128::new(key, &nonce, None, true)?), nonce)) + } + + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + let mut nonce = [0u8; NONCE_LEN]; + rng.next_bytes_out(&mut nonce)?; + Ok((Self(AsconAead128::new(key, &nonce, None, true)?), nonce)) + } + + /// Ascon-AEAD128 never buffers: every byte given is a byte returned. + fn update_out_len(&self, input_len: usize) -> usize { + input_len + } + + fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); + } + let out = &mut ciphertext[..plaintext.len()]; + out.copy_from_slice(plaintext); + self.0.do_encrypt_update(out); + Ok(plaintext.len()) + } + + /// The inline layout: nothing is held back, so the final buffer is exactly the tag. + fn do_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + Ok((self.0.do_encrypt_final(), TAG_LEN)) + } + + /// The ciphertext, which is as long as the plaintext, followed by the tag. + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + TAG_LEN + } +} + +impl AEADCipherEncryptor for AsconAead128Encryptor { + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.0.do_update_aad(aad) + } + + /// Nothing is ever held back to flush, so `ciphertext` is left untouched. + fn do_final_out_detached( + self, + _ciphertext: &mut [u8; TAG_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + Ok((0, self.0.do_encrypt_final())) + } +} + +/// Adapts [`AsconAead128`]'s decrypting direction to [`AEADCipherDecryptor`] and, through it, +/// [`SymmetricCipherDecryptor`]; see the module docs for why this is a thin wrapper rather than a +/// change to `AsconAead128` itself. +/// +/// Unlike the inherent API this does hold data back: the last `TAG_LEN` bytes of ciphertext it has +/// seen, since until the stream ends it cannot know whether they are the inline tag +/// ([`SymmetricCipherDecryptor::do_final`]) or ciphertext with the tag carried separately +/// ([`AEADCipherDecryptor::do_final_out_detached`]). They are ciphertext, not plaintext, so they need +/// no [`Secret`] wrapper. +pub struct AsconAead128Decryptor { + cipher: AsconAead128, + // The most recent `held_len` bytes of ciphertext, not yet given to `cipher`. + held: [u8; TAG_LEN], + // Always `min(TAG_LEN, total ciphertext seen)`. + held_len: usize, +} + +impl Algorithm for AsconAead128Decryptor { + const ALG_NAME: &'static str = AsconAead128::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = AsconAead128::MAX_SECURITY_STRENGTH; +} + +impl SymmetricCipherDecryptor for AsconAead128Decryptor { + fn do_decrypt_init( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ) -> Result { + Ok(Self { + cipher: AsconAead128::new(key, nonce, None, false)?, + held: [0u8; TAG_LEN], + held_len: 0, + }) + } + + /// Everything but the last `TAG_LEN` bytes seen so far is released. + fn update_out_len(&self, input_len: usize) -> usize { + (self.held_len + input_len).saturating_sub(TAG_LEN) + } + + fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let release = self.update_out_len(ciphertext.len()); + if plaintext.len() < release { + return Err(SymmetricCipherError::OutputBufferTooSmall(release)); + } + // The oldest bytes go first: the held-back ones, then the front of `ciphertext`. + let from_held = release.min(self.held_len); + let from_input = release - from_held; + let out = &mut plaintext[..release]; + out[..from_held].copy_from_slice(&self.held[..from_held]); + out[from_held..].copy_from_slice(&ciphertext[..from_input]); + // Called even when `release` is 0: that is what ends the AAD phase in `cipher`, so a + // later non-empty `do_update_aad` is refused however little ciphertext has been seen. + self.cipher.do_decrypt_update(out); + // Keep the newest `TAG_LEN` (or fewer) bytes: what is left of `held`, then the tail of + // `ciphertext`. + let kept = self.held_len - from_held; + self.held.copy_within(from_held..self.held_len, 0); + let new_len = kept + ciphertext.len() - from_input; + self.held[kept..new_len].copy_from_slice(&ciphertext[from_input..]); + self.held_len = new_len; + Ok(release) + } + + /// The inline layout: the held-back bytes are the tag, so there is no plaintext left to + /// release. + /// + /// # Errors + /// [`SymmetricCipherError::DecryptionFailed`] if fewer than `TAG_LEN` bytes were seen in all; + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. + fn do_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + if self.held_len < TAG_LEN { + return Err(SymmetricCipherError::DecryptionFailed); + } + self.cipher.do_decrypt_final(&self.held)?; + Ok(([0u8; TAG_LEN], 0)) + } + + /// Everything but the trailing tag. + fn decrypt_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(TAG_LEN) + } +} + +impl AEADCipherDecryptor for AsconAead128Decryptor { + /// # Errors + /// [`SymmetricCipherError::StateError`] if `aad` is non-empty and + /// [`SymmetricCipherDecryptor::do_update_out`] has already been called. + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.cipher.do_update_aad(aad) + } + + /// The held-back bytes are ciphertext: decrypts them into `plaintext`, then checks `tag`. On a + /// failed check `plaintext` is zeroized, so the error leaves nothing unauthenticated behind in + /// it (what earlier `do_update_out` calls released is the caller's to scrub). + fn do_final_out_detached( + mut self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; TAG_LEN], + ) -> Result { + let n = self.held_len; + plaintext[..n].copy_from_slice(&self.held[..n]); + self.cipher.do_decrypt_update(&mut plaintext[..n]); + if let Err(e) = self.cipher.do_decrypt_final(tag) { + plaintext.fill(0); + return Err(e); + } + Ok(n) + } +} + +/// Projects a direction marker onto the Ascon-AEAD128 type for that direction, which is what lets +/// [`Ascon_AEAD128`] take its direction as a parameter: a plain type alias cannot choose between two +/// distinct types, so it is written as a projection through this trait instead. +/// +/// Implemented for [`Encrypting`] and [`Decrypting`] and for nothing else, so those are the only +/// usable values of `Dir`. +pub trait AsconAead128Mode { + /// [`AsconAead128Encryptor`] or [`AsconAead128Decryptor`]. + type Mode; +} + +impl AsconAead128Mode for Encrypting { + type Mode = AsconAead128Encryptor; +} + +impl AsconAead128Mode for Decrypting { + type Mode = AsconAead128Decryptor; +} + +/// Ascon-AEAD128 (NIST SP 800-232), spelled as the specification spells it, in one direction: +/// `Ascon_AEAD128` is [`AsconAead128Encryptor`] and `Ascon_AEAD128` is +/// [`AsconAead128Decryptor`]. The wrong direction is a compile error, not a runtime check, and the +/// nonce is generated by encryption and returned, never supplied. +/// +/// Both directions implement [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] and, through them, +/// [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] -- which is the AEAD with no +/// associated data and the tag inline: +/// +/// ``` +/// use bouncycastle_ascon::Ascon_AEAD128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// type Enc = Ascon_AEAD128; +/// type Dec = Ascon_AEAD128; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// +/// let message = b"hello"; +/// let mut ciphertext = [0u8; 5 + 16]; // Enc::encrypt_out_len(5): ciphertext || tag +/// let (nonce, written) = Enc::encrypt_out(&key, message, &mut ciphertext).expect("encryption"); +/// assert_eq!(written, 21); +/// +/// let mut plaintext = [0u8; 5]; // Dec::decrypt_out_max_len(21) +/// let n = Dec::decrypt_out(&key, &nonce, &ciphertext, &mut plaintext).expect("decryption"); +/// assert_eq!(&plaintext[..n], message); +/// ``` +#[allow(non_camel_case_types)] +pub type Ascon_AEAD128 = ::Mode; + +impl Debug for AsconAead128 { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "AsconAead128 (key/state masked)") + } +} + +impl Display for AsconAead128 { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "AsconAead128 (key/state masked)") + } +} + +/// Length in bytes of the serialized state of [`AsconAead128`]. +/// Layout: 3-byte library version || 1-byte state tag || 40-byte permutation state (5 × u64 LE) +/// || 1-byte byte position within the current rate block || 1-byte call-state/direction. +/// The secret key is **not** serialized; it is re-supplied to [`SuspendableKeyed::from_suspended`]. +pub const SUSPENDED_ASCON_AEAD128_STATE_LEN: usize = 46; + +const AEAD128_STATE_TAG: u8 = 0x04; + +impl SuspendableKeyed for AsconAead128 { + // The 128-bit key must be re-supplied when resuming; it is never part of the serialized state, + // and is re-validated exactly as `new()` validates it. + type Key = KeyMaterial; + + fn suspend(self) -> [u8; SUSPENDED_ASCON_AEAD128_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_AEAD128_STATE_LEN]; + // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_AEAD128_STATE_LEN - 3 = 43 bytes. + let out: &mut [u8; SUSPENDED_ASCON_AEAD128_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = AEAD128_STATE_TAG; + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&self.state[i].to_le_bytes()); + } + debug_assert!(self.pos < RATE); + out[41] = self.pos as u8; + out[42] = self.state_machine.to_u8(); + + out_to_return + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_AEAD128_STATE_LEN], + key: &Self::Key, + ) -> Result { + // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_AEAD128_STATE_LEN - 3 = 43 bytes. + let input: &[u8; SUSPENDED_ASCON_AEAD128_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != AEAD128_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + let mut s = Secret::::new(); + for i in 0..5 { + // infallible: each slice is exactly 8 bytes (1+i*8..1+i*8+8) by construction. + s[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + let pos = input[41] as usize; + if pos >= RATE { + return Err(SuspendableError::InvalidData); + } + let state_machine = + StateMachine::from_u8(input[42]).ok_or(SuspendableError::InvalidData)?; + // A nonzero byte position implies at least one AAD/data byte has already been absorbed + // into the current rate block, which is only possible once the *Aad or *Data phase has + // begun -- never while still in *Init. + if pos != 0 && state_machine.is_init() { + return Err(SuspendableError::InvalidData); + } + + let key_words = Self::checked_key(key).map_err(|_| SuspendableError::InvalidData)?; + let mut key_secret = Secret::<[u64; 2]>::new(); + *key_secret = key_words; + + Ok(AsconAead128 { key: key_secret, state: s, pos, state_machine }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // StateMachine is private, so its to_u8/from_u8 round trip -- exercised end-to-end via + // suspend/resume in tests/aead128_tests.rs for the states reachable there -- is pinned + // directly here for every discriminant, including ones a successful resume never needs to + // decode into (EncInit/EncAad/DecInit/DecAad never survive to be the *end* state of a + // still-running cipher in the integration tests, since further processing always advances + // them to *Data). + #[test] + fn state_machine_u8_round_trip() { + let all = [ + StateMachine::EncInit, + StateMachine::EncAad, + StateMachine::EncData, + StateMachine::DecInit, + StateMachine::DecAad, + StateMachine::DecData, + ]; + for s in all { + assert_eq!(StateMachine::from_u8(s.to_u8()), Some(s), "round trip failed for {s:?}"); + } + // Unassigned discriminants (3 and 7 are deliberately skipped by to_u8's encoding) must + // be rejected, not silently mapped to a variant. + for v in [3u8, 7, 200] { + assert_eq!(StateMachine::from_u8(v), None, "discriminant {v} must be rejected"); + } + } +} diff --git a/crypto/ascon/src/ascon_cxof128.rs b/crypto/ascon/src/ascon_cxof128.rs new file mode 100644 index 00000000..6aa719d0 --- /dev/null +++ b/crypto/ascon/src/ascon_cxof128.rs @@ -0,0 +1,382 @@ +//! Ascon-CXOF128 customized extendable-output function (NIST SP 800-232 §5.3). +//! +//! A variant of Ascon-XOF128 that first absorbs a user-supplied customization string `Z` +//! (length-prefixed per SP 800-232 Alg. 7) to provide domain separation. Same sponge parameters as +//! Ascon-XOF128 (rate = 64 bits, capacity = 256 bits, `Ascon-p[12]`). +//! +//! Input absorption and output squeezing are represented by separate Rust types: +//! [`AsconCXof128`] accepts input, while [`AsconCXof128Squeezer`] produces the +//! extendable output stream. + +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{Algorithm, Hash, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_utils::secret::Secret; + +use crate::sponge::{RATE, Sponge}; + +/// Maximum customization-string length in bytes (2048 bits, per SP 800-232 §5.3). +const MAX_CUSTOMIZATION_BYTES: usize = 256; + +/// Nominal hash-view output length for Ascon-CXOF128. +/// +/// XOFs do not have an inherent output length. The [`Hash`] view therefore uses +/// twice the 128-bit security strength, matching the convention used for SHAKE128. +const NOMINAL_OUTPUT_LEN: usize = 32; + +/// Ascon-CXOF128 customized extendable-output function (NIST SP 800-232 §5.3). +#[derive(Clone)] +pub struct AsconCXof128 { + sponge: Sponge, +} + +impl AsconCXof128 { + /// Create a new Ascon-CXOF128 instance with no customization string. + pub fn new() -> Self { + // Precomputed state after initializing and then absorbing an empty customization string + // (SP 800-232 Algorithm 7 with |Z| = 0): starting from the Table 12 CXOF128 initialization + // state, XOR the length word Z_0 = int64(0) into S[0..63], Ascon-p[12], then XOR the + // pad-only last customization block (Eq. 77: pad(empty, 64) = 0x01 || 0^63) into S[0..63] + // and Ascon-p[12] again. Recomputed from those raw Table 12 words and pinned by + // `permutation::tests::cxof128_empty_customization_state_matches_algorithm_7`. + let mut sponge = Sponge::from_state([ + 0x500CCCC894E3C9E8, 0x5BED06F28F71248D, 0x3B03A0F930AFD512, 0x112EF093AA5C698B, + 0x00C8356340A347F0, + ]); + sponge.reset_buffer(); + + Self { sponge } + } + + /// Create a new Ascon-CXOF128 instance with the given customization string `z`. + /// + /// Returns [`HashError::InvalidInput`] if `z` is longer than 256 bytes (2048 bits, the bound + /// required by SP 800-232 §5.3). + pub fn with_customization(z: &[u8]) -> Result { + if z.len() > MAX_CUSTOMIZATION_BYTES { + return Err(HashError::InvalidInput( + "Ascon-CXOF128 customization string exceeds 256 bytes", + )); + } + + if z.is_empty() { + return Ok(Self::new()); + } + + // Precomputed state after the initialization permutation (SP 800-232 Table 12). + let mut sponge = Sponge::from_state([ + 0x675527C2A0E8DE03, 0x43D12D7DC0377BBC, 0xE9901DEC426E81B5, 0x2AB14907720780B6, + 0x8F3F1D02D432BC46, + ]); + + // Z0 = int64(|Z|) in bits, then absorb the parsed/padded customization blocks + // (SP 800-232 §5.3 Eq. 75-78 / Algorithm 7, "Customization" loop). + let bit_length = (z.len() as u64) << 3; + sponge.xor_word0(bit_length); + sponge.permute(); + sponge.absorb(z); + sponge.pad_and_absorb(); + sponge.permute(); + + // Customization is complete; reset the buffer to begin the message-absorb phase. + sponge.reset_buffer(); + + Ok(Self { sponge }) + } + + /// Produces `output.len()` bytes from the XOF stream. + /// + /// The first call ends the message-absorb phase by padding and absorbing the + /// final message block. Subsequent calls continue the same output stream. + fn squeeze_into(&mut self, output: &mut [u8]) -> usize { + output.fill(0); + + if !self.sponge.squeezing() { + self.sponge.pad_and_absorb(); + } + + self.sponge.squeeze(output); + output.len() + } +} + +impl Default for AsconCXof128 { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm for AsconCXof128 { + const ALG_NAME: &'static str = "Ascon-CXOF128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +/// The output-producing half of [`AsconCXof128`]. +/// +/// Calling [`XOF::into_squeezer`] consumes the absorbing `AsconCXof128`, so once +/// output begins there is no longer an object on which [`Hash::do_update`] can +/// be called. +#[derive(Clone)] +pub struct AsconCXof128Squeezer { + xof: AsconCXof128, +} + +impl XOFSqueezer for AsconCXof128Squeezer { + fn do_output(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.do_output_out(&mut out); + out + } + + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + self.xof.squeeze_into(output) + } +} + +impl Hash for AsconCXof128 { + /// Ascon-CXOF128 absorbs at a rate of 64 bits. + fn block_bitlen(&self) -> usize { + RATE * 8 + } + + /// Nominal digest size used when Ascon-CXOF128 is viewed through [`Hash`]. + fn output_len(&self) -> usize { + NOMINAL_OUTPUT_LEN + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + fn do_update(&mut self, data: &[u8]) { + // A caller-visible AsconCXof128 is always in the absorbing phase: + // into_squeezer() consumes it before output can begin. + debug_assert!( + !self.sponge.squeezing(), + "a reachable AsconCXof128 must not already be squeezing" + ); + + self.sponge.absorb(data); + } + + fn do_final(self) -> Vec { + let output_len = self.output_len(); + self.into_squeezer().do_final(output_len) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + let output_len = self.output_len(); + let written = output_len.min(output.len()); + + // Hash::do_final_out requires bytes beyond output_len to be zero. + output[written..].fill(0); + + self.into_squeezer().do_final_out(&mut output[..written]) + } + + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-CXOF128 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final()) + } + + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-CXOF128 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} + +impl XOF for AsconCXof128 { + type Squeezer = AsconCXof128Squeezer; + + fn into_squeezer(self) -> Self::Squeezer { + AsconCXof128Squeezer { xof: self } + } + + fn into_squeezer_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-CXOF128 does not support partial byte input", + )); + } + + // Per the XOF trait contract, zero partial bits is exactly the + // byte-aligned into_squeezer() operation. + let _ = partial_byte; + Ok(self.into_squeezer()) + } +} + +/// Length in bytes of the serialized Ascon-CXOF128 state. +/// +/// Layout: +/// +/// - 3-byte library version +/// - 1-byte state tag +/// - 40-byte sponge state (`5 × u64`, little endian) +/// - 8-byte rate buffer +/// - 1-byte buffer position +/// - 1-byte squeezing flag +/// +/// The customization string is already absorbed during construction, so it +/// does not need to be stored separately in the suspended representation. +pub const SUSPENDED_ASCON_CXOF128_STATE_LEN: usize = 54; + +/// Distinguishes an Ascon-CXOF128 serialized state from other Ascon sponge states. +const CXOF128_STATE_TAG: u8 = 0x03; + +/// Deserialize the common sponge representation used by both the absorbing +/// [`AsconCXof128`] and squeezing [`AsconCXof128Squeezer`] forms. +fn deserialize_sponge( + serialized_state: [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN], +) -> Result { + // Infallible: check_lib_ver returns exactly 51 bytes after removing + // the three-byte library-version prefix. + let input: &[u8; SUSPENDED_ASCON_CXOF128_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != CXOF128_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + + let mut state = Secret::<[u64; 5]>::new(); + + for i in 0..5 { + // Each selected slice is exactly eight bytes. + state[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + + let mut buf = Secret::<[u8; RATE]>::new(); + buf.copy_from_slice(&input[41..49]); + + let buf_pos = input[49] as usize; + + let squeezing = match input[50] { + 0 => false, + 1 => true, + _ => return Err(SuspendableError::InvalidData), + }; + + // While absorbing, a full rate buffer is drained immediately, so the + // position must be strictly less than RATE. During squeezing, RATE is + // allowed to represent "no buffered squeezed byte remains". + let valid_pos = if squeezing { buf_pos <= RATE } else { buf_pos < RATE }; + + if !valid_pos { + return Err(SuspendableError::InvalidData); + } + + Ok(Sponge::from_parts(state, buf, buf_pos, squeezing)) +} + +/// Serialize the common sponge representation. +fn serialize_sponge(sponge: &Sponge) -> [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_CXOF128_STATE_LEN]; + + // Infallible: add_lib_ver returns exactly 51 bytes. + let out: &mut [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = CXOF128_STATE_TAG; + + let state = sponge.state_words(); + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); + } + + out[41..49].copy_from_slice(&sponge.buf_bytes()); + + debug_assert!(sponge.buf_pos() <= RATE); + out[49] = sponge.buf_pos() as u8; + out[50] = sponge.squeezing() as u8; + + out_to_return +} + +impl Suspendable for AsconCXof128 { + fn suspend(self) -> [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN] { + serialize_sponge(&self.sponge) + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN], + ) -> Result { + let sponge = deserialize_sponge(serialized_state)?; + + // The absorbing type must never contain a state that has already + // transitioned into squeezing. Such states belong to the squeezer. + if sponge.squeezing() { + return Err(SuspendableError::InvalidData); + } + + Ok(Self { sponge }) + } +} + +impl Suspendable for AsconCXof128Squeezer { + fn suspend(self) -> [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN] { + serialize_sponge(&self.xof.sponge) + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN], + ) -> Result { + let sponge = deserialize_sponge(serialized_state)?; + + // The squeezer is only valid after the phase transition has happened. + if !sponge.squeezing() { + return Err(SuspendableError::InvalidData); + } + + Ok(Self { xof: AsconCXof128 { sponge } }) + } +} diff --git a/crypto/ascon/src/ascon_hash256.rs b/crypto/ascon/src/ascon_hash256.rs new file mode 100644 index 00000000..dcf90a32 --- /dev/null +++ b/crypto/ascon/src/ascon_hash256.rs @@ -0,0 +1,186 @@ +//! Ascon-Hash256 cryptographic hash (NIST SP 800-232 §5.1), producing a 256-bit digest. +//! +//! Sponge mode over `Ascon-p[12]` with rate = 64 bits, capacity = 256 bits. + +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{Algorithm, Hash, HashAlgParams, Suspendable}; +use bouncycastle_utils::secret::Secret; + +use crate::sponge::{RATE, Sponge}; + +const DIGEST_BYTES: usize = 32; + +/// Ascon-Hash256 hash function (NIST SP 800-232 §5.1), producing a 256-bit digest. +#[derive(Clone)] +pub struct AsconHash256 { + sponge: Sponge, +} + +impl AsconHash256 { + /// Creates a new AsconHash256 instance. + pub fn new() -> Self { + // Precomputed state after the initialization permutation (SP 800-232 Table 12). + Self { + sponge: Sponge::from_state([ + 0x9B1E_5494_E934_D681, 0x4BC3_A01E_3337_51D2, 0xAE65_396C_6B34_B81A, + 0x3C7F_D4A4_D56A_4DB3, 0x1A5C_4649_06C5_976D, + ]), + } + } + + /// One-shot hash of `data`, returning the 32-byte digest. + pub fn digest(data: &[u8]) -> [u8; DIGEST_BYTES] { + let mut hasher = Self::new(); + hasher.sponge.absorb(data); + let mut out = [0u8; DIGEST_BYTES]; + hasher.squeeze_into(&mut out); + out + } + + // Pad, absorb the final block, and squeeze the four 64-bit digest blocks (SP 800-232 + // Algorithm 5). The 32-byte digest is exactly RATE * 4 bytes, so a single generic + // `Sponge::squeeze()` call over the whole output produces all four blocks with no leftover. + fn squeeze_into(&mut self, output: &mut [u8; DIGEST_BYTES]) { + self.sponge.pad_and_absorb(); + self.sponge.squeeze(output); + } +} + +impl Default for AsconHash256 { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm for AsconHash256 { + const ALG_NAME: &'static str = "Ascon-Hash256"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl HashAlgParams for AsconHash256 { + const OUTPUT_LEN: usize = DIGEST_BYTES; + const BLOCK_LEN: usize = RATE; +} + +impl Hash for AsconHash256 { + fn block_bitlen(&self) -> usize { + RATE * 8 + } + + fn output_len(&self) -> usize { + DIGEST_BYTES + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.sponge.absorb(data); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + out.to_vec() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.sponge.absorb(data); + output.fill(0); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + let n = core::cmp::min(output.len(), DIGEST_BYTES); + output[..n].copy_from_slice(&out[..n]); + n + } + + fn do_update(&mut self, data: &[u8]) { + self.sponge.absorb(data); + } + + fn do_final(mut self) -> Vec { + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + out.to_vec() + } + + fn do_final_out(mut self, output: &mut [u8]) -> usize { + output.fill(0); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + let n = core::cmp::min(output.len(), DIGEST_BYTES); + output[..n].copy_from_slice(&out[..n]); + n + } + + fn do_final_partial_bits( + self, + _partial_byte: u8, + _num_partial_bits: usize, + ) -> Result, HashError> { + Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + _num_partial_bits: usize, + _output: &mut [u8], + ) -> Result { + Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} + +/// Length in bytes of the serialized state of [`AsconHash256`]. +/// Layout: 3-byte library version || 1-byte state tag || 40-byte sponge state (5 × u64 LE) +/// || 8-byte rate buffer || 1-byte buffer position. +pub const SUSPENDED_ASCON_HASH256_STATE_LEN: usize = 53; + +// Distinguishes an Ascon-Hash256 serialized state from the other (same-shaped) Ascon sponge states. +const HASH256_STATE_TAG: u8 = 0x01; + +impl Suspendable for AsconHash256 { + fn suspend(self) -> [u8; SUSPENDED_ASCON_HASH256_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_HASH256_STATE_LEN]; + // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_HASH256_STATE_LEN - 3 = 50 bytes. + let out: &mut [u8; SUSPENDED_ASCON_HASH256_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = HASH256_STATE_TAG; + let state = self.sponge.state_words(); + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); + } + out[41..49].copy_from_slice(&self.sponge.buf_bytes()); + // buf_pos is always < RATE (8) before squeezing has begun, so it fits in one byte. + debug_assert!(self.sponge.buf_pos() < RATE); + out[49] = self.sponge.buf_pos() as u8; + + out_to_return + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_HASH256_STATE_LEN], + ) -> Result { + // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_HASH256_STATE_LEN - 3 = 50 bytes. + let input: &[u8; SUSPENDED_ASCON_HASH256_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != HASH256_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + let mut s = Secret::<[u64; 5]>::new(); + for i in 0..5 { + // infallible: each slice is exactly 8 bytes (1+i*8..1+i*8+8) by construction. + s[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + let mut buf = Secret::<[u8; RATE]>::new(); + buf.copy_from_slice(&input[41..49]); + let buf_pos = input[49] as usize; + if buf_pos >= RATE { + return Err(SuspendableError::InvalidData); + } + + Ok(AsconHash256 { sponge: Sponge::from_parts(s, buf, buf_pos, false) }) + } +} diff --git a/crypto/ascon/src/ascon_xof128.rs b/crypto/ascon/src/ascon_xof128.rs new file mode 100644 index 00000000..db9fe561 --- /dev/null +++ b/crypto/ascon/src/ascon_xof128.rs @@ -0,0 +1,332 @@ +//! Ascon-XOF128 extendable-output function (NIST SP 800-232 §5.2). +//! +//! Sponge mode over `Ascon-p[12]` with rate = 64 bits and capacity = 256 bits. +//! Input absorption and output squeezing are represented by separate Rust types: +//! [`AsconXof128`] accepts input, while [`AsconXof128Squeezer`] produces the +//! extendable output stream. + +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{Algorithm, Hash, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_utils::secret::Secret; + +use crate::sponge::{RATE, Sponge}; + +/// Nominal hash-view output length for Ascon-XOF128. +/// +/// XOFs do not have an inherent output length. The [`Hash`] view therefore uses +/// twice the 128-bit security strength, matching the convention used for SHAKE128. +const NOMINAL_OUTPUT_LEN: usize = 32; + +/// Ascon-XOF128 as specified in NIST SP 800-232. +#[derive(Clone)] +pub struct AsconXof128 { + sponge: Sponge, +} + +impl AsconXof128 { + /// Creates a new Ascon-XOF128 instance. + pub fn new() -> Self { + // Precomputed state after the initialization permutation + // (SP 800-232 Table 12). + Self { + sponge: Sponge::from_state([ + 0xDA82CE768D9447EB, 0xCC7CE6C75F1EF969, 0xE7508FD780085631, 0x0EE0EA53416B58CC, + 0xE0547524DB6F0BDE, + ]), + } + } + + /// Produces `output.len()` bytes from the XOF stream. + /// + /// The first call ends the absorb phase by padding and absorbing the final + /// message block. Subsequent calls continue the same output stream. + fn squeeze_into(&mut self, output: &mut [u8]) -> usize { + output.fill(0); + + if !self.sponge.squeezing() { + self.sponge.pad_and_absorb(); + } + + self.sponge.squeeze(output); + output.len() + } +} + +impl Default for AsconXof128 { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm for AsconXof128 { + const ALG_NAME: &'static str = "Ascon-XOF128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +/// The output-producing half of [`AsconXof128`]. +/// +/// Calling [`XOF::into_squeezer`] consumes the absorbing `AsconXof128`, so once +/// output begins there is no longer an object on which [`Hash::do_update`] can +/// be called. +#[derive(Clone)] +pub struct AsconXof128Squeezer { + xof: AsconXof128, +} + +impl XOFSqueezer for AsconXof128Squeezer { + fn do_output(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.do_output_out(&mut out); + out + } + + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + self.xof.squeeze_into(output) + } +} + +impl Hash for AsconXof128 { + /// Ascon-XOF128 absorbs at a rate of 64 bits. + fn block_bitlen(&self) -> usize { + RATE * 8 + } + + /// Nominal digest size used when Ascon-XOF128 is viewed through [`Hash`]. + fn output_len(&self) -> usize { + NOMINAL_OUTPUT_LEN + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + fn do_update(&mut self, data: &[u8]) { + // A caller-visible AsconXof128 is always in the absorbing phase: + // into_squeezer() consumes it before output can begin. + debug_assert!( + !self.sponge.squeezing(), + "a reachable AsconXof128 must not already be squeezing" + ); + + self.sponge.absorb(data); + } + + fn do_final(self) -> Vec { + let output_len = self.output_len(); + self.into_squeezer().do_final(output_len) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + let output_len = self.output_len(); + let written = output_len.min(output.len()); + + // Hash::do_final_out requires bytes beyond output_len to be zero. + output[written..].fill(0); + + self.into_squeezer().do_final_out(&mut output[..written]) + } + + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-XOF128 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final()) + } + + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-XOF128 does not support partial byte input", + )); + } + + // A zero-bit partial byte means the message is byte-aligned. + let _ = partial_byte; + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} + +impl XOF for AsconXof128 { + type Squeezer = AsconXof128Squeezer; + + fn into_squeezer(self) -> Self::Squeezer { + AsconXof128Squeezer { xof: self } + } + + fn into_squeezer_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result { + if num_bits > 7 { + return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); + } + + if num_bits != 0 { + return Err(HashError::InvalidInput( + "Ascon-XOF128 does not support partial byte input", + )); + } + + // Per the XOF trait contract, zero partial bits is exactly the + // byte-aligned into_squeezer() operation. + let _ = partial_byte; + Ok(self.into_squeezer()) + } +} + +/// Length in bytes of the serialized Ascon-XOF128 state. +/// +/// Layout: +/// +/// - 3-byte library version +/// - 1-byte state tag +/// - 40-byte sponge state (`5 × u64`, little endian) +/// - 8-byte rate buffer +/// - 1-byte buffer position +/// - 1-byte squeezing flag +pub const SUSPENDED_ASCON_XOF128_STATE_LEN: usize = 54; + +/// Distinguishes an Ascon-XOF128 serialized state from other Ascon sponge states. +const XOF128_STATE_TAG: u8 = 0x02; + +/// Deserialize the common sponge representation used by both the absorbing +/// [`AsconXof128`] and squeezing [`AsconXof128Squeezer`] forms. +fn deserialize_sponge( + serialized_state: [u8; SUSPENDED_ASCON_XOF128_STATE_LEN], +) -> Result { + // Infallible: check_lib_ver returns exactly 51 bytes after removing + // the three-byte library-version prefix. + let input: &[u8; SUSPENDED_ASCON_XOF128_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != XOF128_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + + let mut state = Secret::<[u64; 5]>::new(); + + for i in 0..5 { + // Each selected slice is exactly eight bytes. + state[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + + let mut buf = Secret::<[u8; RATE]>::new(); + buf.copy_from_slice(&input[41..49]); + + let buf_pos = input[49] as usize; + + let squeezing = match input[50] { + 0 => false, + 1 => true, + _ => return Err(SuspendableError::InvalidData), + }; + + // While absorbing, a full rate buffer is drained immediately, so the + // position must be strictly less than RATE. During squeezing, RATE is + // allowed to represent "no buffered squeezed byte remains". + let valid_pos = if squeezing { buf_pos <= RATE } else { buf_pos < RATE }; + + if !valid_pos { + return Err(SuspendableError::InvalidData); + } + + Ok(Sponge::from_parts(state, buf, buf_pos, squeezing)) +} + +/// Serialize the common sponge representation. +fn serialize_sponge(sponge: &Sponge) -> [u8; SUSPENDED_ASCON_XOF128_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_XOF128_STATE_LEN]; + + // Infallible: add_lib_ver returns exactly 51 bytes. + let out: &mut [u8; SUSPENDED_ASCON_XOF128_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = XOF128_STATE_TAG; + + let state = sponge.state_words(); + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); + } + + out[41..49].copy_from_slice(&sponge.buf_bytes()); + + debug_assert!(sponge.buf_pos() <= RATE); + out[49] = sponge.buf_pos() as u8; + out[50] = sponge.squeezing() as u8; + + out_to_return +} + +impl Suspendable for AsconXof128 { + fn suspend(self) -> [u8; SUSPENDED_ASCON_XOF128_STATE_LEN] { + serialize_sponge(&self.sponge) + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_XOF128_STATE_LEN], + ) -> Result { + let sponge = deserialize_sponge(serialized_state)?; + + // The absorbing type must never contain a state that has already + // transitioned into squeezing. Such states belong to the squeezer. + if sponge.squeezing() { + return Err(SuspendableError::InvalidData); + } + + Ok(Self { sponge }) + } +} + +impl Suspendable for AsconXof128Squeezer { + fn suspend(self) -> [u8; SUSPENDED_ASCON_XOF128_STATE_LEN] { + serialize_sponge(&self.xof.sponge) + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_XOF128_STATE_LEN], + ) -> Result { + let sponge = deserialize_sponge(serialized_state)?; + + // The squeezer is only valid after the phase transition has happened. + if !sponge.squeezing() { + return Err(SuspendableError::InvalidData); + } + + Ok(Self { xof: AsconXof128 { sponge } }) + } +} diff --git a/crypto/ascon/src/lib.rs b/crypto/ascon/src/lib.rs new file mode 100644 index 00000000..78338887 --- /dev/null +++ b/crypto/ascon/src/lib.rs @@ -0,0 +1,183 @@ +//! Ascon-based lightweight cryptography (NIST SP 800-232). +//! +//! This crate implements the four Ascon functions standardized in NIST SP 800-232 (August 2025): +//! +//! - [`ascon_aead128::AsconAead128`] — Ascon-AEAD128 authenticated encryption (128-bit +//! key/nonce/tag, 128-bit single-key security). +//! - [`ascon_hash256::AsconHash256`] — Ascon-Hash256 hash function (256-bit digest, 128-bit +//! security). +//! - [`ascon_xof128::AsconXof128`] — Ascon-XOF128 extendable-output function. +//! - [`ascon_cxof128::AsconCXof128`] — Ascon-CXOF128 customized extendable-output function. +//! +//! # Usage Examples +//! +//! Hashing (one-shot and streaming): +//! ``` +//! use bouncycastle_ascon::ascon_hash256::AsconHash256; +//! use bouncycastle_core::traits::Hash; +//! use bouncycastle_core::traits::XOF; +//! +//! // One-shot: +//! let digest = AsconHash256::digest(b"hello world"); +//! assert_eq!(digest.len(), 32); +//! +//! // Streaming: +//! let mut h = AsconHash256::new(); +//! h.do_update(b"hello "); +//! h.do_update(b"world"); +//! let mut out = [0u8; 32]; +//! h.do_final_out(&mut out); +//! assert_eq!(out, digest); +//! ``` +//! +//! Authenticated encryption (one-shot): +//! ``` +//! use bouncycastle_ascon::ascon_aead128::AsconAead128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let nonce = [1u8; 16]; // MUST be unique per encryption under a given key +//! let ad = b"associated data"; +//! let plaintext = b"secret message"; +//! +//! let mut ct = vec![0u8; plaintext.len() + 16]; // ciphertext || 16-byte tag +//! let n = AsconAead128::encrypt(&key, &nonce, Some(ad), plaintext, &mut ct).unwrap(); +//! ct.truncate(n); +//! +//! let mut pt = vec![0u8; ct.len() - 16]; +//! let m = AsconAead128::decrypt(&key, &nonce, Some(ad), &ct, &mut pt).unwrap(); +//! pt.truncate(m); +//! assert_eq!(&pt, plaintext); +//! ``` +//! +//! Authenticated encryption (streaming, detached tag). The decryptor holds back the last 16 +//! bytes it has seen, in case they are an inline tag, so `do_final_out_detached` is where they come out: +//! ``` +//! use bouncycastle_ascon::ascon_aead128::{AsconAead128Decryptor, AsconAead128Encryptor}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{ +//! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +//! }; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! +//! let plaintext = b"secret message!!"; +//! let (mut enc, nonce) = AsconAead128Encryptor::do_encrypt_init(&key).unwrap(); +//! enc.do_update_aad(b"associated data").unwrap(); +//! let mut ciphertext = [0u8; 16]; +//! enc.do_update_out(plaintext, &mut ciphertext).unwrap(); +//! let mut final_buf = [0u8; 16]; +//! let (_, tag) = enc.do_final_out_detached(&mut final_buf).unwrap(); +//! +//! let mut dec = AsconAead128Decryptor::do_decrypt_init(&key, &nonce).unwrap(); +//! dec.do_update_aad(b"associated data").unwrap(); +//! let mut recovered = [0u8; 16]; +//! let n = dec.do_update_out(&ciphertext, &mut recovered).unwrap(); // 0: all 16 held back +//! let m = dec.do_final_out_detached(&tag, &mut final_buf).unwrap(); // now authenticated +//! recovered[n..n + m].copy_from_slice(&final_buf[..m]); +//! assert_eq!(&recovered, plaintext); +//! ``` +//! +//! For the inline `ciphertext || tag` layout that most wire formats and files use, the pair is +//! also a [`bouncycastle_core::traits::SymmetricCipherEncryptor`] / +//! [`bouncycastle_core::traits::SymmetricCipherDecryptor`], which covers the no-AAD case -- +//! streaming, or through its `encrypt_out` / `decrypt_out` one-shots -- and +//! [`bouncycastle_core::traits::AEADCipherEncryptor::encrypt_out_with_aad`] / +//! [`bouncycastle_core::traits::AEADCipherDecryptor::decrypt_out_with_aad`] are the one-shots with AAD: +//! ``` +//! use bouncycastle_ascon::ascon_aead128::{AsconAead128Decryptor, AsconAead128Encryptor}; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{ +//! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +//! }; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let plaintext = b"secret message!!"; +//! +//! // No AAD: just a symmetric cipher. +//! let mut inline = [0u8; 32]; // AsconAead128Encryptor::encrypt_out_len(16) +//! let (nonce, len) = AsconAead128Encryptor::encrypt_out(&key, plaintext, &mut inline).unwrap(); +//! assert_eq!(len, plaintext.len() + 16); // ciphertext || tag +//! let mut recovered = [0u8; 16]; +//! let n = AsconAead128Decryptor::decrypt_out(&key, &nonce, &inline[..len], &mut recovered).unwrap(); +//! assert_eq!(&recovered[..n], plaintext); +//! +//! // With AAD. +//! let (nonce, len) = AsconAead128Encryptor::encrypt_out_with_aad(&key, b"aad", plaintext, &mut inline).unwrap(); +//! let n = AsconAead128Decryptor::decrypt_out_with_aad(&key, &nonce, b"aad", &inline[..len], &mut recovered).unwrap(); +//! assert_eq!(&recovered[..n], plaintext); +//! ``` +//! +//! Extendable output: +//! ``` +//! use bouncycastle_ascon::ascon_xof128::AsconXof128; +//! use bouncycastle_core::traits::XOF; +//! +//! let out = AsconXof128::new().xof(b"input", 64); +//! assert_eq!(out.len(), 64); +//! ``` +//! +//! # Memory Usage +//! +//! Ascon is a lightweight, permutation-based design intended for constrained devices. The internal +//! permutation state is 320 bits (40 bytes), held as five `u64` words, shared by all four +//! functions. There are no heap allocations in the streaming/`*_out` APIs, and stack usage is +//! small and constant; consequently this crate has no dedicated `mem_usage_benches` harness. +//! +//! | Type | In-memory size (bytes) | Suspended state size (bytes) | +//! |------|-------------------------|-------------------------------| +//! | [`ascon_aead128::AsconAead128`] | 72 | [`ascon_aead128::SUSPENDED_ASCON_AEAD128_STATE_LEN`] (46) | +//! | [`ascon_hash256::AsconHash256`] | 64 | [`ascon_hash256::SUSPENDED_ASCON_HASH256_STATE_LEN`] (53) | +//! | [`ascon_xof128::AsconXof128`] | 64 | [`ascon_xof128::SUSPENDED_ASCON_XOF128_STATE_LEN`] (54) | +//! | [`ascon_cxof128::AsconCXof128`] | 64 | [`ascon_cxof128::SUSPENDED_ASCON_CXOF128_STATE_LEN`] (54) | +//! +//! "In-memory size" is `core::mem::size_of` on a 64-bit target. +//! +//! # Security Considerations +//! +//! - **Nonce uniqueness (SP 800-232 R3):** a (key, nonce) pair must never be reused for two +//! different Ascon-AEAD128 encryptions. Nonce reuse breaks confidentiality. +//! - **Tag length:** this crate always produces and verifies the full 128-bit tag. Truncated tags +//! (SP 800-232 §4.2.1) are not exposed. +//! - **No partial-byte input:** Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 are byte-oriented; +//! their `do_final_partial_bits`/`do_final_partial_bits_out` (and the equivalent XOF methods) +//! always return `HashError::InvalidInput`, including when reached through `HashFactory`. A +//! caller that needs a partial-byte final block should reach for SHA-3, which supports one. +//! - **Decryption tag check failure:** a ciphertext decryption whose finalization returns +//! `Err(SymmetricCipherError::AEADTagCheckFailed)` must be treated as tampered, and the entire +//! plaintext rejected. The one-shot APIs ([`ascon_aead128::AsconAead128::decrypt`], +//! [`bouncycastle_core::traits::AEADCipherDecryptor::decrypt_out_detached`], +//! [`bouncycastle_core::traits::AEADCipherDecryptor::decrypt_out_with_aad`] and +//! [`bouncycastle_core::traits::SymmetricCipherDecryptor::decrypt_out`]) zeroize their output +//! buffer before returning that error. The streaming API +//! ([`ascon_aead128::AsconAead128::do_decrypt_update`] / +//! [`ascon_aead128::AsconAead128::do_decrypt_final`], or `do_update_out` followed by +//! [`bouncycastle_core::traits::AEADCipherDecryptor::do_final_out_detached`] or +//! [`bouncycastle_core::traits::SymmetricCipherDecryptor::do_final`]) does not: plaintext +//! bytes are necessarily written to the caller's buffer *before* the tag can be checked, so an +//! application streaming a large plaintext must have a way to cancel the operation or +//! transaction if finalization returns an error. + +// `bouncycastle-core` still uses `Vec` internally (see the TODO at the top of +// crypto/core/src/lib.rs), which blocks this crate from being `#![no_std]` as long as it depends +// on core's `std`-gated APIs. +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod permutation; +mod sponge; + +pub mod ascon_aead128; +pub use ascon_aead128::Ascon_AEAD128; +pub mod ascon_cxof128; +pub mod ascon_hash256; +pub mod ascon_xof128; + +/// Algorithm name for Ascon-AEAD128. +pub const ASCON_AEAD128_NAME: &str = "Ascon-AEAD128"; +/// Algorithm name for Ascon-Hash256. +pub const ASCON_HASH256_NAME: &str = "Ascon-Hash256"; +/// Algorithm name for Ascon-XOF128. +pub const ASCON_XOF128_NAME: &str = "Ascon-XOF128"; +/// Algorithm name for Ascon-CXOF128. +pub const ASCON_CXOF128_NAME: &str = "Ascon-CXOF128"; diff --git a/crypto/ascon/src/permutation.rs b/crypto/ascon/src/permutation.rs new file mode 100644 index 00000000..a373bb78 --- /dev/null +++ b/crypto/ascon/src/permutation.rs @@ -0,0 +1,138 @@ +//! The Ascon-p permutation family (NIST SP 800-232 §3), shared by all four functions in this +//! crate: Ascon-AEAD128 uses both `Ascon-p[12]` and `Ascon-p[8]`; Ascon-Hash256, Ascon-XOF128, and +//! Ascon-CXOF128 use only `Ascon-p[12]`. +//! +//! These also carry the little-endian load/store helpers, replacing the external `arrayref` +//! crate so that this crate carries no third-party runtime dependencies (per the project's +//! QUALITY_AND_STYLE rules). All callers pass slices that are at least 8 bytes long at the given +//! offset, so `copy_from_slice` is infallible by construction and no fallible conversion is +//! involved. + +/// Load the 8 bytes at `src[off..off + 8]` as a little-endian `u64`. +#[inline(always)] +pub(crate) fn load_u64_le(src: &[u8], off: usize) -> u64 { + let mut b = [0u8; 8]; + b.copy_from_slice(&src[off..off + 8]); + u64::from_le_bytes(b) +} + +/// Store `val` as little-endian into `dst[off..off + 8]`. +#[inline(always)] +pub(crate) fn store_u64_le(dst: &mut [u8], off: usize, val: u64) { + dst[off..off + 8].copy_from_slice(&val.to_le_bytes()); +} + +/// The 320-bit Ascon state (SP 800-232 §3.1 Eq. 2): five 64-bit words S0..S4. +pub(crate) type AsconState = [u64; 5]; + +// The constants const_0..const_15 used to derive the round constants of Ascon-p[r] +// (SP 800-232 Table 5). The round constant for round i (0 <= i <= r-1) of Ascon-p[r] is +// c_i = const_{16-r+i} (SP 800-232 §3.2 Eq. 3). +const ROUND_CONSTS: [u64; 16] = [ + 0x3c, 0x2d, 0x1e, 0x0f, 0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87, 0x78, 0x69, 0x5a, 0x4b, +]; + +/// One round p = p_L ∘ p_S ∘ p_C (SP 800-232 §3.2–3.4 Eq. 1): the constant-addition layer p_C +/// (§3.2 Eq. 4), the substitution layer p_S (§3.3 Eqs. 6–7), and the linear diffusion layer p_L +/// (§3.4 Eqs. 8–12) are fused here in their bitsliced form. +#[inline(always)] +pub(crate) fn round(s: &mut AsconState, c: u64) { + let sx = s[2] ^ c; + let t0 = s[0] ^ s[1] ^ sx ^ s[3] ^ (s[1] & (s[0] ^ sx ^ s[4])); + let t1 = s[0] ^ sx ^ s[3] ^ s[4] ^ ((s[1] ^ sx) & (s[1] ^ s[3])); + let t2 = s[1] ^ sx ^ s[4] ^ (s[3] & s[4]); + let t3 = s[0] ^ s[1] ^ sx ^ ((!s[0]) & (s[3] ^ s[4])); + let t4 = s[1] ^ s[3] ^ s[4] ^ ((s[0] ^ s[4]) & s[1]); + s[0] = t0 ^ t0.rotate_right(19) ^ t0.rotate_right(28); + s[1] = t1 ^ t1.rotate_right(39) ^ t1.rotate_right(61); + s[2] = !(t2 ^ t2.rotate_right(1) ^ t2.rotate_right(6)); + s[3] = t3 ^ t3.rotate_right(10) ^ t3.rotate_right(17); + s[4] = t4 ^ t4.rotate_right(7) ^ t4.rotate_right(41); +} + +/// Ascon-p[12] (SP 800-232 §3.2 Eq. 3: c_i = const_{4+i} for i = 0..11, i.e. round constants +/// const_4..const_15 of Table 5). +#[inline(always)] +pub(crate) fn p12(s: &mut AsconState) { + for &c in &ROUND_CONSTS[4..16] { + round(s, c); + } +} + +/// Ascon-p[8] (SP 800-232 §3.2 Eq. 3: c_i = const_{8+i} for i = 0..7, i.e. round constants +/// const_8..const_15 of Table 5). +#[inline(always)] +pub(crate) fn p8(s: &mut AsconState) { + for &c in &ROUND_CONSTS[8..16] { + round(s, c); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // SP 800-232 Table 14: initial values (before the initialization permutation). + const HASH256_IV: u64 = 0x0000080100cc0002; + const XOF128_IV: u64 = 0x0000080000cc0003; + const CXOF128_IV: u64 = 0x0000080000cc0004; + + // Pins the permutation independently of the KAT sweeps: SP 800-232 Table 12 gives the state + // at the end of each function's initialization phase, i.e. Ascon-p[12](IV || 0^256). + #[test] + fn p12_matches_table_12_precomputed_states() { + let mut s: AsconState = [HASH256_IV, 0, 0, 0, 0]; + p12(&mut s); + assert_eq!( + s, + [ + 0x9b1e5494e934d681, 0x4bc3a01e333751d2, 0xae65396c6b34b81a, 0x3c7fd4a4d56a4db3, + 0x1a5c464906c5976d, + ] + ); + + let mut s: AsconState = [XOF128_IV, 0, 0, 0, 0]; + p12(&mut s); + assert_eq!( + s, + [ + 0xda82ce768d9447eb, 0xcc7ce6c75f1ef969, 0xe7508fd780085631, 0x0ee0ea53416b58cc, + 0xe0547524db6f0bde, + ] + ); + + let mut s: AsconState = [CXOF128_IV, 0, 0, 0, 0]; + p12(&mut s); + assert_eq!( + s, + [ + 0x675527c2a0e8de03, 0x43d12d7dc0377bbc, 0xe9901dec426e81b5, 0x2ab14907720780b6, + 0x8f3f1d02d432bc46, + ] + ); + } + + // Pins `AsconCXof128::new()`'s precomputed empty-customization state (see + // `ascon_cxof128.rs`) by recomputing it from the Table 12 CXOF128 state above, following + // SP 800-232 Algorithm 7 with |Z| = 0: XOR the length word Z_0 = int64(0) into S[0..63], + // Ascon-p[12], then XOR the pad-only last customization block (Eq. 77: pad(empty, 64) = + // 0x01 || 0^63, i.e. byte 0x01 loaded little-endian into S[0..63]) and Ascon-p[12] again. + #[test] + fn cxof128_empty_customization_state_matches_algorithm_7() { + let mut s: AsconState = [ + 0x675527c2a0e8de03, 0x43d12d7dc0377bbc, 0xe9901dec426e81b5, 0x2ab14907720780b6, + 0x8f3f1d02d432bc46, + ]; + s[0] ^= 0u64; // Z_0 = int64(|Z|) = int64(0) = 0 (a no-op XOR, spelled out for clarity) + p12(&mut s); + s[0] ^= 0x01u64; // pad(empty, 64) = 0x01 || 0^63, loaded little-endian + p12(&mut s); + assert_eq!( + s, + [ + 0x500cccc894e3c9e8, 0x5bed06f28f71248d, 0x3b03a0f930afd512, 0x112ef093aa5c698b, + 0x00c8356340a347f0, + ] + ); + } +} diff --git a/crypto/ascon/src/sponge.rs b/crypto/ascon/src/sponge.rs new file mode 100644 index 00000000..c1618b6d --- /dev/null +++ b/crypto/ascon/src/sponge.rs @@ -0,0 +1,189 @@ +//! The absorb/pad/squeeze sponge shared by Ascon-Hash256, Ascon-XOF128, and Ascon-CXOF128 +//! (NIST SP 800-232 §5): a 64-bit rate over `Ascon-p[12]`. Each of those three types holds one +//! [`Sponge`] and differs only in its initial state and (for Ascon-CXOF128) an extra +//! customization-string absorption performed before message absorption begins. + +use bouncycastle_utils::secret::Secret; + +use crate::permutation::{AsconState, load_u64_le, p12, store_u64_le}; + +/// Rate in bytes for the Hash256/XOF128/CXOF128 sponge (64 bits, per SP 800-232 §5). +pub(crate) const RATE: usize = 8; + +pub(crate) struct Sponge { + // 320-bit sponge state (five 64-bit words S0..S4). Wrapped in `Secret` so the working state + // -- which absorbs the message -- is scrubbed with volatile writes when dropped. + s: Secret, + // Rate buffer: partial input block while absorbing, or leftover squeezed bytes afterwards. + buf: Secret<[u8; RATE]>, + buf_pos: usize, + squeezing: bool, +} + +impl Sponge { + /// Construct a sponge already in the given state (typically a function's precomputed + /// post-initialization state, SP 800-232 Table 12), ready to absorb. + pub(crate) fn from_state(state: AsconState) -> Self { + let mut s: Secret = Secret::new(); + *s = state; + Self { s, buf: Secret::new(), buf_pos: 0, squeezing: false } + } + + /// Reconstruct a sponge from raw parts (used by `Suspendable::from_suspended`). + pub(crate) fn from_parts( + s: Secret, + buf: Secret<[u8; RATE]>, + buf_pos: usize, + squeezing: bool, + ) -> Self { + Self { s, buf, buf_pos, squeezing } + } + + pub(crate) fn state_words(&self) -> [u64; 5] { + *self.s + } + + pub(crate) fn buf_bytes(&self) -> [u8; RATE] { + *self.buf + } + + pub(crate) fn buf_pos(&self) -> usize { + self.buf_pos + } + + pub(crate) fn squeezing(&self) -> bool { + self.squeezing + } + + /// XOR `v` into the first state word. Used by Ascon-CXOF128 to absorb the customization + /// string's bit length (SP 800-232 §5.3 Eq. 75) before the length-prefixed customization + /// blocks are absorbed via [`Sponge::absorb`]. + pub(crate) fn xor_word0(&mut self, v: u64) { + self.s[0] ^= v; + } + + /// Apply `Ascon-p[12]` to the state directly. Used by Ascon-CXOF128 between customization + /// blocks (SP 800-232 Algorithm 7). + pub(crate) fn permute(&mut self) { + p12(&mut self.s); + } + + /// Reset the rate buffer to begin a fresh absorb phase. Used by Ascon-CXOF128 once the + /// customization string has been fully absorbed, before message absorption begins. + pub(crate) fn reset_buffer(&mut self) { + self.buf.fill(0); + self.buf_pos = 0; + } + + /// Absorb input data. Panics if called after squeezing has begun. + pub(crate) fn absorb(&mut self, input: &[u8]) { + if self.squeezing { + panic!("attempt to absorb while squeezing"); + } + + let available = RATE - self.buf_pos; + if input.len() < available { + self.buf[self.buf_pos..self.buf_pos + input.len()].copy_from_slice(input); + self.buf_pos += input.len(); + return; + } + + let mut input = input; + + if self.buf_pos > 0 { + self.buf[self.buf_pos..].copy_from_slice(&input[..available]); + self.s[0] ^= u64::from_le_bytes(*self.buf); + p12(&mut self.s); + input = &input[available..]; + } + + while input.len() >= RATE { + self.s[0] ^= load_u64_le(input, 0); + p12(&mut self.s); + input = &input[RATE..]; + } + + self.buf[..input.len()].copy_from_slice(input); + self.buf_pos = input.len(); + } + + // Pad the final absorbed block (SP 800-232 Appendix A.2, Algorithm 2) by XORing in the + // buffered bytes (masked to `buf_pos` bytes -- any stale bytes beyond that in `buf` are + // masked off) followed by the padding bit at byte position `buf_pos`. Deliberately does not + // permute: the permutation is folded into the first block of `squeeze()` below, since Ascon- + // Hash256's fixed 4-block output and Ascon-XOF128/CXOF128's streaming output both begin + // their squeeze phase with a permute-then-read (SP 800-232 Algorithms 5-7). + pub(crate) fn pad_and_absorb(&mut self) { + let final_bits = (self.buf_pos << 3) as u32; + let x = u64::from_le_bytes(*self.buf); + let mask = + if final_bits == 0 { 0u64 } else { 0x00FF_FFFF_FFFF_FFFF_u64 >> (56 - final_bits) }; + self.s[0] ^= x & mask; + self.s[0] ^= 0x01u64 << final_bits; + } + + /// Squeeze `output.len()` bytes. May be called multiple times; the first call must follow + /// [`Sponge::pad_and_absorb`] and ends the absorb phase. + pub(crate) fn squeeze(&mut self, output: &mut [u8]) { + let mut output = output; + + if !self.squeezing { + self.squeezing = true; + self.buf_pos = RATE; + } else if self.buf_pos < RATE { + let available = RATE - self.buf_pos; + if output.len() <= available { + let end_pos = self.buf_pos + output.len(); + output.copy_from_slice(&self.buf[self.buf_pos..end_pos]); + self.buf_pos = end_pos; + return; + } + + output[..available].copy_from_slice(&self.buf[self.buf_pos..]); + output = &mut output[available..]; + self.buf_pos = RATE; + } + + while output.len() >= RATE { + p12(&mut self.s); + store_u64_le(output, 0, self.s[0]); + output = &mut output[RATE..]; + } + + if !output.is_empty() { + p12(&mut self.s); + *self.buf = self.s[0].to_le_bytes(); + output.copy_from_slice(&self.buf[..output.len()]); + self.buf_pos = output.len(); + } + } +} + +impl Clone for Sponge { + fn clone(&self) -> Self { + Self { + s: self.s.clone(), + buf: self.buf.clone(), + buf_pos: self.buf_pos, + squeezing: self.squeezing, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // `xor_word0` cannot be exercised as an XOR (as opposed to e.g. an OR) via any published KAT: + // its only caller (Ascon-CXOF128's customization-length absorption) combines a bit_length + // value -- always a multiple of 8 -- with a state word whose low 3 bits happen to be the + // only ones set for every customization length actually covered by NIST's KAT file (max 32 + // bytes). Pin the arithmetic directly instead. + #[test] + fn xor_word0_is_xor_not_or() { + let mut sponge = Sponge::from_state([0b0000_0101, 0, 0, 0, 0]); + sponge.xor_word0(0b0000_0110); + // 0b101 ^ 0b110 = 0b011. An OR would give 0b111. + assert_eq!(sponge.state_words()[0], 0b0000_0011); + } +} diff --git a/crypto/ascon/tests/aead128_tests.rs b/crypto/ascon/tests/aead128_tests.rs new file mode 100644 index 00000000..cdb274db --- /dev/null +++ b/crypto/ascon/tests/aead128_tests.rs @@ -0,0 +1,707 @@ +//! Ascon-AEAD128 tests (NIST SP 800-232). +//! +//! - A small embedded set of NIST LWC known-answer vectors (always-on correctness, no external +//! repo required). The full sweep lives in `bc_test_data.rs`. +//! - Behavioral / contract tests (round-trips, streaming chunk-boundary equivalence, authentication +//! failures, determinism), driven through the inherent explicit-nonce API. +//! - The shared conformance framework (`core-test-framework`), which exercises the +//! `AEADCipherEncryptor`/`AEADCipherDecryptor` pair, with internally-generated nonces, in both +//! the detached-tag (`*_detached`) and the inline `ciphertext || tag` layouts -- the latter also +//! through the `SymmetricCipherEncryptor`/`SymmetricCipherDecryptor` traits they extend. + +use bouncycastle_ascon::ascon_aead128::{ + AsconAead128, AsconAead128Decryptor, AsconAead128Encryptor, +}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkAEADCipher; +use bouncycastle_hex as hex; + +// All embedded vectors use this fixed key/nonce (the NIST LWC KAT convention). +const KEY: [u8; 16] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, +]; +const NONCE: [u8; 16] = [ + 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, +]; + +const PT_SIZES: [usize; 10] = [0, 1, 15, 16, 17, 31, 32, 33, 64, 100]; +const CHUNK_SIZES: [usize; 6] = [1, 3, 7, 13, 16, 17]; + +/// Embedded NIST LWC Ascon-AEAD128 vectors `(plaintext, associated_data, ciphertext||tag)` in hex. +/// Key = Nonce = 000102…0F. Spans empty input, AD-only (incl. a full 32-byte AD block), partial PT +/// with AD, and a multi-block plaintext. (Counts 1, 2, 5, 33, 68, 69, 153, 1057 of +/// LWC_AEAD_KAT_128_128.txt.) +const AEAD_KAT: &[(&str, &str, &str)] = &[ + ("", "", "4427D64B8E1E1451FC445960F0839BB0"), + ("", "00", "103AB79D913A0321287715A979BB8585"), + ("", "00010203", "C6FF3CF70575B144B955820D9BC7685E"), + ( + "", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "22133A313FBF0B38029A45870AADC542", + ), + ("0001", "00", "25FB41D2732019820A0F8BAB4248B35E7B0B"), + ("0001", "0001", "49E57017A30E8073D1FA284AC8346110F89F"), + ( + "00010203", + "000102030405060708090A0B0C0D0E0F10111213", + "C305EB0E9A9A7833C5F6FB36BD82F1C78C322678", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "", + "E770D289D2A44AEE7CD0A48ECE5274E381BAD7E163DCC4970F7873610DEBBEB1A28657F6E82FE53D08B09EFF9330BD2B", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn ad_opt(ad: &[u8]) -> Option<&[u8]> { + if ad.is_empty() { None } else { Some(ad) } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +/// Build a `KeyMaterial<16>` suitable for `AsconAead128`. The NIST LWC KAT vectors include an +/// all-zero key (Count=1), which `KeyMaterial::from_bytes_as_type` would otherwise tag +/// `KeyType::Zeroized` / `SecurityStrength::None`; force the type/strength the way a caller who +/// knows the provenance of the key would (see `cli/src/helpers.rs::parse_seed`). +fn key_material(key: &[u8; 16]) -> KeyMaterial<16> { + let mut km = KeyMaterial::<16>::from_bytes_as_type(key, KeyType::SymmetricCipherKey).unwrap(); + do_hazardous_operations(&mut km, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::_128bit) + }) + .unwrap(); + km +} + +fn enc_oneshot(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8]) -> Vec { + let km = key_material(key); + let mut out = vec![0u8; pt.len() + 16]; + let n = AsconAead128::encrypt(&km, nonce, ad_opt(ad), pt, &mut out).unwrap(); + out.truncate(n); + out +} + +fn dec_oneshot( + key: &[u8; 16], + nonce: &[u8; 16], + ad: &[u8], + ct: &[u8], +) -> Result, SymmetricCipherError> { + let km = key_material(key); + let mut out = vec![0u8; ct.len()]; + let n = AsconAead128::decrypt(&km, nonce, ad_opt(ad), ct, &mut out)?; + out.truncate(n); + Ok(out) +} + +fn enc_chunked(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8], chunk: usize) -> Vec { + let km = key_material(key); + let mut cipher = AsconAead128::new_encrypting(&km, nonce, ad_opt(ad)).unwrap(); + let mut out = vec![0u8; pt.len() + 16]; + out[..pt.len()].copy_from_slice(pt); + + let chunk = chunk.max(1); + let mut off = 0; + while off < pt.len() { + let end = (off + chunk).min(pt.len()); + cipher.do_encrypt_update(&mut out[off..end]); + off = end; + } + let tag = cipher.do_encrypt_final(); + out[pt.len()..].copy_from_slice(&tag); + out +} + +fn dec_chunked( + key: &[u8; 16], + nonce: &[u8; 16], + ad: &[u8], + ct: &[u8], + chunk: usize, +) -> Result, SymmetricCipherError> { + let km = key_material(key); + let mut cipher = AsconAead128::new_decrypting(&km, nonce, ad_opt(ad)).unwrap(); + let pt_len = ct.len() - 16; + let mut out = vec![0u8; pt_len]; + out.copy_from_slice(&ct[..pt_len]); + + let chunk = chunk.max(1); + let mut off = 0; + while off < pt_len { + let end = (off + chunk).min(pt_len); + cipher.do_decrypt_update(&mut out[off..end]); + off = end; + } + // infallible: ct.len() - pt_len == 16 by construction above. + let tag: [u8; 16] = ct[pt_len..].try_into().unwrap(); + cipher.do_decrypt_final(&tag)?; + Ok(out) +} + +/* -------------------------------------------------------------------------- */ +/* Embedded known-answer vectors */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead128_embedded_kat() { + // The NIST LWC AEAD KAT convention uses Key == Nonce == 000102…0F (i.e. KEY for both). + let kat_nonce = KEY; + for (pt_hex, ad_hex, ct_hex) in AEAD_KAT { + let pt = dh(pt_hex); + let ad = dh(ad_hex); + let expected_ct = dh(ct_hex); + + let got_ct = enc_oneshot(&KEY, &kat_nonce, &ad, &pt); + assert_eq!(got_ct, expected_ct, "encrypt mismatch for PT={pt_hex} AD={ad_hex}"); + + let got_pt = + dec_oneshot(&KEY, &kat_nonce, &ad, &expected_ct).expect("decrypt should succeed"); + assert_eq!(got_pt, pt, "decrypt mismatch for CT={ct_hex}"); + } +} + +/* -------------------------------------------------------------------------- */ +/* Round-trips and AAD handling */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_round_trip_sizes_and_ad() { + for &pt_len in PT_SIZES.iter() { + let pt = pattern(pt_len); + for ad in [Vec::new(), b"associated-data".to_vec(), pattern(40)] { + let ct = enc_oneshot(&KEY, &NONCE, &ad, &pt); + assert_eq!(ct.len(), pt_len + 16, "ciphertext = plaintext || 16-byte tag"); + let recovered = dec_oneshot(&KEY, &NONCE, &ad, &ct).expect("decrypt should succeed"); + assert_eq!(recovered, pt, "round-trip mismatch (pt_len={pt_len}, ad_len={})", ad.len()); + } + } +} + +#[test] +fn aead_aad_only_round_trip() { + // Empty plaintext, non-empty AD: ciphertext is just the 16-byte tag. + let ad = b"only-associated-data"; + let ct = enc_oneshot(&KEY, &NONCE, ad, b""); + assert_eq!(ct.len(), 16); + let recovered = dec_oneshot(&KEY, &NONCE, ad, &ct).expect("decrypt should succeed"); + assert!(recovered.is_empty()); +} + +/* -------------------------------------------------------------------------- */ +/* Streaming chunk-boundary equivalence */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_streaming_matches_one_shot() { + for &pt_len in PT_SIZES.iter() { + let pt = pattern(pt_len); + let ad = pattern(20); + let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); + + for &chunk in CHUNK_SIZES.iter() { + let ct = enc_chunked(&KEY, &NONCE, &ad, &pt, chunk); + assert_eq!(ct, ct_ref, "chunked encrypt mismatch (pt_len={pt_len}, chunk={chunk})"); + + let pt_back = dec_chunked(&KEY, &NONCE, &ad, &ct_ref, chunk) + .expect("chunked decrypt should pass"); + assert_eq!(pt_back, pt, "chunked decrypt mismatch (pt_len={pt_len}, chunk={chunk})"); + } + } +} + +#[test] +fn aead_chunked_aad_matches_one_shot() { + let pt = pattern(30); + let ad = pattern(40); + let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); + let km = key_material(&KEY); + + for &chunk in CHUNK_SIZES.iter() { + let mut e = AsconAead128::new_encrypting(&km, &NONCE, None).unwrap(); + for piece in ad.chunks(chunk) { + e.do_update_aad(piece).unwrap(); + } + let mut out = vec![0u8; pt.len() + 16]; + out[..pt.len()].copy_from_slice(&pt); + e.do_encrypt_update(&mut out[..pt.len()]); + let tag = e.do_encrypt_final(); + out[pt.len()..].copy_from_slice(&tag); + assert_eq!(out, ct_ref, "chunked AAD mismatch (chunk={chunk})"); + } +} + +/* -------------------------------------------------------------------------- */ +/* Streaming chunk sweep (this is what would have caught F1/F2) */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_streaming_chunk_sweep() { + let km = key_material(&KEY); + for pt_len in 0..=40 { + let pt = pattern(pt_len); + for ad_len in [0, 1, 15, 16, 17, 33] { + let ad = pattern(ad_len); + let ad_opt_ = ad_opt(&ad); + let ct_ref = enc_oneshot(&KEY, &NONCE, &ad, &pt); + let (ct_ref_body, tag_ref) = ct_ref.split_at(pt_len); + + for &chunk in [1, 2, 7, 15, 16, 17, 31, 32, 1024].iter() { + let mut e = AsconAead128::new_encrypting(&km, &NONCE, ad_opt_).unwrap(); + let mut out = pt.clone(); + let chunk = chunk.max(1); + let mut off = 0; + while off < out.len() { + let end = (off + chunk).min(out.len()); + e.do_encrypt_update(&mut out[off..end]); + off = end; + } + let tag = e.do_encrypt_final(); + assert_eq!(out, ct_ref_body, "pt_len={pt_len} ad_len={ad_len} chunk={chunk}"); + assert_eq!(tag, tag_ref, "pt_len={pt_len} ad_len={ad_len} chunk={chunk}"); + + let mut d = AsconAead128::new_decrypting(&km, &NONCE, ad_opt_).unwrap(); + let mut back = ct_ref_body.to_vec(); + let mut off = 0; + while off < back.len() { + let end = (off + chunk).min(back.len()); + d.do_decrypt_update(&mut back[off..end]); + off = end; + } + let tag_arr: [u8; 16] = tag_ref.try_into().unwrap(); + d.do_decrypt_final(&tag_arr).unwrap(); + assert_eq!(back, pt, "pt_len={pt_len} ad_len={ad_len} chunk={chunk}"); + } + } + } +} + +#[test] +fn do_decrypt_final_rejects_wrong_tag() { + let km = key_material(&KEY); + let pt = pattern(20); + let mut d = AsconAead128::new_decrypting(&km, &NONCE, None).unwrap(); + let mut buf = pt.clone(); + d.do_decrypt_update(&mut buf); + let wrong_tag = [0xFFu8; 16]; + assert!(matches!( + d.do_decrypt_final(&wrong_tag), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); +} + +/* -------------------------------------------------------------------------- */ +/* One-shot buffer-length contract */ +/* -------------------------------------------------------------------------- */ + +// The length checks in the inherent one-shots are never triggered by the tests above, which all +// size their own buffers correctly, so exercise each one directly -- including the two boundary +// cases that must NOT be rejected. +#[test] +fn aead128_undersized_buffers_are_rejected() { + let km = key_material(&KEY); + let msg = pattern(40); + + // encrypt: output buffer shorter than plaintext.len() + 16. + let mut too_small = vec![0u8; msg.len() + 15]; + match AsconAead128::encrypt(&km, &NONCE, None, &msg, &mut too_small) { + Err(SymmetricCipherError::OutputBufferTooSmall(needed)) => { + assert_eq!(needed, msg.len() + 16); + } + other => panic!("expected OutputBufferTooSmall, got {other:?}"), + } + + // decrypt: ciphertext shorter than the 16-byte tag, which is checked before the output buffer. + let short = [0u8; 8]; + let mut pt_buf = [0u8; 8]; + match AsconAead128::decrypt(&km, &NONCE, None, &short, &mut pt_buf) { + Err(SymmetricCipherError::GenericError(_)) => {} + other => panic!("expected GenericError, got {other:?}"), + } + + // decrypt: valid-length ciphertext, but an undersized plaintext buffer. + let ct = enc_oneshot(&KEY, &NONCE, &[], &msg); + let mut too_small_pt = vec![0u8; msg.len() - 1]; + match AsconAead128::decrypt(&km, &NONCE, None, &ct, &mut too_small_pt) { + Err(SymmetricCipherError::OutputBufferTooSmall(needed)) => { + assert_eq!(needed, msg.len()); + } + other => panic!("expected OutputBufferTooSmall, got {other:?}"), + } + + // A ciphertext of exactly 16 bytes -- an empty plaintext plus its tag -- is the boundary case + // and must decrypt, not be rejected as shorter than the tag. + let empty_ct = enc_oneshot(&KEY, &NONCE, &[], &[]); + assert_eq!(empty_ct.len(), 16); + let mut empty_pt_buf = [0u8; 0]; + assert_eq!(AsconAead128::decrypt(&km, &NONCE, None, &empty_ct, &mut empty_pt_buf).unwrap(), 0); + + // An output buffer larger than needed must succeed, with only the recovered bytes written. + let mut oversized_pt = vec![0xAAu8; msg.len() + 5]; + let n = AsconAead128::decrypt(&km, &NONCE, None, &ct, &mut oversized_pt).unwrap(); + assert_eq!(n, msg.len()); + assert_eq!(&oversized_pt[..n], &msg[..]); + assert_eq!(&oversized_pt[n..], &[0xAAu8; 5]); +} + +/* -------------------------------------------------------------------------- */ +/* Authentication failures */ +/* -------------------------------------------------------------------------- */ + +fn assert_auth_failed(result: Result, SymmetricCipherError>, ctx: &str) { + match result { + Err(SymmetricCipherError::AEADTagCheckFailed) => {} + other => panic!("{ctx}: expected AEADTagCheckFailed, got {other:?}"), + } +} + +#[test] +fn aead_rejects_tampering() { + let pt = pattern(50); + let ad = b"the-aad"; + let ct = enc_oneshot(&KEY, &NONCE, ad, &pt); + + // Wrong key. + let mut bad_key = KEY; + bad_key[0] ^= 0x01; + assert_auth_failed(dec_oneshot(&bad_key, &NONCE, ad, &ct), "wrong key"); + + // Wrong nonce. + let mut bad_nonce = NONCE; + bad_nonce[3] ^= 0x80; + assert_auth_failed(dec_oneshot(&KEY, &bad_nonce, ad, &ct), "wrong nonce"); + + // Modified associated data. + assert_auth_failed(dec_oneshot(&KEY, &NONCE, b"the-AAD", &ct), "modified ad"); + + // Flipped tag byte (last byte). + let mut tag_flip = ct.clone(); + let last = tag_flip.len() - 1; + tag_flip[last] ^= 0x01; + assert_auth_failed(dec_oneshot(&KEY, &NONCE, ad, &tag_flip), "flipped tag"); + + // Flipped ciphertext body byte. + let mut body_flip = ct.clone(); + body_flip[0] ^= 0x01; + assert_auth_failed(dec_oneshot(&KEY, &NONCE, ad, &body_flip), "flipped body"); +} + +#[test] +fn aead_tamper_leaves_no_plaintext_in_output_buffer() { + let pt = pattern(20); + let ad = b"ctx"; + let ct = enc_oneshot(&KEY, &NONCE, ad, &pt); + let mut tampered = ct.clone(); + tampered[0] ^= 0x01; + + let km = key_material(&KEY); + let mut out = vec![0xAAu8; pt.len()]; + let n = AsconAead128::decrypt(&km, &NONCE, ad_opt(ad), &tampered, &mut out); + assert!(matches!(n, Err(SymmetricCipherError::AEADTagCheckFailed))); + assert!(out.iter().all(|&b| b == 0), "output buffer must be zeroized on tag failure"); +} + +#[test] +fn aead_short_ciphertext_is_error() { + let short = [0u8; 8]; // shorter than the 16-byte tag + let km = key_material(&KEY); + let mut out = [0u8; 16]; + match AsconAead128::decrypt(&km, &NONCE, None, &short, &mut out) { + Err(SymmetricCipherError::GenericError(_)) => {} + other => panic!("expected GenericError for short ciphertext, got {other:?}"), + } +} + +/* -------------------------------------------------------------------------- */ +/* Determinism / nonce sensitivity / Debug mask */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_is_deterministic_and_nonce_sensitive() { + let pt = pattern(40); + let ad = b"ctx"; + let a = enc_oneshot(&KEY, &NONCE, ad, &pt); + let b = enc_oneshot(&KEY, &NONCE, ad, &pt); + assert_eq!(a, b, "same (key,nonce,ad,pt) must yield identical (ct,tag)"); + + let mut other_nonce = NONCE; + other_nonce[0] ^= 0x01; + let c = enc_oneshot(&KEY, &other_nonce, ad, &pt); + assert_ne!(a, c, "changing the nonce must change the ciphertext (SP 800-232 R3)"); +} + +#[test] +fn aead_debug_display_are_masked() { + let km = key_material(&KEY); + let e = AsconAead128::new_encrypting(&km, &NONCE, None).unwrap(); + assert!(format!("{e:?}").contains("masked")); + assert!(format!("{e}").contains("masked")); +} + +/* -------------------------------------------------------------------------- */ +/* Direction-misuse guards */ +/* -------------------------------------------------------------------------- */ + +#[test] +#[should_panic(expected = "decryptor")] +fn do_encrypt_update_on_decryptor_panics() { + let km = key_material(&KEY); + let mut d = AsconAead128::new_decrypting(&km, &NONCE, None).unwrap(); + let mut buf = [0u8; 4]; + d.do_encrypt_update(&mut buf); +} + +#[test] +#[should_panic(expected = "encryptor")] +fn do_decrypt_update_on_encryptor_panics() { + let km = key_material(&KEY); + let mut e = AsconAead128::new_encrypting(&km, &NONCE, None).unwrap(); + let mut buf = [0u8; 4]; + e.do_decrypt_update(&mut buf); +} + +/* -------------------------------------------------------------------------- */ +/* Trait conformance (shared core-test-framework) */ +/* -------------------------------------------------------------------------- */ + +/// Exercises [`AEADCipherEncryptor`]/[`AEADCipherDecryptor`], the streaming pair +/// [`AsconAead128Encryptor`]/[`AsconAead128Decryptor`] adapt [`AsconAead128`] to: `update_out_len` +/// correctness, chunking-independence of both AAD and data, the AAD-after-data `StateError`, and +/// tamper detection, all against the generic conformance suite rather than hand-written here. +/// +/// [`AEADCipherEncryptor`]: bouncycastle_core::traits::AEADCipherEncryptor +/// [`AEADCipherDecryptor`]: bouncycastle_core::traits::AEADCipherDecryptor +#[test] +fn aead128_encryptor_decryptor_trait_framework() { + TestFrameworkAEADCipher::new() + .test_encryptor_decryptor::<16, 16, 16, 16, AsconAead128Encryptor, AsconAead128Decryptor>(); +} + +/// The same conformance suite through [`Ascon_AEAD128`], which must resolve to the same pair. +/// +/// [`Ascon_AEAD128`]: bouncycastle_ascon::Ascon_AEAD128 +#[test] +fn aead128_dir_alias_trait_framework() { + use bouncycastle_ascon::Ascon_AEAD128; + use bouncycastle_modes::{Decrypting, Encrypting}; + TestFrameworkAEADCipher::new().test_encryptor_decryptor::< + 16, + 16, + 16, + 16, + Ascon_AEAD128, + Ascon_AEAD128, + >(); +} + +#[test] +fn aead_framework_buffering_toy() { + TestFrameworkAEADCipher::new().test_buffering_toy(); +} + +/// The two tag layouts must agree byte for byte: `direct_ciphertext || direct_tag`, produced by +/// streaming [`AsconAead128Encryptor`] and taking the tag from `do_final_out_detached`, must equal what +/// the inline layout produces for the same key, nonce (driven by the same RNG stream), AAD and +/// message -- through both `encrypt_out_with_aad` and the inherited `do_final` -- and either must +/// decrypt back to the original plaintext. +#[test] +fn aead128_tagged_and_direct_layouts_agree() { + use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, + }; + use bouncycastle_core_test_framework::FixedSeedRNG; + + let km = key_material(&KEY); + let aad = b"tagged-layout-aad"; + for pt_len in [0usize, 1, 15, 16, 17, 40] { + let pt = pattern(pt_len); + let pinned = [0x11u8; 16]; + + // detached tag, streamed + let (mut direct_enc, direct_nonce) = + AsconAead128Encryptor::do_encrypt_init_rng(&km, &mut FixedSeedRNG::<16>::new(pinned)) + .unwrap(); + direct_enc.do_update_aad(aad).unwrap(); + let mut direct_ct = vec![0u8; pt.len()]; + direct_enc.do_update_out(&pt, &mut direct_ct).unwrap(); + let mut unused = [0u8; 16]; + let (flushed, direct_tag) = direct_enc.do_final_out_detached(&mut unused).unwrap(); + assert_eq!(flushed, 0, "Ascon-AEAD128 holds nothing back to flush"); + let mut direct_inline = direct_ct.clone(); + direct_inline.extend_from_slice(&direct_tag); + + // inline tag, streamed + let (mut tagged_enc, tagged_nonce) = + AsconAead128Encryptor::do_encrypt_init_rng(&km, &mut FixedSeedRNG::<16>::new(pinned)) + .unwrap(); + tagged_enc.do_update_aad(aad).unwrap(); + let mut tagged_out = vec![0u8; AsconAead128Encryptor::encrypt_out_len(pt.len())]; + let written = tagged_enc.do_update_out(&pt, &mut tagged_out).unwrap(); + let mut last = [0u8; 16]; + let last_len = tagged_enc.do_final_out(&mut last).unwrap(); + tagged_out[written..written + last_len].copy_from_slice(&last[..last_len]); + tagged_out.truncate(written + last_len); + + assert_eq!(direct_nonce, tagged_nonce, "pt_len {pt_len}: same RNG stream, same nonce"); + assert_eq!(direct_inline, tagged_out, "pt_len {pt_len}: inline layout must agree"); + + // inline tag, one-shot: its own generated nonce, so what must match is the round trip + // and the length, not the bytes. + let mut one_shot = vec![0u8; AsconAead128Encryptor::encrypt_out_len(pt.len())]; + let (one_nonce, one_len) = + AsconAead128Encryptor::encrypt_out_with_aad(&km, aad, &pt, &mut one_shot).unwrap(); + assert_eq!(one_len, tagged_out.len(), "pt_len {pt_len}: one-shot writes the same length"); + let mut one_back = vec![0u8; AsconAead128Decryptor::decrypt_out_max_len(one_len)]; + let one_n = AsconAead128Decryptor::decrypt_out_with_aad( + &km, + &one_nonce, + aad, + &one_shot[..one_len], + &mut one_back, + ) + .unwrap(); + assert_eq!(&one_back[..one_n], &pt[..], "pt_len {pt_len}: one-shot round trip"); + + // ...and all of it decrypts back, each through its own view. The decryptor holds the + // last 16 bytes back either way; detached, `do_final_out_detached` releases them. + let mut direct_dec = AsconAead128Decryptor::do_decrypt_init(&km, &direct_nonce).unwrap(); + direct_dec.do_update_aad(aad).unwrap(); + let mut direct_pt = vec![0u8; direct_ct.len()]; + let got = direct_dec.do_update_out(&direct_ct, &mut direct_pt).unwrap(); + assert_eq!(got, pt_len.saturating_sub(16), "pt_len {pt_len}: the last 16 bytes are held"); + let mut last = [0u8; 16]; + let last_len = direct_dec.do_final_out_detached(&direct_tag, &mut last).unwrap(); + assert_eq!(got + last_len, pt_len, "pt_len {pt_len}: detached final releases the rest"); + direct_pt[got..].copy_from_slice(&last[..last_len]); + assert_eq!(direct_pt, pt, "pt_len {pt_len}: direct decrypt round trip"); + + let mut tagged_dec = AsconAead128Decryptor::do_decrypt_init(&km, &tagged_nonce).unwrap(); + tagged_dec.do_update_aad(aad).unwrap(); + let mut tagged_pt = vec![0u8; tagged_out.len()]; + let got = tagged_dec.do_update_out(&tagged_out, &mut tagged_pt).unwrap(); + assert_eq!(got, pt_len, "pt_len {pt_len}: all but the tag is released"); + let (_, data_len) = tagged_dec.do_final().unwrap(); + assert_eq!(data_len, 0, "pt_len {pt_len}: nothing but the tag was held back"); + assert_eq!(&tagged_pt[..got], &pt[..], "pt_len {pt_len}: tagged decrypt round trip"); + + let mut one_pt = vec![0u8; AsconAead128Decryptor::decrypt_out_max_len(tagged_out.len())]; + let n = AsconAead128Decryptor::decrypt_out_with_aad( + &km, &tagged_nonce, aad, &tagged_out, &mut one_pt, + ) + .unwrap(); + assert_eq!(&one_pt[..n], &pt[..], "pt_len {pt_len}: streamed ciphertext, one-shot decrypt"); + } +} + +/// With no associated data, the pair used purely as a [`SymmetricCipherEncryptor`] / +/// [`SymmetricCipherDecryptor`] -- nonce driven to the KAT's by a fixed RNG -- reproduces the +/// embedded NIST LWC vectors' `ciphertext || tag`, and decrypts them back. +/// +/// [`SymmetricCipherEncryptor`]: bouncycastle_core::traits::SymmetricCipherEncryptor +/// [`SymmetricCipherDecryptor`]: bouncycastle_core::traits::SymmetricCipherDecryptor +#[test] +fn aead128_symmetric_cipher_view_matches_kat() { + use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; + use bouncycastle_core_test_framework::FixedSeedRNG; + + let km = key_material(&KEY); + // The NIST LWC AEAD KAT convention uses Key == Nonce == 000102…0F (i.e. KEY for both). + let kat_nonce = KEY; + let mut tested = 0; + for (pt_hex, ad_hex, ct_hex) in AEAD_KAT.iter().filter(|(_, ad, _)| ad.is_empty()) { + let pt = dh(pt_hex); + let expected = dh(ct_hex); + assert!(dh(ad_hex).is_empty()); + + let mut ct = vec![0u8; AsconAead128Encryptor::encrypt_out_len(pt.len())]; + let (nonce, n) = AsconAead128Encryptor::encrypt_out_rng( + &km, + &mut FixedSeedRNG::<16>::new(kat_nonce), + &pt, + &mut ct, + ) + .unwrap(); + assert_eq!(nonce, kat_nonce); + assert_eq!(&ct[..n], &expected[..], "pt {pt_hex}: SymmetricCipherEncryptor view vs KAT"); + + let recovered = AsconAead128Decryptor::decrypt(&km, &kat_nonce, &expected).unwrap(); + assert_eq!(recovered, pt, "pt {pt_hex}: SymmetricCipherDecryptor view vs KAT"); + tested += 1; + } + assert!(tested >= 2, "the embedded KATs must include no-AD vectors"); +} + +#[test] +fn aead128_suspendable_keyed_state() { + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::SuspendableKeyed; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; + + let pt = pattern(40); + let ad = b"suspend-ad"; + let ct_ref = enc_oneshot(&KEY, &NONCE, ad, &pt); + let km = key_material(&KEY); + + // Encrypt part of the plaintext, suspend, resume with the re-supplied key, finish, and confirm + // the output matches a one-shot encryption. The key is never part of the serialized state. + let mut e = AsconAead128::new_encrypting(&km, &NONCE, Some(ad)).unwrap(); + let mut out = vec![0u8; pt.len() + 16]; + out[..pt.len()].copy_from_slice(&pt); + e.do_encrypt_update(&mut out[..18]); + + TestFrameworkSuspendableKeyedState::new().test(&e, &km); + + let serialized = e.clone().suspend(); + let mut resumed = AsconAead128::from_suspended(serialized, &km).unwrap(); + resumed.do_encrypt_update(&mut out[18..pt.len()]); + let tag = resumed.do_encrypt_final(); + out[pt.len()..].copy_from_slice(&tag); + assert_eq!(out, ct_ref, "resumed AEAD ciphertext must match one-shot encryption"); + + // A corrupted state tag must be rejected (the tag is the byte after the 3-byte version prefix). + let mut busted = serialized; + busted[3] ^= 0xFF; + assert!(matches!( + AsconAead128::from_suspended(busted, &km), + Err(SuspendableError::InvalidData) + )); + + // An unknown call-state discriminant must be rejected. + let last = serialized.len() - 1; + let pos_offset = serialized.len() - 2; + let mut bad_state = serialized; + bad_state[last] = 200; + assert!(matches!( + AsconAead128::from_suspended(bad_state, &km), + Err(SuspendableError::InvalidData) + )); + + // A nonzero byte position while still in an *Init state must be rejected. + let mut inconsistent = serialized; + inconsistent[pos_offset] = 3; // pos = 3 + inconsistent[last] = 0; // EncInit + assert!(matches!( + AsconAead128::from_suspended(inconsistent, &km), + Err(SuspendableError::InvalidData) + )); + + // pos >= RATE (16) must be rejected. + let mut bad_pos = serialized; + bad_pos[pos_offset] = 16; + assert!(matches!( + AsconAead128::from_suspended(bad_pos, &km), + Err(SuspendableError::InvalidData) + )); +} diff --git a/crypto/ascon/tests/bc_test_data.rs b/crypto/ascon/tests/bc_test_data.rs new file mode 100644 index 00000000..f7fc0826 --- /dev/null +++ b/crypto/ascon/tests/bc_test_data.rs @@ -0,0 +1,278 @@ +//! Test against the bc-test-data repo. +//! Requires that the bc-test-data repository is cloned and available for testing at +//! "../bc-test-data" relative to the root of this git project (or "../../../bc-test-data" relative +//! to this crate). When the repo is absent these tests print a warning and are skipped. +//! +//! The NIST SP 800-232 ASCON known-answer test (KAT) vectors live under +//! `bc-test-data/crypto/ascon//`. These full sweeps (1025–1089 cases each) complement the +//! small embedded vector sets in the per-primitive test files. + +#[cfg(test)] +mod bc_test_data { + use bouncycastle_ascon::ascon_aead128::AsconAead128; + use bouncycastle_ascon::ascon_cxof128::AsconCXof128; + use bouncycastle_ascon::ascon_hash256::AsconHash256; + use bouncycastle_ascon::ascon_xof128::AsconXof128; + use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, + }; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::XOF; + use bouncycastle_hex as hex; + use std::collections::BTreeMap; + use std::fs; + use std::path::Path; + use std::sync::Once; + + const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto/ascon"; + const TEST_DATA_PATH: &str = "../bc-test-data/crypto/ascon"; + + static TEST_DATA_CHECK: Once = Once::new(); + + fn get_test_data(filename: &str) -> Result { + let found: u8; + + if Path::new(TEST_DATA_PATH_RELATIVE).exists() { + found = 1; + } else if Path::new(TEST_DATA_PATH).exists() { + found = 2; + } else { + found = 3; + } + + // just print once + TEST_DATA_CHECK.call_once(|| match found { + 1 => println!("bc-test-data found at: {:?}", TEST_DATA_PATH_RELATIVE), + 2 => println!("bc-test-data found at: {:?}", TEST_DATA_PATH), + _ => println!("WARNING: bc-test-data directory not found; tests will be skipped"), + }); + + let contents = if Path::new(TEST_DATA_PATH_RELATIVE).exists() { + fs::read_to_string(TEST_DATA_PATH_RELATIVE.to_string() + "/" + filename).unwrap() + } else if Path::new(TEST_DATA_PATH).exists() { + fs::read_to_string(TEST_DATA_PATH.to_string() + "/" + filename).unwrap() + } else { + return Err(()); + }; + + Ok(contents) + } + + fn decode_hex(value: &str) -> Vec { + let clean = value.trim(); + + if clean.is_empty() { Vec::new() } else { hex::decode(clean).expect("valid hex") } + } + + /// Parse a NIST LWC KAT file: blank-line-delimited `Tag = Value` cases. + fn parse_kat(contents: &str) -> Vec> { + let mut cases = Vec::new(); + let mut current = BTreeMap::new(); + + for raw in contents.lines() { + let line = raw.trim(); + + if line.is_empty() { + if !current.is_empty() { + cases.push(std::mem::take(&mut current)); + } + continue; + } + + if line.starts_with('#') { + continue; + } + + if let Some((key, value)) = line.split_once('=') { + let key = key.trim().to_string(); + let value = value.trim().to_string(); + + if key == "Count" && !current.is_empty() { + cases.push(std::mem::take(&mut current)); + } + + current.insert(key, value); + } + } + + if !current.is_empty() { + cases.push(current); + } + + cases + } + + fn field<'a>(case: &'a BTreeMap, names: &[&str]) -> &'a str { + for name in names { + if let Some(v) = case.get(*name) { + return v.as_str(); + } + } + + panic!("missing field {names:?}; case had {:?}", case.keys().collect::>()); + } + + fn to_16(bytes: &[u8], what: &str) -> [u8; 16] { + bytes.try_into().unwrap_or_else(|_| panic!("{what} must be 16 bytes, got {}", bytes.len())) + } + + /// Build a `KeyMaterial<16>` for a KAT key. The NIST LWC vectors include an all-zero key + /// (Count=1), which `KeyMaterial::from_bytes_as_type` would otherwise tag + /// `KeyType::Zeroized` / `SecurityStrength::None`; force the type/strength the way a caller + /// who knows the provenance of the key would (see `cli/src/helpers.rs::parse_seed`). + fn key_material(key: &[u8; 16]) -> KeyMaterial<16> { + let mut km = + KeyMaterial::<16>::from_bytes_as_type(key, KeyType::SymmetricCipherKey).unwrap(); + + do_hazardous_operations(&mut km, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::_128bit) + }) + .unwrap(); + + km + } + + #[test] + fn ascon_aead128_kat() { + let contents = match get_test_data("asconaead128/LWC_AEAD_KAT_128_128.txt") { + Ok(c) => c, + Err(()) => return, + }; + + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no AEAD cases parsed"); + + for case in &cases { + let key = key_material(&to_16(&decode_hex(field(case, &["Key", "K"])), "key")); + let nonce = to_16(&decode_hex(field(case, &["Nonce", "N"])), "nonce"); + let ad = decode_hex(field(case, &["AD", "A"])); + let pt = decode_hex(field(case, &["PT", "P"])); + let expected_ct = decode_hex(field(case, &["CT", "C"])); + + let ad_opt = if ad.is_empty() { None } else { Some(ad.as_slice()) }; + + // One-shot encrypt. + let mut ct = vec![0u8; pt.len() + 16]; + let n = AsconAead128::encrypt(&key, &nonce, ad_opt, &pt, &mut ct).unwrap(); + ct.truncate(n); + + assert_eq!(ct, expected_ct, "encrypt mismatch (Count {})", field(case, &["Count"])); + + // One-shot decrypt round-trip. + let mut pt_out = vec![0u8; expected_ct.len()]; + let m = AsconAead128::decrypt(&key, &nonce, ad_opt, &expected_ct, &mut pt_out) + .expect("decrypt should authenticate"); + + pt_out.truncate(m); + + assert_eq!(pt_out, pt, "decrypt mismatch (Count {})", field(case, &["Count"])); + + // Byte-at-a-time streaming encrypt/decrypt, through the inherent API. + let mut enc = AsconAead128::new_encrypting(&key, &nonce, ad_opt).unwrap(); + let mut stream_ct = pt.clone(); + + for byte in stream_ct.iter_mut() { + enc.do_encrypt_update(core::slice::from_mut(byte)); + } + + let tag = enc.do_encrypt_final(); + stream_ct.extend_from_slice(&tag); + + assert_eq!( + stream_ct, + expected_ct, + "streaming encrypt mismatch (Count {})", + field(case, &["Count"]) + ); + + let mut dec = AsconAead128::new_decrypting(&key, &nonce, ad_opt).unwrap(); + let mut stream_pt = expected_ct[..pt.len()].to_vec(); + + for byte in stream_pt.iter_mut() { + dec.do_decrypt_update(core::slice::from_mut(byte)); + } + + dec.do_decrypt_final(&tag).expect("streaming decrypt should authenticate"); + + assert_eq!( + stream_pt, + pt, + "streaming decrypt mismatch (Count {})", + field(case, &["Count"]) + ); + } + + println!("Ascon-AEAD128: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_hash256_kat() { + let contents = match get_test_data("asconhash256/LWC_HASH_KAT_256.txt") { + Ok(c) => c, + Err(()) => return, + }; + + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no Hash256 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let expected = decode_hex(field(case, &["MD"])); + + assert_eq!( + AsconHash256::digest(&msg).as_slice(), + expected.as_slice(), + "Hash256 mismatch (Count {})", + field(case, &["Count"]) + ); + } + + println!("Ascon-Hash256: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_xof128_kat() { + let contents = match get_test_data("asconxof128/LWC_XOF_KAT_128_512.txt") { + Ok(c) => c, + Err(()) => return, + }; + + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no XOF128 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let expected = decode_hex(field(case, &["MD", "Output"])); + + let got = AsconXof128::new().xof(&msg, expected.len()); + + assert_eq!(got, expected, "XOF128 mismatch (Count {})", field(case, &["Count"])); + } + + println!("Ascon-XOF128: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_cxof128_kat() { + let contents = match get_test_data("asconcxof128/LWC_CXOF_KAT_128_512.txt") { + Ok(c) => c, + Err(()) => return, + }; + + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no CXOF128 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let z = decode_hex(field(case, &["Z", "Customization"])); + let expected = decode_hex(field(case, &["MD", "Output"])); + + let got = AsconCXof128::with_customization(&z).unwrap().xof(&msg, expected.len()); + + assert_eq!(got, expected, "CXOF128 mismatch (Count {})", field(case, &["Count"])); + } + + println!("Ascon-CXOF128: {} KAT cases passed", cases.len()); + } +} diff --git a/crypto/ascon/tests/cxof128_tests.rs b/crypto/ascon/tests/cxof128_tests.rs new file mode 100644 index 00000000..d641bc5c --- /dev/null +++ b/crypto/ascon/tests/cxof128_tests.rs @@ -0,0 +1,336 @@ +//! Ascon-CXOF128 tests (NIST SP 800-232 §5.3). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus +//! domain-separation, streaming/byte-at-a-time equivalence, trait-API, partial-input rejection, +//! and suspend/resume tests. + +use bouncycastle_ascon::ascon_cxof128::{AsconCXof128, AsconCXof128Squeezer}; +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Hash, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; +use bouncycastle_hex as hex; + +/// Embedded NIST LWC Ascon-CXOF128 vectors `(message, customization Z, 512-bit output)` in hex, +/// spanning empty/non-empty customization and message. (Counts 1, 2, 3, 35, 36 of +/// LWC_CXOF_KAT_128_512.txt; each output is 64 bytes.) +const CXOF_KAT: &[(&str, &str, &str)] = &[ + ( + "", + "", + "4F50159EF70BB3DAD8807E034EAEBD44C4FA2CBBC8CF1F05511AB66CDCC529905CA12083FC186AD899B270B1473DC5F7EC88D1052082DCDFE69FB75D269E7B74", + ), + ( + "", + "10", + "0C93A483E7D574D49FE52CCE03EE646117977D57A8AA57704AB4DAF44B501430FF6AC11A5D1FD6F2154B5C65728268270C8BB578508487B8965718ADA6272FD6", + ), + ( + "", + "1011", + "D1106C7622E79FE955BD9D79E03B918E770FE0E0CDDDE28BEB924B02C5FC936B33ACCA299C89ECA5D71886CBBFA4D54A21C55FDE2B679F5E2488063A1719DC32", + ), + ( + "00", + "10", + "63FA8BA86382F2D544580F51322D080424B42C556EB74503CD73CF052BB993BD6F5210984C71C9C445F43CCC5B158226E509BD339CD634414377F79411AA8D5C", + ), + ( + "00", + "1011", + "DF7909DD1F371E54ABBABB50DDEE195720D7EF1BB2CF2271C36A76C19908178BA3255E5A3D31D994C1D217A67AE4D13681AC1ABC4FAA2ECDD1681520BC7D7347", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +#[test] +fn cxof128_embedded_kat() { + for (msg_hex, z_hex, md_hex) in CXOF_KAT { + let msg = dh(msg_hex); + let z = dh(z_hex); + let expected = dh(md_hex); + + let got = AsconCXof128::with_customization(&z).unwrap().xof(&msg, expected.len()); + + assert_eq!(got, expected, "msg={msg_hex} z={z_hex}"); + + // AsconCXof128::default() uses an empty customization string, so the generic XOF + // framework, which constructs a fresh value itself, only applies directly to empty-Z + // vectors. Non-empty customization is exercised explicitly by the other tests below. + if z.is_empty() { + let mut framework = TestFrameworkXOF::new(); + + // SP 800-232 Ascon-CXOF128 operates on byte strings in this implementation, so + // non-byte-aligned final input is deliberately unsupported. + framework.enable_partial_byte_tests = false; + + framework.test_xof(AsconCXof128::new, &msg, &expected); + } + } +} + +#[test] +fn cxof128_domain_separation() { + let msg = pattern(48); + + let out_z1 = AsconCXof128::with_customization(b"context-1").unwrap().xof(&msg, 64); + + let out_z2 = AsconCXof128::with_customization(b"context-2").unwrap().xof(&msg, 64); + + assert_ne!(out_z1, out_z2, "different customization strings must give different output"); + + // Empty-customization CXOF128 must differ from XOF128 because the two functions use + // different initialization/domain separation. + let cxof_empty = AsconCXof128::new().xof(&msg, 64); + let xof = AsconXof128::new().xof(&msg, 64); + + assert_ne!(cxof_empty, xof, "CXOF128 (empty Z) must differ from XOF128"); +} + +#[test] +fn cxof128_prefix_property_and_streaming() { + let z = b"cust"; + let msg = pattern(70); + + let full = AsconCXof128::with_customization(z).unwrap().xof(&msg, 100); + + // Reading from one squeezer in several calls must produce exactly the same continuous + // stream as requesting the whole output in one shot. + let mut x = AsconCXof128::with_customization(z).unwrap(); + x.do_update(&msg); + let mut squeezer = x.into_squeezer(); + + let mut piecewise = Vec::new(); + + for n in [30usize, 40, 30] { + let mut part = vec![0u8; n]; + let written = squeezer.do_output_out(&mut part); + + assert_eq!(written, n); + piecewise.extend_from_slice(&part); + } + + assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + + // Absorbing the message in chunks must equal absorbing it in one call. + for chunk in [1usize, 8, 9, 64] { + let mut xc = AsconCXof128::with_customization(z).unwrap(); + + for piece in msg.chunks(chunk) { + xc.do_update(piece); + } + + let mut got = vec![0u8; 100]; + let written = xc.into_squeezer().do_output_out(&mut got); + + assert_eq!(written, got.len()); + assert_eq!(got, full, "chunked absorb mismatch (chunk={chunk})"); + } +} + +#[test] +fn cxof128_hash_view_metadata() { + let x = AsconCXof128::new(); + + assert_eq!(x.block_bitlen(), 64); + assert_eq!(x.output_len(), 32); + assert_eq!(x.hash(b"").len(), 32); +} + +#[test] +fn cxof128_byte_at_a_time_matches_one_shot() { + let msg = pattern(40); + + let reference = AsconCXof128::with_customization(b"zz").unwrap().xof(&msg, 48); + + let mut c = AsconCXof128::with_customization(b"zz").unwrap(); + + for &b in &msg { + c.do_update(&[b]); + } + + let mut out = [0u8; 48]; + let written = c.into_squeezer().do_output_out(&mut out); + + assert_eq!(written, out.len()); + assert_eq!(out.to_vec(), reference, "CXOF128 byte-at-a-time absorb mismatch"); +} + +#[test] +fn cxof128_unsupported_partial_input_returns_err() { + // num_bits == 0 means there is no partial byte and must behave exactly like ordinary + // finalization / into_squeezer. + assert!(AsconCXof128::new().into_squeezer_partial_bits(0xFF, 0).is_ok()); + + assert!(AsconCXof128::new().do_final_partial_bits(0x80, 0).is_ok()); + + for num_bits in [3usize, 7] { + assert!(matches!( + AsconCXof128::new().into_squeezer_partial_bits(0xA0, num_bits), + Err(HashError::InvalidInput(_)) + )); + + assert!(matches!( + AsconCXof128::new().do_final_partial_bits(0xA0, num_bits), + Err(HashError::InvalidInput(_)) + )); + + let mut out = [0u8; 32]; + + assert!(matches!( + AsconCXof128::new().do_final_partial_bits_out(0xA0, num_bits, &mut out), + Err(HashError::InvalidInput(_)) + )); + } + + for num_bits in [8usize, 9] { + assert!(matches!( + AsconCXof128::new().into_squeezer_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + )); + + assert!(matches!( + AsconCXof128::new().do_final_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + )); + + let mut out = [0u8; 32]; + + assert!(matches!( + AsconCXof128::new().do_final_partial_bits_out(0xFF, num_bits, &mut out), + Err(HashError::InvalidLength(_)) + )); + } +} + +#[test] +fn cxof128_absorb_then_squeeze_type_transition() { + let mut x = AsconCXof128::with_customization(b"z").unwrap(); + x.do_update(b"data"); + + let mut squeezer = x.into_squeezer(); + + let first = squeezer.do_output(8); + let second = squeezer.do_output(8); + + let whole = AsconCXof128::with_customization(b"z").unwrap().xof(b"data", 16); + + assert_eq!( + [first, second].concat(), + whole, + "successive reads must continue the same XOF stream" + ); + + // There is deliberately no "absorb after squeeze" runtime test anymore. + // `into_squeezer()` consumes the AsconCXof128, and the returned squeezer does not implement + // Hash::do_update, so that invalid state is prevented by the type system. +} + +#[test] +fn cxof128_suspendable_state() { + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; + + let z = b"customization"; + let data: Vec = (0..30u8).collect(); + + // Reference: uninterrupted absorb + squeeze under the same customization string. + let mut reference = AsconCXof128::with_customization(z).unwrap(); + reference.do_update(&data); + + let mut expected = [0u8; 40]; + reference.into_squeezer().do_output_out(&mut expected); + + // Suspend in the absorbing phase, resume, finish the remaining input, and confirm that + // the output matches the uninterrupted computation. The customization string has already + // been folded into the sponge state at construction time. + let mut x = AsconCXof128::with_customization(z).unwrap(); + x.do_update(&data[..5]); + + TestFrameworkSuspendableState::new().test(&x); + + let serialized = x.clone().suspend(); + + let mut resumed = AsconCXof128::from_suspended(serialized).unwrap(); + resumed.do_update(&data[5..]); + + let mut out = [0u8; 40]; + resumed.into_squeezer().do_output_out(&mut out); + + assert_eq!(out, expected, "resumed CXOF output must match uninterrupted output"); + + // A corrupted state tag must be rejected. + let mut busted = serialized; + busted[3] ^= 0xFF; + + assert!(matches!(AsconCXof128::from_suspended(busted), Err(SuspendableError::InvalidData))); + + // Cross-type guard: an Ascon-XOF128 state has the same serialized length but a different + // state tag, so Ascon-CXOF128 must reject it. + let mut xof = AsconXof128::new(); + xof.do_update(&data); + + let xof_state = xof.suspend(); + + assert!(matches!(AsconCXof128::from_suspended(xof_state), Err(SuspendableError::InvalidData))); + + // An inconsistent buf_pos/squeezing combination must be rejected: buf_pos == RATE (8) + // is only valid after squeezing has begun. + let mut bad = serialized; + let len = bad.len(); + + bad[len - 2] = 8; + bad[len - 1] = 0; + + assert!(matches!(AsconCXof128::from_suspended(bad), Err(SuspendableError::InvalidData))); + + // Suspend after squeezing has actually begun and confirm that restoring the squeezer + // continues the same stream. + let mut sq = AsconCXof128::with_customization(z).unwrap(); + sq.do_update(&data); + + let mut sq = sq.into_squeezer(); + + let mut head = [0u8; 5]; + sq.do_output_out(&mut head); + + let squeezing_state = sq.clone().suspend(); + + // A squeezing state belongs to AsconCXof128Squeezer, not the absorbing AsconCXof128 type. + assert!(matches!( + AsconCXof128::from_suspended(squeezing_state), + Err(SuspendableError::InvalidData) + )); + + let mut resumed_sq = AsconCXof128Squeezer::from_suspended(squeezing_state).unwrap(); + + let mut tail = [0u8; 35]; + resumed_sq.do_output_out(&mut tail); + + let mut combined = Vec::new(); + combined.extend_from_slice(&head); + combined.extend_from_slice(&tail); + + assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); +} + +#[test] +fn cxof128_customization_length_bound() { + // SP 800-232 §5.3: the customization string shall be at most 2048 bits (256 bytes). + let ok = vec![0u8; 256]; + + assert!(AsconCXof128::with_customization(&ok).is_ok()); + + let too_long = vec![0u8; 257]; + + assert!(matches!(AsconCXof128::with_customization(&too_long), Err(HashError::InvalidInput(_)))); +} diff --git a/crypto/ascon/tests/hash256_tests.rs b/crypto/ascon/tests/hash256_tests.rs new file mode 100644 index 00000000..8e6ee545 --- /dev/null +++ b/crypto/ascon/tests/hash256_tests.rs @@ -0,0 +1,152 @@ +//! Ascon-Hash256 tests (NIST SP 800-232 §5.1). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus +//! streaming-equivalence, one-shot/trait-API, metadata, and unsupported-partial-op tests. + +use bouncycastle_ascon::ascon_hash256::AsconHash256; +use bouncycastle_core::traits::{Hash, HashAlgParams}; +use bouncycastle_core_test_framework::hash::TestFrameworkHash; +use bouncycastle_hex as hex; + +/// Embedded NIST LWC Ascon-Hash256 vectors `(message, digest)` in hex, spanning empty, sub-block, +/// exact-block, and multi-block messages. (Counts 1, 2, 9, 17, 33 of LWC_HASH_KAT_256.txt.) +const HASH_KAT: &[(&str, &str)] = &[ + ("", "0B3BE5850F2F6B98CAF29F8FDEA89B64A1FA70AA249B8F839BD53BAA304D92B2"), + ("00", "0728621035AF3ED2BCA03BF6FDE900F9456F5330E4B5EE23E7F6A1E70291BC80"), + ("0001020304050607", "B88E497AE8E6FB641B87EF622EB8F2FCA0ED95383F7FFEBE167ACF1099BA764F"), + ( + "000102030405060708090A0B0C0D0E0F", + "3158C1940A2FBADBD68AB661777859B94A689E4EFC375911467ADDD641835C38", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "BD9D3D60A66B53868EAB2A5C74539A518A1F60F01EB176C60E43DEE81680B33E", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +#[test] +fn hash256_embedded_kat() { + for (msg_hex, md_hex) in HASH_KAT { + let msg = dh(msg_hex); + let expected = dh(md_hex); + assert_eq!(AsconHash256::digest(&msg).as_slice(), expected.as_slice(), "msg={msg_hex}"); + + // AsconHash256 has no do_final_partial_bits support, so that part of the framework + // is disabled; everything else (hash/hash_out/do_update+do_final(_out), truncation, + // oversized-buffer zero-fill) is exercised here. + TestFrameworkHash { enable_partial_byte_tests: false } + .test_hash::(&msg, &expected); + } +} + +#[test] +fn hash256_streaming_matches_one_shot() { + let msg = pattern(100); + let expected = AsconHash256::digest(&msg); + + // One-shot APIs agree. + assert_eq!(AsconHash256::new().hash(&msg), expected.to_vec()); + let mut buf = [0u8; 32]; + let mut h = AsconHash256::new(); + h.do_update(&msg); + h.do_final_out(&mut buf); + assert_eq!(buf, expected); + + // Chunked do_update agrees for a range of chunk sizes. + for chunk in [1usize, 7, 8, 9, 16, 33] { + let mut hasher = AsconHash256::new(); + for piece in msg.chunks(chunk) { + hasher.do_update(piece); + } + let mut got = [0u8; 32]; + hasher.do_final_out(&mut got); + assert_eq!(got, expected, "chunked hash mismatch (chunk={chunk})"); + } + + // Byte-at-a-time do_update() agrees. + let mut hasher = AsconHash256::new(); + for &b in &msg { + hasher.do_update(&[b]); + } + let mut got = [0u8; 32]; + hasher.do_final_out(&mut got); + assert_eq!(got, expected, "byte-at-a-time hash mismatch"); +} + +#[test] +fn hash256_metadata_accessors() { + assert_eq!(AsconHash256::OUTPUT_LEN, 32); + let h = AsconHash256::new(); + assert_eq!(h.output_len(), 32); + assert_eq!(h.block_bitlen(), 64); +} + +#[test] +fn hash256_do_final_out_truncates_to_buffer() { + let msg = pattern(50); + let expected = AsconHash256::digest(&msg); + + let mut h = AsconHash256::new(); + h.do_update(&msg); + let mut o = [0u8; 16]; + assert_eq!(h.do_final_out(&mut o), 16); + assert_eq!(o, expected[..16]); +} + +#[test] +fn hash256_hash_out_zeroizes_past_output_len() { + let msg = pattern(50); + let expected = AsconHash256::digest(&msg); + + let mut o = [0xEEu8; 64]; + assert_eq!(AsconHash256::new().hash_out(&msg, &mut o), 32); + assert_eq!(&o[..32], &expected[..]); + assert_eq!(&o[32..], &[0u8; 32]); +} + +#[test] +fn hash256_unsupported_partial_ops_return_err() { + assert!(AsconHash256::new().do_final_partial_bits(0, 3).is_err()); + let mut o = [0u8; 32]; + assert!(AsconHash256::new().do_final_partial_bits_out(0, 3, &mut o).is_err()); +} + +#[test] +fn hash256_suspendable_state() { + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::Suspendable; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; + + let data: Vec = (0..37u8).collect(); + let expected = AsconHash256::digest(&data).to_vec(); + + // Suspend mid-absorb, resume, finish, and confirm the digest matches an uninterrupted run. + let mut h = AsconHash256::new(); + h.do_update(&data[..7]); + TestFrameworkSuspendableState::new().test(&h); + + let serialized = h.clone().suspend(); + let mut resumed = AsconHash256::from_suspended(serialized).unwrap(); + resumed.do_update(&data[7..]); + assert_eq!(resumed.do_final(), expected, "resumed digest must match uninterrupted digest"); + + // A corrupted state tag must be rejected (the tag is the byte after the 3-byte version prefix). + let mut busted = serialized; + busted[3] ^= 0xFF; + assert!(matches!(AsconHash256::from_suspended(busted), Err(SuspendableError::InvalidData))); + + // An out-of-range buffer position must be rejected (buf_pos is the final byte). + let mut bad_pos = serialized; + let last = bad_pos.len() - 1; + bad_pos[last] = 99; // >= RATE (8) + assert!(matches!(AsconHash256::from_suspended(bad_pos), Err(SuspendableError::InvalidData))); +} diff --git a/crypto/ascon/tests/xof128_tests.rs b/crypto/ascon/tests/xof128_tests.rs new file mode 100644 index 00000000..b0c2c74d --- /dev/null +++ b/crypto/ascon/tests/xof128_tests.rs @@ -0,0 +1,290 @@ +//! Ascon-XOF128 tests (NIST SP 800-232 §5.2). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus the +//! prefix property, streaming/byte-at-a-time equivalence, trait-API, partial-input rejection, +//! and suspend/resume tests. + +use bouncycastle_ascon::ascon_xof128::{AsconXof128, AsconXof128Squeezer}; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Hash, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; +use bouncycastle_hex as hex; + +/// Embedded NIST LWC Ascon-XOF128 vectors `(message, 512-bit output)` in hex, spanning empty, +/// sub-block, exact-block, and multi-block messages. (Counts 1, 2, 9, 17, 33 of +/// LWC_XOF_KAT_128_512.txt; each output is 64 bytes.) +const XOF_KAT: &[(&str, &str)] = &[ + ( + "", + "473D5E6164F58B39DFD84AACDB8AE42EC2D91FED33388EE0D960D9B3993295C6AD77855A5D3B13FE6AD9E6098988373AF7D0956D05A8F1665D2C67D1A3AD10FF", + ), + ( + "00", + "51430E0438ECDF642B393630D977625F5F337656BA58AB1E960784AC32A16E0D446405551F5469384F8EA283CF12E64FA72C426BFEBAEA3AA1529E2C4AB23A2F", + ), + ( + "0001020304050607", + "8D1886F5D3EC4AF8D15B44BC62B74DA6EA91BC28FB82F9C34079B5ED6E38B6C951803D7DFB3C5E512A0EF5E4060062A6FD067F9C73EF9BEE527411BDA67FC896", + ), + ( + "000102030405060708090A0B0C0D0E0F", + "10BFEDC5F6442D3E1D8C324878CE1DDF73B01CAFC365589283AC4CBB98E48DE3CEDA8A41BB0983D539E4D90F6458C5C781724FAD641ED3CDB4779931097440B3", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "2E5F3403F4171471CC7934B51982CECE8D6628435DB70E89880F3BE4E0B7B05232DFE63C44A836D771337C9C5A2688D1B71ECABE0D5C2006FEF36EF3186138AD", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +#[test] +fn xof128_embedded_kat() { + for (msg_hex, md_hex) in XOF_KAT { + let msg = dh(msg_hex); + let expected = dh(md_hex); + + let got = AsconXof128::new().xof(&msg, expected.len()); + + assert_eq!(got, expected, "msg={msg_hex}"); + + let mut framework = TestFrameworkXOF::new(); + + // This implementation intentionally supports only byte-aligned Ascon-XOF128 input. + framework.enable_partial_byte_tests = false; + + framework.test_xof(AsconXof128::new, &msg, &expected); + } +} + +#[test] +fn xof128_prefix_property_and_streaming() { + let msg = pattern(70); + + let full = AsconXof128::new().xof(&msg, 100); + + // Squeezing in several calls yields the same continuous stream. + let mut x = AsconXof128::new(); + x.do_update(&msg); + + let mut squeezer = x.into_squeezer(); + let mut piecewise = Vec::new(); + + for n in [30usize, 40, 30] { + let mut part = vec![0u8; n]; + let written = squeezer.do_output_out(&mut part); + + assert_eq!(written, n); + piecewise.extend_from_slice(&part); + } + + assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + + // Absorbing in chunks equals one-shot input. + for chunk in [1usize, 8, 9, 64] { + let mut xc = AsconXof128::new(); + + for piece in msg.chunks(chunk) { + xc.do_update(piece); + } + + let mut got = vec![0u8; 100]; + let written = xc.into_squeezer().do_output_out(&mut got); + + assert_eq!(written, got.len()); + + assert_eq!(got, full, "chunked absorb mismatch (chunk={chunk})"); + } +} + +#[test] +fn xof128_hash_view_metadata() { + let x = AsconXof128::new(); + + assert_eq!(x.block_bitlen(), 64); + assert_eq!(x.output_len(), 32); + assert_eq!(x.hash(b"").len(), 32); +} + +#[test] +fn xof128_byte_at_a_time_matches_one_shot() { + let msg = pattern(40); + + let reference = AsconXof128::new().xof(&msg, 48); + + let mut x = AsconXof128::new(); + + for &b in &msg { + x.do_update(&[b]); + } + + let mut out = [0u8; 48]; + let written = x.into_squeezer().do_output_out(&mut out); + + assert_eq!(written, out.len()); + + assert_eq!(out.to_vec(), reference, "XOF128 byte-at-a-time absorb mismatch"); +} + +#[test] +fn xof128_unsupported_partial_input_returns_err() { + // num_bits == 0 is byte-aligned input and must behave like ordinary finalization. + assert!(AsconXof128::new().into_squeezer_partial_bits(0xFF, 0).is_ok()); + + assert!(AsconXof128::new().do_final_partial_bits(0x80, 0).is_ok()); + + for num_bits in [3usize, 7] { + assert!(matches!( + AsconXof128::new().into_squeezer_partial_bits(0xA0, num_bits), + Err(HashError::InvalidInput(_)) + )); + + assert!(matches!( + AsconXof128::new().do_final_partial_bits(0xA0, num_bits), + Err(HashError::InvalidInput(_)) + )); + + let mut out = [0u8; 32]; + + assert!(matches!( + AsconXof128::new().do_final_partial_bits_out(0xA0, num_bits, &mut out), + Err(HashError::InvalidInput(_)) + )); + } + + for num_bits in [8usize, 9] { + assert!(matches!( + AsconXof128::new().into_squeezer_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + )); + + assert!(matches!( + AsconXof128::new().do_final_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + )); + + let mut out = [0u8; 32]; + + assert!(matches!( + AsconXof128::new().do_final_partial_bits_out(0xFF, num_bits, &mut out), + Err(HashError::InvalidLength(_)) + )); + } +} + +#[test] +fn xof128_absorb_then_squeeze_type_transition() { + let mut x = AsconXof128::new(); + x.do_update(b"data"); + + let mut squeezer = x.into_squeezer(); + + let first = squeezer.do_output(8); + let second = squeezer.do_output(8); + + let whole = AsconXof128::new().xof(b"data", 16); + + assert_eq!( + [first, second].concat(), + whole, + "successive reads must continue the same XOF stream" + ); + + // There is deliberately no runtime "absorb after squeeze" test anymore. + // into_squeezer() consumes AsconXof128, and the resulting squeezer does not implement + // Hash::do_update, so that invalid state cannot be expressed. +} + +#[test] +fn xof128_suspendable_state() { + use bouncycastle_ascon::ascon_cxof128::AsconCXof128; + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; + + let data: Vec = (0..30u8).collect(); + + // Reference: uninterrupted absorb + squeeze. + let mut reference = AsconXof128::new(); + reference.do_update(&data); + + let mut expected = [0u8; 40]; + reference.into_squeezer().do_output_out(&mut expected); + + // Suspend mid-absorb, resume, finish, and confirm the squeezed output matches. + let mut x = AsconXof128::new(); + x.do_update(&data[..5]); + + TestFrameworkSuspendableState::new().test(&x); + + let serialized = x.clone().suspend(); + + let mut resumed = AsconXof128::from_suspended(serialized).unwrap(); + resumed.do_update(&data[5..]); + + let mut out = [0u8; 40]; + resumed.into_squeezer().do_output_out(&mut out); + + assert_eq!(out, expected, "resumed XOF output must match uninterrupted output"); + + // A corrupted state tag must be rejected. + let mut busted = serialized; + busted[3] ^= 0xFF; + + assert!(matches!(AsconXof128::from_suspended(busted), Err(SuspendableError::InvalidData))); + + // Cross-type guard: an Ascon-CXOF128 state has the same serialized length but a different + // state tag, so Ascon-XOF128 must reject it. + let mut c = AsconCXof128::with_customization(b"z").unwrap(); + c.do_update(&data); + + let c_state = c.suspend(); + + assert!(matches!(AsconXof128::from_suspended(c_state), Err(SuspendableError::InvalidData))); + + // An inconsistent buf_pos/squeezing combination must be rejected: buf_pos == RATE (8) + // is only valid once squeezing has begun. + let mut bad = serialized; + let len = bad.len(); + + bad[len - 2] = 8; + bad[len - 1] = 0; + + assert!(matches!(AsconXof128::from_suspended(bad), Err(SuspendableError::InvalidData))); + + // Suspend after squeezing has begun and confirm that restoring the squeezer continues the + // same stream. + let mut sq = AsconXof128::new(); + sq.do_update(&data); + + let mut sq = sq.into_squeezer(); + + let mut head = [0u8; 5]; + sq.do_output_out(&mut head); + + let squeezing_state = sq.clone().suspend(); + + // A squeezing state must not be accepted as the absorbing AsconXof128 type. + assert!(matches!( + AsconXof128::from_suspended(squeezing_state), + Err(SuspendableError::InvalidData) + )); + + let mut resumed_sq = AsconXof128Squeezer::from_suspended(squeezing_state).unwrap(); + + let mut tail = [0u8; 35]; + resumed_sq.do_output_out(&mut tail); + + let mut combined = Vec::new(); + combined.extend_from_slice(&head); + combined.extend_from_slice(&tail); + + assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); +} diff --git a/crypto/core-test-framework/src/hash.rs b/crypto/core-test-framework/src/hash.rs index 44037462..2b6b0c1d 100644 --- a/crypto/core-test-framework/src/hash.rs +++ b/crypto/core-test-framework/src/hash.rs @@ -16,6 +16,64 @@ impl TestFrameworkHash { Self { enable_partial_byte_tests: true } } + /// Checks [`Hash::do_final_out`] and [`Hash::hash_out`] against every buffer length, for a + /// hash whose output length is bound into the computation. + /// + /// [`test_hash`](Self::test_hash) covers this too, but only for a `Default + HashAlgParams` + /// implementor. The SP 800-185 functions take constructor arguments and so cannot reach it; + /// `TupleHash` and `ParallelHash` both panicked on a short buffer until this existed. + /// + /// Not for XOFs. A XOF's [`Hash::output_len`] is nominal rather than bound, and its + /// `do_final_out` fills whatever buffer it is handed rather than stopping at `output_len`, so + /// the over-long case below does not describe one. Use `TestFrameworkXOF` for those. + pub fn test_hash_output_buffers(&self, make: impl Fn() -> H, input: &[u8]) { + let expected = { + let mut h = make(); + h.do_update(input); + h.do_final() + }; + let n = make().output_len(); + assert_eq!(expected.len(), n, "do_final() must produce output_len() bytes"); + + // Short: the buffer is filled and the digest truncated to it. + for length in 1..n { + let mut buf = vec![0xAA_u8; length]; + let mut h = make(); + h.do_update(input); + let written = h.do_final_out(&mut buf); + assert_eq!(written, length, "a {length}-byte buffer must take {length} bytes"); + assert_eq!(buf, expected[..length], "short buffer must truncate the digest"); + + // hash_out is the one-shot spelling of the same thing. + let mut buf = vec![0xAA_u8; length]; + let written = make().hash_out(input, &mut buf); + assert_eq!(written, length, "hash_out must agree with do_final_out"); + assert_eq!(buf, expected[..length], "hash_out must truncate the digest"); + } + + // Exact. + let mut buf = vec![0xAA_u8; n]; + let mut h = make(); + h.do_update(input); + assert_eq!(h.do_final_out(&mut buf), n); + assert_eq!(buf, expected, "an exactly-sized buffer must take the whole digest"); + + // Long: the digest lands in the first output_len bytes and the rest is zeroized. + for extra in [1, n, 2 * n + 1] { + let mut buf = vec![0xAA_u8; n + extra]; + let mut h = make(); + h.do_update(input); + let written = h.do_final_out(&mut buf); + assert_eq!(written, n, "a long buffer must still write only output_len bytes"); + assert_eq!(&buf[..n], &expected[..], "the digest must land at the start"); + assert!( + buf[n..].iter().all(|&b| b == 0), + "bytes past output_len must be zeroized, buffer was {} bytes", + n + extra + ); + } + } + /// Test all the members of trait Hash against the given input-output pair. /// This gives good baseline test coverage, but is not exhaustive; for example it does not test /// do_final_partial_bits() or do_final_partial_bits_out() @@ -205,6 +263,40 @@ impl TestFrameworkHash { ); } + /*** Clone: a hash mid-stream can be forked ***/ + // A clone continues from the same absorbed prefix, so finishing the two on the same tail + // must give the same digest, and finishing them on different tails must not. + let (prefix, tail) = input.split_at(input.len() / 2); + let mut original = H::default(); + original.do_update(prefix); + let mut forked = original.clone(); + original.do_update(tail); + forked.do_update(tail); + assert_eq!( + original.do_final(), + expected_output, + "the original must be unaffected by cloning" + ); + assert_eq!( + forked.do_final(), + expected_output, + "a clone must continue from the same absorbed prefix" + ); + + let mut original = H::default(); + original.do_update(prefix); + let mut forked = original.clone(); + original.do_update(tail); + forked.do_update(&[0xA5]); + forked.do_update(tail); + let original_out = original.do_final(); + assert_eq!(original_out, expected_output); + assert_ne!( + forked.do_final(), + original_out, + "a clone must have its own state, not share the original's" + ); + // check that if you feed it an output slice that's bigger than it needs, that it doesn't touch the extra bytes. let mut message_digest = H::default(); let mut buf = vec![0u8; 2 * H::OUTPUT_LEN]; diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index d2881e0b..8e8690a2 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -7,8 +7,9 @@ use bouncycastle_core::key_material::{ }; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - AEADCipher, BlockCipherDecryptor, BlockCipherEncryptor, StreamCipherDecryptor, - StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, + AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, }; /// Instance of the test framework. @@ -19,12 +20,18 @@ pub struct TestFrameworkSymmetricCipher { /// round-trip, and every other length must be *rejected* by `do_final` / `encrypt_out` with a /// `PaddingError`, which the test then asserts instead. pub required_alignment: usize, + /// For [`test_encryptor_decryptor`](Self::test_encryptor_decryptor): the longest message the + /// pair's streaming methods accept. `usize::MAX` (the default) means there is no limit. A + /// cipher that has to buffer the whole message before it can process any of it -- CCM, whose + /// `B0` block encodes the payload length -- sets its buffer's capacity here, and the test caps + /// every message it tries at that length. + pub max_message_len: usize, } impl TestFrameworkSymmetricCipher { /// pub fn new() -> Self { - Self { required_alignment: 1 } + Self { required_alignment: 1, max_message_len: usize::MAX } } /// Exercises the [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] contract for a @@ -62,7 +69,7 @@ impl TestFrameworkSymmetricCipher { .unwrap(); // Enough plaintext lengths to cross several final-chunk boundaries (a block, for padding). let align = self.required_alignment.max(1); - let max_len = (3 * FINAL_LEN.max(1) + 5).next_multiple_of(align); + let max_len = (3 * FINAL_LEN.max(1) + 5).next_multiple_of(align).min(self.max_message_len); // one-shot round trip, every (accepted) length; every other length must be refused for len in 0..=max_len { @@ -223,6 +230,14 @@ impl TestFrameworkSymmetricCipher { other => panic!("decrypt_out into a short buffer: {other:?}"), } } + // ...and ones with room to spare are accepted: without this each `<` guard can be flipped + // to `>` and the short-buffer probes above still "pass". + let mut roomy = vec![0u8; E::encrypt_out_len(len) + 3]; + let (_, n) = E::encrypt_out(&key, msg, &mut roomy).unwrap(); + assert_eq!(n, E::encrypt_out_len(len), "encrypt_out into a roomy buffer"); + let mut roomy = vec![0u8; need + 3]; + let n = D::decrypt_out(&key, &init_data, &ct[..ct_len], &mut roomy).unwrap(); + assert_eq!(&roomy[..n], msg, "decrypt_out into a roomy buffer"); let (mut enc, _) = E::do_encrypt_init(&key).unwrap(); let need = enc.update_out_len(len); if need > 0 { @@ -425,6 +440,7 @@ impl TestFrameworkBlockCipher { SecurityStrength::_192bit, SecurityStrength::_256bit, ]; + let mut strengths_tested = 0; for ss in security_strengths.iter() { // `set_security_strength` enforces its key-length guard even inside a // do_hazardous_operations() closure -- a KEY_LEN-byte key cannot be tagged at a @@ -435,9 +451,10 @@ impl TestFrameworkBlockCipher { if ss > &SecurityStrength::from_bytes(KEY_LEN) { continue; } - - // Tag the key at an arbitrary strength for the purpose of this test. + // Inside a do_hazardous_operations() closure set_security_strength() raises the + // strength without complaining; any error here is a framework bug, hence unwrap(). do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); + strengths_tested += 1; match E::do_encrypt_init(&key) { Ok(_) => { @@ -455,259 +472,883 @@ impl TestFrameworkBlockCipher { _ => panic!("Unexpected error"), }; } + assert!(strengths_tested > 0, "strength sweep must not be vacuous"); } } /// Instance of the test framework. pub struct TestFrameworkAEADCipher { - // Put any config options here + /// The longest message the pair's streaming methods accept; see + /// [`TestFrameworkSymmetricCipher::max_message_len`], which this is passed on to. `usize::MAX` + /// (the default) means there is no limit. + pub max_message_len: usize, } impl TestFrameworkAEADCipher { /// pub fn new() -> Self { - Self {} + Self { max_message_len: usize::MAX } } - /// Tests the plain one-shots -- [`AEADCipher::encrypt_out`] and - /// [`AEADCipher::decrypt_out`], which take no additional authenticated data. + /// Exercises the [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] streaming contract for a + /// paired implementor. The counterpart of [`TestFrameworkBlockCipher::test`] for an + /// authenticated cipher. + /// + /// Checks, in order: + /// * the whole [`TestFrameworkSymmetricCipher::test_encryptor_decryptor`] suite, since an AEAD + /// with no associated data and the tag inline *is* a [`SymmetricCipherEncryptor`] / + /// [`SymmetricCipherDecryptor`] pair, and that `FINAL_LEN` has room for the tag; + /// * the detached one-shot round trip for every message length from 0 to a few times + /// `TAG_LEN`, and that the tag is not the all-zero array; + /// * the inline layout with associated data, one-shot and streaming, is exactly the detached + /// ciphertext with the tag appended, and a stream shorter than the tag is a failed + /// decryption; + /// * streaming in every chunking, of both the AAD and the data, agrees with `update_out_len` + /// on every call and gives the one-shot's ciphertext and tag byte for byte, and decrypts in + /// every chunking; + /// * an empty AAD is a no-op -- it gives what absorbing no AAD at all gives -- and a message + /// with no data still authenticates its AAD; + /// * `do_update_aad` with non-empty AAD after the first `do_update_out` is refused with a + /// [`SymmetricCipherError::StateError`], and the refusal leaves the value usable; + /// * a tampered ciphertext, tag, AAD or nonce all fail the tag check, and the one-shots leave + /// no plaintext behind when they do; + /// * two encryptions under the same key draw different nonces; + /// * a key of the wrong [`KeyType`] is rejected, and the security-strength policy matches + /// [`Algorithm::MAX_SECURITY_STRENGTH`]. + /// + /// [`Self::test_buffering_toy`] separately pins that a cipher which holds back more than the + /// tag is handled correctly, since `E`/`D` here are supplied by the caller and might not. /// - /// These four methods were the former `SymmetricCipher` trait, and this was its suite; they now - /// belong to `AEADCipher`, so the suite comes with them. Called by - /// [`test`](Self::test), so an implementor gets it without asking, and public so it can be run - /// on its own. - pub fn test_plain_one_shots< + /// [`Algorithm::MAX_SECURITY_STRENGTH`]: bouncycastle_core::traits::Algorithm::MAX_SECURITY_STRENGTH + pub fn test_encryptor_decryptor< const KEY_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize, - C: AEADCipher, + const FINAL_LEN: usize, + E: AEADCipherEncryptor, + D: AEADCipherDecryptor, >( &self, ) { - let msg = b"The quick brown fox jumps over the lazy dog"; + assert!( + FINAL_LEN >= TAG_LEN, + "FINAL_LEN must have room for the inline tag the decryptor holds back" + ); + // No AAD and the tag inline is the plain symmetric-cipher contract. + let mut symmetric = TestFrameworkSymmetricCipher::new(); + symmetric.max_message_len = self.max_message_len; + symmetric.test_encryptor_decryptor::(); let key = KeyMaterial::::from_bytes_as_type( &DUMMY_SEED[..KEY_LEN], KeyType::SymmetricCipherKey, ) .unwrap(); + let aad: &[u8] = b"some associated data"; + let pinned = [0xA5u8; NONCE_LEN]; - // one-shot API - let mut ct = [0u8; 1024]; - let (iv, ct_bytes_written) = C::encrypt_out(&key, msg, &mut ct).unwrap(); - assert_ne!(ct_bytes_written, 0); - - let mut pt = [0u8; 1024]; - let pt_bytes_written = C::decrypt_out(&key, iv, &ct[..ct_bytes_written], &mut pt).unwrap(); - assert_ne!(pt_bytes_written, 0); - assert_eq!(msg, &pt[..pt_bytes_written]); + // one-shot round trip, every length up to a few times the tag length + let max_len = (3 * TAG_LEN.max(1) + 5).min(self.max_message_len); + for len in 0..=max_len { + let msg = &DUMMY_SEED[..len]; + let mut ct = vec![0u8; E::encrypt_out_len_detached(len)]; + let (nonce, ct_len, tag) = E::encrypt_out_detached(&key, aad, msg, &mut ct).unwrap(); + ct.truncate(ct_len); + assert_ne!(tag, [0u8; TAG_LEN], "len {len}: the tag must not be all zeros"); + // Only assert the ciphertext differs from the plaintext once there is enough of it for + // an accidental match to be negligible rather than a 1-in-256 flake. + if len >= 8 { + assert_ne!(&ct[..], msg, "len {len}: the ciphertext must not be the plaintext"); + } + let mut pt = vec![0u8; D::decrypt_out_max_len_detached(ct.len())]; + let pt_len = D::decrypt_out_detached(&key, &nonce, aad, &ct, &tag, &mut pt).unwrap(); + pt.truncate(pt_len); + assert_eq!(&pt[..], msg, "one-shot round trip, len {len}"); + + // the std one-shots agree with the _out ones for the same nonce + let (nonce2, ct2, tag2) = E::encrypt_detached(&key, aad, msg).unwrap(); + assert_eq!(ct2.len(), ct_len, "encrypt_detached must return exactly the bytes written"); + let pt2 = D::decrypt_detached(&key, &nonce2, aad, &ct2, &tag2).unwrap(); + assert_eq!(pt2, msg, "std round trip, len {len}"); + let pt3 = D::decrypt_detached(&key, &nonce, aad, &ct, &tag).unwrap(); + assert_eq!(pt3, msg, "decrypt_detached must agree with decrypt_out_detached"); + + // the inline `ciphertext || tag` layout with AAD: `encrypt_out_with_aad` must write exactly + // the detached ciphertext with the tag appended -- the same bytes under the same + // nonce -- and both the one-shot and the streaming finalizer must round trip it. + let mut detached = vec![0u8; E::encrypt_out_len_detached(len)]; + let (pinned_nonce, detached_len, detached_tag) = E::encrypt_out_rng_detached( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut detached, + ) + .unwrap(); + detached.truncate(detached_len); + detached.extend_from_slice(&detached_tag); - // todo -- add tests for encrypt() / decrypt() wrapped in a #[cfg(std)] + let mut inline = vec![0u8; E::encrypt_out_len(len)]; + let (inline_nonce, inline_len) = + E::encrypt_out_with_aad(&key, aad, msg, &mut inline).unwrap(); + assert_eq!( + inline_len, + E::encrypt_out_len_detached(len) + TAG_LEN, + "encrypt_out_with_aad must write the ciphertext plus the tag, len {len}" + ); + let mut pt4 = vec![0u8; D::decrypt_out_max_len(inline_len)]; + let pt4_len = + D::decrypt_out_with_aad(&key, &inline_nonce, aad, &inline[..inline_len], &mut pt4) + .unwrap(); + assert_eq!(&pt4[..pt4_len], msg, "tagged one-shot round trip, len {len}"); - // messing with the ciphertext does not give back the same plaintext (or failing to decrypt is also ok) - ct[17] ^= 0xFF; - match C::decrypt_out(&key, iv, &ct[..ct_bytes_written], &mut pt) { - Ok(bytes_written) => { - // so it decrypted something, but it had better not match the original plaintext - assert_eq!(bytes_written, pt_bytes_written); - assert_ne!(&pt[..bytes_written], msg); + // ...and so must the RNG-driven and allocating inline-with-AAD one-shots. The roomy + // buffer is deliberate: see the `encrypt_out_rng_detached` probe below. + let mut inline_rng = vec![0u8; E::encrypt_out_len(len) + 3]; + let (rng_nonce, rng_len) = E::encrypt_out_rng_with_aad( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut inline_rng, + ) + .unwrap(); + assert_eq!(rng_nonce, pinned_nonce, "the same RNG stream must give the same nonce"); + assert_eq!( + &inline_rng[..rng_len], + &detached[..], + "len {len}: encrypt_out_rng_with_aad must be the detached ciphertext and its tag" + ); + // exactly the length it asks for must be enough too + let mut exact = vec![0u8; E::encrypt_out_len(len)]; + let (_, exact_len) = E::encrypt_out_rng_with_aad( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut exact, + ) + .unwrap(); + assert_eq!(&exact[..exact_len], &detached[..], "len {len}: exact-size buffer"); + let mut short = vec![0u8; E::encrypt_out_len(len) - 1]; + match E::encrypt_out_rng_with_aad( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut short, + ) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, E::encrypt_out_len(len)) + } + other => panic!("encrypt_out_rng_with_aad into a short buffer: {other:?}"), + } + let (alloc_nonce, alloc_ct) = E::encrypt_with_aad(&key, aad, msg).unwrap(); + assert_eq!( + alloc_ct.len(), + inline_len, + "encrypt_with_aad must return the bytes written" + ); + let alloc_pt = D::decrypt_with_aad(&key, &alloc_nonce, aad, &alloc_ct).unwrap(); + assert_eq!(alloc_pt, msg, "allocating inline-with-AAD round trip, len {len}"); + + let (mut enc5, nonce5) = + E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)).unwrap(); + assert_eq!(nonce5, pinned_nonce, "the same RNG stream must give the same nonce"); + enc5.do_update_aad(aad).unwrap(); + let mut inline5 = vec![0u8; enc5.update_out_len(len)]; + let written5 = enc5.do_update_out(msg, &mut inline5).unwrap(); + inline5.truncate(written5); + let (last5, last5_len) = enc5.do_final().unwrap(); + inline5.extend_from_slice(&last5[..last5_len]); + assert_eq!( + inline5.len(), + inline_len, + "tagged streaming must write as much as the one-shot" + ); + assert_eq!( + inline5, detached, + "len {len}: the inline layout must be the detached ciphertext followed by its tag" + ); + let mut dec5 = D::do_decrypt_init(&key, &nonce5).unwrap(); + dec5.do_update_aad(aad).unwrap(); + let mut pt5 = vec![0u8; dec5.update_out_len(inline5.len())]; + let got5 = dec5.do_update_out(&inline5, &mut pt5).unwrap(); + pt5.truncate(got5); + let (last, data_len) = dec5.do_final().unwrap(); + pt5.extend_from_slice(&last[..data_len]); + assert_eq!(pt5, msg, "tagged streaming round trip, len {len}"); + + // a stream that ends before a whole tag has been seen is not a short buffer, it is a + // failed decryption + if TAG_LEN > 0 { + let mut dec6 = D::do_decrypt_init(&key, &nonce5).unwrap(); + dec6.do_update_aad(aad).unwrap(); + let short = &inline5[..TAG_LEN - 1]; + let mut scratch = vec![0u8; dec6.update_out_len(short.len())]; + dec6.do_update_out(short, &mut scratch).unwrap(); + assert!( + matches!(dec6.do_final(), Err(SymmetricCipherError::DecryptionFailed)), + "a stream shorter than the tag must be DecryptionFailed, len {len}" + ); } - Err(SymmetricCipherError::DecryptionFailed) => { /* also ok */ } - _ => panic!("Unexpected error"), - }; - // error case: KeyMaterial of wrong type - let mac_key = - KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) + // too-short output buffers on the one-shots are refused with the required length, + // before any work is done + let need = E::encrypt_out_len_detached(len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match E::encrypt_out_detached(&key, aad, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, need) + } + other => panic!("encrypt_out_detached into a short buffer: {other:?}"), + } + let mut short = vec![0u8; need - 1]; + match E::encrypt_out_rng_detached( + &key, + &mut FixedSeedRNG::::new([0xA5u8; NONCE_LEN]), + aad, + msg, + &mut short, + ) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, need) + } + other => panic!("encrypt_out_rng_detached into a short buffer: {other:?}"), + } + // ...and one with room to spare must be accepted: without this the guard can be + // flipped to `>` and every short-buffer probe still "passes", because the error + // then comes from `do_update_out` behind it with the same variant and length. + let mut roomy = vec![0u8; need + 3]; + let (_, n, _) = E::encrypt_out_detached(&key, aad, msg, &mut roomy).unwrap(); + assert_eq!(n, need, "encrypt_out_detached into a roomy buffer"); + let mut roomy = vec![0u8; need + 3]; + let (_, n, _) = E::encrypt_out_rng_detached( + &key, + &mut FixedSeedRNG::::new([0xA5u8; NONCE_LEN]), + aad, + msg, + &mut roomy, + ) .unwrap(); - match C::encrypt_out(&mac_key, msg, &mut ct) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("Unexpected error"), - }; - - // error case: security strengths too weak and too strong - let mut key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - let security_strengths = [ - SecurityStrength::None, - SecurityStrength::_112bit, - SecurityStrength::_128bit, - SecurityStrength::_192bit, - SecurityStrength::_256bit, - ]; - for ss in security_strengths.iter() { - // `set_security_strength` enforces its key-length guard even inside a - // do_hazardous_operations() closure -- a KEY_LEN-byte key cannot be tagged at a - // strength above `from_bytes(KEY_LEN)` -- so skip the strengths this key cannot carry - // rather than unwrapping an error. (A 16-byte key can reach 128-bit and no higher.) - // Do NOT "fix" this by relaxing that guard in `KeyMaterial`: core's - // `test_hazardous_ops_error_handling` requires it to stay enforced. - if ss > &SecurityStrength::from_bytes(KEY_LEN) { - continue; + assert_eq!( + n, need, + "encrypt_out_rng_detached must write exactly encrypt_out_len_detached bytes" + ); } - - // Tag the key at an arbitrary strength for the purpose of this test. - do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); - - match C::encrypt_out(&key, msg, &mut ct) { - Ok(_) => { - if ss >= &C::MAX_SECURITY_STRENGTH { /* good */ - } else { - panic!("Should have been a strong enough key"); + let need = E::encrypt_out_len(len); + let mut short = vec![0u8; need - 1]; + match E::encrypt_out_with_aad(&key, aad, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("encrypt_out_with_aad into a short buffer: {other:?}"), + } + let need = D::decrypt_out_max_len_detached(ct.len()); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match D::decrypt_out_detached(&key, &nonce, aad, &ct, &tag, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, need) } + other => panic!("decrypt_out_detached into a short buffer: {other:?}"), } - Err(SymmetricCipherError::KeyMaterialError(_)) => { - if ss < &C::MAX_SECURITY_STRENGTH { /* good */ - } else { - panic!("Should not have accepted a key weaker than algorithm"); + } + let need = D::decrypt_out_max_len(inline_len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match D::decrypt_out_with_aad(&key, &inline_nonce, aad, &inline, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, need) } + other => panic!("decrypt_out_with_aad into a short buffer: {other:?}"), } - _ => panic!("Unexpected error"), - }; + } } - } - - /// Test all the members of trait AEADCipher against the given input-output pair. - /// This gives good baseline test coverage, but is not exhaustive. - pub fn test< - const KEY_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - C: AEADCipher, - >( - &self, - ) { - // The plain one-shots this trait absorbed from the former `SymmetricCipher`. - self.test_plain_one_shots::(); - let msg = b"The quick brown fox jumps over the lazy dog"; - let aad = b"some associated data"; - - let key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, + // streaming in every chunking agrees with the one-shot, for both the AAD and the data. + // The pinned RNG is what makes the nonce -- and so the ciphertext -- comparable. + let msg = &DUMMY_SEED[..max_len.max(17).min(self.max_message_len)]; + let mut ct_ref = vec![0u8; E::encrypt_out_len_detached(msg.len())]; + let (nonce_ref, ct_ref_len, tag_ref) = E::encrypt_out_rng_detached( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut ct_ref, ) .unwrap(); - - // one-shot API - let mut ct = [0u8; 1024]; - let (nonce, ct_bytes_written, tag) = C::aead_encrypt_out(&key, aad, msg, &mut ct).unwrap(); - if nonce.len() != 0 { - assert_ne!(nonce, [0u8; NONCE_LEN]); + ct_ref.truncate(ct_ref_len); + + for chunk in [1usize, 2, 3, 7, TAG_LEN.max(1), TAG_LEN + 1, msg.len()] { + let (mut enc, nonce) = + E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)).unwrap(); + assert_eq!(nonce, nonce_ref, "the same RNG stream must give the same nonce"); + for piece in aad.chunks(chunk) { + enc.do_update_aad(piece).unwrap(); + } + let mut ct = Vec::new(); + for piece in msg.chunks(chunk) { + let expect = enc.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "chunk {chunk}: update_out_len must be exact (encrypt)"); + ct.extend_from_slice(&buf[..n]); + } + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_final_out_detached(&mut final_buf).unwrap(); + assert!( + final_len + TAG_LEN <= FINAL_LEN, + "chunk {chunk}: the detached flush must leave FINAL_LEN room for the tag" + ); + ct.extend_from_slice(&final_buf[..final_len]); + assert_eq!(ct, ct_ref, "chunk {chunk}: streaming must give the one-shot ciphertext"); + assert_eq!(tag, tag_ref, "chunk {chunk}: streaming must give the one-shot tag"); + + // ...and the decryptor agrees in every chunking too + let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); + for piece in aad.chunks(chunk) { + dec.do_update_aad(piece).unwrap(); + } + let mut pt = Vec::new(); + for piece in ct.chunks(chunk) { + let expect = dec.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "chunk {chunk}: update_out_len must be exact (decrypt)"); + pt.extend_from_slice(&buf[..n]); + } + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_final_out_detached(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(pt, msg, "chunk {chunk}: streaming round trip"); } - assert_ne!(ct_bytes_written, 0); - assert_ne!(tag, [0u8; TAG_LEN]); - let mut pt = [0u8; 1024]; - let pt_bytes_written = - C::aead_decrypt_out(&key, &nonce, aad, &ct[..ct_bytes_written], &tag, &mut pt).unwrap(); - assert_ne!(pt_bytes_written, 0); - assert_eq!(msg, &pt[..pt_bytes_written]); + // the array-returning finals agree with the `_out` ones the chunked loop above used + let (mut enc, nonce) = + E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)).unwrap(); + enc.do_update_aad(aad).unwrap(); + let mut ct = vec![0u8; enc.update_out_len(msg.len())]; + let n = enc.do_update_out(msg, &mut ct).unwrap(); + ct.truncate(n); + let (last, last_len, tag) = enc.do_final_detached().unwrap(); + ct.extend_from_slice(&last[..last_len]); + assert_eq!(ct, ct_ref, "do_final_detached must give the one-shot ciphertext"); + assert_eq!(tag, tag_ref, "do_final_detached must give the one-shot tag"); + let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); + dec.do_update_aad(aad).unwrap(); + let mut pt = vec![0u8; dec.update_out_len(ct.len())]; + let n = dec.do_update_out(&ct, &mut pt).unwrap(); + pt.truncate(n); + let (last, data_len) = dec.do_final_detached(&tag).unwrap(); + pt.extend_from_slice(&last[..data_len]); + assert_eq!(pt, msg, "do_final_detached must round trip"); + let mut wrong_tag = tag; + wrong_tag[0] ^= 0xFF; + let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); + dec.do_update_aad(aad).unwrap(); + let mut pt = vec![0u8; dec.update_out_len(ct.len())]; + dec.do_update_out(&ct, &mut pt).unwrap(); + assert!( + matches!( + dec.do_final_detached(&wrong_tag), + Err(SymmetricCipherError::AEADTagCheckFailed) + ), + "do_final_detached must check the tag" + ); + + // an empty AAD is a no-op: it must give exactly what absorbing no AAD at all gives + let mut with_empty = vec![0u8; E::encrypt_out_len_detached(msg.len())]; + let (nonce_empty, len_empty, tag_empty) = E::encrypt_out_rng_detached( + &key, + &mut FixedSeedRNG::::new(pinned), + b"", + msg, + &mut with_empty, + ) + .unwrap(); + with_empty.truncate(len_empty); + let mut without = vec![0u8; E::encrypt_out_len_detached(msg.len())]; + let (nonce_none, len_none, tag_none) = E::encrypt_out_rng_detached( + &key, + &mut FixedSeedRNG::::new(pinned), + &[], + msg, + &mut without, + ) + .unwrap(); + without.truncate(len_none); + assert_eq!(nonce_empty, nonce_none); + assert_eq!(tag_empty, tag_none, "an empty AAD must be a no-op"); + assert_eq!(with_empty, without, "an empty AAD must be a no-op"); + + // ...and no AAD at all is what the inherited `SymmetricCipherEncryptor` one-shot gives + let mut plain = vec![0u8; E::encrypt_out_len(msg.len())]; + let (nonce_plain, len_plain) = + E::encrypt_out_rng(&key, &mut FixedSeedRNG::::new(pinned), msg, &mut plain) + .unwrap(); + assert_eq!(nonce_plain, nonce_none); + assert_eq!(&plain[..len_plain - TAG_LEN], &without[..], "no-AAD inline ciphertext"); + assert_eq!(&plain[len_plain - TAG_LEN..len_plain], &tag_none, "no-AAD inline tag"); + + // a message with no data at all still authenticates its AAD + let (nonce, _ct_len, tag) = E::encrypt_out_detached(&key, aad, &[], &mut []).unwrap(); + D::decrypt_out_detached(&key, &nonce, aad, &[], &tag, &mut []).unwrap(); + match D::decrypt_out_detached( + &key, + &nonce, + b"different associated data", + &[], + &tag, + &mut [], + ) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("an empty message must still authenticate its AAD, got {other:?}"), + }; - // todo -- add tests for aead_encrypt() / aead_decrypt() wrapped in a #[cfg(std)] + // the AAD phase is over once data has been fed in -- on both sides, and on the decrypting + // side even when all of it is still being held back as a possible tag + let (mut enc, nonce) = E::do_encrypt_init(&key).unwrap(); + let mut ct = vec![0u8; enc.update_out_len(msg.len())]; + enc.do_update_out(msg, &mut ct).unwrap(); + match enc.do_update_aad(aad) { + Err(SymmetricCipherError::StateError(_)) => { /* good */ } + other => panic!("AAD after data must be refused, got {other:?}"), + }; + // an empty AAD stays a no-op even here, and the refused call must not have disturbed the + // state: the value is still good for the rest of the flow. + enc.do_update_aad(b"").unwrap(); + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_final_out_detached(&mut final_buf).unwrap(); + ct.extend_from_slice(&final_buf[..final_len]); + + let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); + let mut pt = vec![0u8; dec.update_out_len(1)]; + let mut got = dec.do_update_out(&ct[..1], &mut pt).unwrap(); + pt.truncate(got); + match dec.do_update_aad(aad) { + Err(SymmetricCipherError::StateError(_)) => { /* good */ } + other => panic!("AAD after data must be refused, got {other:?}"), + }; + dec.do_update_aad(b"").unwrap(); + let mut rest = vec![0u8; dec.update_out_len(ct.len() - 1)]; + got = dec.do_update_out(&ct[1..], &mut rest).unwrap(); + pt.extend_from_slice(&rest[..got]); + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_final_out_detached(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(&pt[..], msg, "a refused do_update_aad must not disturb the state"); + + // tampering: every one of these must fail the tag check, and the one-shots must leave no + // plaintext behind when they do + let mut ct = vec![0u8; E::encrypt_out_len_detached(msg.len())]; + let (nonce, ct_len, tag) = E::encrypt_out_detached(&key, aad, msg, &mut ct).unwrap(); + ct.truncate(ct_len); + + let mut tampered = ct.clone(); + tampered[3] ^= 0xFF; + let mut buf = vec![0u8; D::decrypt_out_max_len_detached(tampered.len())]; + match D::decrypt_out_detached(&key, &nonce, aad, &tampered, &tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified ciphertext must fail the tag check, got {other:?}"), + }; + assert!( + buf.iter().all(|&b| b == 0), + "the one-shot decrypt must zeroize the buffer when the tag check fails" + ); + + let mut tampered_inline = ct.clone(); + tampered_inline.extend_from_slice(&tag); + tampered_inline[3] ^= 0xFF; + for with_aad in [false, true] { + let mut buf = vec![0u8; D::decrypt_out_max_len(tampered_inline.len())]; + let result = if with_aad { + D::decrypt_out_with_aad(&key, &nonce, aad, &tampered_inline, &mut buf) + } else { + D::decrypt_out(&key, &nonce, &tampered_inline, &mut buf) + }; + // Without the AAD the tag was never going to verify; either way what matters is the + // failure and the zeroized buffer. + match result { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified inline ciphertext must fail, got {other:?}"), + }; + assert!( + buf.iter().all(|&b| b == 0), + "the inline one-shot (aad {with_aad}) must zeroize the buffer on a failed check" + ); + } + match D::decrypt_with_aad(&key, &nonce, aad, &tampered_inline) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("decrypt_with_aad of a modified ciphertext must fail, got {other:?}"), + }; - // Modifying the ciphertext MUST cause an AEAD failure: unlike an unauthenticated cipher, - // a conformant AEAD must never return plaintext for a ciphertext that fails its tag check. - ct[17] ^= 0xFF; - match C::aead_decrypt_out(&key, &nonce, aad, &ct[..ct_bytes_written], &tag, &mut pt) { + let mut wrong_tag = tag; + wrong_tag[0] ^= 0xFF; + let mut buf = vec![0u8; D::decrypt_out_max_len_detached(ct.len())]; + match D::decrypt_out_detached(&key, &nonce, aad, &ct, &wrong_tag, &mut buf) { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - Err(SymmetricCipherError::DecryptionFailed) => { /* also acceptable */ } - _ => panic!("Modified ciphertext must fail the AEAD tag check"), + other => panic!("a modified tag must fail the tag check, got {other:?}"), }; - // restore the ciphertext so the AAD- and tag-tamper checks below each test one variable - ct[17] ^= 0xFF; - // messing with the aad causes the aead_decrypt to fail - match C::aead_decrypt_out( + let mut buf = vec![0u8; D::decrypt_out_max_len_detached(ct.len())]; + match D::decrypt_out_detached( &key, &nonce, b"not the right associated data", - &ct[..ct_bytes_written], + &ct, &tag, - &mut pt, + &mut buf, ) { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - _ => panic!("Expected TagCheckFailed error"), + other => panic!("a modified AAD must fail the tag check, got {other:?}"), }; - // messing with the tag causes the aead_decrypt to fail - match C::aead_decrypt_out( - &key, - &nonce, - aad, - &ct[..ct_bytes_written], - &[3u8; TAG_LEN], - &mut pt, - ) { - Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - _ => panic!("Expected TagCheckFailed error"), - }; + if NONCE_LEN > 0 { + let mut wrong_nonce = nonce; + wrong_nonce[0] ^= 0xFF; + let mut buf = vec![0u8; D::decrypt_out_max_len_detached(ct.len())]; + match D::decrypt_out_detached(&key, &wrong_nonce, aad, &ct, &tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified nonce must fail the tag check, got {other:?}"), + }; - // multiple invocations give different nonces - let (nonce1, _ct_bytes_written, _tag) = - C::aead_encrypt_out(&key, aad, msg, &mut ct).unwrap(); - let (nonce2, _ct_bytes_written, _tag) = - C::aead_encrypt_out(&key, aad, msg, &mut ct).unwrap(); - assert_ne!(nonce1, nonce2); + // two encryptions under the same key must not reuse a nonce + let (_enc1, nonce1) = E::do_encrypt_init(&key).unwrap(); + let (_enc2, nonce2) = E::do_encrypt_init(&key).unwrap(); + assert_ne!(nonce1, nonce2); + } - // error case: KeyMaterial of wrong type - let mac_key = - KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) - .unwrap(); - match C::aead_encrypt_out(&mac_key, aad, msg, &mut ct) { - Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("Unexpected error"), + // The key-type and security-strength checks on `do_encrypt_init` / `do_decrypt_init` are + // covered by the `TestFrameworkSymmetricCipher` suite run above. + } + + /// Pins that a *genuinely buffering* [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] pair's + /// `update_out_len` is honoured through every chunking, against a toy built to hold back up to + /// three bytes at a time before releasing them -- more than the tag the decryptor has to hold + /// back anyway -- the property [`Self::test_encryptor_decryptor`] cannot pin on its own, since + /// a caller-supplied `E`/`D` might hold back nothing but the tag (Ascon-AEAD128 holds back + /// nothing else). Modelled on the toy permutations `crypto/modes/tests/common/mod.rs` uses for + /// the equivalent block-cipher property. + /// + /// The toy's "ciphertext" is the plaintext with a per-byte counter XORed in, released three + /// bytes behind what it has consumed when encrypting and three plus `TAG_LEN` when decrypting; + /// its "tag" is a length check. Not remotely a real AEAD -- it exists solely to make holding + /// data back observable. + pub fn test_buffering_toy(&self) { + use bouncycastle_core::errors::SymmetricCipherError; + use bouncycastle_core::key_material::{KeyMaterial, KeyType}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, }; - // error case: security strengths too weak and too strong - let mut key = KeyMaterial::::from_bytes_as_type( + const HOLD_BACK: usize = 3; + const KEY_LEN: usize = 4; + const NONCE_LEN: usize = 4; + const TAG_LEN: usize = 1; + // What either side's final call can produce: the encryptor's held-back bytes plus the tag + // after them, or everything the decryptor held back. + const FINAL_LEN: usize = HOLD_BACK + TAG_LEN; + + struct Buffered { + hold: usize, + pos: u8, + held: [u8; FINAL_LEN], + held_len: usize, + len_seen: usize, + } + + impl Buffered { + fn new(hold: usize) -> Self { + Self { hold, pos: 0, held: [0u8; FINAL_LEN], held_len: 0, len_seen: 0 } + } + + fn update_out_len(&self, input_len: usize) -> usize { + (self.held_len + input_len).saturating_sub(self.hold) + } + + /// Feeds `input` in, holding back the last `hold` bytes and releasing (XORed with a + /// running counter) everything older than that into `output`. + fn update_out(&mut self, input: &[u8], output: &mut [u8]) -> usize { + self.len_seen += input.len(); + let total = self.held_len + input.len(); + let releasable = total.saturating_sub(self.hold); + let from_held = self.held_len.min(releasable); + let from_new = releasable - from_held; + for (i, b) in self.held[..from_held].iter().enumerate() { + output[i] = *b ^ self.pos; + self.pos = self.pos.wrapping_add(1); + } + for (i, b) in input[..from_new].iter().enumerate() { + output[from_held + i] = *b ^ self.pos; + self.pos = self.pos.wrapping_add(1); + } + // The amount kept is `total - releasable`, which is `hold` once `total` reaches it + // but only `total` itself before that -- so the tail of `new_held` actually in use + // is `new_len`, not always the full `hold`. + let new_len = total - releasable; + let mut new_held = [0u8; FINAL_LEN]; + let kept_from_held = self.held_len - from_held; + new_held[..kept_from_held].copy_from_slice(&self.held[from_held..self.held_len]); + new_held[kept_from_held..new_len].copy_from_slice(&input[from_new..]); + self.held = new_held; + self.held_len = new_len; + releasable + } + + /// Releases the first `n` held-back bytes into `output`. + fn finish(&mut self, n: usize, output: &mut [u8]) { + for (i, b) in self.held[..n].iter().enumerate() { + output[i] = *b ^ self.pos; + self.pos = self.pos.wrapping_add(1); + } + } + } + + fn toy_tag(data_len: usize) -> [u8; TAG_LEN] { + [(data_len % 256) as u8; TAG_LEN] + } + + struct Enc(Buffered); + struct Dec(Buffered); + + impl Algorithm for Enc { + const ALG_NAME: &'static str = "buffering-toy"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; + } + impl Algorithm for Dec { + const ALG_NAME: &'static str = "buffering-toy"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; + } + + impl SymmetricCipherEncryptor for Enc { + fn do_encrypt_init( + _key: &KeyMaterial, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + Ok((Self(Buffered::new(HOLD_BACK)), [0u8; NONCE_LEN])) + } + fn do_encrypt_init_rng( + key: &KeyMaterial, + _rng: &mut dyn RNG, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + Self::do_encrypt_init(key) + } + fn update_out_len(&self, input_len: usize) -> usize { + self.0.update_out_len(input_len) + } + fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + Ok(self.0.update_out(plaintext, ciphertext)) + } + fn do_final(self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError> { + let mut out = [0u8; FINAL_LEN]; + let (n, tag) = self.do_final_out_detached(&mut out)?; + out[n..n + TAG_LEN].copy_from_slice(&tag); + Ok((out, n + TAG_LEN)) + } + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + TAG_LEN + } + } + + impl AEADCipherEncryptor for Enc { + fn do_update_aad(&mut self, _aad: &[u8]) -> Result<(), SymmetricCipherError> { + Ok(()) + } + fn do_final_out_detached( + mut self, + ciphertext: &mut [u8; FINAL_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + let n = self.0.held_len; + self.0.finish(n, ciphertext); + Ok((n, toy_tag(self.0.len_seen))) + } + } + + impl SymmetricCipherDecryptor for Dec { + fn do_decrypt_init( + _key: &KeyMaterial, + _nonce: &[u8; NONCE_LEN], + ) -> Result { + Ok(Self(Buffered::new(FINAL_LEN))) + } + fn update_out_len(&self, input_len: usize) -> usize { + self.0.update_out_len(input_len) + } + fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + Ok(self.0.update_out(ciphertext, plaintext)) + } + /// The last `TAG_LEN` held-back bytes are the tag, the rest ciphertext. + fn do_final(mut self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError> { + let Some(n) = self.0.held_len.checked_sub(TAG_LEN) else { + return Err(SymmetricCipherError::DecryptionFailed); + }; + let mut out = [0u8; FINAL_LEN]; + self.0.finish(n, &mut out); + if self.0.held[n..n + TAG_LEN] != toy_tag(self.0.len_seen - TAG_LEN) { + return Err(SymmetricCipherError::AEADTagCheckFailed); + } + Ok((out, n)) + } + fn decrypt_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(TAG_LEN) + } + } + + impl AEADCipherDecryptor for Dec { + fn do_update_aad(&mut self, _aad: &[u8]) -> Result<(), SymmetricCipherError> { + Ok(()) + } + fn do_final_out_detached( + mut self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; FINAL_LEN], + ) -> Result { + let n = self.0.held_len; + self.0.finish(n, plaintext); + if *tag != toy_tag(self.0.len_seen) { + return Err(SymmetricCipherError::AEADTagCheckFailed); + } + Ok(n) + } + } + + let key = KeyMaterial::::from_bytes_as_type( &DUMMY_SEED[..KEY_LEN], KeyType::SymmetricCipherKey, ) .unwrap(); - let security_strengths = [ - SecurityStrength::None, - SecurityStrength::_112bit, - SecurityStrength::_128bit, - SecurityStrength::_192bit, - SecurityStrength::_256bit, - ]; - for ss in security_strengths.iter() { - // `set_security_strength` enforces its key-length guard even inside a - // do_hazardous_operations() closure -- a KEY_LEN-byte key cannot be tagged at a - // strength above `from_bytes(KEY_LEN)` -- so skip the strengths this key cannot carry - // rather than unwrapping an error. (A 16-byte key can reach 128-bit and no higher.) - // Do NOT "fix" this by relaxing that guard in `KeyMaterial`: core's - // `test_hazardous_ops_error_handling` requires it to stay enforced. - if ss > &SecurityStrength::from_bytes(KEY_LEN) { - continue; - } - // Tag the key at an arbitrary strength for the purpose of this test. - do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); + for len in 0..=(3 * FINAL_LEN + 5) { + let msg = &DUMMY_SEED[..len]; + let mut ct = vec![0u8; len]; + let (nonce, ct_len, tag) = Enc::encrypt_out_detached(&key, b"", msg, &mut ct).unwrap(); + assert_eq!(ct_len, len, "the toy never expands the data, only the finalizer flushes"); + + for chunk in [1usize, 2, 3, HOLD_BACK, FINAL_LEN, FINAL_LEN + 1, len.max(1)] { + let (mut enc, _) = Enc::do_encrypt_init(&key).unwrap(); + let mut chunked = Vec::new(); + for piece in msg.chunks(chunk) { + let expect = enc.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "len {len} chunk {chunk}: update_out_len must be exact"); + chunked.extend_from_slice(&buf[..n]); + } + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, chunked_tag) = enc.do_final_out_detached(&mut final_buf).unwrap(); + chunked.extend_from_slice(&final_buf[..final_len]); + assert_eq!(chunked, ct, "len {len} chunk {chunk}: chunking must not be visible"); + assert_eq!( + chunked_tag, tag, + "len {len} chunk {chunk}: tag must not depend on chunking" + ); - // The key-strength requirement must be enforced both by the AEAD one-shot and by the - // plain one (encrypt_out), so exercise both. - let check_strength = |result: Result<(), SymmetricCipherError>| match result { - Ok(_) => { - if ss >= &C::MAX_SECURITY_STRENGTH { /* good */ - } else { - panic!("Should have been a strong enough key"); - } + // detached: the decryptor releases what it held back as a possible tag in + // `do_final_out_detached`, alongside what it held back of its own accord + let mut dec = Dec::do_decrypt_init(&key, &nonce).unwrap(); + let mut pt = Vec::new(); + for piece in ct.chunks(chunk) { + let expect = dec.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "len {len} chunk {chunk}: update_out_len must be exact"); + pt.extend_from_slice(&buf[..n]); } - Err(SymmetricCipherError::KeyMaterialError(_)) => { - if ss < &C::MAX_SECURITY_STRENGTH { /* good */ - } else { - panic!("Should not have accepted a key weaker than algorithm"); - } + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_final_out_detached(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(pt, msg, "len {len} chunk {chunk}: detached round trip"); + + // inline: the same stream with the tag on the end, chunked the same way + let mut inline = ct.clone(); + inline.extend_from_slice(&tag); + let mut dec = Dec::do_decrypt_init(&key, &nonce).unwrap(); + let mut pt = Vec::new(); + for piece in inline.chunks(chunk) { + let expect = dec.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "len {len} chunk {chunk}: update_out_len must be exact"); + pt.extend_from_slice(&buf[..n]); } - _ => panic!("Unexpected error"), - }; - check_strength(C::aead_encrypt_out(&key, aad, msg, &mut ct).map(|_| ())); - check_strength(C::encrypt_out(&key, msg, &mut ct).map(|_| ())); + let (last, data_len) = dec.do_final().unwrap(); + pt.extend_from_slice(&last[..data_len]); + assert_eq!(pt, msg, "len {len} chunk {chunk}: inline round trip"); + } + + // The inline `ciphertext || tag` layout, which is where a buffering cipher makes + // `do_final` do two things at once: flush the held-back bytes and then append the tag + // after them. + let (mut enc, nonce) = Enc::do_encrypt_init(&key).unwrap(); + let mut inline = vec![0u8; enc.update_out_len(len)]; + let written = enc.do_update_out(msg, &mut inline).unwrap(); + assert!(written < len || len == 0, "len {len}: the toy must be holding something back"); + let (last, last_len) = enc.do_final().unwrap(); + inline.extend_from_slice(&last[..last_len]); + assert_eq!( + inline.len(), + len + TAG_LEN, + "len {len}: inline layout is the message plus a tag" + ); + + let mut one = vec![0u8; Enc::encrypt_out_len(len)]; + let (one_nonce, one_len) = Enc::encrypt_out_with_aad(&key, b"", msg, &mut one).unwrap(); + assert_eq!(&one[..one_len], &inline[..], "len {len}: one-shot must agree"); + assert_eq!(one_nonce, nonce); + // Exactly the buffer it asks for: that is what makes the `+ data_len` arithmetic in + // the one-shot observable, since with a generous buffer any arithmetic there would do. + let mut back = vec![0u8; Dec::decrypt_out_max_len(one_len)]; + let back_len = + Dec::decrypt_out_with_aad(&key, &one_nonce, b"", &one[..one_len], &mut back) + .unwrap(); + assert_eq!(&back[..back_len], msg, "len {len}: inline one-shot round trip"); + + // Every other one-shot over the toy too: its final calls flush real data, which is + // what makes the `written + final_len` arithmetic in each of them observable. + let mut ct_rng = vec![0u8; len]; + let (_, n_rng, tag_rng) = Enc::encrypt_out_rng_detached( + &key, + &mut FixedSeedRNG::::new([0u8; NONCE_LEN]), + b"", + msg, + &mut ct_rng, + ) + .unwrap(); + assert_eq!(&ct_rng[..n_rng], &ct[..], "len {len}: encrypt_out_rng_detached"); + assert_eq!(tag_rng, tag, "len {len}: encrypt_out_rng_detached tag"); + let mut back = vec![0u8; len]; + let back_len = + Dec::decrypt_out_detached(&key, &nonce, b"", &ct, &tag, &mut back).unwrap(); + assert_eq!(&back[..back_len], msg, "len {len}: decrypt_out_detached"); + let mut plain = vec![0u8; Enc::encrypt_out_len(len)]; + let (plain_nonce, plain_len) = Enc::encrypt_out(&key, msg, &mut plain).unwrap(); + assert_eq!(&plain[..plain_len], &inline[..], "len {len}: encrypt_out"); + let mut back = vec![0u8; Dec::decrypt_out_max_len(plain_len)]; + let back_len = + Dec::decrypt_out(&key, &plain_nonce, &plain[..plain_len], &mut back).unwrap(); + assert_eq!(&back[..back_len], msg, "len {len}: decrypt_out"); + + // For any length past the hold-back window, at least one prefix of the input must be + // held back rather than released immediately -- the property this whole test exists + // to pin. (For `len < HOLD_BACK` nothing is ever releasable until `do_final`, which is + // also correct but does not exercise `do_update_out` returning less than it was + // given.) + if len > HOLD_BACK { + let (mut enc, _) = Enc::do_encrypt_init(&key).unwrap(); + let first = &msg[..1]; + let mut buf = vec![0u8; enc.update_out_len(first.len())]; + let n = enc.do_update_out(first, &mut buf).unwrap(); + assert_eq!(n, 0, "len {len}: the first byte alone must be held back, not released"); + } } } } diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index fbbe7006..66348daa 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -1,254 +1,324 @@ //! Generic behaviour tests for anything that implements [`XOF`]. use bouncycastle_core::errors::HashError; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{XOF, XOFSqueezer}; /// Instance of the test framework. pub struct TestFrameworkXOF { // Put any config options here - /// Can be disabled for XOFs that don't implement [`XOF::absorb_last_partial_byte`]. + /// Can be disabled for XOFs that don't support a partial final byte of input. pub enable_partial_byte_tests: bool, + /// Set for XOFs whose [`XOFSqueezer::do_final`] binds the length it is asked for when it is + /// the first read -- the SP 800-185 forms, which then compute their fixed-length counterpart + /// rather than the XOF stream. The suite cannot know those bytes, so it checks the split + /// instead and leaves the values to the implementation's own vector tests. + pub do_final_binds_output_length: bool, } impl TestFrameworkXOF { /// pub fn new() -> Self { - Self { enable_partial_byte_tests: true } + Self { enable_partial_byte_tests: true, do_final_binds_output_length: false } } - /// Test the absorb-after-squeeze members of trait XOF against the given input-output pair. - /// This is not exhaustive; it covers the rules laid out in the "State and Absorb-after-Squeeze" - /// section of the [`XOF`] docs: an XOF is an absorb phase followed by a squeeze phase, once - /// squeezing has begun any further absorb returns [`HashError::InvalidState`], and a rejected - /// absorb leaves the object usable for further squeezing. - /// `expected_output` is the result of squeezing `expected_output.len()` bytes after absorbing - /// `input`. - pub fn test_xof(&self, input: &[u8], expected_output: &[u8]) { - /*** fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> ***/ - // Absorbing is fine, repeatedly, right up until the first squeeze. - let mut xof = X::default(); + /// Exercises the trait against a known input-output pair. + /// + /// `expected_output` is the result of reading `expected_output.len()` bytes after absorbing + /// `input`. There is deliberately no absorb-after-squeeze test: [`XOF::into_squeezer`] consumes + /// the XOF, so absorbing afterwards is not expressible and there is no runtime rule left to + /// check. That guarantee is asserted instead by `compile_fail` doctests on the implementors. + pub fn test_xof(&self, make: impl Fn() -> X, input: &[u8], expected_output: &[u8]) { + /*** fn do_update(&mut self, data: &[u8]) ***/ + // Feeding the input in pieces must equal feeding it in one go. + let mut xof = make(); for chunk in input.chunks(16) { - xof.absorb(chunk).expect("absorb() before any squeeze must succeed"); + xof.do_update(chunk); } - - // "once the XOF has begun squeezing, attempting to absorb more will return - // HashError::InvalidState" - // squeeze() begins squeezing ... - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(expected_output.len()); - assert!( - matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), - "absorb() after squeeze() must return InvalidState" + assert_eq!( + xof.into_squeezer().do_output(expected_output.len()), + expected_output, + "chunked input must equal a single update" ); - // ... and so does squeeze_out() - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let mut output = vec![0u8; expected_output.len()]; - xof.squeeze_out(&mut output); - assert!( - matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), - "absorb() after squeeze_out() must return InvalidState" + /*** fn do_output(&mut self, num_bytes: usize) -> Vec ***/ + let mut xof = make(); + xof.do_update(input); + assert_eq!( + xof.into_squeezer().do_output(expected_output.len()), + expected_output, + "do_output must produce the expected bytes" ); - /*** fn squeeze(&mut self, num_bytes: usize) -> Vec ***/ - /*** fn squeeze_out(&mut self, output: &mut [u8]) -> usize ***/ - // "... and leave the object usable for further squeezing" - // So squeezing the output in two halves around a rejected absorb must give exactly the same - // stream as one clean squeeze: a rejected absorb must not consume, pad, or otherwise - // disturb the sponge. + /*** fn do_output_out(&mut self, output: &mut [u8]) -> usize ***/ + // Pre-filled so that the documented zeroization is observable. + let mut output = vec![0xFFu8; expected_output.len()]; + let mut xof = make(); + xof.do_update(input); + let n = xof.into_squeezer().do_output_out(&mut output); + assert_eq!(n, expected_output.len(), "do_output_out must report what it wrote"); + assert_eq!(output, expected_output, "do_output_out must agree with do_output"); + + // One output stream: reading it in two goes equals reading it in one. let split = expected_output.len() / 2; + let mut xof = make(); + xof.do_update(input); + let mut out = xof.into_squeezer(); + let first = out.do_output(split); + let mut second = vec![0u8; expected_output.len() - split]; + out.do_output_out(&mut second); + assert_eq!( + [first, second].concat(), + expected_output, + "successive reads must continue one stream" + ); - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let first_half = xof.squeeze(split); - assert!(xof.absorb(b"more input").is_err()); - let mut second_half = vec![0u8; expected_output.len() - split]; - xof.squeeze_out(&mut second_half); + // do_output_out zeroizes the caller's buffer before writing, so a dirty one still comes + // back holding exactly the output. + let mut buf = vec![0xFFu8; expected_output.len()]; + let mut xof = make(); + xof.do_update(input); + let n = xof.into_squeezer().do_output_out(&mut buf); + assert_eq!(n, expected_output.len()); + assert_eq!(buf, expected_output, "do_output_out must zeroize before writing"); + /*** fn do_final(self, num_bytes: usize) -> Vec ***/ + // As the first read, do_final is either the end of this stream or -- for a XOF that binds + // the length it is asked for -- a different function altogether. Both are pinned here; the + // second's bytes belong to the implementation's own vector tests. + let mut xof = make(); + xof.do_update(input); + let first_read = xof.into_squeezer().do_final(expected_output.len()); + if self.do_final_binds_output_length { + assert_ne!( + first_read, expected_output, + "a length-binding do_final must not reproduce the XOF stream" + ); + } else { + assert_eq!(first_read, expected_output, "do_final must produce the expected bytes"); + } + + /*** fn do_final_out(self, output: &mut [u8]) -> usize ***/ + // Pre-filled so that the documented zeroization is observable. + let mut buf = vec![0xFFu8; expected_output.len()]; + let mut xof = make(); + xof.do_update(input); + let n = xof.into_squeezer().do_final_out(&mut buf); + assert_eq!(n, expected_output.len(), "do_final_out must report what it wrote"); + assert_eq!(buf, first_read, "do_final_out must agree with do_final"); + + // Once a read has happened there is nothing left to bind, so do_final continues the stream + // that read began rather than restarting it -- however the two behave as a first read. + let mut xof = make(); + xof.do_update(input); + let mut out = xof.into_squeezer(); + let first = out.do_output(split); assert_eq!( - first_half.as_slice(), - &expected_output[..split], - "Incorrect output for input / the output stream must be unchanged by a rejected absorb" + [first, out.do_final(expected_output.len() - split)].concat(), + expected_output, + "do_final after a read must continue that stream" ); + + /*** fn xof(self, data: &[u8], result_len: usize) -> Vec ***/ + // The one-shots name their length and never come back, so they read as do_final does: for + // a XOF that binds its output length they produce what do_final produced above, not the + // stream. + let one_shot: &[u8] = + if self.do_final_binds_output_length { &first_read } else { expected_output }; assert_eq!( - second_half.as_slice(), - &expected_output[split..], - "Incorrect output for input / the output stream must continue as if the rejected absorb never happened" + make().xof(input, expected_output.len()), + one_shot, + "the one-shot must equal update-then-do_final" ); + let mut output = vec![0xFFu8; expected_output.len()]; + let n = make().xof_out(input, &mut output); + assert_eq!(n, expected_output.len()); + assert_eq!(output, one_shot, "xof_out must agree with xof"); + + /*** Clone: a XOF mid-absorb can be forked ***/ + // The clone continues from the same absorbed prefix and owns its own sponge. + let (prefix, tail) = input.split_at(input.len() / 2); + let mut original = make(); + original.do_update(prefix); + let mut forked = original.clone(); + original.do_update(tail); + forked.do_update(tail); + assert_eq!( + original.into_squeezer().do_output(expected_output.len()), + expected_output, + "the original must be unaffected by cloning" + ); + assert_eq!( + forked.into_squeezer().do_output(expected_output.len()), + expected_output, + "a clone must continue from the same absorbed prefix" + ); + + let mut original = make(); + original.do_update(prefix); + let mut forked = original.clone(); + original.do_update(tail); + forked.do_update(&[0xA5]); + forked.do_update(tail); + assert_ne!( + forked.into_squeezer().do_output(expected_output.len()), + original.into_squeezer().do_output(expected_output.len()), + "a clone must have its own state, not share the original's" + ); + + /*** the Hash half: a XOF is a hash ***/ + self.test_xof_as_hash(&make, input, expected_output); + if self.enable_partial_byte_tests { - /*** fn absorb_last_partial_byte(&mut self, partial_byte: u8, num_bits: usize) -> Result<(), HashError> ***/ - // The same phase rule applies to absorb_last_partial_byte() once squeezing has begun. - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(expected_output.len()); - assert!( - matches!(xof.absorb_last_partial_byte(0x01, 3), Err(HashError::InvalidState(_))), - "absorb_last_partial_byte() after squeeze() must return InvalidState" - ); + self.test_xof_partial_bits(&make, input, expected_output); + } + } - // "Unlike XOF::absorb, this switches the XOF from Absorbing mode into Squeezing mode - // because absorbing more input after absorbing a partial byte is undefined - // behaviour." - // So it leaves the object in the same state a squeeze does, for every valid num_bits, - // with no squeeze having happened at all. - for num_bits in 0..=7 { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - xof.absorb_last_partial_byte(0xFF, num_bits) - .expect("absorb_last_partial_byte() must succeed for num_bits in 0..=7"); - let expected_partial_output = xof.squeeze(expected_output.len()); - - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - xof.absorb_last_partial_byte(0xFF, num_bits) - .expect("absorb_last_partial_byte() must succeed for num_bits in 0..=7"); - - assert!( - matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), - "absorb() after absorb_last_partial_byte() must return InvalidState / num_bits: {num_bits}" - ); - assert!( - matches!( - xof.absorb_last_partial_byte(0xFF, num_bits), - Err(HashError::InvalidState(_)) - ), - "a second absorb_last_partial_byte() must return InvalidState / num_bits: {num_bits}" - ); + /// The inherited [`Hash`] surface. `XOF: Hash`, so SHAKE can be used wherever a hash is wanted; + /// these checks pin that the inherited methods agree with the XOF ones. + fn test_xof_as_hash(&self, make: impl Fn() -> X, input: &[u8], expected_output: &[u8]) { + let xof = make(); + let output_len = xof.output_len(); + assert!(output_len > 0, "output_len must be positive"); + assert!(xof.block_bitlen() > 0, "block_bitlen must be positive"); + assert!( + xof.block_bitlen().is_multiple_of(8), + "block_bitlen must be a whole number of bytes" + ); - // ... and, again, the rejections must leave the object usable for further squeezing. - assert_eq!( - xof.squeeze(expected_output.len()), - expected_partial_output, - "the output stream must be unchanged by a rejected absorb / num_bits: {num_bits}" - ); - } + let mut a = make(); + a.do_update(input); + let via_hash = a.do_final(); + assert_eq!(via_hash.len(), output_len, "do_final must produce output_len bytes"); + + let mut b = make(); + b.do_update(input); + if self.do_final_binds_output_length { + // The Hash view is a final read at the nominal length, so it binds that length and is + // a different function from the stream -- and must agree with the squeezer's own final + // read at the same length. + assert_ne!( + via_hash, + b.into_squeezer().do_output(output_len), + "a length-binding Hash::do_final must not be the stream truncated" + ); + let mut c = make(); + c.do_update(input); + assert_eq!( + via_hash, + c.into_squeezer().do_final(output_len), + "Hash::do_final must be the squeezer's final read at output_len" + ); + } else { + // do_final is do_output at the nominal length: the same stream, truncated. + assert_eq!( + via_hash, + b.into_squeezer().do_output(output_len), + "do_final must equal do_output(output_len)" + ); - // Helper: the output stream of `input` finished with the top `num_bits` bits of - // `partial_byte`. - let partial_absorb_output = |partial_byte: u8, num_bits: usize| -> Vec { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - xof.absorb_last_partial_byte(partial_byte, num_bits) - .expect("absorb_last_partial_byte() must succeed for num_bits in 0..=7"); - xof.squeeze(expected_output.len()) - }; - - // "0 is a valid value and means the message ends on a byte boundary (equivalent to - // XOF::absorb)." - // So the message is still just `input`, whatever the discarded bits of partial_byte are. - for partial_byte in [0x00u8, 0x01, 0x80, 0xA5, 0xFF] { + // ... and a prefix of the longer output, because a XOF cannot diversify by length. + if expected_output.len() >= output_len { assert_eq!( - partial_absorb_output(partial_byte, 0), - expected_output, - "num_bits = 0 must leave the message byte-aligned / partial_byte: {partial_byte:#04X}" + &via_hash[..], + &expected_output[..output_len], + "do_final must be a prefix of the longer output" ); } + } - // "the num_bits message bits are the most significant bits of partial_byte ... and the - // low 8 - num_bits bits (the BIT STRING's "unused bits") are ignored". - // So the unused low bits are not part of the message and must not change the output. - for num_bits in 0..=7 { - // the used bits are the top num_bits; built in u16 so that num_bits == 0 cannot overflow - let mask = (0xFF00u16 >> num_bits) as u8; - for partial_byte in [0x00u8, 0x5A, 0xA5, 0xFF] { - assert_eq!( - partial_absorb_output(partial_byte, num_bits), - partial_absorb_output(partial_byte & mask, num_bits), - "the low 8 - num_bits = {} bits must be ignored / partial_byte: {partial_byte:#04X}", - 8 - num_bits - ); - } - } + // do_final_out fills the caller's buffer, zeroizing it first. + let mut buf = vec![0xFFu8; output_len]; + let mut c = make(); + c.do_update(input); + let n = c.do_final_out(&mut buf); + assert_eq!(n, output_len); + assert_eq!(buf, via_hash, "do_final_out must agree with do_final"); - // "num_bits must be in 0..=7; larger values return HashError::InvalidLength." - // Checked on an absorbing object, so that it is the range check rejecting the call and - // not the phase check above. - for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - assert!( - matches!( - xof.absorb_last_partial_byte(0xFF, num_bits), - Err(HashError::InvalidLength(_)) - ), - "absorb_last_partial_byte() must reject num_bits = {num_bits} with InvalidLength" - ); - } + // The one-shot Hash entry points. + assert_eq!(make().hash(input), via_hash, "hash must equal update-then-do_final"); + let mut buf = vec![0xFFu8; output_len]; + assert_eq!(make().hash_out(input, &mut buf), output_len); + assert_eq!(buf, via_hash, "hash_out must agree with hash"); + } - /*** fn squeeze_partial_byte_final(self, num_bits: usize) -> Result ***/ - /*** fn squeeze_partial_byte_final_out(self, num_bits: usize, output: &mut u8) -> Result<(), HashError> ***/ - // "in the most significant num_bits bits of the returned u8, first output bit first, with - // the low 8 - num_bits "unused" bits zero." - // They are the first bits of the next byte of the output stream, which `expected_output` - // gives us: after squeezing `split` bytes, the next byte is expected_output[split]. In - // that byte the first output bit is the LSB (FIPS 202 B.1 / the byte-oriented stream), so - // the expected partial byte is the bit-reversal of it, masked to the top num_bits bits. - let split = expected_output.len() / 2; - for num_bits in 0..=7 { - // the used bits are the top num_bits; built in u16 so that num_bits == 0 cannot overflow - let mask = (0xFF00u16 >> num_bits) as u8; - - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - let partial_byte = xof - .squeeze_partial_byte_final(num_bits) - .expect("squeeze_partial_byte_final() must succeed for num_bits in 0..=7"); + /// A partial final byte of input, in both the XOF and the Hash spelling. + fn test_xof_partial_bits( + &self, + make: impl Fn() -> X, + input: &[u8], + expected_output: &[u8], + ) { + // num_bits = 0 means the message ended on a byte boundary, so it must match plain input. + let mut xof = make(); + xof.do_update(input); + assert_eq!( + xof.into_squeezer_partial_bits(0, 0) + .expect("0 is in range") + .do_output(expected_output.len()), + expected_output, + "num_bits = 0 must equal a byte-aligned message" + ); - assert_eq!( - partial_byte, - expected_output[split].reverse_bits() & mask, - "the squeezed bits must be the first bits of the next output byte, MSB-first / num_bits: {num_bits}" - ); - assert_eq!( - partial_byte & !mask, - 0x00, - "the unused low bits of the result must be zero / num_bits: {num_bits}" - ); + // A real partial byte must change the output, and both spellings must agree. + for num_bits in 1..=7usize { + let mut a = make(); + a.do_update(input); + let with_bits = a + .into_squeezer_partial_bits(0xFE, num_bits) + .expect("num_bits is in 1..=7") + .do_output(expected_output.len()); + assert_ne!( + with_bits, expected_output, + "a partial byte must change the output / num_bits: {num_bits}" + ); - // "The same as XOF::squeeze_partial_byte_final, but writes into the provided output - // byte. The output byte is zeroized before the result is written." - // Pre-filled with 0xFF so that the zeroization is observable. - let mut output_byte = 0xFFu8; - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - xof.squeeze_partial_byte_final_out(num_bits, &mut output_byte) - .expect("squeeze_partial_byte_final_out() must succeed for num_bits in 0..=7"); - assert_eq!( - output_byte, partial_byte, - "squeeze_partial_byte_final_out() must agree with squeeze_partial_byte_final() / num_bits: {num_bits}" - ); - } + let mut b = make(); + b.do_update(input); + let via_hash = b.do_final_partial_bits(0xFE, num_bits).expect("num_bits is in 1..=7"); + assert_eq!( + via_hash, + with_bits[..via_hash.len()], + "do_final_partial_bits must be the same stream / num_bits: {num_bits}" + ); - // "num_bits must be in 0..=7; larger values return HashError::InvalidLength." - for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] { - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - assert!( - matches!( - xof.squeeze_partial_byte_final(num_bits), - Err(HashError::InvalidLength(_)) - ), - "squeeze_partial_byte_final() must reject num_bits = {num_bits} with InvalidLength" - ); + let mut buf = vec![0xFFu8; via_hash.len()]; + let mut c = make(); + c.do_update(input); + let n = c + .do_final_partial_bits_out(0xFE, num_bits, &mut buf) + .expect("num_bits is in 1..=7"); + assert_eq!(n, via_hash.len()); + assert_eq!(buf, via_hash, "the _out form must agree / num_bits: {num_bits}"); + } - let mut output_byte = 0u8; - let mut xof = X::default(); - xof.absorb(input).expect("absorb() before any squeeze must succeed"); - let _ = xof.squeeze(split); - assert!( - matches!( - xof.squeeze_partial_byte_final_out(num_bits, &mut output_byte), - Err(HashError::InvalidLength(_)) - ), - "squeeze_partial_byte_final_out() must reject num_bits = {num_bits} with InvalidLength" - ); - } + // "num_bits must be in 0..=7; larger values return HashError::InvalidLength." + for num_bits in [8usize, 9, 15, 16, 64, usize::MAX] { + let mut xof = make(); + xof.do_update(input); + assert!( + matches!( + xof.into_squeezer_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + ), + "into_squeezer_partial_bits must reject num_bits = {num_bits}" + ); + + let mut xof = make(); + xof.do_update(input); + assert!( + matches!( + xof.do_final_partial_bits(0xFF, num_bits), + Err(HashError::InvalidLength(_)) + ), + "do_final_partial_bits must reject num_bits = {num_bits}" + ); } } } + +impl Default for TestFrameworkXOF { + fn default() -> Self { + Self::new() + } +} diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 98478524..baec7baf 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -15,117 +15,509 @@ use crate::key_material::KeyMaterial; use crate::key_material::KeyType; // end of imports needed for docs -/// The basic functions of an Authenticated Encryption with Addititional Data cipher. -pub trait AEADCipher: - Algorithm + Sized +/// What the allocating one-shot [`AEADCipherEncryptor::encrypt_detached`] hands back: the nonce it +/// generated, the ciphertext, and the tag, in that order. A named type because the bare triple is +/// past what is readable inline (clippy's `type_complexity`). +#[cfg(feature = "std")] +pub type AEADEncrypted = + ([u8; NONCE_LEN], Vec, [u8; TAG_LEN]); + +/// The decryption half of an AEAD cipher's streaming API; see [`AEADCipherEncryptor`], whose notes +/// on the AAD phase, the two tag layouts, buffering, and the `Result` all apply here too. +/// +/// This extends [`SymmetricCipherDecryptor`], whose methods are the AEAD with no associated data +/// and the tag inline -- the last `TAG_LEN` bytes of the ciphertext. That is why a decryptor has +/// to hold back the last `TAG_LEN` bytes it has seen at all times: the tag is only identifiable +/// once the stream ends, and [`SymmetricCipherDecryptor::do_update_out`] cannot know which final +/// method will be called. With the tag detached those held-back bytes turn out to be ciphertext, +/// and [`do_final_out_detached`](Self::do_final_out_detached) decrypts them; with it inline, +/// [`SymmetricCipherDecryptor::do_final`] checks them as the tag. So `FINAL_LEN` is at least +/// `TAG_LEN`, plus whatever else the cipher holds back of its own accord. +/// +/// # The plaintext is not authenticated until the final call returns `Ok` +/// +/// This is the one thing a streaming AEAD API cannot hide from its caller. +/// [`SymmetricCipherDecryptor::do_update_out`] releases plaintext as soon as it can, long before +/// there is a tag to check it against, so a caller that *uses* those bytes before +/// [`do_final_out_detached`](Self::do_final_out_detached) or [`SymmetricCipherDecryptor::do_final`] has +/// returned `Ok` is acting on unauthenticated plaintext -- bytes an attacker may have chosen. +/// Preventing exactly that is what the tag is for. A streaming caller must therefore treat +/// everything `do_update_out` produces as untrusted until the final call succeeds, and scrub it if +/// it does not. +/// +/// The one-shots -- [`decrypt_out_detached`](Self::decrypt_out_detached), +/// [`decrypt_out_with_aad`](Self::decrypt_out_with_aad) and [`SymmetricCipherDecryptor::decrypt_out`] -- have +/// no such caveat: each owns the whole message, so it zeroizes the buffer itself before returning +/// the error. +pub trait AEADCipherDecryptor< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +>: SymmetricCipherDecryptor { - #[cfg(feature = "std")] - /// A one-shot API to encrypt some plaintext with the given key, with no additional - /// authenticated data. + /// Absorbs additional authenticated data; see [`AEADCipherEncryptor::do_update_aad`] for the + /// rules, which are the same on both sides. The concatenation of what a decryptor absorbs must + /// be byte-for-byte the concatenation the encryptor absorbed, or the tag check fails. /// - /// Returns the generated nonce, and the ciphertext as a `Vec`, so it needs the `std` - /// feature. - /// This API does not allow for including additional data (AAD), for that use [`aead_encrypt`](Self::aead_encrypt). - fn encrypt( - key: &KeyMaterial, - plaintext: &[u8], - ) -> Result<([u8; NONCE_LEN], Vec), SymmetricCipherError>; + /// # Errors + /// [`SymmetricCipherError::StateError`] if called with a non-empty `aad` after + /// [`SymmetricCipherDecryptor::do_update_out`]. + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError>; + + /// Finishes the decryption with the tag detached, consuming the decryptor: decrypts whatever + /// ciphertext was held back into `plaintext` -- including the last `TAG_LEN` bytes, which with + /// the tag carried separately are ciphertext like the rest -- computes the tag over the AAD and + /// ciphertext it has seen, and compares it against `tag`. Returns how many leading bytes of + /// `plaintext` are data; the remainder is not data and must not be used. `Ok` is the only + /// thing that makes those bytes -- or anything already released by + /// [`SymmetricCipherDecryptor::do_update_out`] -- trustworthy. + /// + /// # Errors + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. Implementors must + /// compare in constant time, and the caller learns only that the check failed. + fn do_final_out_detached( + self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; FINAL_LEN], + ) -> Result; - /// As [`encrypt`](Self::encrypt), writing into a caller-supplied buffer. + /// As [`do_final_out_detached`](Self::do_final_out_detached), returning the final buffer + /// together with the number of leading bytes of it that are plaintext, the shape of + /// [`SymmetricCipherDecryptor::do_final`]. The two are provided the other way round from the + /// base trait's pair -- the `_out` form is the one an implementor writes -- because that is the + /// form that lets an implementor decrypt the held-back bytes straight into the caller's buffer. + /// On failure no buffer is returned, so nothing unauthenticated is left behind by this call. /// - /// See the documentation for the underlying implementation for how big the ciphertext buffer - /// must be; an AEAD needs room for the tag as well as the data. Returns the generated nonce and - /// the number of bytes written. - fn encrypt_out( - key: &KeyMaterial, - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError>; + /// # Errors + /// As [`do_final_out_detached`](Self::do_final_out_detached). + fn do_final_detached( + self, + tag: &[u8; TAG_LEN], + ) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError> { + let mut plaintext = [0u8; FINAL_LEN]; + let data_len = self.do_final_out_detached(tag, &mut plaintext)?; + Ok((plaintext, data_len)) + } - #[cfg(feature = "std")] - /// A one-shot API to decrypt what [`encrypt`](Self::encrypt) produced, with no additional - /// authenticated data. Returns the plaintext as a `Vec`, so it needs the `std` feature. + /// An upper bound on the plaintext recovered from `ciphertext_len` bytes of ciphertext with + /// the tag detached, i.e. the buffer [`decrypt_out_detached`](Self::decrypt_out_detached) + /// requires. The default returns `ciphertext_len` itself, which is exact for every conformant + /// AEAD: unlike a padding scheme, an AEAD never expands or shrinks the data it is given, only + /// adds the separate `tag`. + fn decrypt_out_max_len_detached(ciphertext_len: usize) -> usize { + ciphertext_len + } + + /// One-shot with the tag detached: decrypts `ciphertext` into `plaintext`, which needs + /// [`decrypt_out_max_len_detached`](Self::decrypt_out_max_len_detached) bytes, under `nonce` + /// and `aad`, and checks `tag`. Returns the number of plaintext bytes written. + /// + /// Unlike the streaming methods this releases nothing unauthenticated: on failure `plaintext` + /// is zeroized before the error is returned, so a caller who ignores the `Result` is left with + /// zeros rather than attacker-chosen plaintext. /// /// # Errors - /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. The caller learns - /// only that decryption failed. - fn decrypt( + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, checked + /// before any work is done; otherwise whatever the streaming methods return, including + /// [`do_final_out_detached`](Self::do_final_out_detached)'s. + fn decrypt_out_detached( key: &KeyMaterial, - init_data: [u8; NONCE_LEN], + nonce: &[u8; NONCE_LEN], + aad: &[u8], ciphertext: &[u8], - ) -> Result, SymmetricCipherError>; + tag: &[u8; TAG_LEN], + plaintext: &mut [u8], + ) -> Result { + let needed = Self::decrypt_out_max_len_detached(ciphertext.len()); + if plaintext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let mut dec = Self::do_decrypt_init(key, nonce)?; + dec.do_update_aad(aad)?; + let written = dec.do_update_out(ciphertext, plaintext)?; + let mut final_buf = [0u8; FINAL_LEN]; + match dec.do_final_out_detached(tag, &mut final_buf) { + Ok(final_len) => { + // Everything held back comes out of `do_final_out_detached`, so `written + final_len` + // is the ciphertext length, which `decrypt_out_max_len_detached` bounds. + plaintext[written..written + final_len].copy_from_slice(&final_buf[..final_len]); + Ok(written + final_len) + } + Err(e) => { + // As in the trait docs: what `do_update_out` already released is unauthenticated, + // and this one-shot owns the whole message, so it does not leave that in the + // caller's hands. A plain `fill` rather than a volatile write because `core` is + // `#![forbid(unsafe_code)]`; the store is to the caller's own buffer, which the + // caller may read after this returns, so it is not a dead store the optimizer is + // entitled to drop. + plaintext[..written].fill(0); + Err(e) + } + } + } - /// As [`decrypt`](Self::decrypt), writing into a caller-supplied buffer so it is available - /// without `std`. Returns the number of bytes written. + /// One-shot over the inline `ciphertext || tag` layout with associated data: the trailing + /// `TAG_LEN` bytes of `ciphertext` are the tag. This is [`SymmetricCipherDecryptor::decrypt_out`] + /// with an `aad`, and needs the same + /// [`decrypt_out_max_len`](SymmetricCipherDecryptor::decrypt_out_max_len) bytes of + /// `plaintext`. As with every AEAD one-shot, `plaintext` is zeroized when the tag does not + /// verify. /// /// # Errors - /// As [`decrypt`](Self::decrypt). - fn decrypt_out( + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, checked + /// before any work is done; [`SymmetricCipherError::DecryptionFailed`] if `ciphertext` is + /// shorter than the tag it is supposed to end with; + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. + fn decrypt_out_with_aad( key: &KeyMaterial, - init_data: [u8; NONCE_LEN], + nonce: &[u8; NONCE_LEN], + aad: &[u8], ciphertext: &[u8], plaintext: &mut [u8], - ) -> Result; + ) -> Result { + let needed = Self::decrypt_out_max_len(ciphertext.len()); + if plaintext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let mut dec = Self::do_decrypt_init(key, nonce)?; + dec.do_update_aad(aad)?; + let written = dec.do_update_out(ciphertext, plaintext)?; + match dec.do_final() { + Ok((last, data_len)) => { + // `decrypt_out_max_len` bounds `written + data_len`, so this fits in + // `plaintext[..needed]`. + plaintext[written..written + data_len].copy_from_slice(&last[..data_len]); + Ok(written + data_len) + } + Err(e) => { + // As in `decrypt_out_detached`. + plaintext[..written].fill(0); + Err(e) + } + } + } + + #[cfg(feature = "std")] + /// One-shot, allocating, with the tag detached: as + /// [`decrypt_out_detached`](Self::decrypt_out_detached), returning the plaintext as a + /// `Vec` of exactly the recovered length. Only available with the `std` feature. + fn decrypt_detached( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + ) -> Result, SymmetricCipherError> { + let mut plaintext = vec![0u8; Self::decrypt_out_max_len_detached(ciphertext.len())]; + let written = Self::decrypt_out_detached(key, nonce, aad, ciphertext, tag, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } #[cfg(feature = "std")] - /// A one-shot API to encrypt some plaintext with the given key. - /// A distinguishing feature of AEAD ciphers is the ability to provide additional authenticated data (AAD) - /// that is not encrypted but is protected by the authentication tag; ie it can be sent along with the ciphertext - /// and any tampering with it will result in the decryption operation failing the tag check. - /// This function returns the ciphertext as a `Vec`, and therefore is only available when compiling with std. - /// Returns a tuple containing a generated nonce, the ciphertext and the tag. - fn aead_encrypt( + /// One-shot, allocating, over the inline `ciphertext || tag` layout with associated data: as + /// [`decrypt_out_with_aad`](Self::decrypt_out_with_aad), returning the plaintext as a + /// `Vec` of exactly the recovered length. This is [`SymmetricCipherDecryptor::decrypt`] + /// with an `aad`. Only available with the `std` feature. + fn decrypt_with_aad( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + ) -> Result, SymmetricCipherError> { + let mut plaintext = vec![0u8; Self::decrypt_out_max_len(ciphertext.len())]; + let written = Self::decrypt_out_with_aad(key, nonce, aad, ciphertext, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } +} + +/// The encryption half of an AEAD cipher's streaming API. This extends +/// [`SymmetricCipherEncryptor`] -- the same separate-output, init-data-generating, +/// possibly-buffering shape -- with the two things authentication adds. +/// +/// # Two tag layouts +/// +/// The inherited [`SymmetricCipherEncryptor`] methods are this AEAD with no associated data and +/// the tag *inline*: [`SymmetricCipherEncryptor::do_final`] flushes whatever ciphertext was held +/// back and appends the tag after it, so the output is simply `ciphertext || tag`. `FINAL_LEN` is +/// therefore the tag length plus whatever the cipher holds back, and a caller that holds a +/// [`SymmetricCipherEncryptor`] can use an AEAD without knowing it is one. +/// +/// The methods ending in `_detached` hand the tag back as a value of its own instead, for callers +/// whose protocol carries it in a separate field. [`do_final_detached`](Self::do_final_detached) / +/// [`do_final_out_detached`](Self::do_final_out_detached) consume the encryptor, flush the +/// held-back ciphertext and return the tag, which the recipient needs for +/// [`AEADCipherDecryptor::do_final_detached`]. +/// +/// The methods ending in `_with_aad` are the inherited inline-tag one-shots with an `aad` +/// parameter added: [`encrypt_out_with_aad`](Self::encrypt_out_with_aad) is +/// [`SymmetricCipherEncryptor::encrypt_out`] with associated data, and so on. +/// +/// # Associated data +/// +/// An AEAD authenticates data it does not encrypt -- additional authenticated data (AAD), +/// typically a header that has to travel in the clear but must still be protected against +/// tampering -- and every AEAD construction absorbs that AAD *before* the plaintext. So +/// [`do_update_aad`](Self::do_update_aad) may be called any number of times after the constructor +/// and before the first [`SymmetricCipherEncryptor::do_update_out`], and returns +/// [`SymmetricCipherError::StateError`] thereafter. (An empty `aad` slice is a no-op and is +/// accepted at any point, so a generic caller may pass one unconditionally.) That is a runtime +/// error for the same reason [`XOF`] rejects absorb-after-squeeze at runtime: the phase order is a +/// property of a value's history, and encoding it in the type would cost every implementor an +/// extra type and an explicit transition. Not calling it at all is the no-AAD case the inherited +/// methods cover. +/// +/// Encryption and decryption are separate traits, as with [`BlockCipherEncryptor`] / +/// [`BlockCipherDecryptor`], so that the direction is encoded in the type. For an AEAD that also +/// buys away a class of runtime check: a single type serving both directions has to remember which +/// one it is and refuse the other's methods, whereas a paired-type implementation cannot be asked +/// the question. +/// +/// # The nonce is generated, not supplied +/// +/// The constructor draws the nonce itself and returns it for transmission alongside the ciphertext; +/// there is no API here for the caller to choose one, for the same reason as in +/// [`BlockCipherEncryptor`], but with sharper consequences. Reusing a nonce under one key does not +/// merely leak equality of plaintexts as it does for an unauthenticated mode -- for most AEAD +/// constructions it forfeits confidentiality of the affected messages and can expose the material +/// the tag is computed from, costing authenticity for every other message under that key. A caller +/// who genuinely needs a deterministic, caller-chosen nonce (to follow a protocol's construction, +/// or to run a spec's test vectors) should see the documentation of the underlying implementation, +/// which is where that hazard belongs. +/// +/// # A cipher may buffer +/// +/// [`SymmetricCipherEncryptor::do_update_out`] takes separate input and output buffers, because an +/// AEAD is not guaranteed to release a ciphertext byte the moment it sees the matching plaintext +/// byte. Ascon-AEAD128 does -- each rate-block byte is transformed independently of the others in +/// that block -- but a block-oriented AEAD holds back a partial final block, and every decryptor +/// holds back at least `TAG_LEN` bytes until it knows they are not the tag (see +/// [`AEADCipherDecryptor`]). [`SymmetricCipherEncryptor::update_out_len`] answers exactly how many +/// bytes the next call releases, so a caller never has to guess a buffer size or find plaintext +/// left over at the end of one it guessed too large; the concatenation of everything released, in +/// any chunking, plus the data part of the final call, is the ciphertext. +/// +/// # A length-dependent construction still has to buffer +/// +/// [`SymmetricCipherEncryptor::do_encrypt_init`] takes no length, and +/// [`do_update_aad`](Self::do_update_aad) / [`SymmetricCipherEncryptor::do_update_out`] are +/// open-ended by design -- most AEAD constructions never need to know a total in advance. +/// Ascon-AEAD128 does not; GCM, once it exists in this crate, will not either, because its length +/// block is computed from tallied byte counts at finalization, not up front. +/// +/// CCM (NIST SP 800-38C) is the exception, and this trait was partly implemented for CCM specifically +/// to find out whether it was: Appendix A.2.1 puts the payload's octet length inside `B0`, the very +/// first block the CBC-MAC absorbs, and Appendix A.2.2's AAD length encoding must precede the AAD bytes +/// it describes, so neither AAD nor payload can be authenticated until the caller has finished handing +/// over the total of each. A construction with that property has exactly two options, and changing the +/// shape of this trait for one implementor's benefit is neither of them: buffer the whole message +/// internally and pay the memory cost (see `bouncycastle_modes::CcmEncryptor` / `CcmDecryptor`), or, +/// preferably when the caller can supply the lengths up front -- which a packet-oriented protocol +/// generally can -- provide a separate, purpose-built non-buffering API instead (see +/// `bouncycastle_modes::Ccm::new`). Do not add a length parameter here to spare one implementor a +/// buffer; every other implementor would carry a parameter it never uses. +/// +/// # Why the data methods still return `Result` +/// +/// Nothing about the buffer can go wrong, and a constructed value is always ready to use, so +/// `do_update_out` has nothing to report for most ciphers. The `Result` is for the per-(key, nonce) +/// data limit an AEAD generally has -- past it the construction's security argument no longer +/// holds -- which a streaming API cannot check any earlier than the call that would cross it, and +/// for [`OutputBufferTooSmall`](SymmetricCipherError::OutputBufferTooSmall) if the caller +/// under-sized `ciphertext`. +pub trait AEADCipherEncryptor< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +>: SymmetricCipherEncryptor +{ + /// Absorbs `aad`: data that is authenticated by the tag but not encrypted. May be called + /// repeatedly before the first [`SymmetricCipherEncryptor::do_update_out`]; a sequence of calls + /// is equivalent to one call over the concatenation. An empty `aad` is a no-op. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if called with a non-empty `aad` after + /// [`SymmetricCipherEncryptor::do_update_out`] -- see the trait docs for why the AAD comes + /// first. An implementor whose buffering has a fixed capacity -- see "A length-dependent + /// construction still has to buffer" above -- may also return + /// [`SymmetricCipherError::GenericError`] if `aad` would exceed it; that is a property of the + /// implementor, not of this trait, so it is not listed as a general contract here. + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError>; + + /// Finishes the encryption with the tag detached, consuming the encryptor: flushes whatever + /// plaintext was held back, encrypted, into `ciphertext`, and returns how many leading bytes of + /// it are ciphertext together with the tag over the AAD and plaintext it has seen. The tag must + /// be transmitted with the ciphertext; the recipient passes it to + /// [`AEADCipherDecryptor::do_final_out_detached`]. + /// + /// `ciphertext` is `FINAL_LEN` long so that both final methods share one buffer size; the + /// flush written here is at most `FINAL_LEN - TAG_LEN` of it, the tag not being part of it. + fn do_final_out_detached( + self, + ciphertext: &mut [u8; FINAL_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError>; + + /// As [`do_final_out_detached`](Self::do_final_out_detached), returning the final buffer, the + /// number of leading bytes of it that are ciphertext, and the tag -- the shape of + /// [`SymmetricCipherEncryptor::do_final`] with the tag alongside. Provided over the `_out` + /// form, the other way round from the base trait's pair; see + /// [`AEADCipherDecryptor::do_final_detached`]. + fn do_final_detached( + self, + ) -> Result<([u8; FINAL_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + let mut ciphertext = [0u8; FINAL_LEN]; + let (out_len, tag) = self.do_final_out_detached(&mut ciphertext)?; + Ok((ciphertext, out_len, tag)) + } + + /// The exact ciphertext length for a `plaintext_len`-byte plaintext with the tag detached, i.e. + /// the buffer [`encrypt_out_detached`](Self::encrypt_out_detached) requires and the number of + /// bytes it writes (the tag is returned separately, not counted here). The default returns + /// `plaintext_len` itself, which holds for every conformant AEAD: unlike a padding scheme, an + /// AEAD never expands or shrinks the data it is given. + fn encrypt_out_len_detached(plaintext_len: usize) -> usize { + plaintext_len + } + + /// One-shot with the tag detached: encrypts `plaintext` into `ciphertext`, which needs + /// [`encrypt_out_len_detached`](Self::encrypt_out_len_detached) bytes, authenticating `aad` + /// along with it under a fresh nonce. Returns the generated nonce, the number of bytes + /// written, and the tag. + /// + /// Provided as `do_encrypt_init`, one `do_update_aad`, one `do_update_out` and + /// `do_final_out_detached`. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked + /// before any work is done; otherwise whatever the streaming methods return. + fn encrypt_out_detached( key: &KeyMaterial, aad: &[u8], plaintext: &[u8], - ) -> Result<([u8; NONCE_LEN], Vec, [u8; TAG_LEN]), SymmetricCipherError>; - /// A one-shot API to encrypt some plaintext with the given key. - /// A distinguishing feature of AEAD ciphers is the ability to provide additional authenticated data (AAD) - /// that is not encrypted but is protected by the authentication tag; ie it can be sent along with the ciphertext - /// and any tampering with it will result in the decryption operation failing the tag check. - /// Returns a tuple containing the randomly-generated nonce, number of bytes written to the ciphertext buffer, and the tag. - /// If you need a deterministic mode where you feed in the nonce, use the streaming API of [`BlockCipherEncryptor`] - /// or [`StreamCipherEncryptor`] as appropriate and feed the nonce into the IV field. - fn aead_encrypt_out( + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + let needed = Self::encrypt_out_len_detached(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let (mut enc, nonce) = Self::do_encrypt_init(key)?; + enc.do_update_aad(aad)?; + let written = enc.do_update_out(plaintext, ciphertext)?; + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_final_out_detached(&mut final_buf)?; + // Implementors that hold plaintext back must override `encrypt_out_len_detached` if + // `written + final_len` can exceed the plaintext length, so this fits in + // `ciphertext[..needed]`. + ciphertext[written..written + final_len].copy_from_slice(&final_buf[..final_len]); + Ok((nonce, written + final_len, tag)) + } + + /// As [`encrypt_out_detached`](Self::encrypt_out_detached), but sources randomness from the + /// provided RNG. + fn encrypt_out_rng_detached( + key: &KeyMaterial, + rng: &mut dyn RNG, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + let needed = Self::encrypt_out_len_detached(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let (mut enc, nonce) = Self::do_encrypt_init_rng(key, rng)?; + enc.do_update_aad(aad)?; + let written = enc.do_update_out(plaintext, ciphertext)?; + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_final_out_detached(&mut final_buf)?; + // As in `encrypt_out_detached`. + ciphertext[written..written + final_len].copy_from_slice(&final_buf[..final_len]); + Ok((nonce, written + final_len, tag)) + } + + /// One-shot into the inline `ciphertext || tag` layout with associated data: this is + /// [`SymmetricCipherEncryptor::encrypt_out`] with an `aad`, and needs the same + /// [`encrypt_out_len`](SymmetricCipherEncryptor::encrypt_out_len) bytes of `ciphertext`. + /// Returns the generated nonce and the total number of bytes written, tag included. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked + /// before any work is done; otherwise whatever the streaming methods return. + fn encrypt_out_with_aad( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + let needed = Self::encrypt_out_len(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let (mut enc, nonce) = Self::do_encrypt_init(key)?; + enc.do_update_aad(aad)?; + let written = enc.do_update_out(plaintext, ciphertext)?; + let (last, last_len) = enc.do_final()?; + // `encrypt_out_len` is exactly `written + last_len`, so this fits in `ciphertext[..needed]`. + ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); + Ok((nonce, written + last_len)) + } + + /// As [`encrypt_out_with_aad`](Self::encrypt_out_with_aad), but sources randomness from the + /// provided RNG: [`SymmetricCipherEncryptor::encrypt_out_rng`] with an `aad`. + fn encrypt_out_rng_with_aad( key: &KeyMaterial, + rng: &mut dyn RNG, aad: &[u8], plaintext: &[u8], ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError>; - /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a stream cipher ([`StreamCipherEncryptor`] / [`StreamCipherDecryptor`]), and so will already - /// have a streaming API. - /// This allows you to finish either style of streaming API flow with AEAD specific do_final() - /// that computes and returns the authentication tag. - fn do_aead_encrypt_final(self) -> Result<[u8; TAG_LEN], SymmetricCipherError>; + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + let needed = Self::encrypt_out_len(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let (mut enc, nonce) = Self::do_encrypt_init_rng(key, rng)?; + enc.do_update_aad(aad)?; + let written = enc.do_update_out(plaintext, ciphertext)?; + let (last, last_len) = enc.do_final()?; + // As in `encrypt_out_with_aad`. + ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); + Ok((nonce, written + last_len)) + } + #[cfg(feature = "std")] - /// A one-shot API to decrypt some ciphertext with the given key. - /// This function returns the ciphertext as a `Vec`, and therefore is only available when compiling with std. - fn aead_decrypt( + /// One-shot, allocating, with the tag detached: as + /// [`encrypt_out_detached`](Self::encrypt_out_detached), returning the ciphertext as a + /// `Vec`. Only available with the `std` feature. + fn encrypt_detached( key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], aad: &[u8], - ciphertext: &[u8], - tag: &[u8; TAG_LEN], - ) -> Result, SymmetricCipherError>; - /// A one-shot API to decrypt some ciphertext with the given key. - /// This function takes a reference to the output buffer for the plaintext, and is therefore available in no_std. - /// See the documentation for the underlying implementation for details on providing a plaintext buffer of sufficient size; - /// typically the ciphertext is the same length as the plaintext, but some ciphers may have an expansion factor or require - /// extra space for a nonce or tag. - /// Returns the number of bytes written to the plaintext buffer. - fn aead_decrypt_out( + plaintext: &[u8], + ) -> Result, SymmetricCipherError> { + let mut ciphertext = vec![0u8; Self::encrypt_out_len_detached(plaintext.len())]; + let (nonce, written, tag) = + Self::encrypt_out_detached(key, aad, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((nonce, ciphertext, tag)) + } + + #[cfg(feature = "std")] + /// One-shot, allocating, into the inline `ciphertext || tag` layout with associated data: as + /// [`encrypt_out_with_aad`](Self::encrypt_out_with_aad), returning the ciphertext, tag + /// included, as a `Vec`. This is [`SymmetricCipherEncryptor::encrypt`] with an `aad`. Only + /// available with the `std` feature. + fn encrypt_with_aad( key: &KeyMaterial, - nonce: &[u8; NONCE_LEN], aad: &[u8], - ciphertext: &[u8], - tag: &[u8; TAG_LEN], - plaintext: &mut [u8], - ) -> Result; - /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) or a stream cipher ([`StreamCipherEncryptor`] / [`StreamCipherDecryptor`]), and so will already - /// have a streaming API. - /// This allows you to finish either style of streaming API flow with AEAD specific do_final() - /// that computes and returns the authentication tag. - fn do_aead_decrypt_final(self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError>; + plaintext: &[u8], + ) -> Result<([u8; NONCE_LEN], Vec), SymmetricCipherError> { + let mut ciphertext = vec![0u8; Self::encrypt_out_len(plaintext.len())]; + let (nonce, written) = Self::encrypt_out_with_aad(key, aad, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((nonce, ciphertext)) + } } /// Metadata about a cryptographic algorithm. @@ -411,11 +803,45 @@ pub trait ElectronicCodeBook: /// * Collision resistance: finding two inputs that yield the same output is computationally difficult. /// * Preimage resistance: from a given output, finding an input that generates it is computationally difficult. /// * Second preimage resistance: given an input, finding another input that yields the same output is computationally difficult. -pub trait Hash: Algorithm + Default { +/// +/// # Construction is not part of this trait +/// +/// There is deliberately no `Default` supertrait. Feeding bytes in and finalising is one concern; +/// making an instance is another, and not every implementor has a canonical zero-argument one -- +/// a keyed construction such as KMAC (SP 800-185 Sec 4) has no meaningful default, and requiring +/// one would exclude it from this trait and from [`XOF`] with it. +/// +/// Generic code that needs to *build* a hasher asks for it: `fn digest(..)`. +/// That is what `HMAC` and the shared test framework already do, so the bound sits where the +/// requirement actually is rather than on every implementor. +/// +/// # Forking is part of this trait +/// +/// `Clone` *is* a supertrait: a hash mid-stream can be copied, and the copy continues independently +/// from the same absorbed prefix. That is how a running hash of a common prefix is finished several +/// ways -- a transcript hash checkpointed at each handshake message, HMAC's inner and outer states +/// held ready across many MACs under one key, or a Merkle node whose prefix is shared by its +/// siblings -- without re-absorbing the prefix each time. Every implementor is a fixed-size state +/// plus a small buffer, so the derive is the right implementation; the shared test framework checks +/// that a clone and its original finish to the same digest, and diverge once fed different input. +pub trait Hash: Algorithm + Clone { /// The size of the internal block in bits -- needed by functions such as HMAC to compute security parameters. fn block_bitlen(&self) -> usize; /// The size of the output in bytes. + /// + /// # This is not always part of the function's identity + /// + /// For most hashes the length is bound into the computation, so asking for a different length + /// gives a different function rather than more or fewer bytes of the same one. TupleHash and + /// KMAC are built that way deliberately -- SP 800-185 absorbs `right_encode(L)` before + /// squeezing. + /// + /// A [`XOF`] is the exception. Its length is chosen at the point of output and is *not* an + /// input to the computation, so this returns a nominal length only -- 32 bytes for SHAKE128 -- + /// and two outputs of different lengths share their leading bytes. Generic code over `Hash` + /// must therefore not infer "different `output_len` implies unrelated output"; see the + /// discussion on [`XOF`]. fn output_len(&self) -> usize; /// A static one-shot API that hashes the provided data. @@ -1334,7 +1760,10 @@ pub trait SymmetricCipherDecryptor< /// # Errors /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is shorter than /// [`update_out_len`](Self::update_out_len), carrying the required length. Nothing is - /// consumed in that case. + /// consumed in that case. An implementor with a fixed buffering capacity, such as an AEAD + /// that has to see the whole message before it can process any of it (see + /// [`AEADCipherEncryptor`]), may also return [`SymmetricCipherError::GenericError`] if the + /// input would exceed it. fn do_update_out( &mut self, ciphertext: &[u8], @@ -1370,7 +1799,9 @@ pub trait SymmetricCipherDecryptor< /// [`decrypt_out_max_len`](Self::decrypt_out_max_len) bytes. Returns the number of plaintext /// bytes written. /// - /// Provided as `do_decrypt_init`, one `do_update_out` and `do_final`. + /// Provided as `do_decrypt_init`, one `do_update_out` and `do_final`. If `do_final` fails -- + /// a bad tag, bad padding -- the plaintext already written is zeroized before the error is + /// returned, so a caller who ignores the `Result` is not left holding unauthenticated data. /// /// # Errors /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, checked @@ -1387,10 +1818,21 @@ pub trait SymmetricCipherDecryptor< } let mut dec = Self::do_decrypt_init(key, init_data)?; let written = dec.do_update_out(ciphertext, plaintext)?; - let (last, data_len) = dec.do_final()?; - // `decrypt_out_max_len` bounds `written + data_len`, so this fits in `plaintext[..needed]`. - plaintext[written..written + data_len].copy_from_slice(&last[..data_len]); - Ok(written + data_len) + match dec.do_final() { + Ok((last, data_len)) => { + // `decrypt_out_max_len` bounds `written + data_len`, so this fits in + // `plaintext[..needed]`. + plaintext[written..written + data_len].copy_from_slice(&last[..data_len]); + Ok(written + data_len) + } + Err(e) => { + // An AEAD reaches this one-shot through its `SymmetricCipherDecryptor` side, and + // what `do_update_out` released is unauthenticated; see + // `AEADCipherDecryptor::decrypt_out_detached` for why a plain `fill` is enough. + plaintext[..written].fill(0); + Err(e) + } + } } #[cfg(feature = "std")] @@ -1489,7 +1931,10 @@ pub trait SymmetricCipherEncryptor< /// # Errors /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is shorter than /// [`update_out_len`](Self::update_out_len), carrying the required length. Nothing is - /// consumed in that case. + /// consumed in that case. An implementor with a fixed buffering capacity, such as an AEAD + /// that has to see the whole message before it can process any of it (see + /// [`AEADCipherEncryptor`]), may also return [`SymmetricCipherError::GenericError`] if the + /// input would exceed it. fn do_update_out( &mut self, plaintext: &[u8], @@ -1584,91 +2029,143 @@ pub trait SymmetricCipherEncryptor< } } -/// Extensible Output Functions (XOFs) are similar to hash functions, except that they can produce output of arbitrary length. -/// The naming used for the functions of this trait are borrowed from the SHA3-style sponge constructions that split XOF operation -/// into two phases: an absorb phase in which an arbitrary amount of input is provided to the XOF, -/// and then a squeeze phase in which an arbitrary amount of output is extracted. -/// Once squeezing begins, no more input can be absorbed. -/// -/// XOFs are _similar to_ hash functions, but are not hash functions for one technical but important reason: -/// since the amount of output to produce is not provided to the XOF in advance, it cannot be used to -/// diversify the XOF output streams. -/// In other words, the overlapping parts of their outputs will be the same! -/// For example, consider two XOFs that absorb the same input data, one that is squeezed to produce 32 bytes, -/// and the other to produce 1 kb; both outputs will be identical in their first 32 bytes. -/// This could lead to loss of security in a number of ways, for example distinguishing attacks where -/// it is sufficient for the attacker to know that two values came from the same input, even if the -/// attacker cannot learn what that input was. This is attack is often sufficient, for example, -/// to break anonymity-preserving technology. -/// Applications that require the arbitrary-length output of an XOF, but also care about these -/// distinguishing attacks should consider adding a cryptographic salt to diversify the inputs. -/// -/// # State and Absorb-after-Squeeze -/// This trait makes the design choice that an XOF consists of an absorb phase followed by a squeeze phase. -/// This means that once the XOF has begun squeezing, attempting to absorb more will return -/// [`HashError::InvalidState`] and leave the object usable for further squeezing. -/// -/// Without this restriction, the [`XOF::absorb_last_partial_byte`] API cannot function correctly. -/// -/// If Absorb-after-Squeeze becomes necessary to support in the future, then these design choices can be revisited. -pub trait XOF: Default { - /// A static one-shot API that digests the input data and produces `result_len` bytes of output. - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec; - - /// A static one-shot API that digests the input data and produces `result_len` bytes of output. - /// Fills the provided output slice. - /// The entire output buffer is zeroized before the output is written. - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize; - - /// Absorb some amount of input. - fn absorb(&mut self, data: &[u8]) -> Result<(), HashError>; - - /// The same as [`XOF::absorb`], but allows for supplying a partial byte as the last input. - /// The partial byte is taken as it arrives in the final octet of an ASN.1 BIT STRING - /// (X.690 s. 8.6.2.1): the `num_bits` message bits are the most significant bits of - /// `partial_byte`, leading bit first, and the low `8 - num_bits` bits (the BIT STRING's "unused - /// bits") are ignored. This is the same convention as [`Hash::do_final_partial_bits`]; see there - /// for the relationship to the FIPS 202 Appendix B.1 bit order and to the NIST test vector files. - /// 0 is a valid value and means the message ends on a byte boundary (equivalent to [`XOF::absorb`]). - /// `num_bits` must be in `0..=7`; larger values return [`HashError::InvalidLength`]. +/// The squeezing phase of an [`XOF`]: a value that produces output and can no longer take input. +/// +/// This is the type [`XOF::into_squeezer`] hands back. Absorbing and squeezing are separate types +/// rather than separate states of one type, so "no more input once output has begun" is a fact the +/// compiler enforces rather than a rule the documentation asks callers to follow, and so there is +/// no "absorbed after squeezing" error to raise or to test for. +/// +/// Output is one continuous stream: successive calls continue where the last left off, so reading +/// 16 bytes twice gives the same 32 bytes as reading 32 once. +/// +/// [`do_final`](Self::do_final) means something weaker here than on [`Hash`] and [`MAC`]. On those +/// it is load-bearing -- the only way to get output, and it must consume the value because +/// finalizing pads the state. A squeeze has nothing to finalize, so it produces exactly the bytes +/// [`do_output`](Self::do_output) would and differs only in taking ownership: it is how a caller +/// says "this read is my last", and it ends the stream at the point of the call rather than +/// leaving a `mut` binding alive for the rest of the scope. +/// +/// # Being the last read can be an input to the function +/// +/// For SHAKE and cSHAKE the bytes do not depend on how much of the stream is taken, so `do_final` +/// really is just `do_output` plus ownership, which is what the default does. That is not +/// universal. The SP 800-185 functions end their absorbed input with `right_encode(L)`, and their +/// XOF forms (s. 4.3.1, 5.3.1 and 6.3.1) differ from the fixed-length ones only in putting 0 there +/// -- so an implementation can leave `L` unchosen until it knows how the caller intends to read. +/// A `do_final` that is also the *first* read says both how many bytes are wanted and that there +/// will be no more, which is exactly `L`; such an implementation binds it and produces the +/// fixed-length function (KMAC, TupleHash, ParallelHash) rather than a prefix of the XOF stream. +/// +/// After a [`do_output`](Self::do_output) there is nothing left to choose -- `right_encode(0)` is +/// in the sponge and a length bound into a sponge cannot be revised -- so `do_final` then just +/// ends the stream that read began. Implementors that have no such choice to make should keep the +/// default. +pub trait XOFSqueezer { + /// Produces the next `num_bytes` bytes of the output stream. + fn do_output(&mut self, num_bytes: usize) -> Vec; + + /// As [`do_output`](Self::do_output), filling the caller's buffer, which is zeroized first. + /// Returns the number of bytes written. + fn do_output_out(&mut self, output: &mut [u8]) -> usize; + + /// Produces the last `num_bytes` bytes of the output stream and ends the object. + /// + /// Consumes self, so this must be the final call to this object. The default is a plain last + /// read -- the bytes [`do_output`](Self::do_output) would give, continuing from wherever + /// earlier reads left the stream. An implementation with an output length still to bind + /// overrides it to bind `num_bytes` when nothing has been read yet; see the trait docs. + fn do_final(mut self, num_bytes: usize) -> Vec + where + Self: Sized, + { + self.do_output(num_bytes) + } + + /// As [`do_final`](Self::do_final), filling the caller's buffer, which is zeroized first. + /// Returns the number of bytes written. /// - /// Unlike [`XOF::absorb`], this switches the XOF from Absorbing mode into Squeezing mode because - /// absorbing more input after absorbing a partial byte is undefined behaviour. - fn absorb_last_partial_byte( - &mut self, - partial_byte: u8, - num_bits: usize, - ) -> Result<(), HashError>; - - /// Can be called multiple times. - fn squeeze(&mut self, num_bytes: usize) -> Vec; - - /// Can be called multiple times. - /// Fills the provided output slice. - /// The entire output buffer is zeroized before the output is written. - fn squeeze_out(&mut self, output: &mut [u8]) -> usize; - - /// Squeezes a partial byte (`num_bits` in `0..=7`) from the XOF. - /// The bits are returned as they would be placed in the final octet of an ASN.1 BIT STRING - /// (X.690 s. 8.6.2.1): in the most significant `num_bits` bits of the returned u8, first output - /// bit first, with the low `8 - num_bits` "unused" bits zero. This matches the input convention of - /// [`XOF::absorb_last_partial_byte`]. (FIPS 202 Appendix B.1 orders the bits of an output byte - /// LSB-first; the implementation converts.) - /// 0 is a valid value and requests no bits, so the result is `0x00`. - /// `num_bits` must be in `0..=7`; larger values return [`HashError::InvalidLength`]. - /// This is a final call and consumes self. - fn squeeze_partial_byte_final(self, num_bits: usize) -> Result; + /// Defaulted as [`do_final`](Self::do_final) is. + fn do_final_out(mut self, output: &mut [u8]) -> usize + where + Self: Sized, + { + self.do_output_out(output) + } +} - /// The same as [`XOF::squeeze_partial_byte_final`], but writes into the provided output byte. - /// The output byte is zeroized before the result is written. - fn squeeze_partial_byte_final_out( +/// Extendable-Output Functions (XOFs): hashes whose output length is chosen by the caller. +/// +/// `XOF: Hash`, so SHAKE128 and SHAKE256 *are* hashes and can be used wherever one is wanted. As a +/// hash, a XOF has a nominal output length -- [`Hash::output_len`], which for SHAKE is twice the +/// security strength, 32 bytes for SHAKE128 and 64 for SHAKE256 -- and [`Hash::do_final`] produces +/// exactly that many bytes. This trait adds the ability to ask for a different number. +/// +/// # Absorb, then squeeze +/// +/// A sponge takes input, then produces output, and cannot go back. Here that is expressed in the +/// types: [`into_squeezer`](Self::into_squeezer) consumes the XOF and returns an [`XOFSqueezer`], so +/// after output has begun there is no value left on which to call [`Hash::do_update`]. Nothing +/// returns an "absorbed after squeezing" error because nothing can reach that state. +/// +/// # A XOF is not a hash, cryptographically +/// +/// It satisfies the trait, but the output length is not an input to the computation, so it cannot +/// diversify the output. Two XOFs given the same input, one read for 32 bytes and one for 1 KiB, +/// agree on their first 32 bytes. An attacker who only needs to know that two values came from the +/// same input -- enough to break an anonymity property -- learns it from the overlap. Where that +/// matters, salt the input. +pub trait XOF: Hash { + /// The squeezing state this XOF turns into. + type Squeezer: XOFSqueezer; + + /// Ends the input phase and begins producing output. + /// + /// The phase change is in the type: what comes back takes no more input. + fn into_squeezer(self) -> Self::Squeezer; + + /// As [`into_squeezer`](Self::into_squeezer), with a final partial **byte** of input. + /// + /// The partial byte arrives as the final octet of an ASN.1 BIT STRING (X.690 s. 8.6.2.1): the + /// `num_bits` message bits are the most significant bits of `partial_byte`, leading bit first, + /// and the low `8 - num_bits` "unused" bits are ignored. Same convention as + /// [`Hash::do_final_partial_bits`]. `num_bits` of 0 means the message ended on a byte boundary + /// and is equivalent to [`into_squeezer`](Self::into_squeezer). + /// + /// # Errors + /// [`HashError::InvalidLength`] if `num_bits` is not in `0..=7`. + fn into_squeezer_partial_bits( self, + partial_byte: u8, num_bits: usize, - output: &mut u8, - ) -> Result<(), HashError>; + ) -> Result; + + /// One-shot: absorbs `data` and produces `result_len` bytes. + /// + /// A one-shot names its length and never comes back, so this is + /// [`XOFSqueezer::do_final`]'s reading of the stream, not + /// [`do_output`](XOFSqueezer::do_output)'s: where an implementation binds the length it is + /// asked for, this binds `result_len`. For SHAKE and cSHAKE the two are the same bytes. + /// + /// The default absorbs and reads in the obvious way; override it only where the type can do + /// better, as SHAKE does. + fn xof(mut self, data: &[u8], result_len: usize) -> Vec + where + Self: Sized, + { + self.do_update(data); + self.into_squeezer().do_final(result_len) + } - /// Returns the maximum security strength that this KDF is capable of supporting, based on the underlying primitives. - // todo: we should do a refactor to make [Algorithm] be a `security_strength()` function instead of constant, - // then have `RNG: Algorithm`, then delete this function. - fn max_security_strength(&self) -> SecurityStrength; + /// One-shot: absorbs `data` and fills `output`, which is zeroized first. Returns the number of + /// bytes written. + /// + /// A final read of `output.len()` bytes, and defaulted as [`xof`](Self::xof) is. + fn xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize + where + Self: Sized, + { + self.do_update(data); + self.into_squeezer().do_final_out(output) + } } diff --git a/crypto/core/tests/aead_tagged_tests.rs b/crypto/core/tests/aead_tagged_tests.rs new file mode 100644 index 00000000..c32b02da --- /dev/null +++ b/crypto/core/tests/aead_tagged_tests.rs @@ -0,0 +1,366 @@ +//! Integration tests for the inline `ciphertext || tag` layout on +//! [`AEADCipherEncryptor`]/[`AEADCipherDecryptor`] -- the `encrypt_out_with_aad` / `decrypt_out_with_aad` +//! one-shots, and the inherited `SymmetricCipherEncryptor` / `SymmetricCipherDecryptor` streaming +//! and one-shot methods they sit beside -- driven over a toy AEAD, which is what lets the length +//! and tag-placement edges be checked exactly. + +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_utils::secret::Secret; + +const KEY_LEN: usize = 4; +const NONCE_LEN: usize = 4; +const TAG_LEN: usize = 3; + +/// A toy AEAD: "ciphertext" is the plaintext XORed byte-by-byte with the key (cycled), and the +/// "tag" is a running XOR of every AAD/plaintext byte seen, repeated to `TAG_LEN` bytes. Not +/// remotely secure -- it exists only to drive the inline-layout defaults at exact byte-boundary edge +/// cases around `TAG_LEN`, with a `TAG_LEN` small enough (3) that "the tag is the last few bytes" +/// and "the message is shorter than the tag" are both cheap to enumerate. +#[derive(Clone)] +struct Toy { + key: Secret<[u8; KEY_LEN]>, + pos: usize, + acc: u8, +} + +impl Toy { + fn new(key: &KeyMaterial) -> Result { + let mut k = Secret::<[u8; KEY_LEN]>::new(); + k.copy_from_slice(key.ref_to_bytes()); + Ok(Self { key: k, pos: 0, acc: 0 }) + } + + /// Transforms `data` in place, accumulating `acc` over the *plaintext* byte on both + /// sides: encrypting, `data` starts as plaintext, so `acc` is updated before the XOR; + /// decrypting, `data` starts as ciphertext, so the XOR (which recovers the plaintext byte + /// into the same slot) must happen first. + fn transform(&mut self, data: &mut [u8], encrypting: bool) { + for b in data.iter_mut() { + if encrypting { + self.acc ^= *b; + } + *b ^= self.key[self.pos % KEY_LEN]; + if !encrypting { + self.acc ^= *b; + } + self.pos += 1; + } + } +} + +struct ToyEnc(Toy); + +impl Algorithm for ToyEnc { + const ALG_NAME: &'static str = "toy-aead"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; +} +impl Algorithm for ToyDec { + const ALG_NAME: &'static str = "toy-aead"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; +} + +impl SymmetricCipherEncryptor for ToyEnc { + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + Ok((Self(Toy::new(key)?), [0u8; NONCE_LEN])) + } + fn do_encrypt_init_rng( + key: &KeyMaterial, + _rng: &mut dyn RNG, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + Self::do_encrypt_init(key) + } + fn update_out_len(&self, input_len: usize) -> usize { + input_len + } + fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); + } + let out = &mut ciphertext[..plaintext.len()]; + out.copy_from_slice(plaintext); + self.0.transform(out, true); + Ok(plaintext.len()) + } + fn do_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + Ok(([self.0.acc; TAG_LEN], TAG_LEN)) + } + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + TAG_LEN + } +} + +impl AEADCipherEncryptor for ToyEnc { + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + for &b in aad { + self.0.acc ^= b; + } + Ok(()) + } + fn do_final_out_detached( + self, + _ciphertext: &mut [u8; TAG_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + Ok((0, [self.0.acc; TAG_LEN])) + } +} + +/// Holds back the last `TAG_LEN` bytes of ciphertext seen, as every AEAD decryptor must. +struct ToyDec { + toy: Toy, + held: [u8; TAG_LEN], + held_len: usize, +} + +impl SymmetricCipherDecryptor for ToyDec { + fn do_decrypt_init( + key: &KeyMaterial, + _nonce: &[u8; NONCE_LEN], + ) -> Result { + Ok(Self { toy: Toy::new(key)?, held: [0u8; TAG_LEN], held_len: 0 }) + } + fn update_out_len(&self, input_len: usize) -> usize { + (self.held_len + input_len).saturating_sub(TAG_LEN) + } + fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let release = self.update_out_len(ciphertext.len()); + if plaintext.len() < release { + return Err(SymmetricCipherError::OutputBufferTooSmall(release)); + } + // The same byte-queue shuffle as `AsconAead128Decryptor::do_update_out`, over a stream of + // `held || ciphertext`. + let mut stream = self.held[..self.held_len].to_vec(); + stream.extend_from_slice(ciphertext); + let out = &mut plaintext[..release]; + out.copy_from_slice(&stream[..release]); + self.toy.transform(out, false); + self.held_len = stream.len() - release; + self.held[..self.held_len].copy_from_slice(&stream[release..]); + Ok(release) + } + fn do_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + if self.held_len < TAG_LEN { + return Err(SymmetricCipherError::DecryptionFailed); + } + if [self.toy.acc; TAG_LEN] != self.held { + return Err(SymmetricCipherError::AEADTagCheckFailed); + } + Ok(([0u8; TAG_LEN], 0)) + } + fn decrypt_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(TAG_LEN) + } +} + +impl AEADCipherDecryptor for ToyDec { + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + for &b in aad { + self.toy.acc ^= b; + } + Ok(()) + } + fn do_final_out_detached( + mut self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; TAG_LEN], + ) -> Result { + let n = self.held_len; + plaintext[..n].copy_from_slice(&self.held[..n]); + self.toy.transform(&mut plaintext[..n], false); + if [self.toy.acc; TAG_LEN] != *tag { + return Err(SymmetricCipherError::AEADTagCheckFailed); + } + Ok(n) + } +} + +fn key() -> KeyMaterial { + let mut km = + KeyMaterial::::from_bytes_as_type(&[1, 2, 3, 4], KeyType::SymmetricCipherKey) + .unwrap(); + do_hazardous_operations(&mut km, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::None) + }) + .unwrap(); + km +} + +const AAD: &[u8] = b"aad"; + +/// Encrypts `msg` into the inline layout with the one-shot, and returns it. +fn tagged_ct(km: &KeyMaterial, msg: &[u8]) -> (Vec, [u8; NONCE_LEN]) { + let mut ct = vec![0u8; ToyEnc::encrypt_out_len(msg.len())]; + let (nonce, written) = ToyEnc::encrypt_out_with_aad(km, AAD, msg, &mut ct).unwrap(); + assert_eq!(written, msg.len() + TAG_LEN, "inline layout is ciphertext || tag"); + ct.truncate(written); + (ct, nonce) +} + +/// The one-shot pair round-trips at every length crossing a few multiples of `TAG_LEN`, and the +/// streaming pair agrees with it for every chunking -- the decryptor, not the caller, holding back +/// the last `TAG_LEN` bytes as the possible tag. +#[test] +fn tagged_round_trip_at_every_length_and_chunking() { + let km = key(); + for len in 0..=(4 * TAG_LEN + 5) { + let msg: Vec = (0..len).map(|i| (i as u8).wrapping_mul(31).wrapping_add(7)).collect(); + let (ct, nonce) = tagged_ct(&km, &msg); + + let mut pt = vec![0u8; ToyDec::decrypt_out_max_len(ct.len())]; + let n = ToyDec::decrypt_out_with_aad(&km, &nonce, AAD, &ct, &mut pt).unwrap(); + assert_eq!(&pt[..n], &msg[..], "len {len}: one-shot round trip"); + + // The detached layout is the same ciphertext with the tag split off. + let mut detached = vec![0u8; ToyEnc::encrypt_out_len_detached(len)]; + let (_, d_len, d_tag) = + ToyEnc::encrypt_out_detached(&km, AAD, &msg, &mut detached).unwrap(); + assert_eq!(&detached[..d_len], &ct[..len], "len {len}: detached ciphertext"); + assert_eq!(&d_tag[..], &ct[len..], "len {len}: detached tag"); + + for chunk in [1usize, 2, 3, TAG_LEN.max(1), len.max(1)] { + // Encrypt in chunks, finishing with the tag appended by the streaming finalizer. + let (mut enc, stream_nonce) = ToyEnc::do_encrypt_init(&km).unwrap(); + enc.do_update_aad(AAD).unwrap(); + let mut stream_ct = vec![0u8; msg.len() + TAG_LEN]; + let mut written = 0; + for piece in msg.chunks(chunk) { + written += enc.do_update_out(piece, &mut stream_ct[written..]).unwrap(); + } + let mut last = [0u8; TAG_LEN]; + let last_len = enc.do_final_out(&mut last).unwrap(); + stream_ct[written..written + last_len].copy_from_slice(&last[..last_len]); + written += last_len; + stream_ct.truncate(written); + assert_eq!( + stream_ct, ct, + "len {len}, chunk {chunk}: streaming must match the one-shot" + ); + + // Decrypt in chunks, tag and all: the decryptor holds the tag back itself. + let mut dec = ToyDec::do_decrypt_init(&km, &stream_nonce).unwrap(); + dec.do_update_aad(AAD).unwrap(); + let mut out = vec![0u8; stream_ct.len()]; + let mut written = 0; + for piece in stream_ct.chunks(chunk) { + written += dec.do_update_out(piece, &mut out[written..]).unwrap(); + } + assert_eq!(written, len, "len {len}, chunk {chunk}: the tag must be held back"); + let (last, data_len) = dec.do_final().unwrap(); + assert_eq!(data_len, 0, "len {len}: nothing but the tag was held back"); + out[written..written + data_len].copy_from_slice(&last[..data_len]); + out.truncate(written + data_len); + assert_eq!(out, msg, "len {len}, chunk {chunk}: streaming round trip"); + + // The same held-back bytes are ciphertext if the tag is detached. + let mut dec = ToyDec::do_decrypt_init(&km, &stream_nonce).unwrap(); + dec.do_update_aad(AAD).unwrap(); + let mut out = vec![0u8; len]; + let mut written = 0; + for piece in stream_ct[..len].chunks(chunk) { + written += dec.do_update_out(piece, &mut out[written..]).unwrap(); + } + let mut last = [0u8; TAG_LEN]; + let last_len = dec.do_final_out_detached(&d_tag, &mut last).unwrap(); + assert_eq!(written + last_len, len, "len {len}: detached final flushes the rest"); + out[written..].copy_from_slice(&last[..last_len]); + assert_eq!(out, msg, "len {len}, chunk {chunk}: detached streaming round trip"); + } + } +} + +/// A tampered inline stream fails at finalization on both entry points, zeroizing the one-shot's +/// buffer, and an input shorter than the tag is rejected as `DecryptionFailed` rather than +/// panicking on the short slice. +#[test] +fn tampering_and_short_input_are_rejected() { + let km = key(); + let msg = [7u8; 10]; + let (ct, nonce) = tagged_ct(&km, &msg); + + let mut tampered = ct.clone(); + tampered[0] ^= 0xFF; + let mut pt = vec![0u8; tampered.len()]; + assert!(matches!( + ToyDec::decrypt_out_with_aad(&km, &nonce, AAD, &tampered, &mut pt), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(pt, vec![0u8; tampered.len()], "the one-shot zeroizes on a failed tag check"); + + let mut dec = ToyDec::do_decrypt_init(&km, &nonce).unwrap(); + dec.do_update_aad(AAD).unwrap(); + dec.do_update_out(&tampered, &mut pt).unwrap(); + assert!(matches!(dec.do_final(), Err(SymmetricCipherError::AEADTagCheckFailed))); + + // A wrong detached tag fails, and `decrypt_out_detached` zeroizes what it wrote. + let mut wrong_tag = [0u8; TAG_LEN]; + wrong_tag.copy_from_slice(&ct[msg.len()..]); + wrong_tag[0] ^= 0xFF; + let mut pt = vec![0u8; msg.len()]; + assert!(matches!( + ToyDec::decrypt_out_detached(&km, &nonce, AAD, &ct[..msg.len()], &wrong_tag, &mut pt), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(pt, vec![0u8; msg.len()], "the detached one-shot zeroizes on a failed tag check"); + + for short_len in 0..TAG_LEN { + let mut pt = vec![0u8; TAG_LEN]; + assert!(matches!( + ToyDec::decrypt_out_with_aad(&km, &nonce, AAD, &ct[..short_len], &mut pt), + Err(SymmetricCipherError::DecryptionFailed) + )); + let mut dec = ToyDec::do_decrypt_init(&km, &nonce).unwrap(); + assert_eq!(dec.do_update_out(&ct[..short_len], &mut pt).unwrap(), 0); + assert!(matches!(dec.do_final(), Err(SymmetricCipherError::DecryptionFailed))); + } +} + +/// Every inline one-shot refuses an output buffer that is one byte short, naming the length it +/// needs, and does so before touching the cipher; one of exactly that length is accepted. +#[test] +fn tagged_undersized_buffers_are_rejected() { + let km = key(); + let msg = [3u8; 8]; + let (ct, nonce) = tagged_ct(&km, &msg); + + let needed = ToyEnc::encrypt_out_len(msg.len()); + assert_eq!(needed, msg.len() + TAG_LEN); + let mut short = vec![0u8; needed - 1]; + match ToyEnc::encrypt_out_with_aad(&km, AAD, &msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, needed), + other => panic!("encrypt_out_with_aad into a short buffer: {other:?}"), + } + + let needed = ToyDec::decrypt_out_max_len(ct.len()); + assert_eq!(needed, msg.len()); + let mut short = vec![0u8; needed - 1]; + match ToyDec::decrypt_out_with_aad(&km, &nonce, AAD, &ct, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, needed), + other => panic!("decrypt_out_with_aad into a short buffer: {other:?}"), + } + + // A buffer of exactly the length it asks for must be accepted. Without this the + // `plaintext.len() < needed` guard can be weakened to `<=` or `==` without any test noticing: + // a too-short buffer is caught either way, by the guard or by `do_update_out` behind it, and + // both report the same error with the same length. + let mut exact = vec![0u8; needed]; + let n = ToyDec::decrypt_out_with_aad(&km, &nonce, AAD, &ct, &mut exact).unwrap(); + assert_eq!(&exact[..n], &msg[..], "a buffer of exactly `needed` bytes must be enough"); +} diff --git a/crypto/factory/Cargo.toml b/crypto/factory/Cargo.toml index 22836c5f..c9765796 100644 --- a/crypto/factory/Cargo.toml +++ b/crypto/factory/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true [dependencies] +bouncycastle-ascon.workspace = true bouncycastle-core.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true diff --git a/crypto/factory/src/hash_factory.rs b/crypto/factory/src/hash_factory.rs index f169411b..0598d79a 100644 --- a/crypto/factory/src/hash_factory.rs +++ b/crypto/factory/src/hash_factory.rs @@ -28,6 +28,8 @@ use crate::{AlgorithmFactory, FactoryError}; use crate::{DEFAULT, DEFAULT_128_BIT, DEFAULT_256_BIT}; +use bouncycastle_ascon as ascon; +use bouncycastle_ascon::ASCON_HASH256_NAME; use bouncycastle_core::errors::HashError; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Algorithm, Hash}; @@ -43,6 +45,7 @@ use bouncycastle_sm3::SM3_NAME; /// Wrapper object for all algorithms that impl [`Hash`]. /// Note: no SHAKE because SHAKE is not NIST approved as a hash function. See FIPS 202 section A.2. #[non_exhaustive] +#[derive(Clone)] pub enum HashFactory { /// SHA224(sha2::SHA224), @@ -66,6 +69,8 @@ pub enum HashFactory { SHA3_512(sha3::SHA3_512), /// SM3(sm3::SM3), + /// + AsconHash256(ascon::ascon_hash256::AsconHash256), } impl Default for HashFactory { @@ -98,6 +103,7 @@ impl AlgorithmFactory for HashFactory { SHA3_384_NAME => Ok(Self::SHA3_384(sha3::SHA3_384::new())), SHA3_512_NAME => Ok(Self::SHA3_512(sha3::SHA3_512::new())), SM3_NAME => Ok(Self::SM3(sm3::SM3::new())), + ASCON_HASH256_NAME => Ok(Self::AsconHash256(ascon::ascon_hash256::AsconHash256::new())), _ => Err(FactoryError::UnsupportedAlgorithm(format!( "The algorithm: \"{}\" is not a known Hash", alg_name @@ -129,6 +135,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.block_bitlen(), Self::SHA3_512(h) => h.block_bitlen(), Self::SM3(h) => h.block_bitlen(), + Self::AsconHash256(h) => h.block_bitlen(), } } @@ -145,6 +152,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.output_len(), Self::SHA3_512(h) => h.output_len(), Self::SM3(h) => h.output_len(), + Self::AsconHash256(h) => h.output_len(), } } @@ -161,6 +169,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.hash(data), Self::SHA3_512(h) => h.hash(data), Self::SM3(h) => h.hash(data), + Self::AsconHash256(h) => h.hash(data), } } @@ -179,6 +188,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.hash_out(data, output), Self::SHA3_512(h) => h.hash_out(data, output), Self::SM3(h) => h.hash_out(data, output), + Self::AsconHash256(h) => h.hash_out(data, output), } } @@ -195,6 +205,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.do_update(data), Self::SHA3_512(h) => h.do_update(data), Self::SM3(h) => h.do_update(data), + Self::AsconHash256(h) => h.do_update(data), } } @@ -211,6 +222,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.do_final(), Self::SHA3_512(h) => h.do_final(), Self::SM3(h) => h.do_final(), + Self::AsconHash256(h) => h.do_final(), } } @@ -229,6 +241,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.do_final_out(output), Self::SHA3_512(h) => h.do_final_out(output), Self::SM3(h) => h.do_final_out(output), + Self::AsconHash256(h) => h.do_final_out(output), } } @@ -249,6 +262,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), Self::SHA3_512(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), Self::SM3(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), + Self::AsconHash256(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), } } @@ -282,6 +296,9 @@ impl Hash for HashFactory { h.do_final_partial_bits_out(partial_byte, num_partial_bits, output) } Self::SM3(h) => h.do_final_partial_bits_out(partial_byte, num_partial_bits, output), + Self::AsconHash256(h) => { + h.do_final_partial_bits_out(partial_byte, num_partial_bits, output) + } } } @@ -298,6 +315,7 @@ impl Hash for HashFactory { Self::SHA3_384(h) => h.max_security_strength(), Self::SHA3_512(h) => h.max_security_strength(), Self::SM3(h) => h.max_security_strength(), + Self::AsconHash256(h) => h.max_security_strength(), } } } diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index cb669ddd..f54e01cf 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -84,6 +84,7 @@ use bouncycastle_sha3 as sha3; use bouncycastle_sha3::hmac::{ HMAC_SHA3_224_NAME, HMAC_SHA3_256_NAME, HMAC_SHA3_384_NAME, HMAC_SHA3_512_NAME, }; +use bouncycastle_sha3::{KMAC128, KMAC128_NAME, KMAC256, KMAC256_NAME}; use bouncycastle_sm3 as sm3; use bouncycastle_sm3::hmac::HMAC_SM3_NAME; @@ -102,6 +103,13 @@ pub const DEFAULT_256BIT_MAC_NAME: &str = HMAC_SHA256_NAME; /// instead they have a constructor that takes a [`KeyMaterialTrait`] and can return an error. #[non_exhaustive] pub enum MACFactory { + /// KMAC128 with no customization string and a 32-byte tag (NIST SP 800-185 Sec 4). + /// For a customization string or a different output length, construct + /// `bouncycastle_sha3::KMAC128` directly -- the factory selects by name alone and has no + /// channel for those parameters. + KMAC128(KMAC128), + /// KMAC256 with no customization string and a 64-byte tag. See [`MACFactory::KMAC128`]. + KMAC256(KMAC256), /// HMAC_SHA224(sha2::hmac::HMAC_SHA224), /// @@ -145,6 +153,8 @@ impl MACFactory { DEFAULT => Self::default(key), DEFAULT_128_BIT => Self::default_128_bit(key), DEFAULT_256_BIT => Self::default_256_bit(key), + KMAC128_NAME => Ok(Self::KMAC128(KMAC128::new(key)?)), + KMAC256_NAME => Ok(Self::KMAC256(KMAC256::new(key)?)), HMAC_SHA224_NAME => Ok(Self::HMAC_SHA224(sha2::hmac::HMAC_SHA224::new(key)?)), HMAC_SHA256_NAME => Ok(Self::HMAC_SHA256(sha2::hmac::HMAC_SHA256::new(key)?)), HMAC_SHA384_NAME => Ok(Self::HMAC_SHA384(sha2::hmac::HMAC_SHA384::new(key)?)), @@ -181,6 +191,8 @@ impl MAC for MACFactory { fn output_len(&self) -> usize { match self { + Self::KMAC128(h) => h.output_len(), + Self::KMAC256(h) => h.output_len(), Self::HMAC_SHA224(h) => h.output_len(), Self::HMAC_SHA256(h) => h.output_len(), Self::HMAC_SHA384(h) => h.output_len(), @@ -197,6 +209,8 @@ impl MAC for MACFactory { fn mac(self, data: &[u8]) -> Vec { match self { + Self::KMAC128(h) => h.mac(data), + Self::KMAC256(h) => h.mac(data), Self::HMAC_SHA224(h) => h.mac(data), Self::HMAC_SHA256(h) => h.mac(data), Self::HMAC_SHA384(h) => h.mac(data), @@ -215,6 +229,8 @@ impl MAC for MACFactory { out.fill(0); match self { + Self::KMAC128(h) => h.mac_out(data, out), + Self::KMAC256(h) => h.mac_out(data, out), Self::HMAC_SHA224(h) => h.mac_out(data, out), Self::HMAC_SHA256(h) => h.mac_out(data, out), Self::HMAC_SHA384(h) => h.mac_out(data, out), @@ -231,6 +247,8 @@ impl MAC for MACFactory { fn verify(self, data: &[u8], mac: &[u8]) -> bool { match self { + Self::KMAC128(h) => h.verify(data, mac), + Self::KMAC256(h) => h.verify(data, mac), Self::HMAC_SHA224(h) => h.verify(data, mac), Self::HMAC_SHA256(h) => h.verify(data, mac), Self::HMAC_SHA384(h) => h.verify(data, mac), @@ -247,6 +265,8 @@ impl MAC for MACFactory { fn do_update(&mut self, data: &[u8]) { match self { + Self::KMAC128(h) => h.do_update(data), + Self::KMAC256(h) => h.do_update(data), Self::HMAC_SHA224(h) => h.do_update(data), Self::HMAC_SHA256(h) => h.do_update(data), Self::HMAC_SHA384(h) => h.do_update(data), @@ -263,6 +283,8 @@ impl MAC for MACFactory { fn do_final(self) -> Vec { match self { + Self::KMAC128(h) => h.do_final(), + Self::KMAC256(h) => h.do_final(), Self::HMAC_SHA224(h) => h.do_final(), Self::HMAC_SHA256(h) => h.do_final(), Self::HMAC_SHA384(h) => h.do_final(), @@ -281,6 +303,8 @@ impl MAC for MACFactory { out.fill(0); match self { + Self::KMAC128(h) => h.do_final_out(&mut out), + Self::KMAC256(h) => h.do_final_out(&mut out), Self::HMAC_SHA224(h) => h.do_final_out(&mut out), Self::HMAC_SHA256(h) => h.do_final_out(&mut out), Self::HMAC_SHA384(h) => h.do_final_out(&mut out), @@ -297,6 +321,8 @@ impl MAC for MACFactory { fn do_verify_final(self, mac: &[u8]) -> bool { match self { + Self::KMAC128(h) => h.do_verify_final(mac), + Self::KMAC256(h) => h.do_verify_final(mac), Self::HMAC_SHA224(h) => h.do_verify_final(mac), Self::HMAC_SHA256(h) => h.do_verify_final(mac), Self::HMAC_SHA384(h) => h.do_verify_final(mac), @@ -313,6 +339,8 @@ impl MAC for MACFactory { fn max_security_strength(&self) -> SecurityStrength { match self { + Self::KMAC128(h) => h.max_security_strength(), + Self::KMAC256(h) => h.max_security_strength(), Self::HMAC_SHA224(h) => h.max_security_strength(), Self::HMAC_SHA256(h) => h.max_security_strength(), Self::HMAC_SHA384(h) => h.max_security_strength(), diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index 85468c6a..eb5d64df 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -5,7 +5,7 @@ //! //! Example usage: //! ``` -//! use bouncycastle_core::traits::XOF; +//! use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; //! use bouncycastle_factory::AlgorithmFactory; //! use bouncycastle_factory::xof_factory::XOFFactory; //! use bouncycastle_sha3 as sha3; @@ -13,9 +13,11 @@ //! let data: &[u8] = b"Hello, world!"; //! //! let mut h = XOFFactory::new(sha3::SHAKE128_NAME).unwrap(); -//! h.absorb(data); -//! let output: Vec = h.squeeze(16); +//! h.do_update(data); +//! let output: Vec = h.into_squeezer().do_output(16); //! ``` +//! `XOFFactory` implements [`Hash`] too, so it can be used wherever a hash is wanted; `do_final` +//! then produces the nominal 32 or 64 bytes. //! Equivalently, it may be invoked by passing a string instead of using the constant: //! //! ``` @@ -34,13 +36,16 @@ //! ``` use crate::{AlgorithmFactory, FactoryError}; +use bouncycastle_ascon::ASCON_XOF128_NAME; +use bouncycastle_ascon::ascon_xof128::AsconXof128; use bouncycastle_core::errors::HashError; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{KDF, XOF}; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; use bouncycastle_sha3 as sha3; use bouncycastle_sha3::{SHAKE128_NAME, SHAKE256_NAME}; /*** Defaults ***/ + /// pub const DEFAULT_XOF_NAME: &str = SHAKE128_NAME; /// @@ -50,11 +55,14 @@ pub const DEFAULT_256BIT_XOF_NAME: &str = SHAKE256_NAME; /// Wrapper object for all algorithms that impl [`XOF`]. #[non_exhaustive] +#[derive(Clone)] pub enum XOFFactory { /// SHAKE128(sha3::SHAKE128), /// SHAKE256(sha3::SHAKE256), + /// + AsconXof128(AsconXof128), } impl Default for XOFFactory { @@ -76,6 +84,7 @@ impl AlgorithmFactory for XOFFactory { match alg_name { SHAKE128_NAME => Ok(Self::SHAKE128(sha3::SHAKE128::new())), SHAKE256_NAME => Ok(Self::SHAKE256(sha3::SHAKE256::new())), + ASCON_XOF128_NAME => Ok(Self::AsconXof128(AsconXof128::new())), _ => Err(FactoryError::UnsupportedAlgorithm(format!( "The algorithm: \"{}\" is not a known XOF", alg_name @@ -83,81 +92,184 @@ impl AlgorithmFactory for XOFFactory { } } } -impl XOF for XOFFactory { - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { + +/// `Hash` requires it, and the factory does not know which algorithm it holds until it is +/// constructed, so the constants are placeholders -- the same stance `HashFactory` takes. The +/// per-value answers come from [`Hash::output_len`] and [`Hash::max_security_strength`], which +/// dispatch on the variant. +impl Algorithm for XOFFactory { + const ALG_NAME: &'static str = "TODO"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; +} + +/// The squeezing phase of whichever XOF the factory selected. +/// +/// [`XOF::into_squeezer`] consumes the factory value, so this enum is what remains; like +/// [`XOFFactory`] itself it dispatches on the variant. +pub enum XOFFactorySqueezer { + /// SHAKE128 output. + SHAKE128(::Squeezer), + + /// SHAKE256 output. + SHAKE256(::Squeezer), + + /// Ascon-XOF128 output. + AsconXof128(::Squeezer), +} + +impl XOFSqueezer for XOFFactorySqueezer { + fn do_output(&mut self, num_bytes: usize) -> Vec { match self { - Self::SHAKE128(h) => h.hash_xof(data, result_len), - Self::SHAKE256(h) => h.hash_xof(data, result_len), + Self::SHAKE128(o) => o.do_output(num_bytes), + Self::SHAKE256(o) => o.do_output(num_bytes), + Self::AsconXof128(o) => o.do_output(num_bytes), } } - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { - output.fill(0); + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + match self { + Self::SHAKE128(o) => o.do_output_out(output), + Self::SHAKE256(o) => o.do_output_out(output), + Self::AsconXof128(o) => o.do_output_out(output), + } + } +} +impl Hash for XOFFactory { + fn block_bitlen(&self) -> usize { match self { - Self::SHAKE128(h) => h.hash_xof_out(data, output), - Self::SHAKE256(h) => h.hash_xof_out(data, output), + Self::SHAKE128(h) => h.block_bitlen(), + Self::SHAKE256(h) => h.block_bitlen(), + Self::AsconXof128(h) => h.block_bitlen(), } } - fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { + fn output_len(&self) -> usize { match self { - Self::SHAKE128(h) => h.absorb(data), - Self::SHAKE256(h) => h.absorb(data), + Self::SHAKE128(h) => h.output_len(), + Self::SHAKE256(h) => h.output_len(), + Self::AsconXof128(h) => h.output_len(), } } - fn absorb_last_partial_byte( - &mut self, - partial_byte: u8, - num_partial_bits: usize, - ) -> Result<(), HashError> { + fn hash(self, data: &[u8]) -> Vec { match self { - Self::SHAKE128(h) => h.absorb_last_partial_byte(partial_byte, num_partial_bits), - Self::SHAKE256(h) => h.absorb_last_partial_byte(partial_byte, num_partial_bits), + Self::SHAKE128(h) => h.hash(data), + Self::SHAKE256(h) => h.hash(data), + Self::AsconXof128(h) => h.hash(data), } } - fn squeeze(&mut self, num_bytes: usize) -> Vec { + fn hash_out(self, data: &[u8], output: &mut [u8]) -> usize { match self { - Self::SHAKE128(h) => h.squeeze(num_bytes), - Self::SHAKE256(h) => h.squeeze(num_bytes), + Self::SHAKE128(h) => h.hash_out(data, output), + Self::SHAKE256(h) => h.hash_out(data, output), + Self::AsconXof128(h) => h.hash_out(data, output), } } - fn squeeze_out(&mut self, output: &mut [u8]) -> usize { - output.fill(0); + fn do_update(&mut self, data: &[u8]) { + match self { + Self::SHAKE128(h) => h.do_update(data), + Self::SHAKE256(h) => h.do_update(data), + Self::AsconXof128(h) => h.do_update(data), + } + } + fn do_final(self) -> Vec { match self { - Self::SHAKE128(h) => h.squeeze_out(output), - Self::SHAKE256(h) => h.squeeze_out(output), + Self::SHAKE128(h) => h.do_final(), + Self::SHAKE256(h) => h.do_final(), + Self::AsconXof128(h) => h.do_final(), } } - fn squeeze_partial_byte_final(self, num_bits: usize) -> Result { + fn do_final_out(self, output: &mut [u8]) -> usize { match self { - Self::SHAKE128(h) => h.squeeze_partial_byte_final(num_bits), - Self::SHAKE256(h) => h.squeeze_partial_byte_final(num_bits), + Self::SHAKE128(h) => h.do_final_out(output), + Self::SHAKE256(h) => h.do_final_out(output), + Self::AsconXof128(h) => h.do_final_out(output), } } - fn squeeze_partial_byte_final_out( + fn do_final_partial_bits( self, + partial_byte: u8, num_bits: usize, - output: &mut u8, - ) -> Result<(), HashError> { - *output = 0; + ) -> Result, HashError> { + match self { + Self::SHAKE128(h) => h.do_final_partial_bits(partial_byte, num_bits), + Self::SHAKE256(h) => h.do_final_partial_bits(partial_byte, num_bits), + Self::AsconXof128(h) => h.do_final_partial_bits(partial_byte, num_bits), + } + } + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { match self { - Self::SHAKE128(h) => h.squeeze_partial_byte_final_out(num_bits, output), - Self::SHAKE256(h) => h.squeeze_partial_byte_final_out(num_bits, output), + Self::SHAKE128(h) => h.do_final_partial_bits_out(partial_byte, num_bits, output), + Self::SHAKE256(h) => h.do_final_partial_bits_out(partial_byte, num_bits, output), + Self::AsconXof128(h) => h.do_final_partial_bits_out(partial_byte, num_bits, output), } } fn max_security_strength(&self) -> SecurityStrength { match self { - Self::SHAKE128(h) => KDF::max_security_strength(h), - Self::SHAKE256(h) => XOF::max_security_strength(h), + Self::SHAKE128(h) => Hash::max_security_strength(h), + Self::SHAKE256(h) => Hash::max_security_strength(h), + Self::AsconXof128(h) => Hash::max_security_strength(h), + } + } +} + +impl XOF for XOFFactory { + type Squeezer = XOFFactorySqueezer; + + fn into_squeezer(self) -> Self::Squeezer { + match self { + Self::SHAKE128(h) => XOFFactorySqueezer::SHAKE128(h.into_squeezer()), + Self::SHAKE256(h) => XOFFactorySqueezer::SHAKE256(h.into_squeezer()), + Self::AsconXof128(h) => XOFFactorySqueezer::AsconXof128(h.into_squeezer()), + } + } + + fn into_squeezer_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result { + Ok(match self { + Self::SHAKE128(h) => { + XOFFactorySqueezer::SHAKE128(h.into_squeezer_partial_bits(partial_byte, num_bits)?) + } + Self::SHAKE256(h) => { + XOFFactorySqueezer::SHAKE256(h.into_squeezer_partial_bits(partial_byte, num_bits)?) + } + Self::AsconXof128(h) => XOFFactorySqueezer::AsconXof128( + h.into_squeezer_partial_bits(partial_byte, num_bits)?, + ), + }) + } + + fn xof(self, data: &[u8], result_len: usize) -> Vec { + match self { + Self::SHAKE128(h) => h.xof(data, result_len), + Self::SHAKE256(h) => h.xof(data, result_len), + Self::AsconXof128(h) => h.xof(data, result_len), + } + } + + fn xof_out(self, data: &[u8], output: &mut [u8]) -> usize { + output.fill(0); + + match self { + Self::SHAKE128(h) => h.xof_out(data, output), + Self::SHAKE256(h) => h.xof_out(data, output), + Self::AsconXof128(h) => h.xof_out(data, output), } } } diff --git a/crypto/factory/tests/hash_factory_tests.rs b/crypto/factory/tests/hash_factory_tests.rs index 8d90be83..5d70757f 100644 --- a/crypto/factory/tests/hash_factory_tests.rs +++ b/crypto/factory/tests/hash_factory_tests.rs @@ -160,8 +160,32 @@ mod hash_factory_tests { #[test] fn sha3_xof_tests() { - assert_eq!(XOFFactory::new("SHAKE128").unwrap().hash_xof(&DUMMY_SEED[..512], 32), b"\x88\x90\xed\x20\x4d\x22\x89\xe1\x72\xe9\xae\x68\x48\x18\x23\x77\x08\x20\x90\x80\x60\xa4\xdf\x33\x51\xa3\xf1\x84\xeb\xb6\xdd\x0f"); - assert_eq!(XOFFactory::new("SHAKE256").unwrap().hash_xof(&DUMMY_SEED[..512], 32), b"\xa1\xd7\x18\x85\xb0\xa8\x41\xf0\x3d\x1d\xc7\xf2\x73\x8a\x15\xcc\x98\x40\x71\xa1\x7f\xfe\xd5\xec\xac\xb9\xf5\x87\x20\xa4\x73\xbe"); + assert_eq!(XOFFactory::new("SHAKE128").unwrap().xof(&DUMMY_SEED[..512], 32), b"\x88\x90\xed\x20\x4d\x22\x89\xe1\x72\xe9\xae\x68\x48\x18\x23\x77\x08\x20\x90\x80\x60\xa4\xdf\x33\x51\xa3\xf1\x84\xeb\xb6\xdd\x0f"); + assert_eq!(XOFFactory::new("SHAKE256").unwrap().xof(&DUMMY_SEED[..512], 32), b"\xa1\xd7\x18\x85\xb0\xa8\x41\xf0\x3d\x1d\xc7\xf2\x73\x8a\x15\xcc\x98\x40\x71\xa1\x7f\xfe\xd5\xec\xac\xb9\xf5\x87\x20\xa4\x73\xbe"); + } + + #[test] + fn ascon_hash_tests() { + use bouncycastle_ascon::ASCON_HASH256_NAME; + use bouncycastle_ascon::ascon_hash256::AsconHash256; + use bouncycastle_factory::FactoryError; + + let direct = AsconHash256::new().hash(&DUMMY_SEED[..512]); + + // Construct by literal name and by the crate's name constant; both must match the + // direct implementation. + let by_name = HashFactory::new("Ascon-Hash256").unwrap(); + assert_eq!(by_name.output_len(), 32); + assert_eq!(by_name.hash(&DUMMY_SEED[..512]), direct); + + let by_const = HashFactory::new(ASCON_HASH256_NAME).unwrap(); + assert_eq!(by_const.hash(&DUMMY_SEED[..512]), direct); + + // Unknown algorithm names are still rejected. + assert!(matches!( + HashFactory::new("Ascon-Hash999"), + Err(FactoryError::UnsupportedAlgorithm(_)) + )); } #[test] diff --git a/crypto/factory/tests/xof_factory_tests.rs b/crypto/factory/tests/xof_factory_tests.rs index 7e414f94..2dce009e 100644 --- a/crypto/factory/tests/xof_factory_tests.rs +++ b/crypto/factory/tests/xof_factory_tests.rs @@ -1,4 +1,177 @@ -#[cfg(test)] -mod tests { - // todo +//! `XOFFactory` is a pass-through to the concrete XOF implementations, so the oracle for +//! every method is the same call on the underlying type. Each check below runs the factory and the +//! direct type side by side on the same input; nothing here is an expected value written by hand. + +use bouncycastle_ascon::ASCON_XOF128_NAME; +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; +use bouncycastle_factory::xof_factory::XOFFactory; +use bouncycastle_factory::{AlgorithmFactory, FactoryError}; +use bouncycastle_sha3::{SHAKE128, SHAKE128_NAME, SHAKE256, SHAKE256_NAME}; + +const MSG: &[u8] = b"The quick brown fox jumps over the lazy dog"; + +/// Every `Hash`, `XOF` and `XOFSqueezer` method of the factory against the direct type `S`. +fn check_against(make: impl Fn() -> XOFFactory, ctx: &str) { + let n = S::default().output_len(); + + // metadata + assert_eq!(make().block_bitlen(), S::default().block_bitlen(), "{ctx}: block_bitlen"); + assert_eq!(make().output_len(), n, "{ctx}: output_len"); + assert_eq!( + Hash::max_security_strength(&make()), + Hash::max_security_strength(&S::default()), + "{ctx}: max_security_strength" + ); + + // the Hash view + let expected = S::default().hash(MSG); + assert_eq!(expected.len(), n); + assert_eq!(make().hash(MSG), expected, "{ctx}: hash"); + + let mut out = vec![0u8; n]; + assert_eq!(make().hash_out(MSG, &mut out), n, "{ctx}: hash_out returns the length"); + assert_eq!(out, expected, "{ctx}: hash_out"); + + let mut f = make(); + MSG.chunks(5).for_each(|c| f.do_update(c)); + assert_eq!(f.do_final(), expected, "{ctx}: do_update then do_final"); + + let mut f = make(); + f.do_update(MSG); + let mut out = vec![0u8; n]; + assert_eq!(f.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, expected, "{ctx}: do_final_out"); + + // partial final byte, which SHAKE accepts + let mut s = S::default(); + s.do_update(MSG); + let expected_bits = s.do_final_partial_bits(0x05, 3).unwrap(); + assert_ne!(expected_bits, expected, "three more bits must change the digest"); + + let mut f = make(); + f.do_update(MSG); + assert_eq!(f.do_final_partial_bits(0x05, 3).unwrap(), expected_bits, "{ctx}: partial bits"); + + let mut f = make(); + f.do_update(MSG); + let mut out = vec![0u8; n]; + assert_eq!(f.do_final_partial_bits_out(0x05, 3, &mut out).unwrap(), n, "{ctx}: ..._out length"); + assert_eq!(out, expected_bits, "{ctx}: do_final_partial_bits_out"); + + let mut f = make(); + f.do_update(MSG); + assert!( + matches!(f.do_final_partial_bits(0xFF, 8), Err(HashError::InvalidLength(_))), + "{ctx}: eight partial bits is not a partial byte" + ); + + // the XOF view: one stream, of which the Hash view is the first output_len bytes + let mut s = S::default(); + s.do_update(MSG); + let long = s.into_squeezer().do_output(3 * n); + assert_eq!(&long[..n], &expected[..], "the direct type's hash is a prefix of its stream"); + + let mut f = make(); + f.do_update(MSG); + let mut fo = f.into_squeezer(); + assert_eq!(fo.do_output(n), &long[..n], "{ctx}: do_output"); + + let mut buf = vec![0u8; 2 * n]; + assert_eq!(fo.do_output_out(&mut buf), 2 * n, "{ctx}: do_output_out returns the length"); + assert_eq!(buf, &long[n..], "{ctx}: do_output_out continues the stream"); + + let mut s = S::default(); + s.do_update(MSG); + let want = s.into_squeezer_partial_bits(0x05, 3).unwrap().do_output(n); + + let mut f = make(); + f.do_update(MSG); + assert_eq!( + f.into_squeezer_partial_bits(0x05, 3).unwrap().do_output(n), + want, + "{ctx}: into_squeezer_partial_bits" + ); + + let mut f = make(); + f.do_update(MSG); + assert!(matches!(f.into_squeezer_partial_bits(0xFF, 8), Err(HashError::InvalidLength(_)))); + + // the one-shots + assert_eq!(make().xof(MSG, 3 * n), long, "{ctx}: xof"); + + let mut out = vec![0xFFu8; 3 * n]; + assert_eq!(make().xof_out(MSG, &mut out), 3 * n, "{ctx}: xof_out returns the length"); + assert_eq!(out, long, "{ctx}: xof_out"); +} + +#[test] +fn shake128_by_name_matches_the_direct_type() { + check_against::(|| XOFFactory::new(SHAKE128_NAME).unwrap(), "SHAKE128 by constant"); + check_against::(|| XOFFactory::new("SHAKE128").unwrap(), "SHAKE128 by string"); +} + +#[test] +fn shake256_by_name_matches_the_direct_type() { + check_against::(|| XOFFactory::new(SHAKE256_NAME).unwrap(), "SHAKE256 by constant"); + check_against::(|| XOFFactory::new("SHAKE256").unwrap(), "SHAKE256 by string"); +} + +/// Verify that the Ascon-XOF128 factory registration resolves to the same implementation +/// as constructing Ascon-XOF128 directly. +#[test] +fn ascon_xof128_by_name_matches_the_direct_type() { + let direct = AsconXof128::new().xof(MSG, 64); + + // Construct using the crate constant. + assert_eq!( + XOFFactory::new(ASCON_XOF128_NAME).unwrap().xof(MSG, 64), + direct, + "Ascon-XOF128 by constant" + ); + + // Construct using the literal algorithm name. + assert_eq!( + XOFFactory::new("Ascon-XOF128").unwrap().xof(MSG, 64), + direct, + "Ascon-XOF128 by string" + ); +} + +/// The configured defaults: SHAKE128 for the general and 128-bit defaults, SHAKE256 for 256-bit. +#[test] +fn defaults() { + check_against::(XOFFactory::default, "default()"); + check_against::(XOFFactory::default_128_bit, "default_128_bit()"); + check_against::(XOFFactory::default_256_bit, "default_256_bit()"); +} + +#[test] +fn unknown_names_are_refused() { + for name in ["SHAKE512", "shake128", "", "cSHAKE128", "Ascon-XOF999"] { + assert!( + matches!(XOFFactory::new(name), Err(FactoryError::UnsupportedAlgorithm(_))), + "{name:?} must not construct a XOF" + ); + } +} + +/// The shared `XOF` conformance suite, with the expected stream taken from the direct type. +#[test] +fn test_framework_xof() { + let framework = TestFrameworkXOF::new(); + + framework.test_xof( + || XOFFactory::new(SHAKE128_NAME).unwrap(), + MSG, + &SHAKE128::new().xof(MSG, 100), + ); + + framework.test_xof( + || XOFFactory::new(SHAKE256_NAME).unwrap(), + MSG, + &SHAKE256::new().xof(MSG, 100), + ); } diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 5eaf55f3..7f5c702a 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -7,7 +7,7 @@ use crate::params::{ MLDSAParams, }; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_utils::secret::ZeroizablePrimitive; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) @@ -433,9 +433,10 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 3: ctx ← H.Absorb(ctx, 𝜌) // 4: (ctx, 𝑠) ← H.Squeeze(ctx, 8) let mut h = H::new(); - h.absorb(rho.as_ref()).expect("absorb before squeeze is infallible"); + h.do_update(rho.as_ref()); let mut s = [0u8; 8]; - h.squeeze_out(&mut s); + let mut h = h.into_squeezer(); + h.do_output_out(&mut s); // 5: ℎ ← BytesToBits(𝑠) // ▷ ℎ is a bit string of length 64 @@ -453,13 +454,13 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 7: (ctx, 𝑗) ← H.Squeeze(ctx, 1) // Note: At first, it might seem to be faster to pre-squeeze a buffer outside the loop. // However, after experimentation and testing, the difference is not noticeable. - h.squeeze_out(&mut j); + h.do_output_out(&mut j); // 8: while 𝑗 > 𝑖 do while j[0] as usize > i { // ▷ rejection sampling in {0, … , 𝑖} // 9: (ctx, 𝑗) ← H.Squeeze(ctx, 1) - h.squeeze_out(&mut j); + h.do_output_out(&mut j); } // 11: 𝑐𝑖 ← 𝑐𝑗 @@ -496,8 +497,8 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { let mut w_hat = Polynomial::new(); let mut j: usize = 0; let mut g = G::new(); - g.absorb(rho).expect("absorb before squeeze is infallible"); - g.absorb(nonce).expect("absorb before squeeze is infallible"); + g.do_update(rho); + g.do_update(nonce); // SHAKE is fairly inefficient if only 3 bytes are squeezed at a time, so the implementation does a block instead. // size is not a limitation, so long as it's a multiple of 3. @@ -505,12 +506,13 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's probably around the average rejection rate, and 288 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut s = [0u8; 288]; - g.squeeze_out(&mut s); + let mut g = g.into_squeezer(); + g.do_output_out(&mut s); let mut idx: usize = 0; while j < N { if idx == s.len() { - g.squeeze_out(&mut s); + g.do_output_out(&mut s); idx = 0; } w_hat[j] = match coeff_from_three_bytes(&s[idx..idx + 3].try_into().unwrap()) { @@ -541,8 +543,8 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) let mut a = Polynomial::new(); let mut j: usize = 0; let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); - h.absorb(nonce).expect("absorb before squeeze is infallible"); + h.do_update(rho); + h.do_update(nonce); // SHAKE is fairly inefficient if only 3 bytes are squeezed at a time, so the implementation does a block instead. // size is not a limitation as long as it is a multiple of 3. @@ -550,7 +552,8 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) // which is possibly also related with the average rejection rate. // Also, 312 is a multiple of 8 (efficient for SHAKE) let mut z_arr = [0u8; 312]; - h.squeeze_out(&mut z_arr); + let mut h = h.into_squeezer(); + h.do_output_out(&mut z_arr); let mut idx: usize = 0; while j < N { @@ -568,7 +571,7 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) idx += 1; if idx == z_arr.len() { - h.squeeze_out(&mut z_arr); + h.do_output_out(&mut z_arr); idx = 0; } } @@ -588,10 +591,11 @@ pub(crate) fn expand_mask_poly(rho: &[u8; 64], nonce: u16) -> Po // The 32𝑐 bytes squeezed on line 4 are exactly `P::POLY_Z_PACKED_LEN`, so the buffer for them // is `P::PolyZPacked`; see the docs on `MLDSAParams::POLY_Z_PACKED_LEN`. let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); - h.absorb(&nonce.to_le_bytes()).expect("absorb before squeeze is infallible"); + h.do_update(rho); + h.do_update(&nonce.to_le_bytes()); let mut v = ::ZEROED; - h.squeeze_out(v.as_mut()); + let mut h = h.into_squeezer(); + h.do_output_out(v.as_mut()); bit_unpack_gamma1::

(v.as_ref()) } diff --git a/crypto/mldsa-lowmemory/src/hash_mldsa.rs b/crypto/mldsa-lowmemory/src/hash_mldsa.rs index 8e89589f..35561bf6 100644 --- a/crypto/mldsa-lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa-lowmemory/src/hash_mldsa.rs @@ -84,7 +84,7 @@ use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SignatureVerifier, Signer, - XOF, + XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; @@ -343,19 +343,19 @@ impl< // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) let mut h = H::new(); - h.absorb(&sk.tr()).expect("absorb before squeeze is infallible"); + h.do_update(&sk.tr()); // Algorithm 4 // 23: 𝑀' ← BytesToBits(IntegerToBytes(1, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥 ∥ OID ∥ PH𝑀) // all done together - h.absorb(&[1u8]).expect("absorb before squeeze is infallible"); - h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - h.absorb(ctx).expect("absorb before squeeze is infallible"); - h.absorb(::OID_DER) - .expect("absorb before squeeze is infallible"); - h.absorb(ph).expect("absorb before squeeze is infallible"); + h.do_update(&[1u8]); + h.do_update(&[ctx.len() as u8]); + h.do_update(ctx); + h.do_update(::OID_DER); + h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - let bytes_written = h.squeeze_out(&mut mu); + let mut h = h.into_squeezer(); + let bytes_written = h.do_output_out(&mut mu); debug_assert_eq!(bytes_written, MLDSA_MU_LEN); // 24: 𝜎 ← ML-DSA.Sign_internal(𝑠𝑘, 𝑀', 𝑟𝑛𝑑) @@ -632,19 +632,19 @@ impl< // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) let mut h = H::new(); - h.absorb(&pk.compute_tr()).expect("absorb before squeeze is infallible"); + h.do_update(&pk.compute_tr()); // Algorithm 4 // 23: 𝑀 ← BytesToBits(IntegerToBytes(1, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥 ∥ OID ∥ PH𝑀) // all done together - h.absorb(&[1u8]).expect("absorb before squeeze is infallible"); - h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - h.absorb(ctx).expect("absorb before squeeze is infallible"); - h.absorb(::OID_DER) - .expect("absorb before squeeze is infallible"); - h.absorb(ph).expect("absorb before squeeze is infallible"); + h.do_update(&[1u8]); + h.do_update(&[ctx.len() as u8]); + h.do_update(ctx); + h.do_update(::OID_DER); + h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - _ = h.squeeze_out(&mut mu); + let mut h = h.into_squeezer(); + _ = h.do_output_out(&mut mu); MLDSA::::verify_mu( pk, &mu, sig_sized, diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index e9090a5c..6af355d5 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -400,7 +400,7 @@ use bouncycastle_core::errors::{RNGError, SignatureError, SuspendableError}; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, RNG, SignatureVerifier, Signer, Suspendable, XOF, + Algorithm, AlgorithmOID, Hash, RNG, SignatureVerifier, Signer, Suspendable, XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; @@ -789,11 +789,12 @@ impl< // Alg 7; 7: 𝜌″ ← H(𝐾||𝑟𝑛𝑑||𝜇, 64) let rho_p_p: [u8; 64] = { let mut h = H::new(); - h.absorb(sk.K()).expect("absorb before squeeze is infallible"); - h.absorb(&rnd).expect("absorb before squeeze is infallible"); - h.absorb(mu).expect("absorb before squeeze is infallible"); + h.do_update(sk.K()); + h.do_update(&rnd); + h.do_update(mu); let mut rho_p_p = [0u8; 64]; - h.squeeze_out(&mut rho_p_p); + let mut h = h.into_squeezer(); + h.do_output_out(&mut rho_p_p); rho_p_p }; @@ -819,15 +820,15 @@ impl< let sig_val_c_tilde = { // scope for hash let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); for row in 0..P::k { let mut w = compute_w_row::

(&sk.rho(), &rho_p_p, kappa, row); w.high_bits::

(); - hash.absorb(w.w1_encode::

().as_ref()) - .expect("absorb before squeeze is infallible"); + hash.do_update(w.w1_encode::

().as_ref()); } let mut sig_val_c_tilde = ::ZEROED; - hash.squeeze_out(sig_val_c_tilde.as_mut()); + let mut hash = hash.into_squeezer(); + hash.do_output_out(sig_val_c_tilde.as_mut()); sig_val_c_tilde }; // 16: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) @@ -1015,7 +1016,7 @@ impl< // 12: 𝑐_tilde_p ← H(𝜇||w1Encode(𝐰1'), 𝜆/4) // ▷ hash it; this should match 𝑐_tilde let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); for row in 0..P::k { let mut wp_approx = match { @@ -1036,12 +1037,12 @@ impl< // 10: 𝐰1′ ← UseHint(𝐡, 𝐰'_approx) // ▷ reconstruction of signer’s commitment wp_approx.use_hint::

(&h_i); - hash.absorb(wp_approx.w1_encode::

().as_ref()) - .expect("absorb before squeeze is infallible"); + hash.do_update(wp_approx.w1_encode::

().as_ref()); } let mut c_tilde_p = ::ZEROED; - hash.squeeze_out(c_tilde_p.as_mut()); + let mut hash = hash.into_squeezer(); + hash.do_output_out(c_tilde_p.as_mut()); // Verification is also done in constant time // 13 (second half): return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] and [[𝑐 ̃ = 𝑐′ ]] @@ -1448,14 +1449,14 @@ impl MuBuilder { // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) let mut mb = Self { h: H::new() }; - mb.h.absorb(tr).expect("absorb before squeeze is infallible"); + mb.h.do_update(tr); // Algorithm 2 // 10: 𝑀′ ← BytesToBits(IntegerToBytes(0, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥) ∥ 𝑀 // all done together - mb.h.absorb(&[0u8]).expect("absorb before squeeze is infallible"); - mb.h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - mb.h.absorb(ctx).expect("absorb before squeeze is infallible"); + mb.h.do_update(&[0u8]); + mb.h.do_update(&[ctx.len() as u8]); + mb.h.do_update(ctx); // now ready to absorb M Ok(mb) @@ -1463,16 +1464,16 @@ impl MuBuilder { /// Stream a chunk of the message. pub fn do_update(&mut self, msg_chunk: &[u8]) { - self.h.absorb(msg_chunk).expect("absorb before squeeze is infallible"); + self.h.do_update(msg_chunk); } /// Finalize and return the mu value. - pub fn do_final(mut self) -> [u8; 64] { + pub fn do_final(self) -> [u8; 64] { // Completion of // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) let mut mu = [0u8; 64]; - self.h.squeeze_out(&mut mu); + self.h.into_squeezer().do_output_out(&mut mu); mu } diff --git a/crypto/mldsa-lowmemory/src/mldsa_keys.rs b/crypto/mldsa-lowmemory/src/mldsa_keys.rs index 35c216aa..bfa0d0f7 100644 --- a/crypto/mldsa-lowmemory/src/mldsa_keys.rs +++ b/crypto/mldsa-lowmemory/src/mldsa_keys.rs @@ -12,7 +12,7 @@ use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; -use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey, XOF}; +use bouncycastle_core::traits::{Hash, SignaturePrivateKey, SignaturePublicKey, XOF, XOFSqueezer}; use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; use core::fmt; use core::fmt::{Debug, Display, Formatter}; @@ -96,7 +96,7 @@ impl MLDSAPublicKeyTrait fn compute_tr(&self) -> [u8; 64] { let mut tr = [0u8; 64]; - H::new().hash_xof_out(&self.encode(), &mut tr); + H::new().xof_out(&self.encode(), &mut tr); tr } @@ -338,14 +338,15 @@ impl = Secret::new(); let mut h = H::default(); - h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - let bytes_written = h.squeeze_out(&mut rho); + h.do_update(seed.ref_to_bytes()); + h.do_update(&(P::k as u8).to_le_bytes()); + h.do_update(&(P::l as u8).to_le_bytes()); + let mut h = h.into_squeezer(); + let bytes_written = h.do_output_out(&mut rho); debug_assert_eq!(bytes_written, 32); - let bytes_written = h.squeeze_out(rho_prime.deref_mut()); + let bytes_written = h.do_output_out(rho_prime.deref_mut()); debug_assert_eq!(bytes_written, 64); - let bytes_written = h.squeeze_out(K.deref_mut()); + let bytes_written = h.do_output_out(K.deref_mut()); debug_assert_eq!(bytes_written, 32); (rho, rho_prime, K) diff --git a/crypto/mldsa-lowmemory/tests/bc_test_data.rs b/crypto/mldsa-lowmemory/tests/bc_test_data.rs index a4c0d029..4972f557 100644 --- a/crypto/mldsa-lowmemory/tests/bc_test_data.rs +++ b/crypto/mldsa-lowmemory/tests/bc_test_data.rs @@ -1,9 +1,9 @@ +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; // Test against the bc-test-data repo // Requires that the bc-test-data repository is cloned and available for testing at "../bc-test-data" // relative to the root of this git project. use bouncycastle_core::errors::SignatureError; -use bouncycastle_core::traits::XOF; use bouncycastle_sha3::SHAKE256; #[allow(unused_imports)] @@ -20,7 +20,7 @@ mod bc_test_data { use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Hash, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, + Hash, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, XOF, XOFSqueezer, }; use bouncycastle_hex as hex; use bouncycastle_mldsa_lowmemory::{ @@ -965,14 +965,14 @@ impl BustedMuBuilder { // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) let mut mb = Self { h: SHAKE256::new() }; - mb.h.absorb(tr).expect("absorb before squeeze is infallible"); + mb.h.do_update(tr); // Algorithm 2 // 10: 𝑀′ ← BytesToBits(IntegerToBytes(0, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥) ∥ 𝑀 // all done together - // mb.h.absorb(&[0u8]); // these are the busted lines -- bc-java just doesn't do these in the test code - // mb.h.absorb(&[ctx.len() as u8]); - // mb.h.absorb(ctx); + // mb.h.do_update(&[0u8]); // these are the busted lines -- bc-java just doesn't do these in the test code + // mb.h.do_update(&[ctx.len() as u8]); + // mb.h.do_update(ctx); // now ready to absorb M Ok(mb) @@ -980,16 +980,16 @@ impl BustedMuBuilder { /// Stream a chunk of the message. pub fn do_update(&mut self, msg_chunk: &[u8]) { - self.h.absorb(msg_chunk).expect("absorb before squeeze is infallible"); + self.h.do_update(msg_chunk); } /// Finalize and return the mu value. - pub fn do_final(mut self) -> [u8; 64] { + pub fn do_final(self) -> [u8; 64] { // Completion of // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) let mut mu = [0u8; 64]; - self.h.squeeze_out(&mut mu); + self.h.into_squeezer().do_output_out(&mut mu); mu } diff --git a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs index 2921f096..ae84fdfa 100644 --- a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs @@ -7,7 +7,7 @@ mod mldsa_tests { use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - RNG, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, Signer, Suspendable, + Hash, RNG, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, Signer, Suspendable, }; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_core_test_framework::FixedSeedRNG; @@ -867,7 +867,6 @@ mod mldsa_tests { #[test] fn serializable_state_mubuilder_rejects_wrong_variant() { - use bouncycastle_core::traits::XOF; use bouncycastle_sha3::SHAKE128; // A MuBuilder is always backed by SHAKE256. A serialized SHAKE128 state has the same length @@ -875,9 +874,7 @@ mod mldsa_tests { // variant tag weren't checked -- SHAKE128 (tag 5) must be rejected by MuBuilder (SHAKE256, // tag 6). let mut shake128 = SHAKE128::new(); - shake128 - .absorb(b"Colorless green ideas sleep furiously") - .expect("absorb before squeeze is infallible"); + shake128.do_update(b"Colorless green ideas sleep furiously"); let serialized_128 = shake128.suspend(); match MuBuilder::from_suspended(serialized_128) { diff --git a/crypto/mldsa/src/aux_functions.rs b/crypto/mldsa/src/aux_functions.rs index f603a45c..bc9f1b1e 100644 --- a/crypto/mldsa/src/aux_functions.rs +++ b/crypto/mldsa/src/aux_functions.rs @@ -7,7 +7,7 @@ use crate::params::{ MLDSAParams, }; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) @@ -500,9 +500,10 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 3: ctx ← H.Absorb(ctx, 𝜌) // 4: (ctx, 𝑠) ← H.Squeeze(ctx, 8) let mut h = H::new(); - h.absorb(rho.as_ref()).expect("absorb before squeeze is infallible"); + h.do_update(rho.as_ref()); let mut s = [0u8; 8]; - h.squeeze_out(&mut s); + let mut h = h.into_squeezer(); + h.do_output_out(&mut s); // 5: ℎ ← BytesToBits(𝑠) // ▷ ℎ is a bit string of length 64 @@ -521,13 +522,13 @@ pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // Note: Even though it may appear that pre-squeezing a buffer outside the loop would be faster, // testing it both ways doesn't make a noticeable difference, so this has been left as is // for better correspondence with the FIPS sample algorithm. - h.squeeze_out(&mut j); + h.do_output_out(&mut j); // 8: while 𝑗 > 𝑖 do while j[0] as usize > i { // ▷ rejection sampling in {0, … , 𝑖} // 9: (ctx, 𝑗) ← H.Squeeze(ctx, 1) - h.squeeze_out(&mut j); + h.do_output_out(&mut j); } // 11: 𝑐𝑖 ← 𝑐𝑗 @@ -564,8 +565,8 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { let mut w_hat = Polynomial::new(); let mut j: usize = 0; let mut g = G::new(); - g.absorb(rho).expect("absorb before squeeze is infallible"); - g.absorb(nonce).expect("absorb before squeeze is infallible"); + g.do_update(rho); + g.do_update(nonce); // SHAKE is fairly inefficient if only 3 bytes are squeezed at a time, so instead this implementation does a block. // Size is not a limitation, so long as it's a multiple of 3. @@ -573,12 +574,13 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's probably around the average rejection rate, and 288 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut s = [0u8; 288]; - g.squeeze_out(&mut s); + let mut g = g.into_squeezer(); + g.do_output_out(&mut s); let mut idx: usize = 0; while j < N { if idx == s.len() { - g.squeeze_out(&mut s); + g.do_output_out(&mut s); idx = 0; } w_hat[j] = match coeff_from_three_bytes(&s[idx..idx + 3].try_into().unwrap()) { @@ -609,15 +611,16 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) let mut a = Polynomial::new(); let mut j: usize = 0; let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); - h.absorb(nonce).expect("absorb before squeeze is infallible"); + h.do_update(rho); + h.do_update(nonce); // size doesn't really matter // 312 seemed to be the sweet spot from playing with benchmarks // maybe something to do with the average rejection rate? // Also, 312 is a multiple of 8 (efficient for SHAKE) let mut z_arr = [0u8; 312]; - h.squeeze_out(&mut z_arr); + let mut h = h.into_squeezer(); + h.do_output_out(&mut z_arr); let mut idx: usize = 0; while j < N { @@ -635,7 +638,7 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) idx += 1; if idx == z_arr.len() { - h.squeeze_out(&mut z_arr); + h.do_output_out(&mut z_arr); idx = 0; } } @@ -713,11 +716,11 @@ pub(crate) fn expand_mask(rho: &[u8; 64], mu: u16) -> P::VecL { // 4: 𝑣 ← H(𝜌′, 32𝑐) let v = { let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); - h.absorb(&(mu + (r as u16)).to_le_bytes()) - .expect("absorb before squeeze is infallible"); + h.do_update(rho); + h.do_update(&(mu + (r as u16)).to_le_bytes()); let mut v = ::ZEROED; - h.squeeze_out(v.as_mut()); + let mut h = h.into_squeezer(); + h.do_output_out(v.as_mut()); v }; diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index b20fa196..16429732 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -85,7 +85,7 @@ use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SignatureVerifier, Signer, - XOF, + XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; @@ -385,19 +385,19 @@ impl< // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) let mu = { let mut h = H::new(); - h.absorb(sk.tr()).expect("absorb before squeeze is infallible"); + h.do_update(sk.tr()); // Algorithm 4 // 23: 𝑀' ← BytesToBits(IntegerToBytes(1, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥 ∥ OID ∥ PH𝑀) // all done together - h.absorb(&[1u8]).expect("absorb before squeeze is infallible"); - h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - h.absorb(ctx).expect("absorb before squeeze is infallible"); - h.absorb(::OID_DER) - .expect("absorb before squeeze is infallible"); - h.absorb(ph).expect("absorb before squeeze is infallible"); + h.do_update(&[1u8]); + h.do_update(&[ctx.len() as u8]); + h.do_update(ctx); + h.do_update(::OID_DER); + h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - let bytes_written = h.squeeze_out(&mut mu); + let mut h = h.into_squeezer(); + let bytes_written = h.do_output_out(&mut mu); debug_assert_eq!(bytes_written, MLDSA_MU_LEN); mu @@ -490,19 +490,19 @@ impl< // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) let mu = { let mut h = H::new(); - h.absorb(&pk.compute_tr()).expect("absorb before squeeze is infallible"); + h.do_update(&pk.compute_tr()); // Algorithm 4 // 23: 𝑀 ← BytesToBits(IntegerToBytes(1, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥 ∥ OID ∥ PH𝑀) // all done together - h.absorb(&[1u8]).expect("absorb before squeeze is infallible"); - h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - h.absorb(ctx).expect("absorb before squeeze is infallible"); - h.absorb(::OID_DER) - .expect("absorb before squeeze is infallible"); - h.absorb(ph).expect("absorb before squeeze is infallible"); + h.do_update(&[1u8]); + h.do_update(&[ctx.len() as u8]); + h.do_update(ctx); + h.do_update(::OID_DER); + h.do_update(ph); let mut mu = [0u8; MLDSA_MU_LEN]; - _ = h.squeeze_out(&mut mu); + let mut h = h.into_squeezer(); + _ = h.do_output_out(&mut mu); mu }; diff --git a/crypto/mldsa/src/matrix.rs b/crypto/mldsa/src/matrix.rs index 51a5aa90..31ac9856 100644 --- a/crypto/mldsa/src/matrix.rs +++ b/crypto/mldsa/src/matrix.rs @@ -5,7 +5,7 @@ use crate::aux_functions::multiply_ntt; use crate::mldsa::H; use crate::params::MLDSAParams; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::Hash; use bouncycastle_utils::secret::ZeroizablePrimitive; use core::ops::{Index, IndexMut}; @@ -311,7 +311,7 @@ impl VectorTrait for Vector { // 3: 𝐰̃1 ← 𝐰̃1 || SimpleBitPack (𝐰1[𝑖], (𝑞 − 1)/(2𝛾2) − 1) // 4: end for for w in self.elems.iter() { - h.absorb(w.w1_encode::

().as_ref()).expect("absorb before squeeze is infallible"); + h.do_update(w.w1_encode::

().as_ref()); } } } diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index 8ccd99d7..2ddd972b 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -491,7 +491,7 @@ use bouncycastle_core::errors::{RNGError, SignatureError, SuspendableError}; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterial256, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, RNG, SignatureVerifier, Signer, Suspendable, XOF, + Algorithm, AlgorithmOID, Hash, RNG, SignatureVerifier, Signer, Suspendable, XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; @@ -691,15 +691,16 @@ impl< let (s1_hat, mut s2) = { // scope for h let mut h = H::default(); - h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - let bytes_written = h.squeeze_out(&mut rho); + h.do_update(seed.ref_to_bytes()); + h.do_update(&(P::k as u8).to_le_bytes()); + h.do_update(&(P::l as u8).to_le_bytes()); + let mut h = h.into_squeezer(); + let bytes_written = h.do_output_out(&mut rho); debug_assert_eq!(bytes_written, 32); let mut rho_prime: [u8; 64] = [0u8; 64]; - let bytes_written = h.squeeze_out(&mut rho_prime); + let bytes_written = h.do_output_out(&mut rho_prime); debug_assert_eq!(bytes_written, 64); - let bytes_written = h.squeeze_out(&mut *K); + let bytes_written = h.do_output_out(&mut *K); debug_assert_eq!(bytes_written, 32); // 4: (𝐬1, 𝐬2) ← ExpandS(𝜌′) @@ -787,11 +788,12 @@ impl< // scope for h // 7: 𝜌″ ← H(𝐾||𝑟𝑛𝑑||𝜇, 64) let mut h = H::new(); - h.absorb(&**sk.K()).expect("absorb before squeeze is infallible"); - h.absorb(&rnd).expect("absorb before squeeze is infallible"); - h.absorb(mu).expect("absorb before squeeze is infallible"); + h.do_update(&**sk.K()); + h.do_update(&rnd); + h.do_update(mu); let mut rho_p_p = [0u8; 64]; - h.squeeze_out(&mut rho_p_p); + let mut h = h.into_squeezer(); + h.do_output_out(&mut rho_p_p); rho_p_p }; @@ -846,9 +848,10 @@ impl< // 15: 𝑐_tilde ← H(𝜇||w1Encode(𝐰1), 𝜆/4) // ▷ commitment hash let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); w1.w1_encode_and_hash::

(&mut hash); - hash.squeeze_out(sig_val_c_tilde.as_mut()); + let mut hash = hash.into_squeezer(); + hash.do_output_out(sig_val_c_tilde.as_mut()); } // 16: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) @@ -1028,9 +1031,10 @@ impl< let c_tilde_p = { let mut c_tilde_p = ::ZEROED; let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); w1p.w1_encode_and_hash::

(&mut hash); - hash.squeeze_out(c_tilde_p.as_mut()); + let mut hash = hash.into_squeezer(); + hash.do_output_out(c_tilde_p.as_mut()); c_tilde_p }; @@ -1251,17 +1255,18 @@ impl< // ▷ expand seed let (rho, rho_prime, K) = { let mut h = H::default(); - h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(P::l as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); + h.do_update(seed.ref_to_bytes()); + h.do_update(&(P::k as u8).to_le_bytes()); + h.do_update(&(P::l as u8).to_le_bytes()); let mut rho = [0u8; 32]; - let bytes_written = h.squeeze_out(&mut rho); + let mut h = h.into_squeezer(); + let bytes_written = h.do_output_out(&mut rho); debug_assert_eq!(bytes_written, 32); let mut rho_prime = [0u8; 64]; - let bytes_written = h.squeeze_out(&mut rho_prime); + let bytes_written = h.do_output_out(&mut rho_prime); debug_assert_eq!(bytes_written, 64); let mut K: [u8; 32] = [0u8; 32]; - let bytes_written = h.squeeze_out(&mut K); + let bytes_written = h.do_output_out(&mut K); debug_assert_eq!(bytes_written, 32); (rho, rho_prime, K) @@ -1270,11 +1275,12 @@ impl< // Alg 7; 7: 𝜌″ ← H(𝐾||𝑟𝑛𝑑||𝜇, 64) let rho_p_p = { let mut h = H::new(); - h.absorb(&K).expect("absorb before squeeze is infallible"); - h.absorb(&rnd).expect("absorb before squeeze is infallible"); - h.absorb(mu).expect("absorb before squeeze is infallible"); + h.do_update(&K); + h.do_update(&rnd); + h.do_update(mu); let mut rho_p_p = [0u8; 64]; - h.squeeze_out(&mut rho_p_p); + let mut h = h.into_squeezer(); + h.do_output_out(&mut rho_p_p); rho_p_p }; @@ -1344,9 +1350,10 @@ impl< // 15: 𝑐_tilde ← H(𝜇||w1Encode(𝐰1), 𝜆/4) // ▷ commitment hash let mut hash = H::new(); - hash.absorb(mu).expect("absorb before squeeze is infallible"); + hash.do_update(mu); w1.w1_encode_and_hash::

(&mut hash); - hash.squeeze_out(sig_val_c_tilde.as_mut()); + let mut hash = hash.into_squeezer(); + hash.do_output_out(sig_val_c_tilde.as_mut()); } // Alg 7; 16: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) @@ -1974,14 +1981,14 @@ impl MuBuilder { // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) let mut mb = Self { h: H::new() }; - mb.h.absorb(tr).expect("absorb before squeeze is infallible"); + mb.h.do_update(tr); // Algorithm 2 // 10: 𝑀′ ← BytesToBits(IntegerToBytes(0, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥) ∥ 𝑀 // all done together - mb.h.absorb(&[0u8]).expect("absorb before squeeze is infallible"); - mb.h.absorb(&[ctx.len() as u8]).expect("absorb before squeeze is infallible"); - mb.h.absorb(ctx).expect("absorb before squeeze is infallible"); + mb.h.do_update(&[0u8]); + mb.h.do_update(&[ctx.len() as u8]); + mb.h.do_update(ctx); // now ready to absorb M Ok(mb) @@ -1989,16 +1996,16 @@ impl MuBuilder { /// Stream a chunk of the message. pub fn do_update(&mut self, msg_chunk: &[u8]) { - self.h.absorb(msg_chunk).expect("absorb before squeeze is infallible"); + self.h.do_update(msg_chunk); } /// Finalize and return the mu value. - pub fn do_final(mut self) -> [u8; 64] { + pub fn do_final(self) -> [u8; 64] { // Completion of // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) let mut mu = [0u8; 64]; - self.h.squeeze_out(&mut mu); + self.h.into_squeezer().do_output_out(&mut mu); mu } diff --git a/crypto/mldsa/src/mldsa_keys.rs b/crypto/mldsa/src/mldsa_keys.rs index ff448704..e89d857c 100644 --- a/crypto/mldsa/src/mldsa_keys.rs +++ b/crypto/mldsa/src/mldsa_keys.rs @@ -179,7 +179,7 @@ impl MLDSAPublicKeyTrait fn compute_tr(&self) -> [u8; 64] { let mut tr = [0u8; 64]; - H::new().hash_xof_out(&self.encode(), &mut tr); + H::new().xof_out(&self.encode(), &mut tr); tr } diff --git a/crypto/mldsa/tests/bc_test_data.rs b/crypto/mldsa/tests/bc_test_data.rs index 701ee1bd..3e3637b4 100644 --- a/crypto/mldsa/tests/bc_test_data.rs +++ b/crypto/mldsa/tests/bc_test_data.rs @@ -5,7 +5,7 @@ #![allow(dead_code)] use bouncycastle_core::errors::SignatureError; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_sha3::SHAKE256; #[cfg(test)] @@ -967,14 +967,14 @@ impl BustedMuBuilder { // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) let mut mb = Self { h: SHAKE256::new() }; - mb.h.absorb(tr).expect("absorb before squeeze is infallible"); + mb.h.do_update(tr); // Algorithm 2 // 10: 𝑀′ ← BytesToBits(IntegerToBytes(0, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥) ∥ 𝑀 // all done together - // mb.h.absorb(&[0u8]); // these are the busted lines -- bc-java just doesn't do these in the test code - // mb.h.absorb(&[ctx.len() as u8]); - // mb.h.absorb(ctx); + // mb.h.do_update(&[0u8]); // these are the busted lines -- bc-java just doesn't do these in the test code + // mb.h.do_update(&[ctx.len() as u8]); + // mb.h.do_update(ctx); // now ready to absorb M Ok(mb) @@ -982,16 +982,16 @@ impl BustedMuBuilder { /// Stream a chunk of the message. pub fn do_update(&mut self, msg_chunk: &[u8]) { - self.h.absorb(msg_chunk).expect("absorb before squeeze is infallible"); + self.h.do_update(msg_chunk); } /// Finalize and return the mu value. - pub fn do_final(mut self) -> [u8; 64] { + pub fn do_final(self) -> [u8; 64] { // Completion of // Algorithm 7 // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) let mut mu = [0u8; 64]; - self.h.squeeze_out(&mut mu); + self.h.into_squeezer().do_output_out(&mut mu); mu } diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index c9e744ba..ee403880 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -8,7 +8,7 @@ mod mldsa_tests { }; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - RNG, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, Signer, Suspendable, + Hash, RNG, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, Signer, Suspendable, }; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_core_test_framework::FixedSeedRNG; @@ -1053,7 +1053,6 @@ mod mldsa_tests { #[test] fn serializable_state_mubuilder_rejects_wrong_variant() { - use bouncycastle_core::traits::XOF; use bouncycastle_sha3::SHAKE128; // A MuBuilder is always backed by SHAKE256. A serialized SHAKE128 state has the same length @@ -1061,9 +1060,7 @@ mod mldsa_tests { // variant tag weren't checked -- SHAKE128 (tag 5) must be rejected by MuBuilder (SHAKE256, // tag 6). let mut shake128 = SHAKE128::new(); - shake128 - .absorb(b"Colorless green ideas sleep furiously") - .expect("absorb before squeeze is infallible"); + shake128.do_update(b"Colorless green ideas sleep furiously"); let serialized_128 = shake128.suspend(); match MuBuilder::from_suspended(serialized_128) { diff --git a/crypto/mlkem-lowmemory/src/aux_functions.rs b/crypto/mlkem-lowmemory/src/aux_functions.rs index 406ef47a..874e5ca9 100644 --- a/crypto/mlkem-lowmemory/src/aux_functions.rs +++ b/crypto/mlkem-lowmemory/src/aux_functions.rs @@ -2,7 +2,7 @@ use crate::mlkem::{N, q, q_inv}; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_sha3::{SHAKE128, SHAKE256}; /// Algorithm 5 ByteEncode_d(𝐹) @@ -83,8 +83,8 @@ pub(crate) fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // 1: ctx ← XOF.Init() // 2: ctx ← XOF.Absorb(ctx, 𝐵) ▷ input the given byte array into XOF let mut xof = SHAKE128::new(); - xof.absorb(rho).expect("absorb before squeeze is infallible"); - xof.absorb(nonce).expect("absorb before squeeze is infallible"); + xof.do_update(rho); + xof.do_update(nonce); // 3: 𝑗 ← 0 let mut j = 0usize; @@ -95,7 +95,8 @@ pub(crate) fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's likely around the average rejection rate, and 216 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut C = [0u8; 216]; - xof.squeeze_out(&mut C); + let mut xof = xof.into_squeezer(); + xof.do_output_out(&mut C); let mut idx: usize = 0; // 4: while 𝑗 < 256 do @@ -103,7 +104,7 @@ pub(crate) fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // 5: (ctx, 𝐶) ← XOF.Squeeze(ctx, 3) // ▷ get a fresh 3-byte array 𝐶 from XOF if idx == C.len() { - xof.squeeze_out(&mut C); + xof.do_output_out(&mut C); idx = 0; } @@ -200,11 +201,12 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { 2 => { let buf = { let mut xof = SHAKE256::new(); - xof.absorb(b).expect("absorb before squeeze is infallible"); - xof.absorb(&n.to_le_bytes()).expect("absorb before squeeze is infallible"); + xof.do_update(b); + xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 2 * 64]; - xof.squeeze_out(&mut buf); + let mut xof = xof.into_squeezer(); + xof.do_output_out(&mut buf); buf }; @@ -213,10 +215,11 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { 3 => { let buf = { let mut xof = SHAKE256::new(); - xof.absorb(b).expect("absorb before squeeze is infallible"); - xof.absorb(&n.to_le_bytes()).expect("absorb before squeeze is infallible"); + xof.do_update(b); + xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 3 * 64]; - xof.squeeze_out(&mut buf); + let mut xof = xof.into_squeezer(); + xof.do_output_out(&mut buf); buf }; diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index 857c3442..76888bf8 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -19,7 +19,7 @@ use bouncycastle_core::key_material::{ }; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, XOF, + Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; @@ -432,9 +432,10 @@ impl< K_bar = { let mut K_bar: Secret<[u8; MLKEM_SS_LEN]> = Secret::new(); let mut j = J::new(); - j.absorb(dk.z()).expect("absorb before squeeze is infallible"); - j.absorb(&c).expect("absorb before squeeze is infallible"); - let bytes_written = j.squeeze_out(&mut *K_bar); + j.do_update(dk.z()); + j.do_update(&c); + let mut j = j.into_squeezer(); + let bytes_written = j.do_output_out(&mut *K_bar); debug_assert_eq!(bytes_written, MLKEM_SS_LEN); K_bar diff --git a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs index 5ae7e44c..cbc2e2d4 100644 --- a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs @@ -7,7 +7,7 @@ mod mlkem_tests { }; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, XOF, + Hash, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, XOF, XOFSqueezer, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -435,12 +435,11 @@ mod mlkem_tests { // J is SHAKE256(𝑠, 8*32) let mut shake = SHAKE256::new(); - shake - .absorb(&seed.ref_to_bytes()[32..64]) - .expect("absorb before squeeze is infallible"); - shake.absorb(&busted_ciphertext).expect("absorb before squeeze is infallible"); + shake.do_update(&seed.ref_to_bytes()[32..64]); + shake.do_update(&busted_ciphertext); let mut buf = [0u8; 32]; - _ = shake.squeeze_out(&mut buf); + let mut shake = shake.into_squeezer(); + _ = shake.do_output_out(&mut buf); assert_eq!(ss.ref_to_bytes(), buf); } diff --git a/crypto/mlkem/src/aux_functions.rs b/crypto/mlkem/src/aux_functions.rs index 3dbb8683..18de14a5 100644 --- a/crypto/mlkem/src/aux_functions.rs +++ b/crypto/mlkem/src/aux_functions.rs @@ -4,7 +4,7 @@ use crate::matrix::{MatrixTrait, VectorTrait}; use crate::mlkem::{N, q, q_inv}; use crate::params::MLKEMParams; use crate::polynomial::Polynomial; -use bouncycastle_core::traits::XOF; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_sha3::{SHAKE128, SHAKE256}; pub(crate) fn expandA(rho: &[u8; 32]) -> P::MatrixA { @@ -92,8 +92,8 @@ pub fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // 1: ctx ← XOF.Init() // 2: ctx ← XOF.Absorb(ctx, 𝐵) ▷ input the given byte array into XOF let mut xof = SHAKE128::new(); - xof.absorb(rho).expect("absorb before squeeze is infallible"); - xof.absorb(nonce).expect("absorb before squeeze is infallible"); + xof.do_update(rho); + xof.do_update(nonce); // 3: 𝑗 ← 0 let mut j = 0usize; @@ -104,7 +104,8 @@ pub fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // It's probably around the average rejection rate, and 216 is a multiple of both 3 (required for this alg) // and 8 (efficient for SHAKE). let mut C = [0u8; 216]; - xof.squeeze_out(&mut C); + let mut xof = xof.into_squeezer(); + xof.do_output_out(&mut C); let mut idx: usize = 0; // 4: while 𝑗 < 256 do @@ -112,7 +113,7 @@ pub fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { // 5: (ctx, 𝐶) ← XOF.Squeeze(ctx, 3) // ▷ get a fresh 3-byte array 𝐶 from XOF if idx == C.len() { - xof.squeeze_out(&mut C); + xof.do_output_out(&mut C); idx = 0; } @@ -209,11 +210,12 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { 2 => { let buf = { let mut xof = SHAKE256::new(); - xof.absorb(b).expect("absorb before squeeze is infallible"); - xof.absorb(&n.to_le_bytes()).expect("absorb before squeeze is infallible"); + xof.do_update(b); + xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 2 * 64]; - xof.squeeze_out(&mut buf); + let mut xof = xof.into_squeezer(); + xof.do_output_out(&mut buf); buf }; @@ -222,10 +224,11 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { 3 => { let buf = { let mut xof = SHAKE256::new(); - xof.absorb(b).expect("absorb before squeeze is infallible"); - xof.absorb(&n.to_le_bytes()).expect("absorb before squeeze is infallible"); + xof.do_update(b); + xof.do_update(&n.to_le_bytes()); let mut buf = [0u8; 3 * 64]; - xof.squeeze_out(&mut buf); + let mut xof = xof.into_squeezer(); + xof.do_output_out(&mut buf); buf }; diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index c869a3e6..5504cc2b 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -151,7 +151,7 @@ use bouncycastle_core::key_material::{ }; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, XOF, + Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; @@ -636,10 +636,11 @@ impl< let K_bar: [u8; MLKEM_SS_LEN]; K_bar = { let mut j = J::new(); - j.absorb(dk.z().as_ref()).expect("absorb before squeeze is infallible"); - j.absorb(&c).expect("absorb before squeeze is infallible"); + j.do_update(dk.z().as_ref()); + j.do_update(&c); let mut buf = [0u8; MLKEM_SS_LEN]; - let bytes_written = j.squeeze_out(&mut buf); + let mut j = j.into_squeezer(); + let bytes_written = j.do_output_out(&mut buf); debug_assert_eq!(bytes_written, MLKEM_SS_LEN); buf diff --git a/crypto/mlkem/tests/mlkem_tests.rs b/crypto/mlkem/tests/mlkem_tests.rs index 731f981c..c228a2f4 100644 --- a/crypto/mlkem/tests/mlkem_tests.rs +++ b/crypto/mlkem/tests/mlkem_tests.rs @@ -6,7 +6,7 @@ mod mlkem_tests { use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, XOF, + Hash, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, XOF, XOFSqueezer, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; @@ -470,12 +470,11 @@ mod mlkem_tests { // J is SHAKE256(𝑠, 8*32) let mut shake = SHAKE256::new(); - shake - .absorb(&seed.ref_to_bytes()[32..64]) - .expect("absorb before squeeze is infallible"); - shake.absorb(&busted_ciphertext).expect("absorb before squeeze is infallible"); + shake.do_update(&seed.ref_to_bytes()[32..64]); + shake.do_update(&busted_ciphertext); let mut buf = [0u8; 32]; - _ = shake.squeeze_out(&mut buf); + let mut shake = shake.into_squeezer(); + _ = shake.do_output_out(&mut buf); assert_eq!(ss.ref_to_bytes(), buf); } diff --git a/crypto/modes/benches/modes_benches.rs b/crypto/modes/benches/modes_benches.rs index 970fe5af..fc8a31e5 100644 --- a/crypto/modes/benches/modes_benches.rs +++ b/crypto/modes/benches/modes_benches.rs @@ -42,10 +42,11 @@ use bouncycastle_core::errors::SymmetricCipherError; use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, + AEADCipherEncryptor, Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, StreamCipherDecryptor, StreamCipherEncryptor, }; -use bouncycastle_modes::{Cbc, Cfb, Cfb8, Ctr, Decrypting, Ecb, Encrypting}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_modes::{Cbc, Ccm, CcmEncryptor, Cfb, Cfb8, Ctr, Decrypting, Ecb, Encrypting}; use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main}; use std::hint::black_box; @@ -59,6 +60,22 @@ type Aes256Cbc

= Cbc; type Aes128Cfb = Cfb; type Aes256Cfb = Cfb; type Aes128Cfb8 = Cfb8; + +/// CCM at the parameters the ACVP vectors and most protocols use: a 12-byte nonce and a full +/// 16-byte tag. The direction is in the type as for the other modes, but the two directions are +/// separate aliases here rather than one generic over `Dir`, because CCM's one-shots live on the +/// direction-specific impl blocks. +const CCM_NONCE_LEN: usize = 12; +const CCM_TAG_LEN: usize = 16; +type Aes128CcmEnc = Ccm; +type Aes128CcmDec = Ccm; + +/// The trait adapter needs a compile-time maximum for streaming. Its one-shots bypass that buffer, +/// but using the same 4 KiB value and message keeps this comparison representative of the public +/// alias a packet protocol would choose. +const CCM_BUFFER_LEN: usize = 4096; +type Aes128CcmEncryptor = + CcmEncryptor; type Aes128Ctr = Ctr; type Aes256Ctr = Ctr; type Aes128Ecb = Ecb; @@ -762,8 +779,192 @@ fn bench_init(c: &mut Criterion) { group.finish(); } +/// CCM (SP 800-38C), which is the only authenticated mode here and the only one that costs +/// **two** cipher calls per block -- but only one of the two batches. +/// +/// Sec 5.2 builds CCM out of CTR for confidentiality and CBC-MAC for authenticity, over the same +/// key, so every payload block goes through the forward cipher twice: once as a counter block and +/// once as a CBC-MAC input. The CBC-MAC half is serial by construction (Sec 6.1 step 3: `Yi` is +/// the cipher of `Bi XOR Yi-1`), so unlike [`Ctr`] and the decrypt direction of `Cbc`/`Cfb` it has +/// no pair or four path -- but the CTR half has exactly `Ctr`'s parallelism (A.3's `Ctrj` depends +/// only on `j`), and `Ccm::apply_keystream` batches it the same way. So CCM sits *between* CTR's +/// two numbers, not at a fixed fraction of either: +/// +/// * against `modes::ctr::AES_128/16KiB encrypt -- N=1`, CTR's unbatched single-block path, CCM +/// should be noticeably better than half -- one full unbatched pass (the MAC) plus a batched +/// pass that costs much less than a second unbatched one would; +/// * against CTR's `N=8` batched path, CCM should be noticeably better than a quarter, for the +/// same reason: only the MAC half pays the unbatched price. +/// +/// Measured on the reference machine: 36 MiB/s for CCM against 52 MiB/s for CTR `N=1` (CCM at +/// ~69%, not ~50%) and 103 MiB/s for CTR `N=8` (CCM at ~35%, not ~25%) -- both above the naive +/// "two full unbatched passes" ratios, which is the batched CTR half showing up. +/// +/// Encryption and decryption should be within noise of each other: Sec 6.1 and Sec 6.2 do the same +/// work in the opposite order (MAC-then-XOR versus XOR-then-MAC), and only the forward cipher is +/// ever used, so the inverse cipher's cost never enters. +/// +/// The AAD is measured separately, and is the cheap half: it is absorbed into the CBC-MAC only, +/// one unbatched cipher call per block, against the payload's one unbatched call plus one batched +/// call. Batching the keystream narrows this gap from the naive "twice the payload's throughput" +/// to about **1.5x** -- measured 52 MiB/s AAD-only against 36 MiB/s for the payload -- and AAD-only +/// throughput should now sit close to CTR's *unbatched* number, since both are exactly one +/// unbatched cipher call per block. +fn bench_ccm_aes128(c: &mut Criterion) { + let key = key::<16>(); + let nonce = [0x24u8; CCM_NONCE_LEN]; + let data = [0xA5u8; DATA_LEN]; + let no_aad: [u8; 0] = []; + + let mut group = c.benchmark_group("modes::ccm::AES_128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("encrypt 16KiB, no AAD", |b| { + b.iter_batched_ref( + || [0u8; DATA_LEN], + |out| { + black_box( + Aes128CcmEnc::encrypt_detached( + black_box(&key), + &nonce, + &no_aad, + black_box(&data), + out, + ) + .unwrap(), + ) + }, + BatchSize::LargeInput, + ) + }); + + // Encrypt once outside the loop so decryption measures a ciphertext that authenticates: a + // failing tag check would short-circuit the comparison and measure the wrong thing. + let mut ciphertext = [0u8; DATA_LEN]; + let (_, tag) = + Aes128CcmEnc::encrypt_detached(&key, &nonce, &no_aad, &data, &mut ciphertext).unwrap(); + + group.bench_function("decrypt 16KiB, no AAD", |b| { + b.iter_batched_ref( + || [0u8; DATA_LEN], + |out| { + black_box( + Aes128CcmDec::decrypt_detached( + black_box(&key), + &nonce, + &no_aad, + black_box(&ciphertext), + &tag, + out, + ) + .unwrap(), + ) + }, + BatchSize::LargeInput, + ) + }); + + // The same payload with 16 KiB of AAD alongside it. The difference from the no-AAD case is one + // cipher call per AAD block, so this should cost about 1.5x the no-AAD case for 2x the bytes. + group.bench_function("encrypt 16KiB with 16KiB AAD", |b| { + b.iter_batched_ref( + || [0u8; DATA_LEN], + |out| { + black_box( + Aes128CcmEnc::encrypt_detached( + black_box(&key), + &nonce, + black_box(&data), + black_box(&data), + out, + ) + .unwrap(), + ) + }, + BatchSize::LargeInput, + ) + }); + + // AAD only: CCM as a pure authentication mode, which Sec 5.3's footnote calls out as the + // empty-payload degenerate case. One cipher call per block, so this is the CTR-comparable half. + group.bench_function("authenticate 16KiB AAD, empty payload", |b| { + b.iter(|| { + let mut out: [u8; 0] = []; + black_box( + Aes128CcmEnc::encrypt_detached( + black_box(&key), + &nonce, + black_box(&data), + &no_aad, + &mut out, + ) + .unwrap(), + ) + }) + }); + + group.finish(); +} + +/// The [`AEADCipherEncryptor`] one-shot against the inherent one-shot on the same message. +/// +/// The trait override ends in the same `Ccm` implementation. A cheap deterministic RNG, created +/// once outside the timed loop, isolates its nonce draw from OS entropy and DRBG construction. +fn bench_ccm_one_shot_pair(c: &mut Criterion) { + let key = key::<16>(); + let data = [0xA5u8; CCM_BUFFER_LEN]; + let no_aad: [u8; 0] = []; + let nonce = [0x24u8; CCM_NONCE_LEN]; + let mut rng = FixedSeedRNG::::new(nonce); + + let mut group = c.benchmark_group("modes::ccm::one_shot"); + group.throughput(Throughput::Bytes(CCM_BUFFER_LEN as u64)); + + group.bench_function("AEADCipherEncryptor::encrypt_out_rng_detached 4KiB", |b| { + b.iter_batched_ref( + || [0u8; CCM_BUFFER_LEN], + |out| { + black_box( + Aes128CcmEncryptor::encrypt_out_rng_detached( + black_box(&key), + &mut rng, + &no_aad, + black_box(&data), + out, + ) + .unwrap(), + ) + }, + BatchSize::LargeInput, + ) + }); + + // The same 4 KiB and nonce through `Ccm` directly, for the ratio. + group.bench_function("Ccm::encrypt_detached 4KiB", |b| { + b.iter_batched_ref( + || [0u8; CCM_BUFFER_LEN], + |out| { + black_box( + Aes128CcmEnc::encrypt_detached( + black_box(&key), + &nonce, + &no_aad, + black_box(&data), + out, + ) + .unwrap(), + ) + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + criterion_group!( benches, bench_aes128, bench_aes256, bench_cfb_aes128, bench_cfb_aes256, bench_cfb8_aes128, - bench_ctr_aes128, bench_ctr_aes256, bench_ecb_aes128, bench_init + bench_ctr_aes128, bench_ctr_aes256, bench_ecb_aes128, bench_ccm_aes128, + bench_ccm_one_shot_pair, bench_init ); criterion_main!(benches); diff --git a/crypto/modes/src/ccm.rs b/crypto/modes/src/ccm.rs new file mode 100644 index 00000000..5732959a --- /dev/null +++ b/crypto/modes/src/ccm.rs @@ -0,0 +1,1898 @@ +//! The CCM mode of operation: Counter with Cipher Block Chaining-Message Authentication Code +//! (NIST SP 800-38C, May 2004, errata update 07-20-2007). +//! +//! CCM is the one mode in this crate that is *authenticated*: it produces a tag as well as a +//! ciphertext, and decryption either returns the plaintext or refuses. It is built from two +//! mechanisms this crate already has, under a single key (Sec 5.2: "The same key, K, is used for +//! both the CTR and CBC-MAC mechanisms within CCM"): +//! +//! * **CTR** for confidentiality, over the counter blocks of Appendix A.3; +//! * **CBC-MAC** for authenticity, over the formatted blocks of Appendix A.2. +//! +//! Only the forward cipher function is ever used, in both directions (Sec 3: "Only the forward +//! cipher function of the block cipher algorithm is used within these primitives"), so a +//! permutation that implements nothing but `encrypt_block` works here. +//! +//! # The specification +//! +//! Sec 6.1, the generation-encryption process, quoted verbatim: +//! +//! ```text +//! 1. Apply the formatting function to (N, A, P) to produce the blocks B0, B1, ..., Br. +//! 2. Set Y0 = CIPH_K(B0). +//! 3. For i = 1 to r, do Yi = CIPH_K(Bi XOR Yi-1). +//! 4. Set T = MSB_Tlen(Yr). +//! 5. Apply the counter generation function to generate the counter blocks Ctr0, Ctr1, +//! ..., Ctrm, where m = ceil(Plen/128). +//! 6. For j = 0 to m, do Sj = CIPH_K(Ctrj). +//! 7. Set S = S1 || S2 || ... || Sm. +//! 8. Return C = (P XOR MSB_Plen(S)) || (T XOR MSB_Tlen(S0)). +//! ``` +//! +//! Sec 6.2, the decryption-verification process, quoted verbatim: +//! +//! ```text +//! 1. If Clen <= Tlen, then return INVALID. +//! 2. Apply the counter generation function to generate the counter blocks Ctr0, Ctr1, +//! ..., Ctrm, where m = ceil((Clen - Tlen)/128). +//! 3. For j = 0 to m, do Sj = CIPH_K(Ctrj). +//! 4. Set S = S1 || S2 || ... || Sm. +//! 5. Set P = MSB_Clen-Tlen(C) XOR MSB_Clen-Tlen(S). +//! 6. Set T = LSB_Tlen(C) XOR MSB_Tlen(S0). +//! 7. If N, A, or P is not valid, as discussed in Section 5.4, then return INVALID, else +//! apply the formatting function to (N, A, P) to produce the blocks B0, B1, ..., Br. +//! 8. Set Y0 = CIPH_K(B0). +//! 9. For i = 1 to r, do Yj = CIPH_K(Bi XOR Yi-1). +//! 10. If T != MSB_Tlen(Yr), then return INVALID, else return P. +//! ``` +//! +//! Note step 8's `T XOR MSB_Tlen(S0)`: the tag CCM transmits is the CBC-MAC value **encrypted** +//! under the counter block `Ctr0`, which is reserved for exactly that and never used for payload +//! keystream -- step 7 starts the payload at `S1`. +//! +//! ## Where the ciphertext ends and the tag begins +//! +//! Step 8 returns a single string, `ciphertext || tag`. This type offers both layouts: the inherent +//! [`Ccm::encrypt`] / [`Ccm::decrypt`] produce and consume the spec's own inline string, and the +//! detached pair [`Ccm::encrypt_detached`] / [`Ccm::decrypt_detached`] keeps the tag separate, +//! which is the shape [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] use. +//! +//! # Formatting: the parameters are the const generics +//! +//! Appendix A gives "an example of a formatting function and counter generation function"; Sec 5.4 +//! permits others, but A's is the one every deployment of CCM uses -- it is what makes this +//! "essentially equivalent to the specification of CCM in the draft amendment to the IEEE Standard +//! 802.11" (Appendix A) -- and it is the only one implemented here. Its length conditions (A.1), +//! quoted verbatim: +//! +//! ```text +//! * t is an element of {4, 6, 8, 10, 12, 14, 16}; +//! * q is an element of {2, 3, 4, 5, 6, 7, 8}; +//! * n is an element of {7, 8, 9, 10, 11, 12, 13} +//! * n+q=15; +//! * a<2^64. +//! ``` +//! +//! `t` is `TAG_LEN` and `n` is `NONCE_LEN`, so **`q` is not a parameter**: `n + q = 15` fixes it at +//! `15 - NONCE_LEN`, and A.1 says as much ("a choice for q determines the value of n, namely, +//! n=15-q"). All four of the first conditions are therefore properties of the const parameters and +//! are `const` assertions in the constructor: a `NONCE_LEN` or `TAG_LEN` A.1 does not permit is a +//! **compile** error at the call site, not a runtime `Err`. The fifth, `a < 2^64`, cannot be +//! violated by a `&[u8]` whose length is a `usize`, so there is nothing to check. +//! +//! ## `q` trades nonce space against payload size +//! +//! Because `n + q = 15`, a longer nonce means a shorter length field, and `q` bounds the payload: +//! A.1's "by definition, p<2^8q". A.1 calls this "a tradeoff between the maximum number of +//! invocations of CCM under a given key and the maximum payload length for those invocations": +//! +//! | `NONCE_LEN` (n) | q | max payload | +//! |---|---|---| +//! | 7 | 8 | 2^64 - 1 bytes (no bound in practice) | +//! | 11 | 4 | 4 GiB - 1 | +//! | 12 | 3 | 16 MiB - 1 | +//! | 13 | 2 | 64 KiB - 1 | +//! +//! A payload past that limit is refused with [`SymmetricCipherError::GenericError`]: both the +//! counter and the length field `Q` would overflow, and `Q` is what the MAC commits to. +//! +//! # CCM is not a streaming mode, and what this crate does about it +//! +//! Sec 3 is explicit: +//! +//! > CCM is intended for use in a packet environment, i.e., when all of the data is available in +//! > storage before CCM is applied; CCM is not designed to support partial processing or stream +//! > processing. +//! +//! The reason is `B0`. Appendix A.2.1 puts `Q`, the payload's octet length, *inside the first block +//! the CBC-MAC absorbs*, so nothing at all can be authenticated until the total payload length is +//! known. [`Ctr`](crate::Ctr) and [`Cfb`](crate::Cfb) can hash as they go; CCM structurally cannot. +//! +//! There are exactly two honest ways to live with that, and this module provides both: +//! +//! 1. **Declare the length up front.** [`Ccm::new`] takes the whole AAD and the payload length, so +//! `B0` is formed at construction and everything after it streams with **no buffering at all**: +//! each byte is MACed and XORed as it arrives, and the payload may be any length up to the `q` +//! limit. This is the efficient path and the one the one-shots use. +//! 2. **Buffer streaming calls.** [`CcmEncryptor`] / [`CcmDecryptor`] implement +//! [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], and through them +//! [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`], whose `do_encrypt_init` is +//! handed a key and nothing else, so they have no length from which to form `B0`. Their +//! streaming methods accumulate the message in a fixed `FINAL_LEN`-byte array and do all the +//! work at finalization. Their one-shots already have both lengths and therefore use the first path +//! directly. +//! +//! A caller who reaches for CCM at all is in Sec 3's packet environment and knows the length, so +//! (1) is the one to use; (2) exists so that CCM composes with code written against the trait. +//! +//! # Security considerations +//! +//! **The nonce must never repeat under one key.** Sec 5.3: "any two distinct data pairs to be +//! protected by CCM during the lifetime of the key shall be assigned distinct nonces". A repeat is +//! worse here than in an unauthenticated mode: it reuses the CTR keystream, and Appendix B.1's +//! footnote describes the resulting forgery -- an attacker who can "induce the +//! decryption-verification process to reuse the nonce" can flip any chosen bit of the payload. The +//! nonce is *not* required to be random ("The nonce is not required to be random"), only unique, so +//! a counter is a valid and often better choice; every deterministic entry point here takes the +//! nonce from the caller, and the entry points that generate one draw it from the library's DRBG. +//! +//! **`TAG_LEN` is a security parameter.** Sec B.2: "a value of Tlen that is less than 64 shall not +//! be used without a careful analysis of the risks of accepting inauthentic data as authentic", and +//! it gives the bound `Tlen >= lg(MaxErrs / Risk)`. A `TAG_LEN` of 4 or 6 is permitted by A.1 and +//! accepted here: the spec's own Appendix C.1 and C.2 examples use `Tlen=32` and `Tlen=48`, i.e. +//! `t = 4` and `t = 6`, and constrained protocols do the same. Prefer 16. (Those two examples are +//! what exercises the short tags in this crate's tests; the ACVP set it also runs uses only 96- and +//! 128-bit tags.) +//! +//! **The key is for CCM only.** Sec 5.1: "The key shall be kept secret and shall only be used for +//! the CCM mode", and "The total number of invocations of the block cipher algorithm during the +//! lifetime of the key shall be limited to 2^61". +//! +//! **A failed tag check reveals nothing.** Sec 6.2: "the payload P and the MAC T shall not be +//! revealed", and an unauthorized party must not be able to distinguish a step 7 failure from a +//! step 10 failure, "for example, from the timing of the error message". Step 7 cannot fail here -- +//! the const parameters and the declared length make `N`, `A` and `P` valid by construction -- so +//! there is only one failure path, the constant-time comparison in [`Ccm::do_decrypt_final`]. The +//! one-shots zeroize the plaintext buffer before returning the error. The streaming API cannot; see +//! [`AEADCipherDecryptor`]'s own warning that what `do_update_out` released is not authenticated +//! until the final call returns `Ok`. + +use crate::iv::random_iv; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, ElectronicCodeBook, RNG, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; + +use crate::{Decrypting, Encrypting}; + +/// CCM (SP 800-38C) over any [`ElectronicCodeBook`] with a 128-bit block. +/// +/// `NONCE_LEN` is the spec's `n` and `TAG_LEN` its `t`; `q`, the width of the length field, is +/// `15 - NONCE_LEN`, because A.1 requires `n + q = 15`. See the module docs for the permitted +/// values -- all checked at compile time -- and for the payload limit `q` implies. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`], exactly as for the other modes in this crate: +/// `Ccm` has Sec 6.1's methods and nothing else, and `Ccm` +/// has Sec 6.2's. Using the wrong direction is a compile error rather than a runtime one, and there +/// is no state to police: pointing a decryptor at a plaintext is not a mistake this type can be +/// asked to make. +/// +/// [`CcmEncryptor`] and [`CcmDecryptor`] wrap these for the generic +/// [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] traits. Their streaming methods buffer; their +/// one-shots delegate directly to this type. See the module docs. +/// +/// Asking an encryptor to verify a tag does not compile -- `do_decrypt_final` exists only on +/// `Ccm`: +/// +/// ```compile_fail +/// use bouncycastle_aes::aes_internal::AES128Internal; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_modes::{Ccm, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let ccm = Ccm::::new(&key, &[0u8; 12], &[], 0).unwrap(); +/// ccm.do_decrypt_final(&[0u8; 16]).unwrap(); +/// ``` +/// +/// And nor does the reverse -- a decryptor has no `do_encrypt_final`, so it cannot be tricked into +/// producing a tag over data it never encrypted: +/// +/// ```compile_fail +/// use bouncycastle_aes::aes_internal::AES128Internal; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_modes::{Ccm, Decrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let ccm = Ccm::::new(&key, &[0u8; 12], &[], 0).unwrap(); +/// let _tag = ccm.do_encrypt_final().unwrap(); +/// ``` +/// +/// A nonce length A.1 does not permit does not compile: +/// +/// ```compile_fail +/// use bouncycastle_aes::aes_internal::AES128Internal; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_modes::{Ccm, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// // n = 6 is not in {7, ..., 13}: it would make q = 9, which A.1 does not allow. +/// let _ = Ccm::::new(&key, &[0u8; 6], &[], 0); +/// ``` +/// +/// Nor does an odd tag length: +/// +/// ```compile_fail +/// use bouncycastle_aes::aes_internal::AES128Internal; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_modes::{Ccm, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// // t = 15 is not in {4, 6, 8, 10, 12, 14, 16}. +/// let _ = Ccm::::new(&key, &[0u8; 12], &[], 0); +/// ``` +pub struct Ccm< + P, + Dir, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, +> where + P: ElectronicCodeBook, +{ + perm: P, + // The CBC-MAC chaining value: `Y0` once the constructor has absorbed `B0` (Sec 6.1 step 2), + // then `Yi` as further blocks arrive (step 3). Bytes are XORed into it in place, so part-way + // through a block it holds `Yi-1 XOR (the part of Bi seen so far)`. + // + // `Yr`'s low `TAG_LEN` bytes are the raw tag `T` before it is masked with `S0` (`finish_mac`), + // and every intermediate `Yi` is key-dependent CBC-MAC state, so this gets the same treatment + // as `ks` below rather than a plain array. + y: Secret<[u8; BLOCK_LEN]>, + // How many bytes of the current CBC-MAC input block have been XORed into `y`. + mac_pos: usize, + // `Ctr_i` with its counter field zeroed (A.3, Table 3): the flags octet and the nonce, which + // are the same in every counter block. Public data -- flags and nonce travel in the clear -- + // so deliberately not a `Secret`. + ctr_template: [u8; BLOCK_LEN], + // The current keystream block `Sj` and how much of it has been consumed. Live keystream for + // the payload bytes still to come, so it is zeroized on drop for the same reason `Ctr`'s is. + ks: Secret<[u8; BLOCK_LEN]>, + ks_pos: usize, + // The index `j` of the next keystream block. Starts at 1: step 7 sets `S = S1 || ... || Sm`, + // and `S0` is reserved for the tag. + next_ctr: u64, + // How much of the payload length declared to `new` has not yet been supplied. That length is + // committed to inside `B0`, so supplying a different amount would authenticate a message no + // verifier could reproduce; both directions refuse instead of doing it. + owed: usize, + // Which of the two Sec 6 processes this value runs. Zero-sized: the direction costs no memory. + _dir: PhantomData, +} + +impl< + P, + Dir, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, +> Ccm +where + P: ElectronicCodeBook, +{ + /// The spec's `q`: the octet length of the payload-length field `Q`. A.1 requires `n + q = 15`. + const Q_LEN: usize = 15 - NONCE_LEN; + + /// The largest payload this parameterization can carry, from A.1's "by definition, p<2^8q". + /// + /// `q = 8` would make `2^8q` exactly `2^64`, which does not fit a `u64`; there the bound is + /// `p <= 2^64 - 1`, i.e. `u64::MAX`, which is no bound at all on a `usize` length. Public so a + /// caller choosing a `FINAL_LEN` for [`CcmEncryptor`] / [`CcmDecryptor`], or reporting the + /// limit in an error message, has the real number instead of re-deriving it. + pub const MAX_PAYLOAD_LEN: u64 = + if Self::Q_LEN >= 8 { u64::MAX } else { (1u64 << (8 * Self::Q_LEN)) - 1 }; + + /// The compile-time shape check, from Appendix A.1 and Sec 5.1; run from the constructor. + /// + /// Every one of these is a property of the const parameters alone, so each is a compile error + /// at the call site. `q` is not checked separately: `NONCE_LEN` in `7..=13` with `q = 15 - n` + /// gives exactly A.1's `q` in `2..=8`. + #[inline] + fn check_shape() { + const { + // Sec 5.1: "For CCM, the block size of the block cipher algorithm shall be 128 bits". + assert!( + BLOCK_LEN == 16, + "CCM requires a 128-bit block cipher (SP 800-38C Sec 5.1): BLOCK_LEN must be 16" + ); + // A.1: "n is an element of {7, 8, 9, 10, 11, 12, 13}". + assert!( + NONCE_LEN >= 7 && NONCE_LEN <= 13, + "CCM nonce length must be 7..=13 bytes (SP 800-38C A.1)" + ); + // A.1: "t is an element of {4, 6, 8, 10, 12, 14, 16}", i.e. even and in 4..=16. Sec 5.4 + // gives the same lower bound from the other side: "No value of Tlen smaller than 32 + // shall be valid". + assert!( + TAG_LEN >= 4 && TAG_LEN <= 16 && TAG_LEN % 2 == 0, + "CCM tag length must be one of 4, 6, 8, 10, 12, 14, 16 bytes (SP 800-38C A.1)" + ); + }; + } + + /// Begins a CCM flow: formats `B0`, absorbs it and all of `A` into the CBC-MAC, and readies the + /// counter blocks. Everything after this streams without buffering. + /// + /// The whole AAD is taken here, and `payload_len` declared here, because Appendix A.2.1 puts the + /// payload length inside `B0` and A.2.2 puts the AAD length in front of the AAD: neither can be + /// encoded incrementally. See the module docs. + /// + /// * `key` must be a [`KeyType::SymmetricCipherKey`](bouncycastle_core::key_material::KeyType::SymmetricCipherKey) + /// of at least the permutation's strength. + /// * `nonce` **must not** repeat under `key`; see the module's security considerations. + /// * `aad` is authenticated but not encrypted, and may be empty. + /// * `payload_len` is the exact number of payload bytes that will follow. Supplying any other + /// amount is refused, at the update or at finalization. + /// + /// # Errors + /// [`SymmetricCipherError::KeyMaterialError`] for a key of the wrong type or strength, and + /// [`SymmetricCipherError::GenericError`] if `payload_len` exceeds A.1's `2^8q - 1`; see + /// [`Ccm`] for the table. + pub fn new( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + payload_len: usize, + ) -> Result { + // The shape check and the payload-limit check both belong to `from_perm`, which is the one + // path every construction goes through; duplicating them here would be two more `Err` + // sites that could drift apart from it. `P::new`'s own `KeyType`/strength checks are the + // only key validation needed, exactly as for every other mode in this crate. + let perm = P::new(key)?; + Self::from_perm(perm, nonce, aad, payload_len) + } + + /// As [`Self::new`], from a key schedule that has already been expanded and a payload length + /// that has already been checked against [`Self::MAX_PAYLOAD_LEN`]. + /// + /// This is what [`CcmEncryptor`] / [`CcmDecryptor`] call at finalization: they expand the key + /// once in their own constructor, long before they know the payload length, and hand the + /// schedule over here rather than storing the [`KeyMaterial`] and re-expanding it. + fn from_perm( + perm: P, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + payload_len: usize, + ) -> Result { + Self::check_shape(); + if payload_len as u64 > Self::MAX_PAYLOAD_LEN { + return Err(SymmetricCipherError::GenericError( + "CCM payload longer than 2^8q - 1, the limit the nonce length implies (A.1)", + )); + } + + // A.3, Tables 3 and 4: `Ctr_i` is `Flags || N || [i]_8q`, and its flags octet has both + // reserved bits and bits 3, 4 and 5 zero -- "to ensure that all the counter blocks are + // distinct from B0", whose bits 3..5 encode `t` and so cannot all be zero -- leaving bits + // 0..2 to hold "the same encoding of q as in B0". + let mut ctr_template = [0u8; BLOCK_LEN]; + ctr_template[0] = (Self::Q_LEN - 1) as u8; + ctr_template[1..1 + NONCE_LEN].copy_from_slice(nonce); + + let mut ccm = Self { + perm, + // Sec 6.1 step 2 is `Y0 = CIPH_K(B0)`, with no XOR, unlike step 3's `Bi XOR Yi-1`. + // Starting the chaining value at zero unifies the two: `B0 XOR 0 = B0`, so absorbing + // `B0` through the same path as every other block yields exactly `Y0`. + y: Secret::new(), + mac_pos: 0, + ctr_template, + ks: Secret::new(), + // Nothing buffered; the first payload byte forces a refill. + ks_pos: BLOCK_LEN, + next_ctr: 1, + owed: payload_len, + _dir: PhantomData, + }; + + ccm.mac_absorb(&Self::format_b0(nonce, !aad.is_empty(), payload_len as u64)); + + // A.2.2: if `a > 0`, "the encoding of a is concatenated with the associated data A, + // followed by the minimum number of '0' bits, possibly none, such that the resulting string + // can be partitioned into 16-octet blocks". If `a = 0` there are no AAD blocks at all, so + // nothing is absorbed and nothing is padded. + if !aad.is_empty() { + let (encoded, encoded_len) = Self::encode_aad_len(aad.len() as u64); + ccm.mac_absorb(&encoded[..encoded_len]); + ccm.mac_absorb(aad); + // The AAD's own blocks `B1 ... Bu` end on a block boundary, and A.2.3's payload blocks + // are `Bu+1 ...`. So the zero pad happens *here*, not once at the very end. + ccm.mac_pad(); + } + + Ok(ccm) + } + + /// The encoding of `a`, the AAD's octet length, which A.2.2 places in front of the AAD. + /// + /// Returns the bytes and how many of them are used; the buffer is sized for the longest case. + /// A.2.2 gives three, quoted verbatim: + /// + /// ```text + /// * If 0 < a < 2^16-2^8, then a is encoded as [a]_16, i.e., two octets. + /// * If 2^16-2^8 <= a < 2^32, then a is encoded as 0xff || 0xfe || [a]_32, i.e., six octets. + /// * If 2^32 <= a < 2^64, then a is encoded as 0xff || 0xff || [a]_64, i.e., ten octets. + /// ``` + /// + /// The first boundary is `2^16 - 2^8` (65280), **not** `2^16`: A.2.2 reserves the encodings + /// whose first octet is `0xff` so that the three cases can be told apart, and `[a]_16` for + /// `a >= 65280` would collide with them ("in the first case, the first octet will not be 0xff + /// as it will for the second and third cases"). Getting that bound wrong is the kind of error + /// that only shows up on a 64 KiB AAD, which is why this is a separate function with its own + /// tests rather than three inline branches: the third case's `2^32` boundary is not reachable + /// through the public API at all without a 4 GiB allocation, but it is trivially reachable here. + /// + /// `a` is a `usize` at every call site, so A.1's `a < 2^64` holds for free and there is nothing + /// to reject; the third case is reachable in practice only on a target with a >32-bit `usize`. + #[inline] + fn encode_aad_len(a: u64) -> ([u8; 10], usize) { + let mut out = [0u8; 10]; + if a < (1 << 16) - (1 << 8) { + out[..2].copy_from_slice(&(a as u16).to_be_bytes()); + (out, 2) + } else if a < (1u64 << 32) { + out[0] = 0xff; + out[1] = 0xfe; + out[2..6].copy_from_slice(&(a as u32).to_be_bytes()); + (out, 6) + } else { + out[0] = 0xff; + out[1] = 0xff; + out[2..10].copy_from_slice(&a.to_be_bytes()); + (out, 10) + } + } + + /// `B0`, the first block of the formatted input (A.2.1). + /// + /// Table 1 gives the flags octet: + /// + /// ```text + /// Bit number 7 6 5 4 3 2 1 0 + /// Contents Reserved Adata [(t-2)/2]_3 [q-1]_3 + /// ``` + /// + /// with the Reserved bit "reserved to enable future extensions of the formatting; it shall be + /// set to '0'", and A.2.2's rule for the other flag: "The Adata bit is '0' if a=0 and '1' if + /// a>0", which is what `has_aad` carries. Table 2 gives the rest: + /// + /// ```text + /// Octet number 0 1 ... 15-q 16-q ... 15 + /// Contents Flags N Q + /// ``` + /// + /// Neither three-bit field can be zero -- A.1 notes "the encoding 000 in both cases does not + /// correspond to a permitted value of t or q" -- which is what [`Self::check_shape`] enforces + /// and what keeps `B0` distinct from every counter block (A.3). + #[inline] + fn format_b0(nonce: &[u8; NONCE_LEN], has_aad: bool, payload_len: u64) -> [u8; BLOCK_LEN] { + let mut b0 = [0u8; BLOCK_LEN]; + // The three fields occupy disjoint bit ranges -- bit 6, bits 5-3, bits 2-0 -- and + // `check_shape` bounds the two encoded values so neither can overflow its field. So these + // `|`s are exactly equivalent to `^`, and `cargo mutants` reports that substitution as a + // surviving mutant; it is one of the OR/XOR equivalences CLAUDE.md calls acceptable, not a + // gap in the tests. `|` is written because these are field assignments, not a combination. + b0[0] = (u8::from(has_aad) << 6) + | ((((TAG_LEN - 2) / 2) as u8) << 3) + | ((Self::Q_LEN - 1) as u8); + b0[1..1 + NONCE_LEN].copy_from_slice(nonce); + Self::put_q_field(&mut b0, payload_len); + b0 + } + + /// Writes `[x]_8q` into the trailing `Q_LEN` octets of `block`: the `Q` field of `B0` (A.2.1, + /// Table 2) and the counter field of `Ctr_i` (A.3, Table 3), which occupy the same octets. + /// + /// `Q_LEN <= 8`, so the low `Q_LEN` bytes of a big-endian `u64` are exactly `[x]_8q`. Nothing + /// is ever truncated in a way that matters: [`Self::new`] refuses a payload above + /// [`Self::MAX_PAYLOAD_LEN`], and the counter cannot pass that either, since there is one + /// counter block per `BLOCK_LEN` payload bytes. + #[inline] + fn put_q_field(block: &mut [u8; BLOCK_LEN], x: u64) { + let be = x.to_be_bytes(); + block[BLOCK_LEN - Self::Q_LEN..].copy_from_slice(&be[8 - Self::Q_LEN..]); + } + + /// Absorbs `data` into the CBC-MAC as the next bytes of the formatted block string. + /// + /// Implements Sec 6.1 steps 2 and 3 together, incrementally: bytes are XORed into `y` at + /// `mac_pos`, and each time a whole block has gone in, `CIPH_K` is applied. Since `y` holds + /// `Yi-1` when a block starts, XORing `Bi` in byte by byte and then enciphering is exactly + /// `Yi = CIPH_K(Bi XOR Yi-1)`, whatever chunking `data` arrives in. + #[inline] + fn mac_absorb(&mut self, data: &[u8]) { + let mut rest = data; + while !rest.is_empty() { + let take = core::cmp::min(BLOCK_LEN - self.mac_pos, rest.len()); + let (now, later) = rest.split_at(take); + for (slot, b) in self.y[self.mac_pos..].iter_mut().zip(now) { + *slot ^= *b; + } + self.mac_pos += take; + if self.mac_pos == BLOCK_LEN { + self.perm.encrypt_block(&mut self.y); + self.mac_pos = 0; + } + rest = later; + } + } + + /// Finishes a partly-filled CBC-MAC block by zero-padding it: A.2.2 for the AAD and A.2.3 for + /// the payload, both "concatenated with the minimum number of '0' bits, possibly none". + /// + /// The pad itself is free. [`Self::mac_absorb`] XORs into `y`, and XORing zero changes nothing, + /// so all that is left to do is apply `CIPH_K` to the block already sitting there. "Possibly + /// none" is the `mac_pos == 0` case, where the string already ends on a block boundary and + /// adding a whole block of zeros would be wrong. + #[inline] + fn mac_pad(&mut self) { + if self.mac_pos != 0 { + self.perm.encrypt_block(&mut self.y); + self.mac_pos = 0; + } + } + + /// Builds `Ctrj` (A.3, Table 3) for counter index `j`, without encrypting it. + #[inline] + fn counter_block(&self, j: u64) -> [u8; BLOCK_LEN] { + let mut ctr = self.ctr_template; + Self::put_q_field(&mut ctr, j); + ctr + } + + /// Generates the next keystream block, `Sj = CIPH_K(Ctrj)` for the current `j` (Sec 6.1 + /// steps 5-6), and advances `j`. + #[inline] + fn refill_keystream(&mut self) { + *self.ks = self.counter_block(self.next_ctr); + self.perm.encrypt_block(&mut self.ks); + self.next_ctr += 1; + self.ks_pos = 0; + } + + /// XORs `data` (shorter than a block, or finishing/opening one) with the open keystream block, + /// refilling one block at a time as needed. Used for the bytes before and after the batched + /// whole-block run in [`Self::apply_keystream`]. + #[inline] + fn apply_keystream_bytes(&mut self, data: &mut [u8]) { + for byte in data.iter_mut() { + if self.ks_pos == BLOCK_LEN { + self.refill_keystream(); + } + *byte ^= self.ks[self.ks_pos]; + self.ks_pos += 1; + } + } + + /// XORs `N` whole blocks against `N` counter blocks encrypted in one batched call. + /// + /// `Ctrj` (A.3) depends only on `j`, not on the plaintext/ciphertext or on any other counter + /// block's cipher output, so the `N` forward ciphers here are independent -- the same + /// parallelism [`crate::Ctr`] uses, and unrelated to the CBC-MAC, which stays byte-at-a-time + /// serial (Sec 6.1 step 3: `Yi` depends on `Yi-1`) in [`Self::mac_absorb`]. Only the counter + /// half batches; nothing here changes what the MAC absorbs or when. + /// + /// `ks` is the caller's scratch for the `N` blocks of `Sj`: [`Self::apply_keystream`] holds it + /// in a [`Secret`] for the whole call, so the batched keystream gets the same drop-time scrub + /// as the single-block `ks` in `self` without a fresh allocation and scrub per batch. The same + /// arrangement as [`crate::Ctr`]'s. + #[inline] + fn apply_keystream_batch( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]; N], + ks: &mut [[u8; BLOCK_LEN]; N], + batch: impl Fn(&P, &mut [[u8; BLOCK_LEN]; N]), + ) { + for slot in ks.iter_mut() { + *slot = self.counter_block(self.next_ctr); + self.next_ctr += 1; + } + batch(&self.perm, ks); + for (block, k) in blocks.iter_mut().zip(ks.iter()) { + for (b, k) in block.iter_mut().zip(k.iter()) { + *b ^= *k; + } + } + } + + /// XORs `data` in place with the next `data.len()` bytes of `S1 || S2 || ...`. + /// + /// This is step 8's `P XOR MSB_Plen(S)` and Sec 6.2 step 5's `MSB(C) XOR MSB(S)` -- the same + /// operation, which is why one function serves both directions. A call may start and end + /// part-way through a keystream block, so the caller's chunking is invisible in the output, and + /// only the tail of the very last block is ever discarded. + /// + /// Splits into the bytes that finish an already-open keystream block, the whole blocks that + /// follow, and the short tail, exactly as [`crate::Ctr::apply`] does; the middle goes through + /// the batch paths, only the two ends go byte by byte. + #[inline] + fn apply_keystream(&mut self, data: &mut [u8]) { + // `ks_pos` never exceeds `BLOCK_LEN` (it is reset to 0 on refill and only ever + // incremented up to it), so at the one point `<` and `<=` disagree -- `ks_pos == + // BLOCK_LEN` -- both give `head_len = 0`: the `if` arm's `BLOCK_LEN - BLOCK_LEN` matches + // the `else` arm exactly. `cargo mutants` reports `<` to `<=` as a surviving mutant; it + // is provably equivalent, not a gap, for the same reason `format_b0`'s `|`/`^` ones are. + let head_len = if self.ks_pos < BLOCK_LEN { BLOCK_LEN - self.ks_pos } else { 0 }; + let (head, rest) = data.split_at_mut(core::cmp::min(head_len, data.len())); + self.apply_keystream_bytes(head); + + // Scratch for the batched keystream, one per width and per call rather than per batch: + // held in a `Secret` so it is zeroized when this call returns, like `ks` in `self`. + let (blocks, tail) = rest.as_chunks_mut::(); + let (fours, rest_blocks) = blocks.as_chunks_mut::<4>(); + let mut ks4: Secret<[[u8; BLOCK_LEN]; 4]> = Secret::new(); + for four in fours.iter_mut() { + self.apply_keystream_batch(four, &mut ks4, P::encrypt_4blocks); + } + let (pairs, single) = rest_blocks.as_chunks_mut::<2>(); + let mut ks2: Secret<[[u8; BLOCK_LEN]; 2]> = Secret::new(); + for pair in pairs.iter_mut() { + self.apply_keystream_batch(pair, &mut ks2, P::encrypt_2blocks); + } + for block in single.iter_mut() { + self.apply_keystream_bytes(block); + } + + self.apply_keystream_bytes(tail); + } + + /// Debits `len` bytes from the payload length declared to [`Self::new`]. + #[inline] + fn take_owed(&mut self, len: usize) -> Result<(), SymmetricCipherError> { + if len > self.owed { + return Err(SymmetricCipherError::StateError( + "CCM was given more payload than the length declared to `new`, which B0 commits to", + )); + } + self.owed -= len; + Ok(()) + } + + /// Completes the CBC-MAC and returns the transmitted tag: step 4's `T = MSB_Tlen(Yr)`, + /// encrypted as step 8's `T XOR MSB_Tlen(S0)`. + /// + /// `S0 = CIPH_K(Ctr0)` is computed here rather than at construction because `Ctr0` is used + /// exactly once, at the end; the payload keystream starts at `S1` (step 7). + fn finish_mac(mut self) -> [u8; TAG_LEN] { + // A.2.3: the payload's own blocks are zero-padded to a block boundary. + self.mac_pad(); + + // A keystream block of exactly the kind `ks` holds, so it gets the same `Secret` treatment + // rather than a plain local that outlives this function's stack frame unzeroed. + let mut s0: Secret<[u8; BLOCK_LEN]> = Secret::new(); + *s0 = self.ctr_template; + Self::put_q_field(&mut s0, 0); + self.perm.encrypt_block(&mut s0); + + // `MSB_Tlen` of a byte-aligned value is its first `TAG_LEN` bytes; A.1 makes `t` an octet + // count, so `Tlen` is always a multiple of 8 here. + let mut tag = [0u8; TAG_LEN]; + for (t, (y, s)) in tag.iter_mut().zip(self.y.iter().zip(s0.iter())) { + *t = *y ^ *s; + } + tag + } +} + +/// Sec 6.1, the generation-encryption process. Present only on the encrypting direction, so a +/// decryptor cannot be asked to produce a tag. +impl + Ccm +where + P: ElectronicCodeBook, +{ + /// Encrypts `data` in place and authenticates it. + /// + /// Step 8 XORs the *plaintext* with the keystream, and step 1 formats the *plaintext* into the + /// blocks the MAC covers, so the plaintext is absorbed before it is overwritten. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if `data` would take the total past the declared + /// payload length. + pub fn do_encrypt_update(&mut self, data: &mut [u8]) -> Result<(), SymmetricCipherError> { + self.take_owed(data.len())?; + self.mac_absorb(data); + self.apply_keystream(data); + Ok(()) + } + + /// Finishes an encryption and returns the tag (Sec 6.1 steps 4 and 8). + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if less payload was supplied than the length declared + /// to [`Self::new`] -- `B0` commits to that length, so a short message would produce a tag no + /// verifier could reproduce. + pub fn do_encrypt_final(self) -> Result<[u8; TAG_LEN], SymmetricCipherError> { + if self.owed != 0 { + return Err(SymmetricCipherError::StateError( + "CCM was given less payload than the length declared to `new`, which B0 commits to", + )); + } + Ok(self.finish_mac()) + } + + /// One-shot generation-encryption with a **detached** tag (Sec 6.1). + /// + /// Writes `plaintext.len()` bytes of ciphertext into `ciphertext` and returns that count with + /// the tag. For the spec's own inline `ciphertext || tag` string, use [`Self::encrypt`]. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, plus + /// [`Self::new`]'s errors. + pub fn encrypt_detached( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); + } + let mut ccm = Self::new(key, nonce, aad, plaintext.len())?; + let out = &mut ciphertext[..plaintext.len()]; + out.copy_from_slice(plaintext); + ccm.do_encrypt_update(out)?; + let tag = ccm.do_encrypt_final()?; + Ok((plaintext.len(), tag)) + } + + /// One-shot generation-encryption producing the spec's own output string (Sec 6.1 step 8): + /// `C = (P XOR MSB_Plen(S)) || (T XOR MSB_Tlen(S0))`, i.e. `ciphertext || tag` inline. + /// + /// `ciphertext` needs `plaintext.len() + TAG_LEN` bytes; the return is how many were written. + /// + /// # Errors + /// As [`Self::encrypt_detached`]. + pub fn encrypt( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + let needed = plaintext.len() + TAG_LEN; + if ciphertext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let (data, tag_out) = ciphertext[..needed].split_at_mut(plaintext.len()); + let (_, tag) = Self::encrypt_detached(key, nonce, aad, plaintext, data)?; + tag_out.copy_from_slice(&tag); + Ok(needed) + } +} + +/// Sec 6.2, the decryption-verification process. Present only on the decrypting direction, so an +/// encryptor cannot be asked to verify a tag. +impl + Ccm +where + P: ElectronicCodeBook, +{ + /// Decrypts `data` in place and authenticates the recovered plaintext. + /// + /// The mirror of [`Self::do_encrypt_update`] with the two steps swapped: Sec 6.2 recovers `P` in + /// step 5 and only then formats `(N, A, P)` in step 7, so the MAC is fed the plaintext here too, + /// never the ciphertext. + /// + /// The bytes this writes are **not authenticated** until [`Self::do_decrypt_final`] returns + /// `Ok`. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if `data` would take the total past the declared + /// payload length. + pub fn do_decrypt_update(&mut self, data: &mut [u8]) -> Result<(), SymmetricCipherError> { + self.take_owed(data.len())?; + self.apply_keystream(data); + self.mac_absorb(data); + Ok(()) + } + + /// Finishes a decryption by checking `tag`: Sec 6.2 step 10, "If T != MSB_Tlen(Yr), then return + /// INVALID, else return P". + /// + /// The comparison is [`ct_eq_bytes`], so it does not leak how much of the tag matched. Sec 6.2 + /// also requires that a caller cannot tell step 7's failure from step 10's; step 7 cannot fail + /// here, so there is nothing to distinguish -- see the module's security considerations. + /// + /// # Errors + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify, and + /// [`SymmetricCipherError::StateError`] if less ciphertext was supplied than the length declared + /// to [`Self::new`]. + pub fn do_decrypt_final(self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + if self.owed != 0 { + return Err(SymmetricCipherError::StateError( + "CCM was given less ciphertext than the length declared to `new`, which B0 commits to", + )); + } + if ct_eq_bytes(&self.finish_mac(), tag) { + Ok(()) + } else { + Err(SymmetricCipherError::AEADTagCheckFailed) + } + } + + /// One-shot decryption-verification with a **detached** tag (Sec 6.2). + /// + /// On failure `plaintext` is zeroized before the error is returned, so Sec 6.2's "the payload P + /// and the MAC T shall not be revealed" holds even for a caller who ignores the `Result`. + /// + /// # Errors + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify, + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short, plus + /// [`Self::new`]'s errors. + pub fn decrypt_detached( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + plaintext: &mut [u8], + ) -> Result { + if plaintext.len() < ciphertext.len() { + return Err(SymmetricCipherError::OutputBufferTooSmall(ciphertext.len())); + } + let mut ccm = Self::new(key, nonce, aad, ciphertext.len())?; + let out = &mut plaintext[..ciphertext.len()]; + out.copy_from_slice(ciphertext); + ccm.do_decrypt_update(out)?; + match ccm.do_decrypt_final(tag) { + Ok(()) => Ok(ciphertext.len()), + Err(e) => { + // Sec 6.2: on INVALID the payload "shall not be revealed". A plain `fill` because + // this crate is `#![forbid(unsafe_code)]`; the store is to the caller's own buffer, + // which the caller may read after this returns, so it is not a dead store the + // optimizer is entitled to drop. + out.fill(0); + Err(e) + } + } + } + + /// One-shot decryption-verification of the spec's own output string (Sec 6.2), splitting the + /// trailing `TAG_LEN` bytes off `ciphertext` as the tag -- step 6's `LSB_Tlen(C)`. + /// + /// # Errors + /// [`SymmetricCipherError::DecryptionFailed`] for Sec 6.2 step 1, "If Clen <= Tlen, then + /// return INVALID": a malformed input rather than a failed check, reported with the variant + /// [`SymmetricCipherDecryptor::do_final`] specifies for a malformed ciphertext so that every + /// inline entry point -- this one, [`CcmDecryptor::do_final`] and + /// [`CcmDecryptor::decrypt_out_with_aad`](AEADCipherDecryptor::decrypt_out_with_aad) -- agrees + /// on the same input. Otherwise as [`Self::decrypt_detached`]. + pub fn decrypt( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + // Sec 6.2 step 1, "If Clen <= Tlen, then return INVALID", and the split of step 6's + // `LSB_Tlen(C)` off the end, in one operation: `split_last_chunk` is `None` exactly when + // the string is too short to contain a tag, and otherwise hands back the tag already typed + // as `&[u8; TAG_LEN]`. Doing it in two steps would leave an arithmetic split followed by an + // array conversion that cannot fail but still has to be handled. + // + // Note the spec's `Clen <= Tlen` is on the *bit* lengths of a string that also carries the + // payload; a `C` of exactly `TAG_LEN` octets is an empty payload plus its tag, which is + // valid -- Sec 5.3's footnote, "The payload may also be empty". So the octet test here + // admits equality, which is what `split_last_chunk` does. + let Some((data, tag)) = ciphertext.split_last_chunk::() else { + return Err(SymmetricCipherError::DecryptionFailed); + }; + Self::decrypt_detached(key, nonce, aad, data, tag, plaintext) + } +} + +impl< + P, + Dir, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, +> Algorithm for Ccm +where + P: ElectronicCodeBook, +{ + /// The underlying permutation's name. The mode is not appended: `&'static str`s cannot be + /// concatenated in a `const`, and the mode is already in the type. + const ALG_NAME: &'static str = P::ALG_NAME; + /// A mode does not change the strength of the underlying cipher. + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +/// Shared buffering state for [`CcmEncryptor`] / [`CcmDecryptor`]: everything Sec 6 needs before +/// it can run, factored out once because the two adapters need it in the identical shape (see +/// [`CcmEncryptor`] for why buffering is here at all). The direction-specific parts -- what the +/// buffered bytes are called, how many of them there may be, and which `Ccm` process finalization +/// runs -- stay on the two newtypes that wrap this. +/// +/// Both arrays are `FINAL_LEN` long, the adapters' one size parameter. The AAD may use +/// `FINAL_LEN - TAG_LEN` of its array, as may the encryptor's payload; the decryptor may fill all +/// of `data`, since with the tag inline the last `TAG_LEN` bytes it buffers are the tag. +struct CcmBuffer< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> where + P: ElectronicCodeBook, +{ + // The key schedule, expanded once here and handed to `Ccm::from_perm` at finalization, so no + // second copy of the key material is kept. + perm: P, + nonce: [u8; NONCE_LEN], + // Associated data is authenticated but not encrypted, and travels in the clear, so it is not + // secret and is not wrapped. + aad: [u8; FINAL_LEN], + aad_len: usize, + // Plaintext for the encryptor, ciphertext (and possibly the inline tag) for the decryptor; + // either way held until finalization, so wrapped so it is zeroized on drop. + data: Secret<[u8; FINAL_LEN]>, + data_len: usize, + // Set by the first non-empty `do_update_out`, which closes the AAD phase (see + // `do_update_aad`). + data_started: bool, +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> CcmBuffer +where + P: ElectronicCodeBook, +{ + /// The largest payload -- and the largest AAD -- the streaming methods accept: what is left of + /// `FINAL_LEN` once the inline tag has room. + const CAPACITY: usize = FINAL_LEN - TAG_LEN; + + /// The compile-time nonce-length floor for the trait adapters, run from every entry point of + /// both [`CcmEncryptor`] and [`CcmDecryptor`], one-shots included. + /// + /// The encrypting side draws its nonce at random, and the random-collision bound is only + /// useful from 96 bits up. The decrypting side is given its nonce, so it has no such need of + /// its own; it carries the same floor so that the pair stays symmetric -- a `NONCE_LEN` for + /// which `CcmDecryptor` compiles but `CcmEncryptor` does not would be a trap for code written + /// against the generic traits, which instantiates both with one set of parameters. The + /// inherent [`Ccm`] API supports every A.1 length from 7 through 13 under a caller-managed + /// nonce. + #[inline] + fn check_random_nonce_len() { + const { + assert!( + NONCE_LEN >= 12, + "CCM: the random-nonce AEAD adapters require NONCE_LEN >= 12; use Ccm directly with a caller-managed unique nonce for shorter lengths" + ); + } + } + + fn new(perm: P, nonce: [u8; NONCE_LEN]) -> Self { + Self::check_random_nonce_len(); + const { + // `FINAL_LEN` has to hold the tag the inline `do_final` appends; without this, + // `CAPACITY` would underflow at compile time with a less helpful message. + assert!(FINAL_LEN >= TAG_LEN, "CCM: FINAL_LEN must be at least TAG_LEN"); + // Without this, a `FINAL_LEN` beyond what `NONCE_LEN` allows compiles fine and only + // fails at finalization, after the whole message has been buffered for nothing. + assert!( + (FINAL_LEN - TAG_LEN) as u64 + <= Ccm::::MAX_PAYLOAD_LEN, + "CCM: FINAL_LEN - TAG_LEN exceeds the payload limit 2^8q - 1 that NONCE_LEN implies (A.1)" + ); + }; + Self { + perm, + nonce, + aad: [0u8; FINAL_LEN], + aad_len: 0, + data: Secret::new(), + data_len: 0, + data_started: false, + } + } + + /// Buffers `aad`. A sequence of calls is equivalent to one call over the concatenation, which + /// is what A.2.2 needs: the AAD is length-prefixed, so it can only be encoded once all of it + /// is in hand. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] for a non-empty `aad` after the first + /// `do_update_out`, and [`SymmetricCipherError::GenericError`] if the total would exceed + /// `FINAL_LEN - TAG_LEN`. + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + if aad.is_empty() { + return Ok(()); + } + if self.data_started { + return Err(SymmetricCipherError::StateError("CCM: do_update_aad after do_update_out")); + } + let end = self.aad_len + aad.len(); + if end > Self::CAPACITY { + return Err(SymmetricCipherError::GenericError( + "CCM: associated data longer than FINAL_LEN - TAG_LEN", + )); + } + self.aad[self.aad_len..end].copy_from_slice(aad); + self.aad_len = end; + Ok(()) + } + + /// Buffers `data`, up to `limit` bytes in all, and writes nothing: nothing can be released + /// before the payload length is known, so the whole ciphertext or plaintext comes out at + /// finalization. + /// + /// An empty `data` is a no-op, and in particular does **not** close the AAD phase: the trait + /// makes an empty `aad` a no-op "at any point" so that a generic caller can pass one + /// unconditionally, and a caller looping over a reader that returns an empty first chunk + /// deserves the same on this side. Only a non-empty call is the start of the data phase. + /// + /// # Errors + /// [`SymmetricCipherError::GenericError`], carrying `too_long`, if the total would exceed + /// `limit`. Nothing is consumed in that case. The two callers have different limits -- the + /// encryptor's is [`Self::CAPACITY`], the decryptor's `FINAL_LEN` -- so each supplies the + /// message that names its own bound. + fn do_update_out( + &mut self, + data: &[u8], + limit: usize, + too_long: &'static str, + ) -> Result<(), SymmetricCipherError> { + if data.is_empty() { + return Ok(()); + } + // Set before the length check so that a refused oversized call still closes the AAD phase: + // the phase order is about call history, and this call happened. + self.data_started = true; + let end = self.data_len + data.len(); + if end > limit { + return Err(SymmetricCipherError::GenericError(too_long)); + } + self.data[self.data_len..end].copy_from_slice(data); + self.data_len = end; + Ok(()) + } +} + +/// Adapts [`Ccm`] to [`AEADCipherEncryptor`] and, through it, [`SymmetricCipherEncryptor`], +/// buffering only genuinely streaming use. +/// +/// [`SymmetricCipherEncryptor::do_encrypt_init`] is handed a key and nothing else, but CCM cannot +/// form `B0` -- and so cannot authenticate anything at all -- until it knows the total payload +/// length (Appendix A.2.1; see the module docs). This type therefore accumulates the AAD and the +/// payload in two `FINAL_LEN`-byte arrays and runs the whole of Sec 6.1 at finalization, so +/// [`update_out_len`](SymmetricCipherEncryptor::update_out_len) is identically `0` and every +/// ciphertext byte comes out of the final call. +/// +/// `FINAL_LEN` is the size of that final output with the tag inline: the whole ciphertext followed +/// by the `TAG_LEN`-byte tag. So the largest message -- and the largest AAD -- the streaming `do_*` +/// methods accept is `FINAL_LEN - TAG_LEN`, and anything longer is refused with +/// [`SymmetricCipherError::GenericError`]. Pick it from the largest packet the protocol allows +/// plus the tag -- CCM is a packet mode (Sec 3), so there is such a number. The one-shot methods +/// already have the complete lengths, so they bypass this buffer and accept data up to CCM's `q` +/// limit. +/// +/// **The AAD shares `FINAL_LEN`'s bound although it is never part of the output.** The AAD is +/// buffered in its own `FINAL_LEN`-byte array, and `FINAL_LEN - TAG_LEN` is its capacity too, so a +/// protocol whose authenticated header can be longer than its payload has to size `FINAL_LEN` for +/// the header: `FINAL_LEN >= max(largest payload, largest AAD) + TAG_LEN`. That is a property of +/// this adapter's single size parameter, not of CCM -- A.1 bounds `a` only at `2^64` -- and the +/// cost of oversizing is every `[u8; FINAL_LEN]` the trait puts on the stack, so a header-heavy +/// protocol is better served by the inherent [`Ccm`] API, which takes the whole AAD by reference +/// and buffers nothing. +/// +/// A `FINAL_LEN - TAG_LEN` past what `NONCE_LEN` allows (A.1's `2^8q - 1`) does not compile, +/// rather than buffering the whole message only to fail at finalization: +/// +/// ```compile_fail +/// use bouncycastle_aes::aes_internal::AES128Internal; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SymmetricCipherEncryptor; +/// use bouncycastle_modes::CcmEncryptor; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// // NONCE_LEN = 13 gives q = 2, a 65535-byte limit; FINAL_LEN - TAG_LEN = 99_992 exceeds it. +/// let _ = CcmEncryptor::::do_encrypt_init(&key); +/// ``` +/// +/// # Random nonce length +/// +/// The trait generates a random nonce rather than accepting a caller-managed counter. To keep the +/// random-collision bound useful, `NONCE_LEN` must therefore be at least 12 here, and +/// [`CcmDecryptor`] carries the same floor so that the pair stays symmetric. The inherent [`Ccm`] +/// API still supports every A.1 nonce length from 7 through 13 when the caller guarantees +/// uniqueness. +/// +/// ```compile_fail +/// use bouncycastle_aes::AES_CCM_128_Encryptor; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SymmetricCipherEncryptor; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// // A 7-byte nonce is valid for caller-managed Ccm, but too short for this random-nonce adapter. +/// let _ = AES_CCM_128_Encryptor::<7, 16, 2064>::do_encrypt_init(&key); +/// ``` +/// +/// The decryptor is given its nonce rather than drawing one, but refuses the same lengths, so a +/// parameter set that compiles for one side compiles for the other: +/// +/// ```compile_fail +/// use bouncycastle_aes::AES_CCM_128_Decryptor; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SymmetricCipherDecryptor; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .unwrap(); +/// let _ = AES_CCM_128_Decryptor::<7, 16, 2064>::do_decrypt_init(&key, &[0u8; 7]); +/// ``` +/// +/// See [`AEADCipherEncryptor`]'s "A length-dependent construction still has to buffer" section for +/// why this trait was not reshaped to avoid the buffering instead. +/// +/// # Memory +/// +/// A streaming value holds `2 * FINAL_LEN` bytes. The one-shots bypass that value and use the +/// fixed-size inherent [`Ccm`] state directly, so their stack use is independent of `FINAL_LEN`. +pub struct CcmEncryptor< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +>(CcmBuffer) +where + P: ElectronicCodeBook; + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> Algorithm for CcmEncryptor +where + P: ElectronicCodeBook, +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> CcmEncryptor +where + P: ElectronicCodeBook, +{ + /// Every one-shot comes here: they already have both lengths, so they skip the buffer and run + /// the inherent non-buffering [`Ccm::encrypt_detached`] under a freshly drawn nonce. + fn one_shot( + key: &KeyMaterial, + rng: &mut dyn RNG, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); + } + Ccm::::check_shape(); + CcmBuffer::::check_random_nonce_len(); + let nonce = random_iv::(rng)?; + let (written, tag) = + Ccm::::encrypt_detached( + key, &nonce, aad, plaintext, ciphertext, + )?; + Ok((nonce, written, tag)) + } + + /// [`Self::one_shot`] into the inline `ciphertext || tag` layout. + fn one_shot_inline( + key: &KeyMaterial, + rng: &mut dyn RNG, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + let needed = plaintext.len() + TAG_LEN; + if ciphertext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let (data, tag_out) = ciphertext[..needed].split_at_mut(plaintext.len()); + let (nonce, written, tag) = Self::one_shot(key, rng, aad, plaintext, data)?; + tag_out.copy_from_slice(&tag); + Ok((nonce, written + TAG_LEN)) + } +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> SymmetricCipherEncryptor + for CcmEncryptor +where + P: ElectronicCodeBook, +{ + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + Self::do_encrypt_init_rng(key, &mut rng) + } + + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; NONCE_LEN]), SymmetricCipherError> { + // The shape check belongs here too: this type never calls `Ccm::new`, and without it a + // `NONCE_LEN` or `TAG_LEN` A.1 forbids would not be caught until finalization. The + // random-nonce floor is `CcmBuffer::new`'s. + Ccm::::check_shape(); + // `P::new`'s own checks are the only key validation needed, exactly as for `Ccm` itself + // and every other mode in this crate; `random_iv` is CBC/CFB's same OS-backed draw -- + // Sec 5.3 asks only for uniqueness, not CBC/CFB's unpredictability, but a CSPRNG draw is + // the only way to be unique without state `do_encrypt_init` does not have. + let perm = P::new(key)?; + let nonce = random_iv::(rng)?; + Ok((Self(CcmBuffer::new(perm, nonce)), nonce)) + } + + /// Identically `0`: nothing can be released before the payload length is known, so the whole + /// ciphertext comes out of the final call. + fn update_out_len(&self, _input_len: usize) -> usize { + 0 + } + + /// Buffers `plaintext` and writes nothing, per [`Self::update_out_len`]. `ciphertext` is + /// untouched and may be empty. An empty `plaintext` is a no-op and leaves the AAD phase open. + /// + /// # Errors + /// [`SymmetricCipherError::GenericError`] if the total would exceed `FINAL_LEN - TAG_LEN`. + fn do_update_out( + &mut self, + plaintext: &[u8], + _ciphertext: &mut [u8], + ) -> Result { + self.0.do_update_out( + plaintext, + CcmBuffer::::CAPACITY, + "CCM: plaintext longer than FINAL_LEN - TAG_LEN, the streaming capacity", + )?; + Ok(0) + } + + /// Runs the whole of Sec 6.1 over the buffered message and returns the spec's own output + /// string, `ciphertext || tag` (step 8), with its length. + /// + /// # Errors + /// As [`AEADCipherEncryptor::do_final_out_detached`]. + fn do_final(self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError> { + let mut out = [0u8; FINAL_LEN]; + let (len, tag) = self.do_final_out_detached(&mut out)?; + // `do_update_out` held the payload to `FINAL_LEN - TAG_LEN`, so the tag fits after it. + out[len..len + TAG_LEN].copy_from_slice(&tag); + Ok((out, len + TAG_LEN)) + } + + /// The ciphertext, which is as long as the plaintext, followed by the tag. + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + TAG_LEN + } + + fn encrypt_out( + key: &KeyMaterial, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + Self::one_shot_inline(key, &mut rng, &[], plaintext, ciphertext) + } + + fn encrypt_out_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + Self::one_shot_inline(key, rng, &[], plaintext, ciphertext) + } +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> AEADCipherEncryptor + for CcmEncryptor +where + P: ElectronicCodeBook, +{ + /// Buffers `aad`. A sequence of calls is equivalent to one call over the concatenation, which + /// is what A.2.2 needs: the AAD is length-prefixed, so it can only be encoded once all of it + /// is in hand. + /// + /// # Errors + /// `SymmetricCipherError::StateError` for a non-empty `aad` after the first `do_update_out`, + /// and `SymmetricCipherError::GenericError` if the total would exceed `FINAL_LEN - TAG_LEN`. + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.0.do_update_aad(aad) + } + + /// Runs the whole of Sec 6.1 over the buffered message: writes the ciphertext to `ciphertext` + /// and returns its length with the tag. + /// + /// # Errors + /// None, in practice: the `const` assertion in construction already guarantees + /// `FINAL_LEN - TAG_LEN <= `[`Ccm::MAX_PAYLOAD_LEN`], the only thing [`Ccm::new`]'s equivalent + /// construction path can fail on, and `do_update_out` already guarantees the AAD and payload + /// it buffered are each no more than that. The `Result` return exists to satisfy the trait's + /// signature. + fn do_final_out_detached( + mut self, + ciphertext: &mut [u8; FINAL_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + let len = self.0.data_len; + ciphertext[..len].copy_from_slice(&self.0.data[..len]); + let mut ccm = Ccm::::from_perm( + self.0.perm, + &self.0.nonce, + &self.0.aad[..self.0.aad_len], + len, + )?; + // Scrub the plaintext copy as soon as the ciphertext is in `ciphertext`, rather than + // waiting for `data` to drop at the end of this call: the buffer is large and this keeps + // the window short. + ccm.do_encrypt_update(&mut ciphertext[..len])?; + self.0.data.zeroize(); + let tag = ccm.do_encrypt_final()?; + Ok((len, tag)) + } + + fn encrypt_out_detached( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + Self::one_shot(key, &mut rng, aad, plaintext, ciphertext) + } + + fn encrypt_out_rng_detached( + key: &KeyMaterial, + rng: &mut dyn RNG, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + Self::one_shot(key, rng, aad, plaintext, ciphertext) + } + + fn encrypt_out_with_aad( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + Self::one_shot_inline(key, &mut rng, aad, plaintext, ciphertext) + } + + fn encrypt_out_rng_with_aad( + key: &KeyMaterial, + rng: &mut dyn RNG, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + Self::one_shot_inline(key, rng, aad, plaintext, ciphertext) + } +} + +/// Adapts [`Ccm`] to [`AEADCipherDecryptor`] and, through it, [`SymmetricCipherDecryptor`], by +/// buffering the whole message; the mirror of [`CcmEncryptor`], and see it for why the buffering +/// is unavoidable, what it costs, and what `FINAL_LEN` means. +/// +/// The decryptor buffers up to `FINAL_LEN` bytes -- a `FINAL_LEN - TAG_LEN`-byte ciphertext and, +/// with the tag inline, the tag after it -- because until the final call it cannot know which +/// layout it is being given. With the tag detached the ciphertext is still held to +/// `FINAL_LEN - TAG_LEN`, the same limit the encryptor applies. +/// +/// `NONCE_LEN` must be at least 12, as for [`CcmEncryptor`]: the nonce is supplied here rather +/// than drawn, but the pair is kept symmetric so that a parameter set which compiles for one side +/// compiles for the other. See "Random nonce length" on [`CcmEncryptor`]. +pub struct CcmDecryptor< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +>(CcmBuffer) +where + P: ElectronicCodeBook; + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> Algorithm for CcmDecryptor +where + P: ElectronicCodeBook, +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> CcmDecryptor +where + P: ElectronicCodeBook, +{ + /// Runs the whole of Sec 6.2 over the first `len` buffered bytes as ciphertext, checking `tag`, + /// with the plaintext written to `plaintext[..len]`. On failure that is zeroized before the + /// error is returned: Sec 6.2's "the payload P and the MAC T shall not be revealed". + fn finish( + mut self, + len: usize, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; FINAL_LEN], + ) -> Result { + plaintext[..len].copy_from_slice(&self.0.data[..len]); + let mut ccm = Ccm::::from_perm( + self.0.perm, + &self.0.nonce, + &self.0.aad[..self.0.aad_len], + len, + )?; + ccm.do_decrypt_update(&mut plaintext[..len])?; + self.0.data.zeroize(); + match ccm.do_decrypt_final(tag) { + Ok(()) => Ok(len), + Err(e) => { + plaintext[..len].fill(0); + Err(e) + } + } + } +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> SymmetricCipherDecryptor + for CcmDecryptor +where + P: ElectronicCodeBook, +{ + fn do_decrypt_init( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ) -> Result { + Ccm::::check_shape(); + // `P::new`'s own checks are the only key validation needed; see the encryptor's identical + // reasoning. `CcmBuffer::new` carries the `FINAL_LEN` assertions and the nonce floor. + let perm = P::new(key)?; + Ok(Self(CcmBuffer::new(perm, *nonce))) + } + + /// Identically `0`. This is the one thing a CCM decryptor gets *right* by being forced to + /// buffer: it releases no plaintext at all before the tag has been checked, so + /// [`AEADCipherDecryptor`]'s warning about unauthenticated output cannot bite a caller here. + fn update_out_len(&self, _input_len: usize) -> usize { + 0 + } + + /// Buffers `ciphertext` and writes nothing, per [`Self::update_out_len`]. An empty + /// `ciphertext` is a no-op and leaves the AAD phase open. + /// + /// # Errors + /// [`SymmetricCipherError::GenericError`] if the total would exceed `FINAL_LEN`. + fn do_update_out( + &mut self, + ciphertext: &[u8], + _plaintext: &mut [u8], + ) -> Result { + self.0.do_update_out(ciphertext, FINAL_LEN, "CCM: ciphertext longer than FINAL_LEN")?; + Ok(0) + } + + /// The inline layout: the last `TAG_LEN` buffered bytes are the tag (Sec 6.2 step 6's + /// `LSB_Tlen(C)`), and Sec 6.2 runs over the rest. + /// + /// # Errors + /// [`SymmetricCipherError::DecryptionFailed`] if fewer than `TAG_LEN` bytes were buffered, + /// Sec 6.2 step 1; [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. + fn do_final(self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError> { + let Some(len) = self.0.data_len.checked_sub(TAG_LEN) else { + return Err(SymmetricCipherError::DecryptionFailed); + }; + let mut tag = [0u8; TAG_LEN]; + tag.copy_from_slice(&self.0.data[len..len + TAG_LEN]); + let mut plaintext = [0u8; FINAL_LEN]; + let n = self.finish(len, &tag, &mut plaintext)?; + Ok((plaintext, n)) + } + + /// Everything but the trailing tag. + fn decrypt_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(TAG_LEN) + } + + fn decrypt_out( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + Self::decrypt_out_with_aad(key, nonce, &[], ciphertext, plaintext) + } +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, +> AEADCipherDecryptor + for CcmDecryptor +where + P: ElectronicCodeBook, +{ + /// As [`CcmEncryptor::do_update_aad`](AEADCipherEncryptor::do_update_aad); the concatenation + /// must match the encryptor's byte for byte or the tag check fails. + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.0.do_update_aad(aad) + } + + /// The detached layout: every buffered byte is ciphertext, and Sec 6.2 runs over all of it + /// against `tag`. On failure `plaintext` is zeroized before the error is returned. + /// + /// # Errors + /// [`SymmetricCipherError::GenericError`] if more than `FINAL_LEN - TAG_LEN` bytes were + /// buffered -- room the decryptor keeps only for an inline tag; + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. + fn do_final_out_detached( + self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; FINAL_LEN], + ) -> Result { + let len = self.0.data_len; + if len > CcmBuffer::::CAPACITY { + return Err(SymmetricCipherError::GenericError( + "CCM: detached ciphertext longer than FINAL_LEN - TAG_LEN", + )); + } + self.finish(len, tag, plaintext) + } + + fn decrypt_out_detached( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + plaintext: &mut [u8], + ) -> Result { + // The one-shots never construct a `CcmBuffer`, so the nonce floor is asserted here, as + // the encryptor's `one_shot` does. + CcmBuffer::::check_random_nonce_len(); + Ccm::::decrypt_detached( + key, nonce, aad, ciphertext, tag, plaintext, + ) + } + + /// Splits the trailing `TAG_LEN` bytes off as the tag and runs the non-buffering + /// [`Ccm::decrypt_detached`], checking the output buffer first so that a short one is reported + /// before a short ciphertext. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is too short; + /// [`SymmetricCipherError::DecryptionFailed`] if `ciphertext` is shorter than the tag; + /// otherwise as [`Ccm::decrypt_detached`]. + fn decrypt_out_with_aad( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let needed = Self::decrypt_out_max_len(ciphertext.len()); + if plaintext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let Some((data, tag)) = ciphertext.split_last_chunk::() else { + return Err(SymmetricCipherError::DecryptionFailed); + }; + Self::decrypt_out_detached(key, nonce, aad, data, tag, plaintext) + } +} + +#[cfg(test)] +mod tests { + //! Tests for the private formatting helpers, which are what a reviewer with SP 800-38C open + //! most needs to check and which no public API exposes directly. + //! + //! The expected values are the `B` and `Ctr_i` strings printed in the spec's own Appendix C + //! examples, transcribed from the errata-updated PDF. Appendix C gives the formatted block + //! string for each example, so these pin the flags octet, the placement of `N` and `Q`, and + //! the AAD length encoding against the document rather than against this implementation. + + use super::*; + use bouncycastle_core::key_material::KeyType; + + /// A stand-in permutation: the identity. `B0` and `Ctr_i` are formatted *before* any cipher + /// call, so the identity is enough to read them back out of the state, and it keeps these + /// tests about the formatting function rather than about AES. + struct Identity; + + impl Algorithm for Identity { + const ALG_NAME: &'static str = "identity"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + } + + impl ElectronicCodeBook<16, 16> for Identity { + fn new(_key: &KeyMaterial<16>) -> Result { + Ok(Identity) + } + fn encrypt_block(&self, _block: &mut [u8; 16]) {} + fn decrypt_block(&self, _block: &mut [u8; 16]) {} + fn encrypt_2blocks(&self, _blocks: &mut [[u8; 16]; 2]) {} + fn decrypt_2blocks(&self, _blocks: &mut [[u8; 16]; 2]) {} + fn encrypt_4blocks(&self, _blocks: &mut [[u8; 16]; 4]) {} + fn decrypt_4blocks(&self, _blocks: &mut [[u8; 16]; 4]) {} + } + + fn key() -> KeyMaterial<16> { + KeyMaterial::<16>::from_bytes_as_type( + &[ + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, + 0x4e, 0x4f, + ], + KeyType::SymmetricCipherKey, + ) + .expect("Appendix C's 128-bit key") + } + + /// Appendix C.1: `Tlen=32, Nlen=56, Alen=64, Plen=32`, so `t = 4`, `n = 7`, `q = 8`. + /// + /// The spec prints `B` as + /// `4f101112 13141516 00000000 00000004 | 00080001 02030405 06070000 00000000 | ...`, + /// so `B0` is `4f` then the 7-byte nonce then `[4]_64`, and `B1` is `[8]_16` then the 8-byte + /// AAD then six zero bytes of pad. + /// + /// C.1's AAD is 8 bytes, so its Adata bit is set. + #[test] + fn c1_b0_matches_the_spec() { + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + assert_eq!( + Ccm::::format_b0(&nonce, true, 4), + [0x4f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0, 0, 0, 0, 0, 0, 0, 4], + "C.1 B0: flags 0x4f = Adata 1 | [(4-2)/2]_3 = 001 | [8-1]_3 = 111, then Q = [4]_64" + ); + } + + /// A.2.2: the Adata bit is "'0' if a=0 and '1' if a>0", and it is bit 6 -- so clearing it must + /// take C.1's `0x4f` to `0x0f` and change nothing else in the block. + #[test] + fn adata_flag_is_bit_6_of_the_flags_octet() { + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + let with = Ccm::::format_b0(&nonce, true, 4); + let without = Ccm::::format_b0(&nonce, false, 4); + assert_eq!(without[0], 0x0f, "a = 0 clears bit 6, leaving the t and q fields alone"); + assert_eq!(with[0] ^ without[0], 1 << 6, "Adata is bit 6 and nothing else"); + assert_eq!(with[1..], without[1..], "the flag must not disturb N or Q"); + } + + /// The constructor really does absorb the `B0` that [`Ccm::format_b0`] built. With the identity + /// permutation the CBC-MAC chaining value after one block is that block itself, so a + /// no-AAD, no-payload construction leaves `B0` sitting in `y`. + /// + /// Without this, `format_b0` could be correct and unused. + #[test] + fn the_constructor_absorbs_b0() { + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + let ccm = Ccm::::new(&key(), &nonce, &[], 4).unwrap(); + assert_eq!(*ccm.y, Ccm::::format_b0(&nonce, false, 4)); + assert_eq!(ccm.mac_pos, 0, "a whole block was absorbed, so nothing is part-filled"); + } + + /// Appendix C.4: `Tlen=112, Nlen=104, Plen=256`, so `t = 14`, `n = 13`, `q = 2`; the spec + /// prints `B0` as `71101112 13141516 1718191a 1b1c0020`. + /// + /// This is the other end of the `q` range from C.1, so between them the two tests pin the + /// `[q-1]_3` encoding and the fact that `Q` is `q` octets wide, not a fixed width. + #[test] + fn c4_b0_matches_the_spec() { + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c]; + assert_eq!( + Ccm::::format_b0(&nonce, true, 32), + [ + 0x71, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x00, 0x20 + ], + "C.4 B0: flags 0x71 = Adata 1 | [(14-2)/2]_3 = 110 | [2-1]_3 = 001, then Q = [32]_16" + ); + } + + /// Appendix C.1 prints `Ctr0` as `07101112 13141516 00000000 00000000` and `Ctr1` as the same + /// with a trailing `01`; C.4's are `01101112 ... 1b1c0000` and `... 1b1c0001`. + /// + /// Table 4 makes the counter flags `[q-1]_3` alone, with every other bit zero -- which is what + /// keeps them distinct from `B0`, whose `t` field cannot be zero. + #[test] + fn counter_blocks_match_the_spec() { + let nonce_c1 = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + let mut ccm = + Ccm::::new(&key(), &nonce_c1, &[], 4).unwrap(); + // `Ctr0` is the template with a zero counter field. + let mut ctr0 = ccm.ctr_template; + Ccm::::put_q_field(&mut ctr0, 0); + assert_eq!( + ctr0, + [0x07, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0, 0, 0, 0, 0, 0, 0, 0], + "C.1 Ctr0" + ); + // The first payload keystream block is `S1`, so one refill must produce `Ctr1`. + ccm.refill_keystream(); + let mut ctr1 = ctr0; + ctr1[15] = 1; + assert_eq!(*ccm.ks, ctr1, "C.1 Ctr1 (the identity permutation leaves S1 = Ctr1)"); + + let nonce_c4 = + [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c]; + let ccm4 = + Ccm::::new(&key(), &nonce_c4, &[], 32).unwrap(); + let mut ctr0_c4 = ccm4.ctr_template; + Ccm::::put_q_field(&mut ctr0_c4, 0); + assert_eq!( + ctr0_c4, + [ + 0x01, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x00, 0x00 + ], + "C.4 Ctr0" + ); + } + + /// A.2.2's three AAD length encodings, at and around both boundaries. + /// + /// Two of these values come from the spec itself: C.1's `a = 8` is printed as `0008`, and + /// C.4's `a = 65536` (`Alen = 524288` bits) is printed as + /// `11111111 11111110 00000000 00000001 00000000 00000000`, i.e. `ff fe 00 01 00 00`. + /// + /// The rest pin the boundaries, which is the part no end-to-end test can reach: the first is + /// `2^16 - 2^8` = 65280 rather than the obvious-but-wrong `2^16`, and the second is `2^32`, + /// which through the public API would need a 4 GiB AAD. + #[test] + fn aad_length_encoding_matches_a_2_2() { + type Mode = Ccm; + + // Case 1: 0 < a < 2^16 - 2^8, two octets, `[a]_16`. + assert_eq!( + Mode::encode_aad_len(8), + ([0x00, 0x08, 0, 0, 0, 0, 0, 0, 0, 0], 2), + "C.1's a = 8" + ); + assert_eq!(Mode::encode_aad_len(1).1, 2); + // 65279 = 2^16 - 2^8 - 1 is the largest value still in the first case. + assert_eq!( + Mode::encode_aad_len(65279), + ([0xfe, 0xff, 0, 0, 0, 0, 0, 0, 0, 0], 2), + "65279 is still [a]_16" + ); + + // Case 2: 2^16 - 2^8 <= a < 2^32, six octets, `0xff || 0xfe || [a]_32`. 65280 is the first. + assert_eq!( + Mode::encode_aad_len(65280), + ([0xff, 0xfe, 0x00, 0x00, 0xff, 0x00, 0, 0, 0, 0], 6), + "65280 crosses into the six-octet case; a two-octet 0xff00 would be ambiguous" + ); + assert_eq!( + Mode::encode_aad_len(65536), + ([0xff, 0xfe, 0x00, 0x01, 0x00, 0x00, 0, 0, 0, 0], 6), + "C.4's a = 65536" + ); + // 2^32 - 1 is the largest value still in the second case. + assert_eq!( + Mode::encode_aad_len(u32::MAX as u64), + ([0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0], 6), + "2^32 - 1 is still the six-octet case" + ); + + // Case 3: 2^32 <= a < 2^64, ten octets, `0xff || 0xff || [a]_64`. + assert_eq!( + Mode::encode_aad_len(1u64 << 32), + ([0xff, 0xff, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00], 10), + "2^32 is the first ten-octet case" + ); + assert_eq!( + Mode::encode_aad_len(u64::MAX), + ([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], 10) + ); + + // A.2.2's whole point: the three cases are distinguishable by their leading octets, so no + // two distinct lengths can encode to the same prefix. The first octet is 0xff only in the + // second and third cases, and the second octet separates those. + for a in [1u64, 8, 65279] { + assert_ne!(Mode::encode_aad_len(a).0[0], 0xff, "case 1 must not lead with 0xff"); + } + } + + /// The constructor really uses [`Ccm::encode_aad_len`], and puts it *before* the AAD. + /// + /// With the identity permutation the CBC-MAC is `y = B0 ^ B1 ^ ... ^ Br`, so with a one-block + /// all-zero AAD the only nonzero contributions are `B0` and the length encoding. That makes the + /// encoding readable back out, which is what pins the ordering rather than just the value. + #[test] + fn the_constructor_prefixes_the_aad_with_its_length() { + type Mode = Ccm; + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + // 14 zero bytes of AAD: the 2-byte length plus 14 bytes is exactly one 16-byte block, so + // there is no padding to reason about. + let ccm = Mode::new(&key(), &nonce, &[0u8; 14], 0).unwrap(); + + let b0 = Mode::format_b0(&nonce, true, 0); + let mut b1 = [0u8; 16]; + b1[..2].copy_from_slice(&14u16.to_be_bytes()); + let expected: [u8; 16] = core::array::from_fn(|i| b0[i] ^ b1[i]); + assert_eq!(*ccm.y, expected, "y must be B0 ^ B1, with B1 starting with [14]_16"); + } +} diff --git a/crypto/modes/src/ctr.rs b/crypto/modes/src/ctr.rs index 4b2e77fc..27ae428c 100644 --- a/crypto/modes/src/ctr.rs +++ b/crypto/modes/src/ctr.rs @@ -249,6 +249,28 @@ where } } + /// As [`start`](Self::start), but the counter of the *next* block is `counter` instead of 0. + /// + /// GCM's GCTR (SP 800-38D Sec 6.5) runs the data through this mode starting at `inc32(J0)`, + /// whose counter field is `2` -- see `gcm.rs`. Crate-private because the public API's contract + /// is that a message starts at counter 0; only `gcm.rs` needs otherwise. + #[inline] + pub(crate) fn start_at(perm: P, nonce: [u8; INIT_DATA_LEN], counter: u64) -> Self { + Self::check_shape(); + debug_assert!( + counter < Self::BLOCK_LIMIT, + "start_at must not be handed an already-exhausted counter" + ); + Self { + perm, + nonce, + next_counter: counter, + keystream: Secret::new(), + used: BLOCK_LEN, + _dir: PhantomData, + } + } + /// `Tj = N | [j]m`: the nonce followed by the counter, big-endian, in the trailing `CTR_LEN` /// bytes. /// @@ -293,7 +315,8 @@ where /// block is used up and capacity has already been checked. #[inline] fn refill(&mut self) { - // Encrypted in place in the `Secret`: `Oj` never sits in a plain stack array. + // Enciphered in place inside the `Secret`, so no copy of the keystream block is ever left + // on the stack unzeroized; the counter block it starts from is public. *self.keystream = self.counter_block(); self.perm.encrypt_block(&mut self.keystream); self.next_counter += 1; @@ -318,19 +341,22 @@ where /// The counter blocks are built first -- they depend only on the nonce and the index, not on /// the data or on each other's cipher output -- so the `N` forward ciphers are independent. /// This is the parallelism Sec 6.5 describes, and it applies to both directions. + /// + /// `keystream` is the caller's scratch for the `N` blocks of `Oj`: [`Self::apply`] holds it in + /// a [`Secret`] for the whole call, so the batched keystream gets the same drop-time scrub as + /// the single-block buffer in `self` without a fresh allocation and scrub per batch. #[inline] fn apply_batch( &mut self, blocks: &mut [[u8; BLOCK_LEN]; N], + keystream: &mut [[u8; BLOCK_LEN]; N], batch: impl Fn(&P, &mut [[u8; BLOCK_LEN]; N]), ) { - // Live keystream until XORed in, so it gets the same drop-time scrub as `self.keystream`. - let mut keystream: Secret<[[u8; BLOCK_LEN]; N]> = Secret::new(); for slot in keystream.iter_mut() { *slot = self.counter_block(); self.next_counter += 1; } - batch(&self.perm, &mut keystream); + batch(&self.perm, keystream); for (block, o) in blocks.iter_mut().zip(keystream.iter()) { for (b, o) in block.iter_mut().zip(o.iter()) { *b ^= *o; @@ -341,13 +367,13 @@ where } /// XORs one whole block at a block boundary. + /// + /// Goes through the `Secret` keystream buffer rather than a plain local for the same reason + /// [`Self::refill`] does: a whole block of `Oj` must not be left on the stack unzeroized. #[inline] fn apply_one(&mut self, block: &mut [u8; BLOCK_LEN]) { - let mut o: Secret<[u8; BLOCK_LEN]> = Secret::new(); - *o = self.counter_block(); - self.perm.encrypt_block(&mut o); - self.next_counter += 1; - for (b, o) in block.iter_mut().zip(o.iter()) { + self.refill(); + for (b, o) in block.iter_mut().zip(self.keystream.iter()) { *b ^= *o; } self.used = BLOCK_LEN; @@ -370,14 +396,18 @@ where let (head, rest) = data.split_at_mut(head_len); self.apply_bytes(head); + // Scratch for the batched keystream, one per width and per call rather than per batch: + // held in a `Secret` so it is zeroized when this call returns, like the block in `self`. let (blocks, tail) = rest.as_chunks_mut::(); let (fours, rest_blocks) = blocks.as_chunks_mut::<4>(); + let mut ks4: Secret<[[u8; BLOCK_LEN]; 4]> = Secret::new(); for four in fours.iter_mut() { - self.apply_batch(four, P::encrypt_4blocks); + self.apply_batch(four, &mut ks4, P::encrypt_4blocks); } let (pairs, single) = rest_blocks.as_chunks_mut::<2>(); + let mut ks2: Secret<[[u8; BLOCK_LEN]; 2]> = Secret::new(); for pair in pairs.iter_mut() { - self.apply_batch(pair, P::encrypt_2blocks); + self.apply_batch(pair, &mut ks2, P::encrypt_2blocks); } for block in single.iter_mut() { self.apply_one(block); @@ -465,3 +495,52 @@ where Ok(len) } } + +#[cfg(test)] +mod tests { + //! Unit tests for `start_at`, which is `pub(crate)` and so cannot be reached from + //! `tests/ctr_tests.rs` -- exactly the "high-risk code that cannot be reached through the + //! public API" case QUALITY_AND_STYLE.md carves out for a unit test here rather than an + //! integration test. + + use super::*; + use bouncycastle_aes::aes_internal::AES128Internal; + use bouncycastle_core::key_material::{KeyMaterial, KeyType}; + use bouncycastle_core::traits::ElectronicCodeBook; + + type ToyCtr = Ctr; + + fn key() -> KeyMaterial<16> { + KeyMaterial::<16>::from_bytes_as_type(&[0x5Au8; 16], KeyType::SymmetricCipherKey) + .expect("a valid AES-128 key") + } + + /// `start_at(.., 2)` must produce the same keystream as `start` after its first two blocks + /// (32 bytes) have been discarded. This is what lets GCM's GCTR (SP 800-38D Sec 6.5) begin at + /// `inc32(J0)`, whose counter field is 2 -- see `gcm.rs`. + #[test] + fn start_at_matches_start_after_discarding_blocks() { + let nonce = [0x11u8; 12]; + + let mut from_start = ToyCtr::start(AES128Internal::new(&key()).unwrap(), nonce); + let mut discarded = [0u8; 32]; + from_start.apply(&mut discarded).unwrap(); + + let mut from_start_at = ToyCtr::start_at(AES128Internal::new(&key()).unwrap(), nonce, 2); + + let mut a = [0x42u8; 48]; + let mut b = a; + from_start.apply(&mut a).unwrap(); + from_start_at.apply(&mut b).unwrap(); + assert_eq!(a, b, "start_at(.., 2) must agree with start() past its first two blocks"); + } + + /// The capacity left after starting at counter 2 is exactly `2^32 - 2` blocks -- the SP + /// 800-38D Sec 5.2.1.1 plaintext length bound (`len(P) <= 2^39 - 256` bits, i.e. `2^32 - 2` + /// 128-bit blocks) that GCM relies on `Ctr`'s existing "counter exhausted" error to enforce. + #[test] + fn start_at_capacity_is_block_limit_minus_the_starting_counter() { + let ctr = ToyCtr::start_at(AES128Internal::new(&key()).unwrap(), [0u8; 12], 2); + assert_eq!(ctr.remaining_capacity(), (ToyCtr::BLOCK_LIMIT - 2) * 16); + } +} diff --git a/crypto/modes/src/gcm.rs b/crypto/modes/src/gcm.rs new file mode 100644 index 00000000..a9fccb2f --- /dev/null +++ b/crypto/modes/src/gcm.rs @@ -0,0 +1,670 @@ +//! Galois/Counter Mode (NIST SP 800-38D), the authenticated encryption mode built from CTR +//! (Sec 6.5's GCTR) and the GHASH universal hash in `ghash.rs` (Sec 6.4). +//! +//! # Scope: a 96-bit nonce and a 96-128-bit tag +//! +//! [`Gcm`] has no `NONCE_LEN` parameter: the nonce is always [`GCM_NONCE_LEN`] (12) bytes, generated +//! by the encryptor from the library's default RNG (Sec 8.2.2's RBG-based construction, with an +//! empty free field so the whole IV is the random field). Sec 5.2.1.1: "For IVs, it is recommended +//! that implementations restrict support to the length of 96 bits, to promote interoperability, +//! efficiency, and simplicity of design." The `len(IV) != 96` branch of Algorithm 4 step 2 (deriving +//! `J0` from a GHASH of the IV) is not implemented; every IV this type produces or accepts is 96 +//! bits, so that branch is unreachable here. +//! +//! The tag length is a const generic `TAG_LEN`, checked at compile time to lie in `12..=16` bytes +//! (96, 104, 112, 120 or 128 bits -- Sec 5.2.1.2's five recommended values). The 32- and 64-bit tags +//! Sec 5.2.1.2 permits "for certain applications" (Appendix C) are not supported: Appendix C +//! requires the *controlling protocol* to bound packet size and invocation counts (its Tables 1 and +//! 2), which this library cannot enforce, so it does not offer the option. +//! +//! # The API is the AEAD traits +//! +//! [`Gcm`] is used through [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], with +//! `FINAL_LEN = TAG_LEN`, and through the [`SymmetricCipherEncryptor`] / +//! [`SymmetricCipherDecryptor`] traits they extend: +//! +//! * The inherited symmetric-cipher methods are GCM with no AAD and the tag *inline*: +//! `ciphertext || tag`, streaming or through the `encrypt_out` / `decrypt_out` one-shots. +//! * The AEAD traits add `do_update_aad`, the detached-tag `*_detached` methods -- the spec's own +//! interface, where the tag is a separate value from the ciphertext (Algorithm 4's `(C, T)`, +//! Algorithm 5's separate `T` input) -- and the inline one-shots with AAD, `*_with_aad`. +//! +//! The decryptor holds back the last `TAG_LEN` bytes it has seen, because until the stream ends it +//! cannot know whether they are the inline tag or, detached, the end of the ciphertext. +//! +//! AAD must be supplied before any plaintext or ciphertext: SP 800-38D Algorithm 4 absorbs `A` +//! before `C` in one GHASH pass, so AAD after the first `do_update_out` is +//! [`SymmetricCipherError::StateError`] (empty AAD after data is a no-op, since it changes nothing). +//! +//! # Usage Examples +//! +//! Detached tag, one-shot: +//! +//! ``` +//! use bouncycastle_aes::aes_internal::AES128Internal; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +//! use bouncycastle_modes::{Decrypting, Encrypting, Gcm}; +//! +//! type Aes128Gcm = Gcm; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let aad = b"header, sent in the clear"; +//! let plaintext = *b"attack at dawn!!"; +//! +//! let mut ciphertext = [0u8; 16]; +//! let (nonce, _, tag) = +//! Aes128Gcm::::encrypt_out_detached(&key, aad, &plaintext, &mut ciphertext).unwrap(); +//! assert_ne!(ciphertext, plaintext); +//! +//! let mut recovered = [0u8; 16]; +//! Aes128Gcm::::decrypt_out_detached(&key, &nonce, aad, &ciphertext, &tag, &mut recovered) +//! .unwrap(); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! Inline `ciphertext || tag`, and streaming with AAD: +//! +//! ``` +//! use bouncycastle_aes::aes_internal::AES256Internal; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{ +//! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +//! }; +//! use bouncycastle_modes::{Decrypting, Encrypting, Gcm}; +//! +//! type Aes256Gcm = Gcm; +//! +//! let key = KeyMaterial::<32>::from_bytes_as_type(&[0x07; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! let aad = b"associated data"; +//! let message = b"a message that streams in over more than one call"; +//! +//! let (mut enc, nonce) = Aes256Gcm::::do_encrypt_init(&key).unwrap(); +//! enc.do_update_aad(aad).unwrap(); +//! let mut ct = vec![0u8; message.len()]; +//! enc.do_update_out(message, &mut ct).unwrap(); +//! let (tag_block, tag_len) = enc.do_final().unwrap(); +//! ct.extend_from_slice(&tag_block[..tag_len]); +//! +//! let mut dec = Aes256Gcm::::do_decrypt_init(&key, &nonce).unwrap(); +//! dec.do_update_aad(aad).unwrap(); +//! let mut pt = vec![0u8; ct.len()]; +//! let written = dec.do_update_out(&ct, &mut pt).unwrap(); +//! let (_last, last_len) = dec.do_final().unwrap(); +//! pt.truncate(written + last_len); +//! assert_eq!(pt, message); +//! ``` +//! +//! # Security Considerations +//! +//! * **Nonce uniqueness is everything.** Sec 8: "The probability that the authenticated encryption +//! function ever will be invoked with the same IV and the same key on two (or more) distinct sets +//! of input data shall be no greater than 2^-32." Appendix A: a repeated nonce lets an adversary +//! recover the hash subkey `H` from the two ciphertexts, after which "the authentication +//! assurance essentially is lost" and GCM inherits CTR's plaintext-controlling malleability. The +//! nonce is always drawn from the library's default RNG (Sec 8.2.2's RBG-based construction, +//! empty free field) and never accepted from the caller. +//! * **Invocation limit.** Sec 8.2.2 / 8.3: with the RBG-based construction, "the total number of +//! invocations of the authenticated encryption function shall not exceed 2^32 ... with the given +//! key." This is a caller obligation this type cannot enforce across calls; rotate the key well +//! before 2^32 messages. +//! * **Forgery probability and failed-verification limits.** Appendix B: a targeted forgery over +//! `n` blocks of AAD and ciphertext succeeds with probability about `n / 2^t`, and each success +//! leaks information about `H`; "the system or protocol that implements GCM should monitor and, if +//! necessary, limit the number of unsuccessful verification attempts for each key." +//! * **32- and 64-bit tags are not offered** (Appendix C); see the module docs above. +//! * **Streaming decryption releases plaintext before the tag is checked; the one-shots do not.** +//! [`SymmetricCipherDecryptor::do_update_out`] hands back plaintext as it goes, which is +//! unauthenticated until `do_final` / `do_final_detached` succeeds -- do not act on it before +//! then. The one-shots (`decrypt_out`, `decrypt_out_detached`, `decrypt_out_with_aad`) verify the +//! tag first and release nothing on failure, zeroizing the output buffer (Sec 7.2 permits +//! checking the tag before computing the plaintext, and this is why the one-shots are more than +//! init/update/final glued together). +//! * **Intermediates are secret.** Sec 5.3: "the intermediate values in the execution of the GCM +//! functions shall be secret." `H`, the running GHASH accumulator, the pending partial block, the +//! tag mask `CIPH_K(J0)` and the CTR keystream all live in +//! [`Secret`](bouncycastle_utils::secret::Secret). +//! * **The `2^39 - 256`-bit plaintext bound (Sec 5.2.1.1) is `Ctr`'s own counter-exhaustion error.** +//! GCTR runs from counter 2 (D6), leaving `2^32 - 2` blocks, i.e. exactly `2^39 - 256` bits, before +//! `Ctr` refuses with [`SymmetricCipherError::StateError`]. +//! * **Constant time.** GHASH multiplication (`ghash.rs`) and the tag comparison +//! (`bouncycastle_utils::ct::ct_eq_bytes`) touch no table indexed by secret data, with the same +//! caveats `bouncycastle-aes` states about compiler guarantees and side channels other than +//! timing. +//! * **GMAC is GCM with no plaintext** (Sec 5.2): feed only AAD and call `do_final_detached`: there +//! is no separate `Gmac` type. + +use crate::ghash::Ghash; +use crate::{Ctr, Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, ElectronicCodeBook, RNG, + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; +use core::marker::PhantomData; + +/// The nonce (IV) length this type uses: 96 bits, SP 800-38D Sec 5.2.1.1's recommended length. +pub const GCM_NONCE_LEN: usize = 12; + +/// Which category of bytes `Gcm` is currently absorbing into GHASH: additional authenticated data, +/// or plaintext/ciphertext. AAD is only accepted in the first phase (SP 800-38D Algorithm 4 absorbs +/// `A` before `C`); the transition also pads the AAD to a block boundary (the `0^v` of step 5). +#[derive(Clone, Copy, PartialEq, Eq)] +enum Phase { + Aad, + Data, +} + +/// Galois/Counter Mode over any [`ElectronicCodeBook`] permutation, direction typed as +/// [`Encrypting`] / [`Decrypting`]. See the module docs for the two APIs this type exposes and +/// [`GCM_NONCE_LEN`] / `TAG_LEN` for what is fixed and what is chosen. +pub struct Gcm +where + P: ElectronicCodeBook, +{ + /// `GCTR_K(inc32(J0), .)`: Algorithm 4 step 3 / Algorithm 5 step 4, started at counter 2 (see + /// [`Gcm::setup`]). + ctr: Ctr, + /// `GHASH_H` over `A || 0^v || C || 0^u`, Algorithm 4/5 step 5/6. + ghash: Ghash, + /// `CIPH_K(J0)`, the one-time mask for the tag (step 6's `GCTR_K(J0, S) = S (+) CIPH_K(J0)`, + /// valid because `S` is exactly one block). + ek_j0: Secret<[u8; 16]>, + /// `len(A)` in bytes so far; converted to bits at [`Gcm::tag_block`]. + aad_len: u64, + /// `len(C)` in bytes so far; converted to bits at [`Gcm::tag_block`]. + data_len: u64, + phase: Phase, + /// The last up to `TAG_LEN` bytes of ciphertext seen by [`SymmetricCipherDecryptor::do_update_out`] + /// but not yet released, because they might be the tag. Meaningful only on the `Decrypting` + /// side; kept on both directions rather than splitting the struct by `Dir` -- seeded random + /// bytes are indistinguishable from a design that carries them deliberately, so this trades + /// `TAG_LEN` bytes of unused state on the encryptor for one struct definition instead of two. + tail: Secret<[u8; TAG_LEN]>, + /// How many bytes of `tail` are meaningful, `0..=TAG_LEN`. + tail_len: usize, + _dir: PhantomData, +} + +impl Gcm +where + P: ElectronicCodeBook, +{ + /// The compile-time shape check: `TAG_LEN` must be one of Sec 5.2.1.2's five recommended tag + /// lengths in bytes (96, 104, 112, 120, 128 bits -- Appendix C's 32- and 64-bit tags are a + /// documented non-goal; see the module docs). Called from every constructor. + #[inline] + fn check_shape() { + const { + assert!( + TAG_LEN >= 12 && TAG_LEN <= 16, + "GCM tag length must be 12..=16 bytes (96, 104, 112, 120 or 128 bits), \ + SP 800-38D Sec 5.2.1.2" + ); + }; + } + + /// Algorithm 4 steps 1-2 and the precomputation for step 6's tag mask. + fn setup(perm: P, nonce: [u8; GCM_NONCE_LEN]) -> Self { + Self::check_shape(); + + // Step 1: H = CIPH_K(0^128). Encrypted in place inside a `Secret` so that `H` is never + // held in an unzeroized stack array (Sec 5.3; Appendix A on what `H` gives an attacker). + let mut h: Secret<[u8; 16]> = Secret::new(); + perm.encrypt_block(&mut h); + + // Step 2 (len(IV) = 96 branch, the only one this type implements): J0 = IV || 0^31 || 1, + // built directly in the `Secret` that will hold CIPH_K(J0). + // + // Precompute CIPH_K(J0) now, while J0 is fully known: step 6's GCTR_K(J0, S) reduces to + // S (+) CIPH_K(J0) because S is exactly one block (Algorithm 3 with a single, complete + // input block), so this one-time mask is all GCTR at J0 will ever be asked to produce. + let mut ek_j0: Secret<[u8; 16]> = Secret::new(); + ek_j0[..GCM_NONCE_LEN].copy_from_slice(&nonce); + ek_j0[15] = 1; + perm.encrypt_block(&mut ek_j0); + + // Step 3's inc32(J0): J0's rightmost 32 bits are 1, so inc32(J0) has counter field 2. + let ctr = Ctr::start_at(perm, nonce, 2); + + Self { + ctr, + ghash: Ghash::new(&h), + ek_j0, + aad_len: 0, + data_len: 0, + phase: Phase::Aad, + tail: Secret::new(), + tail_len: 0, + _dir: PhantomData, + } + } + + /// Absorbs additional authenticated data: the body of both directions' + /// `AEADCipher*::do_update_aad`. Any number of calls before the first `do_update_out`; a + /// non-empty call after data has started is [`SymmetricCipherError::StateError`] (Algorithm 4 + /// absorbs `A` before `C` in one GHASH pass, D4). Empty AAD is always a no-op. + fn absorb_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + if self.phase == Phase::Data { + if aad.is_empty() { + return Ok(()); + } + return Err(SymmetricCipherError::StateError( + "GCM: additional authenticated data must be supplied before any plaintext or \ + ciphertext (SP 800-38D Algorithm 4 absorbs A before C in one GHASH pass)", + )); + } + self.ghash.update(aad); + self.aad_len = + self.aad_len.checked_add(aad.len() as u64).ok_or(SymmetricCipherError::StateError( + "GCM: additional authenticated data length exceeds the supported range", + ))?; + Ok(()) + } + + /// The AAD-to-data transition: pads the AAD to a block boundary (the `0^v` of step 5) the + /// first time data arrives. A no-op on every later call. + fn begin_data_if_needed(&mut self) { + if self.phase == Phase::Aad { + self.ghash.pad_to_block(); + self.phase = Phase::Data; + } + } + + /// Absorbs `data` -- always ciphertext, whichever direction is calling -- into GHASH and + /// tracks its length. Shared by the encryptor (which calls this *after* GCTR has turned + /// plaintext into ciphertext in place) and the decryptor (which calls this *before* GCTR turns + /// the ciphertext back into plaintext): either way GHASH must see ciphertext, never plaintext. + fn absorb_data(&mut self, data: &[u8]) -> Result<(), SymmetricCipherError> { + self.begin_data_if_needed(); + self.ghash.update(data); + self.data_len = self.data_len.checked_add(data.len() as u64).ok_or( + SymmetricCipherError::StateError("GCM: data length exceeds the supported range"), + )?; + Ok(()) + } + + /// Algorithm 4 steps 4-6 / Algorithm 5 steps 5-7: pads GHASH to the block boundary (the `0^u` + /// of step 5), appends `[len(A)]_64 || [len(C)]_64`, and masks the result with `CIPH_K(J0)`. + /// Writes the full 16-byte block to `out`; callers truncate to `TAG_LEN`. `out` is a `Secret` + /// because on the decrypting side it is the expected tag `T'`, which forges the rejected + /// ciphertext if it survives a failed comparison. + /// + /// The byte-to-bit multiplication (`* 8`) is not checked for overflow: `aad_len` and `data_len` + /// are accumulated with `checked_add` at every absorption (`absorb_aad`, `absorb_data`), so + /// reaching a count whose `* 8` could overflow `u64` would already require far more calls than + /// are physically possible to make. + fn tag_block(&mut self, out: &mut Secret<[u8; 16]>) { + self.ghash.pad_to_block(); + let aad_bits = self.aad_len * 8; + let data_bits = self.data_len * 8; + self.ghash.finish(aad_bits, data_bits, out); + for (o, m) in out.iter_mut().zip(self.ek_j0.iter()) { + *o ^= m; + } + } +} + +impl Algorithm for Gcm +where + P: ElectronicCodeBook, +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +impl Gcm +where + P: ElectronicCodeBook, +{ + /// Encrypts `data` in place (GCTR, Algorithm 4 step 3) and absorbs the resulting ciphertext + /// into GHASH (step 5). Nothing is held back. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if the underlying `Ctr` counter would be exhausted -- + /// the SP 800-38D Sec 5.2.1.1 bound `len(P) <= 2^39 - 256` bits -- or if the AAD/data length + /// bookkeeping would overflow. Nothing is consumed in either case. + fn encrypt_in_place(&mut self, data: &mut [u8]) -> Result<(), SymmetricCipherError> { + self.ctr.do_encrypt(data)?; + self.absorb_data(data) + } + + /// Algorithm 4 steps 4-6: finishes the message and returns the detached authentication tag, + /// truncated to `TAG_LEN` bytes (`MSB_t`, step 6). Consumes the encryptor. + fn finish(mut self) -> [u8; TAG_LEN] { + // Covers an AAD-only or entirely empty message, where no data was ever encrypted. + self.begin_data_if_needed(); + let mut full: Secret<[u8; 16]> = Secret::new(); + self.tag_block(&mut full); + let mut tag = [0u8; TAG_LEN]; + tag.copy_from_slice(&full[..TAG_LEN]); + tag + } +} + +impl + SymmetricCipherEncryptor + for Gcm +where + P: ElectronicCodeBook, +{ + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + Self::do_encrypt_init_rng(key, &mut rng) + } + + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; GCM_NONCE_LEN]), SymmetricCipherError> { + Self::check_shape(); + let perm = P::new(key)?; + let nonce = crate::iv::random_iv::(rng)?; + Ok((Self::setup(perm, nonce), nonce)) + } + + /// The identity: GCM's encryptor holds nothing back. + fn update_out_len(&self, input_len: usize) -> usize { + input_len + } + + fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); + } + ciphertext[..plaintext.len()].copy_from_slice(plaintext); + self.encrypt_in_place(&mut ciphertext[..plaintext.len()])?; + Ok(plaintext.len()) + } + + fn do_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + let tag = self.finish(); + Ok((tag, TAG_LEN)) + } + + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + TAG_LEN + } +} + +/// The AEAD view: [`AEADCipherEncryptor`] over the [`SymmetricCipherEncryptor`] impl above, with +/// `FINAL_LEN = TAG_LEN`. The encryptor holds nothing back, so the detached final flushes nothing +/// and returns only the tag. +impl + AEADCipherEncryptor + for Gcm +where + P: ElectronicCodeBook, +{ + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.absorb_aad(aad) + } + + /// Algorithm 4 steps 4-6; `ciphertext` is left untouched, since nothing is held back. + fn do_final_out_detached( + self, + _ciphertext: &mut [u8; TAG_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + Ok((0, self.finish())) + } +} + +impl Gcm +where + P: ElectronicCodeBook, +{ + /// Absorbs `data` (ciphertext) into GHASH, then decrypts it in place. Order matters and is the + /// reverse of the encryptor's: GHASH must see ciphertext on both sides, so it is absorbed + /// *before* GCTR turns it into plaintext here. + /// + /// The plaintext this releases is **not yet authenticated**; see the module docs' Security + /// Considerations section. + /// + /// # Errors + /// As `encrypt_in_place`. + fn decrypt_in_place(&mut self, data: &mut [u8]) -> Result<(), SymmetricCipherError> { + self.absorb_data(data)?; + self.ctr.do_decrypt(data)?; + Ok(()) + } + + /// Algorithm 5 steps 5-8: recomputes `T'` and compares it against `tag` in constant time. + /// Consumes the decryptor; `Ok(())` is the only thing that makes the plaintext released so far + /// trustworthy. + /// + /// # Errors + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not match. + fn finish(mut self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + self.begin_data_if_needed(); + let mut full: Secret<[u8; 16]> = Secret::new(); + self.tag_block(&mut full); + if ct_eq_bytes(&full[..TAG_LEN], tag) { + Ok(()) + } else { + Err(SymmetricCipherError::AEADTagCheckFailed) + } + } + + /// Shared by the trait one-shots (`decrypt_out`, `decrypt_out_detached`, + /// `decrypt_out_with_aad`): absorbs `aad` and + /// `data` (still ciphertext) into GHASH and checks the tag *before* touching `data`, so no + /// unauthenticated plaintext is ever written to the caller's buffer (Sec 7.2 explicitly permits + /// checking the tag before computing the plaintext). Only on success is `data` decrypted. + fn verify_then_decrypt( + key: &KeyMaterial, + nonce: &[u8; GCM_NONCE_LEN], + aad: &[u8], + data: &mut [u8], + tag: &[u8; TAG_LEN], + ) -> Result<(), SymmetricCipherError> { + Self::check_shape(); + let perm = P::new(key)?; + let mut gcm = Self::setup(perm, *nonce); + gcm.absorb_aad(aad)?; + gcm.absorb_data(data)?; + let mut computed: Secret<[u8; 16]> = Secret::new(); + gcm.tag_block(&mut computed); + if !ct_eq_bytes(&computed[..TAG_LEN], tag) { + return Err(SymmetricCipherError::AEADTagCheckFailed); + } + gcm.ctr.do_decrypt(data)?; + Ok(()) + } +} + +impl + SymmetricCipherDecryptor + for Gcm +where + P: ElectronicCodeBook, +{ + fn do_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; GCM_NONCE_LEN], + ) -> Result { + Self::check_shape(); + let perm = P::new(key)?; + Ok(Self::setup(perm, *init_data)) + } + + /// `tail_len + input_len`, minus up to `TAG_LEN` bytes held back because they might be the tag. + fn update_out_len(&self, input_len: usize) -> usize { + (self.tail_len + input_len).saturating_sub(TAG_LEN) + } + + /// Releases every byte of `tail ++ ciphertext` except the last (up to) `TAG_LEN`, which become + /// the new tail. Decrypts (via `decrypt_in_place`) exactly the bytes released this call, so + /// GHASH absorbs each ciphertext byte exactly once across the whole stream. + fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let release = self.update_out_len(ciphertext.len()); + if plaintext.len() < release { + return Err(SymmetricCipherError::OutputBufferTooSmall(release)); + } + // Data has started even if every byte is still held back as a possible tag, so the AAD + // phase ends here rather than at the first byte released: otherwise a `do_update_aad` + // after a first call shorter than `TAG_LEN` would be accepted, and absorbed as if it came + // before the ciphertext (Algorithm 5 absorbs `A` before `C`). + self.begin_data_if_needed(); + + // Bytes of the old tail that are now known to be ciphertext, then bytes of the new input + // that are also released this call. + let tail_release = release.min(self.tail_len); + let input_release = release - tail_release; + if tail_release > 0 { + plaintext[..tail_release].copy_from_slice(&self.tail[..tail_release]); + } + if input_release > 0 { + plaintext[tail_release..release].copy_from_slice(&ciphertext[..input_release]); + } + if release > 0 { + self.decrypt_in_place(&mut plaintext[..release])?; + } + + // The new tail is whatever of (old tail ++ ciphertext) survives past `release` bytes -- + // at most TAG_LEN bytes, by construction of `release` above. + let mut new_tail = [0u8; TAG_LEN]; + let old_tail_kept = self.tail_len - tail_release; + new_tail[..old_tail_kept].copy_from_slice(&self.tail[tail_release..self.tail_len]); + let input_kept = ciphertext.len() - input_release; + new_tail[old_tail_kept..old_tail_kept + input_kept] + .copy_from_slice(&ciphertext[input_release..]); + *self.tail = new_tail; + self.tail_len = old_tail_kept + input_kept; + + Ok(release) + } + + /// If fewer than `TAG_LEN` bytes were ever seen, the ciphertext was too short to carry a tag at + /// all (Algorithm 5 step 1's "lengths not supported"). Otherwise checks the tag held in `tail` + /// against the GHASH state built up by every prior `do_update_out` call. Releases nothing: an + /// authenticated cipher's final output may be empty once the tag has been checked. + fn do_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + if self.tail_len < TAG_LEN { + return Err(SymmetricCipherError::DecryptionFailed); + } + let tag = *self.tail; + self.finish(&tag)?; + Ok(([0u8; TAG_LEN], 0)) + } + + fn decrypt_out_max_len(ciphertext_len: usize) -> usize { + ciphertext_len.saturating_sub(TAG_LEN) + } + + /// Overrides the trait's default (which would stream plaintext out before the tag is checked): + /// verifies the tag first and only then decrypts, so this one-shot never exposes + /// unauthenticated plaintext. The streaming path above, by its nature, still does. + fn decrypt_out( + key: &KeyMaterial, + init_data: &[u8; GCM_NONCE_LEN], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + >::decrypt_out_with_aad( + key, + init_data, + &[], + ciphertext, + plaintext, + ) + } +} + +/// The AEAD view: [`AEADCipherDecryptor`] over the [`SymmetricCipherDecryptor`] impl above, with +/// `FINAL_LEN = TAG_LEN`. The one-shots are overridden, as `decrypt_out` is, to check the tag +/// before any plaintext is written. +impl + AEADCipherDecryptor + for Gcm +where + P: ElectronicCodeBook, +{ + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.absorb_aad(aad) + } + + /// The detached layout: the up to `TAG_LEN` bytes held back as a possible tag are ciphertext + /// after all, so they are decrypted into `plaintext` before the tag is checked against `tag` + /// (Algorithm 5 steps 5-8). On failure `plaintext` is zeroized before the error is returned. + fn do_final_out_detached( + mut self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; TAG_LEN], + ) -> Result { + let n = self.tail_len; + plaintext[..n].copy_from_slice(&self.tail[..n]); + self.decrypt_in_place(&mut plaintext[..n])?; + if let Err(e) = self.finish(tag) { + plaintext.fill(0); + return Err(e); + } + Ok(n) + } + + /// Verifies `tag` before decrypting, so no unauthenticated plaintext reaches `plaintext`; on + /// failure what was written there is zeroized. + fn decrypt_out_detached( + key: &KeyMaterial, + nonce: &[u8; GCM_NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + plaintext: &mut [u8], + ) -> Result { + let len = ciphertext.len(); + if plaintext.len() < len { + return Err(SymmetricCipherError::OutputBufferTooSmall(len)); + } + plaintext[..len].copy_from_slice(ciphertext); + Self::verify_then_decrypt(key, nonce, aad, &mut plaintext[..len], tag).inspect_err( + |_| { + // The buffer holds ciphertext rather than unauthenticated plaintext here, since the + // tag is checked before decryption, but the trait's contract is a zeroized buffer on + // failure, and a caller who ignores the `Result` should find nothing in it at all. + plaintext[..len].fill(0); + }, + )?; + Ok(len) + } + + /// The inline layout with AAD: splits the trailing `TAG_LEN` bytes off as the tag and verifies + /// it before decrypting, as the detached one-shot does, zeroizing `plaintext` on failure. + fn decrypt_out_with_aad( + key: &KeyMaterial, + nonce: &[u8; GCM_NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let needed = Self::decrypt_out_max_len(ciphertext.len()); + if plaintext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let Some((data, tag)) = ciphertext.split_last_chunk::() else { + return Err(SymmetricCipherError::DecryptionFailed); + }; + let len = data.len(); + plaintext[..len].copy_from_slice(data); + Self::verify_then_decrypt(key, nonce, aad, &mut plaintext[..len], tag) + .inspect_err(|_| plaintext[..len].fill(0))?; + Ok(len) + } +} diff --git a/crypto/modes/src/ghash.rs b/crypto/modes/src/ghash.rs new file mode 100644 index 00000000..78eb366b --- /dev/null +++ b/crypto/modes/src/ghash.rs @@ -0,0 +1,426 @@ +//! GHASH: the universal hash function GCM builds its authentication on (NIST SP 800-38D Sec 6.3, +//! 6.4), and the GF(2^128) multiplication it is defined over. +//! +//! This is the only genuinely new cryptographic code `gcm.rs` needs; everything else there is +//! plumbing around this and [`crate::Ctr`]. +//! +//! # Field element representation +//! +//! A block of `GF(2^128)` is represented as `[u64; 2]`: `x[0]` is the first eight bytes of the +//! 16-byte block read big-endian, `x[1]` the last eight -- the same `asLongs`/`asBytes` convention +//! BC Java's `GCMUtil` uses. Sec 6.3 fixes the bit convention as "little endian": bit `x_0`, the +//! *leftmost* (most significant) bit of the first byte, is the coefficient of `u^0`. In this `u64` +//! pair form that means `x_0` is the *top* bit of `x[0]`, `x_63` is its bottom bit, `x_64` is the +//! top bit of `x[1]`, and `x_127` is its bottom bit. So Algorithm 1's "V >> 1" (discard the +//! rightmost bit of the whole 128-bit string, prepend a zero on the left) is a right shift across +//! the `x[0], x[1]` pair carrying the bottom bit of `x[0]` into the top bit of `x[1]`, and `R` +//! (`11100001 || 0^120`, Sec 6.3) is the block whose first byte is `0xE1` and the rest zero, i.e. +//! `[0xE1 << 56, 0]` in this representation. +//! +//! Getting this orientation right once, here, is worth the length of this comment: every GCM +//! implementation bug report in the wild is an orientation bug, and [`mul_reference`] exists so +//! [`mul`] can be checked against something whose correctness is visible by inspection of the spec +//! text above rather than by parity with another implementation. + +use bouncycastle_utils::secret::Secret; + +/// A block of `GF(2^128)`, in the two-`u64` form described in the module docs. +type Block = [u64; 2]; + +/// `R = 11100001 || 0^120` (Sec 6.3): first byte `0xE1`, the rest zero. Used only by +/// [`mul_reference`]: [`mul`] folds the same constant into its own reduction step directly, as +/// literal shift amounts rather than a named block. +#[cfg(test)] +const R: Block = [0xE100_0000_0000_0000, 0]; + +/// `x[0]` is the first eight bytes of `b` read big-endian, `x[1]` the last eight. +fn block_from_bytes(b: &[u8; 16]) -> Block { + [ + u64::from_be_bytes(b[..8].try_into().expect("first half of a 16-byte block is 8 bytes")), + u64::from_be_bytes(b[8..].try_into().expect("second half of a 16-byte block is 8 bytes")), + ] +} + +/// Inverse of [`block_from_bytes`]. Used only by the tests: [`Ghash::finish`] writes `S` straight +/// into the caller's `Secret` rather than returning it through a stack array. +#[cfg(test)] +fn block_to_bytes(x: &Block) -> [u8; 16] { + let mut out = [0u8; 16]; + out[..8].copy_from_slice(&x[0].to_be_bytes()); + out[8..].copy_from_slice(&x[1].to_be_bytes()); + out +} + +/// Algorithm 1 (Sec 6.3), a direct transcription, computed bit-serially with masks so it is itself +/// constant time. This is the *oracle*: [`mul`] is checked against it in the test module below, and +/// it is never used outside `#[cfg(test)]`. Kept short and boring on purpose. +#[cfg(test)] +fn mul_reference(x: &Block, y: &Block) -> Block { + // Step 2: Z_0 = 0^128, V_0 = Y. + let mut z: Block = [0, 0]; + let mut v: Block = *y; + // Step 3: for i = 0 to 127 ... + for i in 0..128u32 { + // Step 1 / step 3: bit x_i of X. x_0 is the top bit of x[0] (see module docs), so bit i + // for i < 64 is bit (63 - i) of x[0], and for i >= 64 is bit (127 - i) of x[1]. + let bit = if i < 64 { (x[0] >> (63 - i)) & 1 } else { (x[1] >> (127 - i)) & 1 }; + // All-ones if x_i = 1, all-zero if x_i = 0 -- a constant-time select, standing in for the + // spec's "Z_{i+1} = Z_i if x_i = 0; Z_i (+) V_i if x_i = 1". + let m = 0u64.wrapping_sub(bit); + z[0] ^= v[0] & m; + z[1] ^= v[1] & m; + + // "V_{i+1} = V_i >> 1 if LSB_1(V_i) = 0; (V_i >> 1) (+) R if LSB_1(V_i) = 1." LSB_1 of the + // 128-bit string V is the bottom bit of v[1]; ">> 1" is a right shift across the pair. + let lsb = v[1] & 1; + let lm = 0u64.wrapping_sub(lsb); + let carry_in = v[0] & 1; + v[0] >>= 1; + v[1] = (v[1] >> 1) | (carry_in << 63); + v[0] ^= R[0] & lm; + v[1] ^= R[1] & lm; + } + // Step 4: return Z_128. + z +} + +/// The masked-lane carry-less multiply of two 64-bit halves. +/// +/// Ported from BC Java's `GCMUtil.implMul64(long, long)` +/// (`crypto/modes/gcm/GCMUtil.java`). Four lane masks (`0x1111...`, `0x2222...`, `0x4444...`, +/// `0x8888...`) space the input bits four apart, so the sixteen masked products summed into each +/// output lane carry at most fifteen ways -- never enough for an integer carry to reach a live lane +/// -- which is what makes ordinary `u64` multiplication (relying on the CPU's integer multiplier +/// being constant time, the same assumption the rest of this library's constant-time code makes) +/// compute a carry-less (XOR-add) product on each lane. Masking again after summing discards the +/// garbage that leaked into the gaps between lanes. +fn impl_mul64(x: u64, y: u64) -> u64 { + let x0 = x & 0x1111_1111_1111_1111; + let x1 = x & 0x2222_2222_2222_2222; + let x2 = x & 0x4444_4444_4444_4444; + let x3 = x & 0x8888_8888_8888_8888; + + let y0 = y & 0x1111_1111_1111_1111; + let y1 = y & 0x2222_2222_2222_2222; + let y2 = y & 0x4444_4444_4444_4444; + let y3 = y & 0x8888_8888_8888_8888; + + let z0 = x0.wrapping_mul(y0) ^ x1.wrapping_mul(y3) ^ x2.wrapping_mul(y2) ^ x3.wrapping_mul(y1); + let z1 = x0.wrapping_mul(y1) ^ x1.wrapping_mul(y0) ^ x2.wrapping_mul(y3) ^ x3.wrapping_mul(y2); + let z2 = x0.wrapping_mul(y2) ^ x1.wrapping_mul(y1) ^ x2.wrapping_mul(y0) ^ x3.wrapping_mul(y3); + let z3 = x0.wrapping_mul(y3) ^ x1.wrapping_mul(y2) ^ x2.wrapping_mul(y1) ^ x3.wrapping_mul(y0); + + let z0 = z0 & 0x1111_1111_1111_1111; + let z1 = z1 & 0x2222_2222_2222_2222; + let z2 = z2 & 0x4444_4444_4444_4444; + let z3 = z3 & 0x8888_8888_8888_8888; + + // The four lanes are disjoint (each mask owns one bit in every nibble), so `|` and `^` agree + // here; `cargo mutants` is expected to report this substitution as a surviving, equivalent + // mutant rather than a missing test. + z0 | z1 | z2 | z3 +} + +/// The constant-time, table-free `GF(2^128)` product `x . y` (Sec 6.3's `*` operator). +/// +/// Ported from BC Java's `GCMUtil.multiply(long[], long[])`: a "three-way recursion" (Karatsuba +/// over the two 64-bit halves, per Bernstein's "Batch binary Edwards") built on [`impl_mul64`], with +/// a bit-reversal trick (`rev(x)*rev(y) == rev((x*y) << 1)`) to reach the high 64 bits of each +/// 64x64 product without a 128-bit multiply, followed by the standard two-step reduction by `R`. +/// Variable names (`h0..h5`, `z0..z3`) match the Java source so the two can be diffed side by side. +pub(crate) fn mul(x: &Block, y: &Block) -> Block { + let (x0, x1) = (x[0], x[1]); + let (y0, y1) = (y[0], y[1]); + let (x0r, x1r) = (x0.reverse_bits(), x1.reverse_bits()); + let (y0r, y1r) = (y0.reverse_bits(), y1.reverse_bits()); + + let h0 = impl_mul64(x0r, y0r).reverse_bits(); + let h1 = impl_mul64(x0, y0) << 1; + let h2 = impl_mul64(x1r, y1r).reverse_bits(); + let h3 = impl_mul64(x1, y1) << 1; + let h4 = impl_mul64(x0r ^ x1r, y0r ^ y1r).reverse_bits(); + let h5 = impl_mul64(x0 ^ x1, y0 ^ y1) << 1; + + let z0 = h0; + let mut z1 = h1 ^ h0 ^ h2 ^ h4; + let mut z2 = h2 ^ h1 ^ h3 ^ h5; + let z3 = h3; + + // Reduction by R, step 1: fold z3 into z1 and z2. The commented-out `(z3 << 63)` term in BC + // Java's source is dropped because it is folded into the `z2 ^= ... (z3 << 62) ...` line below + // instead: `z3 << 63` contributes only its bit 63 (all lower bits are shifted out), which is the + // same single bit that `(z3 << 62) << 1`, i.e. bit 62 of `(z3 << 62)`, would carry forward one + // more position -- BC Java's own comment marks this as the intentional omission. + z1 ^= z3 ^ (z3 >> 1) ^ (z3 >> 2) ^ (z3 >> 7); + z2 ^= (z3 << 62) ^ (z3 << 57); + + let mut z0 = z0; + // Reduction by R, step 2: fold the now-complete z2 into z0 and z1. + z0 ^= z2 ^ (z2 >> 1) ^ (z2 >> 2) ^ (z2 >> 7); + z1 ^= (z2 << 63) ^ (z2 << 62) ^ (z2 << 57); + + [z0, z1] +} + +/// The `GHASH` accumulator (Algorithm 2, Sec 6.4). +/// +/// `Y_0 = 0^128` (step 2); each call to [`update`](Self::update) absorbs whole blocks via +/// `Y_i = (Y_{i-1} (+) X_i) . H` (step 3), buffering any partial block for the next call so that a +/// sequence of calls is equivalent to one call over the concatenation. [`finish`](Self::finish) +/// returns `Y_m` (step 4) after appending the 64-bit AAD- and data-bit-length block that Algorithm +/// 4 step 5 / Algorithm 5 step 6 fold into the same hash. +/// +/// `H` and the running hash `Y` are the GCM intermediates Sec 5.3 requires to be secret ("the +/// intermediate values in the execution of the GCM functions shall be secret"), so both live in a +/// [`Secret`] and are zeroized on drop; the pending partial block is live plaintext-or-ciphertext +/// bytes still waiting to be absorbed and is wrapped for the same reason. +pub(crate) struct Ghash { + /// The hash subkey `H = CIPH_K(0^128)`. + h: Secret, + /// `Y_i` of Algorithm 2. + y: Secret, + /// Bytes of the current block not yet absorbed. + pending: Secret<[u8; 16]>, + /// How many bytes of `pending` are meaningful, `0..=16`. + pending_len: usize, +} + +impl Ghash { + /// `Y_0 = 0^128` (Algorithm 2 step 2), keyed by the hash subkey `H`. + pub(crate) fn new(h: &[u8; 16]) -> Self { + let mut hs: Secret = Secret::new(); + hs[0] = u64::from_be_bytes(h[..8].try_into().expect("first half of H is 8 bytes")); + hs[1] = u64::from_be_bytes(h[8..].try_into().expect("second half of H is 8 bytes")); + Self { h: hs, y: Secret::new(), pending: Secret::new(), pending_len: 0 } + } + + /// `Y_i = (Y_{i-1} (+) X_i) . H` for one whole block `X_i`, updating `Y` in place. + /// + /// An associated function over the two fields rather than a `&mut self` method, so that + /// `pending` can be passed as `block` without first being copied out of its `Secret`. + fn absorb(y: &mut Secret, h: &Secret, block: &[u8; 16]) { + let xi = block_from_bytes(block); + y[0] ^= xi[0]; + y[1] ^= xi[1]; + **y = mul(y, h); + } + + /// Absorbs whole blocks of `data` immediately and buffers any remainder for the next call. + /// Chunking-independent: a sequence of calls over pieces of a message is equivalent to one call + /// over the whole message. + pub(crate) fn update(&mut self, mut data: &[u8]) { + if self.pending_len > 0 { + let need = 16 - self.pending_len; + let take = need.min(data.len()); + (*self.pending)[self.pending_len..self.pending_len + take] + .copy_from_slice(&data[..take]); + self.pending_len += take; + data = &data[take..]; + if self.pending_len < 16 { + return; + } + Self::absorb(&mut self.y, &self.h, &self.pending); + self.pending_len = 0; + } + + let (blocks, rest) = data.as_chunks::<16>(); + for block in blocks { + Self::absorb(&mut self.y, &self.h, block); + } + (*self.pending)[..rest.len()].copy_from_slice(rest); + self.pending_len = rest.len(); + } + + /// The `0^v` / `0^u` zero-padding of Algorithm 4 step 5 / Algorithm 5 step 6: rounds the + /// pending partial block up to a whole block with zero bytes and absorbs it. A no-op when + /// nothing is pending, so it is safe to call unconditionally at a phase boundary. + pub(crate) fn pad_to_block(&mut self) { + if self.pending_len == 0 { + return; + } + (*self.pending)[self.pending_len..].fill(0); + Self::absorb(&mut self.y, &self.h, &self.pending); + self.pending_len = 0; + } + + /// Appends `[aad_bits]_64 || [data_bits]_64` (Algorithm 4 step 5's final block) and writes + /// `Y_m`, i.e. `S`, to `out` -- a `Secret`, since `S` is the tag with its mask removed. + /// + /// Takes `&mut self` rather than `self` -- `Gcm`'s verify-before-decrypt one-shot needs the rest + /// of its own state (the `Ctr` field) after computing the tag, so consuming `Ghash` here would + /// force that caller to reconstruct it. Nothing asserts a "was padded" flag: the caller is + /// expected to have called [`pad_to_block`](Self::pad_to_block) for both the AAD and the data + /// phase already (the `0^v` and `0^u` of step 5), so by the time `finish` runs there is nothing + /// pending except this one final length block, and no caller should call `update` or + /// `pad_to_block` again afterward. + pub(crate) fn finish(&mut self, aad_bits: u64, data_bits: u64, out: &mut Secret<[u8; 16]>) { + debug_assert_eq!( + self.pending_len, 0, + "caller must pad_to_block before finish: nothing but the length block may be pending" + ); + let mut len_block = [0u8; 16]; + len_block[..8].copy_from_slice(&aad_bits.to_be_bytes()); + len_block[8..].copy_from_slice(&data_bits.to_be_bytes()); + Self::absorb(&mut self.y, &self.h, &len_block); + out[..8].copy_from_slice(&self.y[0].to_be_bytes()); + out[8..].copy_from_slice(&self.y[1].to_be_bytes()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A minimal xorshift64* generator, so the >= 10000 pseudo-random test pairs below do not need + /// the `rand` crate (CLAUDE.md: no new runtime dependency, and this is test-only anyway). + struct Lcg(u64); + impl Lcg { + fn next_u64(&mut self) -> u64 { + let mut x = self.0; + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + self.0 = x; + x + } + fn next_block(&mut self) -> Block { + [self.next_u64(), self.next_u64()] + } + } + + /// The spec's `1`: `1 || 0^127`, the leftmost bit set and everything else zero. The + /// multiplicative identity: `X . 1 == X` (Sec 6.3, "For a positive integer i, the ith power of a + /// block X ... H^2 = H.H, H^3 = H.H.H"). + const ONE: Block = [0x8000_0000_0000_0000, 0]; + + #[test] + fn mul_matches_the_reference_on_zero() { + let h: Block = [0x1122_3344_5566_7788, 0x99aa_bbcc_ddee_ff00]; + assert_eq!(mul(&[0, 0], &h), mul_reference(&[0, 0], &h)); + assert_eq!(mul(&h, &[0, 0]), mul_reference(&h, &[0, 0])); + } + + #[test] + fn mul_matches_the_reference_at_every_single_bit_position() { + let h: Block = [0xdead_beef_cafe_babe, 0x0123_4567_89ab_cdef]; + for i in 0..128u32 { + let x: Block = if i < 64 { [1u64 << (63 - i), 0] } else { [0, 1u64 << (127 - i)] }; + assert_eq!(mul(&x, &h), mul_reference(&x, &h), "bit position {i}"); + } + } + + #[test] + fn mul_matches_the_reference_on_all_ones() { + let h: Block = [0xfeed_face_dead_beef, 0x0102_0304_0506_0708]; + let ones: Block = [u64::MAX, u64::MAX]; + assert_eq!(mul(&ones, &h), mul_reference(&ones, &h)); + assert_eq!(mul(&h, &ones), mul_reference(&h, &ones)); + } + + #[test] + fn mul_matches_the_reference_on_ten_thousand_random_pairs() { + let mut rng = Lcg(0x2545_f491_4f6c_dd1d); + for _ in 0..10_000 { + let x = rng.next_block(); + let y = rng.next_block(); + assert_eq!(mul(&x, &y), mul_reference(&x, &y), "x={x:?} y={y:?}"); + } + } + + #[test] + fn mul_by_one_is_the_identity() { + let mut rng = Lcg(0x9e37_79b9_7f4a_7c15); + for _ in 0..256 { + let x = rng.next_block(); + assert_eq!(mul(&x, &ONE), x, "x . 1 == x for x={x:?}"); + assert_eq!(mul(&ONE, &x), x, "1 . x == x for x={x:?}"); + } + } + + #[test] + fn mul_is_commutative() { + let mut rng = Lcg(0xbf58_476d_1ce4_e5b9); + for _ in 0..256 { + let x = rng.next_block(); + let y = rng.next_block(); + assert_eq!(mul(&x, &y), mul(&y, &x), "x={x:?} y={y:?}"); + } + } + + /// A hand-checkable case for the oracle itself: `R . 1 == R`, the identity applied to the fixed + /// reduction constant. + #[test] + fn reference_r_times_one_is_r() { + assert_eq!(mul_reference(&R, &ONE), R); + } + + /// `GHASH` over one, two and three blocks must equal folding [`mul_reference`] by hand, per + /// Algorithm 2 step 3: `Y_i = (Y_{i-1} (+) X_i) . H`. + #[test] + fn ghash_matches_folding_the_reference_multiplier_by_hand() { + let h_bytes = [0x42u8; 16]; + let h = block_from_bytes(&h_bytes); + + let blocks: [[u8; 16]; 3] = [[0x11; 16], [0x22; 16], [0x33; 16]]; + + let mut y = [0u64, 0u64]; + for block in &blocks { + let xi = block_from_bytes(block); + y[0] ^= xi[0]; + y[1] ^= xi[1]; + y = mul_reference(&y, &h); + } + + for n in 1..=3 { + let mut g = Ghash::new(&h_bytes); + for block in &blocks[..n] { + g.update(block); + } + g.pad_to_block(); + // finish() also absorbs the zero-length block, so compare against one more fold step + // over the all-zero length block for a fair comparison of the n-block prefix alone. + let mut expected = [0u64, 0u64]; + for block in &blocks[..n] { + let xi = block_from_bytes(block); + expected[0] ^= xi[0]; + expected[1] ^= xi[1]; + expected = mul_reference(&expected, &h); + } + let zero_len_block = [0u8; 16]; + let xi = block_from_bytes(&zero_len_block); + expected[0] ^= xi[0]; + expected[1] ^= xi[1]; + expected = mul_reference(&expected, &h); + + let mut s: Secret<[u8; 16]> = Secret::new(); + g.finish(0, 0, &mut s); + assert_eq!(block_to_bytes(&expected), *s, "n={n}"); + } + // Silence the unused full-message `y` computed above; it documents the general recurrence. + let _ = y; + } + + /// Chunking independence: absorbing a 100-byte message in one call must equal absorbing it in + /// two pieces, at every possible split point. + #[test] + fn update_is_chunking_independent() { + let h_bytes = [0x7eu8; 16]; + let data: [u8; 100] = core::array::from_fn(|i| i as u8); + + let mut whole = Ghash::new(&h_bytes); + whole.update(&data); + whole.pad_to_block(); + let mut expected: Secret<[u8; 16]> = Secret::new(); + whole.finish(0, data.len() as u64 * 8, &mut expected); + + for split in 0..=data.len() { + let mut g = Ghash::new(&h_bytes); + g.update(&data[..split]); + g.update(&data[split..]); + g.pad_to_block(); + let mut s: Secret<[u8; 16]> = Secret::new(); + g.finish(0, data.len() as u64 * 8, &mut s); + assert_eq!(*s, *expected, "split at {split}"); + } + } +} diff --git a/crypto/modes/src/lib.rs b/crypto/modes/src/lib.rs index 612e52d2..cace5bd5 100644 --- a/crypto/modes/src/lib.rs +++ b/crypto/modes/src/lib.rs @@ -1,4 +1,4 @@ -//! Block cipher modes of operation (NIST SP 800-38A). +//! Block cipher modes of operation (NIST SP 800-38A, SP 800-38C and SP 800-38D). //! //! A mode turns a keyed block permutation -- `bouncycastle-aes`'s `AES128Internal` and friends, //! or anything else implementing [`ElectronicCodeBook`] -- into something that can encrypt more than @@ -11,20 +11,53 @@ //! | CFB | [`Cfb`] | SP 800-38A Sec 6.3 | Cipher Feedback, full-block segment (`s = b`), i.e. CFB128 for AES | //! | CFB8 | [`Cfb8`] | SP 800-38A Sec 6.3 | Cipher Feedback, 8-bit segment (`s = 8`) | //! | CTR | [`Ctr`] | SP 800-38A Sec 6.5 | Counter. Nonce plus counter, both directions parallel | -//! -//! They divide two ways. **ECB and CBC are block ciphers** ([`BlockCipherEncryptor`] / -//! [`BlockCipherDecryptor`]): whole blocks in, whole blocks out, and arbitrary-length data needs -//! the padding layer. **CFB, CFB8 and CTR are stream ciphers** ([`StreamCipherEncryptor`] / -//! [`StreamCipherDecryptor`]): any length in, the same length out, no padding, no finalization -- -//! see [Block alignment, and which modes need it](#block-alignment-and-which-modes-need-it). -//! -//! **All five reach the same arbitrary-length API**, so code can be written against one trait and -//! handed any mode. A block mode gets there by being wrapped in `bouncycastle-padding`'s adapters, -//! which are [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] with the padded block as -//! their final output; a stream mode implements those traits directly, with `FINAL_LEN = 0` because -//! it has no final output at all. The `bouncycastle-aes` aliases show the difference in -//! one line each: `AES_CBC_128` names a padding scheme, `AES_CTR_128` -//! has nothing to name. +//! | CCM | [`Ccm`] | SP 800-38C | Counter with CBC-MAC. **Authenticated**: CTR plus CBC-MAC, with a tag and AAD | +//! | GCM | [`Gcm`] | SP 800-38D | **Authenticated**: 96-bit nonce, 96-128-bit tag, no padding; AAD before data | +//! +//! They divide three ways. +//! +//! **ECB and CBC are block ciphers** ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]): whole +//! blocks in, whole blocks out, and arbitrary-length data needs the padding layer. **CFB, CFB8 and +//! CTR are stream ciphers** ([`StreamCipherEncryptor`] / [`StreamCipherDecryptor`]): any length in, +//! the same length out, no padding, no finalization -- see +//! [Block alignment, and which modes need it](#block-alignment-and-which-modes-need-it). +//! +//! **Those five reach the same arbitrary-length API**, so code can be written against one trait and +//! handed any of them. A block mode gets there by being wrapped in `bouncycastle-padding`'s +//! adapters, which are [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] with the padded +//! block as their final output; a stream mode implements those traits directly, with +//! `FINAL_LEN = 0` because it has no final output at all. The `bouncycastle-aes` aliases show the +//! difference in one line each: `AES_CBC_128` names a padding scheme, +//! `AES_CTR_128` has nothing to name. +//! +//! **CCM and GCM are the odd ones out, and deliberately so.** Both are AEADs: they take additional +//! authenticated data, and they produce a tag as well as a ciphertext, so they do not fit either +//! of the traits above -- there is nowhere in them to put the AAD or the tag. Both implement +//! [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] instead, and through them +//! [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] with no AAD and the tag inline. +//! +//! CCM reaches the AEAD traits through [`CcmEncryptor`] / [`CcmDecryptor`]; its own inherent API is +//! the one to reach for. Two other things set it apart: +//! +//! * **There is an extra input and an extra output.** The AAD is authenticated but not encrypted, +//! and the tag has to travel with the ciphertext; `Ccm` offers both the spec's inline +//! `ciphertext || tag` layout and a detached-tag pair. +//! * **The nonce is supplied, not generated.** CCM requires the nonce to be unique but *not* +//! unpredictable (SP 800-38C Sec 5.3), which is the opposite of the IV requirement the other +//! modes have, so a caller with a counter can do better than this crate's DRBG. +//! +//! See [`Ccm`] for both. +//! +//! **GCM is the other authenticated mode**, built from CTR and a universal hash rather than a +//! CBC-MAC. [`Gcm`] implements the AEAD traits itself, with `FINAL_LEN = TAG_LEN`: the traits are +//! its whole API, the inline `ciphertext || tag` view through the symmetric-cipher methods and the +//! spec's detached `(C, T)` pair through the `*_detached` methods. It differs from CCM in the other +//! direction on both counts above -- its 12-byte nonce is generated from the library's default RNG +//! rather than supplied, because a repeated GCM nonce gives away the hash subkey (SP 800-38D +//! Appendix A), and it streams. See [`Gcm`]. +//! +//! [Choosing between the modes](#choosing-between-the-modes) covers when each is the right answer +//! -- which, for a new design, one of them usually is. //! //! 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 @@ -38,14 +71,15 @@ //! //! The crate is deliberately cipher-agnostic: it depends on no concrete block cipher, only on the //! trait. Define a one-line alias for the combination you use -- or use the ready-made -//! `AES_CBC_128` / `AES_CFB_128` / `AES_CFB8_128` / `AES_CTR_128` / `AES_ECB_128` and friends from -//! `bouncycastle-aes`. Those aliases are not all the same shape: the two block modes take -//! a padding scheme as well as a direction, since neither is usable on data of arbitrary length -//! without one, while the three stream modes take only the direction: +//! `AES_CBC_128` / `AES_CCM_128` / `AES_CFB_128` / `AES_CFB8_128` / `AES_CTR_128` / `AES_ECB_128` / +//! `AES_GCM_128` and friends from `bouncycastle-aes`. Those aliases are not all the same shape: the +//! two block modes take a padding scheme as well as a direction, since neither is usable on data of +//! arbitrary length without one, the three stream modes take only the direction, CCM takes the +//! direction too, plus its nonce and tag lengths, and GCM takes the direction and its tag length: //! //! ``` //! use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; -//! use bouncycastle_modes::{Cbc, Cfb, Cfb8, Ctr, Ecb}; +//! use bouncycastle_modes::{Cbc, Ccm, Cfb, Cfb8, Ctr, Ecb, Gcm}; //! //! type Aes128Cbc = Cbc; //! type Aes192Cbc = Cbc; @@ -62,6 +96,20 @@ //! type Aes128Ctr = Ctr; //! //! type Aes128Ecb = Ecb; +//! +//! // CCM takes the direction like the rest, plus the nonce length and the tag length -- both +//! // real cryptographic choices rather than AES constants. The nonce length caps the payload +//! // (SP 800-38C A.1: `n + q = 15`, `p < 2^8q`) and the tag length is the forgery bound; +//! // 12 and 16 are the usual pair. +//! type Aes128Ccm = Ccm; +//! type Aes256Ccm = Ccm; +//! // A 13-byte nonce leaves q = 2, so a payload of at most 64 KiB - 1; 802.11 CCMP's pair. +//! type Aes128CcmShortTag = Ccm; +//! +//! // GCM takes the tag length but no nonce length: the nonce is always 12 bytes (SP 800-38D Sec +//! // 5.2.1.1's recommended 96 bits), and the block is always 16, so neither is a parameter. +//! type Aes128Gcm = Gcm; +//! type Aes256Gcm = Gcm; //! ``` //! //! # Usage Examples @@ -212,6 +260,73 @@ //! assert_eq!(data, plaintext); //! ``` //! +//! CCM is shaped differently from all of the above, because it is authenticated. The +//! nonce is supplied rather than generated, and there is an extra input (the AAD, authenticated but +//! not encrypted) and an extra output (the tag). Decryption either returns the plaintext or fails +//! -- it never returns plausible-looking rubbish the way the unauthenticated modes do when the +//! ciphertext has been altered: +//! +//! ``` +//! use bouncycastle_aes::aes_internal::AES128Internal; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_modes::{Ccm, Decrypting, Encrypting}; +//! +//! type Aes128Ccm = Ccm; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! // Supplied, not generated -- and it must never repeat under this key. +//! let nonce = [0x01u8; 12]; +//! let header = b"authenticated, not encrypted"; +//! let message = b"any length: CCM pads internally"; +//! +//! // The spec's own layout (SP 800-38C Sec 6.1 step 8): `ciphertext || tag`. +//! let mut sealed = vec![0u8; message.len() + 16]; +//! Aes128Ccm::::encrypt(&key, &nonce, header, message, &mut sealed).expect("encryption"); +//! +//! let mut opened = vec![0u8; message.len()]; +//! let n = Aes128Ccm::::decrypt(&key, &nonce, header, &sealed, &mut opened).expect("decryption"); +//! assert_eq!(&opened[..n], message); +//! +//! // Any change to the ciphertext, the tag, the header or the nonce is detected -- which is the +//! // whole difference from the five modes above. +//! let mut tampered = sealed.clone(); +//! tampered[0] ^= 1; +//! assert!(Aes128Ccm::::decrypt(&key, &nonce, header, &tampered, &mut opened).is_err()); +//! assert!(Aes128Ccm::::decrypt(&key, &nonce, b"other header", &sealed, &mut opened).is_err()); +//! ``` +//! +//! GCM gives the same guarantee through the AEAD traits, with the nonce generated and returned +//! like the other modes' IVs; see [`Gcm`] for the detached and streaming forms: +//! +//! ``` +//! use bouncycastle_aes::aes_internal::AES128Internal; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +//! use bouncycastle_modes::{Decrypting, Encrypting, Gcm}; +//! +//! type Aes128Gcm = Gcm; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let header = b"authenticated, not encrypted"; +//! let message = b"any length: GCM needs no padding"; +//! +//! let mut ciphertext = [0u8; 32]; +//! let (nonce, _, tag) = +//! Aes128Gcm::::encrypt_out_detached(&key, header, message, &mut ciphertext) +//! .expect("encryption"); +//! +//! let mut opened = [0u8; 32]; +//! Aes128Gcm::::decrypt_out_detached(&key, &nonce, header, &ciphertext, &tag, &mut opened) +//! .expect("decryption"); +//! assert_eq!(&opened, message); +//! +//! let mut tampered = ciphertext; +//! tampered[0] ^= 1; +//! assert!(Aes128Gcm::::decrypt_out_detached(&key, &nonce, header, &tampered, &tag, &mut opened).is_err()); +//! ``` +//! //! Using the wrong direction does not compile: //! //! ```compile_fail @@ -229,8 +344,44 @@ //! //! # Choosing between the modes //! -//! None is authenticated, so the honest answer for new designs is "none of them -- use an AEAD". -//! ECB is not a candidate for data at all (below). Between the rest: +//! **For a new design, use [`Gcm`] or [`Ccm`].** Both are authenticated, and an unauthenticated +//! mode is almost never what a new protocol wants: the other five leave the ciphertext malleable in +//! the specific, exploitable ways set out in +//! [None of the other modes is authenticated](#none-of-the-other-modes-is-authenticated), and +//! bolting a MAC on afterwards is a design most people get wrong. +//! +//! GCM is the more widely deployed of the two and the one that streams. Its costs: +//! +//! * **The nonce is generated, and a repeat is catastrophic.** Reusing a nonce under a key gives +//! away the hash subkey, and with it the ability to forge (SP 800-38D Appendix A). [`Gcm`] never +//! takes a nonce from the caller, which removes the accident but also the option of a counter. +//! * **At most 2^32 messages per key** with a random nonce (Sec 8.3), a limit the caller has to +//! enforce, since no value here sees every message under a key. +//! * **Streaming decryption releases plaintext before the tag is checked.** The one-shots do not; +//! see [`Gcm`]. +//! +//! CCM's costs, so that the choice between them is informed rather than reflexive: +//! +//! * **Two cipher calls per block, only one of which batches.** CCM runs both CTR and a CBC-MAC +//! over the same data (Sec 5.2). The CBC-MAC is serial by construction (Sec 6.1 step 3: `Yi` +//! depends on `Yi-1`), so it cannot use the permutation's pair or four path, but the CTR half +//! can and does, exactly as [`Ctr`] does. This crate's benches measure roughly two thirds of +//! CTR's unbatched throughput and a third of CTR's batched -- better than a naive "two full +//! passes" would suggest, because only one of the two passes pays the unbatched cost. +//! * **It does not stream.** SP 800-38C Sec 3: "CCM is not designed to support partial processing +//! or stream processing", because the payload length is inside the first block the MAC covers. +//! `Ccm` handles that by taking the length up front, which costs nothing. The generic AEAD +//! adapters do the same for one-shots and buffer only genuinely streaming calls. See [`Ccm`]. +//! * **The payload is capped** by the nonce length, at `2^(8 * (15 - NONCE_LEN)) - 1` bytes. +//! * **The nonce must be unique.** Reuse is worse than for CTR: it loses confidentiality *and* +//! enables forgery. +//! +//! For a genuinely streaming multi-gigabyte input, choose GCM, or `bouncycastle-ascon`'s +//! Ascon-AEAD128, which also streams. Choosing an unauthenticated +//! mode from this crate should be a deliberate decision, made because an existing format or spec +//! requires it, and paired with separate authentication. +//! +//! ECB is not a candidate for data at all (below). Between the five unauthenticated modes: //! //! * **Only CBC needs padding.** CFB and CFB8 are stream ciphers: any length in, the same length //! out. CBC needs the data padded to a whole number of blocks, which means a padding layer and @@ -320,7 +471,9 @@ //! //! No heap allocation, and no lookup tables of its own. A CBC or CFB8 value is the permutation plus //! one block of chaining value; a CFB value adds a `usize` to that; a CTR value carries the nonce, -//! a counter and a keystream block; an ECB value is just the permutation, since nothing chains: +//! a counter and a keystream block; an ECB value is just the permutation, since nothing chains; a +//! CCM value carries three blocks (the CBC-MAC chaining value, the counter template and the +//! keystream) plus four counters, because it runs two mechanisms at once: //! //! ```text //! size_of::>() == size_of::

() + BLOCK_LEN @@ -331,6 +484,16 @@ //! // CTR, rounded up to the counter's 8-byte alignment: //! size_of::>() //! == align8(size_of::

() + NONCE_LEN + 8 + BLOCK_LEN + 8) +//! +//! // CCM. Independent of NONCE_LEN and TAG_LEN: the nonce lives inside the counter template and +//! // the tag is built at finalization, so neither adds a field. `Dir` is zero-sized. +//! size_of::>() +//! == align8(size_of::

() + 3 * BLOCK_LEN + 3 * size_of::() + 8) +//! +//! // The buffering AEAD-trait adapter values used by the streaming API: two FINAL_LEN arrays. +//! // Their one-shots bypass these values and use Ccm directly. +//! size_of::>() +//! == align8(size_of::

() + 2 * FINAL_LEN + NONCE_LEN + 2 * size_of::() + 1) //! ``` //! //! | Combination | Permutation | Chain | Count | Total | @@ -347,6 +510,24 @@ //! | AES-128 ECB | 176 B | 0 B | -- | 176 B | //! | AES-192 ECB | 208 B | 0 B | -- | 208 B | //! | AES-256 ECB | 240 B | 0 B | -- | 240 B | +//! | AES-128 CCM | 176 B | 16 B MAC + 16 B counter template + 16 B keystream | 32 B | 256 B | +//! | AES-192 CCM | 208 B | 48 B, as above | 32 B | 288 B | +//! | AES-256 CCM | 240 B | 48 B, as above | 32 B | 320 B | +//! +//! CCM is the largest of the streaming values, because it is the only mode running two mechanisms +//! at once: the CBC-MAC needs its chaining value, and the CTR half needs both a keystream block and +//! the counter template that generates it. It is **independent of `NONCE_LEN` and `TAG_LEN`** -- +//! `Ccm` and `Ccm` are both 256 B -- +//! because the nonce is stored inside the counter template rather than separately, and the tag is +//! assembled at finalization rather than held. +//! +//! **Streaming [`CcmEncryptor`] and [`CcmDecryptor`] values are a different order of magnitude**, +//! and that is the one memory figure in this crate worth thinking about before choosing an API. +//! They buffer the whole message, so at `FINAL_LEN = 2048` an AES-128 adapter is **4304 B**. +//! Their one-shots override the trait defaults and use [`Ccm`] directly, costing 256 B for AES-128 +//! (the table above) regardless of `FINAL_LEN`; the like-for-like benchmark compares that path +//! with [`Ccm::encrypt_detached`]. See [`Ccm`] for why only the open-ended streaming methods must +//! buffer. //! //! CFB8 is the same size as CBC because it stores the same thing: one block of input to the next //! cipher call. CFB adds one `usize` because its segment is a whole block and a call may end @@ -361,7 +542,7 @@ //! that read its position back out of those bytes could not tell "just started" from "used up". //! The keystream block is held in a `Secret`: unlike a chaining value it is live key material for //! the bytes not yet consumed. The transient keystream blocks of its batch paths are `Secret`s for -//! the same reason, which costs a zeroizing write per batch. +//! the same reason: one per batch width, held for the whole call and zeroized once at its end. //! //! The data methods work in place. The batch paths in a decryptor are the transient cost: a //! `[[u8; BLOCK_LEN]; 4]` of stack for the four-block path -- 64 B on AES -- and a @@ -392,9 +573,16 @@ //! data. If you find yourself reaching for it because it needs no IV, that is the problem the IV //! solves. //! -//! ## None of the modes is authenticated +//! ## None of the other modes is authenticated //! -//! All four provide, at best, confidentiality only. None detects tampering, and each is malleable +//! This section is about the five SP 800-38A modes. **[`Ccm`] and [`Gcm`] are exempt**: they are +//! AEADs, their tags cover the payload, the AAD and the nonce, and decryption returns `Err` rather +//! than plaintext if any of them has been altered. (GCM's streaming decryptor releases plaintext +//! before that `Err`; see [`Gcm`].) Everything below is a description of what you give up by +//! choosing one of the other five, and the reason +//! [Choosing between the modes](#choosing-between-the-modes) starts with the AEADs. +//! +//! Those five provide, at best, confidentiality only. None detects tampering, and each is malleable //! in specific, exploitable ways -- SP 800-38A Appendix D, Table D.2, whose CFB row is //! "SBE in the decryption of `Cj`" plus "RBE in the decryption of `Cj+1`,...,`Cj+b/s`" (SBE = //! specific bit errors, the same positions; RBE = random bit errors): @@ -416,8 +604,9 @@ //! CFB8 is chosen for -- and it also means a tampered byte damages a bounded, predictable window //! rather than the rest of the message. //! -//! **Authenticate the ciphertext.** Prefer an AEAD; if you must use one of these, MAC the -//! ciphertext *and* the IV, and verify before decrypting. +//! **Authenticate the ciphertext.** Prefer an AEAD -- [`Gcm`] and [`Ccm`] are in this crate, and +//! need no separate MAC, no key-separation decision and no encrypt-then-MAC ordering care. If you must use +//! one of the five, MAC the ciphertext *and* the IV, and verify before decrypting. //! //! Combining decryption with a padding check is the classic padding-oracle setup. It applies to CBC //! here, the one mode that needs padding; do not report padding failures distinguishably, and do @@ -484,16 +673,27 @@ //! * **CFB1**, the `s = 1` segment size (SP 800-38A Appendix F.3.1-F.3.6). Its segment is a single //! *bit*, so unlike [`Cfb`] and [`Cfb8`] it does not fit a byte-oriented API at all: a message is //! a bit string whose length need not be a multiple of 8, which this crate has no type for. -//! * **OFB**, the one remaining mode of the recommendation. It is a keystream mode and, like CFB, +//! * **OFB**, the one remaining mode of SP 800-38A. It is a keystream mode and, like CFB, //! CFB8 and CTR, would implement [`StreamCipherEncryptor`] / [`StreamCipherDecryptor`]. +//! * **GCM with a nonce other than 96 bits** (SP 800-38D Algorithm 4 step 2's `len(IV) != 96` +//! branch, which derives `J0` by GHASHing the IV). Sec 5.2.1.1 recommends restricting support to +//! 96 bits, and [`Gcm`] does. +//! * **GCM with a 32- or 64-bit tag** (Sec 5.2.1.2, Appendix C). Those need the controlling +//! protocol to bound packet sizes and invocation counts, which this crate cannot enforce. +//! * **CCM with a formatting function other than Appendix A's.** SP 800-38C Sec 5.4 allows +//! alternatives and says "Alternative formatting functions may be developed in the future"; +//! Appendix A's is the only one that exists in practice and the only one [`Ccm`] implements. //! //! # Command line //! -//! The `bc-rust` CLI exposes all five modes for all three AES key lengths: `aes{128,192,256}-cbc`, -//! `-cfb`, `-cfb8`, `-ctr` and `-ecb`, each taking `encrypt` or `decrypt` and streaming stdin to -//! stdout. There is no API for caller-supplied init data anywhere, so `encrypt` writes what it -//! generated at the front of its output and `decrypt` reads it back, and the two compose. That is -//! one block for CBC, CFB and CFB8, **12 bytes** for CTR, and nothing at all for `-ecb`: +//! The `bc-rust` CLI exposes all seven modes for all three AES key lengths: `aes{128,192,256}-cbc`, +//! `-ccm`, `-cfb`, `-cfb8`, `-ctr`, `-ecb` and `-gcm`, each taking `encrypt` or `decrypt`. All but +//! `-ccm` stream stdin to stdout; see below for why CCM cannot. +//! +//! For the five unauthenticated modes there is no API for caller-supplied init data anywhere, so +//! `encrypt` writes what it generated at the front of its output and `decrypt` reads it back, and +//! the two compose. That is one block for CBC, CFB and CFB8, **12 bytes** for CTR, and nothing at +//! all for `-ecb`: //! //! ```text //! bc-rust aes256-cbc encrypt --key-file k.bin < plain.bin > cipher.bin @@ -512,39 +712,81 @@ //! [`Cfb8`]; the two are not interoperable. The `-ctr` commands use a 12-byte nonce and so a 4-byte //! counter, matching `AES_CTR_*`. Input must be block-aligned for the `-cbc` and `-ecb` commands, //! and may be any length for `-cfb`, `-cfb8` and `-ctr`, for the reason given above. +//! +//! **`-ccm` is different in three visible ways**, all of them following from CCM being an AEAD: +//! +//! ```text +//! # The nonce is a flag, and the same one is needed to decrypt: CCM needs it unique, not +//! # unpredictable (SP 800-38C Sec 5.3), so the caller chooses it. +//! bc-rust aes256-ccm encrypt --key-file k.bin --nonce 000102030405060708090a0b \ +//! --aad cafebabe < plain.bin > sealed.bin +//! bc-rust aes256-ccm decrypt --key-file k.bin --nonce 000102030405060708090a0b \ +//! --aad cafebabe < sealed.bin | cmp - plain.bin +//! ``` +//! +//! 1. **`--nonce` / `--nonce-file` is required and is not written to the output**, unlike every +//! other mode's generated IV. `--aad` adds data that is authenticated but not encrypted, and +//! must match on both sides. `--tag-len` selects the tag length, defaulting to 16. +//! 2. **The output is `--tag-len` bytes longer than the input** (`ciphertext || tag`, Sec 6.1 +//! step 8), and `decrypt` **fails with a non-zero exit** rather than emitting rubbish if +//! anything has been altered. +//! 3. **It does not stream**: it reads all of stdin before doing any work, so memory use is +//! proportional to the input. That is Sec 3's "CCM is not designed to support partial processing +//! or stream processing", not a limitation of this implementation. It does buy something, +//! though -- no plaintext is written until the tag has verified, so a failed `decrypt` leaves +//! nothing to discard. For a streaming AEAD use `-gcm` or `bc-rust ascon-aead128`. +//! +//! **`-gcm` streams, and frames its output like the unauthenticated modes**: `encrypt` writes the +//! generated 12-byte nonce first, then the ciphertext, then the 16-byte tag, and `decrypt` reads +//! the same layout back. `--aad` (hex, as for `-ccm`) or `--aad-file` supplies the AAD. The cost +//! of streaming is that a failed `decrypt` has **already written plaintext** by the time it reaches +//! the tag and exits non-zero, so check the exit code before using the output: +//! +//! ```text +//! bc-rust aes256-gcm encrypt --key-file k.bin --aad cafebabe < plain.bin > sealed.bin +//! bc-rust aes256-gcm decrypt --key-file k.bin --aad cafebabe < sealed.bin > out.bin && cmp out.bin plain.bin +//! ``` #![no_std] #![forbid(unsafe_code)] #![forbid(missing_docs)] mod cbc; +mod ccm; mod cfb; mod cfb8; mod ctr; mod ecb; +mod gcm; +mod ghash; mod iv; pub use cbc::Cbc; +pub use ccm::{Ccm, CcmDecryptor, CcmEncryptor}; pub use cfb::Cfb; pub use cfb8::Cfb8; pub use ctr::Ctr; pub use ecb::Ecb; +pub use gcm::{GCM_NONCE_LEN, Gcm}; // Imports needed for docs #[allow(unused_imports)] use bouncycastle_core::traits::{ - BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, StreamCipherDecryptor, - StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, + AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, + ElectronicCodeBook, StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, }; // end of imports needed for docs -/// Direction marker for a mode that encrypts. See [`Cbc`], [`Cfb`], [`Cfb8`], [`Ctr`] and [`Ecb`]. +/// Direction marker for a mode that encrypts. See [`Cbc`], [`Ccm`], [`Cfb`], [`Cfb8`], [`Ctr`], +/// [`Ecb`] and [`Gcm`]. /// /// Zero-sized: encoding the direction in the type costs no memory. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Encrypting; -/// Direction marker for a mode that decrypts. See [`Cbc`], [`Cfb`], [`Cfb8`], [`Ctr`] and [`Ecb`]. +/// Direction marker for a mode that decrypts. See [`Cbc`], [`Ccm`], [`Cfb`], [`Cfb8`], [`Ctr`], +/// [`Ecb`] and [`Gcm`]. /// /// Zero-sized: encoding the direction in the type costs no memory. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crypto/modes/tests/acvp_ccm_tests.rs b/crypto/modes/tests/acvp_ccm_tests.rs new file mode 100644 index 00000000..a0294d3f --- /dev/null +++ b/crypto/modes/tests/acvp_ccm_tests.rs @@ -0,0 +1,372 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-CCM` 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 test prints a warning and passes, +//! matching the convention used by the other ACVP suites -- `cargo test` must stay green for +//! someone who has only cloned this repository. +//! +//! # The tag is inline, so this drives the inline API +//! +//! The set has **no `tag` field anywhere**. An encrypt group's answer `ct` is the ciphertext with +//! the tag appended, and a decrypt group's input `ct` is the same, which is exactly SP 800-38C +//! Sec 6.1 step 8's own output string. So the cases go through [`Ccm::encrypt`] / [`Ccm::decrypt`], +//! the inline pair, and the group's `payloadLen` / `tagLen` are only needed to pick `TAG_LEN` and +//! to check the answer's length. +//! +//! # Failure cases are part of the vectors +//! +//! 52 of the 240 decrypt cases are inauthentic, and the response file marks them with +//! `"testPassed": false` and no `pt`. There is no `decryptVerificationFailed` field in this set. +//! Those cases are run and required to come back +//! [`AEADTagCheckFailed`](SymmetricCipherError::AEADTagCheckFailed) -- they are the only official +//! negative vectors this library has for CCM, so they are checked, not skipped. +//! +//! # Joining the request and response files +//! +//! As with the other AES sets, the response file carries only the answer against a `tcId`; the key, +//! nonce, AAD and input live in the request file, and so does the group metadata that says which +//! direction a case is. Both files are read and joined on `tcId`, which is unique across the whole +//! set. +//! +//! # What this set does *not* cover +//! +//! Worth stating, so the gaps stay visible rather than looking like coverage: +//! +//! * **`ivLen` is 96 in every group**, so `n = 12` and `q = 3` throughout. The nonce-length / +//! payload-limit tradeoff of A.1 is entirely untested here; `sp800_38c_tests.rs` covers `q` of 8, +//! 7, 3 and 2 against Appendix C. +//! * **`tagLen` is only 96 or 128.** The short tags A.1 permits (`t` of 4 or 6) appear in Appendix +//! C instead. +//! * **No empty AAD and no empty payload**: `aadLen` is 128 or 256 bits and `payloadLen` is 64, +//! 128 or 192. Sec 5.3 permits both to be empty, and `sp800_38c_tests.rs` covers that. +//! * **Every payload is 8, 16 or 24 bytes**, i.e. one or two blocks, so nothing here stresses a +//! long message. The `chunks` sweep below and the Appendix C.4 case cover the multi-block paths. +//! +//! The 6 Monte Carlo groups that the CTR and CBC sets have do not exist here: every group in this +//! set is `testType: "AFT"`, so nothing is skipped for that reason. + +use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::ElectronicCodeBook; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Ccm, Decrypting, Encrypting}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Every group in this set has `ivLen: 96`. +const NONCE_LEN: usize = 12; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = [ + "../../../bc-test-data/crypto/aes_tdes_vectors/CCM", + "../bc-test-data/crypto/aes_tdes_vectors/CCM", +]; + +const REQUEST_FILE: &str = "ACVP-AES-CCM.4014548.req.json"; +const RESPONSE_FILE: &str = "ACVP-AES-CCM.4014548.rsp.json"; + +fn test_data_dir() -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate); + if path.join(REQUEST_FILE).exists() && path.join(RESPONSE_FILE).exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); \ + ACVP AES-CCM tests will be skipped" + ); + None +} + +fn decode(value: &Value, field: &str, tc_id: u64) -> Vec { + let s = value + .get(field) + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("tcId {tc_id}: missing field {field}")); + hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc_id}: bad hex in {field}")) +} + +/// Wraps the vector's raw key bytes, promoting them if `KeyMaterial`'s entropy heuristic declined +/// to call them a cipher key. Same helper as the other ACVP suites in this crate. +fn cipher_key(bytes: &[u8]) -> KeyMaterial { + assert_eq!(bytes.len(), N, "key length should match the parameter set"); + let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("ACVP key bytes fit the buffer"); + + if key.key_type() != KeyType::SymmetricCipherKey { + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::from_bytes(N)) + }) + .expect("promoting a NIST test key"); + } + key +} + +/// The outcome of one decrypt case, so that an expected authentication failure can be asserted +/// rather than merely tolerated. +enum Decrypted { + Plaintext(Vec), + TagCheckFailed, +} + +/// Runs one encrypt case: `Ccm::encrypt` must produce the response file's `ct`, which is +/// `ciphertext || tag`. +/// +/// Also re-runs it through the length-declared streaming API in several chunkings, since these are +/// the only real vectors available for that path and the one-shot is a single call over the whole +/// payload. +fn encrypt_case( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + plaintext: &[u8], +) -> Vec +where + P: ElectronicCodeBook, +{ + let mut inline = vec![0u8; plaintext.len() + TAG_LEN]; + let written = Ccm::::encrypt( + key, nonce, aad, plaintext, &mut inline, + ) + .expect("CCM encryption of a valid ACVP case"); + assert_eq!(written, inline.len(), "the inline layout writes ciphertext || tag"); + + // The same answer must come out of the streaming API, in any chunking of both phases. + for chunk in [1usize, 5, 16] { + let mut ccm = Ccm::::new( + key, + nonce, + aad, + plaintext.len(), + ) + .expect("streaming init"); + let mut streamed = plaintext.to_vec(); + for piece in streamed.chunks_mut(chunk) { + ccm.do_encrypt_update(piece).expect("update"); + } + let tag = ccm.do_encrypt_final().expect("final"); + assert_eq!(&streamed[..], &inline[..plaintext.len()], "streamed in {chunk}-byte chunks"); + assert_eq!(&tag[..], &inline[plaintext.len()..], "streamed tag, {chunk}-byte chunks"); + } + + inline +} + +/// Runs one decrypt case over the inline `ciphertext || tag` string the vectors carry. +fn decrypt_case( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ct_and_tag: &[u8], +) -> Decrypted +where + P: ElectronicCodeBook, +{ + let mut plaintext = vec![0u8; ct_and_tag.len().saturating_sub(TAG_LEN)]; + match Ccm::::decrypt( + key, nonce, aad, ct_and_tag, &mut plaintext, + ) { + Ok(n) => { + plaintext.truncate(n); + Decrypted::Plaintext(plaintext) + } + Err(SymmetricCipherError::AEADTagCheckFailed) => { + assert!( + plaintext.iter().all(|b| *b == 0), + "Sec 6.2: the payload must not be revealed when the check fails" + ); + Decrypted::TagCheckFailed + } + Err(other) => panic!("unexpected CCM decryption error: {other:?}"), + } +} + +/// Dispatches a case to the right `(KEY_LEN, TAG_LEN)` instantiation. +/// +/// Both are const generics, so the six combinations this set uses are spelled out. `ivLen` is 96 in +/// every group, so `NONCE_LEN` is not part of the dispatch; an unexpected value is a hard failure +/// rather than a silent skip, so that a future revision of the vector file cannot quietly reduce +/// coverage. +#[allow(clippy::too_many_arguments)] +fn run_case( + tc_id: u64, + key_len: u64, + tag_len: u64, + encrypt: bool, + key_bytes: &[u8], + nonce: &[u8; NONCE_LEN], + aad: &[u8], + input: &[u8], +) -> Result, ()> { + macro_rules! dispatch { + ($k:literal, $t:literal, $p:ty) => {{ + let key = cipher_key::<$k>(key_bytes); + if encrypt { + Ok(encrypt_case::<$k, $t, $p>(&key, nonce, aad, input)) + } else { + match decrypt_case::<$k, $t, $p>(&key, nonce, aad, input) { + Decrypted::Plaintext(p) => Ok(p), + Decrypted::TagCheckFailed => Err(()), + } + } + }}; + } + + // A macro here rather than the unrolled six arms purely because the *type* arguments differ: + // `KEY_LEN`, `TAG_LEN` and the AES type all vary together, and a function cannot take them as + // runtime values. The body is one expression, and each arm is its own instantiation, so + // `cargo mutants` still sees the code it expands to. + match (key_len, tag_len) { + (128, 96) => dispatch!(16, 12, AES128Internal), + (128, 128) => dispatch!(16, 16, AES128Internal), + (192, 96) => dispatch!(24, 12, AES192Internal), + (192, 128) => dispatch!(24, 16, AES192Internal), + (256, 96) => dispatch!(32, 12, AES256Internal), + (256, 128) => dispatch!(32, 16, AES256Internal), + other => panic!("tcId {tc_id}: unexpected (keyLen, tagLen) {other:?}"), + } +} + +#[test] +fn acvp_aes_ccm_known_answer_tests() { + let Some(dir) = test_data_dir() else { return }; + + let req: Value = serde_json::from_str( + &fs::read_to_string(dir.join(REQUEST_FILE)).expect("readable request file"), + ) + .expect("valid ACVP request JSON"); + let rsp: Value = serde_json::from_str( + &fs::read_to_string(dir.join(RESPONSE_FILE)).expect("readable response file"), + ) + .expect("valid ACVP response JSON"); + + // The response file carries only the answer, against a tcId. Index it. + let mut answers: BTreeMap = BTreeMap::new(); + for group in rsp + .get(1) + .and_then(|s| s.get("testGroups")) + .and_then(Value::as_array) + .expect("response testGroups") + { + for test in group.get("tests").and_then(Value::as_array).expect("response tests") { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + answers.insert(tc_id, test.clone()); + } + } + + let groups = req + .get(1) + .and_then(|s| s.get("testGroups")) + .and_then(Value::as_array) + .expect("request testGroups"); + + let mut encrypt_cases = 0usize; + let mut decrypt_pass_cases = 0usize; + let mut decrypt_fail_cases = 0usize; + let mut per_kind: BTreeMap = BTreeMap::new(); + + for group in groups { + let test_type = group.get("testType").and_then(Value::as_str).expect("testType"); + assert_eq!(test_type, "AFT", "this set is documented as AFT-only"); + let direction = group.get("direction").and_then(Value::as_str).expect("direction"); + let encrypt = match direction { + "encrypt" => true, + "decrypt" => false, + other => panic!("unexpected direction {other}"), + }; + let key_len = group.get("keyLen").and_then(Value::as_u64).expect("keyLen"); + let tag_len = group.get("tagLen").and_then(Value::as_u64).expect("tagLen"); + let iv_len = group.get("ivLen").and_then(Value::as_u64).expect("ivLen"); + let payload_len = group.get("payloadLen").and_then(Value::as_u64).expect("payloadLen"); + assert_eq!(iv_len, 96, "every group in this set has a 96-bit nonce"); + assert_eq!(tag_len % 8, 0, "tagLen must be a whole number of octets"); + + for test in group.get("tests").and_then(Value::as_array).expect("tests") { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + let answer = answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + + let key_bytes = decode(test, "key", tc_id); + let nonce_bytes = decode(test, "iv", tc_id); + let nonce: [u8; NONCE_LEN] = nonce_bytes + .try_into() + .unwrap_or_else(|_| panic!("tcId {tc_id}: iv is not 12 bytes")); + let aad = decode(test, "aad", tc_id); + + // Input comes from the request, expected output from the response. + let input = decode(test, if encrypt { "pt" } else { "ct" }, tc_id); + + let expect_failure = answer + .get("testPassed") + .and_then(Value::as_bool) + .map(|passed| !passed) + .unwrap_or(false); + + let got = run_case(tc_id, key_len, tag_len, encrypt, &key_bytes, &nonce, &aad, &input); + + if encrypt { + assert!(!expect_failure, "tcId {tc_id}: an encrypt case cannot be a failure case"); + let expected = decode(answer, "ct", tc_id); + assert_eq!( + expected.len() as u64, + (payload_len + tag_len) / 8, + "tcId {tc_id}: the answer must be ciphertext || tag" + ); + let got = got.expect("an encrypt case never reports a tag failure"); + assert_eq!(got, expected, "tcId {tc_id}: AES-{key_len} CCM encrypt"); + encrypt_cases += 1; + } else if expect_failure { + assert!( + got.is_err(), + "tcId {tc_id}: the vectors say this ciphertext is inauthentic, \ + but decryption returned a payload" + ); + decrypt_fail_cases += 1; + } else { + let expected = decode(answer, "pt", tc_id); + let got = got.unwrap_or_else(|()| { + panic!("tcId {tc_id}: an authentic ACVP case failed its tag check") + }); + assert_eq!(got, expected, "tcId {tc_id}: AES-{key_len} CCM decrypt"); + decrypt_pass_cases += 1; + } + + *per_kind.entry(format!("AES-{key_len} t={} {direction}", tag_len / 8)).or_default() += + 1; + } + } + + println!("ACVP AES-CCM cases by parameter set:"); + for (kind, count) in &per_kind { + println!(" {kind}: {count}"); + } + println!( + " totals: {encrypt_cases} encrypt, {decrypt_pass_cases} decrypt-authentic, \ + {decrypt_fail_cases} decrypt-inauthentic" + ); + + // Guard against a silently-empty or partial run. These are the exact counts of the vector set, + // so a file that changed shape fails loudly instead of quietly testing less. + assert_eq!(encrypt_cases, 240, "expected 240 encrypt cases"); + assert_eq!(decrypt_pass_cases, 188, "expected 188 authentic decrypt cases"); + assert_eq!(decrypt_fail_cases, 52, "expected 52 inauthentic decrypt cases"); + assert_eq!( + encrypt_cases + decrypt_pass_cases + decrypt_fail_cases, + 480, + "every case in the set should be checked; none are skipped" + ); + // Three key lengths x two tag lengths x two directions: the full cross product, so every one + // of the six `run_case` instantiations is exercised in both directions. + assert_eq!( + per_kind.len(), + 12, + "expected all three key lengths at both tag lengths, in both directions" + ); +} diff --git a/crypto/modes/tests/acvp_gcm_tests.rs b/crypto/modes/tests/acvp_gcm_tests.rs new file mode 100644 index 00000000..cb6855fe --- /dev/null +++ b/crypto/modes/tests/acvp_gcm_tests.rs @@ -0,0 +1,131 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-GCM` 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 test prints a warning and passes, +//! matching the convention used by the other ACVP suites in this crate. +//! +//! The set (`ACVP-AES-GCM.4014542`) covers all three AES key lengths, a 96-bit IV throughout, +//! 96- and 128-bit tags, payload lengths of 64/128/192 bits and AAD lengths of 128/256 bits, in +//! both directions -- 270 cases total. Not every decrypt case in this particular set is a +//! forgery, but the ones that are all report `testPassed: false`; the valid-decrypt path is +//! additionally exercised by round-tripping every encrypt case through both the detached one-shot +//! and the inline `SymmetricCipherDecryptor` streaming view (`acvp_gcm::run_decrypt_case`, below). +//! +//! **Not covered here:** `bc-test-data` has no CAVP `.rsp` GCM vector files and no Wycheproof +//! `aes_gcm_test.json` -- only `sm4_gcm_test.json` exists under `wycheproof/`, and there is no +//! `GCM/cavp/` directory. This file and `acvp_gmac_tests.rs` are therefore the full extent of the +//! vector-based coverage against `bc-test-data`. If those files are added later, `cavp_gcm_tests.rs` +//! and `wycheproof_gcm_tests.rs` should be written against them following this file's shape. + +// Not `mod common;`: this crate-private helper's `serde_json::Value` usage, if pulled into the +// shared `common` module that most other test binaries in this crate include via `mod common;`, +// makes `u8: PartialEq<_>` ambiguous (`core`'s impl vs. serde_json's `impl PartialEq for +// u8`) at every bare `assert_eq!(byte_array, [])` in *those* files too -- `ecb_tests.rs` hit this +// exactly. Giving it its own module path keeps that ambiguity local to the two files that actually +// need ACVP JSON parsing. +#[path = "common/acvp_gcm.rs"] +mod acvp_gcm; + +use acvp_gcm::{GCM_NONCE_LEN, decode, run_decrypt_case, run_encrypt_case, test_data_dir}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; + +const SUBDIR: &str = "aes_tdes_vectors/GCM"; +const REQUEST_FILE: &str = "ACVP-AES-GCM.4014542.req.json"; +const RESPONSE_FILE: &str = "ACVP-AES-GCM.4014542.rsp.json"; + +#[test] +fn acvp_aes_gcm_known_answer_tests() { + let Some(dir) = test_data_dir(SUBDIR, REQUEST_FILE, RESPONSE_FILE) else { return }; + + let req: Value = serde_json::from_str( + &fs::read_to_string(dir.join(REQUEST_FILE)).expect("readable request file"), + ) + .expect("valid ACVP request JSON"); + let rsp: Value = serde_json::from_str( + &fs::read_to_string(dir.join(RESPONSE_FILE)).expect("readable response file"), + ) + .expect("valid ACVP response JSON"); + + // The response file carries only the answer, against a tcId. Index it. + let mut answers: BTreeMap = BTreeMap::new(); + for group in rsp[1]["testGroups"].as_array().expect("response testGroups") { + for test in group["tests"].as_array().expect("response tests") { + let tc_id = test["tcId"].as_u64().expect("tcId"); + answers.insert(tc_id, test.clone()); + } + } + + let groups = req[1]["testGroups"].as_array().expect("request testGroups"); + + let mut checked = 0usize; + let mut encrypt_checked = 0usize; + let mut decrypt_failed_checked = 0usize; + let mut per_kind: BTreeMap = BTreeMap::new(); + + for group in groups { + let direction = group["direction"].as_str().expect("direction"); + let tag_len = (group["tagLen"].as_u64().expect("tagLen") / 8) as usize; + let iv_len = group["ivLen"].as_u64().expect("ivLen"); + assert_eq!(iv_len, 96, "every group in this set has a 96-bit IV"); + + for test in group["tests"].as_array().expect("tests") { + let tc_id = test["tcId"].as_u64().expect("tcId"); + let key_bytes = decode(test, "key", tc_id); + let aad = decode(test, "aad", tc_id); + let iv_bytes = decode(test, "iv", tc_id); + let iv: [u8; GCM_NONCE_LEN] = iv_bytes + .try_into() + .unwrap_or_else(|_| panic!("tcId {tc_id}: expected a 12-byte IV")); + + match direction { + "encrypt" => { + let pt = decode(test, "pt", tc_id); + let answer = + answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + let ct = decode(answer, "ct", tc_id); + let tag = decode(answer, "tag", tc_id); + run_encrypt_case(&key_bytes, iv, &aad, &pt, tag_len, &ct, &tag); + + // Also round-trip this known-good ciphertext through decryption, since every + // decrypt group in this particular ACVP set is a forgery (below) and this is + // otherwise the only valid-decrypt coverage this file would have. + run_decrypt_case(&key_bytes, iv, &aad, &ct, &tag, Some(&pt)); + encrypt_checked += 1; + } + "decrypt" => { + let ct = decode(test, "ct", tc_id); + let tag = decode(test, "tag", tc_id); + let answer = + answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + // A forgery reports `testPassed: false` and no plaintext; a valid case reports + // `pt` directly, with no `testPassed` field at all (ACVP's convention: the key + // is present only to report failure). + if answer.get("testPassed").and_then(Value::as_bool) == Some(false) { + run_decrypt_case(&key_bytes, iv, &aad, &ct, &tag, None); + decrypt_failed_checked += 1; + } else { + let pt = decode(answer, "pt", tc_id); + run_decrypt_case(&key_bytes, iv, &aad, &ct, &tag, Some(&pt)); + } + } + other => panic!("unexpected direction {other}"), + } + + *per_kind.entry(format!("AES-{} {direction}", key_bytes.len() * 8)).or_default() += 1; + checked += 1; + } + } + + for (kind, n) in &per_kind { + println!("ACVP AES-GCM {kind}: {n} cases"); + } + println!( + "ACVP AES-GCM: {checked} cases checked ({encrypt_checked} encrypt, also round-tripped \ + through decrypt; {decrypt_failed_checked} decrypt forgeries)" + ); + + assert_eq!(checked, 270, "expected all 270 ACVP AES-GCM cases to run"); + assert!(encrypt_checked > 0 && decrypt_failed_checked > 0, "expected both directions covered"); +} diff --git a/crypto/modes/tests/acvp_gmac_tests.rs b/crypto/modes/tests/acvp_gmac_tests.rs new file mode 100644 index 00000000..4e58c3f2 --- /dev/null +++ b/crypto/modes/tests/acvp_gmac_tests.rs @@ -0,0 +1,109 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-GMAC` vectors from the `bc-test-data` repo. +//! +//! Same joiner and shape as `acvp_gcm_tests.rs` (see its module docs for the `bc-test-data` +//! requirement and what is and is not covered against `bc-test-data`), over the GMAC set +//! (`ACVP-AES-GMAC.4014543`, 270 cases): `payloadLen` is 0 throughout -- SP 800-38D Sec 5.2, GMAC is +//! GCM restricted to `P = ""` -- with AAD lengths of 128/192/256 bits, both directions, all three +//! key lengths, 96- and 128-bit tags. + +// See `acvp_gcm_tests.rs` for why this is its own module path rather than `mod common;`. +#[path = "common/acvp_gcm.rs"] +mod acvp_gcm; + +use acvp_gcm::{GCM_NONCE_LEN, decode, run_decrypt_case, run_encrypt_case, test_data_dir}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; + +const SUBDIR: &str = "aes_tdes_vectors/GCM"; +const REQUEST_FILE: &str = "ACVP-AES-GMAC.4014543.req.json"; +const RESPONSE_FILE: &str = "ACVP-AES-GMAC.4014543.rsp.json"; + +#[test] +fn acvp_aes_gmac_known_answer_tests() { + let Some(dir) = test_data_dir(SUBDIR, REQUEST_FILE, RESPONSE_FILE) else { return }; + + let req: Value = serde_json::from_str( + &fs::read_to_string(dir.join(REQUEST_FILE)).expect("readable request file"), + ) + .expect("valid ACVP request JSON"); + let rsp: Value = serde_json::from_str( + &fs::read_to_string(dir.join(RESPONSE_FILE)).expect("readable response file"), + ) + .expect("valid ACVP response JSON"); + + let mut answers: BTreeMap = BTreeMap::new(); + for group in rsp[1]["testGroups"].as_array().expect("response testGroups") { + for test in group["tests"].as_array().expect("response tests") { + let tc_id = test["tcId"].as_u64().expect("tcId"); + answers.insert(tc_id, test.clone()); + } + } + + let groups = req[1]["testGroups"].as_array().expect("request testGroups"); + + let mut checked = 0usize; + let mut encrypt_checked = 0usize; + let mut decrypt_failed_checked = 0usize; + let mut per_kind: BTreeMap = BTreeMap::new(); + + for group in groups { + let direction = group["direction"].as_str().expect("direction"); + let tag_len = (group["tagLen"].as_u64().expect("tagLen") / 8) as usize; + let iv_len = group["ivLen"].as_u64().expect("ivLen"); + assert_eq!(iv_len, 96, "every group in this set has a 96-bit IV"); + let payload_len = group["payloadLen"].as_u64().expect("payloadLen"); + assert_eq!(payload_len, 0, "GMAC groups carry no plaintext"); + + for test in group["tests"].as_array().expect("tests") { + let tc_id = test["tcId"].as_u64().expect("tcId"); + let key_bytes = decode(test, "key", tc_id); + let aad = decode(test, "aad", tc_id); + let iv_bytes = decode(test, "iv", tc_id); + let iv: [u8; GCM_NONCE_LEN] = iv_bytes + .try_into() + .unwrap_or_else(|_| panic!("tcId {tc_id}: expected a 12-byte IV")); + + match direction { + "encrypt" => { + let answer = + answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + let tag = decode(answer, "tag", tc_id); + // A GMAC "ciphertext" is always empty. + run_encrypt_case(&key_bytes, iv, &aad, &[], tag_len, &[], &tag); + run_decrypt_case(&key_bytes, iv, &aad, &[], &tag, Some(&[])); + encrypt_checked += 1; + } + "decrypt" => { + let tag = decode(test, "tag", tc_id); + let answer = + answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + // See `acvp_gcm_tests.rs`: a forgery reports `testPassed: false`; a valid case + // reports success with no `testPassed` field at all (here there is no `pt` to + // report either, since GMAC's plaintext is always empty). + if answer.get("testPassed").and_then(Value::as_bool) == Some(false) { + run_decrypt_case(&key_bytes, iv, &aad, &[], &tag, None); + decrypt_failed_checked += 1; + } else { + run_decrypt_case(&key_bytes, iv, &aad, &[], &tag, Some(&[])); + } + } + other => panic!("unexpected direction {other}"), + } + + *per_kind.entry(format!("AES-{} {direction}", key_bytes.len() * 8)).or_default() += 1; + checked += 1; + } + } + + for (kind, n) in &per_kind { + println!("ACVP AES-GMAC {kind}: {n} cases"); + } + println!( + "ACVP AES-GMAC: {checked} cases checked ({encrypt_checked} encrypt, also round-tripped \ + through decrypt; {decrypt_failed_checked} decrypt forgeries)" + ); + + assert_eq!(checked, 270, "expected all 270 ACVP AES-GMAC cases to run"); + assert!(encrypt_checked > 0 && decrypt_failed_checked > 0, "expected both directions covered"); +} diff --git a/crypto/modes/tests/common/acvp_gcm.rs b/crypto/modes/tests/common/acvp_gcm.rs new file mode 100644 index 00000000..c1a56853 --- /dev/null +++ b/crypto/modes/tests/common/acvp_gcm.rs @@ -0,0 +1,232 @@ +//! Shared plumbing for the ACVP AES-GCM and AES-GMAC known-answer test files +//! (`acvp_gcm_tests.rs`, `acvp_gmac_tests.rs`), whose request/response JSON shape is identical +//! between the two: GMAC is just the `payloadLen = 0` slice of the same ACVP AES-GCM protocol +//! (SP 800-38D Sec 5.2: GMAC is GCM restricted to `P = ""`). +//! +//! 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, callers print a warning and skip, +//! matching the convention the other ACVP suites in this crate use. + +#![allow(dead_code)] + +use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Decrypting, Encrypting, Gcm}; +use serde_json::Value; +use std::path::{Path, PathBuf}; + +/// The nonce length these vectors use; every group in the ACVP AES-GCM/GMAC sets has `ivLen = 96`. +pub const GCM_NONCE_LEN: usize = 12; + +/// Finds the directory holding `req_file` and `rsp_file` under either of the two candidate roots +/// this crate's other ACVP suites use, or `None` (with a printed warning) if neither has both. +pub fn test_data_dir(subdir: &str, req_file: &str, rsp_file: &str) -> Option { + let candidates = [ + format!("../../../bc-test-data/crypto/{subdir}"), + format!("../bc-test-data/crypto/{subdir}"), + ]; + for candidate in &candidates { + let path = Path::new(candidate); + if path.join(req_file).exists() && path.join(rsp_file).exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {candidates:?}); \ + this suite will be skipped" + ); + None +} + +/// Builds a `KeyMaterial` from raw ACVP key bytes, including the all-zero keys the set includes +/// deliberately: `KeyMaterial` tags an all-zero buffer as `KeyType::Zeroized` and will not promote +/// it outside a `do_hazardous_operations` closure, so this opts in explicitly. +pub fn cipher_key(bytes: &[u8]) -> KeyMaterial { + assert_eq!(bytes.len(), N, "key length should match the parameter set"); + let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("ACVP key bytes fit the buffer"); + if key.key_type() != KeyType::SymmetricCipherKey { + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::from_bytes(N)) + }) + .expect("promoting a NIST all-zero test key"); + } + key +} + +pub fn decode(value: &Value, field: &str, tc_id: u64) -> Vec { + let s = value + .get(field) + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("tcId {tc_id}: missing field {field}")); + hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc_id}: bad hex in {field}")) +} + +/// Runs one ACVP AES-GCM/GMAC encrypt case: encrypts `pt` under `key`/`aad`, driving the nonce +/// through a `FixedSeedRNG` seeded with the vector's own `iv` and asserting it is reproduced +/// exactly (so a change that ignored the RNG could not pass silently), then compares the resulting +/// ciphertext and tag against the response file's `ct`/`tag`. +pub fn run_encrypt_case( + key_bytes: &[u8], + iv: [u8; GCM_NONCE_LEN], + aad: &[u8], + pt: &[u8], + tag_len: usize, + expected_ct: &[u8], + expected_tag: &[u8], +) { + macro_rules! dispatch { + ($p:ty, $klen:literal) => {{ + let key = cipher_key::<$klen>(key_bytes); + let mut data = pt.to_vec(); + match tag_len { + 12 => run_encrypt::<$p, $klen, 12>(&key, iv, aad, &mut data, expected_tag), + 13 => run_encrypt::<$p, $klen, 13>(&key, iv, aad, &mut data, expected_tag), + 14 => run_encrypt::<$p, $klen, 14>(&key, iv, aad, &mut data, expected_tag), + 15 => run_encrypt::<$p, $klen, 15>(&key, iv, aad, &mut data, expected_tag), + 16 => run_encrypt::<$p, $klen, 16>(&key, iv, aad, &mut data, expected_tag), + other => panic!("unsupported ACVP tagLen {other} bytes"), + } + assert_eq!(data, expected_ct); + }}; + } + match key_bytes.len() { + 16 => dispatch!(AES128Internal, 16), + 24 => dispatch!(AES192Internal, 24), + 32 => dispatch!(AES256Internal, 32), + other => panic!("unexpected AES key length {other}"), + } +} + +fn run_encrypt( + key: &KeyMaterial, + iv: [u8; GCM_NONCE_LEN], + aad: &[u8], + data: &mut [u8], + expected_tag: &[u8], +) where + P: bouncycastle_core::traits::ElectronicCodeBook, +{ + let mut ct = vec![0u8; data.len()]; + let (got_iv, written, tag) = Gcm::::encrypt_out_rng_detached( + key, + &mut FixedSeedRNG::::new(iv), + aad, + data, + &mut ct, + ) + .expect("encrypt"); + assert_eq!(got_iv, iv, "the pinned RNG should reproduce the vector's IV"); + assert_eq!(written, data.len(), "GCM ciphertext is as long as the plaintext"); + assert_eq!(&tag[..], expected_tag, "tag mismatch"); + data.copy_from_slice(&ct); +} + +/// Runs one ACVP AES-GCM/GMAC decrypt case: decrypts `ct` under `key`/`aad`/`iv` and either +/// compares against `expected_pt` (a valid case) or asserts `AEADTagCheckFailed` (a forgery) from +/// both the detached one-shot and the inline stream, with the one-shot's plaintext buffer zeroized. +pub fn run_decrypt_case( + key_bytes: &[u8], + iv: [u8; GCM_NONCE_LEN], + aad: &[u8], + ct: &[u8], + tag: &[u8], + expected_pt: Option<&[u8]>, +) { + macro_rules! dispatch { + ($p:ty, $klen:literal) => {{ + let key = cipher_key::<$klen>(key_bytes); + match tag.len() { + 12 => run_decrypt::<$p, $klen, 12>(&key, iv, aad, ct, tag, expected_pt), + 13 => run_decrypt::<$p, $klen, 13>(&key, iv, aad, ct, tag, expected_pt), + 14 => run_decrypt::<$p, $klen, 14>(&key, iv, aad, ct, tag, expected_pt), + 15 => run_decrypt::<$p, $klen, 15>(&key, iv, aad, ct, tag, expected_pt), + 16 => run_decrypt::<$p, $klen, 16>(&key, iv, aad, ct, tag, expected_pt), + other => panic!("unsupported ACVP tagLen {other} bytes"), + } + }}; + } + match key_bytes.len() { + 16 => dispatch!(AES128Internal, 16), + 24 => dispatch!(AES192Internal, 24), + 32 => dispatch!(AES256Internal, 32), + other => panic!("unexpected AES key length {other}"), + } +} + +fn run_decrypt( + key: &KeyMaterial, + iv: [u8; GCM_NONCE_LEN], + aad: &[u8], + ct: &[u8], + tag: &[u8], + expected_pt: Option<&[u8]>, +) where + P: bouncycastle_core::traits::ElectronicCodeBook, +{ + let tag_arr: [u8; TAG_LEN] = tag.try_into().expect("tag length matches TAG_LEN"); + + // The detached one-shot: AAD-capable, and never releases plaintext before the tag checks out. + let mut data = vec![0xEEu8; ct.len()]; + let one_shot_result = Gcm::::decrypt_out_detached( + key, &iv, aad, ct, &tag_arr, &mut data, + ); + + // The inline `SymmetricCipherDecryptor` streaming view, `ciphertext || tag` through + // `do_update_out`/`do_final`, with AAD fed via `do_update_aad` first. Note this is + // *not* the AAD-less static `decrypt_out` one-shot (which has no AAD parameter at all and so + // cannot be checked against these vectors, none of which have empty AAD): the streaming path + // is where the inline layout meets AAD support, and unlike the one-shot it releases plaintext + // before the tag is checked -- see `gcm_tests.rs` for that distinction pinned with empty AAD. + let mut dec = + Gcm::::do_decrypt_init(key, &iv).expect("decrypt init"); + dec.do_update_aad(aad).expect("aad"); + let mut inline_ct = ct.to_vec(); + inline_ct.extend_from_slice(tag); + let expect_written = dec.update_out_len(inline_ct.len()); + let mut inline_pt = vec![0u8; expect_written]; + let written = dec + .do_update_out(&inline_ct, &mut inline_pt) + .expect("do_update_out on a correctly sized buffer must not fail"); + assert_eq!(written, expect_written, "update_out_len must be exact"); + let inline_result = dec.do_final(); + + match expected_pt { + Some(pt) => { + assert!( + one_shot_result.is_ok(), + "detached one-shot should have verified: {one_shot_result:?}" + ); + assert_eq!(data, pt, "detached one-shot plaintext mismatch"); + + assert!(inline_result.is_ok(), "inline stream should have verified: {inline_result:?}"); + assert_eq!(written, pt.len(), "inline stream released the wrong length"); + assert_eq!(&inline_pt[..written], pt, "inline stream plaintext mismatch"); + } + None => { + assert!( + matches!(one_shot_result, Err(SymmetricCipherError::AEADTagCheckFailed)), + "expected AEADTagCheckFailed from the detached one-shot, got {one_shot_result:?}" + ); + assert!( + data.iter().all(|&b| b == 0), + "a forged tag must leave the one-shot buffer zeroized" + ); + + assert!( + matches!(inline_result, Err(SymmetricCipherError::AEADTagCheckFailed)), + "expected AEADTagCheckFailed from the inline stream's do_final, got {inline_result:?}" + ); + } + } +} diff --git a/crypto/modes/tests/gcm_bc_java_tests.rs b/crypto/modes/tests/gcm_bc_java_tests.rs new file mode 100644 index 00000000..cbc5b100 --- /dev/null +++ b/crypto/modes/tests/gcm_bc_java_tests.rs @@ -0,0 +1,247 @@ +//! Cross-implementation tests against BC Java's `GCMTest.java` `TEST_VECTORS` table +//! (`core/src/test/java/org/bouncycastle/crypto/test/GCMTest.java`), which is itself a transcription +//! of the McGrew/Viega "The Galois/Counter Mode of Operation (GCM)" Appendix B test vectors. +//! +//! Only the cases whose IV is 96 bits are usable here (D2 / the implementation plan): of the 18 +//! vectors, cases 5, 11 and 17 use a 64-bit IV and cases 6, 12 and 18 use a 480-bit IV, both of +//! which exercise the `len(IV) != 96` GHASH-derived-`J0` branch of Algorithm 4 step 2 that this +//! crate does not implement. The remaining twelve (1, 2, 3, 4, 7, 8, 9, 10, 13, 14, 15, 16) are +//! transcribed below, verified against the bc-java source read this session, with all-zero fields +//! built programmatically rather than typed out (a zero key or plaintext cannot be mistyped). + +use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Decrypting, Encrypting, Gcm}; + +fn zeros(byte_len: usize) -> String { + "00".repeat(byte_len) +} + +/// One BC Java `TEST_VECTORS` row: (name, key, plaintext, aad, iv, expected ciphertext, expected +/// tag), all as hex strings. +struct Case { + name: &'static str, + key: String, + pt: String, + aad: &'static str, + iv: &'static str, + ct: String, + tag: &'static str, +} + +fn cases() -> Vec { + let k128 = "feffe9928665731c6d6a8f9467308308".to_string(); + let k192 = format!("{k128}feffe9928665731c"); + let k256 = format!("{k128}{k128}"); + + let p_full = "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255" + .to_string(); + let p_partial = "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72\ + 1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39" + .to_string(); + let aad = "feedfacedeadbeeffeedfacedeadbeefabaddad2"; + let iv_zero = "000000000000000000000000"; + let iv_cafe = "cafebabefacedbaddecaf888"; + + let c3_full = "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e\ + 21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f5985" + .to_string(); + let c4_partial = "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e\ + 21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091" + .to_string(); + let c9_full = "3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c\ + 7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710acade256" + .to_string(); + let c10_partial = "3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c\ + 7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710" + .to_string(); + let c15_full = "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa\ + 8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad" + .to_string(); + let c16_partial = "522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa\ + 8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662" + .to_string(); + + vec![ + Case { + name: "Test Case 1", + key: zeros(16), + pt: String::new(), + aad: "", + iv: iv_zero, + ct: String::new(), + tag: "58e2fccefa7e3061367f1d57a4e7455a", + }, + Case { + name: "Test Case 2", + key: zeros(16), + pt: zeros(16), + aad: "", + iv: iv_zero, + ct: "0388dace60b6a392f328c2b971b2fe78".to_string(), + tag: "ab6e47d42cec13bdf53a67b21257bddf", + }, + Case { + name: "Test Case 3", + key: k128.clone(), + pt: p_full.clone(), + aad: "", + iv: iv_cafe, + ct: c3_full, + tag: "4d5c2af327cd64a62cf35abd2ba6fab4", + }, + Case { + name: "Test Case 4", + key: k128.clone(), + pt: p_partial.clone(), + aad, + iv: iv_cafe, + ct: c4_partial, + tag: "5bc94fbc3221a5db94fae95ae7121a47", + }, + Case { + name: "Test Case 7", + key: zeros(24), + pt: String::new(), + aad: "", + iv: iv_zero, + ct: String::new(), + tag: "cd33b28ac773f74ba00ed1f312572435", + }, + Case { + name: "Test Case 8", + key: zeros(24), + pt: zeros(16), + aad: "", + iv: iv_zero, + ct: "98e7247c07f0fe411c267e4384b0f600".to_string(), + tag: "2ff58d80033927ab8ef4d4587514f0fb", + }, + Case { + name: "Test Case 9", + key: k192.clone(), + pt: p_full.clone(), + aad: "", + iv: iv_cafe, + ct: c9_full, + tag: "9924a7c8587336bfb118024db8674a14", + }, + Case { + name: "Test Case 10", + key: k192.clone(), + pt: p_partial.clone(), + aad, + iv: iv_cafe, + ct: c10_partial, + tag: "2519498e80f1478f37ba55bd6d27618c", + }, + Case { + name: "Test Case 13", + key: zeros(32), + pt: String::new(), + aad: "", + iv: iv_zero, + ct: String::new(), + tag: "530f8afbc74536b9a963b4f1c4cb738b", + }, + Case { + name: "Test Case 14", + key: zeros(32), + pt: zeros(16), + aad: "", + iv: iv_zero, + ct: "cea7403d4d606b6e074ec5d3baf39d18".to_string(), + tag: "d0d1c8a799996bf0265b98b5d48ab919", + }, + Case { + name: "Test Case 15", + key: k256.clone(), + pt: p_full, + aad: "", + iv: iv_cafe, + ct: c15_full, + tag: "b094dac5d93471bdec1a502270e3cc6c", + }, + Case { + name: "Test Case 16", + key: k256, + pt: p_partial, + aad, + iv: iv_cafe, + ct: c16_partial, + tag: "76fc6ece0f4e1768cddf8853bb2d551b", + }, + ] +} + +fn run(case: &Case) +where + P: bouncycastle_core::traits::ElectronicCodeBook, +{ + let key_bytes = hex::decode(&case.key).expect("valid hex key"); + // `KeyMaterial` tags an all-zero buffer as `KeyType::Zeroized` regardless of the type + // requested, and will not promote it outside a `do_hazardous_operations` closure. The + // zero-key cases (1, 2, 7, 8, 13, 14) need that opt-in, same as the ACVP suites' `cipher_key`. + let mut key = + KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .expect("key bytes fit the buffer"); + if key.key_type() != KeyType::SymmetricCipherKey { + bouncycastle_core::key_material::do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength( + bouncycastle_core::security_strength::SecurityStrength::from_bytes(KEY_LEN), + ) + }) + .expect("promoting a known-zero test key"); + } + + let aad = hex::decode(case.aad).expect("valid hex aad"); + let pt = hex::decode(&case.pt).expect("valid hex pt"); + let iv_bytes = hex::decode(case.iv).expect("valid hex iv"); + let iv: [u8; 12] = iv_bytes.try_into().expect("a 96-bit IV"); + let expected_ct = hex::decode(&case.ct).expect("valid hex ct"); + let expected_tag = hex::decode(case.tag).expect("valid hex tag"); + + let mut data = vec![0u8; pt.len()]; + let (got_iv, _, tag) = Gcm::::encrypt_out_rng_detached( + &key, + &mut FixedSeedRNG::<12>::new(iv), + &aad, + &pt, + &mut data, + ) + .expect("encrypt"); + assert_eq!(got_iv, iv, "{}: the pinned RNG should reproduce the vector's IV", case.name); + + assert_eq!(data, expected_ct, "{}: ciphertext mismatch", case.name); + assert_eq!(&tag[..], &expected_tag[..], "{}: tag mismatch", case.name); + + let tag_arr: [u8; 16] = expected_tag.try_into().expect("16-byte tag"); + let mut recovered = vec![0u8; data.len()]; + Gcm::::decrypt_out_detached( + &key, &iv, &aad, &data, &tag_arr, &mut recovered, + ) + .unwrap_or_else(|e| panic!("{}: decrypt should have verified, got {e:?}", case.name)); + assert_eq!(recovered, pt, "{}: decrypted plaintext mismatch", case.name); +} + +#[test] +fn bc_java_test_vectors_with_a_96_bit_iv() { + let mut checked = 0usize; + for case in cases() { + let key_len_bytes = case.key.len() / 2; + match key_len_bytes { + 16 => run::(&case), + 24 => run::(&case), + 32 => run::(&case), + other => panic!("{}: unexpected key length {other} bytes", case.name), + } + checked += 1; + } + println!("bc-java GCMTest 96-bit-IV vectors: {checked} cases checked"); + assert_eq!(checked, 12, "expected the twelve 96-bit-IV McGrew/Viega vectors"); +} diff --git a/crypto/modes/tests/gcm_tests.rs b/crypto/modes/tests/gcm_tests.rs new file mode 100644 index 00000000..5a70b47f --- /dev/null +++ b/crypto/modes/tests/gcm_tests.rs @@ -0,0 +1,337 @@ +//! Structural tests for GCM, driven by a toy permutation and by real AES. +//! +//! These check the properties of the *mode* -- AAD-before-data ordering, chunking independence, +//! the tag-length family, the inline decryptor's tail hold-back, and the one-shot's +//! verify-before-decrypt guarantee -- independently of (or alongside) the ACVP/bc-java known-answer +//! vectors in `acvp_gcm_tests.rs`, `acvp_gmac_tests.rs` and `gcm_bc_java_tests.rs`. + +mod common; + +use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_modes::{Decrypting, Encrypting, Gcm}; +use common::{TOY_LEN, Toy, toy_key}; + +type ToyGcm = Gcm; + +/// Encrypts `message` under `aad` through the detached one-shot, with the nonce driven by `seed` +/// so repeated calls are comparable. Returns the nonce, the ciphertext and the tag. +fn toy_encrypt( + aad: &[u8], + message: &[u8], + seed: [u8; 12], +) -> ([u8; 12], Vec, [u8; TAG_LEN]) { + let mut ct = vec![0u8; message.len()]; + let (nonce, _, tag) = ToyGcm::::encrypt_out_rng_detached( + &toy_key(), + &mut FixedSeedRNG::<12>::new(seed), + aad, + message, + &mut ct, + ) + .unwrap(); + (nonce, ct, tag) +} + +/// AAD must precede data (SP 800-38D Algorithm 4 absorbs `A` before `C`); a non-empty AAD call +/// after data has started is refused, while an empty one is always accepted as a no-op. +#[test] +fn aad_after_data_is_a_state_error_unless_empty() { + let key = toy_key(); + let (mut enc, _nonce) = ToyGcm::::do_encrypt_init(&key).unwrap(); + enc.do_update_aad(b"header").unwrap(); + let mut out = [0u8; 8]; + enc.do_update_out(&[0x11u8; 8], &mut out).unwrap(); + + match enc.do_update_aad(b"too late") { + Err(SymmetricCipherError::StateError(_)) => {} + other => panic!("expected StateError, got {other:?}"), + } + // An empty call after data is always fine. + enc.do_update_aad(&[]).unwrap(); + let _ = enc.do_final_detached().unwrap(); +} + +/// The decryptor holds back the last `TAG_LEN` bytes it has seen, so a first `do_update_out` of +/// fewer than `TAG_LEN` bytes releases nothing -- but data has still started, and AAD after it +/// must be refused all the same, or it would be absorbed as if it came before the ciphertext. +#[test] +fn aad_after_held_back_data_is_still_a_state_error() { + let key = toy_key(); + let mut dec = ToyGcm::::do_decrypt_init(&key, &[0u8; 12]).unwrap(); + let mut nothing = [0u8; 0]; + assert_eq!(dec.do_update_out(&[0x22u8; 5], &mut nothing).unwrap(), 0, "all held back"); + match dec.do_update_aad(b"too late") { + Err(SymmetricCipherError::StateError(_)) => {} + other => panic!("expected StateError, got {other:?}"), + } +} + +/// Chunking independence for both AAD and data: every split of a 40-byte AAD and a 50-byte message +/// must give the same ciphertext and tag as absorbing each in one call. +#[test] +fn chunking_is_independent_for_aad_and_data() { + let key = toy_key(); + let aad: [u8; 40] = core::array::from_fn(|i| i as u8); + let message: [u8; 50] = core::array::from_fn(|i| (i as u8).wrapping_mul(3).wrapping_add(1)); + let seed = [0x5Au8; 12]; + let (nonce, expected_ct, expected_tag) = toy_encrypt::<16>(&aad, &message, seed); + + for aad_split in [0usize, 1, 17, 40] { + for data_split in [0usize, 1, 23, 50] { + let (mut enc, got_nonce) = ToyGcm::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::<12>::new(seed), + ) + .unwrap(); + assert_eq!(got_nonce, nonce); + enc.do_update_aad(&aad[..aad_split]).unwrap(); + enc.do_update_aad(&aad[aad_split..]).unwrap(); + let mut ct = [0u8; 50]; + let n = enc.do_update_out(&message[..data_split], &mut ct).unwrap(); + enc.do_update_out(&message[data_split..], &mut ct[n..]).unwrap(); + let (_, _, tag) = enc.do_final_detached().unwrap(); + assert_eq!(&ct[..], &expected_ct[..], "aad_split {aad_split}, data_split {data_split}"); + assert_eq!(tag, expected_tag, "aad_split {aad_split}, data_split {data_split}"); + } + } +} + +/// Tag-length variants 12..=16 all round-trip, and the 12-byte tag is a prefix of the 16-byte tag +/// for the same inputs -- Algorithm 4 step 6's `T = MSB_t(...)`. +#[test] +fn tag_length_variants_round_trip_and_nest() { + let key = toy_key(); + let aad = b"associated"; + let message = *b"a toy message, sixteen+"; + let seed = [0x6Bu8; 12]; + let (_, ct16, tag16) = toy_encrypt::<16>(aad, &message, seed); + + macro_rules! check_tag_len { + ($n:literal) => {{ + let (nonce, ct, tag) = toy_encrypt::<$n>(aad, &message, seed); + assert_eq!(ct, ct16, "ciphertext must not depend on TAG_LEN ({})", $n); + assert_eq!( + &tag16[..$n], + &tag[..], + "TAG_LEN={} must be a prefix of the 16-byte tag", + $n + ); + let mut pt = [0u8; 23]; + ToyGcm::::decrypt_out_detached(&key, &nonce, aad, &ct, &tag, &mut pt) + .unwrap(); + assert_eq!(pt, message); + }}; + } + check_tag_len!(12); + check_tag_len!(13); + check_tag_len!(14); + check_tag_len!(15); + check_tag_len!(16); +} + +/// GMAC: an all-AAD message (no plaintext at all) still produces a valid tag, and decrypting zero +/// bytes of ciphertext against it verifies. Sec 5.2: GMAC is GCM restricted to `P = ""`. +#[test] +fn an_aad_only_message_is_gmac() { + let key = toy_key(); + let aad = b"the whole message is AAD"; + let (nonce, _, tag) = toy_encrypt::<16>(aad, &[], [0x7Cu8; 12]); + ToyGcm::::decrypt_out_detached(&key, &nonce, aad, &[], &tag, &mut []).unwrap(); + + // Wrong AAD must fail verification. + match ToyGcm::::decrypt_out_detached(&key, &nonce, b"wrong", &[], &tag, &mut []) + { + Err(SymmetricCipherError::AEADTagCheckFailed) => {} + other => panic!("expected AEADTagCheckFailed, got {other:?}"), + } +} + +/// The inline decryptor: input of exactly `TAG_LEN` bytes decrypts to nothing and verifies; input +/// shorter than `TAG_LEN` is `DecryptionFailed`. +#[test] +fn inline_decryptor_handles_short_and_tag_only_input() { + let key = toy_key(); + let (nonce, _, tag) = toy_encrypt::<16>(b"", &[], [0x8Du8; 12]); + + let mut plaintext = [0u8; 16]; + let n = ToyGcm::::decrypt_out(&key, &nonce, &tag, &mut plaintext).unwrap(); + assert_eq!(n, 0, "a tag-only input releases no plaintext"); + + for short_len in 0..16 { + let short = &tag[..short_len]; + match ToyGcm::::decrypt_out(&key, &nonce, short, &mut plaintext) { + Err(SymmetricCipherError::DecryptionFailed) => {} + other => panic!("len {short_len}: expected DecryptionFailed, got {other:?}"), + } + } +} + +/// `update_out_len` must be exact across an irregular sequence of call sizes that walks through +/// the tail hold-back boundary. +#[test] +fn update_out_len_is_exact_across_irregular_chunking() { + let key = toy_key(); + let message: [u8; 64] = core::array::from_fn(|i| i as u8); + let (nonce, ct, tag) = toy_encrypt::<16>(b"aad", &message, [0x9Eu8; 12]); + let mut full_ct = [0u8; 80]; + full_ct[..64].copy_from_slice(&ct); + full_ct[64..].copy_from_slice(&tag); + + let mut dec = ToyGcm::::do_decrypt_init(&key, &nonce).unwrap(); + dec.do_update_aad(b"aad").unwrap(); + let mut released = 0usize; + for chunk in [1usize, 15, 16, 17, 31] { + let piece = &full_ct[released.min(full_ct.len())..(released + chunk).min(full_ct.len())]; + if piece.is_empty() { + continue; + } + let expect = dec.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "chunk {chunk}"); + released += piece.len(); + } + // Drain whatever remains. + let rest = &full_ct[released..]; + let expect = dec.update_out_len(rest.len()); + let mut buf = vec![0u8; expect]; + dec.do_update_out(rest, &mut buf).unwrap(); + let (_last, last_len) = dec.do_final().unwrap(); + assert_eq!(last_len, 0); +} + +/// A forged tag leaves the one-shot's output buffer zeroized, while the streaming path (by its +/// nature) has already released plaintext before the forgery is detected. Pinning the difference. +#[test] +fn one_shot_releases_nothing_on_forgery_but_streaming_does() { + let key = toy_key(); + let message = *b"do not trust me yet"; + let (nonce, ct, mut tag) = toy_encrypt::<16>(b"aad", &message, [0xAFu8; 12]); + tag[0] ^= 0xFF; // forge it + + // One-shot: verify-then-decrypt, so a forged tag leaves nothing but zeros behind. + let mut one_shot_buf = [0xEEu8; 19]; + match ToyGcm::::decrypt_out_detached( + &key, &nonce, b"aad", &ct, &tag, &mut one_shot_buf, + ) { + Err(SymmetricCipherError::AEADTagCheckFailed) => {} + other => panic!("expected AEADTagCheckFailed, got {other:?}"), + } + assert_eq!(one_shot_buf, [0u8; 19], "the one-shot must zeroize its buffer on a forged tag"); + + // Streaming: everything but the held-back last 16 bytes has already been released as + // plaintext by the time the final call rejects the tag. + let mut dec = ToyGcm::::do_decrypt_init(&key, &nonce).unwrap(); + dec.do_update_aad(b"aad").unwrap(); + let mut streaming_buf = [0u8; 19]; + let released = dec.do_update_out(&ct, &mut streaming_buf).unwrap(); + assert_eq!(released, 3, "19 bytes in, the last 16 held back"); + assert_eq!(&streaming_buf[..3], &message[..3], "streaming already produced plaintext"); + match dec.do_final_detached(&tag) { + Err(SymmetricCipherError::AEADTagCheckFailed) => {} + other => panic!("expected AEADTagCheckFailed, got {other:?}"), + } +} + +/// The one-shots round-trip with real AES at all three key lengths, at a length that is not a +/// whole number of blocks. +#[test] +fn the_aes_aliases_round_trip() { + fn check(key_bytes: &[u8]) + where + P: bouncycastle_core::traits::ElectronicCodeBook, + { + let key = + KeyMaterial::::from_bytes_as_type(key_bytes, KeyType::SymmetricCipherKey) + .unwrap(); + let aad = b"associated data of no particular length"; + let message = b"a message that is not a whole number of blocks!!"; + + let mut ct = [0u8; 48]; + let (nonce, _, tag) = + Gcm::::encrypt_out_detached(&key, aad, message, &mut ct) + .unwrap(); + assert_ne!(&ct[..], &message[..]); + let mut pt = [0u8; 48]; + Gcm::::decrypt_out_detached( + &key, &nonce, aad, &ct, &tag, &mut pt, + ) + .unwrap(); + assert_eq!(&pt[..], &message[..]); + } + + check::(&[0x11; 16]); + check::(&[0x22; 24]); + check::(&[0x33; 32]); +} + +/// The whole [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] contract -- which runs the +/// symmetric-cipher suite first -- through the shared framework, over real AES at two key lengths +/// and at both ends of the tag-length range. `FINAL_LEN` is `TAG_LEN`: GCM holds nothing back on +/// encryption and exactly the possible tag on decryption. +/// +/// [`AEADCipherEncryptor`]: bouncycastle_core::traits::AEADCipherEncryptor +/// [`AEADCipherDecryptor`]: bouncycastle_core::traits::AEADCipherDecryptor +#[test] +fn aead_trait_framework() { + use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkAEADCipher; + TestFrameworkAEADCipher::new().test_encryptor_decryptor::< + 16, + 12, + 16, + 16, + Gcm, + Gcm, + >(); + TestFrameworkAEADCipher::new().test_encryptor_decryptor::< + 32, + 12, + 12, + 12, + Gcm, + Gcm, + >(); +} + +/// The trait one-shots check the tag before decrypting anything, as the inherent +/// `decrypt_detached` does, and on a forgery leave the caller's buffer zeroized -- the trait +/// contract -- rather than holding the ciphertext they staged there. +#[test] +fn aead_trait_one_shots_release_nothing_on_forgery() { + type Enc = Gcm; + type Dec = Gcm; + + let key = + KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); + let mut ct = [0u8; 32 + 16]; + let (nonce, n) = Enc::encrypt_out_with_aad(&key, b"aad", &[0x33u8; 32], &mut ct).unwrap(); + ct[0] ^= 1; + + let mut out = [0xEEu8; 32]; + assert!(matches!( + Dec::decrypt_out_with_aad(&key, &nonce, b"aad", &ct[..n], &mut out), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(out, [0u8; 32], "decrypt_out_with_aad must zeroize on a failed tag check"); + + let tag: [u8; 16] = ct[32..48].try_into().unwrap(); + let mut out = [0xEEu8; 32]; + assert!(matches!( + >::decrypt_out_detached( + &key, + &nonce, + b"aad", + &ct[..32], + &tag, + &mut out + ), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(out, [0u8; 32], "decrypt_out_detached must zeroize on a failed tag check"); +} diff --git a/crypto/modes/tests/sp800_38c_tests.rs b/crypto/modes/tests/sp800_38c_tests.rs new file mode 100644 index 00000000..c6431c5d --- /dev/null +++ b/crypto/modes/tests/sp800_38c_tests.rs @@ -0,0 +1,901 @@ +//! The four AES-CCM example vectors of NIST SP 800-38C Appendix C, and the streaming and +//! error-path properties that go with them. +//! +//! The vectors are transcribed from the errata-updated (07-20-2007) PDF of the recommendation. +//! Appendix C: "four examples are provided for the encryption-generation process of CCM with the +//! formatting and counter generation functions that are specified in Appendix A. The underlying +//! block cipher algorithm is the AES algorithm under a key of 128 bits." All four share one key +//! and differ in every length, which is what makes them worth having all four of: between them +//! they cover `t` of 4, 6, 8 and 14 and `q` of 8, 7, 3 and 2, i.e. both ends of each of A.1's +//! ranges. +//! +//! Appendix C prints `C` as a single string, which is Sec 6.1 step 8's +//! `(P XOR MSB_Plen(S)) || (T XOR MSB_Tlen(S0))` -- the ciphertext with the tag appended. It is +//! split here at `Plen`, and both layouts of the API are checked against the two halves. +//! +//! Appendix C gives no decryption examples ("From each example, a corresponding example of the +//! decryption-verification process of CCM is straightforward to construct"), so the decryption +//! direction is checked by round-tripping each vector's own `C` back to its `P`. + +use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkAEADCipher; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Ccm, CcmDecryptor, CcmEncryptor, Decrypting, Encrypting}; + +/// Appendix C's key, the same in all four examples: `40414243 44454647 48494a4b 4c4d4e4f`. +const APPENDIX_C_KEY: &str = "404142434445464748494a4b4c4d4e4f"; + +fn key(hex_key: &str) -> KeyMaterial { + let bytes = hex::decode(hex_key).expect("valid hex key"); + assert_eq!(bytes.len(), N, "key length must match the parameter set"); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a symmetric cipher key") +} + +/// [`SymmetricCipherError`] is deliberately not `PartialEq` -- it carries `&'static str` detail that +/// tests have no business pinning -- so these two match on the variant instead. +fn is_tag_failure(r: Result) -> bool { + matches!(r, Err(SymmetricCipherError::AEADTagCheckFailed)) +} + +fn buffer_len_error(r: Result) -> Option { + match r { + Err(SymmetricCipherError::OutputBufferTooSmall(needed)) => Some(needed), + _ => None, + } +} + +/// Drives one Appendix C example through every entry point, in both layouts and both directions. +/// +/// `c` is the appendix's whole `C` string; it is split at `plaintext.len()` into the ciphertext and +/// the tag, so a mistake in either half is caught, and so is a mistake in where the split belongs. +fn check_vector< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + P: bouncycastle_core::traits::ElectronicCodeBook, +>( + name: &str, + key_hex: &str, + nonce_hex: &str, + aad: &[u8], + plaintext_hex: &str, + c_hex: &str, +) { + type Enc = Ccm; + type Dec = Ccm; + + let k = key::(key_hex); + let nonce_bytes = hex::decode(nonce_hex).expect("valid hex nonce"); + let nonce: [u8; NONCE_LEN] = nonce_bytes.try_into().expect("nonce length matches NONCE_LEN"); + let plaintext = hex::decode(plaintext_hex).expect("valid hex plaintext"); + let c = hex::decode(c_hex).expect("valid hex C"); + + assert_eq!( + c.len(), + plaintext.len() + TAG_LEN, + "{name}: the appendix's C must be Plen + Tlen octets" + ); + let (want_ct, want_tag) = c.split_at(plaintext.len()); + + // --- Sec 6.1, detached tag --- + let mut ct = vec![0u8; plaintext.len()]; + let (written, tag) = Enc::::encrypt_detached( + &k, &nonce, aad, &plaintext, &mut ct, + ) + .expect("encryption"); + assert_eq!(written, plaintext.len(), "{name}: CCM never expands the payload"); + assert_eq!(ct, want_ct, "{name}: ciphertext"); + assert_eq!(tag, want_tag, "{name}: tag"); + + // --- Sec 6.1, the appendix's own inline `ciphertext || tag` layout --- + let mut inline = vec![0u8; plaintext.len() + TAG_LEN]; + let n = + Enc::::encrypt(&k, &nonce, aad, &plaintext, &mut inline) + .expect("encryption"); + assert_eq!(n, c.len(), "{name}: inline output length"); + assert_eq!(inline, c, "{name}: the whole C string of Appendix C"); + + // --- Sec 6.2, both layouts --- + let mut recovered = vec![0u8; plaintext.len()]; + let n = Dec::::decrypt_detached( + &k, + &nonce, + aad, + want_ct, + want_tag.try_into().expect("TAG_LEN bytes"), + &mut recovered, + ) + .expect("decryption"); + assert_eq!(n, plaintext.len()); + assert_eq!(recovered, plaintext, "{name}: detached round trip"); + + let mut recovered = vec![0u8; plaintext.len()]; + let n = Dec::::decrypt(&k, &nonce, aad, &c, &mut recovered) + .expect("decryption"); + assert_eq!(n, plaintext.len()); + assert_eq!(recovered, plaintext, "{name}: inline round trip"); + + // --- Every ciphertext chunking through the streaming API gives the same answer --- + // Sec 3 says CCM is not a streaming mode, and `Ccm` handles that by taking the payload length + // up front; given that, the chunking must be invisible, exactly as for the other modes. + for chunk in [1usize, 2, 3, 7, 16, 17] { + let mut ccm = Enc::::new(&k, &nonce, aad, plaintext.len()) + .expect("streaming init"); + let mut streamed = plaintext.clone(); + for piece in streamed.chunks_mut(chunk) { + ccm.do_encrypt_update(piece).expect("update"); + } + let streamed_tag = ccm.do_encrypt_final().expect("final"); + assert_eq!(streamed, want_ct, "{name}: ciphertext, streamed in {chunk}-byte chunks"); + assert_eq!(streamed_tag, want_tag, "{name}: tag, streamed in {chunk}-byte chunks"); + + let mut ccm = Dec::::new(&k, &nonce, aad, plaintext.len()) + .expect("streaming init"); + for piece in streamed.chunks_mut(chunk) { + ccm.do_decrypt_update(piece).expect("update"); + } + ccm.do_decrypt_final(want_tag.try_into().expect("TAG_LEN bytes")).expect("tag check"); + assert_eq!(streamed, plaintext, "{name}: plaintext, streamed in {chunk}-byte chunks"); + } + + // --- Every bit of the tag is checked, and so is every byte of the ciphertext and the AAD --- + let tag_arr: &[u8; TAG_LEN] = want_tag.try_into().expect("TAG_LEN bytes"); + for i in 0..TAG_LEN { + let mut bad = *tag_arr; + bad[i] ^= 0x80; + let mut out = vec![0u8; plaintext.len()]; + assert!( + is_tag_failure(Dec::::decrypt_detached( + &k, &nonce, aad, want_ct, &bad, &mut out + )), + "{name}: a flipped bit in tag byte {i} must be caught" + ); + assert!( + out.iter().all(|b| *b == 0), + "{name}: Sec 6.2 -- the payload must not be revealed on INVALID" + ); + } + if !want_ct.is_empty() { + let mut bad_ct = want_ct.to_vec(); + bad_ct[0] ^= 0x01; + let mut out = vec![0u8; plaintext.len()]; + assert!( + is_tag_failure(Dec::::decrypt_detached( + &k, &nonce, aad, &bad_ct, tag_arr, &mut out + )), + "{name}: a modified ciphertext must be caught" + ); + } + if !aad.is_empty() { + let mut bad_aad = aad.to_vec(); + bad_aad[0] ^= 0x01; + let mut out = vec![0u8; plaintext.len()]; + assert!( + is_tag_failure(Dec::::decrypt_detached( + &k, &nonce, &bad_aad, want_ct, tag_arr, &mut out + )), + "{name}: CCM authenticates the AAD as well as the payload" + ); + } + // Truncating the AAD by one byte changes `a`, which A.2.2 encodes in front of it, so this must + // fail even though the remaining bytes are genuine. + if aad.len() > 1 { + let mut out = vec![0u8; plaintext.len()]; + assert!( + is_tag_failure(Dec::::decrypt_detached( + &k, + &nonce, + &aad[..aad.len() - 1], + want_ct, + tag_arr, + &mut out + )), + "{name}: the AAD length is authenticated, not just its contents" + ); + } + // A different nonce must fail too: it changes both `B0` and every counter block. + let mut bad_nonce = nonce; + bad_nonce[0] ^= 0x01; + let mut out = vec![0u8; plaintext.len()]; + assert!( + is_tag_failure(Dec::::decrypt_detached( + &k, &bad_nonce, aad, want_ct, tag_arr, &mut out + )), + "{name}: the nonce is authenticated" + ); +} + +/// Appendix C.1: `Klen = 128, Tlen = 32, Nlen = 56, Alen = 64, Plen = 32`. +/// +/// `n = 7`, so `q = 8`: the widest length field A.1 allows, and the shortest permitted tag. +#[test] +fn appendix_c1() { + check_vector::<16, 7, 4, AES128Internal>( + "C.1", + APPENDIX_C_KEY, + "10111213141516", + &hex::decode("0001020304050607").unwrap(), + "20212223", + // C: 7162015b 4dac255d + "7162015b4dac255d", + ); +} + +/// Appendix C.2: `Klen = 128, Tlen = 48, Nlen = 64, Alen = 128, Plen = 128`. +/// +/// `n = 8`, so `q = 7`. The payload is exactly one block, which is the case where A.2.3's +/// "minimum number of '0' bits, possibly none" is none. +#[test] +fn appendix_c2() { + check_vector::<16, 8, 6, AES128Internal>( + "C.2", + APPENDIX_C_KEY, + "1011121314151617", + &hex::decode("000102030405060708090a0b0c0d0e0f").unwrap(), + "202122232425262728292a2b2c2d2e2f", + // C: d2a1f0e0 51ea5f62 081a7792 073d593d 1fc64fbf accd + "d2a1f0e051ea5f62081a7792073d593d1fc64fbfaccd", + ); +} + +/// Appendix C.3: `Klen = 128, Tlen = 64, Nlen = 96, Alen = 160, Plen = 192`. +/// +/// `n = 12`, so `q = 3`. Both the AAD (20 bytes) and the payload (24 bytes) need zero-padding, and +/// the payload spans two counter blocks. +#[test] +fn appendix_c3() { + check_vector::<16, 12, 8, AES128Internal>( + "C.3", + APPENDIX_C_KEY, + "101112131415161718191a1b", + &hex::decode("000102030405060708090a0b0c0d0e0f10111213").unwrap(), + "202122232425262728292a2b2c2d2e2f3031323334353637", + // C: e3b201a9 f5b71a7a 9b1ceaec cd97e70b + // 6176aad9 a4428aa5 484392fb c1b09951 + "e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5484392fbc1b09951", + ); +} + +/// Appendix C.4: `Klen = 128, Tlen = 112, Nlen = 104, Alen = 524288, Plen = 256`. +/// +/// `n = 13`, so `q = 2`: the narrowest length field A.1 allows. This is the example that exercises +/// A.2.2's **six-octet** AAD length encoding, `0xff || 0xfe || [a]_32` -- `Alen` is 524288 bits, +/// i.e. `a = 65536`, which is past the `2^16 - 2^8` boundary. Nothing else in the appendix does, +/// and neither does the ACVP set, so this test is the only coverage of that branch against an +/// official answer. +/// +/// The appendix does not print `A` in full: "the given string of the first sixteen blocks of the +/// associated data string is concatenated with itself repeatedly to form a string of 524288 bits". +/// Those sixteen blocks are `00 01 02 ... ff`, so `A` is that 256-byte run repeated 256 times. +#[test] +fn appendix_c4() { + let mut aad = Vec::with_capacity(65536); + for _ in 0..256 { + aad.extend(0u8..=255u8); + } + assert_eq!(aad.len(), 65536, "Alen = 524288 bits"); + + check_vector::<16, 13, 14, AES128Internal>( + "C.4", + APPENDIX_C_KEY, + "101112131415161718191a1b1c", + &aad, + "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + // C: 69915dad 1e84c637 6a68c296 7e4dab61 + // 5ae0fd1f aec44cc4 84828529 463ccf72 + // b4ac6bec 93e8598e 7f0dadbc ea5b + "69915dad1e84c6376a68c2967e4dab615ae0fd1faec44cc484828529463ccf72\ + b4ac6bec93e8598e7f0dadbcea5b", + ); +} + +/// An empty payload and an empty AAD, which Appendix C never shows but Sec 5.3 explicitly permits: +/// "A may be the empty string", and its footnote, "The payload may also be empty, in which case +/// the specification degenerates to an authentication mode on the associated data". +/// +/// With `a = 0` and `p = 0` the formatted string is `B0` alone, so `r = 0` and the MAC is +/// `MSB_Tlen(Y0)`. There is no official vector for it; what is checked here is that all four +/// combinations of empty/non-empty are accepted, give distinct tags, and round-trip. +#[test] +fn empty_payload_and_empty_aad_are_permitted() { + type Enc = Ccm; + type Dec = Ccm; + let k = key::<16>(APPENDIX_C_KEY); + let nonce = [0x42u8; 12]; + let aad = b"header"; + let payload = b"payload"; + + let mut tags = Vec::new(); + for (a, p) in + [(&[][..], &[][..]), (&aad[..], &[][..]), (&[][..], &payload[..]), (&aad[..], &payload[..])] + { + let mut ct = vec![0u8; p.len()]; + let (written, tag) = Enc::encrypt_detached(&k, &nonce, a, p, &mut ct).expect("encryption"); + assert_eq!(written, p.len()); + + let mut back = vec![0u8; p.len()]; + let n = Dec::decrypt_detached(&k, &nonce, a, &ct, &tag, &mut back).expect("decryption"); + assert_eq!(n, p.len()); + assert_eq!(back, p, "round trip with aad {} / payload {}", a.len(), p.len()); + tags.push(tag); + } + + // An empty AAD must not be treated as the same message as a present one, nor an empty payload + // as the same as a present one: A.2.1's Adata bit and A.2.1's `Q` respectively make them + // distinct inputs to the MAC. + for i in 0..tags.len() { + for j in i + 1..tags.len() { + assert_ne!(tags[i], tags[j], "tags {i} and {j} must differ"); + } + } +} + +/// The shared framework, told the streaming capacity of a buffering pair, `FINAL_LEN - TAG_LEN`, +/// so that it caps every message it streams at that length. +fn framework(capacity: usize) -> TestFrameworkAEADCipher { + let mut framework = TestFrameworkAEADCipher::new(); + framework.max_message_len = capacity; + framework +} + +/// The whole [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] contract, through the shared +/// framework, for the buffering [`CcmEncryptor`] / [`CcmDecryptor`] pair. +/// +/// `FINAL_LEN` is 256, so the streaming capacity `FINAL_LEN - TAG_LEN` is comfortably above the +/// longest message the suite tries (`3 * FINAL_LEN + 5` in the symmetric-cipher part, capped by +/// nothing here since its one-shots bypass the buffer, and `3 * TAG_LEN + 5 = 53` in the AEAD +/// part). Everything is flushed at finalization. +#[test] +fn framework_streaming_contract() { + framework(256 - 16).test_encryptor_decryptor::< + 16, + 12, + 16, + 256, + CcmEncryptor, + CcmDecryptor, + >(); +} + +/// The same, for the other two AES key lengths and a short tag, so the framework's error and +/// key-policy checks run against every parameterization the CLI and the aliases expose. +#[test] +fn framework_streaming_contract_other_parameter_sets() { + framework(256 - 16).test_encryptor_decryptor::< + 24, + 12, + 16, + 256, + CcmEncryptor, + CcmDecryptor, + >(); + framework(256 - 16).test_encryptor_decryptor::< + 32, + 12, + 16, + 256, + CcmEncryptor, + CcmDecryptor, + >(); + // A 13-byte nonce (q = 2) with an 8-byte tag: the parameterization IEEE 802.11 CCMP uses, and + // the one A.1's narrowest length field applies to. + framework(256 - 8).test_encryptor_decryptor::< + 16, + 13, + 8, + 256, + CcmEncryptor, + CcmDecryptor, + >(); +} + +/// The buffering pair must agree with the non-buffering [`Ccm`] byte for byte -- they are two +/// routes to the same Sec 6.1 -- and it must be driven with a caller-chosen nonce to check that, +/// which is what `do_encrypt_init_rng` and a fixed-output RNG provide. +#[test] +fn the_buffering_pair_agrees_with_the_direct_api_on_appendix_c3() { + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + + let k = key::<16>(APPENDIX_C_KEY); + let nonce_bytes = hex::decode("101112131415161718191a1b").unwrap(); + let aad = hex::decode("000102030405060708090a0b0c0d0e0f10111213").unwrap(); + let plaintext = hex::decode("202122232425262728292a2b2c2d2e2f3031323334353637").unwrap(); + let c = + hex::decode("e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5484392fbc1b09951").unwrap(); + let (want_ct, want_tag) = c.split_at(plaintext.len()); + + // The trait generates the nonce; feed it Appendix C.3's so the answer is comparable, and check + // it came back, so an implementation that ignored the RNG could not pass silently. + let nonce_seed: [u8; 12] = nonce_bytes.clone().try_into().expect("12-byte nonce"); + let mut rng = FixedSeedRNG::<12>::new(nonce_seed); + let (mut enc, nonce) = Enc::do_encrypt_init_rng(&k, &mut rng).expect("init"); + assert_eq!(&nonce[..], &nonce_bytes[..], "the generated nonce must come from the RNG"); + + // Chunk both phases, and check `update_out_len`'s promise that nothing is released early. + enc.do_update_aad(&aad[..5]).expect("aad 1"); + enc.do_update_aad(&aad[5..]).expect("aad 2"); + let mut nothing = [0u8; 0]; + for piece in plaintext.chunks(7) { + assert_eq!(enc.update_out_len(piece.len()), 0, "CCM releases nothing mid-stream"); + assert_eq!(enc.do_update_out(piece, &mut nothing).expect("update"), 0); + } + let mut flushed = [0u8; 256]; + let (len, tag) = enc.do_final_out_detached(&mut flushed).expect("final"); + assert_eq!(len, plaintext.len(), "everything is flushed at finalization"); + assert_eq!(&flushed[..len], want_ct, "C.3 ciphertext via the trait"); + assert_eq!(&tag[..], want_tag, "C.3 tag via the trait"); + + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_aad(&aad).expect("aad"); + for piece in want_ct.chunks(5) { + assert_eq!(dec.do_update_out(piece, &mut nothing).expect("update"), 0); + } + let mut out = [0u8; 256]; + let n = dec + .do_final_out_detached(want_tag.try_into().expect("8 bytes"), &mut out) + .expect("tag check"); + assert_eq!(&out[..n], &plaintext[..], "C.3 plaintext via the trait"); + + // The inline layout through the inherited `SymmetricCipher*` methods: C.3's `C` is exactly + // `ciphertext || tag`, and the decryptor takes the tag back off its end. + let mut rng = FixedSeedRNG::<12>::new(nonce_seed); + let (mut enc, nonce) = Enc::do_encrypt_init_rng(&k, &mut rng).expect("init"); + enc.do_update_aad(&aad).expect("aad"); + enc.do_update_out(&plaintext, &mut nothing).expect("update"); + let (inline, inline_len) = enc.do_final().expect("final"); + assert_eq!(&inline[..inline_len], &c[..], "C.3 `C` via the inline do_final"); + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_aad(&aad).expect("aad"); + for piece in c.chunks(5) { + assert_eq!(dec.do_update_out(piece, &mut nothing).expect("update"), 0); + } + let (out, n) = dec.do_final().expect("tag check"); + assert_eq!(&out[..n], &plaintext[..], "C.3 plaintext via the inline do_final"); +} + +/// A message longer than the streaming capacity, `FINAL_LEN - TAG_LEN`, is refused rather than +/// silently truncated, and so is an oversized AAD. This is the cost of the trait's length-free `do_encrypt_init`; see +/// [`CcmEncryptor`]. +#[test] +fn the_buffering_pair_refuses_a_message_past_its_buffer() { + // A 32-byte capacity: `FINAL_LEN` leaves room for the 16-byte inline tag after it. + type Enc = CcmEncryptor; + let k = key::<16>(APPENDIX_C_KEY); + let mut nothing = [0u8; 0]; + + let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + assert!(matches!( + enc.do_update_out(&[0u8; 33], &mut nothing), + Err(SymmetricCipherError::GenericError(_)) + )); + + // In two calls that together overflow, the first must succeed and the second be refused. + let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + assert_eq!(enc.do_update_out(&[0u8; 20], &mut nothing).expect("fits"), 0); + assert!(matches!( + enc.do_update_out(&[0u8; 13], &mut nothing), + Err(SymmetricCipherError::GenericError(_)) + )); + + let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + assert!(matches!(enc.do_update_aad(&[0u8; 33]), Err(SymmetricCipherError::GenericError(_)))); + + // The encryptor's bound is `FINAL_LEN - TAG_LEN`, not `FINAL_LEN`, and its message must say + // so: 33 bytes is refused although it is well inside the 48-byte `FINAL_LEN`. + let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + match enc.do_update_out(&[0u8; 33], &mut nothing) { + Err(SymmetricCipherError::GenericError(msg)) => assert!( + msg.contains("FINAL_LEN - TAG_LEN"), + "the encryptor's refusal must name its real bound, got: {msg}" + ), + other => panic!("expected GenericError, got {other:?}"), + } +} + +/// An empty `do_update_out` is a no-op and does not close the AAD phase, on either side. The +/// trait makes an empty `aad` a no-op "at any point" so that a generic caller may pass one +/// unconditionally; a caller whose reader hands back an empty first chunk, or that calls +/// `do_update_out(&[])` before deciding on AAD, gets the same treatment here. Only a non-empty +/// call starts the data phase. +#[test] +fn an_empty_update_does_not_close_the_aad_phase() { + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + let k = key::<16>(APPENDIX_C_KEY); + let mut nothing = [0u8; 0]; + let aad = b"header"; + let message = b"payload"; + + let (mut enc, nonce) = Enc::do_encrypt_init(&k).expect("init"); + enc.do_update_out(&[], &mut nothing).expect("an empty update is a no-op"); + enc.do_update_aad(aad).expect("the AAD phase is still open after an empty update"); + enc.do_update_out(message, &mut nothing).expect("buffered"); + assert!( + matches!(enc.do_update_aad(aad), Err(SymmetricCipherError::StateError(_))), + "a non-empty update still closes the AAD phase" + ); + let (sealed, sealed_len) = enc.do_final().expect("final"); + + // The AAD really was absorbed: the direct API with the same AAD must agree, and the + // decryptor, given the same empty-then-AAD sequence, must verify it. + let mut expected = [0u8; 64]; + let n = Ccm::::encrypt( + &k, &nonce, aad, message, &mut expected, + ) + .expect("direct"); + assert_eq!(&sealed[..sealed_len], &expected[..n]); + + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_out(&[], &mut nothing).expect("an empty update is a no-op"); + dec.do_update_aad(aad).expect("the AAD phase is still open after an empty update"); + dec.do_update_out(&sealed[..sealed_len], &mut nothing).expect("buffered"); + assert!( + matches!(dec.do_update_aad(aad), Err(SymmetricCipherError::StateError(_))), + "a non-empty update still closes the AAD phase" + ); + let (opened, opened_len) = dec.do_final().expect("tag check"); + assert_eq!(&opened[..opened_len], message); +} + +/// Filling the streaming capacity *exactly* must be accepted, not refused: `CcmBuffer::do_update_aad` +/// / `do_update_out` check `end > FINAL_LEN - TAG_LEN`, so using all of it is legitimate and only +/// one byte more is not. Both boundary sides, in one call and split across two. +#[test] +fn the_buffering_pair_accepts_a_message_that_exactly_fills_its_buffer() { + // A 32-byte capacity: `FINAL_LEN` leaves room for the 16-byte inline tag after it. + type Enc = CcmEncryptor; + let k = key::<16>(APPENDIX_C_KEY); + let mut nothing = [0u8; 0]; + + let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + assert_eq!(enc.do_update_out(&[0u8; 32], &mut nothing).expect("exactly fills the capacity"), 0); + + let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + assert_eq!(enc.do_update_out(&[0u8; 20], &mut nothing).expect("fits"), 0); + assert_eq!( + enc.do_update_out(&[0u8; 12], &mut nothing).expect("exactly fills the remaining space"), + 0 + ); + + let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + assert!(enc.do_update_aad(&[0u8; 32]).is_ok(), "AAD exactly filling the capacity is accepted"); +} + +/// The decryptor cannot know until the final call whether the tag is inline, so it buffers up to +/// the full `FINAL_LEN` -- a capacity-filling ciphertext with its tag after it -- and decrypts that +/// through the inline `do_final`. The detached final holds the ciphertext to the same capacity as +/// the encryptor, so the room kept for an inline tag cannot be used to smuggle a longer message +/// past it. +#[test] +fn the_buffering_decryptor_holds_the_inline_tag_but_caps_detached_ciphertext() { + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + let k = key::<16>(APPENDIX_C_KEY); + let mut nothing = [0u8; 0]; + let message = [0x5Au8; 32]; + + let (mut enc, nonce) = Enc::do_encrypt_init(&k).expect("init"); + enc.do_update_out(&message, &mut nothing).expect("fills the capacity"); + let (inline, inline_len) = enc.do_final().expect("final"); + assert_eq!(inline_len, 48, "32 bytes of ciphertext and the 16-byte tag"); + + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_out(&inline[..inline_len], &mut nothing) + .expect("all of FINAL_LEN may be buffered"); + let (out, n) = dec.do_final().expect("tag check"); + assert_eq!(&out[..n], &message[..]); + + // One byte past FINAL_LEN is refused even though the tag might be inline. + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + assert!(matches!( + dec.do_update_out(&[0u8; 49], &mut nothing), + Err(SymmetricCipherError::GenericError(_)) + )); + + // Detached, the 48 buffered bytes would all be ciphertext: more than the capacity. + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_out(&inline[..inline_len], &mut nothing).expect("buffered"); + let mut out = [0u8; 48]; + assert!(matches!( + dec.do_final_out_detached(&[0u8; 16], &mut out), + Err(SymmetricCipherError::GenericError(_)) + )); + + // ...and exactly the capacity is fine. + let mut detached = [0u8; 32]; + let (_, _, tag) = Enc::encrypt_out_rng_detached( + &k, + &mut FixedSeedRNG::<12>::new(nonce), + &[], + &message, + &mut detached, + ) + .expect("one-shot"); + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_out(&detached, &mut nothing).expect("buffered"); + let n = dec.do_final_out_detached(&tag, &mut out).expect("tag check"); + assert_eq!(&out[..n], &message[..]); +} + +/// The trait one-shots know both lengths up front, so they use `Ccm` directly rather than imposing +/// the streaming adapter's fixed buffer on otherwise valid packets. +#[test] +fn trait_one_shots_are_not_capped_by_final_len() { + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + + let k = key::<16>(APPENDIX_C_KEY); + let aad = [0x3Cu8; 128]; + let plaintext = [0xA5u8; 4096]; + let mut ciphertext = [0u8; 4096]; + let (nonce, written, tag) = Enc::encrypt_out_rng_detached( + &k, + &mut FixedSeedRNG::<12>::new([0x24u8; 12]), + &aad, + &plaintext, + &mut ciphertext, + ) + .expect("one-shot payload and AAD may exceed FINAL_LEN"); + assert_eq!(written, plaintext.len()); + + let mut opened = [0u8; 4096]; + let opened_len = + Dec::decrypt_out_detached(&k, &nonce, &aad, &ciphertext[..written], &tag, &mut opened) + .expect("direct one-shot decryption"); + assert_eq!(&opened[..opened_len], &plaintext); +} + +/// Resuming a part-way-open keystream block into the batched fours/pairs path. +/// +/// None of the Appendix C vectors are long enough for this: the largest, C.4, is 32 bytes (two +/// blocks), too short for a small opening call to leave enough afterwards to reach +/// `apply_keystream_batch`'s fours/pairs path at all. Every chunking `check_vector` sweeps is also +/// *uniform*, so the only call that can ever see `ks_pos` strictly between `0` and `BLOCK_LEN` on +/// entry is a small final remainder -- never one big enough to batch. A first small, +/// non-block-aligned call followed by one call spanning several whole blocks exercises exactly +/// that: the batched blocks must still line up with the keystream the small call left partway +/// through, not silently skip over it. Checked against a one-shot encryption of the identical +/// plaintext, which does not go anywhere near this split. +#[test] +fn resuming_a_part_way_open_block_agrees_with_a_one_shot() { + type Enc = Ccm; + let k = key::<16>(APPENDIX_C_KEY); + let nonce = [0x24u8; 12]; + let aad = b"header"; + // Long enough that, after a several-byte opening call, what remains spans at least one + // four-block batch and one pair-block batch (4 + 2 = 6 blocks = 96 bytes) plus a short tail. + let plaintext: Vec = (0..123u8).collect(); + + let mut reference = vec![0u8; plaintext.len()]; + let (_, reference_tag) = + Enc::encrypt_detached(&k, &nonce, aad, &plaintext, &mut reference).expect("one-shot"); + + for first in [1usize, 3, 5, 15] { + let mut ccm = Enc::new(&k, &nonce, aad, plaintext.len()).expect("streaming init"); + let mut streamed = plaintext.clone(); + let (head, rest) = streamed.split_at_mut(first); + ccm.do_encrypt_update(head).expect("small first update"); + ccm.do_encrypt_update(rest).expect("large second update"); + let tag = ccm.do_encrypt_final().expect("final"); + assert_eq!(streamed, reference, "ciphertext, resuming a {first}-byte-open block"); + assert_eq!(tag, reference_tag, "tag, resuming a {first}-byte-open block"); + } +} + +/// Sec 6.2 step 1: "If Clen <= Tlen, then return INVALID". The inline layout has to reject a `C` +/// too short to contain a tag before it can split one off. +/// +/// A `C` of exactly `TAG_LEN` octets is *not* too short: it is the empty payload of Sec 5.3's +/// footnote, and must authenticate. +/// +/// All three inline entry points -- the inherent one-shot, the buffering decryptor's `do_final` +/// and its `decrypt_out_with_aad` -- must report the same malformed input with the same variant, +/// [`SymmetricCipherError::DecryptionFailed`], which is what [`SymmetricCipherDecryptor::do_final`] +/// specifies for a malformed ciphertext; a caller telling "malformed" from "inauthentic" must not +/// get a different answer depending on which one it used. +#[test] +fn an_inline_ciphertext_shorter_than_the_tag_is_rejected() { + type Enc = Ccm; + type Dec = Ccm; + type StreamDec = CcmDecryptor; + let k = key::<16>(APPENDIX_C_KEY); + let nonce = [0u8; 12]; + let mut out = [0u8; 16]; + let mut nothing = [0u8; 0]; + + for len in 0..16 { + let short = vec![0u8; len]; + assert!( + matches!( + Dec::decrypt(&k, &nonce, &[], &short, &mut out), + Err(SymmetricCipherError::DecryptionFailed) + ), + "a {len}-byte C cannot carry a 16-byte tag (Ccm::decrypt)" + ); + assert!( + matches!( + StreamDec::decrypt_out_with_aad(&k, &nonce, &[], &short, &mut out), + Err(SymmetricCipherError::DecryptionFailed) + ), + "a {len}-byte C cannot carry a 16-byte tag (decrypt_out_with_aad)" + ); + let mut dec = StreamDec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_out(&short, &mut nothing).expect("buffered"); + assert!( + matches!(dec.do_final(), Err(SymmetricCipherError::DecryptionFailed)), + "a {len}-byte C cannot carry a 16-byte tag (do_final)" + ); + } + + // Exactly TAG_LEN: an empty payload plus its tag, which must verify. + let mut inline = [0u8; 16]; + let n = Enc::encrypt(&k, &nonce, &[], &[], &mut inline).expect("encryption"); + assert_eq!(n, 16); + assert_eq!(Dec::decrypt(&k, &nonce, &[], &inline, &mut out).expect("decryption"), 0); +} + +/// An output buffer that is too short is refused with the length required, before any work. +#[test] +fn undersized_output_buffers_are_refused() { + type Enc = Ccm; + type Dec = Ccm; + let k = key::<16>(APPENDIX_C_KEY); + let nonce = [0u8; 12]; + let plaintext = [0xAAu8; 24]; + + let mut too_small = [0u8; 23]; + assert_eq!( + buffer_len_error(Enc::encrypt_detached(&k, &nonce, &[], &plaintext, &mut too_small)), + Some(24) + ); + + let mut too_small = [0u8; 39]; + assert_eq!( + buffer_len_error(Enc::encrypt(&k, &nonce, &[], &plaintext, &mut too_small)), + Some(40) + ); + + let mut ct = [0u8; 40]; + Enc::encrypt(&k, &nonce, &[], &plaintext, &mut ct).expect("encryption"); + let mut too_small = [0u8; 23]; + assert_eq!(buffer_len_error(Dec::decrypt(&k, &nonce, &[], &ct, &mut too_small)), Some(24)); +} + +/// A key of the wrong [`KeyType`] is rejected by every entry point, in both directions. +#[test] +fn a_non_cipher_key_is_rejected() { + type Enc = Ccm; + type Dec = Ccm; + let wrong = + KeyMaterial::<16>::from_bytes_as_type(&[0x11; 16], KeyType::MACKey).expect("a MAC key"); + let mut out = [0u8; 16]; + assert!(matches!( + Enc::encrypt_detached(&wrong, &[0u8; 12], &[], &[], &mut out), + Err(SymmetricCipherError::KeyMaterialError(_)) + )); + assert!(matches!( + Enc::new(&wrong, &[0u8; 12], &[], 0), + Err(SymmetricCipherError::KeyMaterialError(_)) + )); + assert!(matches!( + Dec::decrypt(&wrong, &[0u8; 12], &[], &[0u8; 16], &mut out), + Err(SymmetricCipherError::KeyMaterialError(_)) + )); + assert!(matches!( + Dec::new(&wrong, &[0u8; 12], &[], 0), + Err(SymmetricCipherError::KeyMaterialError(_)) + )); +} + +/// The direction is in the type, so the wrong direction's method is a **compile** error rather +/// than a runtime one. This is what the `Dir` parameter buys over a runtime flag, and without a +/// test the guarantee could quietly regress into an inherent method on the shared impl block. +/// +/// Both of these are checked as `compile_fail` doctests on [`Ccm`] itself; this test is the +/// positive half -- that the *right* direction's methods do exist on each -- which a +/// `compile_fail` cannot express. +#[test] +fn each_direction_has_its_own_methods() { + type Enc = Ccm; + type Dec = Ccm; + let k = key::<16>(APPENDIX_C_KEY); + let nonce = [0x55u8; 12]; + + let mut enc = Enc::new(&k, &nonce, b"aad", 4).expect("encrypt init"); + let mut data = [1u8, 2, 3, 4]; + enc.do_encrypt_update(&mut data).expect("encrypt update"); + let tag = enc.do_encrypt_final().expect("encrypt final"); + + let mut dec = Dec::new(&k, &nonce, b"aad", 4).expect("decrypt init"); + dec.do_decrypt_update(&mut data).expect("decrypt update"); + dec.do_decrypt_final(&tag).expect("decrypt final"); + assert_eq!(data, [1u8, 2, 3, 4]); +} + +// ---- memory ------------------------------------------------------------------------------ + +/// Pins the "Memory Usage" table in the crate docs: `Ccm` is 256/288/320 B for AES-128/192/256, +/// independent of `NONCE_LEN`/`TAG_LEN`, and the buffering pair is `2 * FINAL_LEN`. +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::size_of; + + assert_eq!(size_of::>(), 256); + assert_eq!(size_of::>(), 288); + assert_eq!(size_of::>(), 320); + + // Independent of NONCE_LEN and TAG_LEN: the nonce lives inside the counter template and the + // tag is assembled at finalization, not held. + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); + + // The direction marker is free, and does not change the layout. + assert_eq!( + size_of::>(), + size_of::>() + ); + + // The buffering adapters: 2 * FINAL_LEN each (an `aad` array and a `data` array). + assert_eq!( + size_of::>(), + size_of::>() + ); + assert!(size_of::>() >= 2 * 4096); +} + +// ---- moved from crypto/modes/src/ccm.rs's in-file unit tests ----------------------------- + +/// A.1's `p < 2^8q`. With `n = 13`, `q = 2`, so the limit is 65535 and 65536 must be refused. +/// +/// Only the public API is exercised, so this belongs here rather than in `ccm.rs`'s own +/// `#[cfg(test)]` block, which is for the private formatting helpers no public API reaches. +#[test] +fn payload_longer_than_the_q_limit_is_refused() { + let k = key::<16>(APPENDIX_C_KEY); + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c]; + assert!( + Ccm::::new(&k, &nonce, &[], 65535).is_ok(), + "2^16 - 1 is the largest payload q = 2 can encode" + ); + assert!( + matches!( + Ccm::::new(&k, &nonce, &[], 65536), + Err(SymmetricCipherError::GenericError(_)) + ), + "2^16 does not fit [p]_16" + ); +} + +/// The declared payload length is inside `B0`, so neither direction may be finalized with the +/// wrong amount of data. +#[test] +fn a_short_or_long_payload_is_refused() { + let k = key::<16>(APPENDIX_C_KEY); + let nonce = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]; + let mut ccm = Ccm::::new(&k, &nonce, &[], 8).unwrap(); + let mut too_much = [0u8; 9]; + assert!( + matches!(ccm.do_encrypt_update(&mut too_much), Err(SymmetricCipherError::StateError(_))), + "9 bytes against a declared 8" + ); + let mut some = [0u8; 4]; + ccm.do_encrypt_update(&mut some).expect("4 of the 8 declared bytes"); + assert!( + matches!(ccm.do_encrypt_final(), Err(SymmetricCipherError::StateError(_))), + "finalizing 4 bytes short" + ); +} diff --git a/crypto/modes/tests/wycheproof_ccm_tests.rs b/crypto/modes/tests/wycheproof_ccm_tests.rs new file mode 100644 index 00000000..cebc3580 --- /dev/null +++ b/crypto/modes/tests/wycheproof_ccm_tests.rs @@ -0,0 +1,306 @@ +//! Known-answer tests against Project Wycheproof's `aes_ccm_test.json`, vendored into +//! `bc-test-data/crypto/wycheproof/` alongside the sibling `sm4_ccm_test.json`. +//! +//! 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 test prints a warning and passes, +//! matching the convention used by the ACVP suite in this crate. +//! +//! # Why this set is worth having alongside the ACVP one +//! +//! `acvp_ccm_tests.rs` covers 480 cases, but every one of them uses a 96-bit nonce, and the only +//! failures it carries are tag-check failures on an otherwise well-formed message. Wycheproof's +//! set is deliberately adversarial in the ways ACVP is not: malformed and truncated tags, every +//! nonce length from 8 to 2144 *bits* (most of which A.1 does not permit at all), a tag size of +//! 16 bits that SP 800-38C Appendix B.2 calls insecure, and pseudorandom sizes meant to catch an +//! implementation that only handles the common cases. See +//! `bc-test-data/crypto/wycheproof/aes_ccm_test.json`'s own `"notes"` object for exactly what each +//! `flags` entry is checking. +//! +//! # Ciphertext and tag are separate fields, unlike the ACVP set +//! +//! Wycheproof's AEAD schema carries `ct` and `tag` as distinct fields (the `aead_test_schema_v1` +//! schema), so these cases go through [`Ccm::encrypt_detached`] / [`Ccm::decrypt_detached`], not +//! the inline pair `acvp_ccm_tests.rs` uses. +//! +//! # Most of the parameter space cannot be dispatched to at all, by design +//! +//! `Ccm`'s `NONCE_LEN` and `TAG_LEN` are const generics restricted to A.1's sets -- +//! `NONCE_LEN` in `7..=13` bytes, `TAG_LEN` in `{4, 6, 8, 10, 12, 14, 16}` bytes -- so there is no +//! instantiation to dispatch a group whose `ivSize`/`tagSize` falls outside them to at all; unlike +//! a runtime check, this is not something a case can "fail", because it is a compile-time property +//! of the type, not a value the library ever sees. Those groups (most of the file: the point of +//! `InvalidNonceSize`/`InvalidTagSize` and most of the `Pseudorandom` groups is to probe exactly +//! this boundary) are counted as skipped rather than silently dropped, and the counts are asserted +//! at the end so a change in the vector file's shape is visible. + +use bouncycastle_aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::ElectronicCodeBook; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Ccm, Decrypting, Encrypting}; +use serde_json::Value; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = [ + "../../../bc-test-data/crypto/wycheproof/aes_ccm_test.json", + "../bc-test-data/crypto/wycheproof/aes_ccm_test.json", +]; + +fn test_data_file() -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate); + if path.exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); \ + Wycheproof AES-CCM tests will be skipped" + ); + None +} + +fn decode(value: &Value, field: &str, tc_id: u64) -> Vec { + let s = value + .get(field) + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("tcId {tc_id}: missing field {field}")); + hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc_id}: bad hex in {field}")) +} + +/// Wraps the vector's raw key bytes, promoting them if `KeyMaterial`'s entropy heuristic declined +/// to call them a cipher key. Same helper as the ACVP suite in this crate. +fn cipher_key(bytes: &[u8]) -> KeyMaterial { + assert_eq!(bytes.len(), N, "key length should match the parameter set"); + let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("wycheproof key bytes fit the buffer"); + + if key.key_type() != KeyType::SymmetricCipherKey { + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::from_bytes(N)) + }) + .expect("promoting a wycheproof test key"); + } + key +} + +/// Runs one case at a fully-instantiated `(KEY_LEN, NONCE_LEN, TAG_LEN, P)`. +/// +/// For a `result: "valid"` case, `msg` must encrypt to exactly `expected_ct`/`expected_tag` +/// ([`Ccm::encrypt_detached`]), and `expected_ct`/`expected_tag` must decrypt back to `msg` +/// ([`Ccm::decrypt_detached`]). For `result: "invalid"`, only the decrypt direction is checked -- +/// re-encrypting `msg` has no reason to reproduce a deliberately corrupted `ct`/`tag` -- and it +/// must fail the tag check rather than return a payload. +#[allow(clippy::too_many_arguments)] +fn run_case( + tc_id: u64, + key_bytes: &[u8], + nonce_bytes: &[u8], + aad: &[u8], + msg: &[u8], + expected_ct: &[u8], + expected_tag: &[u8], + valid: bool, +) where + P: ElectronicCodeBook, +{ + let key = cipher_key::(key_bytes); + let nonce: [u8; NONCE_LEN] = + nonce_bytes.try_into().unwrap_or_else(|_| panic!("tcId {tc_id}: bad nonce length")); + let tag: [u8; TAG_LEN] = + expected_tag.try_into().unwrap_or_else(|_| panic!("tcId {tc_id}: bad tag length")); + + if valid { + let mut ct = vec![0u8; msg.len()]; + let (written, got_tag) = + Ccm::::encrypt_detached( + &key, &nonce, aad, msg, &mut ct, + ) + .unwrap_or_else(|e| panic!("tcId {tc_id}: valid case failed to encrypt: {e:?}")); + assert_eq!(written, msg.len(), "tcId {tc_id}: encrypt_detached writes exactly msg.len()"); + assert_eq!(ct, expected_ct, "tcId {tc_id}: ciphertext mismatch"); + assert_eq!(got_tag, tag, "tcId {tc_id}: tag mismatch"); + } + + let mut plaintext = vec![0u8; expected_ct.len()]; + match Ccm::::decrypt_detached( + &key, &nonce, aad, expected_ct, &tag, &mut plaintext, + ) { + Ok(n) => { + assert!(valid, "tcId {tc_id}: an invalid vector decrypted and verified anyway"); + plaintext.truncate(n); + assert_eq!(plaintext, msg, "tcId {tc_id}: decrypted plaintext mismatch"); + } + Err(SymmetricCipherError::AEADTagCheckFailed) => { + assert!(!valid, "tcId {tc_id}: a valid vector failed its tag check"); + } + Err(e) => panic!("tcId {tc_id}: unexpected CCM error: {e:?}"), + } +} + +/// Dispatches to one of the 3 (key) x 7 (nonce) x 7 (tag) valid instantiations, or reports that +/// the case's parameter sizes have no instantiation to dispatch to at all. +#[allow(clippy::too_many_arguments)] +fn dispatch( + tc_id: u64, + key_len_bytes: u64, + nonce_len_bytes: u64, + tag_len_bytes: u64, + key_bytes: &[u8], + nonce_bytes: &[u8], + aad: &[u8], + msg: &[u8], + expected_ct: &[u8], + expected_tag: &[u8], + valid: bool, +) -> bool { + macro_rules! with_key_len { + ($n:literal, $t:literal) => { + match key_len_bytes { + 16 => { + run_case::<16, $n, $t, AES128Internal>( + tc_id, key_bytes, nonce_bytes, aad, msg, expected_ct, expected_tag, valid, + ); + true + } + 24 => { + run_case::<24, $n, $t, AES192Internal>( + tc_id, key_bytes, nonce_bytes, aad, msg, expected_ct, expected_tag, valid, + ); + true + } + 32 => { + run_case::<32, $n, $t, AES256Internal>( + tc_id, key_bytes, nonce_bytes, aad, msg, expected_ct, expected_tag, valid, + ); + true + } + _ => false, + } + }; + } + macro_rules! with_tag_len { + ($n:literal) => { + match tag_len_bytes { + 4 => with_key_len!($n, 4), + 6 => with_key_len!($n, 6), + 8 => with_key_len!($n, 8), + 10 => with_key_len!($n, 10), + 12 => with_key_len!($n, 12), + 14 => with_key_len!($n, 14), + 16 => with_key_len!($n, 16), + _ => false, + } + }; + } + match nonce_len_bytes { + 7 => with_tag_len!(7), + 8 => with_tag_len!(8), + 9 => with_tag_len!(9), + 10 => with_tag_len!(10), + 11 => with_tag_len!(11), + 12 => with_tag_len!(12), + 13 => with_tag_len!(13), + _ => false, + } +} + +#[test] +fn wycheproof_aes_ccm_known_answer_tests() { + let Some(path) = test_data_file() else { return }; + + let doc: Value = serde_json::from_str(&fs::read_to_string(&path).expect("readable file")) + .expect("valid wycheproof JSON"); + + let groups = doc.get("testGroups").and_then(Value::as_array).expect("testGroups"); + + let mut run = 0usize; + let mut valid_count = 0usize; + let mut invalid_count = 0usize; + let mut skipped_groups = 0usize; + let mut skipped_cases = 0usize; + + for group in groups { + let iv_size_bits = group.get("ivSize").and_then(Value::as_u64).expect("ivSize"); + let key_size_bits = group.get("keySize").and_then(Value::as_u64).expect("keySize"); + let tag_size_bits = group.get("tagSize").and_then(Value::as_u64).expect("tagSize"); + assert_eq!(iv_size_bits % 8, 0, "ivSize must be a whole number of octets"); + assert_eq!(key_size_bits % 8, 0, "keySize must be a whole number of octets"); + assert_eq!(tag_size_bits % 8, 0, "tagSize must be a whole number of octets"); + + // A group is only fully within A.1's dispatchable sets if its *declared* nonce/tag sizes + // are; a `Pseudorandom` group whose individual tests vary can still contribute some + // dispatched and some skipped cases, so this is a per-group tally for the printout, not + // something the per-case counts below depend on. + if !(7..=13).contains(&(iv_size_bits / 8)) + || ![4u64, 6, 8, 10, 12, 14, 16].contains(&(tag_size_bits / 8)) + { + skipped_groups += 1; + } + + let tests = group.get("tests").and_then(Value::as_array).expect("tests"); + + // Each case is dispatched on its own actual field lengths, not the group's declared + // sizes: a `Pseudorandom` group's whole point is varying them per test, and `dispatch` + // itself is the authority on what it can run (only A.1's own sets). + for test in tests { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + let key_bytes = decode(test, "key", tc_id); + let nonce_bytes = decode(test, "iv", tc_id); + let aad = decode(test, "aad", tc_id); + let msg = decode(test, "msg", tc_id); + let ct = decode(test, "ct", tc_id); + let tag = decode(test, "tag", tc_id); + let result = test.get("result").and_then(Value::as_str).expect("result"); + let valid = match result { + "valid" => true, + "invalid" => false, + other => panic!("tcId {tc_id}: unexpected result {other}"), + }; + + let ran = dispatch( + tc_id, + key_bytes.len() as u64, + nonce_bytes.len() as u64, + tag.len() as u64, + &key_bytes, + &nonce_bytes, + &aad, + &msg, + &ct, + &tag, + valid, + ); + + if ran { + run += 1; + if valid { + valid_count += 1; + } else { + invalid_count += 1; + } + } else { + skipped_cases += 1; + } + } + } + + println!( + "Wycheproof AES-CCM: {run} cases run ({valid_count} valid, {invalid_count} invalid), \ + {skipped_cases} cases in {skipped_groups} groups skipped (no A.1 instantiation)" + ); + + // Guards against a silently-vacuous run: at least the common 96-bit-nonce/128-bit-tag groups + // must have been dispatched to and must have included both valid and invalid cases. + assert!(run > 0, "expected at least some cases to be within A.1's dispatchable sets"); + assert!(valid_count > 0, "expected at least some valid cases to be run"); + assert!(invalid_count > 0, "expected at least some invalid (tag-failure) cases to be run"); + assert!(skipped_groups > 0, "expected most of this adversarial set to be outside A.1's sets"); +} diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index 3eecb637..86dd7090 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -259,7 +259,10 @@ pub type SHA512_256 = SHA512t<256>; /// /// Crate-private (aka "sealed") on purpose: it cannot be implemented outside this crate, so the /// only parameter sets that exist are the NIST-approved ones below. -trait SHA256InitValue: HashAlgParams { +/// +/// `Clone` because [`Hash`] requires it: a hash mid-stream can be forked and finished several +/// ways from one absorbed prefix. +trait SHA256InitValue: HashAlgParams + Clone { /// The initial hash value H(0), FIPS 180-4 s. 5.3.2 / 5.3.3. const H0: [u32; 8]; } @@ -267,8 +270,8 @@ trait SHA256InitValue: HashAlgParams { /// The SHA-512 family (SHA-384, SHA-512, SHA-512/t) shares one compression function and differs /// only in the initial hash value and the output truncation, so each member supplies its H(0) here. /// -/// Crate-private for the same reason as [`SHA256InitValue`]. -trait SHA512InitValue: HashAlgParams { +/// Crate-private for the same reason as [`SHA256InitValue`], and `Clone` for the same reason. +trait SHA512InitValue: HashAlgParams + Clone { /// The initial hash value H(0), FIPS 180-4 s. 5.3.4 / 5.3.5 / 5.3.6. const H0: [u64; 8]; } diff --git a/crypto/sha3/benches/sha3_benches.rs b/crypto/sha3/benches/sha3_benches.rs index e2006a6a..b7555c80 100644 --- a/crypto/sha3/benches/sha3_benches.rs +++ b/crypto/sha3/benches/sha3_benches.rs @@ -125,7 +125,7 @@ fn bench_shake128_64b(c: &mut Criterion) { format!("input: {} bytes, output: {} bytes -- ::hashes()", big_data.len(), digest.len()), |b| { b.iter(|| { - SHAKE128::new().hash_xof_out(black_box(&big_data), &mut digest); + SHAKE128::new().xof_out(black_box(&big_data), &mut digest); black_box(&digest); }) }, @@ -149,7 +149,7 @@ fn bench_shake128_64k(c: &mut Criterion) { format!("input: {} bytes, output: {} bytes -- ::hashes()", big_data.len(), digest.len()), |b| { b.iter(|| { - SHAKE128::new().hash_xof_out(black_box(&big_data), &mut digest); + SHAKE128::new().xof_out(black_box(&big_data), &mut digest); black_box(&digest); }) }, @@ -173,7 +173,7 @@ fn bench_shake256_64b(c: &mut Criterion) { format!("input: {} bytes, output: {} bytes -- ::hashes()", big_data.len(), digest.len()), |b| { b.iter(|| { - SHAKE256::new().hash_xof_out(black_box(&big_data), &mut digest); + SHAKE256::new().xof_out(black_box(&big_data), &mut digest); black_box(&digest); }) }, @@ -197,7 +197,7 @@ fn bench_shake256_64k(c: &mut Criterion) { format!("input: {} bytes, output: {} bytes -- ::hashes()", big_data.len(), digest.len()), |b| { b.iter(|| { - SHAKE128::new().hash_xof_out(black_box(&big_data), &mut digest); + SHAKE128::new().xof_out(black_box(&big_data), &mut digest); black_box(&digest); }) }, diff --git a/crypto/sha3/src/cshake.rs b/crypto/sha3/src/cshake.rs new file mode 100644 index 00000000..db5237dc --- /dev/null +++ b/crypto/sha3/src/cshake.rs @@ -0,0 +1,236 @@ +//! cSHAKE, the customizable SHAKE of NIST SP 800-185 Sec 3. + +use crate::SHAKEParams; +use crate::shake::{SHAKEInternal, SHAKESqueezer}; +use crate::xof_utils::left_encode; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; + +/// The domain separator cSHAKE absorbs in place of SHAKE's `1111`: the `00` of SP 800-185 Sec 3.3, +/// two zero bits, which is what keeps a customized instance separate from plain SHAKE. +const CSHAKE_SUFFIX: (u8, usize) = (0x00, 2); + +/// Internal struct for cSHAKE. Use [`crate::CSHAKE128`] or [`crate::CSHAKE256`]. +/// +/// cSHAKE is SHAKE with two extra inputs bound to the front of the message: a function-name string +/// `N`, reserved for NIST, and a customization string `S`, chosen by the caller. SP 800-185 Sec 3.1 +/// puts it as strong typing -- two instances with different `N` or `S` produce unrelated output, so +/// a key fingerprint and an email signature computed over the same bytes cannot collide. +/// +/// # The empty case is SHAKE, exactly +/// +/// SP 800-185 Sec 3.3 step 1: when `N` and `S` are both empty, cSHAKE *is* SHAKE, including its +/// `1111` domain separator. This is a required special case, not something that falls out of the +/// general construction -- feeding empty strings through the `bytepad` branch would absorb a +/// non-empty prefix and use a different separator, giving a different function. [`Self::new`] +/// branches on it, and there is a test that the two agree. +#[derive(Clone)] +pub struct CSHAKEInternal { + shake: SHAKEInternal, + /// False when `N` and `S` are both empty, in which case this is plain SHAKE. + customized: bool, +} + +impl Algorithm for CSHAKEInternal { + const ALG_NAME: &'static str = PARAMS::CSHAKE_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl CSHAKEInternal { + /// A new cSHAKE bound to the function name `n` and customization string `s`. + /// + /// Both may be empty; if both are, this is plain SHAKE (Sec 3.3 step 1). + /// + /// `n` is reserved for NIST-defined functions -- Sec 3.4 asks callers not to invent their own, + /// because a value NIST later assigns would then collide. Customization belongs in `s`. + pub fn new(n: &[u8], s: &[u8]) -> Self { + let mut shake = SHAKEInternal::::new(); + let customized = !n.is_empty() || !s.is_empty(); + if customized { + // Sec 3.3: bytepad(encode_string(N) || encode_string(S), rate). + absorb_bytepad(&mut shake, &[n, s]); + } + Self { shake, customized } + } +} + +/// Absorbs `bytepad(encode_string(s[0]) || ... || encode_string(s[n]), rate)`, the padding of +/// SP 800-185 Sec 2.3.3 over the string encodings of Sec 2.3.2. +/// +/// Absorbed straight into the sponge rather than built in a buffer, so there is no allocation and +/// no bound on the length of the strings. +fn absorb_bytepad(shake: &mut SHAKEInternal, strings: &[&[u8]]) { + let rate = PARAMS::RATE_BYTES; + // Step 1: the encoding of the block size comes first. + let mut written = absorb_left_encode(shake, rate as u64); + for s in strings { + written += absorb_encoded_string(shake, s); + } + // Step 3: zero bytes up to a whole number of rate-sized blocks. + absorb_zeros(shake, written.next_multiple_of(rate) - written); +} + +/// [`absorb_bytepad`] against a cSHAKE, for the functions layered on top of it: KMAC binds its key +/// this way (Sec 4.3 step 1) as a second bytepad block inside cSHAKE's message. +pub(crate) fn absorb_bytepad_strings( + cshake: &mut CSHAKEInternal, + strings: &[&[u8]], +) { + absorb_bytepad(&mut cshake.shake, strings); +} + +/// Absorbs `encode_string(s)` into a cSHAKE, for the functions layered on top: TupleHash encodes +/// each tuple element this way (Sec 5.3 step 3), which is what makes the tuple boundaries part of +/// the hash. +pub(crate) fn absorb_encoded_string_into( + cshake: &mut CSHAKEInternal, + s: &[u8], +) { + absorb_encoded_string(&mut cshake.shake, s); +} + +/// Absorbs `left_encode(value)` into a cSHAKE, for the functions layered on top: ParallelHash +/// binds its block size this way (Sec 6.3 step 2). +pub(crate) fn absorb_left_encode_into( + cshake: &mut CSHAKEInternal, + value: u64, +) { + absorb_left_encode(&mut cshake.shake, value); +} + +/// Absorbs `left_encode(value)`, returning how many bytes went in. +fn absorb_left_encode(shake: &mut SHAKEInternal, value: u64) -> usize { + let (buf, len) = left_encode(value); + shake.do_update(&buf[..len]); + len +} + +/// Absorbs `encode_string(s)` -- `left_encode(len(s))` then `s` -- returning how many bytes went +/// in. SP 800-185 Sec 2.3.2 counts the length in bits. +fn absorb_encoded_string( + shake: &mut SHAKEInternal, + s: &[u8], +) -> usize { + let n = absorb_left_encode(shake, (s.len() as u64) * 8); + shake.do_update(s); + n + s.len() +} + +/// Absorbs `count` zero bytes, the padding of `bytepad` (Sec 2.3.3 step 3). +fn absorb_zeros(shake: &mut SHAKEInternal, mut count: usize) { + const ZEROS: [u8; 64] = [0u8; 64]; + while count > 0 { + let n = count.min(ZEROS.len()); + shake.do_update(&ZEROS[..n]); + count -= n; + } +} + +impl Default for CSHAKEInternal { + /// An uncustomized cSHAKE, which by Sec 3.3 step 1 is plain SHAKE. + fn default() -> Self { + Self::new(&[], &[]) + } +} + +impl Hash for CSHAKEInternal { + fn block_bitlen(&self) -> usize { + self.shake.block_bitlen() + } + + fn output_len(&self) -> usize { + self.shake.output_len() + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + fn do_update(&mut self, data: &[u8]) { + self.shake.do_update(data); + } + + /// A final read at the nominal length: [`Hash::output_len`] bytes, 32 for cSHAKE128 and 64 for + /// cSHAKE256, twice the security strength. + /// + /// Like SHAKE and unlike the SP 800-185 functions built on it, cSHAKE has no length to bind -- + /// `L` reaches it as "how much to read", not as absorbed input (Sec 3.3) -- so these are the + /// same bytes the squeezer produces. What the `Hash` view fixes is how many. + fn do_final(self) -> Vec { + let n = self.output_len(); + self.into_squeezer().do_final(n) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + let n = self.output_len(); + // Per Hash::do_final_out: a short buffer is filled and the output truncated, a long one + // takes it in its first output_len bytes and zeros after. To fill a longer buffer, use the + // XOF spelling, which takes its length from the buffer. + let written = n.min(output.len()); + output[written..].fill(0); + self.into_squeezer().do_final_out(&mut output[..written]) + } + + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len()]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + let n = self.output_len(); + // Validated before anything is written, so a rejected call leaves `output` untouched. + let squeezer = self.into_squeezer_partial_bits(partial_byte, num_bits)?; + // The buffer rule of do_final_out applies here too: output_len bytes, then zeros. + let written = n.min(output.len()); + output[written..].fill(0); + Ok(squeezer.do_final_out(&mut output[..written])) + } + + fn max_security_strength(&self) -> SecurityStrength { + Hash::max_security_strength(&self.shake) + } +} + +impl XOF for CSHAKEInternal { + type Squeezer = SHAKESqueezer; + + fn into_squeezer(self) -> Self::Squeezer { + if self.customized { + let (suffix, bits) = CSHAKE_SUFFIX; + self.shake.into_squeezer_with_suffix(suffix, bits) + } else { + // Sec 3.3 step 1: with no N and no S this is SHAKE, separator included. + self.shake.into_squeezer() + } + } + + fn into_squeezer_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result { + if self.customized { + let (suffix, bits) = CSHAKE_SUFFIX; + self.shake.into_squeezer_partial_bits_with_suffix(partial_byte, num_bits, suffix, bits) + } else { + self.shake.into_squeezer_partial_bits(partial_byte, num_bits) + } + } +} diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs new file mode 100644 index 00000000..3cc26d87 --- /dev/null +++ b/crypto/sha3/src/kmac.rs @@ -0,0 +1,327 @@ +//! KMAC, the Keccak Message Authentication Code of NIST SP 800-185 Sec 4. + +use crate::SHAKEParams; +use crate::cshake::CSHAKEInternal; +use crate::length_bound_squeezer::LengthBoundSqueezer; +use crate::xof_utils::right_encode; +use bouncycastle_core::errors::{HashError, KeyMaterialError, MACError}; +use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, MAC, XOF, XOFSqueezer}; +use bouncycastle_utils::ct; + +/// The function-name string every KMAC binds, per SP 800-185 Sec 4.3. Fixed by the specification: +/// it is what separates KMAC from any other cSHAKE-derived function. +const KMAC_FUNCTION_NAME: &[u8] = b"KMAC"; + +/// Internal struct for KMAC. Use [`crate::KMAC128`] or [`crate::KMAC256`]. +/// +/// KMAC is cSHAKE with the function name `"KMAC"`, the key bound to the front of the message and +/// the requested output length bound to the end (Sec 4.3): +/// +/// ```text +/// KMAC128(K, X, L, S) = cSHAKE128(bytepad(encode_string(K), 168) || X || right_encode(L), +/// L, "KMAC", S) +/// ``` +/// +/// # Two functions, not one function truncated +/// +/// The output length is *absorbed*, so KMAC at one length is unrelated to KMAC at another -- +/// Sec 1 puts it as "any change in the requested output length completely changes the function". +/// That is why [`Self::new_with_params`] takes the length up front and [`MAC::do_final`] produces +/// exactly that many bytes. +/// +/// [`KMACXOFInternal`] is the separate function of Sec 4.3.1, KMACXOF, which binds +/// `right_encode(0)` instead and produces as much output as asked for. Its bytes are *not* a +/// prefix of the fixed-length KMAC over the same inputs, and are not meant to be. +pub struct KMACInternal { + cshake: CSHAKEInternal, + output_len: usize, + strength: SecurityStrength, +} + +impl Algorithm for KMACInternal { + const ALG_NAME: &'static str = PARAMS::KMAC_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl KMACInternal { + /// A new KMAC with a customization string and an output length of the caller's choosing. + /// + /// `output_len` is `L` in bytes and is bound into the computation, so it must be the length the + /// verifier will use. `customization` may be empty. [`MAC::new`] is this with no customization + /// and the nominal output length. + /// + /// Sec 8.4.1 requires the key to be at least as long as the security strength for approved use; + /// that is enforced through the key's [`SecurityStrength`] tag, exactly as `HMAC` does, and + /// [`MAC::new_allow_weak_key`] is the escape hatch. + /// + /// # Errors + /// [`MACError::KeyMaterialError`] if the key is not tagged as a MAC key, or -- unless + /// `allow_weak_key` -- if it is tagged below this KMAC's security strength. + pub fn new_with_params( + key: &impl KeyMaterialTrait, + customization: &[u8], + output_len: usize, + allow_weak_key: bool, + ) -> Result { + // Same stance as HMAC: an all-zero key is Zeroized rather than MACKey, and is allowed + // through so callers are not forced to re-tag it. + if !(key.key_type() == KeyType::Zeroized || key.key_type() == KeyType::MACKey) { + return Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType( + "Key type must be a MAC key.", + ))); + } + let strength = SecurityStrength::from_bits(PARAMS::SIZE as usize); + if !allow_weak_key && key.security_strength() < strength { + Err(KeyMaterialError::SecurityStrength( + "KMAC::new(): provided key has a lower security strength than the instantiated KMAC", + ))? + } + + let mut cshake = CSHAKEInternal::::new(KMAC_FUNCTION_NAME, customization); + // Sec 4.3 step 1: bytepad(encode_string(K), rate), absorbed rather than materialised. + crate::cshake::absorb_bytepad_strings(&mut cshake, &[key.ref_to_bytes()]); + + Ok(Self { cshake, output_len, strength }) + } + + /// Absorbs `right_encode(value)`, the length binding of Sec 4.3 step 1. + fn absorb_right_encode(&mut self, value: u64) { + let (buf, len) = right_encode(value); + self.cshake.do_update(&buf[..len]); + } +} + +impl MAC for KMACInternal { + /// A KMAC with no customization string, producing the nominal output length -- 32 bytes for + /// KMAC128 and 64 for KMAC256. Use [`Self::new_with_params`] to choose either. + fn new(key: &impl KeyMaterialTrait) -> Result { + let len = (PARAMS::SIZE as usize) / 4; + Self::new_with_params(key, &[], len, false) + } + + fn new_allow_weak_key(key: &impl KeyMaterialTrait) -> Result { + let len = (PARAMS::SIZE as usize) / 4; + Self::new_with_params(key, &[], len, true) + } + + fn output_len(&self) -> usize { + self.output_len + } + + fn mac(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn mac_out(mut self, data: &[u8], out: &mut [u8]) -> Result { + out.fill(0); + self.do_update(data); + self.do_final_out(out) + } + + fn verify(mut self, data: &[u8], mac: &[u8]) -> bool { + self.do_update(data); + self.do_verify_final(mac) + } + + fn do_update(&mut self, data: &[u8]) { + self.cshake.do_update(data); + } + + fn do_final(mut self) -> Vec { + let n = self.output_len; + // Sec 4.3 step 1: the requested length is bound into the input before any output. + self.absorb_right_encode((n as u64) * 8); + self.cshake.into_squeezer().do_output(n) + } + + fn do_final_out(mut self, out: &mut [u8]) -> Result { + if out.len() < self.output_len { + return Err(MACError::InvalidLength( + "output buffer is smaller than the KMAC output length", + )); + } + let n = self.output_len; + self.absorb_right_encode((n as u64) * 8); + // MAC::do_final_out zeroizes the entire buffer, as HMAC does, so a longer one comes back + // with zeros after the MAC rather than whatever the caller left there. + out[n..].fill(0); + Ok(self.cshake.into_squeezer().do_output_out(&mut out[..n])) + } + + /// Compares in constant time, and only against the full output length: a caller must not be + /// able to pass verification by supplying a shorter prefix. + fn do_verify_final(self, mac: &[u8]) -> bool { + if mac.len() != self.output_len { + return false; + } + let computed = self.do_final(); + ct::ct_eq_bytes(&computed, mac) + } + + fn max_security_strength(&self) -> SecurityStrength { + self.strength + } +} + +/// Internal struct for KMACXOF. Use [`crate::KMACXOF128`] or [`crate::KMACXOF256`]. +/// +/// KMACXOF is the arbitrary-output-length function of SP 800-185 Sec 4.3.1: KMAC with +/// `right_encode(0)` bound in place of the output length. +/// +/// ```text +/// KMACXOF128(K, X, L, S) = cSHAKE128(bytepad(encode_string(K), 168) || X || right_encode(0), +/// L, "KMAC", S) +/// ``` +/// +/// # Why this is a separate type from [`KMACInternal`] +/// +/// The Recommendation defines them as two functions, and they are: over identical inputs KMAC and +/// KMACXOF produce unrelated output, which the published sample values demonstrate directly. They +/// also want different traits -- KMAC's length is fixed at construction and bound into the +/// computation, which is `MAC`; KMACXOF's is not bound at all, which is `XOF`. Since `MAC` and +/// `Hash` share five method names (`do_update`, `do_final`, `output_len` and two more), one type +/// implementing both would make every one of those calls ambiguous, so they are separate types. +/// +/// Read as a stream -- [`XOFSqueezer::do_output`] -- the length really is not bound, so output at +/// one length is a prefix of output at a longer one, the opposite of fixed-length KMAC. +/// +/// Read as a *final* read, it is bound, because a caller that names a length and will not be back +/// has said what `L` is: [`XOFSqueezer::do_final`] and [`XOF::xof`] absorb `right_encode(8n)` and +/// so produce `KMAC(K, X, 8n, S)` exactly (see [`LengthBoundSqueezer`]), and the [`Hash`] view -- +/// [`Hash::do_final`], [`Hash::hash`] and [`Hash::hash_out`] -- does the same at the nominal +/// [`Hash::output_len`], since a hash's output length is fixed by its type. +#[derive(Clone)] +pub struct KMACXOFInternal { + cshake: CSHAKEInternal, + strength: SecurityStrength, +} + +impl Algorithm for KMACXOFInternal { + const ALG_NAME: &'static str = PARAMS::KMACXOF_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl KMACXOFInternal { + /// A new KMACXOF under `key`, optionally customized by `customization`. + /// + /// The key requirements are [`KMACInternal::new_with_params`]'s: tagged as a MAC key, and at + /// least the security strength unless `allow_weak_key`. + /// + /// # Errors + /// [`MACError::KeyMaterialError`] if the key is not a MAC key, or is tagged too weak. + pub fn new( + key: &impl KeyMaterialTrait, + customization: &[u8], + allow_weak_key: bool, + ) -> Result { + // The key binding is identical to KMAC's; only the length encoding differs, and that is + // applied when output begins. + let kmac = KMACInternal::::new_with_params(key, customization, 0, allow_weak_key)?; + Ok(Self { cshake: kmac.cshake, strength: kmac.strength }) + } +} + +impl Hash for KMACXOFInternal { + fn block_bitlen(&self) -> usize { + self.cshake.block_bitlen() + } + + /// The nominal length, 32 or 64 bytes: twice the security strength of this KMAC, which is the + /// length at which the output carries that strength in full. Reading as a XOF does not bind + /// it; the [`Hash`] view does, because a hash has one output length and it is this one. + fn output_len(&self) -> usize { + self.cshake.output_len() + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + fn do_update(&mut self, data: &[u8]) { + self.cshake.do_update(data); + } + + /// A final read at the nominal length, so `L` is bound: this is `KMAC(K, X, 8n, S)` for + /// `n = ` [`Hash::output_len`] -- the fixed-length KMAC of Sec 4.3, not a prefix of the + /// KMACXOF stream. + fn do_final(self) -> Vec { + let n = self.output_len(); + self.into_squeezer().do_final(n) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + let n = self.output_len(); + // Per Hash::do_final_out: a short buffer is filled and the output truncated, a long one + // takes it in its first output_len bytes and zeros after. `n` is what reaches + // right_encode either way, so a truncated read is this KMAC cut short rather than the + // KMAC of the buffer's length. + let written = n.min(output.len()); + output[written..].fill(0); + self.into_squeezer().do_final_out_with_length((n as u64) * 8, &mut output[..written]) + } + + /// # Errors + /// Always [`HashError::InvalidLength`] for a non-zero `num_bits`: `right_encode(0)` has to + /// follow the message, and a partial final byte would leave the sponge unable to absorb it + /// byte-aligned. `num_bits` of 0 means the message ended on a byte boundary and is accepted. + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let n = self.output_len(); + let mut out = vec![0u8; n]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "KMACXOF cannot take a partial final byte: right_encode(0) must follow the message", + )); + } + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + self.strength + } +} + +impl XOF for KMACXOFInternal { + type Squeezer = LengthBoundSqueezer; + + /// The `right_encode(L)` of Sec 4.3.1 step 1 is not absorbed here: which `L` it carries depends + /// on how the first output is read, so [`LengthBoundSqueezer`] decides it. + fn into_squeezer(self) -> Self::Squeezer { + LengthBoundSqueezer::new(self.cshake) + } + + fn into_squeezer_partial_bits( + self, + _partial_byte: u8, + num_bits: usize, + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "KMACXOF cannot take a partial final byte: right_encode(0) must follow the message", + )); + } + Ok(self.into_squeezer()) + } +} diff --git a/crypto/sha3/src/length_bound_squeezer.rs b/crypto/sha3/src/length_bound_squeezer.rs new file mode 100644 index 00000000..e7827cfc --- /dev/null +++ b/crypto/sha3/src/length_bound_squeezer.rs @@ -0,0 +1,106 @@ +//! The squeezing phase of the SP 800-185 functions that have an output length left to bind. + +use crate::SHAKEParams; +use crate::cshake::CSHAKEInternal; +use crate::shake::SHAKESqueezer; +use crate::xof_utils::right_encode; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; + +/// The squeezing phase of KMACXOF, TupleHashXOF and ParallelHashXOF, which still has a choice to +/// make. +/// +/// Every SP 800-185 function ends its absorbed input with `right_encode(L)`, and the two forms of +/// each function differ only in what goes in there: the fixed-length KMAC, TupleHash and +/// ParallelHash of s. 4.3, 5.3 and 6.3 encode the requested output length, and the XOF forms of +/// s. 4.3.1, 5.3.1 and 6.3.1 encode 0. Nothing else about them differs, so the choice can be left +/// until the caller says how it wants to read -- which is what this type does: +/// +/// * [`XOFSqueezer::do_output`] is the XOF reading. It is the caller saying "give me some bytes and +/// I may be back for more", which only `right_encode(0)` can answer, since a length bound into +/// the sponge cannot be revised once output has begun. +/// * [`XOFSqueezer::do_final`], as the **first** read, is the fixed-length reading. It is the +/// caller saying how many bytes it wants and that it will not be back, so `L` is that length in +/// bits and the result is the fixed-length function of s. 4.3, 5.3 or 6.3 -- the same bytes +/// `KMAC128(K, X, L, S)` produces, not a truncation of `KMACXOF128`. +/// +/// The first read commits: the encoding is in the sponge from then on, so a `do_final` that +/// follows a `do_output` cannot bind anything and simply continues the `right_encode(0)` stream +/// the earlier read already chose. +pub struct LengthBoundSqueezer { + phase: Phase, +} + +/// Which side of the first read this squeezer is on. +enum Phase { + /// Nothing read yet, so `right_encode(L)` is still the caller's to choose. + Unbound(CSHAKEInternal), + /// The encoding has been absorbed and the sponge is producing output. + Squeezing(SHAKESqueezer), + /// Never observed: [`LengthBoundSqueezer::read`] leaves this here only while the value moves + /// from one of the phases above to the other. + Binding, +} + +impl LengthBoundSqueezer { + /// Wraps a cSHAKE with everything but its `right_encode(L)` absorbed. + pub(crate) fn new(cshake: CSHAKEInternal) -> Self { + Self { phase: Phase::Unbound(cshake) } + } + + /// [`XOFSqueezer::do_final_out`] with `L` given rather than taken from the buffer. + /// + /// For the `Hash` view of these functions, whose length is fixed by the type: it binds the + /// nominal output length and then writes as much of it as the caller's buffer has room for, + /// which is what [`Hash::do_final_out`] promises. Going through + /// [`XOFSqueezer::do_final_out`] would bind the buffer's length instead, and a short buffer + /// would then compute a different function rather than truncating this one. + pub(crate) fn do_final_out_with_length(mut self, length_bits: u64, output: &mut [u8]) -> usize { + self.read(length_bits, output) + } + + /// Fills `output` from the stream, absorbing `right_encode(length_bits)` first if this is the + /// first read. `output` is zeroized before anything is written to it. + fn read(&mut self, length_bits: u64, output: &mut [u8]) -> usize { + self.phase = match core::mem::replace(&mut self.phase, Phase::Binding) { + Phase::Unbound(mut cshake) => { + let (buf, len) = right_encode(length_bits); + cshake.do_update(&buf[..len]); + Phase::Squeezing(cshake.into_squeezer()) + } + // An earlier read chose the encoding; this one continues that stream. + committed => committed, + }; + match &mut self.phase { + Phase::Squeezing(squeezer) => squeezer.do_output_out(output), + // The match above turns `Unbound` into `Squeezing` and puts `Binding` back as it found + // it, so neither can be live here. + _ => unreachable!("the first read always leaves the squeezing phase"), + } + } +} + +impl XOFSqueezer for LengthBoundSqueezer { + fn do_output(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.do_output_out(&mut out); + out + } + + /// Reading as a XOF, so `right_encode(0)` if this is the first read (s. 4.3.1, 5.3.1, 6.3.1). + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + self.read(0, output) + } + + fn do_final(self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.do_final_out(&mut out); + out + } + + /// The last read, so if it is also the first, `L` is its length in bits and this is the + /// fixed-length function of s. 4.3, 5.3 or 6.3. After a [`XOFSqueezer::do_output`] the encoding + /// is already in the sponge and this just continues that stream. + fn do_final_out(mut self, output: &mut [u8]) -> usize { + self.read((output.len() as u64) * 8, output) + } +} diff --git a/crypto/sha3/src/lib.rs b/crypto/sha3/src/lib.rs index 7abc6073..272a812a 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -60,7 +60,7 @@ //! ## XOF //! SHA3 offers Extendable-Output Functions in the form of SHAKE, which is accessed through the [`XOF`] trait, //! which is implemented by [`SHAKE128`] and [`SHAKE256`]. -//! The difference from [`Hash`] is that SHAKE can produce output of any length. +//! [`XOF`] extends [`Hash`] -- SHAKE *is* a hash -- and adds the ability to choose the output length. //! //! The simplest usage is via the static functions. The following example produces a 16 byte (128-bit) and 16KiB output: //!``` @@ -68,31 +68,39 @@ //! use bouncycastle_sha3 as sha3; //! //! let data: &[u8] = b"Hello, world!"; -//! let output_16byte: Vec = sha3::SHAKE128::new().hash_xof(data, 16); -//! let output_16KiB: Vec = sha3::SHAKE128::new().hash_xof(data, 16 * 1024); +//! let output_16byte: Vec = sha3::SHAKE128::new().xof(data, 16); +//! let output_16KiB: Vec = sha3::SHAKE128::new().xof(data, 16 * 1024); //! ``` //! -//! As with [`Hash`] above, the [`XOF`] trait has streaming APIs in the form of [`XOF::absorb`] and [`XOF::squeeze`]. -//! Unlike [`Hash::do_final`], [`XOF::squeeze`] can be called multiple times. -//! Note, however, that once you start squeezing, you can no longer absorb more input -- [`XOF::absorb`] -//! will throw a [`HashError::InvalidState`], but the SHAKE object will still be usable for squeezing -//! as if the erroneous `absorb` call never happened. +//! [`XOF`] extends [`Hash`], so SHAKE takes input through [`Hash::do_update`] like any other hash. +//! Output is where they differ: [`XOF::into_squeezer`] ends the input phase and returns an +//! [`XOFSqueezer`](bouncycastle_core::traits::XOFSqueezer), whose +//! [`do_output`](bouncycastle_core::traits::XOFSqueezer::do_output) can be called as many times as you +//! like, each call continuing one stream. +//! +//! Absorbing after output has begun is not an error you can make: `into_squeezer` consumes the +//! SHAKE, so there is no value left to call [`Hash::do_update`] on. //! //! The following code produces the same output as the previous example: //!``` -//! use bouncycastle_core::traits::XOF; +//! use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; //! use bouncycastle_sha3 as sha3; //! //! let data: &[u8] = b"Hello, world!"; //! let mut shake = sha3::SHAKE128::new(); -//! shake.absorb(data).expect("infallible before squeeze"); -//! let output_16byte: Vec = shake.squeeze(16); +//! shake.do_update(data); +//! let output_16byte: Vec = shake.into_squeezer().do_output(16); //! -//! let mut shake = sha3::SHAKE128::new(); +//! let mut shake = sha3::SHAKE128::new().into_squeezer(); //! let mut output_16KiB: Vec = vec![]; -//! for i in 0..16 { output_16KiB.extend_from_slice(&shake.squeeze(1024)) } +//! for i in 0..16 { output_16KiB.extend_from_slice(&shake.do_output(1024)) } //! ``` //! +//! Because [`XOF`] extends [`Hash`], SHAKE can also be used wherever a hash is wanted: +//! [`Hash::do_final`] produces the nominal digest size, 32 bytes for SHAKE128 and 64 for SHAKE256 +//! (the length at which the output carries the full security level), and the one-shot +//! [`Hash::hash`] does the same. +//! //! ## KDF //! SHA3 offers Key Derivation Functions in the form of KDF, which is accessed through the [`KDF`] trait, //! which is implemented by all SHA3 and SHAKE variants. @@ -194,9 +202,15 @@ use bouncycastle_core::key_material::{KeyMaterial, KeyType}; use bouncycastle_core::traits::{Hash, KDF, MAC, Suspendable, XOF}; // end of doc-only imports +mod cshake; mod keccak; +mod kmac; +mod length_bound_squeezer; +mod parallelhash; mod sha3; mod shake; +mod tuplehash; +mod xof_utils; pub mod hmac; @@ -213,10 +227,100 @@ pub const SHA3_512_NAME: &str = "SHA3-512"; pub const SHAKE128_NAME: &str = "SHAKE128"; /// Algorithm name string for SHAKE256, as used by the factories and CLI. pub const SHAKE256_NAME: &str = "SHAKE256"; +/// The name of the cSHAKE128 algorithm (NIST SP 800-185 Sec 3). +pub const CSHAKE128_NAME: &str = "CSHAKE128"; +/// The name of the cSHAKE256 algorithm (NIST SP 800-185 Sec 3). +pub const CSHAKE256_NAME: &str = "CSHAKE256"; +/// The name of the KMAC128 algorithm (NIST SP 800-185 Sec 4). +pub const KMAC128_NAME: &str = "KMAC128"; +/// The name of the KMAC256 algorithm (NIST SP 800-185 Sec 4). +pub const KMAC256_NAME: &str = "KMAC256"; +/// The name of the KMACXOF128 algorithm (NIST SP 800-185 Sec 4.3.1). +pub const KMACXOF128_NAME: &str = "KMACXOF128"; +/// The name of the KMACXOF256 algorithm (NIST SP 800-185 Sec 4.3.1). +pub const KMACXOF256_NAME: &str = "KMACXOF256"; +/// The name of the TupleHash128 algorithm (NIST SP 800-185 Sec 5). +pub const TUPLEHASH128_NAME: &str = "TupleHash128"; +/// The name of the TupleHash256 algorithm (NIST SP 800-185 Sec 5). +pub const TUPLEHASH256_NAME: &str = "TupleHash256"; +/// The name of the TupleHashXOF128 algorithm (NIST SP 800-185 Sec 5.3.1). +pub const TUPLEHASHXOF128_NAME: &str = "TupleHashXOF128"; +/// The name of the TupleHashXOF256 algorithm (NIST SP 800-185 Sec 5.3.1). +pub const TUPLEHASHXOF256_NAME: &str = "TupleHashXOF256"; +/// The name of the ParallelHash128 algorithm (NIST SP 800-185 Sec 6). +pub const PARALLELHASH128_NAME: &str = "ParallelHash128"; +/// The name of the ParallelHash256 algorithm (NIST SP 800-185 Sec 6). +pub const PARALLELHASH256_NAME: &str = "ParallelHash256"; +/// The name of the ParallelHashXOF128 algorithm (NIST SP 800-185 Sec 6.3.1). +pub const PARALLELHASHXOF128_NAME: &str = "ParallelHashXOF128"; +/// The name of the ParallelHashXOF256 algorithm (NIST SP 800-185 Sec 6.3.1). +pub const PARALLELHASHXOF256_NAME: &str = "ParallelHashXOF256"; /*** pub types ***/ +pub use cshake::CSHAKEInternal; +pub use kmac::{KMACInternal, KMACXOFInternal}; +pub use length_bound_squeezer::LengthBoundSqueezer; +pub use parallelhash::{ParallelHashInternal, ParallelHashXOFInternal}; pub use sha3::SHA3Internal; -pub use shake::SHAKEInternal; +pub use tuplehash::{TupleHashInternal, TupleHashXOFInternal}; + +/// cSHAKE128: the customizable SHAKE128 of NIST SP 800-185 Sec 3, at a 128-bit security strength. +/// +/// Construct with [`CSHAKEInternal::new`], passing the function-name string `N` (reserved for +/// NIST, normally empty) and the customization string `S`. With both empty this is exactly +/// [`SHAKE128`]. +pub type CSHAKE128 = CSHAKEInternal; +/// cSHAKE256: the customizable SHAKE256 of NIST SP 800-185 Sec 3, at a 256-bit security strength. +/// +/// See [`CSHAKE128`]. +pub type CSHAKE256 = CSHAKEInternal; + +/// KMAC128: the Keccak MAC of NIST SP 800-185 Sec 4, at a 128-bit security strength. +/// +/// [`bouncycastle_core::traits::MAC::new`] gives the common case -- no customization, 32-byte +/// output. [`KMACInternal::new_with_params`] chooses the customization string and output length, +/// [`KMACXOF128`] is the separate arbitrary-length function of Sec 4.3.1. +pub type KMAC128 = KMACInternal; +/// KMAC256: the Keccak MAC of NIST SP 800-185 Sec 4, at a 256-bit security strength. +/// +/// See [`KMAC128`]. The nominal output length is 64 bytes. +pub type KMAC256 = KMACInternal; + +/// KMACXOF128: the arbitrary-output-length KMAC of NIST SP 800-185 Sec 4.3.1. +/// +/// A keyed [`XOF`]. Distinct from [`KMAC128`], and not a longer +/// view of it: over the same inputs the two produce unrelated output. +pub type KMACXOF128 = KMACXOFInternal; +/// KMACXOF256: the arbitrary-output-length KMAC of NIST SP 800-185 Sec 4.3.1. +/// +/// See [`KMACXOF128`]. +pub type KMACXOF256 = KMACXOFInternal; + +/// TupleHash128: the unambiguous tuple hash of NIST SP 800-185 Sec 5, 128-bit strength. +/// +/// Each [`Hash::do_update`] call appends one *tuple +/// element*, not a run of bytes -- so unlike every other hash here, the chunking is part of the +/// input. See [`TupleHashInternal`]. +pub type TUPLEHASH128 = TupleHashInternal; +/// TupleHash256: see [`TUPLEHASH128`]. +pub type TUPLEHASH256 = TupleHashInternal; +/// TupleHashXOF128: the arbitrary-output-length TupleHash of Sec 5.3.1. +pub type TUPLEHASHXOF128 = TupleHashXOFInternal; +/// TupleHashXOF256: see [`TUPLEHASHXOF128`]. +pub type TUPLEHASHXOF256 = TupleHashXOFInternal; + +/// ParallelHash128: the parallelisable hash of NIST SP 800-185 Sec 6, 128-bit strength. +/// +/// The block size `B` is part of the function, not a tuning knob: the same message under a +/// different `B` hashes differently. See [`ParallelHashInternal`]. +pub type PARALLELHASH128 = ParallelHashInternal; +/// ParallelHash256: see [`PARALLELHASH128`]. +pub type PARALLELHASH256 = ParallelHashInternal; +/// ParallelHashXOF128: the arbitrary-output-length ParallelHash of Sec 6.3.1. +pub type PARALLELHASHXOF128 = ParallelHashXOFInternal; +/// ParallelHashXOF256: see [`PARALLELHASHXOF128`]. +pub type PARALLELHASHXOF256 = ParallelHashXOFInternal; +pub use shake::{SHAKEInternal, SHAKESqueezer}; pub use keccak::SUSPENDED_SHA3_STATE_LEN; @@ -236,7 +340,7 @@ pub type SHAKE256 = SHAKEInternal; /*** Param traits ***/ /// Private trait on purpose so that only the NIST-approved params can be used. -trait SHA3Params: HashAlgParams { +trait SHA3Params: HashAlgParams + Clone { const SIZE: KeccakSize; /// A tag, unique across all SHA3 *and* SHAKE variants, identifying which variant produced a /// serialized state. Distinguishing same-rate variants (e.g. SHA3-256 vs SHAKE256) requires @@ -339,10 +443,27 @@ impl AlgorithmOID for SHA3_512 { &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0a]; } -trait SHAKEParams: Algorithm { +trait SHAKEParams: Algorithm + Clone { const SIZE: KeccakSize; /// See [`SHA3Params::STATE_TAG`]. Must be distinct from every SHA3 *and* SHAKE variant's tag. const STATE_TAG: u8; + /// The sponge rate in bytes: `(1600 - 2c) / 8`, 168 for SHAKE128 and 136 for SHAKE256. + /// SP 800-185 Sec 3.3 pads cSHAKE's encoded strings to a multiple of it. + const RATE_BYTES: usize = (1600 - ((Self::SIZE as usize) << 1)) / 8; + /// The name of the cSHAKE built on this parameter set. + const CSHAKE_ALG_NAME: &'static str; + /// The name of the KMAC built on this parameter set. + const KMAC_ALG_NAME: &'static str; + /// The name of the KMACXOF built on this parameter set. + const KMACXOF_ALG_NAME: &'static str; + /// The name of the TupleHash built on this parameter set. + const TUPLEHASH_ALG_NAME: &'static str; + /// The name of the TupleHashXOF built on this parameter set. + const TUPLEHASHXOF_ALG_NAME: &'static str; + /// The name of the ParallelHash built on this parameter set. + const PARALLELHASH_ALG_NAME: &'static str; + /// The name of the ParallelHashXOF built on this parameter set. + const PARALLELHASHXOF_ALG_NAME: &'static str; } /// The parameters for SHAKE128. #[derive(Clone)] @@ -354,6 +475,13 @@ impl Algorithm for SHAKE128Params { impl SHAKEParams for SHAKE128Params { const SIZE: KeccakSize = KeccakSize::_128; const STATE_TAG: u8 = 5; + const CSHAKE_ALG_NAME: &'static str = CSHAKE128_NAME; + const KMAC_ALG_NAME: &'static str = KMAC128_NAME; + const KMACXOF_ALG_NAME: &'static str = KMACXOF128_NAME; + const TUPLEHASH_ALG_NAME: &'static str = TUPLEHASH128_NAME; + const TUPLEHASHXOF_ALG_NAME: &'static str = TUPLEHASHXOF128_NAME; + const PARALLELHASH_ALG_NAME: &'static str = PARALLELHASH128_NAME; + const PARALLELHASHXOF_ALG_NAME: &'static str = PARALLELHASHXOF128_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake128 { hashAlgs 11 } impl AlgorithmOID for SHAKE128 { @@ -371,6 +499,13 @@ impl Algorithm for SHAKE256Params { impl SHAKEParams for SHAKE256Params { const SIZE: KeccakSize = KeccakSize::_256; const STATE_TAG: u8 = 6; + const CSHAKE_ALG_NAME: &'static str = CSHAKE256_NAME; + const KMAC_ALG_NAME: &'static str = KMAC256_NAME; + const KMACXOF_ALG_NAME: &'static str = KMACXOF256_NAME; + const TUPLEHASH_ALG_NAME: &'static str = TUPLEHASH256_NAME; + const TUPLEHASHXOF_ALG_NAME: &'static str = TUPLEHASHXOF256_NAME; + const PARALLELHASH_ALG_NAME: &'static str = PARALLELHASH256_NAME; + const PARALLELHASHXOF_ALG_NAME: &'static str = PARALLELHASHXOF256_NAME; } /// Assigned by NIST in the Computer Security Objects Register: id-shake256 { hashAlgs 12 } impl AlgorithmOID for SHAKE256 { diff --git a/crypto/sha3/src/parallelhash.rs b/crypto/sha3/src/parallelhash.rs new file mode 100644 index 00000000..7c17e8a0 --- /dev/null +++ b/crypto/sha3/src/parallelhash.rs @@ -0,0 +1,332 @@ +//! ParallelHash, the parallelisable hash of NIST SP 800-185 Sec 6. + +use crate::SHAKEParams; +use crate::cshake::{CSHAKEInternal, absorb_left_encode_into}; +use crate::length_bound_squeezer::LengthBoundSqueezer; +use crate::shake::SHAKEInternal; +use crate::xof_utils::right_encode; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; + +/// The function-name string every ParallelHash binds, per SP 800-185 Sec 6.3. +const PARALLELHASH_FUNCTION_NAME: &[u8] = b"ParallelHash"; + +/// The shared machinery of [`ParallelHashInternal`] and [`ParallelHashXOFInternal`]: the outer +/// cSHAKE, the block buffer, and the count of blocks hashed so far. +#[derive(Clone)] +struct ParallelState { + cshake: CSHAKEInternal, + block_size: usize, + /// The partial block still being filled. Bounded by `block_size`, which the caller chooses at + /// construction, so this cannot be a const-sized array. + buffer: Vec, + blocks: u64, +} + +impl ParallelState { + /// Each block is hashed to `2c` bits -- 256 for ParallelHash128, 512 for ParallelHash256 + /// (Sec 6.3 step 3, the `256` and `512` in the inner cSHAKE calls). + const INNER_LEN: usize = (PARAMS::SIZE as usize) / 4; + + fn new(block_size: usize, customization: &[u8]) -> Self { + assert!(block_size > 0, "SP 800-185 Sec 6.2: the block size B must be positive"); + let mut cshake = CSHAKEInternal::new(PARALLELHASH_FUNCTION_NAME, customization); + // Step 2: z = left_encode(B). + absorb_left_encode_into(&mut cshake, block_size as u64); + Self { cshake, block_size, buffer: Vec::new(), blocks: 0 } + } + + /// Step 3 for one whole block: hash it and absorb the digest into the outer cSHAKE. + /// + /// The inner call is `cSHAKE(block, 2c, "", "")`, which by Sec 3.3 step 1 is plain SHAKE -- + /// so SHAKE is what is used here. + fn absorb_block(&mut self, block: &[u8]) { + let inner = SHAKEInternal::::new().xof(block, Self::INNER_LEN); + self.cshake.do_update(&inner); + self.blocks += 1; + } + + fn do_update(&mut self, mut data: &[u8]) { + // Top up a partial block first, then take whole blocks straight from `data` so that a + // caller feeding block-aligned input never copies. + if !self.buffer.is_empty() { + let need = self.block_size - self.buffer.len(); + let take = need.min(data.len()); + self.buffer.extend_from_slice(&data[..take]); + data = &data[take..]; + if self.buffer.len() == self.block_size { + let block = core::mem::take(&mut self.buffer); + self.absorb_block(&block); + } + } + while data.len() >= self.block_size { + let (block, rest) = data.split_at(self.block_size); + self.absorb_block(block); + data = rest; + } + self.buffer.extend_from_slice(data); + } + + /// Flushes the short final block and binds the block count: step 3, and the `right_encode(n)` + /// half of step 4. + /// + /// The `right_encode(L)` that completes step 4 is left to the caller, because which `L` it + /// carries is not settled here: the fixed-length function knows it up front ([`Self::finish`]), + /// and the XOF leaves it to the first read ([`LengthBoundSqueezer`]). + fn finish_blocks(mut self) -> CSHAKEInternal { + if !self.buffer.is_empty() { + let block = core::mem::take(&mut self.buffer); + self.absorb_block(&block); + } + // Step 4: z = z || right_encode(n) ... + let (buf, len) = right_encode(self.blocks); + self.cshake.do_update(&buf[..len]); + self.cshake + } + + /// [`Self::finish_blocks`], then the `right_encode(L)` that completes step 4. + /// + /// `length_bits` is the requested output length of the fixed-length function of Sec 6.3. + fn finish(self, length_bits: u64) -> CSHAKEInternal { + let mut cshake = self.finish_blocks(); + let (buf, len) = right_encode(length_bits); + cshake.do_update(&buf[..len]); + cshake + } +} + +/// Internal struct for ParallelHash. Use [`crate::PARALLELHASH128`] or [`crate::PARALLELHASH256`]. +/// +/// ParallelHash splits the message into `B`-byte blocks, hashes each independently, and hashes the +/// concatenated digests (Sec 6.1). The point is that the per-block hashes can be computed in +/// parallel on long inputs; this implementation is sequential, which gives identical output. +/// +/// ```text +/// ParallelHash128(X, B, L, S) = cSHAKE128(left_encode(B) || SHAKE128(X[0], 256) || ... +/// || right_encode(n) || right_encode(L), +/// L, "ParallelHash", S) +/// ``` +/// +/// # The block size is part of the hash +/// +/// `B` is bound by `left_encode(B)`, so the same message under a different block size gives an +/// unrelated result. It is a parameter of the function, not a tuning knob. +/// +/// Unlike [`crate::TUPLEHASH128`], `do_update` here *is* ordinary byte-wise streaming: the block +/// boundaries come from `B`, not from how the caller chunks its calls. +#[derive(Clone)] +pub struct ParallelHashInternal { + state: ParallelState, + output_len: usize, +} + +impl Algorithm for ParallelHashInternal { + const ALG_NAME: &'static str = PARAMS::PARALLELHASH_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl ParallelHashInternal { + /// A new ParallelHash over `block_size`-byte blocks, producing `output_len` bytes. + /// + /// # Panics + /// If `block_size` is zero, which Sec 6.2 forbids (`0 < B`). + pub fn new(block_size: usize, customization: &[u8], output_len: usize) -> Self { + Self { state: ParallelState::new(block_size, customization), output_len } + } +} + +impl Hash for ParallelHashInternal { + fn block_bitlen(&self) -> usize { + self.state.cshake.block_bitlen() + } + + fn output_len(&self) -> usize { + self.output_len + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + fn do_update(&mut self, data: &[u8]) { + self.state.do_update(data); + } + + fn do_final(self) -> Vec { + let n = self.output_len; + self.state.finish((n as u64) * 8).into_squeezer().do_output(n) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + let n = self.output_len; + // Per Hash::do_final_out: a short buffer is filled and the digest truncated, a long one + // takes the digest in its first output_len bytes and zeros after it. `n` is bound into the + // computation either way -- the buffer's length never reaches the length encoding, so a + // truncated read is this ParallelHash cut short, not the ParallelHash of a shorter length. + let written = n.min(output.len()); + output[written..].fill(0); + self.state.finish((n as u64) * 8).into_squeezer().do_output_out(&mut output[..written]) + } + + /// # Errors + /// Always [`HashError::InvalidLength`] for a non-zero `num_bits`: the block count and length + /// encodings have to follow the message, which a partial final byte would prevent. + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "ParallelHash cannot take a partial final byte: the encodings must follow", + )); + } + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::from_bits(PARAMS::SIZE as usize) + } +} + +/// Internal struct for ParallelHashXOF (Sec 6.3.1). Use [`crate::PARALLELHASHXOF128`] or +/// [`crate::PARALLELHASHXOF256`]. +/// +/// Binds `right_encode(0)` in place of the output length, so -- as for KMACXOF and TupleHashXOF -- +/// it is a different function from the fixed-length one, and its output at one length is a prefix +/// of its output at a longer one. +#[derive(Clone)] +pub struct ParallelHashXOFInternal { + state: ParallelState, +} + +impl Algorithm for ParallelHashXOFInternal { + const ALG_NAME: &'static str = PARAMS::PARALLELHASHXOF_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl ParallelHashXOFInternal { + /// A new ParallelHashXOF over `block_size`-byte blocks. + /// + /// # Panics + /// If `block_size` is zero (Sec 6.2). + pub fn new(block_size: usize, customization: &[u8]) -> Self { + Self { state: ParallelState::new(block_size, customization) } + } +} + +impl Hash for ParallelHashXOFInternal { + fn block_bitlen(&self) -> usize { + self.state.cshake.block_bitlen() + } + + /// The nominal length, 32 or 64 bytes: twice the security strength, the length at which the + /// output carries that strength in full. Bound by the [`Hash`] view and not by the XOF one. + fn output_len(&self) -> usize { + self.state.cshake.output_len() + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + fn do_update(&mut self, data: &[u8]) { + self.state.do_update(data); + } + + /// A final read at the nominal length, so `L` is bound: this is the fixed-length ParallelHash + /// of Sec 6.3 at `n = ` [`Hash::output_len`], not a prefix of the ParallelHashXOF stream. + fn do_final(self) -> Vec { + let n = self.output_len(); + self.into_squeezer().do_final(n) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + let n = self.output_len(); + // Per Hash::do_final_out, as for the fixed-length form: a short buffer truncates this + // ParallelHash rather than computing the ParallelHash of a shorter length, because `n` is + // what reaches right_encode, not the buffer's length. + let written = n.min(output.len()); + output[written..].fill(0); + self.into_squeezer().do_final_out_with_length((n as u64) * 8, &mut output[..written]) + } + + /// # Errors + /// Always [`HashError::InvalidLength`] for a non-zero `num_bits`; see + /// [`ParallelHashInternal::do_final_partial_bits`]. + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len()]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "ParallelHashXOF cannot take a partial final byte: the encodings must follow", + )); + } + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::from_bits(PARAMS::SIZE as usize) + } +} + +impl XOF for ParallelHashXOFInternal { + type Squeezer = LengthBoundSqueezer; + + /// The block count of Sec 6.3.1 step 4 is bound here; the `right_encode` that follows it is + /// not, because whether it carries 0 or the length of a final read is + /// [`LengthBoundSqueezer`]'s decision. + fn into_squeezer(self) -> Self::Squeezer { + LengthBoundSqueezer::new(self.state.finish_blocks()) + } + + fn into_squeezer_partial_bits( + self, + _partial_byte: u8, + num_bits: usize, + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "ParallelHashXOF cannot take a partial final byte: the encodings must follow", + )); + } + Ok(self.into_squeezer()) + } +} diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index aabba93b..e634c551 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -8,7 +8,7 @@ use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; -use bouncycastle_core::traits::{Algorithm, KDF, Suspendable, XOF}; +use bouncycastle_core::traits::{Algorithm, Hash, KDF, Suspendable, XOF, XOFSqueezer}; use bouncycastle_utils::{max, min}; /// Internal struct for SHAKE. @@ -54,32 +54,46 @@ impl SHAKEInternal { } } - /// Swallows errors and simply returns an empty Vec if the hashes fails for whatever reason. fn hash_internal(mut self, data: &[u8], result_len: usize) -> Vec { - // The absorb fails if this object has already begun squeezing, which the caller is free to - // have done: these one-shot APIs take `self`, they do not require a fresh object. - if self.absorb(data).is_err() { - return Vec::new(); - } - self.squeeze(result_len) + self.keccak.absorb(data); + self.into_squeezer().do_output(result_len) } - /// Swallows errors and simply returns 0, leaving `output` zeroized, if the hashes fails for - /// whatever reason. fn hash_internal_out(mut self, data: &[u8], output: &mut [u8]) -> usize { - output.fill(0); + self.keccak.absorb(data); + self.into_squeezer().do_output_out(output) + } + + /// Ends absorbing with a caller-chosen domain separator and returns the squeezing half. + /// + /// SHAKE uses "1111" (FIPS 202 s. 6.2), but cSHAKE uses "00" (SP 800-185 s. 3.3, the `00` in + /// the `KECCAK[c](... || X || 00, L)` branch), so the suffix cannot be baked in here. Crate + /// internal: callers outside pick a function, and the function picks its own separator. + /// + /// Infallible for the same reason [`Hash::do_update`] is: a `SHAKEInternal` a caller can name + /// has never squeezed, so the queue is byte-aligned and `absorb_bits` cannot reject it. + pub(crate) fn into_squeezer_with_suffix( + mut self, + suffix: u8, + num_bits: usize, + ) -> SHAKESqueezer { + self.keccak + .absorb_bits(suffix, num_bits) + .expect("a sponge that has not squeezed can absorb a domain separator"); + SHAKESqueezer { shake: self } + } - // The absorb fails if this object has already begun squeezing, which the caller is free to - // have done: these one-shot APIs take `self`, they do not require a fresh object. - if self.absorb(data).is_err() { - return 0; + /// Produces the next bytes of the output stream, applying the SHAKE "1111" domain separator + /// (FIPS 202 s. 6.2) on the first call. Reached only through [`SHAKESqueezer`], so the caller + /// cannot interleave this with absorbing. + fn squeeze_internal_out(&mut self, output: &mut [u8]) -> usize { + output.fill(0); + if !self.keccak.squeezing { + self.keccak.absorb_bits(0x0F, 4).expect("Absorb_bits failed"); } - self.squeeze_out(output) + self.keccak.squeeze(output) } - /// Returns [`KDFError::HashError`] wrapping a [`HashError::InvalidState`] if this object has - /// already begun squeezing, since key material absorbed after that point would not contribute - /// to the derived key. fn mix_key_internal(&mut self, key: &impl KeyMaterialTrait) -> Result<(), KDFError> { // track the strongest input key type self.kdf_key_type = *max(&self.kdf_key_type, &key.key_type()); @@ -95,9 +109,8 @@ impl SHAKEInternal { ); } - // The absorb fails if this object has already begun squeezing, which the caller is free to - // have done: the KDF entry points take `self`, they do not require a fresh object. - Ok(self.absorb(key.ref_to_bytes())?) + self.keccak.absorb(key.ref_to_bytes()); + Ok(()) } fn derive_key_final_internal( @@ -133,12 +146,11 @@ impl SHAKEInternal { self.kdf_security_strength = SecurityStrength::None; // BytesLowEntropy can't have a securtiy level. } - // As in mix_key_internal(): the absorb fails if this object has already begun squeezing. - self.absorb(additional_input)?; + self.keccak.absorb(additional_input); let mut bytes_written: usize = 0; key_material::do_hazardous_operations(output_key, |output_key| { - bytes_written = self.squeeze_out( + bytes_written = self.squeeze_internal_out( output_key.ref_to_bytes_mut().expect("Infallible within do_hazardous_operations"), ); output_key.set_key_len(bytes_written) @@ -192,6 +204,14 @@ impl Suspendable for SHAKEInterna let (keccak, kdf_key_type, kdf_security_strength, kdf_entropy) = deserialize_sha3_family_state(input, PARAMS::STATE_TAG, rate)?; + // A SHAKEInternal accepts input, so it must never be rebuilt in the squeezing phase -- + // that is the invariant `Hash::do_update` relies on. A suspended squeezing sponge is a + // SHAKESqueezer; resume it as one. + if keccak.squeezing { + // InvalidData rather than a new variant: for this type the phase byte is simply wrong. + return Err(SuspendableError::InvalidData); + } + Ok(SHAKEInternal { _phantomdata: core::marker::PhantomData, keccak, @@ -275,122 +295,251 @@ impl Default for SHAKEInternal { } } -impl XOF for SHAKEInternal { - fn hash_xof(self, data: &[u8], result_len: usize) -> Vec { - self.hash_internal(data, result_len) +/// The squeezing half of SHAKE: what [`XOF::into_squeezer`] hands back. +/// +/// It owns the sponge, so the absorbing value is gone by the time this exists. That is the whole +/// point: [`Hash::do_update`] cannot be called on a SHAKE that has begun producing output, because +/// there is no longer a SHAKE to call it on. +pub struct SHAKESqueezer { + shake: SHAKEInternal, +} + +impl XOFSqueezer for SHAKESqueezer { + fn do_output(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.do_output_out(&mut out); + out } - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { - // hash_internal_out zeroizes `output` before writing. - self.hash_internal_out(data, output) + fn do_output_out(&mut self, output: &mut [u8]) -> usize { + self.shake.squeeze_internal_out(output) } +} - /// This can throw a [`HashError::InvalidState`] if called after squeezing has begun, - /// but is safe to consider infallible otherwise -- IE feel free to use `.unwrap()` or `.expect()` - /// on the result if you are confident that your code cannot call `absorb` after squeezing. - /// - /// A rejected call leaves the SHAKE object untouched so the output stream continues consistently. - /// IE it is safe to attempt to feed in more input and do nothing if the absorb fails - /// ("safe" in the sense that it won't panic, but it may still produce an incorrect output which - /// could be insecure in the sense of being predictable or low-entropy). - fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { - // A sponge XOF cannot return to absorbing once squeezing has begun (FIPS 202 defines SHAKE as - // a single function of the whole message; re-absorbing would be an unapproved duplex). - if self.keccak.squeezing { - return Err(HashError::InvalidState("cannot absorb after squeezing has begun")); +impl Clone for SHAKESqueezer { + fn clone(&self) -> Self { + Self { shake: self.shake.clone() } + } +} + +/// The squeezing phase suspends and resumes just as the absorbing phase does, so a long output +/// stream can be paused. The serialized form is the same one [`SHAKEInternal`] writes -- the +/// keccak state records which phase it is in -- so the two `from_suspended` implementations +/// accept exactly the states the other rejects. +impl Suspendable for SHAKESqueezer { + fn suspend(self) -> [u8; SUSPENDED_SHA3_STATE_LEN] { + self.shake.suspend() + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_SHA3_STATE_LEN], + ) -> Result { + let input: &[u8; SHA3_FAMILY_STATE_LEN] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + let rate = 1600 - ((PARAMS::SIZE as usize) << 1); + let (keccak, kdf_key_type, kdf_security_strength, kdf_entropy) = + deserialize_sha3_family_state(input, PARAMS::STATE_TAG, rate)?; + + // The mirror of the check in `SHAKEInternal::from_suspended`: a state that had not begun + // producing output is still absorbing, and resuming it here would skip the domain suffix. + if !keccak.squeezing { + return Err(SuspendableError::InvalidData); } + + Ok(Self { + shake: SHAKEInternal { + _phantomdata: core::marker::PhantomData, + keccak, + kdf_key_type, + kdf_security_strength, + kdf_entropy, + }, + }) + } +} + +impl Hash for SHAKEInternal { + /// The sponge rate in bits: `1600 - 2c`, where the capacity `c` is twice the security level + /// (FIPS 202 Table 3 -- 1344 bits for SHAKE128, 1088 for SHAKE256). + fn block_bitlen(&self) -> usize { + 1600 - ((PARAMS::SIZE as usize) << 1) + } + + /// The nominal digest size: 32 bytes for SHAKE128, 64 for SHAKE256. + /// + /// A XOF has no inherent output length, so this is a convention rather than a property of the + /// function: it is twice the security strength, the length at which the output carries the + /// full security level. + fn output_len(&self) -> usize { + (PARAMS::SIZE as usize) / 4 + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + /// Infallible, and this is a fact about the type rather than a promise. + /// + /// Absorbing after squeezing has begun would be wrong -- FIPS 202 defines SHAKE as a single + /// function of the whole message, so re-absorbing would be an unapproved duplex -- and it cannot + /// be expressed: producing output goes through [`XOF::into_squeezer`], which consumes the value, + /// and every `KDF` entry point takes `self` by value too. A `SHAKEInternal` a caller can still + /// name has therefore never squeezed. + fn do_update(&mut self, data: &[u8]) { + // Pins the invariant the doc above argues for, so a future change that lets a squeezing + // SHAKE escape fails the test suite rather than silently corrupting the sponge. + debug_assert!(!self.keccak.squeezing, "a reachable SHAKEInternal has never squeezed"); self.keccak.absorb(data); - Ok(()) } - /// Switches to squeezing. - fn absorb_last_partial_byte( - &mut self, - partial_byte: u8, - num_partial_bits: usize, - ) -> Result<(), HashError> { - // Same phase rule as absorb(): reject a partial-byte absorb once squeezing has begun. Checked - // before any state mutation so a rejected call leaves the sponge untouched. - if self.keccak.squeezing { - return Err(HashError::InvalidState("cannot absorb after squeezing has begun")); - } - // A partial byte has at most 7 bits; 0 means the message ends on a byte boundary. - if num_partial_bits > 7 { - return Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")); - } - // Mutants note: This is just bit-setting into empty space. - // It works the same regardless of whether it's OR or XOR. - // The public convention puts the message bits in the most significant bits of partial_byte, - // leading bit first (ASN.1 BIT STRING order, X.690 s. 8.6.2.1). Keccak absorbs a byte - // LSB-first: FIPS 202 Algorithm 10 (h2b) step 3 sets message bit T[8i + j] = b_ij, the bit - // of weight 2^j in byte i. So reverse the bit order and keep the low num_partial_bits bits. - let message_bits = (partial_byte.reverse_bits() as u16) & ((1 << num_partial_bits) - 1); - let mut final_input: u16 = message_bits | (0x0F << num_partial_bits); - let mut final_bits = num_partial_bits + 4; + /// A final read at the nominal length: [`output_len`](Self::output_len) bytes, 32 for + /// SHAKE128 and 64 for SHAKE256, twice the security strength. + /// + /// FIPS 202 gives SHAKE no length to bind -- the output length is not an input to the function + /// -- so these are the same bytes the squeezer produces. What the `Hash` view fixes is *how + /// many*: a hash has one output length and it is this one. Ask for another through the XOF. + fn do_final(self) -> Vec { + let n = self.output_len(); + self.into_squeezer().do_final(n) + } - if final_bits >= 8 { - self.keccak.absorb(&[final_input as u8]); - final_bits -= 8; - final_input >>= 8; - } + fn do_final_out(self, output: &mut [u8]) -> usize { + let n = self.output_len(); + // Per Hash::do_final_out: a short buffer is filled and the output truncated, a long one + // takes it in its first output_len bytes and zeros after. To fill a longer buffer, use the + // XOF spelling -- XOF::xof_out and XOFSqueezer::do_output_out take their length from the + // buffer, which is exactly the difference between a XOF and a hash. + let written = n.min(output.len()); + output[written..].fill(0); + self.into_squeezer().do_final_out(&mut output[..written]) + } - // Infallible: guarded above (not squeezing), the queue is byte-aligned here, and final_bits is - // in 0..=7 by construction. - self.keccak.absorb_bits(final_input as u8, final_bits).expect("Absorb failed."); + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len()]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } - Ok(()) + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + let n = self.output_len(); + // Validated before anything is written, so a rejected call leaves `output` untouched. + let squeezer = self.into_squeezer_partial_bits(partial_byte, num_bits)?; + // The buffer rule of do_final_out applies here too: output_len bytes, then zeros. + let written = n.min(output.len()); + output[written..].fill(0); + Ok(squeezer.do_final_out(&mut output[..written])) } - fn squeeze(&mut self, num_bytes: usize) -> Vec { - let mut out: Vec = vec![0u8; num_bytes]; - self.squeeze_out(&mut out); - out + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::from_bits(PARAMS::SIZE as usize) } +} - fn squeeze_out(&mut self, output: &mut [u8]) -> usize { - output.fill(0); +/// The absorb-then-squeeze rule, as a compile error rather than a runtime one. +/// +/// ```compile_fail +/// use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; +/// use bouncycastle_sha3::SHAKE128; +/// +/// let mut shake = SHAKE128::new(); +/// shake.do_update(b"abc"); +/// let mut out = shake.into_squeezer(); +/// let _ = out.do_output(32); +/// shake.do_update(b"more"); // `shake` was moved by into_squeezer() +/// ``` +/// +/// The same value used correctly: +/// +/// ``` +/// use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; +/// use bouncycastle_sha3::SHAKE128; +/// +/// let mut shake = SHAKE128::new(); +/// shake.do_update(b"abc"); +/// let mut out = shake.into_squeezer(); +/// assert_eq!(out.do_output(32).len(), 32); +/// ``` +impl XOF for SHAKEInternal { + type Squeezer = SHAKESqueezer; - if !self.keccak.squeezing { - self.keccak.absorb_bits(0x0F, 4).expect("Absorb_bits failed"); - }; + fn into_squeezer(self) -> Self::Squeezer { + // The SHAKE domain separator, "1111" (FIPS 202 s. 6.2). + self.into_squeezer_with_suffix(0x0F, 4) + } - self.keccak.squeeze(output) + fn into_squeezer_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result { + // The SHAKE domain separator, "1111" (FIPS 202 s. 6.2). + self.into_squeezer_partial_bits_with_suffix(partial_byte, num_bits, 0x0F, 4) } - fn squeeze_partial_byte_final(self, num_bits: usize) -> Result { - let mut output: u8 = 0; - self.squeeze_partial_byte_final_out(num_bits, &mut output)?; - Ok(output) + fn xof(self, data: &[u8], result_len: usize) -> Vec { + self.hash_internal(data, result_len) } - /// Result is the number of bits squezed into `output`. - fn squeeze_partial_byte_final_out( + fn xof_out(self, data: &[u8], output: &mut [u8]) -> usize { + // hash_internal_out zeroizes `output` before writing. + self.hash_internal_out(data, output) + } +} + +impl SHAKEInternal { + /// [`XOF::into_squeezer_partial_bits`] with a caller-chosen domain separator, for cSHAKE. + /// + /// The message's trailing bits and the separator are absorbed together, so the separator + /// cannot simply be applied afterwards -- hence the suffix travels in rather than being + /// hardcoded. See [`Self::into_squeezer_with_suffix`]. + pub(crate) fn into_squeezer_partial_bits_with_suffix( mut self, + partial_byte: u8, num_bits: usize, - output: &mut u8, - ) -> Result<(), HashError> { - // A partial byte has at most 7 bits; 0 means no bits are requested. Checked before the shift - // below, which would overflow for num_bits >= 8. + suffix: u8, + suffix_bits: usize, + ) -> Result, HashError> { + // A partial byte has at most 7 bits; 0 means the message ends on a byte boundary. + // Checked before any state change, so a rejected call leaves the sponge untouched. if num_bits > 7 { return Err(HashError::InvalidLength("num_bits must be in the range [0,7]")); } + // Mutants note: this is bit-setting into empty space, so OR and XOR behave identically. + // The public convention puts the message bits in the most significant bits of partial_byte, + // leading bit first (ASN.1 BIT STRING order, X.690 s. 8.6.2.1). Keccak absorbs a byte + // LSB-first: FIPS 202 Algorithm 10 (h2b) step 3 sets message bit T[8i + j] = b_ij, the bit + // of weight 2^j in byte i. So reverse the bit order and keep the low num_bits bits. + let message_bits = (partial_byte.reverse_bits() as u16) & ((1 << num_bits) - 1); + let mut final_input: u16 = message_bits | ((suffix as u16) << num_bits); + let mut final_bits = num_bits + suffix_bits; - *output = 0; - - // Via squeeze_out() so the SHAKE "1111" suffix (FIPS 202 s. 6.2) is applied on a first squeeze. - let mut buf = [0u8; 1]; - self.squeeze_out(&mut buf); + if final_bits >= 8 { + self.keccak.absorb(&[final_input as u8]); + final_bits -= 8; + final_input >>= 8; + } - // Keccak emits the bits of an output byte LSB-first (FIPS 202 Algorithm 11, b2h: output bit - // T[8i + j] has weight 2^j), and the public convention returns them as the final octet of an - // ASN.1 BIT STRING (X.690 s. 8.6.2.1): first bit in the MSB, unused low bits zero. So reverse - // the bit order and keep the top num_bits bits. The mask is built in u16 so that num_bits == 0 - // cannot overflow (0xFF00 >> 0 truncates to 0x00). - *output = buf[0].reverse_bits() & ((0xFF00u16 >> num_bits) as u8); - Ok(()) - } + // Infallible: this value has never squeezed, the queue is byte-aligned here, and final_bits + // is in 0..=7 by construction. + self.keccak.absorb_bits(final_input as u8, final_bits).expect("Absorb failed."); - fn max_security_strength(&self) -> SecurityStrength { - SecurityStrength::from_bits(PARAMS::SIZE as usize) + // The suffix is already folded into final_input above, so the sponge is finished + // absorbing; wrap it without applying the suffix a second time. + Ok(SHAKESqueezer { shake: self }) } } diff --git a/crypto/sha3/src/tuplehash.rs b/crypto/sha3/src/tuplehash.rs new file mode 100644 index 00000000..c773f0bc --- /dev/null +++ b/crypto/sha3/src/tuplehash.rs @@ -0,0 +1,279 @@ +//! TupleHash, the tuple-hashing function of NIST SP 800-185 Sec 5. + +use crate::SHAKEParams; +use crate::cshake::{CSHAKEInternal, absorb_encoded_string_into}; +use crate::length_bound_squeezer::LengthBoundSqueezer; +use crate::xof_utils::right_encode; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; + +/// The function-name string every TupleHash binds, per SP 800-185 Sec 5.3. +const TUPLEHASH_FUNCTION_NAME: &[u8] = b"TupleHash"; + +/// Internal struct for TupleHash. Use [`crate::TUPLEHASH128`] or [`crate::TUPLEHASH256`]. +/// +/// TupleHash hashes a *sequence of strings* unambiguously (Sec 5.1): each element is length- +/// prefixed with `encode_string` before absorption, so the boundaries between elements are part of +/// the computation. `("abc", "d")` and `("ab", "cd")` therefore hash differently, even though the +/// concatenations are identical -- which is the whole point of the function. +/// +/// ```text +/// TupleHash128(X, L, S) = cSHAKE128(encode_string(X[0]) || ... || right_encode(L), +/// L, "TupleHash", S) +/// ``` +/// +/// # `do_update` appends an element, it does not append bytes +/// +/// This is the one place TupleHash departs from the usual [`Hash`] contract. For every other hash, +/// feeding the input in pieces gives the same answer as feeding it at once; here each +/// [`Hash::do_update`] call is one tuple element, so the chunking *is* the input. It is worth +/// stating plainly, because code that treats a `TupleHash` as an interchangeable `Hash` and +/// re-chunks its input will silently compute something else. +/// +/// [`TupleHashXOFInternal`] is the arbitrary-output-length function of Sec 5.3.1. +#[derive(Clone)] +pub struct TupleHashInternal { + cshake: CSHAKEInternal, + output_len: usize, +} + +impl Algorithm for TupleHashInternal { + const ALG_NAME: &'static str = PARAMS::TUPLEHASH_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl TupleHashInternal { + /// A new TupleHash producing `output_len` bytes, optionally customized. + /// + /// `output_len` is `L` and is bound into the computation (Sec 5.3 step 4), so a different + /// length is a different function rather than a longer or shorter view of the same one. + pub fn new(customization: &[u8], output_len: usize) -> Self { + Self { cshake: CSHAKEInternal::new(TUPLEHASH_FUNCTION_NAME, customization), output_len } + } + + /// Hashes a whole tuple in one call, the shape the specification is written in. + pub fn hash_tuple(mut self, tuple: &[&[u8]]) -> Vec { + for element in tuple { + self.do_update(element); + } + self.do_final() + } +} + +impl Hash for TupleHashInternal { + fn block_bitlen(&self) -> usize { + self.cshake.block_bitlen() + } + + fn output_len(&self) -> usize { + self.output_len + } + + /// Hashes `data` as a one-element tuple. For more than one element use + /// [`Self::hash_tuple`] or successive [`Hash::do_update`] calls. + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + /// Appends **one tuple element**. See the note on the type: this is not byte-wise streaming. + fn do_update(&mut self, data: &[u8]) { + absorb_encoded_string_into(&mut self.cshake, data); + } + + fn do_final(mut self) -> Vec { + let n = self.output_len; + let (buf, len) = right_encode((n as u64) * 8); + self.cshake.do_update(&buf[..len]); + self.cshake.into_squeezer().do_output(n) + } + + fn do_final_out(mut self, output: &mut [u8]) -> usize { + let n = self.output_len; + let (buf, len) = right_encode((n as u64) * 8); + self.cshake.do_update(&buf[..len]); + // Per Hash::do_final_out: a short buffer is filled and the digest truncated, a long one + // takes the digest in its first output_len bytes and zeros after it. `n` is bound into the + // computation either way -- the buffer's length never reaches right_encode above, so a + // truncated read is this TupleHash cut short, not the TupleHash of a shorter length. + let written = n.min(output.len()); + output[written..].fill(0); + self.cshake.into_squeezer().do_output_out(&mut output[..written]) + } + + /// # Errors + /// Always [`HashError::InvalidLength`] for a non-zero `num_bits`: `right_encode(L)` has to + /// follow the tuple, which a partial final byte would prevent. + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "TupleHash cannot take a partial final byte: the length encoding must follow", + )); + } + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::from_bits(PARAMS::SIZE as usize) + } +} + +/// Internal struct for TupleHashXOF. Use [`crate::TUPLEHASHXOF128`] or [`crate::TUPLEHASHXOF256`]. +/// +/// The arbitrary-output-length TupleHash of Sec 5.3.1: `right_encode(0)` in place of the length. +/// As with KMAC, it is a *different function* from the fixed-length one, not a longer view of it, +/// and it is a separate type for the same reason -- but read as a stream +/// ([`XOFSqueezer::do_output`]) the length is not bound, so output at one length is a prefix of +/// output at a longer one. +/// +/// A *final* read binds it, because a caller that names a length and will not be back has said +/// what `L` is: [`XOFSqueezer::do_final`] and [`XOF::xof`] produce the fixed-length TupleHash of +/// Sec 5.3 (see [`LengthBoundSqueezer`]), and the [`Hash`] view -- [`Hash::do_final`], +/// [`Hash::hash`] and [`Hash::hash_out`] -- does the same at the nominal [`Hash::output_len`], +/// since a hash's output length is fixed by its type. +/// +/// [`Hash::do_update`] appends one tuple element, exactly as for [`TupleHashInternal`]. +#[derive(Clone)] +pub struct TupleHashXOFInternal { + cshake: CSHAKEInternal, +} + +impl Algorithm for TupleHashXOFInternal { + const ALG_NAME: &'static str = PARAMS::TUPLEHASHXOF_ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; +} + +impl TupleHashXOFInternal { + /// A new TupleHashXOF, optionally customized. + pub fn new(customization: &[u8]) -> Self { + Self { cshake: CSHAKEInternal::new(TUPLEHASH_FUNCTION_NAME, customization) } + } + + /// Hashes a whole tuple and returns the output stream. + pub fn output_for(mut self, tuple: &[&[u8]]) -> LengthBoundSqueezer { + for element in tuple { + self.do_update(element); + } + self.into_squeezer() + } +} + +impl Hash for TupleHashXOFInternal { + fn block_bitlen(&self) -> usize { + self.cshake.block_bitlen() + } + + /// The nominal length, 32 or 64 bytes: twice the security strength, the length at which the + /// output carries that strength in full. Bound by the [`Hash`] view and not by the XOF one -- + /// see [`TupleHashXOFInternal`]. + fn output_len(&self) -> usize { + self.cshake.output_len() + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.do_update(data); + self.do_final() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.do_update(data); + self.do_final_out(output) + } + + /// Appends **one tuple element**. + fn do_update(&mut self, data: &[u8]) { + absorb_encoded_string_into(&mut self.cshake, data); + } + + /// A final read at the nominal length, so `L` is bound: this is the fixed-length TupleHash of + /// Sec 5.3 at `n = ` [`Hash::output_len`], not a prefix of the TupleHashXOF stream. + fn do_final(self) -> Vec { + let n = self.output_len(); + self.into_squeezer().do_final(n) + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + let n = self.output_len(); + // Per Hash::do_final_out, as for the fixed-length form: a short buffer truncates this + // TupleHash rather than computing the TupleHash of a shorter length, because `n` is what + // reaches right_encode, not the buffer's length. + let written = n.min(output.len()); + output[written..].fill(0); + self.into_squeezer().do_final_out_with_length((n as u64) * 8, &mut output[..written]) + } + + /// # Errors + /// Always [`HashError::InvalidLength`] for a non-zero `num_bits`; see + /// [`TupleHashInternal::do_final_partial_bits`]. + fn do_final_partial_bits( + self, + partial_byte: u8, + num_bits: usize, + ) -> Result, HashError> { + let mut out = vec![0u8; self.output_len()]; + self.do_final_partial_bits_out(partial_byte, num_bits, &mut out)?; + Ok(out) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + num_bits: usize, + output: &mut [u8], + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "TupleHashXOF cannot take a partial final byte: right_encode(0) must follow", + )); + } + Ok(self.do_final_out(output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::from_bits(PARAMS::SIZE as usize) + } +} + +impl XOF for TupleHashXOFInternal { + type Squeezer = LengthBoundSqueezer; + + /// The `right_encode` of Sec 5.3.1 step 4 is not absorbed here: whether it carries 0 or the + /// length of a final read is [`LengthBoundSqueezer`]'s decision. + fn into_squeezer(self) -> Self::Squeezer { + LengthBoundSqueezer::new(self.cshake) + } + + fn into_squeezer_partial_bits( + self, + _partial_byte: u8, + num_bits: usize, + ) -> Result { + if num_bits != 0 { + return Err(HashError::InvalidLength( + "TupleHashXOF cannot take a partial final byte: right_encode(0) must follow", + )); + } + Ok(self.into_squeezer()) + } +} diff --git a/crypto/sha3/src/xof_utils.rs b/crypto/sha3/src/xof_utils.rs new file mode 100644 index 00000000..6322e0f1 --- /dev/null +++ b/crypto/sha3/src/xof_utils.rs @@ -0,0 +1,121 @@ +//! The integer and string encodings of NIST SP 800-185 Sec 2.3. +//! +//! These are shared by every SHA-3-derived function in the Recommendation: cSHAKE uses +//! `encode_string` and `bytepad` to bind its function-name and customization strings, and KMAC and +//! TupleHash add `right_encode` to bind the key and the requested output length. +//! +//! Lengths in the Recommendation are counted in **bits**, while this crate's API is byte-oriented, +//! so callers pass byte counts and the helpers multiply where the spec says `len(S)`. + +/// The widest encoding these functions produce: a length byte plus up to eight value bytes. +/// +/// SP 800-185 Sec 2.3.1 permits integers up to `2^2040 - 1`, which would need 255 value bytes. A +/// `u64` covers every length this library can be handed -- an input of `2^64` bits is 2 exabytes -- +/// so the buffer is sized for that rather than for the spec's theoretical maximum. +pub(crate) const MAX_ENCODED_LEN: usize = 9; + +/// `left_encode(x)`: SP 800-185 Sec 2.3.1. +/// +/// Encodes `value` so that it can be parsed unambiguously *from the beginning*: the number of +/// value bytes comes first, then the value itself, big-endian. Returns the buffer and how much of +/// it is used. +/// +/// The spec's example: `left_encode(0)` is `10000000 00000000`, which in this document's +/// low-order-bit-first notation is the bytes `01 00`. +pub(crate) fn left_encode(value: u64) -> ([u8; MAX_ENCODED_LEN], usize) { + let mut buf = [0u8; MAX_ENCODED_LEN]; + // Step 1: n is the smallest positive integer with 2^(8n) > value. Zero still takes one byte, + // which is why the count starts at 1 rather than 0. + let n = value_bytes(value); + buf[0] = n as u8; + // Steps 2-4: the base-256 digits of value, most significant first. + for i in 0..n { + buf[1 + i] = (value >> (8 * (n - 1 - i))) as u8; + } + (buf, n + 1) +} + +/// `right_encode(x)`: SP 800-185 Sec 2.3.1. +/// +/// Unused until KMAC and TupleHash land, which bind the requested output length with it. +/// +/// As [`left_encode`], but the length byte comes *last*, so the encoding can be parsed from the end +/// of a string. The spec's example: `right_encode(0)` is the bytes `00 01`. +#[allow(dead_code)] // used by KMAC and TupleHash +pub(crate) fn right_encode(value: u64) -> ([u8; MAX_ENCODED_LEN], usize) { + let mut buf = [0u8; MAX_ENCODED_LEN]; + let n = value_bytes(value); + for i in 0..n { + buf[i] = (value >> (8 * (n - 1 - i))) as u8; + } + buf[n] = n as u8; + (buf, n + 1) +} + +/// The number of base-256 digits in `value`: the spec's `n`, the smallest positive integer with +/// `2^(8n) > value`. Positive, so zero encodes as one byte. +fn value_bytes(value: u64) -> usize { + let mut n = 1; + let mut v = value; + while { + v >>= 8; + v != 0 + } { + n += 1; + } + n +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The two worked examples in SP 800-185 Sec 2.3.1, in the byte spelling of Sec 2 + /// ("bytes are written with the low-order bit first" in binary, high-order digit first in hex). + #[test] + fn spec_examples() { + let (b, n) = right_encode(0); + assert_eq!(&b[..n], &[0x00, 0x01], "right_encode(0) = 00000000 10000000"); + + let (b, n) = left_encode(0); + assert_eq!(&b[..n], &[0x01, 0x00], "left_encode(0) = 10000000 00000000"); + } + + /// The encodings that appear in the NIST cSHAKE sample file: `left_encode(168)` opens the + /// bytepad block, and `left_encode(120)` prefixes the 15-character "Email Signature". + #[test] + fn cshake_sample_encodings() { + let (b, n) = left_encode(168); + assert_eq!(&b[..n], &[0x01, 0xA8], "left_encode(168), the cSHAKE128 rate"); + + let (b, n) = left_encode(120); + assert_eq!(&b[..n], &[0x01, 0x78], "left_encode(15 * 8), for \"Email Signature\""); + } + + /// The length byte grows with the value, and the value is big-endian after it. + #[test] + fn multi_byte_values() { + let (b, n) = left_encode(0x0100); + assert_eq!(&b[..n], &[0x02, 0x01, 0x00]); + let (b, n) = right_encode(0x0100); + assert_eq!(&b[..n], &[0x01, 0x00, 0x02]); + + let (b, n) = left_encode(u64::MAX); + assert_eq!(&b[..n], &[0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); + let (b, n) = right_encode(u64::MAX); + assert_eq!(&b[..n], &[0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x08]); + } + + /// Every boundary where the number of value bytes increases. + #[test] + fn byte_count_boundaries() { + for n in 1..=8u32 { + let just_under = if n == 8 { u64::MAX } else { (1u64 << (8 * n)) - 1 }; + assert_eq!(left_encode(just_under).1, n as usize + 1, "2^{} - 1", 8 * n); + assert_eq!(right_encode(just_under).1, n as usize + 1, "2^{} - 1", 8 * n); + if n < 8 { + assert_eq!(left_encode(1u64 << (8 * n)).1, n as usize + 2, "2^{}", 8 * n); + } + } + } +} diff --git a/crypto/sha3/tests/bc-test-data.rs b/crypto/sha3/tests/bc-test-data.rs index 334bb6f9..071e1b46 100644 --- a/crypto/sha3/tests/bc-test-data.rs +++ b/crypto/sha3/tests/bc-test-data.rs @@ -25,7 +25,7 @@ //! `Outputlen = minoutbytes + (rightmost 16 bits of Output as big-endian integer) mod //! (maxoutbytes - minoutbytes + 1)` bytes; report `Output`/`Outputlen` per COUNT. -use bouncycastle_core::traits::{Hash, XOF}; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; use bouncycastle_hex as hex; use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; use std::fs; @@ -168,19 +168,20 @@ fn run_sha3_monte_file(orientation: &str, filename: &str) { fn shake_bits(msg: &[u8], len_bits: usize, out_bits: usize) -> Vec { let mut x = X::default(); let (whole, partial) = (len_bits / 8, len_bits % 8); - x.absorb(&msg[..whole]).expect("absorb before squeeze is infallible"); - if partial != 0 { - x.absorb_last_partial_byte(msg[whole].reverse_bits(), partial) - .expect("partial is in 1..=7"); - } + x.do_update(&msg[..whole]); + let mut out_stream = if partial != 0 { + x.into_squeezer_partial_bits(msg[whole].reverse_bits(), partial) + .expect("partial is in 1..=7") + } else { + x.into_squeezer() + }; let (out_whole, out_partial) = (out_bits / 8, out_bits % 8); - let mut out = x.squeeze(out_whole); + let mut out = out_stream.do_output(out_whole + usize::from(out_partial != 0)); if out_partial != 0 { - out.push( - x.squeeze_partial_byte_final(out_partial) - .expect("out_partial is in 1..=7") - .reverse_bits(), - ); + // FIPS 202 B.1: an output of `out_bits` bits occupies the low `out_partial` bits of its + // final octet, so the unused high bits of the byte the sponge gave us are dropped. + let last = out.len() - 1; + out[last] &= (1u8 << out_partial) - 1; } out } @@ -291,7 +292,7 @@ fn run_shake_monte_file(orientation: &str, filename: &str) { let n = output.len().min(16); m[..n].copy_from_slice(&output[..n]); // Output = SHAKE(Msg, Outputlen) - output = X::default().hash_xof(&m, out_bytes); + output = X::default().xof(&m, out_bytes); // Rightmost_Output_bits = rightmost 16 bits of Output (big-endian integer) let l = output.len(); let rightmost = u16::from_be_bytes([output[l - 2], output[l - 1]]) as usize; diff --git a/crypto/sha3/tests/cshake_tests.rs b/crypto/sha3/tests/cshake_tests.rs new file mode 100644 index 00000000..7a8288d5 --- /dev/null +++ b/crypto/sha3/tests/cshake_tests.rs @@ -0,0 +1,236 @@ +//! cSHAKE against the NIST SP 800-185 sample values. +//! +//! The vectors live in the `bc-test-data` repo, which must be cloned alongside this one at +//! `../bc-test-data` (the same convention as the ML-KEM, ML-DSA and SHA-3 suites). If it is not +//! present these tests print a warning and pass vacuously. + +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; +use bouncycastle_hex as hex; +use bouncycastle_sha3::{CSHAKE128, CSHAKE256, SHAKE128, SHAKE256}; +use std::fs; +use std::path::Path; + +/// One `COUNT` block of a `.rsp` file. +struct Vector { + strength: usize, + n: String, + s: String, + output_len: usize, + msg: Vec, + output: Vec, +} + +/// Two candidates, as in `cavp_tests.rs`: the first is relative to the crate directory (where cargo +/// runs an integration test), the second to the workspace root. +const DATA_DIRS: [&str; 2] = + ["../../../bc-test-data/crypto/sp800-185", "../bc-test-data/crypto/sp800-185"]; + +fn read_vectors(filename: &str) -> Option> { + let Some(dir) = DATA_DIRS.into_iter().find(|d| Path::new(d).exists()) else { + println!("WARNING: bc-test-data not found; cSHAKE sample-value tests skipped"); + return None; + }; + let path = Path::new(dir).join(filename); + let content = fs::read_to_string(&path).unwrap_or_else(|e| { + panic!("bc-test-data is present but {} is unreadable: {e}", path.display()) + }); + + let mut out = Vec::new(); + let mut cur: Vec<(String, String)> = Vec::new(); + let finish = |cur: &mut Vec<(String, String)>, out: &mut Vec| { + if cur.is_empty() { + return; + } + let get = |k: &str| cur.iter().find(|(a, _)| a == k).map(|(_, b)| b.clone()); + out.push(Vector { + strength: get("Strength").expect("Strength").parse().expect("a number"), + n: get("N").unwrap_or_default(), + s: get("S").unwrap_or_default(), + output_len: get("Outputlen").expect("Outputlen").parse().expect("a number"), + msg: hex::decode(get("Msg").unwrap_or_default()).expect("hex"), + output: hex::decode(get("Output").expect("Output")).expect("hex"), + }); + cur.clear(); + }; + + for line in content.lines() { + let line = line.trim_end(); + if line.starts_with('#') || line.is_empty() { + continue; + } + let Some((k, v)) = line.split_once(" = ") else { continue }; + if k == "COUNT" { + finish(&mut cur, &mut out); + } else { + cur.push((k.to_string(), v.to_string())); + } + } + finish(&mut cur, &mut out); + Some(out) +} + +/// Every published cSHAKE sample value, at both strengths. +#[test] +fn nist_sp800_185_sample_values() { + let Some(vectors) = read_vectors("cSHAKE.rsp") else { return }; + assert!(!vectors.is_empty(), "the vector file must not be empty"); + + for (i, v) in vectors.iter().enumerate() { + assert!(v.output_len.is_multiple_of(8), "COUNT {i}: byte-aligned outputs only"); + let want = v.output_len / 8; + + let got = match v.strength { + 128 => { + let mut c = CSHAKE128::new(v.n.as_bytes(), v.s.as_bytes()); + c.do_update(&v.msg); + c.into_squeezer().do_output(want) + } + 256 => { + let mut c = CSHAKE256::new(v.n.as_bytes(), v.s.as_bytes()); + c.do_update(&v.msg); + c.into_squeezer().do_output(want) + } + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!(got, v.output, "COUNT {i}: cSHAKE{} S={:?}", v.strength, v.s); + } + println!("cSHAKE: {} sample values", vectors.len()); +} + +/// SP 800-185 Sec 3.3 step 1: with `N` and `S` both empty, cSHAKE *is* SHAKE. +/// +/// This is a special case in the definition rather than a consequence of the general construction: +/// the customized branch absorbs a `bytepad` prefix and uses the `00` domain separator, where SHAKE +/// absorbs nothing and uses `1111`. Getting it wrong would leave cSHAKE self-consistent but +/// incompatible with SHAKE, which no sample value would catch, since every published sample has a +/// non-empty `S`. +#[test] +fn empty_name_and_customization_is_plain_shake() { + for msg in [b"".as_slice(), b"abc", &[0u8; 200], b"Hello, world!"] { + for len in [1usize, 16, 32, 168, 200] { + assert_eq!( + CSHAKE128::new(b"", b"").xof(msg, len), + SHAKE128::new().xof(msg, len), + "cSHAKE128 with no N or S must equal SHAKE128 / len {len}" + ); + assert_eq!( + CSHAKE256::new(b"", b"").xof(msg, len), + SHAKE256::new().xof(msg, len), + "cSHAKE256 with no N or S must equal SHAKE256 / len {len}" + ); + } + } +} + +/// Sec 3.1: two instances with different `N` or `S` must produce unrelated output. That is the +/// whole point of customization, so a customized instance must also differ from plain SHAKE. +#[test] +fn customization_separates_the_functions() { + let msg = b"the same message"; + let plain = SHAKE128::new().xof(msg, 32); + let email = CSHAKE128::new(b"", b"Email Signature").xof(msg, 32); + let finger = CSHAKE128::new(b"", b"key fingerprint").xof(msg, 32); + let named = CSHAKE128::new(b"KMAC", b"").xof(msg, 32); + + assert_ne!(plain, email, "a customized cSHAKE must differ from SHAKE"); + assert_ne!(email, finger, "different S must give unrelated output"); + assert_ne!(plain, named, "a function name alone must customize"); + assert_ne!(email, named, "N and S must not be interchangeable"); +} + +/// `N` and `S` are separate inputs, and `encode_string` length-prefixes each, so moving bytes from +/// one to the other must change the result. Without the prefixes, ("AB", "") and ("A", "B") would +/// collide -- the ambiguity Sec 2.3.2 exists to prevent. +#[test] +fn the_boundary_between_n_and_s_is_unambiguous() { + let msg = b"x"; + assert_ne!( + CSHAKE128::new(b"AB", b"").xof(msg, 32), + CSHAKE128::new(b"A", b"B").xof(msg, 32), + "the split between N and S must be part of the computation" + ); +} + +/// Chunked input must equal a single update, and the output must be one continuous stream. +#[test] +fn streaming_matches_one_shot() { + let msg: Vec = (0..=255u8).collect(); + let one = CSHAKE128::new(b"", b"Email Signature").xof(&msg, 64); + + let mut c = CSHAKE128::new(b"", b"Email Signature"); + for chunk in msg.chunks(7) { + c.do_update(chunk); + } + let mut out = c.into_squeezer(); + let head = out.do_output(20); + let tail = out.do_output(44); + assert_eq!([head, tail].concat(), one, "chunked in, split out, must equal the one-shot"); +} + +/// cSHAKE is a `Hash`, so `do_final` gives the nominal digest size and is a prefix of the stream. +#[test] +fn cshake_is_a_hash() { + let mut c = CSHAKE128::new(b"", b"Email Signature"); + c.do_update(b"abc"); + let digest = c.do_final(); + assert_eq!(digest.len(), 32, "cSHAKE128's nominal output length"); + assert_eq!(CSHAKE128::new(b"", b"Email Signature").hash(b"abc"), digest); + + let long = CSHAKE128::new(b"", b"Email Signature").xof(b"abc", 64); + assert_eq!(&long[..32], &digest[..], "do_final must be a prefix of the longer output"); + + let mut c = CSHAKE256::new(b"", b"Email Signature"); + c.do_update(b"abc"); + assert_eq!(c.do_final().len(), 64, "cSHAKE256's nominal output length"); +} + +/// As for SHAKE: the `Hash` view writes [`Hash::output_len`] bytes and zeroizes the rest, while +/// the XOF spelling fills whatever buffer it is given. +#[test] +fn the_hash_view_writes_output_len_bytes_and_zeroes_the_rest() { + let make = || CSHAKE128::new(b"", b"Email Signature"); + + let mut hash_view = [0xFFu8; 100]; + assert_eq!(make().hash_out(b"abc", &mut hash_view), 32, "cSHAKE128's nominal length"); + assert_eq!(&hash_view[..32], &make().hash(b"abc")[..], "... written in full"); + assert_eq!(&hash_view[32..], &[0u8; 68][..], "everything past output_len is zeroized"); + + let mut buf = [0xFFu8; 100]; + let mut c = make(); + c.do_update(b"abc"); + assert_eq!(c.do_final_out(&mut buf), 32); + assert_eq!(buf, hash_view, "do_final_out must agree with hash_out"); + + let mut xof_view = [0xFFu8; 100]; + assert_eq!(make().xof_out(b"abc", &mut xof_view), 100, "the XOF fills the buffer"); + assert_eq!(&xof_view[..32], &hash_view[..32], "the same stream, read further"); + assert_ne!(&xof_view[32..], &[0u8; 68][..], "... rather than stopping at output_len"); + + // cSHAKE256's nominal length is 64, so its split lands elsewhere. + let mut hash_view = [0xFFu8; 100]; + let n = CSHAKE256::new(b"", b"Email Signature").hash_out(b"abc", &mut hash_view); + assert_eq!(n, 64, "cSHAKE256's nominal length"); + assert_eq!(&hash_view[64..], &[0u8; 36][..], "everything past output_len is zeroized"); +} + +/// The algorithm names, so the factory and any registry agree with the specification's spelling. +#[test] +fn algorithm_names() { + assert_eq!(CSHAKE128::ALG_NAME, "CSHAKE128"); + assert_eq!(CSHAKE256::ALG_NAME, "CSHAKE256"); +} + +/// cSHAKE through the shared `XOF` conformance suite, with a published sample value as the +/// expected output -- conformance and a NIST vector in one. +#[test] +fn test_framework_xof() { + let Some(vectors) = read_vectors("cSHAKE.rsp") else { return }; + let v = vectors.first().expect("at least one sample"); + // The partial-byte input path is cSHAKE's own (it inherits SHAKE's), so leave it enabled. + TestFrameworkXOF::new().test_xof( + || CSHAKE128::new(v.n.as_bytes(), v.s.as_bytes()), + &v.msg, + &v.output, + ); +} diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs new file mode 100644 index 00000000..9b2115fe --- /dev/null +++ b/crypto/sha3/tests/kmac_tests.rs @@ -0,0 +1,558 @@ +//! KMAC against the NIST SP 800-185 sample values. +//! +//! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. + +use bouncycastle_core::errors::{KeyMaterialError, MACError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{Algorithm, Hash, MAC, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; +use bouncycastle_hex as hex; +use bouncycastle_sha3::{KMAC128, KMAC256, KMACXOF128, KMACXOF256}; +use std::fs; +use std::path::Path; + +const DATA_DIRS: [&str; 2] = + ["../../../bc-test-data/crypto/sp800-185", "../bc-test-data/crypto/sp800-185"]; + +/// One `COUNT` block of a `.rsp` file. +struct Vector { + strength: usize, + key: Vec, + s: String, + output_len: usize, + msg: Vec, + output: Vec, +} + +fn read_vectors(filename: &str) -> Option> { + let Some(dir) = DATA_DIRS.into_iter().find(|d| Path::new(d).exists()) else { + println!("WARNING: bc-test-data not found; KMAC sample-value tests skipped"); + return None; + }; + let path = Path::new(dir).join(filename); + let content = fs::read_to_string(&path).unwrap_or_else(|e| { + panic!("bc-test-data is present but {} is unreadable: {e}", path.display()) + }); + + let mut out = Vec::new(); + let mut cur: Vec<(String, String)> = Vec::new(); + let finish = |cur: &mut Vec<(String, String)>, out: &mut Vec| { + if cur.is_empty() { + return; + } + let get = |k: &str| cur.iter().find(|(a, _)| a == k).map(|(_, b)| b.clone()); + out.push(Vector { + strength: get("Strength").expect("Strength").parse().expect("a number"), + key: hex::decode(get("Key").expect("Key")).expect("hex"), + s: get("S").unwrap_or_default(), + output_len: get("Outputlen").expect("Outputlen").parse().expect("a number"), + msg: hex::decode(get("Msg").unwrap_or_default()).expect("hex"), + output: hex::decode(get("Output").expect("Output")).expect("hex"), + }); + cur.clear(); + }; + for line in content.lines() { + let line = line.trim_end(); + if line.starts_with('#') || line.is_empty() { + continue; + } + let Some((k, v)) = line.split_once(" = ") else { continue }; + if k == "COUNT" { + finish(&mut cur, &mut out); + } else { + cur.push((k.to_string(), v.to_string())); + } + } + finish(&mut cur, &mut out); + Some(out) +} + +/// Every published sample key is 32 bytes, which carries a 256-bit strength and so satisfies both +/// KMAC128 and KMAC256 without the weak-key escape hatch. +fn key_material(bytes: &[u8]) -> KeyMaterial<32> { + assert_eq!(bytes.len(), 32, "the sample keys are all 32 bytes"); + KeyMaterial::<32>::from_bytes_as_type(bytes, KeyType::MACKey).expect("a valid MAC key") +} + +/// KMAC (Sec 4.3): the requested output length is bound into the input. +#[test] +fn nist_sp800_185_kmac_sample_values() { + let Some(vectors) = read_vectors("KMAC.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + assert!(v.output_len.is_multiple_of(8), "COUNT {i}: byte-aligned outputs only"); + let want = v.output_len / 8; + let key = key_material(&v.key); + + let got = match v.strength { + 128 => KMAC128::new_with_params(&key, v.s.as_bytes(), want, false) + .expect("a valid key") + .mac(&v.msg), + 256 => KMAC256::new_with_params(&key, v.s.as_bytes(), want, false) + .expect("a valid key") + .mac(&v.msg), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!(got, v.output, "COUNT {i}: KMAC{} S={:?}", v.strength, v.s); + } + println!("KMAC: {} sample values", vectors.len()); +} + +/// KMACXOF (Sec 4.3.1): `right_encode(0)` in place of the length, then arbitrary output. +#[test] +fn nist_sp800_185_kmacxof_sample_values() { + let Some(vectors) = read_vectors("KMACXOF.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + let want = v.output_len / 8; + let key = key_material(&v.key); + + // Read with do_output, which is the XOF reading of the stream: the one-shots bind the + // length they are given, and are checked against the fixed-length samples elsewhere. + let got = match v.strength { + 128 => { + let mut k = KMACXOF128::new(&key, v.s.as_bytes(), false).expect("a valid key"); + k.do_update(&v.msg); + k.into_squeezer().do_output(want) + } + 256 => { + let mut k = KMACXOF256::new(&key, v.s.as_bytes(), false).expect("a valid key"); + k.do_update(&v.msg); + k.into_squeezer().do_output(want) + } + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!(got, v.output, "COUNT {i}: KMACXOF{} S={:?}", v.strength, v.s); + } + println!("KMACXOF: {} sample values", vectors.len()); +} + +/// Sec 4.3.1 versus Sec 4.3: with identical key, message, customization *and* length, KMAC and +/// KMACXOF are different functions, because one binds `right_encode(L)` and the other +/// `right_encode(0)`. The published samples use the same inputs for both, so this is checkable +/// directly against them -- and it is the property that would break if `into_squeezer` bound the +/// length by mistake. +#[test] +fn kmacxof_is_not_kmac_truncated() { + let (Some(fixed), Some(xof)) = (read_vectors("KMAC.rsp"), read_vectors("KMACXOF.rsp")) else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + assert_eq!(f.key, x.key, "COUNT {i}: the sample pairs share a key"); + assert_eq!(f.msg, x.msg, "COUNT {i}: ... and a message"); + assert_eq!(f.output_len, x.output_len, "COUNT {i}: ... and an output length"); + assert_ne!( + f.output, x.output, + "COUNT {i}: KMAC and KMACXOF must not agree on the same inputs" + ); + } +} + +/// The output length is absorbed, so asking for a different length is a different function -- not +/// a prefix. Sec 1: "any change in the requested output length completely changes the function". +#[test] +fn output_length_changes_the_function() { + let key = key_material(&[0x42u8; 32]); + let short = KMAC128::new_with_params(&key, b"", 16, false).unwrap().mac(b"abc"); + let long = KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(b"abc"); + + assert_eq!(short.len(), 16); + assert_eq!(long.len(), 32); + assert_ne!(&long[..16], &short[..], "a longer KMAC must not extend a shorter one"); +} + +/// The customization string separates one use of KMAC from another (Sec 4.2). +#[test] +fn customization_separates_the_functions() { + let key = key_material(&[0x42u8; 32]); + let plain = KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(b"abc"); + let custom = + KMAC128::new_with_params(&key, b"My Tagged Application", 32, false).unwrap().mac(b"abc"); + assert_ne!(plain, custom, "a customization string must change the output"); +} + +/// Streaming input must equal the one-shot, and `verify` must accept only the right tag. +#[test] +fn streaming_and_verification() { + let key = key_material(&[0x11u8; 32]); + let msg: Vec = (0..=255u8).collect(); + + let one = KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(&msg); + + let mut k = KMAC128::new_with_params(&key, b"", 32, false).unwrap(); + for chunk in msg.chunks(13) { + k.do_update(chunk); + } + assert_eq!(k.do_final(), one, "chunked input must equal the one-shot"); + + assert!( + KMAC128::new_with_params(&key, b"", 32, false).unwrap().verify(&msg, &one), + "the correct tag must verify" + ); + + let mut wrong = one.clone(); + wrong[0] ^= 1; + assert!( + !KMAC128::new_with_params(&key, b"", 32, false).unwrap().verify(&msg, &wrong), + "a corrupted tag must not verify" + ); + assert!( + !KMAC128::new_with_params(&key, b"", 32, false).unwrap().verify(&msg, &one[..16]), + "a truncated tag must not verify" + ); +} + +/// Sec 8.4.1 wants the key at least as long as the security strength; the tag on the key material +/// is how that is enforced, so a key tagged too weak must be refused unless explicitly allowed. +#[test] +fn weak_keys_are_refused_unless_allowed() { + let weak = KeyMaterial::<16>::from_bytes_as_type(&[0x01u8; 16], KeyType::MACKey) + .expect("a valid 16-byte MAC key"); + assert!( + weak.security_strength() < bouncycastle_core::security_strength::SecurityStrength::_256bit + ); + + assert!(KMAC256::new(&weak).is_err(), "a 128-bit key must not instantiate KMAC256"); + assert!(KMAC256::new_allow_weak_key(&weak).is_ok(), "... unless explicitly allowed"); + assert!(KMAC128::new(&weak).is_ok(), "but it is enough for KMAC128"); +} + +/// The default constructor: no customization, nominal output length. +#[test] +fn default_constructor_uses_the_nominal_length() { + let key = key_material(&[0x42u8; 32]); + assert_eq!(KMAC128::new(&key).unwrap().output_len(), 32); + assert_eq!(KMAC256::new(&key).unwrap().output_len(), 64); + + // ... and agrees with spelling the same thing out in full. + assert_eq!( + KMAC128::new(&key).unwrap().mac(b"abc"), + KMAC128::new_with_params(&key, b"", 32, false).unwrap().mac(b"abc"), + ); +} + +#[test] +fn algorithm_names() { + assert_eq!(KMAC128::ALG_NAME, "KMAC128"); + assert_eq!(KMAC256::ALG_NAME, "KMAC256"); +} + +/// The counterpart to `output_length_changes_the_function`: read as a stream, KMACXOF binds +/// `right_encode(0)` rather than the length, so output at one length *is* a prefix of output at a +/// longer one. The `Hash` view is not part of that stream -- it is a final read at the nominal +/// length, so it binds `L` and computes fixed-length KMAC128 instead. +#[test] +fn kmacxof_output_is_one_stream() { + let key = key_material(&[0x42u8; 32]); + let squeeze = |n| { + let mut k = KMACXOF128::new(&key, b"", false).unwrap(); + k.do_update(b"abc"); + k.into_squeezer().do_output(n) + }; + let long = squeeze(64); + + let short = squeeze(16); + assert_eq!(&long[..16], &short[..], "KMACXOF at a shorter length must be a prefix"); + + let mut k = KMACXOF128::new(&key, b"", false).unwrap(); + k.do_update(b"abc"); + let via_hash = k.do_final(); + assert_eq!(via_hash.len(), 32, "the nominal output length"); + assert_ne!(&long[..32], &via_hash[..], "the Hash view binds L, so it leaves the stream"); + assert_eq!( + via_hash, + KMAC128::new(&key).unwrap().mac(b"abc"), + "... and lands on fixed-length KMAC128 at the nominal length" + ); +} + +/// `do_final` as the first read binds `right_encode(L)`, so it computes fixed-length KMAC. +/// +/// SP 800-185 s. 4.3 and s. 4.3.1 are the same function but for one field: step 1 absorbs +/// `bytepad(encode_string(K), 168) || X || right_encode(L)` for KMAC and `right_encode(0)` for +/// KMACXOF. Nothing else separates them, so the encoding need not be chosen until the caller says +/// how it wants to read -- and `do_final` as the first read says both how many bytes it wants and +/// that it will not be back, which is exactly `L`. +/// +/// So `KMACXOF128::into_squeezer().do_final(n)` must be `KMAC128(K, X, 8n, S)` to the byte, which +/// the paired sample files check directly: `KMAC.rsp` and `KMACXOF.rsp` publish the same key, +/// message, customization and length, and the fixed-length file is what `do_final` has to match. +#[test] +fn do_final_binds_the_length_when_nothing_has_been_read() { + let (Some(fixed), Some(xof)) = (read_vectors("KMAC.rsp"), read_vectors("KMACXOF.rsp")) else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + let key = key_material(&f.key); + let ctx = format!("COUNT {i}: KMACXOF{} S={:?}", f.strength, f.s); + let s = f.s.as_bytes(); + match f.strength { + 128 => check_do_final_binds_length( + || KMACXOF128::new(&key, s, false).expect("a valid key"), + |n| KMAC128::new_with_params(&key, s, n, false).expect("a valid key").mac(&f.msg), + &f.msg, + &f.output, + &x.output, + &ctx, + ), + 256 => check_do_final_binds_length( + || KMACXOF256::new(&key, s, false).expect("a valid key"), + |n| KMAC256::new_with_params(&key, s, n, false).expect("a valid key").mac(&f.msg), + &f.msg, + &f.output, + &x.output, + &ctx, + ), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } + println!("KMACXOF do_final: {} sample values", fixed.len()); +} + +/// One paired sample through `do_final`. `fixed_expected` is the published fixed-length value, +/// `xof_expected` the published XOF value over the same inputs, and `fixed_of` computes the +/// fixed-length function at a length no vector covers. +fn check_do_final_binds_length( + make: impl Fn() -> X, + fixed_of: impl Fn(usize) -> Vec, + msg: &[u8], + fixed_expected: &[u8], + xof_expected: &[u8], + ctx: &str, +) { + let n = fixed_expected.len(); + assert_ne!(fixed_expected, xof_expected, "{ctx}: the two sample values must differ at all"); + + // The first read, with no do_output before it: right_encode(8n), so the fixed-length function. + let mut x = make(); + x.do_update(msg); + assert_eq!(x.into_squeezer().do_final(n), fixed_expected, "{ctx}: do_final binds the length"); + + // Pre-filled, so the documented zeroization is observable. + let mut buf = vec![0xFFu8; n]; + let mut x = make(); + x.do_update(msg); + assert_eq!(x.into_squeezer().do_final_out(&mut buf), n, "{ctx}: do_final_out returns the len"); + assert_eq!(buf, fixed_expected, "{ctx}: do_final_out binds the length"); + + // The `L` bound is the length actually asked for, not a fixed one. No sample value covers + // these lengths, so the comparison is against this library's own fixed-length function. + for shorter in [n / 2, n - 1] { + let mut x = make(); + x.do_update(msg); + assert_eq!(x.into_squeezer().do_final(shorter), fixed_of(shorter), "{ctx}: L = {shorter}"); + } + + // The one-shots name their length and never come back, so they bind it too. + assert_eq!(make().xof(msg, n), fixed_expected, "{ctx}: xof binds the length"); + + let mut buf = vec![0xFFu8; n]; + assert_eq!(make().xof_out(msg, &mut buf), n, "{ctx}: xof_out returns the length"); + assert_eq!(buf, fixed_expected, "{ctx}: xof_out binds the length"); + + // Once a read has happened right_encode(0) is in the sponge and cannot be revised, so do_final + // after a do_output is the XOF stream continuing, not the fixed-length function. + let split = n / 2; + let mut x = make(); + x.do_update(msg); + let mut squeezer = x.into_squeezer(); + let head = squeezer.do_output(split); + let tail = squeezer.do_final(n - split); + assert_eq!([head, tail].concat(), xof_expected, "{ctx}: do_final after a read stays the XOF"); +} + +/// A partial final byte cannot be expressed: `right_encode(0)` has to follow the message, and the +/// sponge cannot absorb byte-aligned data after a partial byte. +#[test] +fn kmacxof_rejects_a_partial_final_byte() { + let key = key_material(&[0x42u8; 32]); + let mut k = KMACXOF128::new(&key, b"", false).unwrap(); + k.do_update(b"abc"); + assert!(matches!( + k.into_squeezer_partial_bits(0xF0, 4), + Err(bouncycastle_core::errors::HashError::InvalidLength(_)) + )); + + // ... but zero bits means the message ended on a byte boundary, which is fine. + let mut k = KMACXOF128::new(&key, b"", false).unwrap(); + k.do_update(b"abc"); + assert!(k.into_squeezer_partial_bits(0, 0).is_ok()); +} + +#[test] +fn kmacxof_algorithm_names() { + assert_eq!(KMACXOF128::ALG_NAME, "KMACXOF128"); + assert_eq!(KMACXOF256::ALG_NAME, "KMACXOF256"); +} + +/// KMACXOF through the shared `XOF` conformance suite. +/// +/// This is what the constructor-closure form of the framework buys: a keyed XOF has no `Default`, +/// so before it the suite could only be pointed at unkeyed functions. The expected output is taken +/// from a published sample value, so this checks conformance and a NIST vector at once. +#[test] +fn test_framework_xof() { + let Some(vectors) = read_vectors("KMACXOF.rsp") else { return }; + let v = vectors.first().expect("at least one sample"); + let key = key_material(&v.key); + + // Partial-byte input is not expressible for KMACXOF -- right_encode(0) has to follow the + // message -- so that part of the suite is switched off. + let mut framework = TestFrameworkXOF::new(); + framework.enable_partial_byte_tests = false; + // Sec 4.3.1: do_final as the first read binds right_encode(L), which is fixed-length KMAC + // rather than this stream. Checked against the paired sample files elsewhere in this file. + framework.do_final_binds_output_length = true; + framework.test_xof( + || KMACXOF128::new(&key, v.s.as_bytes(), false).expect("a valid key"), + &v.msg, + &v.output, + ); +} + +/// `mac_out` and `do_final_out` against one sample value. The sample-value test above goes through +/// `mac` only, so these two, their returned lengths, and the buffer-length check in `do_final_out` +/// were all invisible to `cargo mutants`. +fn check_out_variants(make: impl Fn() -> M, msg: &[u8], expected: &[u8], ctx: &str) { + let n = expected.len(); + + let mut out = vec![0xFFu8; n]; + assert_eq!(make().mac_out(msg, &mut out).unwrap(), n, "{ctx}: mac_out returns the length"); + assert_eq!(out, expected, "{ctx}: mac_out"); + + // mac_out zero-fills the whole buffer first, so a longer one ends in zeros + let mut out = vec![0xFFu8; n + 5]; + assert_eq!(make().mac_out(msg, &mut out).unwrap(), n); + assert_eq!(&out[..n], expected, "{ctx}: mac_out, oversized buffer"); + assert_eq!(&out[n..], &[0u8; 5], "{ctx}: mac_out zeroizes past the tag"); + + let mut m = make(); + msg.chunks(7).for_each(|c| m.do_update(c)); + let mut out = vec![0xFFu8; n]; + assert_eq!(m.do_final_out(&mut out).unwrap(), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, expected, "{ctx}: do_final_out"); + + // do_final_out writes output_len bytes and zeroizes the rest, as mac_out above does -- the two + // used to disagree, mac_out zero-filling and do_final_out leaving the caller's bytes in place. + let mut m = make(); + m.do_update(msg); + let mut out = vec![0xFFu8; n + 5]; + assert_eq!(m.do_final_out(&mut out).unwrap(), n); + assert_eq!(&out[..n], expected, "{ctx}: do_final_out, oversized buffer"); + assert_eq!(&out[n..], &[0u8; 5], "{ctx}: do_final_out zeroizes past the tag"); + + // a buffer one byte short is refused, by both + let mut out = vec![0u8; n - 1]; + assert!( + matches!(make().do_final_out(&mut out), Err(MACError::InvalidLength(_))), + "{ctx}: do_final_out must refuse a short buffer" + ); + assert!( + matches!(make().mac_out(msg, &mut out), Err(MACError::InvalidLength(_))), + "{ctx}: mac_out must refuse a short buffer" + ); +} + +#[test] +fn mac_out_and_do_final_out_agree_with_the_sample_values() { + let Some(vectors) = read_vectors("KMAC.rsp") else { return }; + for (i, v) in vectors.iter().enumerate() { + let n = v.output_len / 8; + let key = key_material(&v.key); + let s = v.s.as_bytes(); + let ctx = format!("COUNT {i}: KMAC{} S={:?}", v.strength, v.s); + match v.strength { + 128 => check_out_variants( + || KMAC128::new_with_params(&key, s, n, false).unwrap(), + &v.msg, + &v.output, + &ctx, + ), + 256 => check_out_variants( + || KMAC256::new_with_params(&key, s, n, false).unwrap(), + &v.msg, + &v.output, + &ctx, + ), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } +} + +/// `new_allow_weak_key` is `new` without the strength check: same customization, same nominal +/// length, same tag. +#[test] +fn new_allow_weak_key_uses_the_nominal_length() { + let key = key_material(&[0x42u8; 32]); + + let k = KMAC128::new_allow_weak_key(&key).unwrap(); + assert_eq!(k.output_len(), 32); + assert_eq!(k.mac(b"abc"), KMAC128::new(&key).unwrap().mac(b"abc")); + + let k = KMAC256::new_allow_weak_key(&key).unwrap(); + assert_eq!(k.output_len(), 64); + assert_eq!(k.mac(b"abc"), KMAC256::new(&key).unwrap().mac(b"abc")); +} + +/// The same stance as HMAC: a key tagged `MACKey` or `Zeroized` is accepted, anything else is +/// refused as the wrong type. A zeroized key carries no security strength, so it also needs +/// `allow_weak_key`. +#[test] +fn key_type_is_checked() { + let cipher_key = + KeyMaterial::<32>::from_bytes_as_type(&[0x42u8; 32], KeyType::SymmetricCipherKey).unwrap(); + assert!(matches!( + KMAC128::new(&cipher_key), + Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + assert!(matches!( + KMAC128::new_with_params(&cipher_key, b"", 32, true), + Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + assert!(matches!( + KMACXOF128::new(&cipher_key, b"", true), + Err(MACError::KeyMaterialError(KeyMaterialError::InvalidKeyType(_))) + )); + + let zero = KeyMaterial::<32>::new(); + assert_eq!(zero.key_type(), KeyType::Zeroized); + assert!(KMAC128::new(&zero).is_err(), "a zeroized key has no security strength"); + assert!(KMAC128::new_with_params(&zero, b"", 32, true).is_ok(), "... but is the right type"); + assert!(KMAC128::new_allow_weak_key(&zero).is_ok()); + assert!(KMACXOF128::new(&zero, b"", true).is_ok()); +} + +/// The `Hash` view of the partial-byte entry points on KMACXOF: zero bits is the byte-aligned case +/// and yields the same bytes as `do_final`; anything else is refused. The test above only covers +/// the `XOF` entry point, `into_squeezer_partial_bits`. +#[test] +fn kmacxof_hash_view_partial_bits() { + let key = key_material(&[0x42u8; 32]); + let fresh = || { + let mut k = KMACXOF128::new(&key, b"", false).unwrap(); + k.do_update(b"abc"); + k + }; + let expected = fresh().do_final(); + assert_eq!(expected.len(), 32); + + assert_eq!(fresh().do_final_partial_bits(0, 0).unwrap(), expected); + let mut out = vec![0u8; 32]; + assert_eq!(fresh().do_final_partial_bits_out(0, 0, &mut out).unwrap(), 32); + assert_eq!(out, expected); + + assert!(matches!( + fresh().do_final_partial_bits(0xF0, 4), + Err(bouncycastle_core::errors::HashError::InvalidLength(_)) + )); + assert!(matches!( + fresh().do_final_partial_bits_out(0xF0, 4, &mut out), + Err(bouncycastle_core::errors::HashError::InvalidLength(_)) + )); +} diff --git a/crypto/sha3/tests/parallelhash_tests.rs b/crypto/sha3/tests/parallelhash_tests.rs new file mode 100644 index 00000000..ede1c60b --- /dev/null +++ b/crypto/sha3/tests/parallelhash_tests.rs @@ -0,0 +1,499 @@ +//! ParallelHash against the NIST SP 800-185 sample values. +//! +//! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::hash::TestFrameworkHash; +use bouncycastle_hex as hex; +use bouncycastle_sha3::{PARALLELHASH128, PARALLELHASH256, PARALLELHASHXOF128, PARALLELHASHXOF256}; +use std::fs; +use std::path::Path; + +const DATA_DIRS: [&str; 2] = + ["../../../bc-test-data/crypto/sp800-185", "../bc-test-data/crypto/sp800-185"]; + +struct Vector { + strength: usize, + block_size: usize, + s: String, + output_len: usize, + msg: Vec, + output: Vec, +} + +fn read_vectors(filename: &str) -> Option> { + let Some(dir) = DATA_DIRS.into_iter().find(|d| Path::new(d).exists()) else { + println!("WARNING: bc-test-data not found; ParallelHash sample-value tests skipped"); + return None; + }; + let path = Path::new(dir).join(filename); + let content = fs::read_to_string(&path).unwrap_or_else(|e| { + panic!("bc-test-data is present but {} is unreadable: {e}", path.display()) + }); + + let mut out = Vec::new(); + let mut cur: Vec<(String, String)> = Vec::new(); + let finish = |cur: &mut Vec<(String, String)>, out: &mut Vec| { + if cur.is_empty() { + return; + } + let get = |k: &str| cur.iter().find(|(a, _)| a == k).map(|(_, b)| b.clone()); + out.push(Vector { + strength: get("Strength").expect("Strength").parse().expect("a number"), + block_size: get("B").expect("B").parse().expect("a number"), + s: get("S").unwrap_or_default(), + output_len: get("Outputlen").expect("Outputlen").parse().expect("a number"), + msg: hex::decode(get("Msg").expect("Msg")).expect("hex"), + output: hex::decode(get("Output").expect("Output")).expect("hex"), + }); + cur.clear(); + }; + for line in content.lines() { + let line = line.trim_end(); + if line.starts_with('#') || line.is_empty() { + continue; + } + let Some((k, v)) = line.split_once(" = ") else { continue }; + if k == "COUNT" { + finish(&mut cur, &mut out); + } else { + cur.push((k.to_string(), v.to_string())); + } + } + finish(&mut cur, &mut out); + Some(out) +} + +/// ParallelHash (Sec 6.3): the output length is bound into the input. +#[test] +fn nist_sp800_185_parallelhash_sample_values() { + let Some(vectors) = read_vectors("ParallelHash.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + let want = v.output_len / 8; + let got = match v.strength { + 128 => PARALLELHASH128::new(v.block_size, v.s.as_bytes(), want).hash(&v.msg), + 256 => PARALLELHASH256::new(v.block_size, v.s.as_bytes(), want).hash(&v.msg), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!( + got, v.output, + "COUNT {i}: ParallelHash{} B={} S={:?}", + v.strength, v.block_size, v.s + ); + } + println!("ParallelHash: {} sample values", vectors.len()); +} + +/// ParallelHashXOF (Sec 6.3.1): `right_encode(0)` in place of the length. +#[test] +fn nist_sp800_185_parallelhashxof_sample_values() { + let Some(vectors) = read_vectors("ParallelHashXOF.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + let want = v.output_len / 8; + // Read with do_output, which is the XOF reading of the stream: the one-shots bind the + // length they are given, and are checked against the fixed-length samples elsewhere. + let got = match v.strength { + 128 => { + let mut p = PARALLELHASHXOF128::new(v.block_size, v.s.as_bytes()); + p.do_update(&v.msg); + p.into_squeezer().do_output(want) + } + 256 => { + let mut p = PARALLELHASHXOF256::new(v.block_size, v.s.as_bytes()); + p.do_update(&v.msg); + p.into_squeezer().do_output(want) + } + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!( + got, v.output, + "COUNT {i}: ParallelHashXOF{} B={} S={:?}", + v.strength, v.block_size, v.s + ); + } + println!("ParallelHashXOF: {} sample values", vectors.len()); +} + +/// `do_final` as the first read binds `right_encode(L)`, so it computes fixed-length ParallelHash. +/// +/// SP 800-185 s. 6.3 and s. 6.3.1 differ in one field: step 4 is `z = z || right_encode(n) || +/// right_encode(L)` for ParallelHash and `right_encode(0)` in that second slot for +/// ParallelHashXOF. The block count is settled when the input ends, but the length is not -- so it +/// waits for the first read, and `do_final` there says both how many bytes are wanted and that +/// there will be no more, which is exactly `L`. +/// +/// `ParallelHash.rsp` and `ParallelHashXOF.rsp` publish the same messages, block sizes, +/// customization and lengths, so the fixed-length file is what `do_final` has to match. +#[test] +fn do_final_binds_the_length_when_nothing_has_been_read() { + let (Some(fixed), Some(xof)) = + (read_vectors("ParallelHash.rsp"), read_vectors("ParallelHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + let ctx = + format!("COUNT {i}: ParallelHashXOF{} B={} S={:?}", f.strength, f.block_size, f.s); + let (b, s) = (f.block_size, f.s.as_bytes()); + match f.strength { + 128 => check_do_final_binds_length( + || PARALLELHASHXOF128::new(b, s), + |n| PARALLELHASH128::new(b, s, n).hash(&f.msg), + &f.msg, + &f.output, + &x.output, + &ctx, + ), + 256 => check_do_final_binds_length( + || PARALLELHASHXOF256::new(b, s), + |n| PARALLELHASH256::new(b, s, n).hash(&f.msg), + &f.msg, + &f.output, + &x.output, + &ctx, + ), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } + println!("ParallelHashXOF do_final: {} sample values", fixed.len()); +} + +/// One paired sample through `do_final`. `fixed_expected` is the published fixed-length value, +/// `xof_expected` the published XOF value over the same message, and `fixed_of` computes the +/// fixed-length function at a length no vector covers. +fn check_do_final_binds_length( + make: impl Fn() -> X, + fixed_of: impl Fn(usize) -> Vec, + msg: &[u8], + fixed_expected: &[u8], + xof_expected: &[u8], + ctx: &str, +) { + let n = fixed_expected.len(); + assert_ne!(fixed_expected, xof_expected, "{ctx}: the two sample values must differ at all"); + let absorbed = || { + let mut x = make(); + x.do_update(msg); + x.into_squeezer() + }; + + // The first read, with no do_output before it: right_encode(8n), so the fixed-length function. + assert_eq!(absorbed().do_final(n), fixed_expected, "{ctx}: do_final binds the length"); + + // Pre-filled, so the documented zeroization is observable. + let mut buf = vec![0xFFu8; n]; + assert_eq!(absorbed().do_final_out(&mut buf), n, "{ctx}: do_final_out returns the length"); + assert_eq!(buf, fixed_expected, "{ctx}: do_final_out binds the length"); + + // The `L` bound is the length actually asked for, not a fixed one. No sample value covers + // these lengths, so the comparison is against this library's own fixed-length function. + for shorter in [n / 2, n - 1] { + assert_eq!(absorbed().do_final(shorter), fixed_of(shorter), "{ctx}: L = {shorter}"); + } + + // The one-shots name their length and never come back, so they bind it too. + assert_eq!(make().xof(msg, n), fixed_expected, "{ctx}: xof binds the length"); + + let mut buf = vec![0xFFu8; n]; + assert_eq!(make().xof_out(msg, &mut buf), n, "{ctx}: xof_out returns the length"); + assert_eq!(buf, fixed_expected, "{ctx}: xof_out binds the length"); + + // Once a read has happened right_encode(0) is in the sponge and cannot be revised, so do_final + // after a do_output is the XOF stream continuing, not the fixed-length function. + let split = n / 2; + let mut squeezer = absorbed(); + let head = squeezer.do_output(split); + let tail = squeezer.do_final(n - split); + assert_eq!([head, tail].concat(), xof_expected, "{ctx}: do_final after a read stays the XOF"); +} + +/// The two are different functions on identical inputs. +#[test] +fn parallelhashxof_is_not_parallelhash_truncated() { + let (Some(fixed), Some(xof)) = + (read_vectors("ParallelHash.rsp"), read_vectors("ParallelHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len()); + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + assert_eq!(f.msg, x.msg, "COUNT {i}: the sample pairs share a message"); + assert_eq!(f.block_size, x.block_size, "COUNT {i}: ... and a block size"); + assert_ne!(f.output, x.output, "COUNT {i}: the two functions must differ"); + } +} + +/// Unlike TupleHash, ParallelHash *is* ordinary byte-wise streaming: the blocks come from `B`, not +/// from how the caller chunks its `do_update` calls. Chunkings that straddle block boundaries are +/// the interesting ones, so this walks a range of chunk sizes against a block size of 8. +#[test] +fn chunking_does_not_change_the_result() { + let msg: Vec = (0..=200u8).collect(); + let one = PARALLELHASH128::new(8, b"S", 32).hash(&msg); + + for chunk in [1usize, 3, 7, 8, 9, 16, 64, 201] { + let mut p = PARALLELHASH128::new(8, b"S", 32); + for piece in msg.chunks(chunk) { + p.do_update(piece); + } + assert_eq!(p.do_final(), one, "chunk size {chunk} must not change the result"); + } +} + +/// Sec 6.2: `B` is a parameter of the function. The same message under a different block size is a +/// different hash, not a re-arrangement of the same work. +#[test] +fn the_block_size_is_part_of_the_hash() { + let msg: Vec = (0..=100u8).collect(); + let b8 = PARALLELHASH128::new(8, b"", 32).hash(&msg); + let b12 = PARALLELHASH128::new(12, b"", 32).hash(&msg); + let b16 = PARALLELHASH128::new(16, b"", 32).hash(&msg); + assert_ne!(b8, b12); + assert_ne!(b8, b16); + assert_ne!(b12, b16); +} + +/// A short final block, an exactly-full final block, and an empty message are the boundary cases +/// of the block loop. +/// +/// This test matters more than it looks: **every published ParallelHash sample value has a +/// block-aligned message** (24 bytes at B = 8, 72 at B = 12), so the NIST vectors never exercise a +/// short final block at all. Deleting the flush of the partial buffer passes all twelve of them +/// and fails only here. +#[test] +fn block_boundary_cases() { + // exactly one full block, versus one full block plus one byte + let full = PARALLELHASH128::new(8, b"", 32).hash(&[0xAAu8; 8]); + let plus = PARALLELHASH128::new(8, b"", 32).hash(&[0xAAu8; 9]); + assert_ne!(full, plus); + + // two full blocks versus one short block: different block counts, so different output + let two = PARALLELHASH128::new(8, b"", 32).hash(&[0xAAu8; 16]); + assert_ne!(two, full); + + // an empty message is zero blocks, and must still produce a hash + let empty = PARALLELHASH128::new(8, b"", 32).hash(b""); + assert_eq!(empty.len(), 32); + assert_ne!(empty, full); +} + +/// The XOF's output at one length is a prefix of its output at a longer one; the fixed-length +/// function's is not. +#[test] +fn length_binding_differs_between_the_two() { + let msg = b"parallel"; + let short = PARALLELHASH128::new(4, b"", 16).hash(msg); + let long = PARALLELHASH128::new(4, b"", 32).hash(msg); + assert_ne!(&long[..16], &short[..], "ParallelHash: a different length is a different function"); + + let squeeze = |n| { + let mut p = PARALLELHASHXOF128::new(4, b""); + p.do_update(msg); + p.into_squeezer().do_output(n) + }; + let short = squeeze(16); + let long = squeeze(32); + assert_eq!(&long[..16], &short[..], "ParallelHashXOF: one stream, so shorter is a prefix"); +} + +/// A partial final byte cannot be expressed: the block count and length encodings must follow. +#[test] +fn partial_final_byte_is_refused() { + let mut p = PARALLELHASH128::new(8, b"", 32); + p.do_update(b"abc"); + assert!(matches!(p.do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); + + let mut p = PARALLELHASHXOF128::new(8, b""); + p.do_update(b"abc"); + assert!(matches!(p.into_squeezer_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); +} + +/// Sec 6.2 forbids a zero block size. +#[test] +#[should_panic(expected = "block size B must be positive")] +fn zero_block_size_is_rejected() { + let _ = PARALLELHASH128::new(0, b"", 32); +} + +#[test] +fn algorithm_names() { + assert_eq!(PARALLELHASH128::ALG_NAME, "ParallelHash128"); + assert_eq!(PARALLELHASH256::ALG_NAME, "ParallelHash256"); + assert_eq!(PARALLELHASHXOF128::ALG_NAME, "ParallelHashXOF128"); + assert_eq!(PARALLELHASHXOF256::ALG_NAME, "ParallelHashXOF256"); +} + +/// Sponge rates from FIPS 202 Table 3, the nominal lengths of the XOF forms, and the constructed +/// length of the fixed forms. The generic checks elsewhere only require these to be positive. +#[test] +fn metadata() { + assert_eq!(PARALLELHASH128::new(8, b"", 32).block_bitlen(), 1344, "cSHAKE128 rate"); + assert_eq!(PARALLELHASH256::new(8, b"", 64).block_bitlen(), 1088, "cSHAKE256 rate"); + assert_eq!(PARALLELHASHXOF128::new(8, b"").block_bitlen(), 1344); + assert_eq!(PARALLELHASHXOF256::new(8, b"").block_bitlen(), 1088); + + assert_eq!(PARALLELHASH128::new(8, b"", 17).output_len(), 17, "whatever was asked for"); + assert_eq!(PARALLELHASH256::new(8, b"", 100).output_len(), 100); + assert_eq!(PARALLELHASHXOF128::new(8, b"").output_len(), 32, "the nominal length"); + assert_eq!(PARALLELHASHXOF256::new(8, b"").output_len(), 64); +} + +/// Every `Hash` entry point of the fixed-length form, against one sample value. +/// +/// The sample-value test above goes through `hash` only, which left `hash_out` and +/// `do_final_out` unexercised: `cargo mutants` could replace each with a constant, and change the +/// `* 8` in the `right_encode(L)` that `do_final_out` binds, without a test noticing. +fn check_fixed_view(make: impl Fn() -> H, msg: &[u8], expected: &[u8], ctx: &str) { + let n = expected.len(); + assert_eq!(make().output_len(), n, "{ctx}: output_len"); + + let mut out = vec![0u8; n]; + assert_eq!(make().hash_out(msg, &mut out), n, "{ctx}: hash_out returns the length"); + assert_eq!(out, expected, "{ctx}: hash_out"); + + let mut h = make(); + msg.chunks(5).for_each(|c| h.do_update(c)); + let mut out = vec![0u8; n]; + assert_eq!(h.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, expected, "{ctx}: do_final_out"); + + // a longer buffer is only written up to the output length + let mut h = make(); + h.do_update(msg); + let mut out = vec![0xFFu8; n + 7]; + assert_eq!(h.do_final_out(&mut out), n); + assert_eq!(&out[..n], expected, "{ctx}: do_final_out, oversized buffer"); + // Hash::do_final_out zeroizes the whole buffer, so the tail is 0 rather than what the caller + // left there -- the same as SHA3, which is the contract these fixed-length types share. + assert_eq!(&out[n..], &[0u8; 7], "{ctx}: bytes past the output length are zeroized"); +} + +/// Every `Hash` and `XOF` entry point of the XOF form, against one paired sample value. +/// +/// The samples ask for the nominal length, and the `Hash` view is a final read at that length, so +/// it binds `L` and must reproduce the *fixed-length* sample; reading the stream with `do_output` +/// must reproduce the XOF one. +fn check_xof_view( + make: impl Fn() -> X, + msg: &[u8], + expected: &[u8], + fixed_expected: &[u8], + ctx: &str, +) { + let n = expected.len(); + assert_eq!(make().output_len(), n, "{ctx}: the samples ask for the nominal length"); + assert_eq!(fixed_expected.len(), n, "{ctx}: ... and the paired samples share it"); + + assert_eq!(make().hash(msg), fixed_expected, "{ctx}: hash"); + + let mut out = vec![0u8; n]; + assert_eq!(make().hash_out(msg, &mut out), n, "{ctx}: hash_out returns the length"); + assert_eq!(out, fixed_expected, "{ctx}: hash_out"); + + let mut x = make(); + msg.chunks(5).for_each(|c| x.do_update(c)); + assert_eq!(x.do_final(), fixed_expected, "{ctx}: do_final"); + + let mut x = make(); + x.do_update(msg); + let mut out = vec![0u8; n]; + assert_eq!(x.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, fixed_expected, "{ctx}: do_final_out"); + + // zero partial bits is the byte-aligned case and must be accepted; any other count refused + let mut x = make(); + x.do_update(msg); + assert_eq!( + x.do_final_partial_bits(0, 0).unwrap(), + fixed_expected, + "{ctx}: do_final_partial_bits(0)" + ); + + let mut x = make(); + x.do_update(msg); + let mut out = vec![0u8; n]; + assert_eq!(x.do_final_partial_bits_out(0, 0, &mut out).unwrap(), n, "{ctx}: ..._out length"); + assert_eq!(out, fixed_expected, "{ctx}: do_final_partial_bits_out(0)"); + + assert!(matches!(make().do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); + let mut out = vec![0u8; n]; + assert!(matches!( + make().do_final_partial_bits_out(0xF0, 4, &mut out), + Err(HashError::InvalidLength(_)) + )); + + // The XOF reading of the stream is do_output; the one-shots bind the length they are given, + // so they belong to `do_final_binds_the_length_when_nothing_has_been_read` instead. + let mut x = make(); + x.do_update(msg); + assert_eq!(x.into_squeezer().do_output(n / 2), &expected[..n / 2], "{ctx}: do_output, shorter"); + + let mut out = vec![0u8; n]; + let mut x = make(); + x.do_update(msg); + assert_eq!(x.into_squeezer().do_output_out(&mut out), n, "{ctx}: do_output_out length"); + assert_eq!(out, expected, "{ctx}: do_output_out"); +} + +#[test] +fn hash_trait_view_agrees_with_the_sample_values() { + let Some(vectors) = read_vectors("ParallelHash.rsp") else { return }; + for (i, v) in vectors.iter().enumerate() { + let n = v.output_len / 8; + let (b, s) = (v.block_size, v.s.as_bytes()); + let ctx = format!("COUNT {i}: ParallelHash{} B={b}", v.strength); + match v.strength { + 128 => check_fixed_view(|| PARALLELHASH128::new(b, s, n), &v.msg, &v.output, &ctx), + 256 => check_fixed_view(|| PARALLELHASH256::new(b, s, n), &v.msg, &v.output, &ctx), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } +} + +#[test] +fn xof_trait_view_agrees_with_the_sample_values() { + let (Some(fixed), Some(xof)) = + (read_vectors("ParallelHash.rsp"), read_vectors("ParallelHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, v)) in fixed.iter().zip(xof.iter()).enumerate() { + let (b, s) = (v.block_size, v.s.as_bytes()); + let ctx = format!("COUNT {i}: ParallelHashXOF{} B={b}", v.strength); + match v.strength { + 128 => { + check_xof_view(|| PARALLELHASHXOF128::new(b, s), &v.msg, &v.output, &f.output, &ctx) + } + 256 => { + check_xof_view(|| PARALLELHASHXOF256::new(b, s), &v.msg, &v.output, &f.output, &ctx) + } + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } +} + +/// Every output-buffer length, at both strengths and a non-default output length. +/// +/// As for TupleHash: `output_len` is bound into the computation, so a short buffer truncates this +/// ParallelHash rather than computing a shorter one, and must not panic. +#[test] +fn output_buffers_of_every_length() { + let framework = TestFrameworkHash::new(); + let input = b"the quick brown fox jumps over the lazy dog"; + + framework.test_hash_output_buffers(|| PARALLELHASH128::new(8, b"", 32), input); + framework.test_hash_output_buffers(|| PARALLELHASH256::new(8, b"", 64), input); + + // A block size that does not divide the input, a customization string, odd output lengths. + framework.test_hash_output_buffers(|| PARALLELHASH128::new(12, b"Parallel Data", 17), input); + framework.test_hash_output_buffers(|| PARALLELHASH256::new(5, b"Parallel Data", 5), input); +} diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index b0073470..8f494ba3 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -8,174 +8,119 @@ mod shake_tests { KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; use bouncycastle_core::security_strength::SecurityStrength; - use bouncycastle_core::traits::{KDF, XOF}; + use bouncycastle_core::traits::{Hash, KDF, XOF, XOFSqueezer}; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_core_test_framework::kdf::TestFrameworkKDF; use bouncycastle_core_test_framework::xof::TestFrameworkXOF; use bouncycastle_sha3::{SHA3_256, SHAKE128, SHAKE256}; - #[test] - fn test_xof_partial_bit_output() { - // The 4th ([3]) byte of the output of SHA128(\x00\x01\x02\x03\x04) is known to be 0xFF - // That fact is used to test partial byte output. - - let output = SHAKE128::new().hash_xof(&[0u8, 1u8, 2u8, 3u8, 4u8], 4); - assert_eq!(output[3], 0xFF); - - // just for comparison - let mut output2 = vec![0u8; 4]; - SHAKE128::new().hash_xof_out(&[0u8, 1u8, 2u8, 3u8, 4u8], &mut output2); - assert_eq!(output, output2); - - // test bounds - // 0 is in range: it requests no bits, so the result is 0x00. - let mut shake = SHAKE128::new(); - shake.absorb(&[0u8, 1u8, 2u8, 3u8, 4u8]).expect("absorb before squeeze is infallible"); - let _throwaway = shake.squeeze(3); - assert_eq!(shake.squeeze_partial_byte_final(0).expect("Squeeze failed"), 0x00); - - // 8 and above are out of range. - for bad in [8usize, 9, 15, 16, 64, usize::MAX] { - let mut shake = SHAKE128::new(); - shake.absorb(&[0u8, 1u8, 2u8, 3u8, 4u8]).expect("absorb before squeeze is infallible"); - let _throwaway = shake.squeeze(3); - assert!( - matches!(shake.squeeze_partial_byte_final(bad), Err(HashError::InvalidLength(_))), - "num_bits={bad}" - ); - } - - for i in 0..=7 { - let mut shake = SHAKE128::new(); - shake.absorb(&[0u8, 1u8, 2u8, 3u8, 4u8]).expect("absorb before squeeze is infallible"); - _ = shake.squeeze(3); - let out: u8 = shake.squeeze_partial_byte_final(i).expect("Squeeze failed"); - // byte [3] of the stream is 0xFF, so its first `i` bits, returned MSB-first, are the top - // `i` set bits. - assert_eq!(out, (0xFF00u16 >> i) as u8); - } - - // success case -- output slice version - let mut shake = SHAKE128::new(); - shake.absorb(&[0u8, 1u8, 2u8, 3u8, 4u8]).expect("absorb before squeeze is infallible"); - _ = shake.squeeze(3); - let mut out = 0u8; - shake.squeeze_partial_byte_final_out(1, &mut out).expect("Squeeze failed"); - assert_eq!(out, 0x80); - } - - /// Regression: squeeze_partial_byte_final() as the *first* squeeze must apply the SHAKE "1111" - /// domain suffix (previously it bypassed it and returned raw Keccak output), and must return the - /// first `num_bits` bits of the next output byte (its low bits, FIPS 202 B.1 bit ordering) in the - /// top `num_bits` bits of the result (ASN.1 BIT STRING order), with the unused low bits zero. - #[test] - fn partial_bit_output_as_first_squeeze_matches_full_output() { - let msg = b"abc"; - for skip in [0usize, 1, 5] { - let mut shake = SHAKE256::new(); - shake.absorb(msg).unwrap(); - let full = shake.squeeze(skip + 1)[skip]; - // pick a byte that is not all-ones/all-zeros so bit selection is actually tested - assert!( - full != 0x00 && full != 0xFF, - "test vector byte must be non-uniform: {full:#x}" - ); - - for n in 0..=7usize { - let mut shake = SHAKE256::new(); - shake.absorb(msg).unwrap(); - if skip > 0 { - _ = shake.squeeze(skip); - } - let got = shake.squeeze_partial_byte_final(n).unwrap(); - assert_eq!( - got, - full.reverse_bits() & ((0xFF00u16 >> n) as u8), - "skip={skip} n={n}" - ); - assert_eq!(got & (0xFFu8 >> n), 0, "unused low bits must be zero"); - } - } - } - /// Regression: when the 4 trailing message bits plus the SHAKE "1111" suffix exactly fill a byte, /// the sponge must still switch to squeezing, otherwise the first squeeze appended a second suffix. /// Vector: NIST CAVP SHA3VS SHAKE128ShortMsg (bit-oriented), Len = 4, Msg = 08 (FIPS 202 B.1 /// packing: message bits 0001 in the low nibble, first bit in the LSB), i.e. 0x10 in the API's /// MSB-first order. #[test] - fn absorb_last_partial_byte_four_bits() { - let mut shake = SHAKE128::new(); - shake.absorb_last_partial_byte(0x10, 4).unwrap(); + fn into_squeezer_partial_bits_four_bits() { + let shake = SHAKE128::new(); + let mut out = shake.into_squeezer_partial_bits(0x10, 4).unwrap(); assert_eq!( - shake.squeeze(16), + out.do_output(16), bouncycastle_hex::decode("d40238024b040a954d9c2c89daf480e5").unwrap(), "SHAKE128 of the 4-bit message 0001" ); } - /// absorb_last_partial_byte() must validate num_partial_bits before shifting: 0 is allowed + /// into_squeezer_partial_bits() must validate num_bits before shifting: 0 is allowed /// (finalize with no partial byte), 8+ is rejected with InvalidLength rather than panicking. #[test] - fn absorb_last_partial_byte_validates_range() { + fn into_squeezer_partial_bits_validates_range() { for bad in [8usize, 9, 15, 16, 64, usize::MAX] { let mut shake = SHAKE128::new(); - shake.absorb(b"abc").unwrap(); + shake.do_update(b"abc"); assert!( matches!( - shake.absorb_last_partial_byte(0xFF, bad), + shake.into_squeezer_partial_bits(0xFF, bad), Err(HashError::InvalidLength(_)) ), - "num_partial_bits={bad}" + "num_bits={bad}" ); } let mut a = SHAKE128::new(); - a.absorb(b"abc").unwrap(); - a.absorb_last_partial_byte(0xFF, 0).unwrap(); - assert_eq!(a.squeeze(32), SHAKE128::new().hash_xof(b"abc", 32)); + a.do_update(b"abc"); + let mut a = a.into_squeezer_partial_bits(0xFF, 0).unwrap(); + assert_eq!(a.do_output(32), SHAKE128::new().xof(b"abc", 32)); // Upper boundary: 7 bits is the largest valid partial byte and must be accepted, and must // actually change the output relative to the byte-aligned message. let mut b = SHAKE128::new(); - b.absorb(b"abc").unwrap(); - b.absorb_last_partial_byte(0xFE, 7).unwrap(); - assert_ne!(b.squeeze(32), SHAKE128::new().hash_xof(b"abc", 32)); + b.do_update(b"abc"); + let mut b = b.into_squeezer_partial_bits(0xFE, 7).unwrap(); + assert_ne!(b.do_output(32), SHAKE128::new().xof(b"abc", 32)); } - /// Once squeezing has begun, a SHAKE cannot return to absorbing (FIPS 202 defines SHAKE as a - /// single function of the whole message). Both absorb entry points must reject a post-squeeze call - /// with `HashError::InvalidState` rather than panicking, and a rejected call must leave the sponge - /// untouched so the output stream continues consistently. + /// The two `Hash` metadata methods, pinned to their actual values. + /// + /// The generic framework can only check that these are positive and byte-aligned, which every + /// plausible mis-derivation also satisfies -- `cargo mutants` survived three separate mutations + /// of them until this test existed. + /// + /// `block_bitlen` is the sponge rate, `1600 - 2c`: FIPS 202 Table 3 gives 1344 bits for + /// SHAKE128 and 1088 for SHAKE256. `output_len` is the nominal digest size, twice the security + /// strength: 32 and 64 bytes. #[test] - fn absorb_after_squeeze_is_rejected() { - use bouncycastle_core::errors::HashError; - - // absorb() after squeeze() -> InvalidState. - let mut shake = SHAKE128::new(); - shake.absorb(b"input").expect("absorb before squeeze is infallible"); - let _ = shake.squeeze(16); - assert!(matches!(shake.absorb(b"more"), Err(HashError::InvalidState(_)))); - - // absorb_last_partial_byte() after squeeze() -> InvalidState. - let mut shake = SHAKE256::new(); - shake.absorb(b"input").expect("absorb before squeeze is infallible"); - let _ = shake.squeeze(16); - assert!(matches!(shake.absorb_last_partial_byte(0x01, 3), Err(HashError::InvalidState(_)))); - - // A rejected absorb must not corrupt state: the output stream continues as if it never - // happened. Squeezing 16 + 16 bytes around a rejected absorb must equal a clean squeeze of 32. - let mut a = SHAKE128::new(); - a.absorb(b"input").expect("absorb before squeeze is infallible"); - let first = a.squeeze(16); - assert!(a.absorb(b"more").is_err()); - let second = a.squeeze(16); - - let mut b = SHAKE128::new(); - b.absorb(b"input").expect("absorb before squeeze is infallible"); - let clean = b.squeeze(32); + fn metadata_matches_fips202() { + assert_eq!(SHAKE128::new().block_bitlen(), 1344, "SHAKE128 rate, FIPS 202 Table 3"); + assert_eq!(SHAKE256::new().block_bitlen(), 1088, "SHAKE256 rate, FIPS 202 Table 3"); + assert_eq!(SHAKE128::new().output_len(), 32, "nominal digest size for SHAKE128"); + assert_eq!(SHAKE256::new().output_len(), 64, "nominal digest size for SHAKE256"); + + // and do_final actually produces that many bytes + assert_eq!(SHAKE128::new().hash(b"abc").len(), 32); + assert_eq!(SHAKE256::new().hash(b"abc").len(), 64); + } - assert_eq!(first.as_slice(), &clean[..16]); - assert_eq!(second.as_slice(), &clean[16..]); + /// The `Hash` view writes [`Hash::output_len`] bytes and zeroizes the rest of the buffer; the + /// XOF spelling is what fills a buffer of the caller's choosing. + /// + /// FIPS 202 binds no length, so the two readings agree on the bytes they share -- the hash is + /// the first `output_len` bytes of the same stream -- and differ only in how much they write. + /// Before this, the `Hash` entry points took their length from the buffer, so a long one came + /// back full of XOF output and `output_len` meant nothing. + #[test] + fn the_hash_view_writes_output_len_bytes_and_zeroes_the_rest() { + let mut hash_view = [0xFFu8; 100]; + assert_eq!(SHAKE128::new().hash_out(b"abc", &mut hash_view), 32, "the nominal length"); + assert_eq!(&hash_view[..32], &SHAKE128::new().hash(b"abc")[..], "... written in full"); + assert_eq!(&hash_view[32..], &[0u8; 68][..], "everything past output_len is zeroized"); + + // do_final_out and the byte-aligned partial-bit spelling follow the same rule. + let mut buf = [0xFFu8; 100]; + let mut h = SHAKE128::new(); + h.do_update(b"abc"); + assert_eq!(h.do_final_out(&mut buf), 32); + assert_eq!(buf, hash_view, "do_final_out must agree with hash_out"); + + let mut buf = [0xFFu8; 100]; + let mut h = SHAKE128::new(); + h.do_update(b"abc"); + assert_eq!(h.do_final_partial_bits_out(0, 0, &mut buf).expect("0 is in range"), 32); + assert_eq!(buf, hash_view, "a zero-bit partial byte is the same call"); + + // A short buffer truncates, as it always did. + let mut short = [0xFFu8; 16]; + assert_eq!(SHAKE128::new().hash_out(b"abc", &mut short), 16); + assert_eq!(&short[..], &hash_view[..16], "a short buffer truncates the same output"); + + // The XOF spelling takes its length from the buffer and keeps reading past output_len. + let mut xof_view = [0xFFu8; 100]; + assert_eq!(SHAKE128::new().xof_out(b"abc", &mut xof_view), 100, "the XOF fills it"); + assert_eq!(&xof_view[..32], &hash_view[..32], "the same stream, read further"); + assert_ne!(&xof_view[32..], &[0u8; 68][..], "... rather than stopping at output_len"); + + // SHAKE256's nominal length is 64, so its split lands elsewhere. + let mut hash_view = [0xFFu8; 100]; + assert_eq!(SHAKE256::new().hash_out(b"abc", &mut hash_view), 64, "the nominal length"); + assert_eq!(&hash_view[64..], &[0u8; 36][..], "everything past output_len is zeroized"); } #[test] @@ -344,9 +289,9 @@ mod shake_tests { #[test] fn security_strength() { assert_eq!(KDF::max_security_strength(&SHAKE128::default()), SecurityStrength::_128bit); - assert_eq!(XOF::max_security_strength(&SHAKE128::default()), SecurityStrength::_128bit); + assert_eq!(Hash::max_security_strength(&SHAKE128::default()), SecurityStrength::_128bit); assert_eq!(KDF::max_security_strength(&SHAKE256::default()), SecurityStrength::_256bit); - assert_eq!(XOF::max_security_strength(&SHAKE256::default()), SecurityStrength::_256bit); + assert_eq!(Hash::max_security_strength(&SHAKE256::default()), SecurityStrength::_256bit); } #[test] @@ -357,8 +302,8 @@ mod shake_tests { #[test] fn test_framework_xof() { let test_framework = TestFrameworkXOF::new(); - test_framework.test_xof::(&DUMMY_SEED[..512], b"\x88\x90\xED\x20\x4D\x22\x89\xE1\x72\xE9\xAE\x68\x48\x18\x23\x77\x08\x20\x90\x80\x60\xA4\xDF\x33\x51\xA3\xF1\x84\xEB\xB6\xDD\x0F\x9D\x23\x15\x60\x68\x0F\x2C\x65\x8A\xC4\x84\x97\xAD\xB5\xA4\x83\x99\x36\xA3\x16\x55\x16\xFA\x5E\x13\xBF\x8A\x15\xBA\xBC\x14\x1F"); - test_framework.test_xof::(&DUMMY_SEED[..512], b"\xA1\xD7\x18\x85\xB0\xA8\x41\xF0\x3D\x1D\xC7\xF2\x73\x8A\x15\xCC\x98\x40\x71\xA1\x7F\xFE\xD5\xEC\xAC\xB9\xF5\x87\x20\xA4\x73\xBE\x1F\x2D\x28\xB9\x6D\x54\x3A\x36\x7C\x81\x11\x42\x06\xF5\xAF\x37\x18\xE7\x31\x5B\x57\xF2\x90\xB6\x4D\x8D\x29\xCF\x43\x7E\x40\x4C"); + test_framework.test_xof(SHAKE128::new, &DUMMY_SEED[..512], b"\x88\x90\xED\x20\x4D\x22\x89\xE1\x72\xE9\xAE\x68\x48\x18\x23\x77\x08\x20\x90\x80\x60\xA4\xDF\x33\x51\xA3\xF1\x84\xEB\xB6\xDD\x0F\x9D\x23\x15\x60\x68\x0F\x2C\x65\x8A\xC4\x84\x97\xAD\xB5\xA4\x83\x99\x36\xA3\x16\x55\x16\xFA\x5E\x13\xBF\x8A\x15\xBA\xBC\x14\x1F"); + test_framework.test_xof(SHAKE256::new, &DUMMY_SEED[..512], b"\xA1\xD7\x18\x85\xB0\xA8\x41\xF0\x3D\x1D\xC7\xF2\x73\x8A\x15\xCC\x98\x40\x71\xA1\x7F\xFE\xD5\xEC\xAC\xB9\xF5\x87\x20\xA4\x73\xBE\x1F\x2D\x28\xB9\x6D\x54\x3A\x36\x7C\x81\x11\x42\x06\xF5\xAF\x37\x18\xE7\x31\x5B\x57\xF2\x90\xB6\x4D\x8D\x29\xCF\x43\x7E\x40\x4C"); } #[test] @@ -370,36 +315,58 @@ mod shake_tests { let str = "Colorless green ideas sleep furiously"; // A helper that exercises the full round-trip for one SHAKE variant. - fn round_trip + Clone>(mut shake: X, input: &[u8]) { - shake.absorb(input).expect("absorb before squeeze is infallible"); + // Each phase suspends as its own type: an absorbing state resumes as `X`, a squeezing one + // as `X::Squeezer`, and each rejects the other's phase. + fn round_trip(mut shake: X, input: &[u8]) + where + X: XOF + Suspendable + Clone, + X::Squeezer: Suspendable + Clone, + { + shake.do_update(input); // do the default trait-conformance tests TestFrameworkSuspendableState::new().test(&shake); // Test #1 - // serialize the in-progress (absorbing) state, then squeeze from the original and compare - let serialized_state = shake.clone().suspend(); - let expected = shake.squeeze(64); + // serialize the in-progress (absorbing) state, then read from the original and compare + let absorbing_state = shake.clone().suspend(); + let mut out = shake.into_squeezer(); + let expected = out.do_output(64); // rebuild from the serialized state and confirm it produces the same output - let mut from_state = X::from_suspended(serialized_state).unwrap(); - assert_eq!(expected, from_state.squeeze(64)); + let from_state = + X::from_suspended(absorbing_state).expect("an absorbing state resumes as the XOF"); + assert_eq!(expected, from_state.into_squeezer().do_output(64)); // Test #2 - // serialize the in-progress (squeezing) state, then squeeze more from the original and compare - let serialized_state = shake.clone().suspend(); - let expected = shake.squeeze(64); + // serialize the in-progress (squeezing) state, then read more from the original and compare + let squeezing_state = out.clone().suspend(); + let expected = out.do_output(64); // rebuild from the serialized state and confirm it produces the same output - let mut from_state = X::from_suspended(serialized_state).unwrap(); - assert_eq!(expected, from_state.squeeze(64)); + let mut from_state = X::Squeezer::from_suspended(squeezing_state) + .expect("a squeezing state resumes as the output"); + assert_eq!(expected, from_state.do_output(64)); + + // The phase is part of the state, so each type refuses the other's. + assert!( + matches!(X::from_suspended(squeezing_state), Err(SuspendableError::InvalidData)), + "a squeezing state must not resume as an absorbing XOF" + ); + assert!( + matches!( + X::Squeezer::from_suspended(absorbing_state), + Err(SuspendableError::InvalidData) + ), + "an absorbing state must not resume as an output" + ); // a corrupt `squeezing` byte (last byte of the keccak state) must be rejected. // Layout: 3 version bytes + variant tag(1) + [u64;25](200) + data_queue(192) // + bits_in_queue(8) + squeezing(1) - let mut busted = serialized_state; + let mut busted = squeezing_state; busted[3 + 1 + 400] = 42; - match X::from_suspended(busted) { + match X::Squeezer::from_suspended(busted) { Err(SuspendableError::InvalidData) => { /* good */ } _ => panic!("Expected an error for a corrupt squeezing byte"), } @@ -412,7 +379,7 @@ mod shake_tests { // variant tag). The SHAKE256 -> SHA3-256 case is the important one: they share the same rate // (1088), so only the variant tag distinguishes them. let mut shake128 = SHAKE128::new(); - shake128.absorb(str.as_bytes()).expect("absorb before squeeze is infallible"); + shake128.do_update(str.as_bytes()); let serialized_128 = shake128.suspend(); match SHAKE256::from_suspended(serialized_128) { Err(SuspendableError::InvalidData) => { /* good */ } @@ -420,7 +387,7 @@ mod shake_tests { } let mut shake256 = SHAKE256::new(); - shake256.absorb(str.as_bytes()).expect("absorb before squeeze is infallible"); + shake256.do_update(str.as_bytes()); let serialized_256 = shake256.suspend(); match SHA3_256::from_suspended(serialized_256) { Err(SuspendableError::InvalidData) => { /* good */ } @@ -447,16 +414,15 @@ mod shake_tests { let output: Vec; if partial_bits == 0 { - shake.absorb(tc.msg.as_slice()).expect("absorb before squeeze is infallible"); - output = shake.squeeze(tc.output.len()); + shake.do_update(tc.msg.as_slice()); + let mut shake = shake.into_squeezer(); + output = shake.do_output(tc.output.len()); } else { - shake - .absorb(&tc.msg[..(tc.msg.len() - 1)]) - .expect("absorb before squeeze is infallible"); - shake - .absorb_last_partial_byte(tc.msg[tc.msg.len() - 1], partial_bits) - .expect("Absorb failed"); - output = shake.squeeze(tc.output.len()); + shake.do_update(&tc.msg[..(tc.msg.len() - 1)]); + let mut shake = shake + .into_squeezer_partial_bits(tc.msg[tc.msg.len() - 1], partial_bits) + .expect("partial_bits is in 1..=7"); + output = shake.do_output(tc.output.len()); } assert_eq!(tc.output, output); diff --git a/crypto/sha3/tests/tuplehash_tests.rs b/crypto/sha3/tests/tuplehash_tests.rs new file mode 100644 index 00000000..8395c13c --- /dev/null +++ b/crypto/sha3/tests/tuplehash_tests.rs @@ -0,0 +1,511 @@ +//! TupleHash against the NIST SP 800-185 sample values. +//! +//! Vectors come from the `bc-test-data` repo cloned alongside this one; see `cshake_tests.rs`. + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::hash::TestFrameworkHash; +use bouncycastle_hex as hex; +use bouncycastle_sha3::{TUPLEHASH128, TUPLEHASH256, TUPLEHASHXOF128, TUPLEHASHXOF256}; +use std::fs; +use std::path::Path; + +const DATA_DIRS: [&str; 2] = + ["../../../bc-test-data/crypto/sp800-185", "../bc-test-data/crypto/sp800-185"]; + +/// One `COUNT` block of a `.rsp` file. +struct Vector { + strength: usize, + s: String, + output_len: usize, + tuple: Vec>, + output: Vec, +} + +fn read_vectors(filename: &str) -> Option> { + let Some(dir) = DATA_DIRS.into_iter().find(|d| Path::new(d).exists()) else { + println!("WARNING: bc-test-data not found; TupleHash sample-value tests skipped"); + return None; + }; + let path = Path::new(dir).join(filename); + let content = fs::read_to_string(&path).unwrap_or_else(|e| { + panic!("bc-test-data is present but {} is unreadable: {e}", path.display()) + }); + + let mut out = Vec::new(); + let mut cur: Vec<(String, String)> = Vec::new(); + let finish = |cur: &mut Vec<(String, String)>, out: &mut Vec| { + if cur.is_empty() { + return; + } + let get = |k: &str| cur.iter().find(|(a, _)| a == k).map(|(_, b)| b.clone()); + let count: usize = get("Count").expect("Count").parse().expect("a number"); + let tuple = (1..=count) + .map(|i| hex::decode(get(&format!("Tuple{i}")).expect("a tuple element")).expect("hex")) + .collect(); + out.push(Vector { + strength: get("Strength").expect("Strength").parse().expect("a number"), + s: get("S").unwrap_or_default(), + output_len: get("Outputlen").expect("Outputlen").parse().expect("a number"), + tuple, + output: hex::decode(get("Output").expect("Output")).expect("hex"), + }); + cur.clear(); + }; + for line in content.lines() { + let line = line.trim_end(); + if line.starts_with('#') || line.is_empty() { + continue; + } + let Some((k, v)) = line.split_once(" = ") else { continue }; + if k == "COUNT" { + finish(&mut cur, &mut out); + } else { + cur.push((k.to_string(), v.to_string())); + } + } + finish(&mut cur, &mut out); + Some(out) +} + +fn as_slices(tuple: &[Vec]) -> Vec<&[u8]> { + tuple.iter().map(|v| v.as_slice()).collect() +} + +/// TupleHash (Sec 5.3): the output length is bound into the input. +#[test] +fn nist_sp800_185_tuplehash_sample_values() { + let Some(vectors) = read_vectors("TupleHash.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + let want = v.output_len / 8; + let t = as_slices(&v.tuple); + let got = match v.strength { + 128 => TUPLEHASH128::new(v.s.as_bytes(), want).hash_tuple(&t), + 256 => TUPLEHASH256::new(v.s.as_bytes(), want).hash_tuple(&t), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!( + got, + v.output, + "COUNT {i}: TupleHash{} with {} elements, S={:?}", + v.strength, + v.tuple.len(), + v.s + ); + } + println!("TupleHash: {} sample values", vectors.len()); +} + +/// TupleHashXOF (Sec 5.3.1): `right_encode(0)` in place of the length. +#[test] +fn nist_sp800_185_tuplehashxof_sample_values() { + let Some(vectors) = read_vectors("TupleHashXOF.rsp") else { return }; + assert!(!vectors.is_empty()); + + for (i, v) in vectors.iter().enumerate() { + let want = v.output_len / 8; + let t = as_slices(&v.tuple); + // do_output is the XOF reading of the stream; do_final and the one-shots bind the length + // they are given, and are checked against the fixed-length samples elsewhere. + let got = match v.strength { + 128 => TUPLEHASHXOF128::new(v.s.as_bytes()).output_for(&t).do_output(want), + 256 => TUPLEHASHXOF256::new(v.s.as_bytes()).output_for(&t).do_output(want), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!(got, v.output, "COUNT {i}: TupleHashXOF{} S={:?}", v.strength, v.s); + } + println!("TupleHashXOF: {} sample values", vectors.len()); +} + +/// `do_final` as the first read binds `right_encode(L)`, so it computes fixed-length TupleHash. +/// +/// SP 800-185 s. 5.3 and s. 5.3.1 differ in one field: step 4 is `newX = z || right_encode(L)` for +/// TupleHash and `newX = z || right_encode(0)` for TupleHashXOF. The encoding therefore need not +/// be chosen until the caller says how it wants to read, and `do_final` as the first read says +/// both how many bytes it wants and that it will not be back -- which is exactly `L`. +/// +/// `TupleHash.rsp` and `TupleHashXOF.rsp` publish the same tuples, customization and lengths, so +/// the fixed-length file is what `do_final` has to match, byte for byte. +#[test] +fn do_final_binds_the_length_when_nothing_has_been_read() { + let (Some(fixed), Some(xof)) = + (read_vectors("TupleHash.rsp"), read_vectors("TupleHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + let t = as_slices(&f.tuple); + let ctx = format!("COUNT {i}: TupleHashXOF{} S={:?}", f.strength, f.s); + let s = f.s.as_bytes(); + match f.strength { + 128 => check_do_final_binds_length( + || TUPLEHASHXOF128::new(s), + |n| TUPLEHASH128::new(s, n).hash_tuple(&t), + &t, + &f.output, + &x.output, + &ctx, + ), + 256 => check_do_final_binds_length( + || TUPLEHASHXOF256::new(s), + |n| TUPLEHASH256::new(s, n).hash_tuple(&t), + &t, + &f.output, + &x.output, + &ctx, + ), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + + // `output_for` hands back the squeezer directly, so `do_final` on it is the first read by + // construction -- the shortest way to spell fixed-length TupleHash through the XOF type. + let n = f.output.len(); + let got = match f.strength { + 128 => TUPLEHASHXOF128::new(s).output_for(&t).do_final(n), + 256 => TUPLEHASHXOF256::new(s).output_for(&t).do_final(n), + other => panic!("COUNT {i}: unexpected strength {other}"), + }; + assert_eq!(got, f.output, "{ctx}: output_for().do_final()"); + } + println!("TupleHashXOF do_final: {} sample values", fixed.len()); +} + +/// One paired sample through `do_final`. `fixed_expected` is the published fixed-length value, +/// `xof_expected` the published XOF value over the same tuple, and `fixed_of` computes the +/// fixed-length function at a length no vector covers. +fn check_do_final_binds_length( + make: impl Fn() -> X, + fixed_of: impl Fn(usize) -> Vec, + tuple: &[&[u8]], + fixed_expected: &[u8], + xof_expected: &[u8], + ctx: &str, +) { + let n = fixed_expected.len(); + assert_ne!(fixed_expected, xof_expected, "{ctx}: the two sample values must differ at all"); + let absorbed = || { + let mut x = make(); + tuple.iter().for_each(|element| x.do_update(element)); + x.into_squeezer() + }; + + // The first read, with no do_output before it: right_encode(8n), so the fixed-length function. + assert_eq!(absorbed().do_final(n), fixed_expected, "{ctx}: do_final binds the length"); + + // Pre-filled, so the documented zeroization is observable. + let mut buf = vec![0xFFu8; n]; + assert_eq!(absorbed().do_final_out(&mut buf), n, "{ctx}: do_final_out returns the length"); + assert_eq!(buf, fixed_expected, "{ctx}: do_final_out binds the length"); + + // The `L` bound is the length actually asked for, not a fixed one. No sample value covers + // these lengths, so the comparison is against this library's own fixed-length function. + for shorter in [n / 2, n - 1] { + assert_eq!(absorbed().do_final(shorter), fixed_of(shorter), "{ctx}: L = {shorter}"); + } + + // The one-shots name their length and never come back, so they bind it too. They take one + // tuple element, the last, after the rest have been fed in. + if let Some((last, rest)) = tuple.split_last() { + let mut x = make(); + rest.iter().for_each(|element| x.do_update(element)); + assert_eq!(x.xof(last, n), fixed_expected, "{ctx}: xof binds the length"); + + let mut buf = vec![0xFFu8; n]; + let mut x = make(); + rest.iter().for_each(|element| x.do_update(element)); + assert_eq!(x.xof_out(last, &mut buf), n, "{ctx}: xof_out returns the length"); + assert_eq!(buf, fixed_expected, "{ctx}: xof_out binds the length"); + } + + // Once a read has happened right_encode(0) is in the sponge and cannot be revised, so do_final + // after a do_output is the XOF stream continuing, not the fixed-length function. + let split = n / 2; + let mut squeezer = absorbed(); + let head = squeezer.do_output(split); + let tail = squeezer.do_final(n - split); + assert_eq!([head, tail].concat(), xof_expected, "{ctx}: do_final after a read stays the XOF"); +} + +/// The two are different functions on identical inputs, as for KMAC. +#[test] +fn tuplehashxof_is_not_tuplehash_truncated() { + let (Some(fixed), Some(xof)) = + (read_vectors("TupleHash.rsp"), read_vectors("TupleHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len()); + for (i, (f, x)) in fixed.iter().zip(xof.iter()).enumerate() { + assert_eq!(f.tuple, x.tuple, "COUNT {i}: the sample pairs share a tuple"); + assert_eq!(f.output_len, x.output_len, "COUNT {i}: ... and an output length"); + assert_ne!(f.output, x.output, "COUNT {i}: the two functions must differ"); + } +} + +/// Sec 5.1, the reason TupleHash exists: the boundaries between elements are part of the hash, so +/// re-splitting the same bytes gives an unrelated result. Every other hash in this library has the +/// opposite property, which is why it is worth pinning explicitly. +#[test] +fn the_tuple_boundaries_are_part_of_the_hash() { + let a = TUPLEHASH128::new(b"", 32).hash_tuple(&[b"abc", b"d"]); + let b = TUPLEHASH128::new(b"", 32).hash_tuple(&[b"ab", b"cd"]); + let c = TUPLEHASH128::new(b"", 32).hash_tuple(&[b"abcd"]); + assert_ne!(a, b, "the same bytes split differently must hash differently"); + assert_ne!(a, c, "... and differently again from a single element"); + assert_ne!(b, c); + + // An empty element is an element: dropping it changes the answer. + let with = TUPLEHASH128::new(b"", 32).hash_tuple(&[b"a", b"", b"b"]); + let without = TUPLEHASH128::new(b"", 32).hash_tuple(&[b"a", b"b"]); + assert_ne!(with, without, "an empty tuple element must still count"); +} + +/// `hash_tuple` and successive `do_update` calls must agree, since each update is one element. +#[test] +fn hash_tuple_matches_successive_updates() { + let tuple: [&[u8]; 3] = [b"first", b"second", b"third"]; + let one = TUPLEHASH128::new(b"S", 32).hash_tuple(&tuple); + + let mut t = TUPLEHASH128::new(b"S", 32); + for element in tuple { + t.do_update(element); + } + assert_eq!(t.do_final(), one, "do_update per element must equal hash_tuple"); +} + +/// The output length is bound for the fixed-length function and not for the XOF, so they have +/// opposite behaviour when the length changes -- the same split as KMAC. +#[test] +fn length_binding_differs_between_the_two() { + let t: [&[u8]; 2] = [b"x", b"y"]; + + let short = TUPLEHASH128::new(b"", 16).hash_tuple(&t); + let long = TUPLEHASH128::new(b"", 32).hash_tuple(&t); + assert_ne!(&long[..16], &short[..], "TupleHash: a different length is a different function"); + + let short = TUPLEHASHXOF128::new(b"").output_for(&t).do_output(16); + let long = TUPLEHASHXOF128::new(b"").output_for(&t).do_output(32); + assert_eq!(&long[..16], &short[..], "TupleHashXOF: one stream, so shorter is a prefix"); +} + +/// The customization string separates one use from another (Sec 5.2). +#[test] +fn customization_separates_the_functions() { + let t: [&[u8]; 2] = [b"x", b"y"]; + assert_ne!( + TUPLEHASH128::new(b"", 32).hash_tuple(&t), + TUPLEHASH128::new(b"My Application", 32).hash_tuple(&t), + ); +} + +/// A partial final byte cannot be expressed: the length encoding has to follow the tuple. +#[test] +fn partial_final_byte_is_refused() { + let mut t = TUPLEHASH128::new(b"", 32); + t.do_update(b"abc"); + assert!(matches!(t.do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); + + let mut t = TUPLEHASHXOF128::new(b""); + t.do_update(b"abc"); + assert!(matches!(t.into_squeezer_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); +} + +#[test] +fn algorithm_names() { + assert_eq!(TUPLEHASH128::ALG_NAME, "TupleHash128"); + assert_eq!(TUPLEHASH256::ALG_NAME, "TupleHash256"); + assert_eq!(TUPLEHASHXOF128::ALG_NAME, "TupleHashXOF128"); + assert_eq!(TUPLEHASHXOF256::ALG_NAME, "TupleHashXOF256"); +} + +/// Sponge rates from FIPS 202 Table 3, the nominal lengths of the XOF forms, and the constructed +/// length of the fixed forms. The generic checks elsewhere only require these to be positive. +#[test] +fn metadata() { + assert_eq!(TUPLEHASH128::new(b"", 32).block_bitlen(), 1344, "cSHAKE128 rate"); + assert_eq!(TUPLEHASH256::new(b"", 64).block_bitlen(), 1088, "cSHAKE256 rate"); + assert_eq!(TUPLEHASHXOF128::new(b"").block_bitlen(), 1344); + assert_eq!(TUPLEHASHXOF256::new(b"").block_bitlen(), 1088); + + assert_eq!(TUPLEHASH128::new(b"", 17).output_len(), 17, "whatever was asked for"); + assert_eq!(TUPLEHASH256::new(b"", 100).output_len(), 100); + assert_eq!(TUPLEHASHXOF128::new(b"").output_len(), 32, "the nominal length"); + assert_eq!(TUPLEHASHXOF256::new(b"").output_len(), 64); +} + +/// Every `Hash` entry point of the fixed-length form, against one sample value. +/// +/// The sample-value test above goes through `hash_tuple` only, which left `hash`, `hash_out` and +/// `do_final_out` unexercised: `cargo mutants` could replace each with a constant, and change the +/// `* 8` in the `right_encode(L)` that `do_final_out` absorbs, without a test noticing. +fn check_fixed_view(make: impl Fn() -> H, tuple: &[&[u8]], expected: &[u8], ctx: &str) { + let n = expected.len(); + assert_eq!(make().output_len(), n, "{ctx}: output_len"); + + // do_final_out into an exact buffer + let mut h = make(); + tuple.iter().for_each(|e| h.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(h.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, expected, "{ctx}: do_final_out"); + + // ... and into a longer one, which is only written up to the output length + let mut h = make(); + tuple.iter().for_each(|e| h.do_update(e)); + let mut out = vec![0xFFu8; n + 7]; + assert_eq!(h.do_final_out(&mut out), n); + assert_eq!(&out[..n], expected, "{ctx}: do_final_out, oversized buffer"); + // Hash::do_final_out zeroizes the whole buffer, so the tail is 0 rather than what the caller + // left there -- the same as SHA3, which is the contract these fixed-length types share. + assert_eq!(&out[n..], &[0u8; 7], "{ctx}: bytes past the output length are zeroized"); + + // hash and hash_out take one element: the last, after the rest have been fed in + let Some((last, rest)) = tuple.split_last() else { return }; + let mut h = make(); + rest.iter().for_each(|e| h.do_update(e)); + assert_eq!(h.hash(last), expected, "{ctx}: hash as the final element"); + + let mut h = make(); + rest.iter().for_each(|e| h.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(h.hash_out(last, &mut out), n, "{ctx}: hash_out returns the length"); + assert_eq!(out, expected, "{ctx}: hash_out"); +} + +/// Every `Hash` and `XOF` entry point of the XOF form, against one paired sample value. +/// +/// The samples ask for the nominal length, and the `Hash` view is a final read at that length, so +/// it binds `L` and must reproduce the *fixed-length* sample; reading the stream with `do_output` +/// must reproduce the XOF one. +fn check_xof_view( + make: impl Fn() -> X, + tuple: &[&[u8]], + expected: &[u8], + fixed_expected: &[u8], + ctx: &str, +) { + let n = expected.len(); + assert_eq!(make().output_len(), n, "{ctx}: the samples ask for the nominal length"); + assert_eq!(fixed_expected.len(), n, "{ctx}: ... and the paired samples share it"); + + let mut x = make(); + tuple.iter().for_each(|e| x.do_update(e)); + assert_eq!(x.do_final(), fixed_expected, "{ctx}: do_final"); + + let mut x = make(); + tuple.iter().for_each(|e| x.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(x.do_final_out(&mut out), n, "{ctx}: do_final_out returns the length"); + assert_eq!(out, fixed_expected, "{ctx}: do_final_out"); + + // zero partial bits is the byte-aligned case and must be accepted; any other count refused + let mut x = make(); + tuple.iter().for_each(|e| x.do_update(e)); + assert_eq!( + x.do_final_partial_bits(0, 0).unwrap(), + fixed_expected, + "{ctx}: do_final_partial_bits(0)" + ); + + let mut x = make(); + tuple.iter().for_each(|e| x.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(x.do_final_partial_bits_out(0, 0, &mut out).unwrap(), n, "{ctx}: ..._out length"); + assert_eq!(out, fixed_expected, "{ctx}: do_final_partial_bits_out(0)"); + + assert!(matches!(make().do_final_partial_bits(0xF0, 4), Err(HashError::InvalidLength(_)))); + let mut out = vec![0u8; n]; + assert!(matches!( + make().do_final_partial_bits_out(0xF0, 4, &mut out), + Err(HashError::InvalidLength(_)) + )); + + // the one-shots take one element: the last, after the rest have been fed in + let Some((last, rest)) = tuple.split_last() else { return }; + let mut x = make(); + rest.iter().for_each(|e| x.do_update(e)); + assert_eq!(x.hash(last), fixed_expected, "{ctx}: hash"); + + let mut x = make(); + rest.iter().for_each(|e| x.do_update(e)); + let mut out = vec![0u8; n]; + assert_eq!(x.hash_out(last, &mut out), n, "{ctx}: hash_out returns the length"); + assert_eq!(out, fixed_expected, "{ctx}: hash_out"); + + // The XOF reading of the stream is do_output; the one-shots bind the length they are given, + // so they belong to `do_final_binds_the_length_when_nothing_has_been_read` instead. + let mut x = make(); + rest.iter().for_each(|e| x.do_update(e)); + x.do_update(last); + assert_eq!(x.into_squeezer().do_output(n), expected, "{ctx}: do_output"); + + let mut x = make(); + rest.iter().for_each(|e| x.do_update(e)); + x.do_update(last); + assert_eq!(x.into_squeezer().do_output(n / 2), &expected[..n / 2], "{ctx}: do_output, shorter"); + + let mut x = make(); + rest.iter().for_each(|e| x.do_update(e)); + x.do_update(last); + let mut out = vec![0u8; n]; + assert_eq!(x.into_squeezer().do_output_out(&mut out), n, "{ctx}: do_output_out length"); + assert_eq!(out, expected, "{ctx}: do_output_out"); +} + +#[test] +fn hash_trait_view_agrees_with_the_sample_values() { + let Some(vectors) = read_vectors("TupleHash.rsp") else { return }; + for (i, v) in vectors.iter().enumerate() { + let n = v.output_len / 8; + let t = as_slices(&v.tuple); + let ctx = format!("COUNT {i}: TupleHash{}", v.strength); + match v.strength { + 128 => check_fixed_view(|| TUPLEHASH128::new(v.s.as_bytes(), n), &t, &v.output, &ctx), + 256 => check_fixed_view(|| TUPLEHASH256::new(v.s.as_bytes(), n), &t, &v.output, &ctx), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } +} + +#[test] +fn xof_trait_view_agrees_with_the_sample_values() { + let (Some(fixed), Some(xof)) = + (read_vectors("TupleHash.rsp"), read_vectors("TupleHashXOF.rsp")) + else { + return; + }; + assert_eq!(fixed.len(), xof.len(), "the two sample files pair up"); + + for (i, (f, v)) in fixed.iter().zip(xof.iter()).enumerate() { + let t = as_slices(&v.tuple); + let ctx = format!("COUNT {i}: TupleHashXOF{}", v.strength); + let s = v.s.as_bytes(); + match v.strength { + 128 => check_xof_view(|| TUPLEHASHXOF128::new(s), &t, &v.output, &f.output, &ctx), + 256 => check_xof_view(|| TUPLEHASHXOF256::new(s), &t, &v.output, &f.output, &ctx), + other => panic!("COUNT {i}: unexpected strength {other}"), + } + } +} + +/// Every output-buffer length, at both strengths and a non-default output length. +/// +/// `output_len` is bound into the computation, so a short buffer must truncate this TupleHash +/// rather than compute the TupleHash of a shorter length -- and must not panic, which it did +/// before this test existed. +#[test] +fn output_buffers_of_every_length() { + let framework = TestFrameworkHash::new(); + let input = b"the quick brown fox"; + + framework.test_hash_output_buffers(|| TUPLEHASH128::new(b"", 32), input); + framework.test_hash_output_buffers(|| TUPLEHASH256::new(b"", 64), input); + + // Non-default lengths, and a customization string. + framework.test_hash_output_buffers(|| TUPLEHASH128::new(b"My Tuple App", 17), input); + framework.test_hash_output_buffers(|| TUPLEHASH256::new(b"My Tuple App", 5), input); +} diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index ae00b642..f6b2cf7f 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -22,3 +22,7 @@ path = "src/bench_sha3_mem_usage.rs" [[bin]] name = "bench_aes_mem_usage" path = "src/bench_aes_mem_usage.rs" + +[[bin]] +name = "bench_ccm_mem_usage" +path = "src/bench_ccm_mem_usage.rs" diff --git a/mem_usage_benches/src/bench_ccm_mem_usage.rs b/mem_usage_benches/src/bench_ccm_mem_usage.rs new file mode 100644 index 00000000..52a34366 --- /dev/null +++ b/mem_usage_benches/src/bench_ccm_mem_usage.rs @@ -0,0 +1,302 @@ +//! The purpose of this binary is to perform a single run of the primitive under test so that +//! its peak memory usage can be measured with: +//! +//! ```text +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_ccm_mem_usage > /dev/null +//! +//! ms_print massif.out.835000 +//! ``` +//! +//! or, shoved all into one line: +//! +//! ```text +//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_ccm_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ``` +//! +//! Make sure you build in release mode! +//! +//! Note: print!() is used to force the compiler not to optimize away the actual code. +//! The important stuff for benchmarking goes to stderr so the junk can be piped to /dev/null. +//! +//! Main is at the bottom, and controls which of these actually runs -- measure one at a time, +//! because massif reports the peak across the whole process. +//! +//! # Why CCM gets a harness when the other modes do not +//! +//! CCM (NIST SP 800-38C) is the only mode in `bouncycastle-modes` with a non-trivial stack +//! profile, and it has it for a specific, avoidable reason. +//! +//! `Ccm` itself is boring: 256 B for AES-128, independent of message length, nonce length and tag +//! length, and per-byte work that touches a constant amount of stack. `print_struct_sizes` records +//! those, and they are the numbers to use. +//! +//! **`CcmEncryptor` / `CcmDecryptor` are the interesting case.** They exist to satisfy +//! `AEADCipherEncryptor` / `AEADCipherDecryptor`, whose `do_encrypt_init` is handed a key and no +//! length; CCM cannot form `B0` -- and so cannot authenticate anything -- until it knows the total +//! payload length (SP 800-38C Appendix A.2.1), so their **streaming** methods buffer the whole +//! message. That is `2 * FINAL_LEN` in the value (the crate docs' "4304 B at `FINAL_LEN = 2048`", +//! which `print_struct_sizes` confirms), and on top of it `do_final` returns a third +//! `[u8; FINAL_LEN]` by value. `bench_streaming_encrypt` / `bench_streaming_encrypt_detached` / +//! `bench_streaming_decrypt` drive that path -- `do_*_init`, `do_update_out`, then a final -- and +//! are what measure it, since it is the one memory claim in that crate large enough to matter. +//! +//! The adapters' **one-shots are not the streaming path**: `encrypt_out_detached` and its +//! siblings override the trait defaults and run `Ccm` directly, so the crate docs claim they cost +//! the same as `Ccm` regardless of `FINAL_LEN`. `bench_oneshot_encrypt_out_detached` checks that +//! claim, and must *not* be mistaken for a measurement of the buffers -- it never touches them. +//! +//! # What it measures +//! +//! Peak stack from `ms_print`, `--heap=no --stacks=yes`, release, on x86-64 with the pinned +//! nightly, at `FINAL_LEN = 16384`; every bench processes the same `FINAL_LEN - TAG_LEN` bytes. +//! `bench_do_nothing`'s 7.7 KB is the process's own start-up and is the floor below which nothing +//! is visible (see `FINAL_LEN` for why the harness is sized to clear it): +//! +//! ```text +//! bench_do_nothing 7 680 B +//! bench_direct_encrypt_detached 34 864 B two 16 KiB arrays (message, ciphertext) + frames +//! bench_direct_streaming 18 512 B one 16 KiB array, encrypted in place +//! bench_oneshot_encrypt_out_detached 36 184 B = direct + 1.3 KB: the DRBG the nonce is drawn from +//! bench_streaming_encrypt 134 968 B ~ 7 * FINAL_LEN above the message array +//! bench_streaming_encrypt_detached 135 000 B the same +//! bench_streaming_decrypt 149 976 B ~ 7 * FINAL_LEN above the message and sealed arrays +//! ``` +//! +//! Two things to take from that. The one-shot really does bypass the buffers: it is within the +//! cost of a DRBG of the direct path, at any `FINAL_LEN`. And the streaming path costs about +//! **`7 * FINAL_LEN`**, not the `3 * FINAL_LEN` a count of the arrays -- two in the value, one +//! returned -- would suggest: every method that finishes the flow takes the `2 * FINAL_LEN` value +//! by value, and each such move that the optimizer does not elide is another `2 * FINAL_LEN` on +//! the stack. That the detached final, which has one array fewer to return, measures the same is +//! consistent with the moves rather than the arrays being what dominates. It is a property of +//! passing a large value by value through the trait's consuming finals, not of CCM, and a caller +//! who cares should use the inherent `Ccm` API, which is the `bench_direct_streaming` line. +//! +//! The comparisons to draw, all on the *same* message: +//! +//! * `bench_streaming_encrypt` against `bench_direct_encrypt_detached`: the direct path does +//! identical cipher work with none of the buffers, so the difference is the whole cost of +//! streaming through the generic trait; +//! * `bench_oneshot_encrypt_out_detached` against `bench_direct_encrypt_detached`: these should +//! be within a couple of KB of each other, which is what "the one-shots bypass the buffer" means +//! in numbers. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::aes::aes_internal::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::core::key_material::{KeyMaterial, KeyType}; +use bouncycastle::core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle::modes::{Ccm, CcmDecryptor, CcmEncryptor, Decrypting, Encrypting}; + +/// The parameters the ACVP vectors and most protocols use: 12-byte nonce, 16-byte tag. +const NONCE_LEN: usize = 12; +const TAG_LEN: usize = 16; + +/// The adapters' `FINAL_LEN`: 16 KiB. Larger than any packet CCM was designed for, on purpose: +/// massif reports a peak of about 7.7 KB for `bench_do_nothing` -- the process's own start-up -- +/// and anything that peaks below that is invisible, so at 4 KiB the direct and one-shot paths all +/// read as "7.7 KB" and nothing can be compared. At 16 KiB every path clears that floor by a +/// wide margin and the multiples of `FINAL_LEN` are legible. The streaming capacity is +/// `FINAL_LEN - TAG_LEN`, so the message every bench sends is that. +const FINAL_LEN: usize = 16384; +const MESSAGE_LEN: usize = FINAL_LEN - TAG_LEN; + +type Aes128Ccm

= Ccm; +type Aes128CcmEncryptor = CcmEncryptor; +type Aes128CcmDecryptor = CcmDecryptor; + +fn key() -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&[0x42u8; N], KeyType::SymmetricCipherKey).unwrap() +} + +/// The message every bench processes, filled at run time and then only ever reached through a +/// `black_box`ed reference, so that it is a whole stack array in every bench alike. Without that, +/// a `[0xA5; N]` literal is a constant the compiler may keep in read-only data in one bench, or +/// fuse straight into the copy `encrypt_detached` makes in another, and the two paths that do +/// identical work measured a whole `MESSAGE_LEN` apart. +fn message() -> [u8; MESSAGE_LEN] { + let mut m = [0u8; MESSAGE_LEN]; + m.fill(core::hint::black_box(0xA5)); + m +} + +/// This exists so /usr/bin/time can measure the base memory footprint of the harness itself. +fn bench_do_nothing() { + eprintln!("DoNothing"); + + print!("{}", 1 + 1); +} + +/// Prints the in-memory size of each CCM value: the persistent cost of holding one open. +/// +/// The two things to notice are that `Ccm` does not depend on `NONCE_LEN` or `TAG_LEN` -- the nonce +/// lives inside the counter template and the tag is assembled at finalization -- and that the +/// buffering pair is more than an order of magnitude larger at any useful `FINAL_LEN`. +fn print_struct_sizes() { + use core::mem::size_of; + + eprintln!("--- Ccm: permutation + 3 blocks + 4 counters, independent of nonce/tag length ---"); + eprintln!("Ccm {:>7} B", size_of::>()); + eprintln!( + "Ccm {:>7} B", + size_of::>() + ); + eprintln!( + "Ccm {:>7} B", + size_of::>() + ); + eprintln!( + "Ccm {:>7} B", + size_of::>() + ); + eprintln!( + "Ccm {:>7} B", + size_of::>() + ); + eprintln!("Decrypting is the same size:"); + eprintln!("Ccm {:>7} B", size_of::>()); + + eprintln!("--- the buffering trait adapters: 2 * FINAL_LEN each ---"); + eprintln!("CcmEncryptor<.., {FINAL_LEN}> {:>7} B", size_of::()); + eprintln!("CcmDecryptor<.., {FINAL_LEN}> {:>7} B", size_of::()); + eprintln!( + "CcmEncryptor<.., 256> {:>7} B", + size_of::>() + ); + + print!("{}", size_of::>()); +} + +/// The direct, non-buffering path over the message: `Ccm` plus the caller's own buffers, and +/// nothing else. This is the baseline for both `bench_streaming_encrypt` and +/// `bench_oneshot_encrypt_out_detached`. +fn bench_direct_encrypt_detached() { + eprintln!("Ccm::encrypt_detached, {MESSAGE_LEN} B"); + + let k = key::<16>(); + let nonce = [0x24u8; NONCE_LEN]; + let plaintext = message(); + let plaintext = core::hint::black_box(&plaintext); + let mut ciphertext = [0u8; MESSAGE_LEN]; + let (_, tag) = + Aes128Ccm::::encrypt_detached(&k, &nonce, &[], plaintext, &mut ciphertext) + .unwrap(); + print!("{:x?}", &tag); +} + +/// The same message through the buffering encryptor's **streaming** methods, which is the only +/// path that touches its buffers: `do_encrypt_init` builds the `2 * FINAL_LEN` value, +/// `do_update_out` fills it and writes nothing, and `do_final` returns a third `[u8; FINAL_LEN]` +/// by value. Measures about `7 * FINAL_LEN` above the message array -- about `6 * FINAL_LEN` above +/// `bench_direct_encrypt_detached`, which also holds a ciphertext array -- see the module docs for +/// why that is more than the three arrays. +fn bench_streaming_encrypt() { + eprintln!( + "CcmEncryptor do_encrypt_init/do_update_out/do_final, {MESSAGE_LEN} B in 1 KiB chunks" + ); + + let k = key::<16>(); + let plaintext = message(); + let plaintext = core::hint::black_box(&plaintext); + let (mut enc, _nonce) = Aes128CcmEncryptor::do_encrypt_init(&k).unwrap(); + for chunk in plaintext.chunks(1024) { + enc.do_update_out(chunk, &mut []).unwrap(); + } + let (sealed, n) = enc.do_final().unwrap(); + print!("{:x?}", &sealed[n - TAG_LEN..n]); +} + +/// The same flow finished with `do_final_out_detached` into the caller's `[u8; FINAL_LEN]`, the +/// shape the shared test framework drives: one fewer `FINAL_LEN` array than `do_final`, which +/// builds that buffer itself and then returns it by value. +fn bench_streaming_encrypt_detached() { + eprintln!( + "CcmEncryptor do_encrypt_init/do_update_out/do_final_out_detached, {MESSAGE_LEN} B in 1 KiB chunks" + ); + + let k = key::<16>(); + let plaintext = message(); + let plaintext = core::hint::black_box(&plaintext); + let (mut enc, _nonce) = Aes128CcmEncryptor::do_encrypt_init(&k).unwrap(); + for chunk in plaintext.chunks(1024) { + enc.do_update_out(chunk, &mut []).unwrap(); + } + let mut ciphertext = [0u8; FINAL_LEN]; + let (_, tag) = enc.do_final_out_detached(&mut ciphertext).unwrap(); + print!("{:x?}", &tag); +} + +/// The decrypting side of the same comparison, with the tag inline: the decryptor buffers the +/// whole `ciphertext || tag` and `do_final` returns the `[u8; FINAL_LEN]` plaintext by value, so +/// the expectation is the same `7 * FINAL_LEN` or so, above the message and sealed arrays. +/// +/// The sealed message is produced with the direct one-shot so that only the streaming decrypt +/// is under measurement; massif reports the peak across the whole process, and the direct path +/// peaks well below the streaming one. +fn bench_streaming_decrypt() { + eprintln!( + "CcmDecryptor do_decrypt_init/do_update_out/do_final, {MESSAGE_LEN} B in 1 KiB chunks" + ); + + let k = key::<16>(); + let nonce = [0x24u8; NONCE_LEN]; + let plaintext = message(); + let plaintext = core::hint::black_box(&plaintext); + let mut sealed = [0u8; FINAL_LEN]; + let n = Aes128Ccm::::encrypt(&k, &nonce, &[], plaintext, &mut sealed).unwrap(); + + let mut dec = Aes128CcmDecryptor::do_decrypt_init(&k, &nonce).unwrap(); + for chunk in sealed[..n].chunks(1024) { + dec.do_update_out(chunk, &mut []).unwrap(); + } + let (opened, m) = dec.do_final().unwrap(); + print!("{}", opened[..m].len()); +} + +/// The buffering encryptor's **one-shot**, which the crate docs claim bypasses the buffers and +/// costs the same as `Ccm` regardless of `FINAL_LEN`. Measures about 1.3 KB above +/// `bench_direct_encrypt_detached` -- the DRBG it draws the nonce from -- and nowhere near +/// `bench_streaming_encrypt`. +fn bench_oneshot_encrypt_out_detached() { + eprintln!("CcmEncryptor::encrypt_out_detached, {MESSAGE_LEN} B"); + + let k = key::<16>(); + let plaintext = message(); + let plaintext = core::hint::black_box(&plaintext); + let mut ciphertext = [0u8; MESSAGE_LEN]; + let (_, _, tag) = + Aes128CcmEncryptor::encrypt_out_detached(&k, &[], plaintext, &mut ciphertext).unwrap(); + print!("{:x?}", &tag); +} + +/// The streaming direct path, which is what a caller in SP 800-38C Sec 3's packet environment +/// should use: the payload length is declared up front and nothing is buffered, so peak stack is +/// the `Ccm` value plus one chunk. +fn bench_direct_streaming() { + eprintln!("Ccm::do_encrypt_update, {MESSAGE_LEN} B in 1 KiB chunks"); + + let k = key::<16>(); + let nonce = [0x24u8; NONCE_LEN]; + let mut data = message(); + let data = core::hint::black_box(&mut data); + let mut ccm = Aes128Ccm::::new(&k, &nonce, &[], data.len()).unwrap(); + for chunk in data.chunks_mut(1024) { + ccm.do_encrypt_update(chunk).unwrap(); + } + let tag = ccm.do_encrypt_final().unwrap(); + print!("{:x?}", &tag); +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_direct_encrypt_detached() + // bench_streaming_encrypt() + // bench_streaming_encrypt_detached() + // bench_streaming_decrypt() + // bench_oneshot_encrypt_out_detached() + // bench_direct_streaming() +} diff --git a/mem_usage_benches/src/bench_sha3_mem_usage.rs b/mem_usage_benches/src/bench_sha3_mem_usage.rs index b08e2c3e..1235b3b1 100644 --- a/mem_usage_benches/src/bench_sha3_mem_usage.rs +++ b/mem_usage_benches/src/bench_sha3_mem_usage.rs @@ -25,7 +25,7 @@ #![allow(dead_code)] #![allow(unused_imports)] -use bouncycastle::core::traits::{Hash, Suspendable, XOF}; +use bouncycastle::core::traits::{Hash, Suspendable, XOF, XOFSqueezer}; use bouncycastle::sha3::{ SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN, }; @@ -85,9 +85,10 @@ fn bench_shake128_xof() { eprintln!("SHAKE128/absorb+squeeze_out"); let mut x = SHAKE128::new(); - x.absorb(&MSG).expect("absorb before squeeze is infallible"); + x.do_update(&MSG); let mut out = [0u8; 512]; - x.squeeze_out(&mut out); + let mut x = x.into_squeezer(); + x.do_output_out(&mut out); println!("{:x?}", out); } @@ -95,9 +96,10 @@ fn bench_shake256_xof() { eprintln!("SHAKE256/absorb+squeeze_out"); let mut x = SHAKE256::new(); - x.absorb(&MSG).expect("absorb before squeeze is infallible"); + x.do_update(&MSG); let mut out = [0u8; 512]; - x.squeeze_out(&mut out); + let mut x = x.into_squeezer(); + x.do_output_out(&mut out); println!("{:x?}", out); } diff --git a/mem_usage_benches/src/lib.rs b/mem_usage_benches/src/lib.rs index 0445bb89..54d20fc5 100644 --- a/mem_usage_benches/src/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -1,4 +1,5 @@ mod bench_aes_mem_usage; +mod bench_ccm_mem_usage; mod bench_mldsa_mem_usage; mod bench_mlkem_mem_usage; mod bench_sha3_mem_usage; diff --git a/src/lib.rs b/src/lib.rs index 16a27ad1..4cd3b075 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub use bouncycastle_aes as aes; +pub use bouncycastle_ascon as ascon; pub use bouncycastle_base64 as base64; pub use bouncycastle_core as core; pub use bouncycastle_factory as factory;