diff --git a/.gitignore b/.gitignore index 6d42084d..bdc86bd8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,23 @@ Cargo.lock **/target mutants.out*/ +custom_mutants_output/ .idea/ .vscode/ + +# editor swap / backup files +*.swp +*.swo +*~ + +# Claude Code: ignore personal/local state, but share team tooling +# (skills, slash commands and subagents). settings.json stays local, so personal permission +# allowlists cannot ride along in a PR. +.claude/* +!.claude/skills/ +!.claude/commands/ +!.claude/agents/ +.claude/settings.local.json +.claude 2/ diff --git a/CLAUDE.md b/CLAUDE.md index 6219e32e..fee4bd09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,8 +11,8 @@ previous session's reading of them. - **[QUALITY_AND_STYLE.md](QUALITY_AND_STYLE.md) — read before writing or changing code, and before reviewing a diff.** The authority on architecture, crate and API shape, naming conventions, fallibility, macros, what tests and - benchmarks a crate owes, and which sections crate docs must have. Its own opening line invites an AI to review a PR - against it, so treat it as exactly that checklist. + benchmarks a crate owes, which sections crate docs must have, and how much they should say. Its own opening line + invites an AI to review a PR against it, so treat it as exactly that checklist. - **[CONTRIBUTING.md](CONTRIBUTING.md) — read before writing a commit message, opening a PR, or advising on how a change gets merged.** The authority on coding philosophy, PR hygiene and self-review, the quality bar a submission must clear to be accepted, how merges actually happen in this project, and the AI policy. That policy places @@ -26,19 +26,39 @@ previous session's reading of them. Where this file and one of those documents disagree, the document wins — and say so, so the stale line here gets fixed. +## Project status: ALPHA + +The library is pre-1.0 alpha (workspace version `0.1.x`, release branches named `release/alpha`) and has no +users to protect yet. Until the first stable release: + +- **Breaking changes are fine.** Public API shape, trait signatures, crate layout and encodings may change in any + PR without deprecation cycles, compatibility shims, or migration notes. Prefer the right design over continuity. +- **Security bugs are ordinary bugs.** Fix them on a normal branch and PR, with a regression test and a plain + description of the defect and its impact. No security advisory, CVE, embargo, or coordinated release is needed. + SECURITY.md's reporting address still applies to reports from outside the project. + +Revisit this section at the first non-alpha release. + ## Toolchain -- Uses Rust **nightly** (pinned in `rust-toolchain.toml`) — `core/src/lib.rs` uses `#![feature(adt_const_params)]`. +- Builds on Rust **stable**: there is no toolchain pin, and no crate enables a `#![feature(...)]` gate, so + nightly-only tooling (`-Z` flags and the like) is not available. CI builds, tests and docs on stable; only the + `rustfmt` job installs nightly. - 2024 edition (set workspace-wide in the root `Cargo.toml`). +- Minimum Rust is 1.88: `rust-version` in the root `Cargo.toml`, inherited by `bouncycastle-utils` (the crate that + needs it, for `slice::as_chunks`) and so enforced for every crate that depends on it. ## Common commands -Build / test / bench / docs run against the cargo workspace from the repo root: +Build / test / bench / docs run against the cargo workspace from the repo root. `--workspace` is +not optional: the root manifest is both the workspace and the umbrella `bouncycastle` package, so a +bare `cargo build` builds only that package (no `cli`, no benches) and a bare `cargo test` runs +**zero** tests and still exits 0, because the umbrella crate has none of its own. ``` -cargo build # whole workspace incl. `bc-rust` CLI binary +cargo build --workspace # whole workspace incl. `bc-rust` CLI binary cargo build -p bouncycastle-sha3 # one sub-crate -cargo test # all tests +cargo test --workspace # all tests cargo test -p bouncycastle-mlkem # tests for one crate cargo test -p bouncycastle-mlkem ml_kem_tests # one integration test file cargo bench --all # all criterion benches @@ -51,22 +71,34 @@ 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/) ``` -Stack-memory benches are separate binaries under `mem_usage_benches/`: +`-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`: ``` cargo run --release -p mem_usage_benches --bin bench_mlkem_mem_usage cargo run --release -p mem_usage_benches --bin bench_mldsa_mem_usage ``` +`mem_usage_benches/src/lib.rs` makes those sources modules of a lib target as well, so their `//!` +headers are rustdoc'd and any indented or fenced block in them is compiled as a Rust doctest. The +valgrind and `ms_print` recipes there are fenced as ```` ```text ```` for that reason — keep it that +way when adding a harness, or `cargo test --workspace` fails to compile them. + ## Workspace architecture -The workspace has three top-level kinds of member: +The workspace has four top-level kinds of member: -1. `crypto/*` — one sub-crate per primitive (`sha2`, `sha3`, `hmac`, `hkdf`, `mlkem`, `mlkem_lowmemory`, `mldsa`, `mldsa_lowmemory`, `rng`, `hex`, `base64`, `utils`) plus the spine crates `core`, `core-test-framework`, and `factory`. Each crate is published as `bouncycastle-` and depended on internally via the `workspace.dependencies` table in the root `Cargo.toml`. -2. `src/` — the umbrella `bouncycastle` crate, which is just `pub use` re-exports of every sub-crate (e.g. `bouncycastle::sha3`, `bouncycastle::mlkem`). It exists so downstream users can pull the whole library with one dependency; it has no code of its own. +1. `crypto/*` — the library's sub-crates, plus the spine crates `core`, `core-test-framework`, and `factory` described below. Most are one primitive each; some, such as `cipher`, hold generic building blocks (modes, padding) as sub-modules. The set changes over time, so take it from `ls crypto/` or the root `Cargo.toml` rather than from a list here. Each crate is published as `bouncycastle-` and depended on internally via the `workspace.dependencies` table in the root `Cargo.toml`. +2. `src/` — the umbrella `bouncycastle` crate, which is just `pub use` re-exports of every sub-crate (e.g. `bouncycastle::sha3`, `bouncycastle::sm3`, `bouncycastle::mlkem`). It exists so downstream users can pull the whole library with one dependency; it has no code of its own. 3. `cli/` — the `bc-rust` binary built on top of `bouncycastle`, exposing every primitive as a streaming stdin→stdout subcommand using `clap`. 4. `mem_usage_benches/` — stand-alone binary crates that measure peak stack usage of algorithms (cannot be done via criterion). @@ -106,8 +138,27 @@ 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. +- AI-drafted docs and comments run long. Before committing, check them against QUALITY_AND_STYLE.md's "Proportion" + rule: cut duplicated material, long rationale and restated spec text rather than adding more. + +## Scope of changes + +Implement what was asked and stop. Unrequested refactors — extracting a trait, renaming for +readability, restructuring impls — are not free even when they are correct: bundled into a feature +commit they make the diff unreviewable, because a reviewer cannot separate the new behaviour from +the restructuring, and the review time that costs is the reason not to do it. + +- If a refactor genuinely unblocks the task, give it **its own commit ahead of** the feature, so it + can be reviewed or dropped on its own. +- If it unblocks nothing, propose it and wait rather than doing it. +- The same goes for drive-by comment rewrites, reformatting and file moves in code you are only + passing through. ## Working from specifications @@ -143,11 +194,16 @@ 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/`). -- Behaviour-critical private functions can use in-file `#[cfg(test)] mod tests` blocks when they can't be exercised from outside the crate. +- `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. - The per-width `impl Condition` blocks in `crypto/utils/src/ct.rs` (and their test modules) are deliberately duplicated rather than macro-generated: `cargo mutants` cannot see into `macro_rules!` bodies, so a macro would hide the mask identities from mutation testing. Do not fold them back into a macro. Any change to one width in a group (i64/i32, u64/u32) must be applied to every width in that group. ## CI -The only workflow is `.github/workflows/publish_doc_benches_to_ghpages.yaml`: on every PR it builds rustdoc and runs `quality_stats.sh`; on `main` it additionally runs `cargo bench --all` and publishes docs, code stats, and benchmark results to GitHub Pages (`https://bcgit.github.io/bc-rust/`). There is no separate CI test/lint job — local `cargo test` is the gate. \ No newline at end of file +The only workflow is `.github/workflows/publish_doc_benches_to_ghpages.yaml`: on every PR it builds rustdoc and runs `quality_stats.sh`; on `main` it additionally runs `cargo bench --all` and publishes docs, code stats, and benchmark results to GitHub Pages (`https://bcgit.github.io/bc-rust/`). There is no separate CI test/lint job — local `cargo test --workspace` is the gate, and nothing but a developer running it stands between a broken test and `main`. \ No newline at end of file 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 82b379fe..11e1f13c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,18 @@ members = ["cli", "crypto/*", "mem_usage_benches"] [workspace.package] edition = "2024" +# `bouncycastle-utils` uses `slice::as_chunks`, stable since 1.88; the 2024 edition alone needs 1.85. +rust-version = "1.88" version = "0.1.3" [workspace.dependencies] # *** Internal Dependencies *** bouncycastle = { path = "./" } +bouncycastle-aes = { path = "./crypto/aes" } +bouncycastle-ascon = { path = "./crypto/ascon" } bouncycastle-base64 = { path = "./crypto/base64" } +bouncycastle-cipher = { path = "./crypto/cipher" } bouncycastle-core = { path = "crypto/core" } bouncycastle-core-test-framework = { path = "./crypto/core-test-framework" } bouncycastle-factory = { path = "./crypto/factory" } @@ -23,6 +28,7 @@ bouncycastle-mldsa-lowmemory = { path = "./crypto/mldsa-lowmemory" } bouncycastle-rng = { path = "./crypto/rng" } bouncycastle-sha2 = { path = "./crypto/sha2" } bouncycastle-sha3 = { path = "./crypto/sha3" } +bouncycastle-sm3 = { path = "./crypto/sm3" } bouncycastle-utils = { path = "./crypto/utils" } @@ -41,7 +47,10 @@ version.workspace = true edition.workspace = true [dependencies] +bouncycastle-aes.workspace = true +bouncycastle-ascon.workspace = true bouncycastle-base64.workspace = true +bouncycastle-cipher.workspace = true bouncycastle-core.workspace = true bouncycastle-factory.workspace = true bouncycastle-hex.workspace = true @@ -54,3 +63,4 @@ bouncycastle-mlkem-lowmemory.workspace = true bouncycastle-rng.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true +bouncycastle-sm3.workspace = true diff --git a/INTRODUCTION.md b/INTRODUCTION.md index 13d816b8..270c3720 100644 --- a/INTRODUCTION.md +++ b/INTRODUCTION.md @@ -1,79 +1,109 @@ -The Legion of the Bouncy Castle is pleased to (finally) be releasing an alpha version of a brand new, from-scratch, Bouncy Castle cryptography library written natively in Rust. +The Legion of the Bouncy Castle is pleased to (finally) be releasing an alpha version of a brand new, from-scratch, +Bouncy Castle cryptography library written natively in Rust. # Why a new BC in Rust? First, a history of the Bouncy Castle project. -The Bouncy Castle project started in 1999 with the goal of providing a high-quality open source cryptographic library. Bouncy Castle has forks in Java, and C#; both platforms that have their own native crypto providers, and yet Bouncy Castle has thrived by providing a crypto library built painstakingly against the NIST FIPS specifications which makes it easy to certify, and due to it being fully open source with a small agile and responsive maintenance team. +The Bouncy Castle project started in 1999 with the goal of providing a high-quality open source cryptographic library. +Bouncy Castle has forks in Java, and C#; both platforms that have their own native crypto providers, and yet Bouncy +Castle has thrived by providing a crypto library built painstakingly against the NIST FIPS specifications which makes it +easy to certify, and due to it being fully open source with a small agile and responsive maintenance team. -Why a new Bouncy Castle in Rust? We have great respect for the [Rust Crypto project](https://github.com/RustCrypto) which has collected contributions from a wide range of developers and which covers a wide range of cryptographic primitives. That said, using it feels like it is a collection of contributions from multiple contributors without central cohesive design and interfaces. We felt that the Rust ecosystem was in need of a Bouncy Castle. +Why a new Bouncy Castle in Rust? We have great respect for the [Rust Crypto project](https://github.com/RustCrypto) +which has collected contributions from a wide range of developers and which covers a wide range of cryptographic +primitives. That said, using it feels like it is a collection of contributions from multiple contributors without +central cohesive design and interfaces. We felt that the Rust ecosystem was in need of a Bouncy Castle. # Design philosophy ## Serving both ends of the complexity spectrum -When you sit down to write a greenfield crypto library, you have to think of a spectrum of people who will use it, with a wide range of use cases and level of familiarity with cryptography. At one extreme you have developers building applications in a highly regulated space such as government or financial services where every aspect of the cryptography from internal security parameters to key lifetimes are strictly regulated. At the other extreme you have students and hobbyists who are using your library to explore cryptography and want it to be simple and just work. In the middle you have full-stack developers who are building production applications that need to be secure, but where the developer doesn't really want to learn any more cryptography than strictly necessary to get their feature working. +When you sit down to write a greenfield crypto library, you have to think of a spectrum of people who will use it, with +a wide range of use cases and level of familiarity with cryptography. At one extreme you have developers building +applications in a highly regulated space such as government or financial services where every aspect of the cryptography +from internal security parameters to key lifetimes are strictly regulated. At the other extreme you have students and +hobbyists who are using your library to explore cryptography and want it to be simple and just work. In the middle you +have full-stack developers who are building production applications that need to be secure, but where the developer +doesn't really want to learn any more cryptography than strictly necessary to get their feature working. -To this end, BC-Rust does expose, through `pub` structs and traits, the algorithm guts and parameters that NIST allows to be changed. For example, the HMAC-based Key Derivation Function (HKDF) is a complex two-step algorithm with many exposed parameters. If your application requires you to use the fully-parametrized version of HKDF, you can do so via these public APIs of BC-Rust's HKDF implementation: +To this end, BC-Rust does expose, through `pub` structs and traits, the algorithm guts and parameters that NIST allows +to be changed. For example, the HMAC-based Key Derivation Function (HKDF) is a complex two-step algorithm with many +exposed parameters. If your application requires you to use the fully-parametrized version of HKDF, you can do so via +these public APIs of BC-Rust's HKDF implementation: ```rust impl HKDF { - do_extract_init(salt: &impl KeyMaterial) -> Result; - - do_extract_update_key(ikm: &impl KeyMaterial) -> Result; - - do_extract_update_bytes(ikm_chunk: &[u8]) -> Result; - + do_extract_init(salt: & impl KeyMaterial) -> Result; + + do_extract_update_key(ikm: & impl KeyMaterial) -> Result; + + do_extract_update_bytes(ikm_chunk: & [u8]) -> Result; + do_extract_final() -> Result; - + expand_out( - prk: &impl KeyMaterial, - info: &[u8], - L: usize, - okm: &mut impl KeyMaterial, - ) -> Result + prk: & impl KeyMaterial, + info: & [u8], + L: usize, + okm: & mut impl KeyMaterial, + ) -> Result } ``` -That is, including the choice of hash function `H`, 7 adjustable input parameters spread across 5 function calls, which is enough to make a novice cryptographer's head spin! Plenty of rope to hang yourself with. To this end, we also offer a much simplified KDF trait and KDFFactory that lets you do the whole operation in two very straightforward lines of code: +That is, including the choice of hash function `H`, 7 adjustable input parameters spread across 5 function calls, which +is enough to make a novice cryptographer's head spin! Plenty of rope to hang yourself with. To this end, we also offer a +much simplified KDF trait and KDFFactory that lets you do the whole operation in two very straightforward lines of code: ```rust -let mut kdf = KDFFactory::new("HKDF-SHA256")?; -let new_key = kdf.derive_key(&seed_key, b"additional_input")?; +let mut kdf = KDFFactory::new("HKDF-SHA256") ?; +let new_key = kdf.derive_key( & seed_key, b"additional_input") ?; ``` or even one line if you need a KDF and aren't picky about which one: ```rust -let new_key = KDFFactory::default().derive_key(&seed_key, b"additional_input")?; +let new_key = KDFFactory::default ().derive_key( & seed_key, b"additional_input") ?; ``` - ## Library features and functionality ### FIPS certification -The primary design goal is straightforward FIPS certification. To this end, the BC-Rust source code is matched as line-for-line as is practical against the sample algorithms in NIST's FIPS, SP, and IG documents; down to function structure and variable names. In some cases, this means forgoing possible performance optimizations in favor of code readability and correspondence with the spec. We're ok with that. +The primary design goal is straightforward FIPS certification. To this end, the BC-Rust source code is matched as +line-for-line as is practical against the sample algorithms in NIST's FIPS, SP, and IG documents; down to function +structure and variable names. In some cases, this means forgoing possible performance optimizations in favor of code +readability and correspondence with the spec. We're ok with that. A few other design principles that we employ are described below. - ### No unsafe code! -Yes, in many cases you can improve performance by skirting the strict type and memory safety system of Rust, including by directly embedding assembly code. But to us, this undermines the primary reason that you're developing in Rust in the first place. We're not saying that we'll _never_ include unsafe code in the future, but we have no plans to do so in the short-term, and we would only do so with great care and only after employing rigorous processes such as formal correctness verification. - +Yes, in many cases you can improve performance by skirting the strict type and memory safety system of Rust, including +by directly embedding assembly code. But to us, this undermines the primary reason that you're developing in Rust in the +first place. Every crate carries `#![forbid(unsafe_code)]`, with one exception: `bouncycastle-utils` holds constant-time +and secure-zeroization code that cannot be accomplished reliably within safe rust. Each instance of unsafe is a few +lines with its safety argument alongside, and all unsafe is jailed to the one crate so that everything built on it stays +in safe Rust. ### If it compiles, then it's safe -That means that, where possible, we turn runtime errors into compile-time errors. For example, you _could_ design your SHA3 object to expose the internal KECCAK object that requires some fiddly parameters such as `rate` in order to instantiate it correctly and securely, but then you either allow people to create wierd non-standard things such as SHA3-257, or you end up with a constructor that throws nitpicky runtime errors about being parametrized incorrectly. Instead, we take the approach of hiding the parameters in a system of private traits and structs that only allow construction of NIST-approved and secure instances. For example, consider how our SHA3 object is constructed internally: +That means that, where possible, we turn runtime errors into compile-time errors. For example, you _could_ design your +SHA3 object to expose the internal KECCAK object that requires some fiddly parameters such as `rate` in order to +instantiate it correctly and securely, but then you either allow people to create wierd non-standard things such as +SHA3-257, or you end up with a constructor that throws nitpicky runtime errors about being parametrized incorrectly. +Instead, we take the approach of hiding the parameters in a system of private traits and structs that only allow +construction of NIST-approved and secure instances. For example, consider how our SHA3 object is constructed internally: ```rust impl SHA3 { - pub fn new() -> Self; + pub fn new() -> Self; } ``` -where `SHA3Params` carries all the fiddly parameters. We've made it a private trait so you can't make one, even if you wanted to; you have to choose from the ones built-in to the library. We then hide all of this behind simplified public types: +where `SHA3Params` carries all the fiddly parameters. We've made it a private trait so you can't make one, even if you +wanted to; you have to choose from the ones built-in to the library. We then hide all of this behind simplified public +types: ```rust pub type SHA3_224 = SHA3; @@ -84,13 +114,27 @@ pub type SHAKE128 = SHAKE; pub type SHAKE256 = SHAKE; ``` -so that in the end `SHA3_256::new().hash(&data)` just does what you expect. The "If it compiles, then it's safe" paradigm is, however, still somewhat aspirational and not a total _fait accompli_, and as the library matures, we will continue to find ways to refine our type system to turn ever more runtime error conditions into compile-time conditions. +so that in the end `SHA3_256::new().hash(&data)` just does what you expect. The "If it compiles, then it's safe" +paradigm is, however, still somewhat aspirational and not a total _fait accompli_, and as the library matures, we will +continue to find ways to refine our type system to turn ever more runtime error conditions into compile-time conditions. + +There is one deliberate exception. A mode of operation has to be built on a raw block permutation, and a stream cipher +on a raw keystream, and those primitives are correct, tested, and insecure if used on data directly. Sealed parameters +like `SHA3Params` keep the caller inside the safe set; these hand the caller the raw operation. They live under a +`hazmat` module in their crate (`bouncycastle_core::hazmat` defines the term), so that the path says what the docs +say, and `grep -rn hazmat` finds every such use in a code base. Everything outside `hazmat` keeps the contract above. ### KeyMaterial wrapper -In a cryptographic application, sometimes an array of bytes is just data, like config data read from a binary file, and sometimes it's the private key to decrypt your database. Keeping those two contexts cleanly separated is not only good hygiene, but it helps avoid vulnerabilities from creeping into your code base. Trust us, it's not just junior developers who fail to think about preventing the private key from getting logged in an error trace, or who lose track of the fact that this 512 bits of seed material went through SHA-256 and is therefore only at the 128-bit security strength now. +In a cryptographic application, sometimes an array of bytes is just data, like config data read from a binary file, and +sometimes it's the private key to decrypt your database. Keeping those two contexts cleanly separated is not only good +hygiene, but it helps avoid vulnerabilities from creeping into your code base. Trust us, it's not just junior developers +who fail to think about preventing the private key from getting logged in an error trace, or who lose track of the fact +that this 512 bits of seed material went through SHA-256 and is therefore only at the 128-bit security strength now. -To help reduce developer mistakes of this kind, we decided to build Bouncy Castle Rust from the beginning around a `KeyMaterial` object that is designed to prevent, or at least force the developer to think about, many of these types of key material misuses. +To help reduce developer mistakes of this kind, we decided to build Bouncy Castle Rust from the beginning around a +`KeyMaterial` object that is designed to prevent, or at least force the developer to think about, many of these types of +key material misuses. The core stucture is: @@ -119,11 +163,23 @@ pub enum KeyType { pub enum SecurityStrength { None, _112bit, _128bit, _192bit, _256bit, } ``` -While the `KeyMaterial` is fundamentally just a buffer of bytes, it tracks many of the things that cause problems if you fail to think about them, and it provides a number of utility functions such as a Drop that guarantees that the memory is zeroized when the object goes out of scope, a `.concatenate()` that correctly preserves the key type and security strength of the two keys being concatenated, a `.truncate()` that automatically downgrade the security strength accordingly, various guards against instantiating a full-entropy key from an all-zero buffer, and so forth. +While the `KeyMaterial` is fundamentally just a buffer of bytes, it tracks many of the things that cause problems if you +fail to think about them, and it provides a number of utility functions such as a Drop that guarantees that the memory +is zeroized when the object goes out of scope, a `.concatenate()` that correctly preserves the key type and security +strength of the two keys being concatenated, a `.truncate()` that automatically downgrade the security strength +accordingly, various guards against instantiating a full-entropy key from an all-zero buffer, and so forth. -The `KeyMaterial` object is used consistently across the library and any functions that manipulate a key material object will properly update the metadata to track any changes made to the key's entropy or security strength. For example, a `KeyMaterial512{ key_type: MACKey, security_strength: _256bit}` will have its security strength downgraded to 128 bit if you pass it through a SHA256-based KDF, indicating that it is no longer sufficient to generate a full-strength AES256 or ML-DSA-87. +The `KeyMaterial` object is used consistently across the library and any functions that manipulate a key material object +will properly update the metadata to track any changes made to the key's entropy or security strength. For example, a +`KeyMaterial512{ key_type: MACKey, security_strength: _256bit}` will have its security strength downgraded to 128 bit if +you pass it through a SHA256-based KDF, indicating that it is no longer sufficient to generate a full-strength AES256 or +ML-DSA-87. -Of course, there will always be things developers need to do that the library did not provide a utility function for, for example, you may actually need an all-zero MACKey in order to implement certain standardized MAC algorithms. To the end, the library will allow you to, for example, force a key type to any full-entropy key type and security strength, or even get a direct immutable or mutable reference to the underlying buffer via `.ref_to_bytes()` and `ref_to_bytes_mut()`, but only with use of the `allow_hazardous_operations` flag: +Of course, there will always be things developers need to do that the library did not provide a utility function for, +for example, you may actually need an all-zero MACKey in order to implement certain standardized MAC algorithms. To the +end, the library will allow you to, for example, force a key type to any full-entropy key type and security strength, or +even get a direct immutable or mutable reference to the underlying buffer via `.ref_to_bytes()` and +`ref_to_bytes_mut()`, but only with use of the `allow_hazardous_operations` flag: ```rust key.allow_hazardous_operations(); @@ -134,15 +190,35 @@ key.allow_hazardous_operations(); key.drop_hazardous_operations(); ``` -In keeping with Rust's general philosophy around unsafe code, the idea is not to prevent developers from doing what they need with their data, but rather to tag sections of source code that require more careful scrutiny from human reviewers and static analysis tools. +In keeping with Rust's general philosophy around unsafe code, the idea is not to prevent developers from doing what they +need with their data, but rather to tag sections of source code that require more careful scrutiny from human reviewers +and static analysis tools. ### Minimal external dependencies -The Rust ecosystem provides a great wealth of publicly-available crates. That said, for something as fundamental as a cryptography library, every external dependency becomes a supply-chain liability. By shipping someone else's code, you become responsible and liable for that code. That ranges from outright malicious or compromised upstream dependencies, to critical vulnerabilities that you get no advanced warning about, to maintenance headaches if you need a feature added to an upstream dependency only to discover that the maintainer has moved on and nobody is maintaining it anymore. So, while it's difficult to build a modern software project with zero external dependencies, we consider each one with great care and try to reproduce functionality internally where practical. +The Rust ecosystem provides a great wealth of publicly-available crates. That said, for something as fundamental as a +cryptography library, every external dependency becomes a supply-chain liability. By shipping someone else's code, you +become responsible and liable for that code. That ranges from outright malicious or compromised upstream dependencies, +to critical vulnerabilities that you get no advanced warning about, to maintenance headaches if you need a feature added +to an upstream dependency only to discover that the maintainer has moved on and nobody is maintaining it anymore. So, +while it's difficult to build a modern software project with zero external dependencies, we consider each one with great +care and try to reproduce functionality internally where practical. ### Designed for lightweight devices -Most people don't put "Java" or "DotNet" in the same sentence as "embedded microcontroller". This is not entirely fair as there are some incredibly lightweight JVMs, such as [Java Card](https://www.oracle.com/java/java-card/), but generally speaking, you'd be right to think that any device too small to run linux will not have a fun time with a java-based library. BC-Rust, however, is designed to go as small as you need. First, is the code structure breaking everything into its own sub-crate. For example, if you only need SHA2, then you can build only SHA2 (plus the small number of support and utility crates such as error types and math functions). Over time we plan to further granularize this by making use of rust cargo's excellent features system. Speaking of features, most rust applications are perfectly fine to compile against the rust standard runtime library (libstd); after all it brings great convenience features such as dynamically-sized arrays (Vec), stack overflow protection, and so forth. But when you get down to devices so small that they don't offer dynamic memory allocation (heap memory), then libstd doesn't work -- so no Vec for you! BC-Rust is designed towards eventually supporting a no_std build. For example, most of the public APIs in BC-Rust are twinned into a more ergonomic version that will return the result in a newly-allocated Vec of bytes, and also a version that takes a mutable slice of memory into which to write the result, as exemplified by the Hash trait: +Most people don't put "Java" or "DotNet" in the same sentence as "embedded microcontroller". This is not entirely fair +as there are some incredibly lightweight JVMs, such as [Java Card](https://www.oracle.com/java/java-card/), but +generally speaking, you'd be right to think that any device too small to run linux will not have a fun time with a +java-based library. BC-Rust, however, is designed to go as small as you need. First, is the code structure breaking +everything into its own sub-crate. For example, if you only need SHA2, then you can build only SHA2 (plus the small +number of support and utility crates such as error types and math functions). Over time we plan to further granularize +this by making use of rust cargo's excellent features system. Speaking of features, most rust applications are perfectly +fine to compile against the rust standard runtime library (libstd); after all it brings great convenience features such +as dynamically-sized arrays (Vec), stack overflow protection, and so forth. But when you get down to devices so small +that they don't offer dynamic memory allocation (heap memory), then libstd doesn't work -- so no Vec for you! BC-Rust is +designed towards eventually supporting a no_std build. For example, most of the public APIs in BC-Rust are twinned into +a more ergonomic version that will return the result in a newly-allocated Vec of bytes, and also a version that takes a +mutable slice of memory into which to write the result, as exemplified by the Hash trait: ```rust pub trait Hash { @@ -159,7 +235,9 @@ pub trait Hash { } ``` -We're also including a few other bells-and-whistles and hygiene items such as benchmark code, unit tests constructed to satisfy the mutation test framework cargo-mutants, as well as providing a `bc-rust` executable that provides a command-line interface to (a simplified subset of) the library's cryptographic primitives. +We're also including a few other bells-and-whistles and hygiene items such as benchmark code, unit tests constructed to +satisfy the mutation test framework cargo-mutants, as well as providing a `bc-rust` executable that provides a +command-line interface to (a simplified subset of) the library's cryptographic primitives. # Roadmap @@ -173,7 +251,8 @@ This alpha release includes the following cryptographic primitives: * HKDF * The NIST HashDRBG random number generator -But more than anything, the alpha release focuses on the design of the public trait and error type system contained in the `core-interface` sub-crate. +But more than anything, the alpha release focuses on the design of the public trait and error type system contained in +the `core-interface` sub-crate. Next up will be to round out the set of cryptographic primitives: @@ -193,7 +272,8 @@ After that, we'll tackle in some kind of order (depending on public interest and # Community feedback is most welcome! -As this is an alpha release, we're eagerly looking for feedback from the community. We would especially like feedback on the following areas: +As this is an alpha release, we're eagerly looking for feedback from the community. We would especially like feedback on +the following areas: * Public API ergonomics and granularity of exposed functionality. * Certification / compliance concerns. @@ -202,4 +282,5 @@ As this is an alpha release, we're eagerly looking for feedback from the communi You can reach us at Sincerely, -Mike Ounsworth (lead maintainer of BC-Rust), on behalf of the Legion of the Bouncy Castle and the entire Bouncy Castle community +Mike Ounsworth (lead maintainer of BC-Rust), on behalf of the Legion of the Bouncy Castle and the entire Bouncy Castle +community diff --git a/QUALITY_AND_STYLE.md b/QUALITY_AND_STYLE.md index 65f7e7e0..e8f79534 100644 --- a/QUALITY_AND_STYLE.md +++ b/QUALITY_AND_STYLE.md @@ -29,10 +29,14 @@ especially in low-level crypto code, that there are multiple correct ways to wri swapping an OR for an XOR results in functionally equivalent code. Where the behaviour of a function is critical to test but cannot be tested from outside the crate because it is on a -private function, in-line tests in the source file should be used. +private function, in-line tests in the source file should be used. In-file unit tests go in a single #[cfg (test)] `mod +tests {}` block at the end of the file, not as bare #[test] functions beside the code. Any helper functions needed for +testing must also be contained within the `mod tests {}` block. All traits in `bouncycastle-core` must have corresponding tests in `bouncycastle-core-test-framework` that exercise all behaviours and error conditions that are common to all implementations of that trait. +`bouncycastle-core-test-framework` is test infrastructure only: it goes under `[dev-dependencies]` and is never a +runtime dependency, since it ships a deterministic `FixedSeedRNG` and a deliberately insecure `ToyBlockCipher`. All crypto algorithms must have tests against the bc-test-data repo and against wycheproof. @@ -63,7 +67,16 @@ which parts were done for a very specific reason and should not be changed on a ## Naming Conventions -All normal rust naming convensions from clippy apply. In addition, some library-specific naming conventions: +All normal rust naming conventions from clippy apply, with one exception: + +* Where a type, constant or variable corresponds to something a specification (FIPS, RFC, etc) names, keep the + specification's spelling and capitalization, and `#[allow(non_camel_case_types)]`, `#[allow(non_snake_case)]` or + `#[allow(non_upper_case_globals)]` the item locally. So the FIPS 204 signature algorithm is `MLDSA65`, not `MlDsa44`, + and it's `AES_CBC_128`, not `AesCbc128`, and if a specification writes `A` for a matrix and `a` for a vector then + `let A = ...; let a = ...;` is the right thing to do for code readability and correspondence with the spec. The point + is that a reviewer with the specification open can match names by eye; that matters more here than rust convention. + +In addition, some library-specific naming conventions: * In constants, "LEN" is the length of a value in bytes (typically used for sizing arrays), whereas "SIZE" is a value in bits (typically used as a security parameter). For example SHA256 could have constants `HASH_SIZE = 256` and @@ -88,6 +101,60 @@ very little) object state to track and return errors about. Any struct that holds sensitive data must impl the `core::Secret` trait and all associated super-traits. +A primitive whose safe use depends on the caller composing it correctly -- a raw block permutation, a raw keystream +-- lives under a `hazmat` module in its crate or sub-module, never at the crate root or next to the safe API; +`bouncycastle_core::hazmat` defines the term and the supported uses. A crate or sub-module with such items declares +`pub mod hazmat;` +in its `lib.rs` and neither the crate nor the sub-module ever `pub use`s anything out of it, since a re-export would +bypass the notice. + +Any function that writes into a caller-provided output buffer must report how many bytes it wrote, as a `usize` in +its `Ok` value (on its own, or alongside anything else the function needs to return, such as a generated IV). This +holds even when the count is fully determined by the input -- a fixed-length `[u8; LEN]` buffer, say, always writes +exactly `LEN` -- so that callers never have to remember which output-buffer methods report their length and which +don't. + +### fn prefixes and suffixes + +Function prefixes and suffixes are used consistently across the library. + +Take, for example a one-shot API `fn encrypt(plaintext: &[u8]) -> Result, SymmetricCipherError>`. + +The following prefixes can be applied: + +* `do_`: this implies that it is part of a stateful streaming API, will typically take `&mut self`, and is likely + accompanied by a `do_encrypt_init()` and `do_encrypt_final()`. + +The following suffixes can be applied + +* `_init / _update / _final`: indicates phase of a stateful streaming API. Other verbs can be used here as appropriate + to the primitive, such as `absorb / squeeze`, `encrypt / decrypt`, etc. `_init` is typically a static constructor + (though exceptions may exist), and `_final` indicates that this function call renders the object unusable afterwards + by consuming `self` via a move: `_final(self, ..)`. +* `_rng`: indicates that this version of the function sources its random numbers from a provided `&mut dyn RNG` instead + of + from the default library RNG. `_rng(.., &mut dyn RNG)`. +* `_out / _inplace`: indicates that the function works in the provided buffer. `_out` indicates that the function takes + an output buffer, which may be oversized, and returns the number of bytes written to it: + `_out(.., out: &mut [u8] -> Result`. `_inplace` indicates that the input and output are + required to be the same size, and so the function uses the same buffer for input and output: + `_inplace(.., data: &mut [u8]) -> Result`. It is assumed that these will be + memory-efficient and work in the provided buffer instead of creating duplicate data on the stack. +* `_out_len`: a pair for an `_out` function that computes the minimum size of the output buffer required for the paired + `_out` function to succeed. This may be an over-estimate in order to guarantee success, for example if the size of the + required output buffer depends on the contents, and a subsequent call to the paired `_out` function does not actually + fill all of the requested space. + +Where multiple suffixes are present on a single function, they should go in this order: + +```text +_{init, update, final, etc}_{rng}_{out, out_len, inplace} +``` + +Any function that takes an output buffer via an `_out` function must zeroize the provided output buffer via a +`out.fill(0)` prior to writing to it. This must be done first, before even any error checking so that no stale content +is left in the output buffer, even in the case of an error. + ## Fallibility As much as humanly possible, Result and unwrap () should be used for "Bad input data" type things and not "Programmer @@ -128,8 +195,55 @@ Note that rust macros tend not to play well with a lot of dev tooling for compil `cargo mutants`, which is a good reason to avoid macros in core algorithm or data processing code. Macros can be used more freely within test code. +## Unit tests vs integration tests + +Unit tests are test code (and supporting helper functions) embedded in src/**.rs files. They have access to +crate-private or module-private functions and constants. + +Integration tests are test code (and supporting helper functions) in tests/**.rs files. They test the crate's code from +the outside -- ie through its public APIs -- since tests/ is a separate crate from src/. + +In general, integration tests are preferred over unit tests. This is for a number of reasons: + +* To reduce reviewer burden; reviewers will typically focus more effort on the src/ than the tests/, so we want to keep + src/ as short as is reasonable. +* Usually it is easier to determine what is the correct behaviour at the public API level. For example, this is the + level at which we typically have KATs and test vectors. +* Tools like cargo mutants are very helpful at detecting branches that are not exercisable via the public APIs, which + often is an indicator that the branch isn't doing what you think it's doing, or is simply not useful and can be + deleted. Unit tests that bypass the public APIs to pin these sorts of branches obscure the fact that this code is + unreachable. + +Unit tests are reasonable to include in the following cases: + +* There is high-risk code (usually meaning that it is complex code whose behaviour is not obvious from inspection) where + unit tests help to document the behaviour and protect against accidental breakage via a benign-looking change. +* AND where known answer tests are available. +* AND where this behaviour cannot be tested from integration tests. + +When writing unit tests, they should be contained with an `mod tests` at the bottom of the file, and ALL helper +functions that support the unit tests must be contained within that module. The intention is to clearly signal to a code +reviewer what is test code vs functional code. + # Docs +## Proportion + +Docs are a reading cost, so default to short. Each fact has one home: the crate docs are an overview plus links, and +the detail lives on the type or module it describes. Rationale is a sentence or two next to the code; history and +derivations go in the commit message. Give a few examples, not one per variant; keep memory tables to the figures, +without a per-row essay; keep CLI docs out of library crates; and never repeat a spec quote across files. Before adding +material, check whether the crate already states it. + +## No internal implementation detail in public API docs + +The doc comment on a `pub` item is read by a calling application, so it says what the caller can observe and must +do: the contract, the buffers and lengths involved, the errors and when they occur. How the implementor meets that +contract -- which bytes it holds back and why, which private helper runs, how another implementor does it, the design +rationale for a trait's shape -- belongs in a `//` comment next to the code, on the private item, or in the commit +message. A trait's docs in particular describe the trait, not any one implementor. When reviewing, read each public +doc comment as a user with no access to the source and strike anything that only makes sense with it. + ## Usage Examples The crate docs needs a section "Usage Examples" with sample code for all the major usage patterns of the primitives in @@ -144,4 +258,12 @@ the crate. Most crates should have a "Security Considerations" section that documents any footguns where the user of this crate could undermine their own security; for example where providing a seed or a nonce that is not truly random would -completely undermine the algorithm. \ No newline at end of file +completely undermine the algorithm. + +The heading is always exactly `# 🚨 Security Considerations 🚨`, wherever it appears: crate docs, module docs, or the +docs of an individual type or function. A consistent heading makes these sections easy to spot when reading and to find +with a search. + +## Release Notes + +For release note entries, keep succinct, one line per significant change at most. diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index 57f9e97c..e848ccc2 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -2,8 +2,37 @@ ## Major features +* 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_CCM, AES_CFB, AES_CFB8, AES_CTR, and AES_GCM. + * ASCON -- Ascon-AEAD128, Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 (NIST SP 800-232). +* Further memory usage improvements on ML-DSA / ML-KEM. New figures for the largest size are: + * ML-DSA-87/Sign 118 kb, ML-DSA-87/Verify 212 kb + * ML-DSA-87_lowmemory/Sign 25 kb, ML-DSA-87_lowmemory/Verify 21 kb + * ML-KEM-1024/Encaps 44 kb, ML-KEM-1024/Decaps 58 kb + * ML-KEM-1024_lowmemory/Encaps 11 kb, ML-KEM-1024/Decaps 21 kb + * Performance (throughput) actually saw a slight performance increase as this cleanup was largely about finding and + removing unnecessary memcpy's. + ## Minor features / bug fixes -* bug fixes to the way SHA3/SHAKE handled absorbing and squeezing a partial final byte. * Design discussions about whether core::traits::XOF (in the abstract) should allow interleaving absorb -> squeeze -> - absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future. + absorb (ie "absorb-after-squeeze). Outcome: absorb-after-squeeze forbidden. Could be changed in the future. Refactored + to an `XOF::xof()`, `XOF.into_squeezer()`, 'XOFSqueezer.output ()' shape. +* SHA2: + * Implemented SHA512/224 and SHA512/256. + * `Hash::do_final_partial_bits()` / `do_final_partial_bits_out()` are now implemented for SHA-2 (FIPS 180-4 s. 5.1). +* SHA3: + * Fixed a bug in `XOF::squeeze_partial_byte_final()`: when it was the first squeeze it bypassed the SHAKE `1111` + domain suffix and returned raw Keccak output, and it returned the wrong `num_bits` bits of the output byte. + * Changed the order of bits when absorbing a final partial byte to match ASN.1 DER BIT_STRING bit ordering. +* The constant-time helpers in bouncycastle-utils now use a more robust optimization barrier based on unsafe + `read_volatile` / `write_volatile` instead of `core::hint::black_box`, which is documented as best-effort only. +* Added the `hazmat` module convention for primitives whose safe use is the caller's job; `bouncycastle_core::hazmat` + defines it. + * The `do_hazardous_operations` handler for `KeyMaterial` is now `hazmat`. + * ML-KEM `encaps_internal` is now `hazmat::EncapsWithRandomness::encaps_with_randomness`, and + `HashDRBG80090A::new_unititialized` is `hazmat::NewUninitialized::new_uninitialized` (typo fixed); both are + extension traits, so the call needs the `hazmat` import. + * New cipher-related features `ElectronicCodeBook`, `KeyStream`, the `AES*Internal` types, `Ecb` and `AES_ECB_*`, + and `CtrKeyStream` under their crates' `hazmat` modules. diff --git a/cli/src/aes_cbc_cmd.rs b/cli/src/aes_cbc_cmd.rs new file mode 100644 index 00000000..ad5f7b94 --- /dev/null +++ b/cli/src/aes_cbc_cmd.rs @@ -0,0 +1,71 @@ +//! AES-CBC encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the IV convention, key loading, stdin framing and +//! block-alignment enforcement are all in [`crate::helpers::block_mode_helpers`], shared with the `aes*-cfb` and +//! `aes*-ecb` commands. See that module for the command-line contract. +//! +//! CBC (NIST SP 800-38A Sec 6.2) provides confidentiality only. It does not detect tampering, and +//! neither the ciphertext nor the IV is authenticated -- a flipped ciphertext bit flips the same bit +//! of the *next* block's plaintext (Appendix D). Do not decrypt data you have not authenticated +//! separately. + +use crate::helpers::block_mode_helpers::{ + BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_key, +}; +use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::cipher::modes::Cbc; +use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::key_material::KeyMaterial; + +/// Names the mode in error messages. +const MODE: &str = "CBC"; + +pub(crate) fn aes128_cbc_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<16>(key, key_file, "AES-128"), output_hex); +} + +pub(crate) fn aes192_cbc_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<24>(key, key_file, "AES-192"), output_hex); +} + +pub(crate) fn aes256_cbc_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<32>(key, key_file, "AES-256"), output_hex); +} + +/// Dispatches to the shared streaming loops with `Cbc` filled in as the mode. +fn run( + action: &CipherDirection, + key: &KeyMaterial, + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + match action { + CipherDirection::Encrypt => { + encrypt_stream::, KEY_LEN, BLOCK_LEN>( + key, output_hex, MODE, + ) + } + CipherDirection::Decrypt => { + decrypt_stream::, KEY_LEN, BLOCK_LEN>( + key, output_hex, MODE, + ) + } + } +} diff --git a/cli/src/aes_ccm_cmd.rs b/cli/src/aes_ccm_cmd.rs new file mode 100644 index 00000000..a2735f46 --- /dev/null +++ b/cli/src/aes_ccm_cmd.rs @@ -0,0 +1,506 @@ +//! 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 AAD is different: `--aad-file` is streamed. CCM needs the AAD's length before its first +//! byte (A.2.2 puts the encoding of `a` in front of `A`), and a regular file's size is known +//! before it is read, so the file is declared by its size and fed to the MAC in 1 KiB chunks +//! without ever being held whole. A file with no size to declare -- a pipe, `/dev/stdin` -- is +//! read whole instead. +//! +//! # 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::fs::File; +use std::io::{self, Read}; +use std::process::exit; + +use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::cipher::modes::Ccm; +use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::core::errors::SymmetricCipherError; +use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::hex; + +use crate::helpers; +use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_key}; + +/// Bytes of `--aad-file` read per call, matching the other commands' streaming chunk. +const CHUNK_LEN: usize = 1024; + +/// AES-128 CCM. See the module docs and the subcommand help. +pub(crate) fn aes128_ccm_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + nonce: &Option, + nonce_file: &Option, + aad: &Option, + aad_file: &Option, + tag_len: usize, + output_hex: bool, +) { + run::( + action, + &load_key::<16>(key, key_file, "AES-128"), + nonce, + nonce_file, + aad, + aad_file, + tag_len, + output_hex, + ); +} + +/// AES-192 CCM. See [`aes128_ccm_cmd`]. +pub(crate) fn aes192_ccm_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + nonce: &Option, + nonce_file: &Option, + aad: &Option, + aad_file: &Option, + tag_len: usize, + output_hex: bool, +) { + run::( + action, + &load_key::<24>(key, key_file, "AES-192"), + nonce, + nonce_file, + aad, + aad_file, + tag_len, + output_hex, + ); +} + +/// AES-256 CCM. See [`aes128_ccm_cmd`]. +pub(crate) fn aes256_ccm_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + nonce: &Option, + nonce_file: &Option, + aad: &Option, + aad_file: &Option, + tag_len: usize, + output_hex: bool, +) { + run::( + action, + &load_key::<32>(key, key_file, "AES-256"), + nonce, + nonce_file, + aad, + aad_file, + 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 +} + +/// Where the AAD comes from. +enum Aad { + /// `--aad` (hex), a `--aad-file` with no size to declare, or no AAD at all: held whole. + Bytes(Vec), + /// A `--aad-file` that is a regular file: declared by its size and read in [`CHUNK_LEN`] + /// pieces by [`feed_aad`], never held whole. + File { file: File, path: String, len: usize }, +} + +impl Aad { + /// The AAD length to declare to [`Ccm::new_with_lengths`]. + fn len(&self) -> usize { + match self { + Aad::Bytes(bytes) => bytes.len(), + Aad::File { len, .. } => *len, + } + } +} + +/// Loads the AAD from `--aad-file` (raw bytes, never hex-decoded) or `--aad` (hex); the file wins +/// if both are given, as for `aes*-gcm`. Empty if neither is given. +/// +/// A regular file is only opened and sized here; [`feed_aad`] reads it. Anything else -- a pipe, a +/// character device -- has no size to declare in advance, so it is read whole. +fn load_aad(aad: &Option, aad_file: &Option) -> Aad { + if let Some(path) = aad_file { + let file = File::open(path).unwrap_or_else(|e| { + eprintln!("Error: couldn't read file '{path}': {e}"); + exit(-1) + }); + let metadata = file.metadata().unwrap_or_else(|e| { + eprintln!("Error: couldn't read file '{path}': {e}"); + exit(-1) + }); + if !metadata.is_file() { + return Aad::Bytes(helpers::read_from_file_raw(path)); + } + let Ok(len) = usize::try_from(metadata.len()) else { + eprintln!("Error: AAD file '{path}' is too large for this platform."); + exit(-1) + }; + Aad::File { file, path: path.clone(), len } + } else if let Some(v) = aad { + Aad::Bytes(hex::decode(v).unwrap_or_else(|_| { + eprintln!("Error: associated data is not valid hex. Use --aad-file for raw bytes."); + exit(-1) + })) + } else { + Aad::Bytes(Vec::new()) + } +} + +/// Supplies all of the AAD declared as [`Aad::len`] to `ccm`, reading an [`Aad::File`] a chunk at +/// a time. +/// +/// The declared length is the file's size when it was opened. If the file changes size while it +/// is being read, the declared length is wrong, and the tag would be computed over a length +/// encoding that does not match the AAD; that is reported and the command exits rather than +/// producing it. +fn feed_aad( + ccm: &mut Ccm, + aad: &mut Aad, +) where + P: ElectronicCodeBook, +{ + match aad { + Aad::Bytes(bytes) => { + // Declared as exactly `bytes.len()`, and supplied in this one call. + ccm.do_update_aad(bytes).expect("declared AAD length matches what was sent"); + } + Aad::File { file, path, len } => { + let mut buf = [0u8; CHUNK_LEN]; + let mut read = 0usize; + loop { + let n = file.read(&mut buf).unwrap_or_else(|e| { + eprintln!("Error: couldn't read file '{path}': {e}"); + exit(-1) + }); + if n == 0 { + break; + } + read += n; + if ccm.do_update_aad(&buf[..n]).is_err() { + eprintln!("Error: AAD file '{path}' grew while it was being read."); + exit(-1) + } + } + if read != *len { + eprintln!("Error: AAD file '{path}' shrank while it was being read."); + exit(-1) + } + } + } +} + +/// 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: &CipherDirection, + key: &KeyMaterial, + nonce: &Option, + nonce_file: &Option, + aad: &Option, + aad_file: &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 mut aad = load_aad(aad, aad_file); + let input = read_all_stdin(); + let encrypt = matches!(action, CipherDirection::Encrypt); + + macro_rules! with_tag_len { + ($n:literal) => { + match tag_len { + 4 => { + go::(key, &nonce_bytes, &mut aad, input, encrypt, output_hex) + } + 6 => { + go::(key, &nonce_bytes, &mut aad, input, encrypt, output_hex) + } + 8 => { + go::(key, &nonce_bytes, &mut aad, input, encrypt, output_hex) + } + 10 => go::( + key, &nonce_bytes, &mut aad, input, encrypt, output_hex, + ), + 12 => go::( + key, &nonce_bytes, &mut aad, input, encrypt, output_hex, + ), + 14 => go::( + key, &nonce_bytes, &mut aad, input, encrypt, output_hex, + ), + 16 => go::( + key, &nonce_bytes, &mut aad, 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_with_lengths`]'s refusal of a payload past the `q` limit and exits. +/// +/// The only [`SymmetricCipherError::GenericError`] it 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_out`]/[`Ccm::decrypt_out`], 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. The AAD goes in through +/// [`Ccm::new_with_lengths`] and [`feed_aad`], so a `--aad-file` is streamed rather than loaded. +fn go( + key: &KeyMaterial, + nonce_bytes: &[u8], + aad: &mut Aad, + 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_with_lengths( + key, + &nonce, + aad.len(), + input.len(), + ) { + Ok(mut ccm) => { + feed_aad(&mut ccm, aad); + // `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(&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 { + crate::helpers::write_stdout(b"\n"); + } + } + 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_out`'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_with_lengths( + key, + &nonce, + aad.len(), + data.len(), + ) { + Ok(mut ccm) => { + feed_aad(&mut ccm, aad); + // 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 { + crate::helpers::write_stdout(b"\n"); + } + } + 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_cfb8_cmd.rs b/cli/src/aes_cfb8_cmd.rs new file mode 100644 index 00000000..a2d08a94 --- /dev/null +++ b/cli/src/aes_cfb8_cmd.rs @@ -0,0 +1,77 @@ +//! AES-CFB8 encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the IV convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the key loader), shared with the +//! `aes*-cfb` commands. See those modules for the command-line contract. +//! +//! # Which CFB +//! +//! These commands are **CFB8**: the segment size is one byte (`s = 8` in NIST SP 800-38A Sec 6.3). +//! That is a different, non-interoperable mode from the CFB128 of `aes*-cfb`, not a variant of it: +//! the two ciphertexts agree on their first byte and differ everywhere after it. It also costs a +//! full AES call per byte of data, sixteen times the work of `aes*-cfb`, so prefer `aes*-cfb` +//! unless a byte-granular self-synchronising stream is required or the format demands CFB8. +//! +//! # Any length +//! +//! CFB8's segment is a single byte, so these commands accept input of any length, pad nothing, and +//! emit a ciphertext exactly as long as the plaintext. +//! +//! # Warning +//! +//! CFB8 provides confidentiality only. It does not detect tampering, and neither the ciphertext nor +//! the IV is authenticated. Appendix D, Table D.2 gives "SBE in the decryption of Cj" plus random +//! errors in the next `b/s` segments: flipping a ciphertext bit flips the *same* bit of the *same* +//! plaintext byte, corrupts the following 16 bytes, and then decryption resynchronises. Do not +//! decrypt data you have not authenticated separately. + +use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_key}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::cipher::modes::Cfb8; +use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::key_material::KeyMaterial; + +pub(crate) fn aes128_cfb8_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<16>(key, key_file, "AES-128"), output_hex); +} + +pub(crate) fn aes192_cfb8_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<24>(key, key_file, "AES-192"), output_hex); +} + +pub(crate) fn aes256_cfb8_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<32>(key, key_file, "AES-256"), output_hex); +} + +/// Dispatches to the shared streaming loops with `Cfb8` filled in as the mode. +fn run( + action: &CipherDirection, + key: &KeyMaterial, + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + run_stream_mode::< + Cfb8, + Cfb8, + KEY_LEN, + BLOCK_LEN, + >(action, key, output_hex) +} diff --git a/cli/src/aes_cfb_cmd.rs b/cli/src/aes_cfb_cmd.rs new file mode 100644 index 00000000..00f99e98 --- /dev/null +++ b/cli/src/aes_cfb_cmd.rs @@ -0,0 +1,78 @@ +//! AES-CFB128 encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the IV convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the key loader), shared with the +//! `aes*-cfb8` commands. See those modules for the command-line contract. +//! +//! # Which CFB +//! +//! These commands are **CFB128**: the segment size is the full 16-byte block (`s = b` in NIST +//! SP 800-38A Sec 6.3). SP 800-38A also defines `s = 8`, which is a different, non-interoperable +//! mode -- if you need CFB8, the `aes*-cfb8` commands are it -- and `s = 1`, which this library does +//! not provide. +//! +//! # Any length +//! +//! CFB is a stream cipher, so unlike `aes*-cbc` and `aes*-ecb` these commands accept input of any +//! length and pad nothing; the ciphertext is exactly as long as the plaintext. For a message that +//! is not a whole number of blocks the last partial block is a short final segment, which is what +//! every streaming CFB128 implementation does; see the `bouncycastle_cipher::modes::Cfb` docs. +//! +//! # Warning +//! +//! CFB provides confidentiality only. It does not detect tampering, and neither the ciphertext nor +//! the IV is authenticated. CFB's malleability is more directly exploitable than CBC's: Appendix D, +//! Table D.2 gives "SBE in the decryption of Cj" -- flipping a ciphertext bit flips the *same* bit +//! of the plaintext in the *same* block, so an attacker edits the block they aimed at, at the cost +//! of randomising the next one. Do not decrypt data you have not authenticated separately. + +use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_key}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::cipher::modes::Cfb; +use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::key_material::KeyMaterial; + +pub(crate) fn aes128_cfb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<16>(key, key_file, "AES-128"), output_hex); +} + +pub(crate) fn aes192_cfb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<24>(key, key_file, "AES-192"), output_hex); +} + +pub(crate) fn aes256_cfb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<32>(key, key_file, "AES-256"), output_hex); +} + +/// Dispatches to the shared streaming loops with `Cfb` filled in as the mode. +fn run( + action: &CipherDirection, + key: &KeyMaterial, + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + run_stream_mode::< + Cfb, + Cfb, + KEY_LEN, + BLOCK_LEN, + >(action, key, output_hex) +} diff --git a/cli/src/aes_ctr_cmd.rs b/cli/src/aes_ctr_cmd.rs new file mode 100644 index 00000000..1281bce0 --- /dev/null +++ b/cli/src/aes_ctr_cmd.rs @@ -0,0 +1,86 @@ +//! AES-CTR encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: the nonce convention, key loading and stdin framing are in +//! [`crate::helpers::stream_mode_helpers`] (and [`crate::helpers::block_mode_helpers`] for the key loader), shared with the +//! `aes*-cfb` and `aes*-cfb8` commands. See those modules for the command-line contract. +//! +//! # The nonce is 12 bytes and the counter is 4 +//! +//! NIST SP 800-38A Sec 6.5 builds CTR on a sequence of counter blocks, and Appendix B.2's second +//! approach makes each one a message nonce followed by a counter. These commands use the +//! `AES_CTR_*` aliases, so the nonce is **12 bytes** and the counter is the remaining 4, giving +//! 2^32 blocks -- 64 GiB -- in a single message. +//! +//! `encrypt` writes that 12-byte nonce as the first bytes of its output and `decrypt` reads it back, +//! exactly as the other modes do with their IVs; note that it is 12 bytes here, not 16. +//! +//! # Any length +//! +//! CTR is a stream cipher: input of any length is accepted, nothing is padded, and the output is +//! exactly as long as the input. +//! +//! # Warning +//! +//! CTR provides confidentiality only. It does not detect tampering, and neither the ciphertext nor +//! the nonce is authenticated. It is the most malleable of the modes here: flipping any ciphertext +//! bit flips exactly the corresponding plaintext bit and affects nothing else (SP 800-38A +//! Appendix D, Table D.2, "SBE in the decryption of Cj"), so an attacker can edit the plaintext at +//! will, wherever they like, without any garbling to give it away. Do not decrypt data you have not +//! authenticated separately. +//! +//! A repeated nonce is fatal here rather than merely unwise: the same nonce under the same key +//! gives the same keystream, and two messages XORed with the same keystream leak their XOR. The +//! nonce is drawn from the OS-backed DRBG for exactly that reason, and there is no way to supply +//! one. + +use crate::helpers::block_mode_helpers::{BLOCK_LEN, CipherDirection, load_key}; +use crate::helpers::stream_mode_helpers::run_stream_mode; +use bouncycastle::aes::CTR_NONCE_LEN; +use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::cipher::modes::Ctr; +use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::key_material::KeyMaterial; + +pub(crate) fn aes128_ctr_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<16>(key, key_file, "AES-128"), output_hex); +} + +pub(crate) fn aes192_ctr_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<24>(key, key_file, "AES-192"), output_hex); +} + +pub(crate) fn aes256_ctr_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<32>(key, key_file, "AES-256"), output_hex); +} + +/// Dispatches to the shared streaming loops with `Ctr` filled in as the mode. +fn run( + action: &CipherDirection, + key: &KeyMaterial, + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + run_stream_mode::< + Ctr, + Ctr, + KEY_LEN, + CTR_NONCE_LEN, + >(action, key, output_hex) +} diff --git a/cli/src/aes_ecb_cmd.rs b/cli/src/aes_ecb_cmd.rs new file mode 100644 index 00000000..d9694390 --- /dev/null +++ b/cli/src/aes_ecb_cmd.rs @@ -0,0 +1,78 @@ +//! AES-ECB encryption and decryption, streaming stdin to stdout. +//! +//! Only the mode wiring lives here: key loading, stdin framing and block-alignment enforcement are +//! all in [`crate::helpers::block_mode_helpers`], shared with the `aes*-cbc` and `aes*-cfb` commands. See that +//! module for the command-line contract. ECB has no IV (`INIT_DATA_LEN = 0`), so unlike those +//! commands nothing is prepended to the output or consumed from the input: the ciphertext is exactly +//! as long as the plaintext. +//! +//! # Warning +//! +//! ECB (NIST SP 800-38A Sec 6.1) is **not a confidentiality mode for data**. Under a given key every +//! plaintext block maps to the same ciphertext block, so equal blocks stay visibly equal, the +//! structure of the plaintext shows through, and blocks can be reordered, repeated or removed with +//! nothing to detect it. The same plaintext encrypts to the same ciphertext every time. These commands +//! exist for interoperability with systems that use ECB and for driving test vectors; for data, use +//! `aes*-cbc` or `aes*-cfb` under separate authentication, or better an AEAD. + +use crate::helpers::block_mode_helpers::{ + BLOCK_LEN, CipherDirection, decrypt_stream, encrypt_stream, load_key, +}; +use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::cipher::modes::hazmat::Ecb; +use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::key_material::KeyMaterial; + +/// Names the mode in error messages. +const MODE: &str = "ECB"; + +pub(crate) fn aes128_ecb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<16>(key, key_file, "AES-128"), output_hex); +} + +pub(crate) fn aes192_ecb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<24>(key, key_file, "AES-192"), output_hex); +} + +pub(crate) fn aes256_ecb_cmd( + action: &CipherDirection, + key: &Option, + key_file: &Option, + output_hex: bool, +) { + run::(action, &load_key::<32>(key, key_file, "AES-256"), output_hex); +} + +/// Dispatches to the shared streaming loops with `Ecb` filled in as the mode. `INIT_DATA_LEN` is 0, +/// so the loops write and read no IV. +fn run( + action: &CipherDirection, + key: &KeyMaterial, + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + match action { + CipherDirection::Encrypt => { + encrypt_stream::, KEY_LEN, 0>( + key, output_hex, MODE, + ) + } + CipherDirection::Decrypt => { + decrypt_stream::, KEY_LEN, 0>( + key, output_hex, MODE, + ) + } + } +} diff --git a/cli/src/aes_gcm_cmd.rs b/cli/src/aes_gcm_cmd.rs new file mode 100644 index 00000000..544dd713 --- /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 +//! [`helpers::aead_cipher_helpers`], 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::helpers::aead_cipher_helpers::{decrypt_gcm, encrypt_gcm, load_aad}; +use crate::helpers::block_mode_helpers::{CipherDirection, load_key}; +use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::key_material::KeyMaterial; + +pub(crate) fn aes128_gcm_cmd( + action: &CipherDirection, + 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: &CipherDirection, + 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: &CipherDirection, + 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: &CipherDirection, + key: &KeyMaterial, + aad: &[u8], + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + match action { + CipherDirection::Encrypt => encrypt_gcm::(key, aad, output_hex), + CipherDirection::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..98c96be1 --- /dev/null +++ b/cli/src/ascon_cmd.rs @@ -0,0 +1,289 @@ +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::hazmat::do_hazardous_operations; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +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_encrypt_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_encrypt_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_encrypt_final_out(&mut tail).unwrap(); + helpers::write_bytes_or_hex(&tail[..tail_len], output_hex); + if output_hex { + crate::helpers::write_stdout(b"\n"); + } +} + +/// 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 { + crate::helpers::write_stdout(b"\n"); + } +} + +/// 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_decrypt_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_decrypt_out(&buf[..n], &mut out).unwrap(); + helpers::write_bytes_or_hex(&out[..written], output_hex); + } + + match cipher.do_decrypt_final() { + Ok((last, last_len)) => { + helpers::write_bytes_or_hex(&last[..last_len], output_hex); + if output_hex { + crate::helpers::write_stdout(b"\n"); + } + } + 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/encoders_cmd.rs b/cli/src/encoders_cmd.rs index c3b6d0b7..6a9aaf8b 100644 --- a/cli/src/encoders_cmd.rs +++ b/cli/src/encoders_cmd.rs @@ -1,70 +1,153 @@ use std::io; -use std::io::{Read, Write}; +use std::io::Read; +use std::process::exit; use bouncycastle::base64; use bouncycastle::hex; pub(crate) fn hex_encode_cmd() { - // Stream from stdin to stdout in chunks of 1 kb - let mut buf: [u8; 1024] = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - io::stdout() - .write_all(hex::encode(&buf[..bytes_read]).as_bytes()) - .expect("Failed to write to stdout"); - - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + // Stream from stdin to stdout in chunks of 1 kb. Hex has no state to carry: every byte + // becomes exactly two characters, so the chunking is invisible. + let mut buf = [0u8; 1024]; + 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 { + return; + } + crate::helpers::write_stdout(hex::encode(&buf[..n]).as_bytes()); } } +/// Streams hex from stdin to raw bytes on stdout. +/// +/// A read from a pipe can end anywhere, including between the two digits of one byte, so each +/// chunk is decoded only as far as it forms whole bytes and the remainder is carried into the next +/// one. [`hex::decode`] already skips whitespace and `\x` prefixes, which is what lets the output +/// of `-x` (hex plus a newline) and `\x41`-style dumps be piped straight in. pub(crate) fn hex_decode_cmd() { - // Stream from stdin to stdout in chunks of 1 kb - let mut buf: [u8; 1024] = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - let chunk_str: String = - String::from_utf8(Vec::from(&buf[..bytes_read])).expect("Input was not valid utf8."); + fn fail(e: hex::HexError) -> ! { + eprintln!("Error: input is not valid hex: {e:?}"); + exit(-1); + } + + let mut buf = [0u8; 1024]; + let mut pending: Vec = Vec::new(); + 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; + } + pending.extend_from_slice(&buf[..n]); - io::stdout() - .write_all(&*hex::decode(chunk_str.as_str()).expect("Input was not valid hex.")) - .expect("Failed to write to stdout"); + // A trailing backslash may be the start of a `\x` that continues in the next chunk, so + // backslashes at the end are never handed to the decoder on their own. + let mut decodable = pending.len(); + while pending[..decodable].last() == Some(&b'\\') { + decodable -= 1; + } + let decoded = match hex::decode(&pending[..decodable]) { + Ok(bytes) => bytes, + Err(hex::HexError::OddLengthInput) => { + // The chunk ended between two digits. Hold the unpaired digit -- the last hex + // digit present, since everything after it is skippable -- back for the next chunk. + let last_digit = pending[..decodable] + .iter() + .rposition(|b| b.is_ascii_hexdigit()) + .expect("OddLengthInput means at least one digit was seen"); + decodable = last_digit; + hex::decode(&pending[..decodable]).unwrap_or_else(|e| fail(e)) + } + Err(e) => fail(e), + }; + crate::helpers::write_stdout(&decoded); + pending.drain(..decodable); + } - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + // Whatever is still held back at end of input has to decode on its own: an unpaired digit or + // a dangling backslash here is a malformed input, not a chunk boundary. + if pending.last() == Some(&b'\\') { + eprintln!("Error: input is not valid hex: it ends in a lone backslash"); + exit(-1); + } + if !pending.is_empty() { + crate::helpers::write_stdout(&hex::decode(&pending).unwrap_or_else(|e| fail(e))); } } +/// Streams raw bytes from stdin to base64 on stdout. +/// +/// [`base64::Base64Encoder`] holds an incomplete 3-byte group across `do_update` calls, so the +/// chunking of the input is invisible; `do_final` at end of input emits the last group with its +/// padding, which is what makes the output decodable by anything. pub(crate) fn base64_encode_cmd() { let mut encoder = base64::Base64Encoder::new(); - // Stream from stdin to stdout in chunks of 1 kb - let mut buf: [u8; 1024] = [0u8; 1024]; - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - io::stdout() - .write_all(encoder.do_update(&buf[..bytes_read]).as_bytes()) - .expect("Failed to write to stdout"); - - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + let mut buf = [0u8; 1024]; + 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 { + crate::helpers::write_stdout(encoder.do_final(&[]).as_bytes()); + return; + } + crate::helpers::write_stdout(encoder.do_update(&buf[..n]).as_bytes()); } } +/// Streams base64 from stdin to raw bytes on stdout. +/// +/// [`base64::Base64Decoder`] is a streaming decoder, so a read from a pipe may end anywhere: it +/// carries a partial quartet across `do_update` calls itself. What it will not do is accept +/// padding through `do_update`; a chunk containing `=` is handed to `do_final` instead, which +/// also tolerates missing padding, so input that simply ends is finished the same way. pub(crate) fn base64_decode_cmd() { - // Stream from stdin to stdout in chunks of 1 kb - let mut buf: [u8; 1024] = [0u8; 1024]; - let mut decoder = base64::Base64Decoder::new(true); - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - let chunk_str: String = - String::from_utf8(Vec::from(&buf[..bytes_read])).expect("Input was not valid utf8."); - - io::stdout() - .write_all( - decoder - .do_update(chunk_str.as_str()) - .expect("Input was not valid base64.") - .as_slice(), - ) - .expect("Failed to write to stdout"); + fn fail(e: base64::Base64Error) -> ! { + eprintln!("Error: input is not valid base64: {e:?}"); + exit(-1); + } + fn read_chunk(buf: &mut [u8]) -> usize { + io::stdin().read(buf).unwrap_or_else(|e| { + eprintln!("Error: failed to read from stdin: {e}"); + exit(-1); + }) + } - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + let mut buf = [0u8; 1024]; + let mut decoder = base64::Base64Decoder::new(true); + loop { + let n = read_chunk(&mut buf); + if n == 0 { + // End of input with no padding seen: finish whatever partial block is held. + crate::helpers::write_stdout(&decoder.do_final(&[]).unwrap_or_else(|e| fail(e))); + return; + } + match decoder.do_update(&buf[..n]) { + Ok(bytes) => crate::helpers::write_stdout(&bytes), + Err(base64::Base64Error::PaddingEncounteredDuringDoUpdate) => { + // The chunk holds the padded final block; the decoder has not consumed it. + crate::helpers::write_stdout( + &decoder.do_final(&buf[..n]).unwrap_or_else(|e| fail(e)), + ); + // Padding ends the message. Anything but whitespace after it is not base64. + loop { + let n = read_chunk(&mut buf); + if n == 0 { + return; + } + if buf[..n].iter().any(|b| !b.is_ascii_whitespace()) { + eprintln!("Error: input is not valid base64: data follows the padding"); + exit(-1); + } + } + } + Err(e) => fail(e), + } } } diff --git a/cli/src/helpers.rs b/cli/src/helpers.rs deleted file mode 100644 index 207f0ee0..00000000 --- a/cli/src/helpers.rs +++ /dev/null @@ -1,118 +0,0 @@ -use bouncycastle::core::key_material::{ - KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; -use bouncycastle::core::traits::SecurityStrength; -use bouncycastle::hex; -use std::fs::File; -use std::io; -use std::io::{Read, Write}; -use std::process::exit; - -/// Reads either bin or hex -pub(crate) fn read_from_file(filename: &str) -> Vec { - let file = File::open(&filename); - if file.is_ok() { - let mut buf = Vec::::new(); - match file.unwrap().read_to_end(&mut buf) { - Ok(_bytes_read) => { - // try hex decoding it - match hex::decode(&buf) { - Ok(decoded) => decoded, - Err(_) => { - // it's not hex, so return it raw - buf - } - } - } - Err(_) => { - eprintln!("Error: couldn't open file '{}'", &filename); - exit(-1); - } - } - } else { - eprintln!("Error: couldn't open file '{}'", &filename); - exit(-1); - } -} - -/// Reads either bin or hex -pub(crate) fn read_from_file_or_stdin(filename: &Option) -> Vec { - if filename.is_some() { - // This already reads either bin or hex - return read_from_file(filename.as_ref().unwrap()); - } - - let mut buf = Vec::::new(); - io::stdin().read_to_end(&mut buf).expect("Failed to read from stdin"); - - // try hex decoding it - match hex::decode(&buf) { - Ok(decoded) => decoded, - Err(_) => { - // it's not hex, so return it raw - buf - } - } -} - -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}"); - } - } else { - io::stdout().write_all(bytes).unwrap(); - } -} - -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(); - } - } else { - file.write_all(bytes).unwrap(); - } -} - -/// Loads it as either hex or bytes -pub(crate) fn parse_seed(bytes: &[u8]) -> Result, ()> { - let bytes = if bytes.len() == 65 { &bytes[..64] } else { bytes }; - - // try decoding it as hex first - let seed_bytes: [u8; SEED_LEN] = match &hex::decode(&bytes) { - Ok(decoded_bytes) => { - if decoded_bytes.len() < SEED_LEN || decoded_bytes.len() > SEED_LEN + 1 { - // it was valid hex, but the wrong length - return Err(()); - } - decoded_bytes[..SEED_LEN].try_into().unwrap() - } - Err(_) => { - // it's not hex, so take the fist 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() - } - }; - - // TODO: Verify that all error conditions have been checked - let mut seed = KeyMaterial::::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); - - if seed.key_type() == KeyType::Zeroized || seed.security_strength() < SecurityStrength::_256bit - { - eprintln!( - "Warning: low entropy seed provided. We'll still process it, but it may be insecure." - ); - do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed)?; - seed.set_security_strength(SecurityStrength::_256bit) - }) - .unwrap(); - } - Ok(seed) -} diff --git a/cli/src/helpers/aead_cipher_helpers.rs b/cli/src/helpers/aead_cipher_helpers.rs new file mode 100644 index 00000000..161d6c54 --- /dev/null +++ b/cli/src/helpers/aead_cipher_helpers.rs @@ -0,0 +1,182 @@ +//! Shared plumbing for the AEAD subcommands: `aes{128,192,256}-gcm`. +//! +//! Parallel to [`crate::helpers::stream_mode_helpers`], but for [`bouncycastle::cipher::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_decrypt_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::{flush_stdout, read_from_file_raw, write_bytes_or_hex, write_stdout}; +use bouncycastle::cipher::modes::Gcm; +use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle::hex; +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 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 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_encrypt_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_encrypt_final_detachedtag().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_decrypt_final`. See the module docs for why +/// plaintext may already be written to stdout by the time a tag failure is reported. +pub 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.do_decrypt_out_len(n); + let mut out = vec![0u8; out_len]; + dec.do_decrypt_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_decrypt_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 { + write_stdout(b"\n"); + } + flush_stdout(); +} diff --git a/cli/src/helpers/block_mode_helpers.rs b/cli/src/helpers/block_mode_helpers.rs new file mode 100644 index 00000000..62cced89 --- /dev/null +++ b/cli/src/helpers/block_mode_helpers.rs @@ -0,0 +1,271 @@ +//! Shared plumbing for the block-cipher-mode subcommands: `aes{128,192,256}-{cbc,ecb}`. +//! +//! Everything here is mode-independent -- key loading, stdin framing, block-alignment enforcement, +//! output formatting -- and is generic over the mode via [`BlockCipherEncryptor`] / +//! [`BlockCipherDecryptor`]. `aes_cbc_cmd` and `aes_ecb_cmd` are thin dispatchers over it, so the +//! commands cannot drift apart on the parts that matter for correctness. +//! +//! The CFB and CTR commands are stream ciphers and live in [`crate::helpers::stream_mode_helpers`] instead; +//! they share +//! [`load_key`] and [`CipherDirection`] with this module, so the key handling and the `encrypt` / +//! `decrypt` spelling stay identical across all of them. +//! +//! # The IV travels in the ciphertext +//! +//! There is no `--iv` flag, and that is deliberate: `bouncycastle_cipher::modes` has no API for a +//! caller-supplied IV, because NIST SP 800-38A Sec 5.3 requires the CBC and CFB IV to be +//! *unpredictable* rather than merely unique. `encrypt` therefore generates one from the OS-backed +//! DRBG and writes it as the **first block of the output**; `decrypt` reads it back from the +//! **first block of the input**. So the two compose directly. The framing is generic over the +//! mode's `INIT_DATA_LEN`: for ECB it is 0, so those commands write and read no IV and the +//! ciphertext is exactly as long as the plaintext. +//! +//! ```text +//! bc-rust aes128-cbc encrypt --key-file k.bin < plain.bin > cipher.bin +//! bc-rust aes128-cbc decrypt --key-file k.bin < cipher.bin > plain.bin +//! ``` +//! +//! The IV is not secret (Sec 5.3), so shipping it in the clear is correct. Its *integrity* is not +//! protected, and neither is the ciphertext's -- see the warnings on each subcommand. +//! +//! # Input must be block-aligned +//! +//! The modes in this module are defined only on whole blocks (SP 800-38A Sec 5.2), and these +//! commands apply no padding, so input that is not a multiple of 16 bytes is rejected rather than +//! silently padded. (The CFB commands have no such requirement; see [`crate::helpers::stream_mode_helpers`].) +//! Padding is the caller's business; the library offers `bouncycastle_cipher::padding` for it, but wiring a +//! padding scheme into the CLI would change the on-the-wire format and is a separate decision. +//! +//! # Binary in, binary out +//! +//! stdin is read as binary so the commands compose in a pipeline. `-x` renders the *output* as hex. +//! For hex input, pipe through `hex-decode` first: +//! +//! ```text +//! cat cipher.hex | bc-rust hex-decode | bc-rust aes256-cbc decrypt --key-file k.bin +//! ``` + +use crate::helpers::{ + flush_stdout, read_from_file, strip_trailing_newline, write_bytes_or_hex, write_stdout, +}; +use bouncycastle::core::hazmat::do_hazardous_operations; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle::core::security_strength::SecurityStrength; +use bouncycastle::core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +use bouncycastle::hex; +use clap::ValueEnum; +use std::io; +use std::io::{Read, Write}; +use std::process::exit; + +/// The AES block length in bytes. +pub(crate) const BLOCK_LEN: usize = 16; + +/// Bytes processed per call: 1 KiB = 64 blocks, matching the other streaming commands. +/// +/// A full chunk goes through `do_*::` in one call, in place, which for decryption means +/// 32 pairs down the mode's two-block path. The at-most-63-block tail at end of input goes one +/// 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 cipher subcommands, -- 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 CipherDirection { + /// Encrypt stdin to stdout. See the subcommand's own help for this mode's exact framing. + Encrypt, + /// Decrypt stdin to stdout. See the subcommand's own help for this mode's exact framing. + Decrypt, +} + +/// Loads the key from `--key` (hex) or `--key-file` (binary or hex), and checks its length. +/// +/// `KEY_LEN` is exact: AES has three key lengths and the command selects one, so a key of the +/// wrong length is a mistake rather than something to truncate or pad. +pub(crate) fn load_key( + key: &Option, + key_file: &Option, + alg: &str, +) -> KeyMaterial { + let key_bytes: Vec = if let Some(key_file) = key_file { + // A file may hold raw bytes or hex; `read_from_file` tries hex first, as the other + // commands do. + let bytes = read_from_file(key_file); + // `read_from_file` already ignores a trailing newline on a hex file. A *raw* key file may + // end in one too, which lengthens the key by a byte; strip it only when that leaves + // exactly the key, so a binary key whose last byte really is `0x0a` is not shortened. + let trimmed = strip_trailing_newline(&bytes); + if trimmed.len() == KEY_LEN { trimmed.to_vec() } else { bytes } + } else if let Some(key) = key { + hex::decode(key).unwrap_or_else(|_| { + eprintln!("Error: `--key` must be hex. Use `--key-file` for raw bytes."); + exit(-1); + }) + } else { + eprintln!("Error: either `--key` or `--key-file` must be supplied."); + exit(-1); + }; + + if key_bytes.len() != KEY_LEN { + eprintln!("Error: {alg} needs a {KEY_LEN}-byte key, got {} bytes.", key_bytes.len()); + exit(-1); + } + + // `from_bytes_as_type` tags the key at the strength its length implies, which is exactly what + // the engine requires -- except for an all-zero key, which it marks Zeroized instead. + let mut key = + KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't load the key: {e:?}"); + exit(-1); + }); + + if key.key_type() != KeyType::SymmetricCipherKey { + // Same stance as `helpers::parse_seed`: warn, then do what was asked. A CLI is used for + // test vectors and scripting, where an all-zero key is a legitimate thing to want. + eprintln!( + "Warning: all-zero (or otherwise zeroized) key provided. Proceeding, but this is not secure." + ); + do_hazardous_operations(&mut key, |key| { + key.set_key_type(KeyType::SymmetricCipherKey)?; + key.set_security_strength(SecurityStrength::from_bytes(KEY_LEN)) + }) + .unwrap_or_else(|e| { + eprintln!("Error: couldn't tag the key: {e:?}"); + exit(-1); + }); + } + + key +} + +/// Encrypts stdin to stdout under the mode `E`, writing the generated init data (the IV) first. +/// +/// `INIT_DATA_LEN` is the mode's: one block for CBC, 0 for ECB, in which case nothing is written +/// ahead of the ciphertext. `mode` names the mode in error messages ("CBC", "ECB"); it has no +/// effect on the output. +pub(crate) fn encrypt_stream( + key: &KeyMaterial, + output_hex: bool, + mode: &str, +) where + E: BlockCipherEncryptor, +{ + let (mut enc, iv) = E::do_encrypt_init(key).unwrap_or_else(|e| { + eprintln!("Error: couldn't start encryption: {e:?}"); + exit(-1); + }); + + // The IV goes out ahead of the ciphertext, so `decrypt` can pick it up. (Empty for ECB.) + if INIT_DATA_LEN > 0 { + write_bytes_or_hex(&iv, output_hex); + } + + // The cipher works in place: `data` holds plaintext on the way in and ciphertext on the way out. + stream_aligned(mode, |data| { + if let Ok(chunk) = <&mut [u8; CHUNK_LEN]>::try_from(&mut *data) { + // Cannot fail: none of these modes has a per-IV data limit. + enc.do_encrypt_inplace(chunk).unwrap(); + } else { + // The bounded tail at end of input: whole blocks, fewer than a chunk. + for block in data.as_chunks_mut::().0 { + enc.do_encrypt_inplace(block).unwrap(); + } + } + write_bytes_or_hex(data, output_hex); + }); + + finish(output_hex); +} + +/// Decrypts stdin to stdout under the mode `D`, taking the init data (the IV) from the first +/// `INIT_DATA_LEN` bytes of input -- one block for CBC, nothing for ECB. +pub(crate) fn decrypt_stream( + key: &KeyMaterial, + output_hex: bool, + mode: &str, +) where + D: BlockCipherDecryptor, +{ + // The leading bytes are the IV, not ciphertext. (None for ECB: the read is skipped.) + let mut iv = [0u8; INIT_DATA_LEN]; + if INIT_DATA_LEN > 0 + && let Err(e) = io::stdin().read_exact(&mut iv) + { + eprintln!( + "Error: input too short to contain the {INIT_DATA_LEN}-byte IV that `encrypt` writes \ + as its first block ({e})." + ); + exit(-1); + } + + let mut dec = D::do_decrypt_init(key, &iv).unwrap_or_else(|e| { + eprintln!("Error: couldn't start decryption: {e:?}"); + exit(-1); + }); + + stream_aligned(mode, |data| { + if let Ok(chunk) = <&mut [u8; CHUNK_LEN]>::try_from(&mut *data) { + // A full chunk is 32 pairs, so this is the mode's two-block path. + dec.do_decrypt_inplace(chunk).unwrap(); + } else { + for block in data.as_chunks_mut::().0 { + dec.do_decrypt_inplace(block).unwrap(); + } + } + write_bytes_or_hex(data, output_hex); + }); + + finish(output_hex); +} + +/// Reads stdin and hands it to `process` in block-aligned pieces, mutably so it can be transformed +/// in place: a full `CHUNK_LEN` bytes each time one has accumulated, then once more at end of input +/// with whatever whole blocks remain (fewer than a chunk). Reads need not respect block or chunk boundaries -- bytes simply accumulate in the +/// buffer until it is full -- so a block split across two reads needs no special handling. +/// +/// Input whose total length is not a multiple of `BLOCK_LEN` is an error, because none of these +/// modes is defined on a partial block and these commands do not pad. +fn stream_aligned(mode: &str, mut process: impl FnMut(&mut [u8])) { + let mut buf = [0u8; CHUNK_LEN]; + let mut filled = 0usize; + + loop { + let n = io::stdin().read(&mut buf[filled..]).unwrap_or_else(|e| { + eprintln!("Error: failed to read from stdin: {e}"); + exit(-1); + }); + if n == 0 { + break; + } + filled += n; + if filled == CHUNK_LEN { + process(&mut buf); + filled = 0; + } + } + + if !filled.is_multiple_of(BLOCK_LEN) { + // Everything before the misaligned tail has already been written, and in hex mode is + // still sitting in stdout's line buffer with no newline to release it. Flush first so the + // ciphertext and the error come out in order rather than the error landing inside it. + let _ = io::stdout().flush(); + eprintln!( + "\nError: input to {mode} must be a whole number of {BLOCK_LEN}-byte blocks (data contained {} trailing byte(s)).", + filled % BLOCK_LEN + ); + exit(-1); + } + if filled != 0 { + process(&mut buf[..filled]); + } +} + +/// Flushes stdout, and adds the trailing newline the hex-output commands all emit. +fn finish(output_hex: bool) { + if output_hex { + write_stdout(b"\n"); + } + flush_stdout(); +} diff --git a/cli/src/helpers/mod.rs b/cli/src/helpers/mod.rs new file mode 100644 index 00000000..eb56485d --- /dev/null +++ b/cli/src/helpers/mod.rs @@ -0,0 +1,223 @@ +use bouncycastle::core::hazmat::do_hazardous_operations; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +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; + +pub mod aead_cipher_helpers; +pub mod block_mode_helpers; +pub mod stream_mode_helpers; + +/// 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); + }) +} + +/// `bytes` without one trailing line ending (`\n` or `\r\n`), if it has one. +/// +/// Files written by shell tools usually end in a newline that is not part of the value. Whether +/// stripping it is right depends on what the caller expects -- a binary key whose last byte +/// happens to be `0x0a` must not lose it -- so this only removes the bytes; the caller decides, +/// by the length it knows, whether to use the result. +pub(crate) fn strip_trailing_newline(bytes: &[u8]) -> &[u8] { + bytes.strip_suffix(b"\r\n").or_else(|| bytes.strip_suffix(b"\n")).unwrap_or(bytes) +} + +/// The bytes a hex-or-raw input stands for: its hex decoding if it is hex -- with one trailing +/// line ending ignored, since files and pasted input usually end in one -- and the bytes +/// themselves, untouched, otherwise. A raw input keeps its trailing newline; only the caller +/// knows whether that byte is part of the value (see `block_mode_helpers::load_key`). +pub(crate) fn hex_or_raw(buf: Vec) -> Vec { + // Decide by the bytes themselves rather than by whether the decoder accepts them: it skips + // NUL bytes as well as whitespace, so an all-zero binary key would otherwise "decode" to an + // empty hex string. Hex text is hex digits, whitespace and `\x` escapes, and nothing else. + let looks_like_hex = buf.iter().any(u8::is_ascii_hexdigit) + && buf + .iter() + .all(|b| b.is_ascii_hexdigit() || b.is_ascii_whitespace() || *b == b'\\' || *b == b'x'); + if !looks_like_hex { + return buf; + } + match hex::decode(strip_trailing_newline(&buf)) { + Ok(decoded) => decoded, + Err(_) => buf, + } +} + +/// Reads either bin or hex +pub(crate) fn read_from_file(filename: &str) -> Vec { + let file = File::open(&filename); + if file.is_ok() { + let mut buf = Vec::::new(); + match file.unwrap().read_to_end(&mut buf) { + Ok(_bytes_read) => hex_or_raw(buf), + Err(_) => { + eprintln!("Error: couldn't open file '{}'", &filename); + exit(-1); + } + } + } else { + eprintln!("Error: couldn't open file '{}'", &filename); + exit(-1); + } +} + +/// Reads either bin or hex +pub(crate) fn read_from_file_or_stdin(filename: &Option) -> Vec { + if filename.is_some() { + // This already reads either bin or hex + return read_from_file(filename.as_ref().unwrap()); + } + + let mut buf = Vec::::new(); + io::stdin().read_to_end(&mut buf).expect("Failed to read from stdin"); + hex_or_raw(buf) +} + +/// Writes `bytes` to stdout, exiting quietly if the reader has gone away. +/// +/// A closed stdout -- `bc-rust ... | head -c 16` -- is the reader's choice, not a failure of ours, +/// and Unix tools die silently of SIGPIPE in that case. Rust ignores SIGPIPE and reports the +/// condition as an `io::Error` of kind `BrokenPipe` instead, which `print!`, `println!` and an +/// `.unwrap()` on a write all turn into a panic. So every write to stdout in this binary goes +/// through here or [`flush_stdout`], and a broken pipe is a quiet exit with status 0: the reader +/// got what it asked for. Any other write failure is reported and is an error. +pub(crate) fn write_stdout(bytes: &[u8]) { + if let Err(e) = io::stdout().write_all(bytes) { + exit_on_write_error(e); + } +} + +/// Flushes stdout; see [`write_stdout`] for the broken-pipe behaviour. +pub(crate) fn flush_stdout() { + if let Err(e) = io::stdout().flush() { + exit_on_write_error(e); + } +} + +/// `println!` without the panic: `text` and a newline, through [`write_stdout`]. +pub(crate) fn println_stdout(text: &str) { + write_stdout(text.as_bytes()); + write_stdout(b"\n"); +} + +fn exit_on_write_error(e: io::Error) -> ! { + if e.kind() == io::ErrorKind::BrokenPipe { + exit(0); + } + eprintln!("Error: failed to write to stdout: {e}"); + exit(-1); +} + +pub(crate) fn write_bytes_or_hex(bytes: &[u8], output_hex: bool) { + if output_hex { + write_stdout(hex::encode(bytes).as_bytes()); + } else { + write_stdout(bytes); + } +} + +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(); + } + } else { + file.write_all(bytes).unwrap(); + } +} + +/// Loads it as either hex or bytes +pub(crate) fn parse_seed(bytes: &[u8]) -> Result, ()> { + let bytes = if bytes.len() == 65 { &bytes[..64] } else { bytes }; + + // try decoding it as hex first + let seed_bytes: [u8; SEED_LEN] = match &hex::decode(&bytes) { + Ok(decoded_bytes) => { + if decoded_bytes.len() < SEED_LEN || decoded_bytes.len() > SEED_LEN + 1 { + // it was valid hex, but the wrong length + return Err(()); + } + + decoded_bytes[..SEED_LEN].try_into().unwrap() + } + Err(_) => { + // 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() + } + }; + + // TODO: Verify that all error conditions have been checked + let mut seed = KeyMaterial::::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); + + if seed.key_type() == KeyType::Zeroized || seed.security_strength() < SecurityStrength::_256bit + { + eprintln!( + "Warning: low entropy seed provided. We'll still process it, but it may be insecure." + ); + + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + } + + Ok(seed) +} + +/// Stream stdin through a [`Hash`] and write the digest to stdout (hex or binary), followed by a +/// newline. Used by both the SHA-3 and Ascon-Hash256 subcommands. +pub(crate) fn stream_hash(mut hasher: impl Hash, output_hex: bool) { + let mut buf: [u8; 1024] = [0u8; 1024]; + + let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + + while bytes_read != 0 { + hasher.do_update(&buf[..bytes_read]); + + bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + } + + let out = hasher.do_final(); + write_bytes_or_hex(&out, output_hex); + write_stdout(b"\n"); +} + +/// Stream stdin through an [`XOF`] and squeeze `output_len` bytes to stdout (hex or binary), +/// followed by a newline. Used by both the SHAKE and Ascon-XOF128/CXOF128 subcommands. +pub(crate) fn stream_xof(mut xof: impl XOF, output_len: usize, output_hex: bool) { + let mut buf: [u8; 1024] = [0u8; 1024]; + + let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + + while bytes_read != 0 { + xof.do_update(&buf[..bytes_read]); + + bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + } + + let out = xof.into_squeezer().do_output_final(output_len); + write_bytes_or_hex(&out, output_hex); + write_stdout(b"\n"); +} diff --git a/cli/src/helpers/stream_mode_helpers.rs b/cli/src/helpers/stream_mode_helpers.rs new file mode 100644 index 00000000..b15bb4c1 --- /dev/null +++ b/cli/src/helpers/stream_mode_helpers.rs @@ -0,0 +1,151 @@ +//! Shared plumbing for the stream-cipher-mode subcommands: `aes{128,192,256}-{cfb,cfb8,ctr}`. +//! +//! The stream-cipher counterpart of [`crate::helpers::block_mode_helpers`], and deliberately parallel to it: +//! same key loading (reused directly from there), same IV convention, same `-x` hex output, same +//! 1 KiB streaming chunk. Everything here is mode-independent and generic over +//! [`StreamCipherEncryptor`] / [`StreamCipherDecryptor`], so `aes_cfb_cmd`, `aes_cfb8_cmd` and +//! `aes_ctr_cmd` are thin dispatchers over it and cannot drift apart on the parts that matter for +//! correctness. The init data length is a parameter, so it need not be a whole block: it is the +//! block for the CFB modes and a 12-byte nonce for CTR. +//! +//! # The IV travels in the ciphertext +//! +//! Exactly as for the block modes: there is no `--iv` flag, because `bouncycastle_cipher::modes` has no API +//! for a caller-supplied IV -- NIST SP 800-38A Sec 5.3 requires the CFB IV to be *unpredictable* +//! rather than merely unique. `encrypt` generates one from the OS-backed DRBG and writes it as the +//! **first block of the output**; `decrypt` reads it back from the **first block of the input**, so +//! the two compose directly in a pipeline. +//! +//! # No alignment requirement, and no padding +//! +//! This is the one place the stream commands differ from the block ones. A stream cipher is defined +//! on any length -- CFB8's segment is a byte, and `Cfb` extends the `s = b` equations to a short +//! final segment (see its module docs) -- so input of *any* size is accepted, nothing is padded, +//! and the ciphertext is exactly as long as the plaintext. A partial read from stdin therefore +//! needs no buffering to a block boundary: whatever arrives is processed immediately. +//! +//! # Binary in, binary out +//! +//! stdin is read as binary so the commands compose in a pipeline. `-x` renders the *output* as hex. +//! For hex input, pipe through `hex-decode` first. + +use crate::helpers::block_mode_helpers::{CHUNK_LEN, CipherDirection}; +use crate::helpers::{flush_stdout, write_bytes_or_hex, write_stdout}; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +use std::io; +use std::io::Read; +use std::process::exit; + +/// Encrypts stdin to stdout under the stream mode `E`, writing the generated IV first. +/// +/// `INIT_DATA_LEN` is the mode's: one block for CFB and CFB8. +pub(crate) fn encrypt_stream( + key: &KeyMaterial, + output_hex: bool, +) where + E: StreamCipherEncryptor, +{ + let (mut enc, iv) = E::do_encrypt_init(key).unwrap_or_else(|e| { + eprintln!("Error: couldn't start encryption: {e:?}"); + exit(-1); + }); + + // The IV goes out ahead of the ciphertext, so `decrypt` can pick it up. + write_bytes_or_hex(&iv, output_hex); + + // The cipher works in place: `data` holds plaintext on the way in and ciphertext on the way out. + stream(|data| { + // Cannot fail: neither CFB nor CFB8 has a per-IV data limit. + // CFB and CFB8 cannot fail here; CTR can, once its counter is exhausted, which is a real + // limit a long enough stream reaches rather than a bug. + enc.do_encrypt_inplace(data).unwrap_or_else(|e| { + eprintln!("Error: encryption failed: {e:?}"); + exit(-1); + }); + write_bytes_or_hex(data, output_hex); + }); + + finish(output_hex); +} + +/// Decrypts stdin to stdout under the stream mode `D`, taking the IV from the first +/// `INIT_DATA_LEN` bytes of input. +pub(crate) fn decrypt_stream( + key: &KeyMaterial, + output_hex: bool, +) where + D: StreamCipherDecryptor, +{ + // The leading bytes are the IV, not ciphertext. + let mut iv = [0u8; INIT_DATA_LEN]; + if let Err(e) = io::stdin().read_exact(&mut iv) { + eprintln!( + "Error: input too short to contain the {INIT_DATA_LEN}-byte IV that `encrypt` writes \ + as its first block ({e})." + ); + exit(-1); + } + + let mut dec = D::do_decrypt_init(key, &iv).unwrap_or_else(|e| { + eprintln!("Error: couldn't start decryption: {e:?}"); + exit(-1); + }); + + stream(|data| { + dec.do_decrypt_inplace(data).unwrap_or_else(|e| { + eprintln!("Error: decryption failed: {e:?}"); + exit(-1); + }); + write_bytes_or_hex(data, output_hex); + }); + + finish(output_hex); +} + +/// Reads stdin and hands it to `process` in pieces of at most `CHUNK_LEN` bytes, mutably so it can +/// be transformed in place. +/// +/// Unlike the block modes' `stream_aligned`, nothing is buffered to a boundary and no length is +/// rejected: a stream cipher takes any number of bytes, and a sequence of calls is equivalent to +/// one call over the concatenation, so whatever a read returns can go straight through. That also +/// means the mode's own byte path is exercised at whatever alignment the pipe happens to deliver, +/// which is precisely what the trait guarantees is safe. +fn stream(mut process: impl FnMut(&mut [u8])) { + 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; + } + process(&mut buf[..n]); + } +} + +/// Flushes stdout, and adds the trailing newline the hex-output commands all emit. +fn finish(output_hex: bool) { + if output_hex { + write_stdout(b"\n"); + } + flush_stdout(); +} + +/// Runs one direction of a stream mode. The two `run` dispatchers in `aes_cfb_cmd` and +/// `aes_cfb8_cmd` differ only in which mode they name, so the match lives here. +pub(crate) fn run_stream_mode( + action: &CipherDirection, + key: &KeyMaterial, + output_hex: bool, +) where + E: StreamCipherEncryptor, + D: StreamCipherDecryptor, +{ + match action { + CipherDirection::Encrypt => encrypt_stream::(key, output_hex), + CipherDirection::Decrypt => decrypt_stream::(key, output_hex), + } +} diff --git a/cli/src/hkdf_cmd.rs b/cli/src/hkdf_cmd.rs index 9c7a00ee..499d11af 100644 --- a/cli/src/hkdf_cmd.rs +++ b/cli/src/hkdf_cmd.rs @@ -1,13 +1,11 @@ -use std::io::Write; +use std::fs; use std::process::exit; -use std::{fs, io}; -use bouncycastle::core::key_material::{ - KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; +use bouncycastle::core::hazmat::do_hazardous_operations; +use bouncycastle::core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle::hex; use bouncycastle::hkdf; -use bouncycastle::sha2::hkdf::{HKDF_SHA256, HKDF_SHA512}; +use bouncycastle::sha2::hkdf::{HKDF_SHA256, HKDF_SHA384, HKDF_SHA512}; pub(crate) fn hkdf_cmd( hkdfname: &str, @@ -20,28 +18,18 @@ pub(crate) fn hkdf_cmd( len: usize, output_hex: bool, ) { - let salt_bytes: Vec; - let ikm_bytes: Vec; - let additional_input_bytes: Vec; - let mut out_key = KeyMaterial::<{ hkdf::MAX_HMAC_OUTPUT_LEN }>::new(); - - if len > 1024 { - eprintln!("Error: The CLI only supports output lengths up to 128 bytes (1024 bits)."); - exit(-1); - } - - // load the values - - salt_bytes = if salt.is_some() { - hex::decode(salt.as_ref().unwrap()).unwrap() - } else if salt_file.is_some() { - fs::read(salt_file.as_ref().unwrap()).unwrap() + // Each value may come from hex on the command line or from a binary file; the file wins if + // both are given, as the subcommands' help says. + let salt_bytes = if let Some(file) = salt_file { + fs::read(file).unwrap() + } else if let Some(hex_str) = salt { + hex::decode(hex_str).unwrap() } else { eprintln!("Error: either `salt` or `salt-file` must be supplied."); exit(-1) }; if salt_bytes.len() > 128 { - eprintln!("Error: The CLI only supports HKDF salts up to 128 bytes (1024 bytes)."); + eprintln!("Error: The CLI only supports HKDF salts up to 128 bytes (1024 bits)."); exit(-1); } let mut salt_key = KeyMaterial::<1024>::from_bytes(&salt_bytes).unwrap(); @@ -49,51 +37,58 @@ pub(crate) fn hkdf_cmd( do_hazardous_operations(&mut salt_key, |salt_key| salt_key.set_key_type(KeyType::MACKey)) .unwrap(); - ikm_bytes = if ikm.is_some() { - hex::decode(ikm.as_ref().unwrap()).unwrap() - } else if ikm_file.is_some() { - fs::read(ikm_file.as_ref().unwrap()).unwrap() + let ikm_bytes = if let Some(file) = ikm_file { + fs::read(file).unwrap() + } else if let Some(hex_str) = ikm { + hex::decode(hex_str).unwrap() } else { eprintln!("Error: either `ikm` or `ikm_file` must be supplied."); exit(-1) }; - additional_input_bytes = if additional_input.is_some() { - hex::decode(additional_input.as_ref().unwrap()).unwrap() - } else if additional_input.is_some() { - fs::read(additional_input_file.as_ref().unwrap()).unwrap() + let info = if let Some(file) = additional_input_file { + fs::read(file).unwrap() + } else if let Some(hex_str) = additional_input { + hex::decode(hex_str).unwrap() } else { eprintln!("Error: either `additional_input` or `additional_input_file` must be supplied."); exit(-1) }; - // Do the HKDF - - match hkdfname { + // RFC 5869: PRK = HKDF-Extract(salt, IKM), then OKM = HKDF-Expand(PRK, info, L). The IKM is + // streamed into the extract phase, so its length is not bounded by a KeyMaterial buffer, and the + // additional input is the expand phase's `info`. The library refuses an L above 255 * HashLen. + let mut out_key = KeyMaterial::<{ 255 * hkdf::MAX_HMAC_OUTPUT_LEN }>::new(); + let result = match hkdfname { "HKDF-SHA256" => { let mut h = HKDF_SHA256::new(); h.do_extract_init(&salt_key).unwrap(); - h.do_extract_update_bytes(ikm_bytes.as_slice()).unwrap(); - h.do_extract_update_bytes(additional_input_bytes.as_slice()).unwrap(); - h.do_extract_final_out(&mut out_key).unwrap(); + h.do_extract_update_bytes(&ikm_bytes).unwrap(); + let prk = h.do_extract_final().unwrap(); + HKDF_SHA256::expand_out(&prk, &info, len, &mut out_key) + } + "HKDF-SHA384" => { + let mut h = HKDF_SHA384::new(); + h.do_extract_init(&salt_key).unwrap(); + h.do_extract_update_bytes(&ikm_bytes).unwrap(); + let prk = h.do_extract_final().unwrap(); + HKDF_SHA384::expand_out(&prk, &info, len, &mut out_key) } "HKDF-SHA512" => { let mut h = HKDF_SHA512::new(); h.do_extract_init(&salt_key).unwrap(); - h.do_extract_update_bytes(ikm_bytes.as_slice()).unwrap(); - h.do_extract_update_bytes(additional_input_bytes.as_slice()).unwrap(); - h.do_extract_final_out(&mut out_key).unwrap(); + h.do_extract_update_bytes(&ikm_bytes).unwrap(); + let prk = h.do_extract_final().unwrap(); + HKDF_SHA512::expand_out(&prk, &info, len, &mut out_key) } _ => { panic!("{} is not a supported HKDF variant.", hkdfname); } + }; + if let Err(e) = result { + eprintln!("Error: {e:?}"); + exit(-1); } - if output_hex { - for b in out_key.ref_to_bytes().iter() { - print!("{b:02x}"); - } - } else { - io::stdout().write(&out_key.ref_to_bytes()).unwrap(); - } + crate::helpers::write_bytes_or_hex(out_key.ref_to_bytes(), output_hex); } diff --git a/cli/src/mac_cmd.rs b/cli/src/mac_cmd.rs index 8f095efc..562bc9b5 100644 --- a/cli/src/mac_cmd.rs +++ b/cli/src/mac_cmd.rs @@ -1,27 +1,26 @@ -use std::io::{Read, Write}; +use std::io::Read; use std::process::exit; use std::{fs, io}; -use bouncycastle::core::key_material::{ - KeyMaterial512, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; +use bouncycastle::core::hazmat::do_hazardous_operations; +use bouncycastle::core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; use bouncycastle::core::traits::MAC; use bouncycastle::hex; -use bouncycastle::sha2::hmac::{HMAC_SHA256, HMAC_SHA512}; +use bouncycastle::sha2::hmac::{HMAC_SHA256, HMAC_SHA512, HMAC_SHA512_224, HMAC_SHA512_256}; +use bouncycastle::sha3::kmac::{KMAC128, KMAC256}; +use bouncycastle::sm3::hmac::HMAC_SM3; +#[allow(non_camel_case_types)] pub(crate) enum HMACVariant { SHA256, SHA512, + SHA512_224, + SHA512_256, + SM3, } -pub(crate) fn mac_cmd( - hmac_variant: HMACVariant, - key: &Option, - 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() { @@ -37,6 +36,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 { @@ -48,6 +58,48 @@ pub(crate) fn mac_cmd( let mac = HMAC_SHA512::new_allow_weak_key(&key).unwrap(); do_mac(mac, verify_val, output_hex); } + HMACVariant::SHA512_224 => { + let mac = HMAC_SHA512_224::new_allow_weak_key(&key).unwrap(); + do_mac(mac, verify_val, output_hex); + } + HMACVariant::SHA512_256 => { + let mac = HMAC_SHA512_256::new_allow_weak_key(&key).unwrap(); + do_mac(mac, verify_val, output_hex); + } + HMACVariant::SM3 => { + let mac = HMAC_SM3::new_allow_weak_key(&key).unwrap(); + do_mac(mac, verify_val, output_hex); + } + } +} + +/// 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}"), } } @@ -64,14 +116,8 @@ fn do_mac(mut mac: impl MAC, verify_val: &Option, output_hex: bool) { // compute a MAC value let out = mac.do_final(); - if output_hex { - for b in out.iter() { - print!("{b:02x}"); - } - } else { - io::stdout().write(&out).unwrap(); - } - println!(); + crate::helpers::write_bytes_or_hex(&out, output_hex); + crate::helpers::write_stdout(b"\n"); } else { // verify a MAC if mac.do_verify_final(&hex::decode(verify_val.as_ref().unwrap()).unwrap()) { diff --git a/cli/src/main.rs b/cli/src/main.rs index c72af13a..e94caa78 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,11 @@ +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 encoders_cmd; mod helpers; mod hkdf_cmd; @@ -7,10 +15,13 @@ mod mlkem_cmd; mod rng_cmd; mod sha2_cmd; mod sha3_cmd; +mod sm3_cmd; use crate::mac_cmd::HMACVariant; use crate::mldsa_cmd::MLDSAAction; +use crate::sha2_cmd::SHA2Variant; use clap::{Parser, Subcommand}; +use helpers::block_mode_helpers::CipherDirection; #[derive(Parser)] #[command(version, about, long_about=None, arg_required_else_help=true)] @@ -22,11 +33,11 @@ struct Cli { #[allow(non_camel_case_types)] #[derive(Subcommand)] enum Subcommands { - /// Encode binary data from stdin to base64. + /// Encode binary data from stdin to hex. /// Supports streaming for low memory footprint and continuous processing from stdin to stdout. HexEncode, - /// Decode base64 data from stdin to binary. + /// Decode hex data from stdin to binary. /// Supports streaming for low memory footprint and continuous processing from stdin to stdout. HexDecode, @@ -70,6 +81,22 @@ enum Subcommands { x: bool, }, + /// Perform SHA512/224 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + SHA512_224 { + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform SHA512/256 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + SHA512_256 { + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + /// Perform SHA3-224 of the content provided on stdin. /// Supports streaming update for low memory footprint. SHA3_224 { @@ -94,177 +121,1224 @@ enum Subcommands { x: bool, }, - /// Perform SHA3-256 of the content provided on stdin. - /// Supports streaming update for low memory footprint. - SHA3_512 { + /// Perform SHA3-256 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + SHA3_512 { + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform SM3 of the content provided on stdin. + /// Supports streaming update for low memory footprint. + SM3 { + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform SHAKE128 of the content provided on stdin. Requires the output length in bytes. + /// Supports streaming update for low memory footprint. + SHAKE128 { + /// Length of the output in bytes. + length: usize, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform SHAKE256 of the content provided on stdin. Requires the output length in bytes. + /// Supports streaming update for low memory footprint. + SHAKE256 { + /// Length of the output in bytes. + length: usize, + + #[arg(short)] + /// Output the hashes in hex format. + 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 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, + + #[arg(short, long)] + direction: CipherDirection, + + #[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 + /// logged in shell history. Use the file-based input instead. + HMAC_SHA256 { + /// The MAC 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 MAC key in binary. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// The command will output either 0 for success or -1 for verification failure. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform HMAC-SHA512 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 + /// logged in shell history. Use the file-based input instead. + HMAC_SHA512 { + /// The MAC 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 MAC key in binary. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// The command will output either 0 for success or -1 for verification failure. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform HMAC-SHA512/224 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 + /// logged in shell history. Use the file-based input instead. + HMAC_SHA512_224 { + /// The MAC 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 MAC key in binary. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// The command will output either 0 for success or -1 for verification failure. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform HMAC-SHA512/256 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 + /// logged in shell history. Use the file-based input instead. + HMAC_SHA512_256 { + /// The MAC 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 MAC key in binary. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// The command will output either 0 for success or -1 for verification failure. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + /// Perform HMAC-SM3 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 + /// logged in shell history. Use the file-based input instead. + HMAC_SM3 { + /// The MAC 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 MAC key in binary. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + /// A MAC value to be verified. + /// The command will output either 0 for success or -1 for verification failure. + #[arg(short, long)] + verify: Option, + + #[arg(short)] + /// Output the hashes in hex format. + x: bool, + }, + + /// Perform HKDF-SHA256 of the provided input keying material. + /// HKDF.extract_and_expand(salt, ikm, additional_info, L) + /// 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. + HKDF_SHA256 { + /// The salt value in hex. + /// The `salt_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + salt: Option, + + /// A file containing the salt value in binary. + /// If both salt and salt_file options are provided, the file will be used. + #[arg(short, long)] + salt_file: Option, + + /// An Input Keying Material in hex. + /// The `ikm_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + ikm: Option, + + /// A file containing the input keying material in binary. + /// If both ikm and ikm_file options are provided, the file will be used. + #[arg(short, long)] + ikm_file: Option, + + /// Additional input data in hex. + #[arg(long)] + additional_input: Option, + + /// A file containing the additional input data in binary. + /// If both additional_input and additional_input_file options are provided, the file will be used. + #[arg(short, long)] + additional_input_file: Option, + + /// Length of output to produce, in bytes: at most 255 times the hash's output length. + #[arg(short, long)] + len: usize, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform HKDF-SHA384 of the provided input keying material. + /// HKDF.extract_and_expand(salt, ikm, additional_info, L) + /// 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. + HKDF_SHA384 { + /// The salt value in hex. + /// The `salt_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + salt: Option, + + /// A file containing the salt value in binary. + /// If both salt and salt_file options are provided, the file will be used. + #[arg(short, long)] + salt_file: Option, + + /// An Input Keying Material in hex. + /// The `ikm_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + ikm: Option, + + /// A file containing the input keying material in binary. + /// If both ikm and ikm_file options are provided, the file will be used. + #[arg(short, long)] + ikm_file: Option, + + /// Additional input data in hex. + #[arg(long)] + additional_input: Option, + + /// A file containing the additional input data in binary. + /// If both additional_input and additional_input_file options are provided, the file will be used. + #[arg(short, long)] + additional_input_file: Option, + + /// Length of output to produce, in bytes: at most 255 times the hash's output length. + #[arg(short, long)] + len: usize, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Perform HKDF-SHA512 of the provided input keying material. + /// HKDF.extract_and_expand(salt, ikm, additional_info, L) + /// 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. + HKDF_SHA512 { + /// The salt value in hex. + /// The `salt_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + salt: Option, + + /// A file containing the salt value in binary. + /// If both salt and salt_file options are provided, the file will be used. + #[arg(short, long)] + salt_file: Option, + + /// An Input Keying Material in hex. + /// The `ikm_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + ikm: Option, + + /// A file containing the input keying material in binary. + /// If both ikm and ikm_file options are provided, the file will be used. + #[arg(short, long)] + ikm_file: Option, + + /// Additional input data in hex. + #[arg(long)] + additional_input: Option, + + /// A file containing the additional input data in binary. + /// If both additional_input and additional_input_file options are provided, the file will be used. + #[arg(short, long)] + additional_input_file: Option, + + /// Length of output to produce, in bytes: at most 255 times the hash's output length. + #[arg(short, long)] + len: usize, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// Generate cryptographically-secure random bytes, seeded from the operating system's entropy source (/dev/random or equivalent). + /// Uses the library's default 256-bit secure RNG algorithm. + RNG { + /// Number of bytes to generate. If omitted, it will stream continuously until the process is terminated. + #[arg(short, long)] + len: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-128 in CBC mode (NIST SP 800-38A Sec 6.2), streaming stdin to stdout. + /// + /// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 16 BYTES of + /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two + /// compose directly in a pipeline. There is deliberately no `--iv` flag. + /// + /// Input must be a whole number of 16-byte blocks: CBC is defined only on whole blocks and + /// these commands apply no padding, so unaligned input is rejected rather than padded. + /// + /// WARNING: CBC provides confidentiality only. It does not detect tampering, and neither the + /// ciphertext nor the IV is authenticated. Do not decrypt data you have not authenticated + /// separately. + /// + /// 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_CBC { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-192 in CBC mode (NIST SP 800-38A Sec 6.2), streaming stdin to stdout. + /// + /// See `aes128-cbc` for the IV convention, block-alignment requirement and warnings; only the + /// key length differs. + AES192_CBC { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-256 in CBC mode (NIST SP 800-38A Sec 6.2), streaming stdin to stdout. + /// + /// See `aes128-cbc` for the IV convention, block-alignment requirement and warnings; only the + /// key length differs. + AES256_CBC { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-128 in CFB128 mode (NIST SP 800-38A Sec 6.3), streaming stdin to stdout. + /// + /// The segment size is the full block, i.e. CFB128. SP 800-38A's 8-bit CFB is a different, + /// non-interoperable mode; use `aes128-cfb8` for that. The 1-bit variant is not provided. + /// + /// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 16 BYTES of + /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two + /// compose directly in a pipeline. There is deliberately no `--iv` flag. + /// + /// Input may be ANY length: CFB is a stream cipher, so nothing is padded and the ciphertext is + /// exactly as long as the plaintext. + /// + /// WARNING: CFB provides confidentiality only. It does not detect tampering, and neither the + /// ciphertext nor the IV is authenticated. Flipping a ciphertext bit flips the same bit of the + /// plaintext in the same block, so tampering is directly exploitable. Do not decrypt data you + /// have not authenticated separately. + /// + /// 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_CFB { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-192 in CFB128 mode (NIST SP 800-38A Sec 6.3), streaming stdin to stdout. + /// + /// See `aes128-cfb` for the IV convention, input-length rule and warnings; only the key length + /// differs. + AES192_CFB { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-256 in CFB128 mode (NIST SP 800-38A Sec 6.3), streaming stdin to stdout. + /// + /// See `aes128-cfb` for the IV convention, input-length rule and warnings; only the key length + /// differs. + AES256_CFB { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-128 in CFB8 mode (NIST SP 800-38A Sec 6.3, s = 8), streaming stdin to stdout. + /// + /// The segment size is one byte. This is a DIFFERENT, NON-INTEROPERABLE mode from the CFB128 + /// of `aes128-cfb`: the two ciphertexts agree only on their first byte. It also costs one AES + /// call per byte, sixteen times the work of `aes128-cfb`, so prefer that unless a byte-granular + /// self-synchronising stream is required or the format demands CFB8. + /// + /// On `encrypt`, a fresh unpredictable IV is generated and written as the FIRST 16 BYTES of + /// the output; on `decrypt` it is read back from the first 16 bytes of the input, so the two + /// compose directly in a pipeline. There is deliberately no `--iv` flag. + /// + /// Input may be ANY length: CFB8's segment is a single byte, so nothing is padded and the + /// ciphertext is exactly as long as the plaintext. + /// + /// WARNING: CFB8 provides confidentiality only. It does not detect tampering, and neither the + /// ciphertext nor the IV is authenticated. Flipping a ciphertext bit flips the same bit of the + /// same plaintext byte and corrupts the following 16 bytes, after which decryption + /// resynchronises. Do not decrypt data you have not authenticated separately. + /// + /// 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_CFB8 { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-192 in CFB8 mode (NIST SP 800-38A Sec 6.3, s = 8), streaming stdin to stdout. + /// + /// See `aes128-cfb8` for the IV convention, input-length rule and warnings; only the key length + /// differs. + AES192_CFB8 { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-256 in CFB8 mode (NIST SP 800-38A Sec 6.3, s = 8), streaming stdin to stdout. + /// + /// See `aes128-cfb8` for the IV convention, input-length rule and warnings; only the key length + /// differs. + AES256_CFB8 { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-128 in CTR mode (NIST SP 800-38A Sec 6.5), streaming stdin to stdout. + /// + /// The counter block is a 12-byte nonce followed by a 4-byte counter starting at zero, so one + /// message can be up to 2^32 blocks (64 GiB); past that the command errors rather than + /// repeating keystream. + /// + /// On `encrypt`, a fresh nonce is generated and written as the FIRST 12 BYTES of the output; + /// on `decrypt` it is read back from the first 12 bytes of the input, so the two compose + /// directly in a pipeline. Note that this is 12 bytes, not the 16 the other modes write. There + /// is deliberately no `--iv` flag. + /// + /// Input may be ANY length: CTR is a stream cipher, so nothing is padded and the ciphertext is + /// exactly as long as the plaintext. + /// + /// WARNING: CTR provides confidentiality only and is the most malleable mode here. It does not + /// detect tampering, and flipping any ciphertext bit flips exactly the corresponding plaintext + /// bit and nothing else, so an attacker can edit the plaintext at will with no garbling to give + /// it away. A repeated nonce under one key leaks the XOR of the two messages outright. Do not + /// decrypt data you have not authenticated separately. + /// + /// 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_CTR { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-192 in CTR mode (NIST SP 800-38A Sec 6.5), streaming stdin to stdout. + /// + /// See `aes128-ctr` for the nonce convention, input-length rule and warnings; only the key + /// length differs. + AES192_CTR { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-256 in CTR mode (NIST SP 800-38A Sec 6.5), streaming stdin to stdout. + /// + /// See `aes128-ctr` for the nonce convention, input-length rule and warnings; only the key + /// length differs. + AES256_CTR { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + #[arg(short)] - /// Output the hashes in hex format. + /// Output in hex format. x: bool, }, - /// Perform SHAKE128 of the content provided on stdin. Requires the output length in bytes. - /// Supports streaming update for low memory footprint. - SHAKE128 { - /// Length of the output in bytes. - length: usize, + /// 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. The AAD is the exception: + /// `--aad-file` is streamed, because a file's size can be declared before it is read. + /// + /// 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 { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + /// A file containing the associated data, as raw bytes (never hex-decoded). A regular file + /// is streamed rather than loaded. If both aad and aad_file options are provided, the file + /// will be used. + #[arg(long)] + aad_file: 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 the hashes in hex format. + /// Output in hex format. x: bool, }, - /// Perform SHAKE256 of the content provided on stdin. Requires the output length in bytes. - /// Supports streaming update for low memory footprint. - SHAKE256 { - /// Length of the output in bytes. - length: usize, + /// 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 { + #[arg(short, long)] + direction: CipherDirection, + + /// 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, + + /// A file containing the associated data, as raw bytes (never hex-decoded). A regular file + /// is streamed rather than loaded. If both aad and aad_file options are provided, the file + /// will be used. + #[arg(long)] + aad_file: 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 the hashes in hex format. + /// 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 - /// logged in shell history. Use the file-based input instead. - HMAC_SHA256 { - /// The MAC key in hex. + /// 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 { + #[arg(short, long)] + direction: CipherDirection, + + /// 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 MAC key in binary. + /// 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, - /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. - #[arg(short, long)] - verify: 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, + + /// A file containing the associated data, as raw bytes (never hex-decoded). A regular file + /// is streamed rather than loaded. If both aad and aad_file options are provided, the file + /// will be used. + #[arg(long)] + aad_file: 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 the hashes in hex format. + /// Output in hex format. x: bool, }, - /// Perform HMAC-SHA512 of the content provided on stdin. - /// Supports streaming update for low memory footprint. + /// 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. - HMAC_SHA512 { - /// The MAC key in hex. + AES128_GCM { + #[arg(short, long)] + direction: CipherDirection, + + /// 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 MAC key in binary. + /// 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, - /// A MAC value to be verified. - /// The command will output either 0 for success or -1 for verification failure. - #[arg(short, long)] - verify: 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 the hashes in hex format. + /// Output in hex format. x: bool, }, - /// Perform HMAC-SHA256 of the content provided on stdin. - /// HKDF.extract_and_expand(salt, ikm, additional_info, L) - /// 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. - HKDF_SHA256 { - /// The salt value in hex. - /// The `salt_file` option is preferred to avoid leaving key material in command history. + /// 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 { + #[arg(short, long)] + direction: CipherDirection, + + /// The 24-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] - salt: Option, + key: Option, - /// A file containing the salt value in binary. - /// If both salt and salt_file options are provided, the file will be used. + /// 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)] - salt_file: Option, + key_file: Option, - /// An Input Keying Material in hex. - /// The `ikm_file` option is preferred to avoid leaving key material in command history. + /// The additional authenticated data, in hex. Covered by the tag but not encrypted. #[arg(long)] - ikm: Option, + aad: Option, - /// A file containing the salt value in binary. - /// If both ikm and ikm_file options are provided, the file will be used. + /// 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 { #[arg(short, long)] - ikm_file: Option, + direction: CipherDirection, - /// Additional input data in hex. + /// The 32-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] - additional_input: Option, + key: Option, - /// A file containing the additional input data in binary. - /// If both additional_input and additional_input_file options are provided, the file will be used. + /// 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)] - additional_input_file: Option, + key_file: Option, - /// Length of output to produce, in bytes. - #[arg(short, long)] - len: usize, + /// 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, }, - /// Perform HMAC-SHA512 of the content provided on stdin. - /// HKDF.extract_and_expand(salt, ikm, additional_info, L) + /// 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 + /// block maps to the same ciphertext block, so equal blocks stay visibly equal, the same input + /// always gives the same output, and blocks can be reordered, repeated or removed undetectably. + /// This command exists for interoperability with systems that require ECB and for test + /// vectors. For data use aes*-cbc or aes*-cfb under separate authentication, or an AEAD. + /// + /// There is NO IV: nothing is prepended on `encrypt` and nothing is consumed on `decrypt`, so + /// the output is exactly as long as the input. + /// + /// Input must be a whole number of 16-byte blocks: this command is block-aligned and applies + /// no padding, so unaligned input is rejected rather than padded. + /// /// 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. - HKDF_SHA512 { - /// The salt value in hex. - /// The `salt_file` option is preferred to avoid leaving key material in command history. - #[arg(long)] - salt: Option, - - /// A file containing the salt value in binary. - /// If both salt and salt_file options are provided, the file will be used. + AES128_ECB { #[arg(short, long)] - salt_file: Option, + direction: CipherDirection, - /// An Input Keying Material in hex. - /// The `ikm_file` option is preferred to avoid leaving key material in command history. + /// The 16-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. #[arg(long)] - ikm: Option, + key: Option, - /// A file containing the salt value in binary. - /// If both ikm and ikm_file options are provided, the file will be used. + /// 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)] - ikm_file: Option, + key_file: Option, - /// Additional input data in hex. - #[arg(long)] - additional_input: Option, + #[arg(short)] + /// Output in hex format. + x: bool, + }, - /// A file containing the additional input data in binary. - /// If both additional_input and additional_input_file options are provided, the file will be used. + /// AES-192 in ECB mode (NIST SP 800-38A Sec 6.1), streaming stdin to stdout. + /// + /// See `aes128-ecb` for the warning, the absence of an IV and the block-alignment requirement; + /// only the key length differs. + AES192_ECB { #[arg(short, long)] - additional_input_file: Option, + direction: CipherDirection, + + /// 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, - /// Length of output to produce, in bytes. + /// 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)] - len: usize, + key_file: Option, #[arg(short)] /// Output in hex format. x: bool, }, - /// Generate cryptographically-secure random bytes, seeded from the operating system's entropy source (/dev/random or equivalent). - /// Uses the library's default 256-bit secure RNG algorithm. - RNG { - /// Number of bytes to generate. If omitted, it will stream continuously until the process is terminated. + /// AES-256 in ECB mode (NIST SP 800-38A Sec 6.1), streaming stdin to stdout. + /// + /// See `aes128-ecb` for the warning, the absence of an IV and the block-alignment requirement; + /// only the key length differs. + AES256_ECB { #[arg(short, long)] - len: Option, + direction: CipherDirection, + + /// 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, #[arg(short)] /// Output in hex format. @@ -486,6 +1560,16 @@ enum Subcommands { } 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 { @@ -502,16 +1586,22 @@ fn main() { encoders_cmd::base64_decode_cmd(); } Some(Subcommands::SHA224 { x }) => { - sha2_cmd::sha2_cmd(224, *x); + sha2_cmd::sha2_cmd(SHA2Variant::SHA224, *x); } Some(Subcommands::SHA256 { x }) => { - sha2_cmd::sha2_cmd(256, *x); + sha2_cmd::sha2_cmd(SHA2Variant::SHA256, *x); } Some(Subcommands::SHA384 { x }) => { - sha2_cmd::sha2_cmd(384, *x); + sha2_cmd::sha2_cmd(SHA2Variant::SHA384, *x); } Some(Subcommands::SHA512 { x }) => { - sha2_cmd::sha2_cmd(512, *x); + sha2_cmd::sha2_cmd(SHA2Variant::SHA512, *x); + } + Some(Subcommands::SHA512_224 { x }) => { + sha2_cmd::sha2_cmd(SHA2Variant::SHA512_224, *x); + } + Some(Subcommands::SHA512_256 { x }) => { + sha2_cmd::sha2_cmd(SHA2Variant::SHA512_256, *x); } Some(Subcommands::SHA3_224 { x }) => { sha3_cmd::sha3_cmd(224, *x); @@ -525,18 +1615,61 @@ fn main() { Some(Subcommands::SHA3_512 { x }) => { sha3_cmd::sha3_cmd(512, *x); } + Some(Subcommands::SM3 { x }) => { + sm3_cmd::sm3_cmd(*x); + } Some(Subcommands::SHAKE128 { length, x }) => { sha3_cmd::shake_cmd(128, *length, *x); } Some(Subcommands::SHAKE256 { length, x }) => { sha3_cmd::shake_cmd(256, *length, *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::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, direction, x }) => { + let decrypt = matches!(direction, CipherDirection::Decrypt); + 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) } Some(Subcommands::HMAC_SHA512 { key, key_file, verify, x }) => { mac_cmd::mac_cmd(HMACVariant::SHA512, key, key_file, verify, *x) } + Some(Subcommands::HMAC_SHA512_224 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA512_224, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SHA512_256 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SHA512_256, key, key_file, verify, *x) + } + Some(Subcommands::HMAC_SM3 { key, key_file, verify, x }) => { + mac_cmd::mac_cmd(HMACVariant::SM3, key, key_file, verify, *x) + } Some(Subcommands::HKDF_SHA256 { salt, salt_file, @@ -550,6 +1683,19 @@ fn main() { "HKDF-SHA256", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, *len, *x, ), + Some(Subcommands::HKDF_SHA384 { + salt, + salt_file, + ikm, + ikm_file, + additional_input, + additional_input_file, + len, + x, + }) => hkdf_cmd::hkdf_cmd( + "HKDF-SHA384", salt, salt_file, ikm, ikm_file, additional_input, additional_input_file, + *len, *x, + ), Some(Subcommands::HKDF_SHA512 { salt, salt_file, @@ -564,6 +1710,105 @@ fn main() { *len, *x, ), Some(Subcommands::RNG { len, x }) => rng_cmd::rng_cmd(*len, *x), + Some(Subcommands::AES128_CBC { direction, key, key_file, x }) => { + aes_cbc_cmd::aes128_cbc_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES192_CBC { direction, key, key_file, x }) => { + aes_cbc_cmd::aes192_cbc_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES256_CBC { direction, key, key_file, x }) => { + aes_cbc_cmd::aes256_cbc_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES128_CFB { direction, key, key_file, x }) => { + aes_cfb_cmd::aes128_cfb_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES192_CFB { direction, key, key_file, x }) => { + aes_cfb_cmd::aes192_cfb_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES256_CFB { direction, key, key_file, x }) => { + aes_cfb_cmd::aes256_cfb_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES128_CFB8 { direction, key, key_file, x }) => { + aes_cfb8_cmd::aes128_cfb8_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES192_CFB8 { direction, key, key_file, x }) => { + aes_cfb8_cmd::aes192_cfb8_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES256_CFB8 { direction, key, key_file, x }) => { + aes_cfb8_cmd::aes256_cfb8_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES128_CTR { direction, key, key_file, x }) => { + aes_ctr_cmd::aes128_ctr_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES192_CTR { direction, key, key_file, x }) => { + aes_ctr_cmd::aes192_ctr_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES256_CTR { direction, key, key_file, x }) => { + aes_ctr_cmd::aes256_ctr_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES128_CCM { + direction, + key, + key_file, + nonce, + nonce_file, + aad, + aad_file, + tag_len, + x, + }) => { + aes_ccm_cmd::aes128_ccm_cmd( + direction, key, key_file, nonce, nonce_file, aad, aad_file, *tag_len, *x, + ); + } + Some(Subcommands::AES192_CCM { + direction, + key, + key_file, + nonce, + nonce_file, + aad, + aad_file, + tag_len, + x, + }) => { + aes_ccm_cmd::aes192_ccm_cmd( + direction, key, key_file, nonce, nonce_file, aad, aad_file, *tag_len, *x, + ); + } + Some(Subcommands::AES256_CCM { + direction, + key, + key_file, + nonce, + nonce_file, + aad, + aad_file, + tag_len, + x, + }) => { + aes_ccm_cmd::aes256_ccm_cmd( + direction, key, key_file, nonce, nonce_file, aad, aad_file, *tag_len, *x, + ); + } + Some(Subcommands::AES128_GCM { direction, key, key_file, aad, aad_file, x }) => { + aes_gcm_cmd::aes128_gcm_cmd(direction, key, key_file, aad, aad_file, *x); + } + Some(Subcommands::AES192_GCM { direction, key, key_file, aad, aad_file, x }) => { + aes_gcm_cmd::aes192_gcm_cmd(direction, key, key_file, aad, aad_file, *x); + } + Some(Subcommands::AES256_GCM { direction, key, key_file, aad, aad_file, x }) => { + aes_gcm_cmd::aes256_gcm_cmd(direction, key, key_file, aad, aad_file, *x); + } + Some(Subcommands::AES128_ECB { direction, key, key_file, x }) => { + aes_ecb_cmd::aes128_ecb_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES192_ECB { direction, key, key_file, x }) => { + aes_ecb_cmd::aes192_ecb_cmd(direction, key, key_file, *x); + } + Some(Subcommands::AES256_ECB { direction, key, key_file, x }) => { + aes_ecb_cmd::aes256_ecb_cmd(direction, key, key_file, *x); + } Some(Subcommands::MLKEM512 { action, skfile, pkfile, ctfile, x }) => { mlkem_cmd::mlkem512_cmd(action, skfile, pkfile, ctfile, *x); } diff --git a/cli/src/mldsa_cmd.rs b/cli/src/mldsa_cmd.rs index 070af7ea..e29db80f 100644 --- a/cli/src/mldsa_cmd.rs +++ b/cli/src/mldsa_cmd.rs @@ -105,7 +105,7 @@ pub(crate) fn mldsa44_cmd( match MLDSA44::keypair_consistency_check(&pk, &sk) { Ok(_) => { - println!("SUCCESS: pk and sk match."); + crate::helpers::println_stdout("SUCCESS: pk and sk match."); } Err(_) => { eprintln!("FAILURE: pk and sk do not match."); @@ -137,17 +137,17 @@ pub(crate) fn mldsa44_cmd( exit(-1); } }; - let mut signer = MLDSA44::sign_init(&sk, Some(&ctx)).unwrap(); + let mut signer = MLDSA44::do_sign_init(&sk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); } - let sig = signer.sign_final().unwrap(); + let sig = signer.do_sign_final().unwrap(); write_bytes_or_hex(&sig, output_hex); } @@ -183,20 +183,20 @@ pub(crate) fn mldsa44_cmd( }; // and now verify, streaming the message from stdin - let mut verifier = MLDSA44::verify_init(&pk, Some(&ctx)).unwrap(); + let mut verifier = MLDSA44::do_verify_init(&pk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); } - let sig = verifier.verify_final(&sig); + let sig = verifier.do_verify_final(&sig); if sig.is_ok() { - println!("Signature is valid."); + crate::helpers::println_stdout("Signature is valid."); } else { eprintln!("Signature is invalid."); exit(-1); @@ -264,7 +264,7 @@ pub(crate) fn mldsa65_cmd( match MLDSA65::keypair_consistency_check(&pk, &sk) { Ok(_) => { - println!("SUCCESS: pk and sk match."); + crate::helpers::println_stdout("SUCCESS: pk and sk match."); } Err(_) => { eprintln!("FAILURE: pk and sk do not match."); @@ -296,17 +296,17 @@ pub(crate) fn mldsa65_cmd( exit(-1); } }; - let mut signer = MLDSA65::sign_init(&sk, Some(&ctx)).unwrap(); + let mut signer = MLDSA65::do_sign_init(&sk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); } - let sig = signer.sign_final().unwrap(); + let sig = signer.do_sign_final().unwrap(); write_bytes_or_hex(&sig, output_hex); } @@ -342,20 +342,20 @@ pub(crate) fn mldsa65_cmd( }; // and now verify, streaming the message from stdin - let mut verifier = MLDSA65::verify_init(&pk, Some(&ctx)).unwrap(); + let mut verifier = MLDSA65::do_verify_init(&pk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); } - let sig = verifier.verify_final(&sig); + let sig = verifier.do_verify_final(&sig); if sig.is_ok() { - println!("Signature is valid."); + crate::helpers::println_stdout("Signature is valid."); } else { eprintln!("Signature is invalid."); exit(-1); @@ -422,7 +422,7 @@ pub(crate) fn mldsa87_cmd( match MLDSA87::keypair_consistency_check(&pk, &sk) { Ok(_) => { - println!("SUCCESS: pk and sk match."); + crate::helpers::println_stdout("SUCCESS: pk and sk match."); } Err(_) => { eprintln!("FAILURE: pk and sk do not match."); @@ -454,17 +454,17 @@ pub(crate) fn mldsa87_cmd( exit(-1); } }; - let mut signer = MLDSA87::sign_init(&sk, Some(&ctx)).unwrap(); + let mut signer = MLDSA87::do_sign_init(&sk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); } - let sig = signer.sign_final().unwrap(); + let sig = signer.do_sign_final().unwrap(); write_bytes_or_hex(&sig, output_hex); } @@ -500,20 +500,20 @@ pub(crate) fn mldsa87_cmd( }; // and now verify, streaming the message from stdin - let mut verifier = MLDSA87::verify_init(&pk, Some(&ctx)).unwrap(); + let mut verifier = MLDSA87::do_verify_init(&pk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); } - let sig = verifier.verify_final(&sig); + let sig = verifier.do_verify_final(&sig); if sig.is_ok() { - println!("Signature is valid."); + crate::helpers::println_stdout("Signature is valid."); } else { eprintln!("Signature is invalid."); exit(-1); @@ -581,7 +581,7 @@ pub(crate) fn hash_mldsa44_sha512_cmd( match MLDSA44::keypair_consistency_check(&pk, &sk) { Ok(_) => { - println!("SUCCESS: pk and sk match."); + crate::helpers::println_stdout("SUCCESS: pk and sk match."); } Err(_) => { eprintln!("FAILURE: pk and sk do not match."); @@ -613,17 +613,17 @@ pub(crate) fn hash_mldsa44_sha512_cmd( exit(-1); } }; - let mut signer = HashMLDSA44_with_SHA512::sign_init(&sk, Some(&ctx)).unwrap(); + let mut signer = HashMLDSA44_with_SHA512::do_sign_init(&sk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); } - let sig = signer.sign_final().unwrap(); + let sig = signer.do_sign_final().unwrap(); write_bytes_or_hex(&sig, output_hex); } @@ -659,20 +659,20 @@ pub(crate) fn hash_mldsa44_sha512_cmd( }; // and now verify, streaming the message from stdin - let mut verifier = HashMLDSA44_with_SHA512::verify_init(&pk, Some(&ctx)).unwrap(); + let mut verifier = HashMLDSA44_with_SHA512::do_verify_init(&pk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); } - let sig = verifier.verify_final(&sig); + let sig = verifier.do_verify_final(&sig); if sig.is_ok() { - println!("Signature is valid."); + crate::helpers::println_stdout("Signature is valid."); } else { eprintln!("Signature is invalid."); exit(-1); @@ -740,7 +740,7 @@ pub(crate) fn hash_mldsa65_sha512_cmd( match MLDSA65::keypair_consistency_check(&pk, &sk) { Ok(_) => { - println!("SUCCESS: pk and sk match."); + crate::helpers::println_stdout("SUCCESS: pk and sk match."); } Err(_) => { eprintln!("FAILURE: pk and sk do not match."); @@ -772,17 +772,17 @@ pub(crate) fn hash_mldsa65_sha512_cmd( exit(-1); } }; - let mut signer = HashMLDSA65_with_SHA512::sign_init(&sk, Some(&ctx)).unwrap(); + let mut signer = HashMLDSA65_with_SHA512::do_sign_init(&sk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); } - let sig = signer.sign_final().unwrap(); + let sig = signer.do_sign_final().unwrap(); write_bytes_or_hex(&sig, output_hex); } @@ -818,20 +818,20 @@ pub(crate) fn hash_mldsa65_sha512_cmd( }; // and now verify, streaming the message from stdin - let mut verifier = HashMLDSA65_with_SHA512::verify_init(&pk, Some(&ctx)).unwrap(); + let mut verifier = HashMLDSA65_with_SHA512::do_verify_init(&pk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); } - let sig = verifier.verify_final(&sig); + let sig = verifier.do_verify_final(&sig); if sig.is_ok() { - println!("Signature is valid."); + crate::helpers::println_stdout("Signature is valid."); } else { eprintln!("Signature is invalid."); exit(-1); @@ -898,7 +898,7 @@ pub(crate) fn hash_mldsa87_sha512_cmd( match MLDSA87::keypair_consistency_check(&pk, &sk) { Ok(_) => { - println!("SUCCESS: pk and sk match."); + crate::helpers::println_stdout("SUCCESS: pk and sk match."); } Err(_) => { eprintln!("FAILURE: pk and sk do not match."); @@ -930,17 +930,17 @@ pub(crate) fn hash_mldsa87_sha512_cmd( exit(-1); } }; - let mut signer = HashMLDSA87_with_SHA512::sign_init(&sk, Some(&ctx)).unwrap(); + let mut signer = HashMLDSA87_with_SHA512::do_sign_init(&sk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - signer.sign_update(&buf[..bytes_read]); + signer.do_sign_update(&buf[..bytes_read]); } - let sig = signer.sign_final().unwrap(); + let sig = signer.do_sign_final().unwrap(); write_bytes_or_hex(&sig, output_hex); } @@ -976,20 +976,20 @@ pub(crate) fn hash_mldsa87_sha512_cmd( }; // and now verify, streaming the message from stdin - let mut verifier = HashMLDSA87_with_SHA512::verify_init(&pk, Some(&ctx)).unwrap(); + let mut verifier = HashMLDSA87_with_SHA512::do_verify_init(&pk, Some(&ctx)).unwrap(); let mut buf = [0u8; 1024]; let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); while bytes_read != 0 { bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - verifier.verify_update(&buf[..bytes_read]); + verifier.do_verify_update(&buf[..bytes_read]); } - let sig = verifier.verify_final(&sig); + let sig = verifier.do_verify_final(&sig); if sig.is_ok() { - println!("Signature is valid."); + crate::helpers::println_stdout("Signature is valid."); } else { eprintln!("Signature is invalid."); exit(-1); diff --git a/cli/src/mlkem_cmd.rs b/cli/src/mlkem_cmd.rs index 67214da2..f46fa424 100644 --- a/cli/src/mlkem_cmd.rs +++ b/cli/src/mlkem_cmd.rs @@ -103,7 +103,7 @@ pub(crate) fn mlkem512_cmd( match MLKEM512::keypair_consistency_check(&pk, &sk) { Ok(_) => { - println!("SUCCESS: pk and sk match."); + crate::helpers::println_stdout("SUCCESS: pk and sk match."); } Err(_) => { eprintln!("FAILURE: pk and sk do not match."); @@ -131,7 +131,7 @@ pub(crate) fn mlkem512_cmd( } else { // write both to stdout in hex, separated by a newline. write_bytes_or_hex(&ct, true); - println!(); + crate::helpers::write_stdout(b"\n"); write_bytes_or_hex(ss.ref_to_bytes(), true); } } @@ -226,7 +226,7 @@ pub(crate) fn mlkem768_cmd( match MLKEM768::keypair_consistency_check(&pk, &sk) { Ok(_) => { - println!("SUCCESS: pk and sk match."); + crate::helpers::println_stdout("SUCCESS: pk and sk match."); } Err(_) => { eprintln!("FAILURE: pk and sk do not match."); @@ -254,7 +254,7 @@ pub(crate) fn mlkem768_cmd( } else { // write both to stdout in hex, separated by a newline. write_bytes_or_hex(&ct, true); - println!(); + crate::helpers::write_stdout(b"\n"); write_bytes_or_hex(ss.ref_to_bytes(), true); } } @@ -349,7 +349,7 @@ pub(crate) fn mlkem1024_cmd( match MLKEM1024::keypair_consistency_check(&pk, &sk) { Ok(_) => { - println!("SUCCESS: pk and sk match."); + crate::helpers::println_stdout("SUCCESS: pk and sk match."); } Err(_) => { eprintln!("FAILURE: pk and sk do not match."); @@ -377,7 +377,7 @@ pub(crate) fn mlkem1024_cmd( } else { // write both to stdout in hex, separated by a newline. write_bytes_or_hex(&ct, true); - println!(); + crate::helpers::write_stdout(b"\n"); write_bytes_or_hex(ss.ref_to_bytes(), true); } } diff --git a/cli/src/rng_cmd.rs b/cli/src/rng_cmd.rs index e7a28e51..a05d9d75 100644 --- a/cli/src/rng_cmd.rs +++ b/cli/src/rng_cmd.rs @@ -19,5 +19,9 @@ pub(crate) fn rng_cmd(len: Option, output_hex: bool) { write_bytes_or_hex(&buf, output_hex); bytes_left_to_write -= buf.len(); } - println!(); + // Only a hex line gets a terminator: raw output is exactly `len` bytes, so that + // `rng --len 16 > key.bin` is a 16-byte key and not a 17-byte one. + if output_hex { + crate::helpers::write_stdout(b"\n"); + } } diff --git a/cli/src/sha2_cmd.rs b/cli/src/sha2_cmd.rs index 3551c9d8..f2fb889a 100644 --- a/cli/src/sha2_cmd.rs +++ b/cli/src/sha2_cmd.rs @@ -1,16 +1,27 @@ use bouncycastle::core::traits::Hash; use std::io; -use std::io::{Read, Write}; +use std::io::Read; -use bouncycastle::sha2::{SHA224, SHA256, SHA384, SHA512}; +use bouncycastle::sha2::{SHA224, SHA256, SHA384, SHA512, SHA512_224, SHA512_256}; -pub(crate) fn sha2_cmd(bit_len: usize, output_hex: bool) { - match bit_len { - 224 => do_sha2(SHA224::new(), output_hex), - 256 => do_sha2(SHA256::new(), output_hex), - 384 => do_sha2(SHA384::new(), output_hex), - 512 => do_sha2(SHA512::new(), output_hex), - _ => panic!("Unsupported algorithm: SHA{}", bit_len), +#[allow(non_camel_case_types)] +pub(crate) enum SHA2Variant { + SHA224, + SHA256, + SHA384, + SHA512, + SHA512_224, + SHA512_256, +} + +pub(crate) fn sha2_cmd(variant: SHA2Variant, output_hex: bool) { + match variant { + SHA2Variant::SHA224 => do_sha2(SHA224::new(), output_hex), + SHA2Variant::SHA256 => do_sha2(SHA256::new(), output_hex), + SHA2Variant::SHA384 => do_sha2(SHA384::new(), output_hex), + SHA2Variant::SHA512 => do_sha2(SHA512::new(), output_hex), + SHA2Variant::SHA512_224 => do_sha2(SHA512_224::new(), output_hex), + SHA2Variant::SHA512_256 => do_sha2(SHA512_256::new(), output_hex), } } @@ -26,12 +37,6 @@ fn do_sha2(mut sha2: impl Hash, output_hex: bool) { let out = sha2.do_final(); - if output_hex { - for b in out.iter() { - print!("{b:02x}"); - } - } else { - io::stdout().write(&out).unwrap(); - } - println!(); + crate::helpers::write_bytes_or_hex(&out, output_hex); + crate::helpers::write_stdout(b"\n"); } diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index b6107e0c..691ac59e 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -1,65 +1,129 @@ -use bouncycastle::core::traits::{Hash, XOF}; +use bouncycastle::core::traits::Hash; use std::io; -use std::io::{Read, Write}; +use std::io::Read; +use bouncycastle::hex; +use bouncycastle::sha3::parallelhash::{ParallelHash128, ParallelHash256}; +use bouncycastle::sha3::tuplehash::{TupleHash128, TupleHash256}; use bouncycastle::sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; +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]; - - // 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"); +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), } +} - let out = sha3.do_final(); +/// 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(); - if output_hex { - for b in out.iter() { - print!("{b:02x}"); - } + // Either the tuple came from flags, or stdin is the single element. + let tuple: Vec> = if elements.is_empty() { + vec![read_stdin()] } else { - io::stdout().write(&out).unwrap(); - } - println!(); + 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); } -pub(crate) fn shake_cmd(bit_len: usize, output_len: usize, output_hex: bool) { +/// 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 => do_shake(SHAKE128::new(), output_len, output_hex), - 256 => do_shake(SHAKE256::new(), output_len, output_hex), - _ => panic!("Unsupported algorithm: SHAKE-{}", 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}"), } } -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"); - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); +/// 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]); } +} - let out = shake.squeeze(output_len); - if output_hex { - for b in out.iter() { - print!("{b:02x}"); +/// 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; } - } else { - io::stdout().write(&out).unwrap(); + sink(&buf[..n]); } - println!(); +} + +/// Writes the digest as raw bytes or hex, with the trailing newline the other commands emit. +fn write_out(out: &[u8], output_hex: bool) { + crate::helpers::write_bytes_or_hex(out, output_hex); + crate::helpers::write_stdout(b"\n"); } diff --git a/cli/src/sm3_cmd.rs b/cli/src/sm3_cmd.rs new file mode 100644 index 00000000..c7bc5acd --- /dev/null +++ b/cli/src/sm3_cmd.rs @@ -0,0 +1,22 @@ +use bouncycastle::core::traits::Hash; +use std::io; +use std::io::Read; + +use bouncycastle::sm3::SM3; + +pub(crate) fn sm3_cmd(output_hex: bool) { + let mut sm3 = SM3::new(); + 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 { + sm3.do_update(&buf[..bytes_read]); + bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + } + + let out = sm3.do_final(); + + crate::helpers::write_bytes_or_hex(&out, output_hex); + crate::helpers::write_stdout(b"\n"); +} diff --git a/cli/tests/lib.sh b/cli/tests/lib.sh new file mode 100644 index 00000000..c10d1f7b --- /dev/null +++ b/cli/tests/lib.sh @@ -0,0 +1,172 @@ +# Shared helpers for the bc-rust CLI shell tests. Sourced by each test_*.sh; not run directly. +# +# A test file defines functions named test_* and ends with `run_all`. Each test runs in its own +# subshell under `set -e`, so any failing command or assertion fails that test and no other, and +# every input it needs comes from the binary itself: keys and data from `bc-rust rng`, hex from +# `bc-rust hex-encode` / `hex-decode`. +# +# Scratch files live under /tmp/bc-rust-cli-tests/./, one subdirectory per +# test, which $TMP points at while the test runs. The whole directory is removed when every test +# in the file passed, and left in place -- with its path printed -- when any failed, so the files +# a failing test was working on can be inspected. +# +# The binary is target/debug/bc-rust, relative to the repository root; set BC_RUST to override. + +set -u + +TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$TESTS_DIR/../.." && pwd)" +BC_RUST="${BC_RUST:-$REPO_ROOT/target/debug/bc-rust}" + +if [ ! -x "$BC_RUST" ]; then + echo "bc-rust binary not found at $BC_RUST -- run \`cargo build -p cli\` first" >&2 + exit 2 +fi + +SCRATCH_ROOT=/tmp/bc-rust-cli-tests +mkdir -p "$SCRATCH_ROOT" +SCRATCH="$(mktemp -d "$SCRATCH_ROOT/$(basename "$0" .sh).XXXXXXXX")" + +PASS=0 +FAIL=0 + +# ---- running -------------------------------------------------------------------------------- + +# Runs one test function in a subshell, in its own scratch subdirectory, and records the result. +# A test's stderr is shown only when it fails. +run_test() { + local name=$1 + local dir="$SCRATCH/$name" + local rc + mkdir -p "$dir" + # The subshell must not be the condition of the `if`: bash ignores `set -e` everywhere inside + # an `if` condition, even when set within it, which would let every assertion but the last + # in a test pass silently. Take the status first, then test it. + ( set -e; TMP="$dir"; LAST_STDERR="$dir/.last_stderr"; "$name" ) 2>"$dir/.stderr" + rc=$? + if [ "$rc" -eq 0 ]; then + PASS=$((PASS + 1)) + echo "ok $name" + else + FAIL=$((FAIL + 1)) + echo "FAIL $name" + sed 's/^/ | /' "$dir/.stderr" + fi +} + +# Runs every function named test_* defined so far, then prints a summary. The scratch directory +# is removed only if everything passed. Exits non-zero if any test failed, which is what +# test_all.sh counts. +run_all() { + local t + for t in $(compgen -A function test_); do + run_test "$t" + done + echo "$(basename "$0"): $PASS passed, $FAIL failed" + if [ "$FAIL" -eq 0 ]; then + rm -rf "$SCRATCH" + return 0 + fi + echo "scratch files left in $SCRATCH" + return 1 +} + +# ---- inputs --------------------------------------------------------------------------------- + +# N random bytes on stdout, from the library's own RNG. +rng() { + "$BC_RUST" rng --len "$1" +} + +# The hex of a file, as one line with no newline. +hex() { + "$BC_RUST" hex-encode <"$1" +} + +# The bytes of a hex string on stdout. +unhex() { + printf '%s' "$1" | "$BC_RUST" hex-decode +} + +# ---- bytes ---------------------------------------------------------------------------------- + +# `keylen BITS` prints the key length in bytes: `keylen 128` is 16. +keylen() { + echo $(($1 / 8)) +} + +# `byte_at FILE OFFSET` prints the decimal value of the byte at OFFSET (0-based). +byte_at() { + od -An -tu1 -j "$2" -N 1 "$1" | tr -d ' ' +} + +# `slice FILE OFFSET LEN` prints LEN bytes of FILE starting at OFFSET (0-based). +slice() { + dd if="$1" bs=1 skip="$2" count="$3" status=none +} + +# `flip_byte FILE OFFSET [MASK]` XORs the byte at OFFSET with MASK, 0x01 by default, in place. +flip_byte() { + local file=$1 offset=$2 mask=${3:-1} byte + byte=$(byte_at "$file" "$offset") + printf "\\$(printf '%03o' $((byte ^ mask)))" | dd of="$file" bs=1 seek="$offset" conv=notrunc status=none +} + +# `flip_hex HEX INDEX MASK` prints HEX with byte INDEX XORed by MASK. +flip_hex() { + local hex=$1 idx=$2 mask=$3 byte + byte=$(printf '%02x' $((0x${hex:$((2 * idx)):2} ^ mask))) + printf '%s%s%s' "${hex:0:$((2 * idx))}" "$byte" "${hex:$((2 * idx + 2))}" +} + +# `hex_out CMD...` runs CMD and prints its hex output as one line: `-x` output ends in a newline, +# which is dropped here. +hex_out() { + "$@" | tr -d '\n' +} + +# ---- assertions ----------------------------------------------------------------------------- + +fail() { + echo "assertion failed: $*" >&2 + return 1 +} + +assert_same() { + cmp -s "$1" "$2" || fail "$3 (files differ: $1 vs $2)" +} + +assert_differs() { + ! cmp -s "$1" "$2" || fail "$3 (files are identical: $1 vs $2)" +} + +assert_size() { + local actual + actual=$(wc -c <"$1") + [ "$actual" -eq "$2" ] || fail "$3 (expected $2 bytes, got $actual)" +} + +assert_eq() { + [ "$1" = "$2" ] || fail "$3 (expected '$2', got '$1')" +} + +# Runs a command that must fail. Its stderr is saved for assert_stderr_has; its stdout is +# discarded. Inherits stdin, so `expect_fail "..." cmd /dev/null 2>"$LAST_STDERR"; then + fail "$msg (command succeeded: $*)" + fi +} + +# Runs a command that must succeed, saving its stderr for assert_stderr_has. +expect_ok() { + local msg=$1 + shift + "$@" 2>"$LAST_STDERR" || fail "$msg (command failed: $*; stderr: $(cat "$LAST_STDERR"))" +} + +assert_stderr_has() { + grep -q -- "$1" "$LAST_STDERR" || fail "stderr should mention '$1', got: $(cat "$LAST_STDERR")" +} diff --git a/cli/tests/test_aes_cbc.sh b/cli/tests/test_aes_cbc.sh new file mode 100755 index 00000000..17fe353d --- /dev/null +++ b/cli/tests/test_aes_cbc.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# The aes128-cbc / aes192-cbc / aes256-cbc subcommands, end to end through the binary. +# +# Framing: encrypt writes a fresh IV as the first 16 bytes of its output, decrypt reads it back +# from the first 16 bytes of its input, and neither applies padding, so input must be a whole +# number of 16-byte blocks. Keys and data come from `bc-rust rng`; the one fixed input is the +# SP 800-38A Appendix F.2 known-answer set. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# One subcommand per key length: `cbc 128` prints "aes128-cbc". +cbc() { echo "aes$1-cbc"; } + +# ---- round trips -------------------------------------------------------------------------- + +test_round_trip_through_files() { + local bits + for bits in 128 192 256; do + rng "$(keylen $bits)" >"$TMP/key" + rng 1024 >"$TMP/pt" + + "$BC_RUST" "$(cbc $bits)" -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((16 + 1024)) "$bits: ciphertext is the IV plus the plaintext length" + assert_differs "$TMP/pt" "$TMP/ct" "$bits: the data must actually be encrypted" + + "$BC_RUST" "$(cbc $bits)" -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$bits: decrypt must recover the plaintext" + done +} + +test_round_trip_through_a_pipe_larger_than_the_pipe_buffer() { + rng 16 >"$TMP/key" + rng $((1024 * 1024)) >"$TMP/pt" + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key" <"$TMP/pt" \ + | "$BC_RUST" aes128-cbc -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "1 MiB must survive encrypt | decrypt with no file in between" +} + +test_hex_output_composes_through_hex_decode() { + rng 16 >"$TMP/key" + rng 4096 >"$TMP/pt" + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key" -x <"$TMP/pt" >"$TMP/ct.hex" + assert_size "$TMP/ct.hex" $((2 * (16 + 4096) + 1)) "-x emits two hex characters per byte, then a newline" + "$BC_RUST" hex-decode <"$TMP/ct.hex" \ + | "$BC_RUST" aes128-cbc -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "-x output must decrypt after hex-decode" +} + +test_each_invocation_uses_a_fresh_iv() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct1" + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct2" + head -c 16 "$TMP/ct1" >"$TMP/iv1" + head -c 16 "$TMP/ct2" >"$TMP/iv2" + assert_differs "$TMP/iv1" "$TMP/iv2" "two encryptions must draw different IVs" + "$BC_RUST" aes128-cbc -d decrypt --key-file "$TMP/key" <"$TMP/ct2" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "the second ciphertext still decrypts" +} + +test_empty_input_produces_only_the_iv() { + rng 16 >"$TMP/key" + : >"$TMP/empty" + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key" <"$TMP/empty" >"$TMP/ct" + assert_size "$TMP/ct" 16 "an empty message encrypts to just the IV" + "$BC_RUST" aes128-cbc -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_size "$TMP/rec" 0 "and decrypts back to nothing" +} + +# ---- keys ----------------------------------------------------------------------------------- + +test_key_file_accepts_binary_hex_and_a_trailing_newline() { + rng 16 >"$TMP/key.bin" + hex "$TMP/key.bin" >"$TMP/key.hex" + { cat "$TMP/key.hex"; printf '\n'; } >"$TMP/key.hex.nl" + { cat "$TMP/key.bin"; printf '\n'; } >"$TMP/key.bin.nl" + rng 256 >"$TMP/pt" + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" + + local form + for form in key.hex key.hex.nl key.bin.nl; do + "$BC_RUST" aes128-cbc -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" + done +} + +test_key_on_the_command_line_matches_the_key_file() { + rng 16 >"$TMP/key" + rng 256 >"$TMP/pt" + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" aes128-cbc -d decrypt --key "$(hex "$TMP/key")" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "--key in hex must decrypt what --key-file encrypted" +} + +test_the_wrong_key_gives_the_wrong_plaintext() { + rng 16 >"$TMP/key" + rng 16 >"$TMP/other" + rng 256 >"$TMP/pt" + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + # CBC is unauthenticated: a wrong key succeeds and produces garbage, never the plaintext. + "$BC_RUST" aes128-cbc -d decrypt --key-file "$TMP/other" <"$TMP/ct" >"$TMP/rec" + assert_differs "$TMP/pt" "$TMP/rec" "a different key must not recover the plaintext" +} + +test_an_all_zero_key_warns_but_proceeds() { + head -c 16 /dev/zero >"$TMP/key" + rng 64 >"$TMP/pt" + expect_ok "an all-zero key is accepted" \ + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_stderr_has "arning" + assert_size "$TMP/ct" $((16 + 64)) "and the output is complete" +} + +# ---- known answers -------------------------------------------------------------------------- + +# SP 800-38A Appendix F.2.2, F.2.4 and F.2.6 (CBC decrypt at each key length), transcribed in +# the Rust suite this file replaces. The CLI takes the IV as the first 16 bytes of its input, so +# the input here is IV || ciphertext, and the output must be the appendix's four plaintext blocks. +F2_IV=000102030405060708090a0b0c0d0e0f +F2_PLAINTEXT=6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710 +F2_KEY_128=2b7e151628aed2a6abf7158809cf4f3c +F2_KEY_192=8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b +F2_KEY_256=603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4 +F2_CT_128=7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b273bed6b8e3c1743b7116e69e222295163ff1caa1681fac09120eca307586e1a7 +F2_CT_192=4f021db243bc633d7178183a9fa071e8b4d9ada9ad7dedf4e5e738763f69145a571b242012fb7ae07fa9baac3df102e008b0e27988598881d920a9e64f5615cd +F2_CT_256=f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d39f23369a9d9bacfa530e26304231461b2eb05e2c39be9fcda6c19078c6a9d1b + +test_decrypt_matches_sp800_38a_f2_vectors() { + local bits key ct got + for bits in 128 192 256; do + key="F2_KEY_$bits" + ct="F2_CT_$bits" + unhex "$F2_IV${!ct}" >"$TMP/ct" + got=$("$BC_RUST" "$(cbc $bits)" -d decrypt --key "${!key}" <"$TMP/ct" | "$BC_RUST" hex-encode) + assert_eq "$got" "$F2_PLAINTEXT" "$bits: F.2 decrypt vector" + done +} + +# ---- rejected inputs ------------------------------------------------------------------------ + +test_unaligned_input_is_rejected() { + rng 16 >"$TMP/key" + rng 1025 >"$TMP/pt" + expect_fail "1025 bytes is not a whole number of blocks" \ + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key" <"$TMP/pt" + assert_stderr_has "whole number of 16-byte blocks" + + rng $((16 + 1025)) >"$TMP/ct" + expect_fail "an unaligned body after the IV is rejected on decrypt too" \ + "$BC_RUST" aes128-cbc -d decrypt --key-file "$TMP/key" <"$TMP/ct" + assert_stderr_has "whole number of 16-byte blocks" +} + +test_decrypt_input_shorter_than_the_iv_is_rejected() { + rng 16 >"$TMP/key" + rng 8 >"$TMP/short" + expect_fail "8 bytes cannot hold a 16-byte IV" \ + "$BC_RUST" aes128-cbc -d decrypt --key-file "$TMP/key" <"$TMP/short" +} + +test_a_key_of_the_wrong_length_is_rejected() { + rng 15 >"$TMP/key" + rng 64 >"$TMP/pt" + expect_fail "a 15-byte key is not an AES-128 key" \ + "$BC_RUST" aes128-cbc -d encrypt --key-file "$TMP/key" <"$TMP/pt" + assert_stderr_has "16-byte key" +} + +test_a_missing_key_is_rejected() { + rng 64 >"$TMP/pt" + expect_fail "neither --key nor --key-file" \ + "$BC_RUST" aes128-cbc -d encrypt <"$TMP/pt" + assert_stderr_has "key" +} + +test_a_missing_direction_is_rejected() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + expect_fail "--direction is required" \ + "$BC_RUST" aes128-cbc --key-file "$TMP/key" <"$TMP/pt" + assert_stderr_has "direction" +} + +run_all diff --git a/cli/tests/test_aes_ccm.sh b/cli/tests/test_aes_ccm.sh new file mode 100755 index 00000000..cc4dcefc --- /dev/null +++ b/cli/tests/test_aes_ccm.sh @@ -0,0 +1,402 @@ +#!/usr/bin/env bash +# The aes128-ccm / aes192-ccm / aes256-ccm subcommands, end to end through the binary. +# +# Framing, and everything CCM does differently from the other modes: the nonce is a required flag +# (`--nonce` hex or `--nonce-file` raw bytes) and is NOT written to the output; the tag rides at the +# end of the ciphertext, `--tag-len` bytes of it, which must match on both sides; `--aad` / +# `--aad-file` is authenticated but not encrypted and must match; a failed tag check exits non-zero +# and writes nothing; nonce length and tag length are validated against SP 800-38C Appendix A.1, +# and the nonce length caps the payload. Keys and data come from `bc-rust rng`; the fixed inputs +# are SP 800-38C Appendix C.1 and C.4, copied from the Rust suite this file replaces. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +KEY_128=2b7e151628aed2a6abf7158809cf4f3c +KEY_192=8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b +KEY_256=603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4 +# A 12-byte nonce, the length these tests use unless they are about nonce length. +NONCE=000102030405060708090a0b + +# ---- local helpers -------------------------------------------------------------------------- + +# `nonce_of 13 5a` prints the hex of 13 bytes of 0x5a. +nonce_of() { + local n=$1 byte=$2 out="" i + for ((i = 0; i < n; i++)); do out="$out$byte"; done + printf '%s' "$out" +} + +# ---- known answers -------------------------------------------------------------------------- + +# SP 800-38C Appendix C.1: Klen = 128, Tlen = 32, Nlen = 56, Alen = 64, Plen = 32. The appendix's +# C is the 4-byte ciphertext followed by the 4-byte tag, exactly what this command writes. The +# appendix gives no decryption example but says one is "straightforward to construct". +test_encrypt_matches_sp800_38c_appendix_c1() { + local got + got=$(unhex 20212223 | "$BC_RUST" aes128-ccm -d encrypt --key 404142434445464748494a4b4c4d4e4f \ + --nonce 10111213141516 --aad 0001020304050607 --tag-len 4 | "$BC_RUST" hex-encode) + assert_eq "$got" "7162015b4dac255d" "Appendix C.1's C string" + got=$(unhex 7162015b4dac255d | "$BC_RUST" aes128-ccm -d decrypt --key 404142434445464748494a4b4c4d4e4f \ + --nonce 10111213141516 --aad 0001020304050607 --tag-len 4 | "$BC_RUST" hex-encode) + assert_eq "$got" "20212223" "Appendix C.1's P" +} + +# SP 800-38C Appendix C.4: the AAD is 65536 bytes -- the sixteen blocks `00 01 .. ff` repeated 256 +# times -- so `--aad-file` is streamed through the MAC in many chunks, and the length is past the +# 2^16 - 2^8 boundary where A.2.2's six-octet encoding applies. +test_aad_file_matches_sp800_38c_appendix_c4() { + local i got + for ((i = 0; i < 256; i++)); do printf "\\x$(printf '%02x' "$i")"; done >"$TMP/block" + for ((i = 0; i < 256; i++)); do cat "$TMP/block"; done >"$TMP/aad" + assert_size "$TMP/aad" 65536 "Alen = 524288 bits" + + unhex 202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f >"$TMP/pt" + "$BC_RUST" aes128-ccm -d encrypt --key 404142434445464748494a4b4c4d4e4f \ + --nonce 101112131415161718191a1b1c --aad-file "$TMP/aad" --tag-len 14 <"$TMP/pt" >"$TMP/ct" + got=$(hex "$TMP/ct") + assert_eq "$got" "69915dad1e84c6376a68c2967e4dab615ae0fd1faec44cc484828529463ccf72b4ac6bec93e8598e7f0dadbcea5b" \ + "Appendix C.4's C string" + "$BC_RUST" aes128-ccm -d decrypt --key 404142434445464748494a4b4c4d4e4f \ + --nonce 101112131415161718191a1b1c --aad-file "$TMP/aad" --tag-len 14 <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "Appendix C.4's P" +} + +# ---- round trips -------------------------------------------------------------------------- + +# Each key length, with AAD, over a payload that spans several blocks and does not end on a block +# boundary. +test_encrypt_then_decrypt_round_trips() { + local bits key + rng 201 >"$TMP/pt" + for bits in 128 192 256; do + key="KEY_$bits" + "$BC_RUST" "aes$bits-ccm" -d encrypt --key "${!key}" --nonce "$NONCE" --aad cafebabe <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((201 + 16)) "$bits: the default tag length is 16, and the nonce is not written" + "$BC_RUST" "aes$bits-ccm" -d decrypt --key "${!key}" --nonce "$NONCE" --aad cafebabe <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$bits: round trip" + done +} + +# 256 KiB, past the usual 64 KiB pipe buffer: the read-all-of-stdin loop must not deadlock against +# its own output. A 12-byte nonce gives q = 3, a 16 MiB limit, so this is well inside it. +test_a_payload_larger_than_the_pipe_buffer_round_trips() { + rng $((256 * 1024)) >"$TMP/pt" + "$BC_RUST" aes256-ccm -d encrypt --key "$KEY_256" --nonce "$NONCE" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((256 * 1024 + 16)) "payload plus tag" + "$BC_RUST" aes256-ccm -d decrypt --key "$KEY_256" --nonce "$NONCE" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "256 KiB round trip" +} + +# `-x` writes hex, and with a supplied nonce the output is deterministic, so it must be exactly the +# hex of what the binary form writes. +test_hex_output_matches_binary_output() { + local as_hex + rng 14 >"$TMP/pt" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/pt" >"$TMP/ct" + as_hex=$("$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" -x <"$TMP/pt") + assert_eq "$as_hex" "$(hex "$TMP/ct")" "-x must be the hex of the binary output" +} + +# A ciphertext from one key length must not decrypt under another, even with a right-length key, +# and the failure is the tag check rather than garbage. +test_the_three_variants_are_not_interchangeable() { + rng 15 >"$TMP/pt" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/pt" >"$TMP/ct" + expect_fail "aes256-ccm must reject an aes128-ccm ciphertext" \ + "$BC_RUST" aes256-ccm -d decrypt --key "$KEY_256" --nonce "$NONCE" <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +# ---- the nonce ------------------------------------------------------------------------------ + +# The nonce is not written to the output, so `decrypt` needs the same `--nonce`: the sharpest +# difference from the other five commands, all of which prepend their generated IV. +test_the_nonce_is_not_written_to_the_output_and_is_required_to_decrypt() { + rng 27 >"$TMP/pt" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((27 + 16)) "output is ciphertext + tag only; no nonce prefix" + # A different nonce must fail: it changes both B0 and every counter block. + expect_fail "a different nonce must fail the tag check" \ + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce 010102030405060708090a0b <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +# `--nonce-file` is raw bytes, not hex-or-raw guessed like `--key-file`: 12 ASCII bytes that are +# also valid hex text must be used as those 12 bytes, not decoded down to 6 (out of 7..=13). +test_nonce_file_is_raw_bytes_not_hex_decoded() { + printf 'aabbccddeeff' >"$TMP/nonce_raw.bin" + rng 35 >"$TMP/pt" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce-file "$TMP/nonce_raw.bin" <"$TMP/pt" >"$TMP/ct" + # The 12 raw bytes passed directly via --nonce must agree. + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$(hex "$TMP/nonce_raw.bin")" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "--nonce-file did not hex-decode its 12 bytes" + # The would-be hex decoding of those bytes is 6 bytes, which is a bad nonce length. + expect_fail "the hex reading of the file is a 6-byte nonce" \ + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce aabbccddeeff <"$TMP/ct" + assert_stderr_has "nonce is 6 bytes" +} + +test_a_missing_nonce_is_rejected_with_an_explanation() { + rng 4 >"$TMP/pt" + expect_fail "no nonce" "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" <"$TMP/pt" + assert_stderr_has "--nonce" + assert_stderr_has "no generated nonce" +} + +# 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_nonce_len_is_validated_across_a_1_s_whole_range() { + local n nonce + rng 13 >"$TMP/pt" + for n in 7 8 9 10 11 12 13; do + nonce=$(nonce_of $n 5a) + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$nonce" <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$nonce" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "nonce length $n" + done + # A.1: n is an element of {7, ..., 13}. + for n in 0 1 6 14 16; do + nonce=$(nonce_of $n 5a) + expect_fail "nonce length $n" \ + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$nonce" <"$TMP/pt" + assert_stderr_has "7 to 13" + done +} + +# The nonce length caps the payload (A.1's p < 2^8q, q = 15 - n), and the error gives the numbers. +test_a_payload_past_the_q_limit_is_rejected_with_the_numbers() { + local nonce + nonce=$(nonce_of 13 5a) # q = 2, so the limit is 65535 bytes + rng 65536 >"$TMP/too_big" + expect_fail "65536 bytes is past the q = 2 limit" \ + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$nonce" <"$TMP/too_big" + assert_stderr_has "65535" + assert_stderr_has "65536" + + # One byte under the limit is fine, which pins the boundary rather than just the rejection. + rng 65535 >"$TMP/ok" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$nonce" <"$TMP/ok" >"$TMP/ct" + assert_size "$TMP/ct" $((65535 + 16)) "65535 bytes is accepted" + + # The decrypt side hits the same limit on the input minus its tag, explained the same way. + rng $((65536 + 16)) >"$TMP/too_big_sealed" + expect_fail "65536 bytes of ciphertext plus a tag is past the limit" \ + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$nonce" <"$TMP/too_big_sealed" + assert_stderr_has "65535" + assert_stderr_has "65536" + assert_stderr_has "shorter nonce" + ! grep -q "GenericError" "$LAST_STDERR" || fail "not the Debug form: $(cat "$LAST_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, but is warned about: every length in +# 7..=13 is valid, so the only other symptom would be a failed tag check on the far side. +test_a_nonce_file_ending_in_a_newline_is_used_as_is_but_warned_about() { + { unhex "$NONCE"; printf '\n'; } >"$TMP/nonce_nl.bin" + unhex "$NONCE" >"$TMP/nonce_clean.bin" + rng 47 >"$TMP/pt" + + expect_ok "a 13-byte nonce file is still valid" \ + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce-file "$TMP/nonce_nl.bin" <"$TMP/pt" >"$TMP/ct" + assert_stderr_has "newline" + assert_stderr_has "echo -n" + + # The 13 bytes, newline included, are the nonce. + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$(hex "$TMP/nonce_nl.bin")" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "decrypting with the 13-byte nonce" + expect_fail "the 12-byte nonce the file was meant to hold does not decrypt it" \ + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/ct" + assert_stderr_has "authentication failed" + + # A file without the newline draws no warning. + expect_ok "a clean nonce file" \ + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce-file "$TMP/nonce_clean.bin" <"$TMP/pt" >"$TMP/ct" + assert_size "$LAST_STDERR" 0 "no warning for a clean file" +} + +test_nonce_file_read_errors_are_reported_as_read_errors() { + rng 4 >"$TMP/pt" + expect_fail "a missing nonce file" \ + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce-file "$TMP/does-not-exist/nonce.bin" <"$TMP/pt" + assert_stderr_has "couldn't read file" + assert_stderr_has "nonce.bin" +} + +# ---- the AAD -------------------------------------------------------------------------------- + +# The AAD is authenticated but not encrypted: it does not change the ciphertext length or the +# ciphertext, it does change the tag, and a mismatch on decryption is caught. +test_the_aad_is_authenticated_but_not_encrypted() { + rng 7 >"$TMP/pt" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" --aad 0011 <"$TMP/pt" >"$TMP/with" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/pt" >"$TMP/without" + assert_size "$TMP/with" $(wc -c <"$TMP/without") "AAD does not change the output length" + head -c 7 "$TMP/with" >"$TMP/with.ct" + head -c 7 "$TMP/without" >"$TMP/without.ct" + assert_same "$TMP/with.ct" "$TMP/without.ct" "AAD does not change the ciphertext, only the tag" + tail -c 16 "$TMP/with" >"$TMP/with.tag" + tail -c 16 "$TMP/without" >"$TMP/without.tag" + assert_differs "$TMP/with.tag" "$TMP/without.tag" "AAD changes the tag" + + # Wrong AAD, missing AAD and extra AAD must all be caught. + expect_fail "wrong AAD" \ + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$NONCE" --aad 0012 <"$TMP/with" + assert_stderr_has "authentication failed" + expect_fail "missing AAD" \ + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/with" + assert_stderr_has "authentication failed" + expect_fail "extra AAD" \ + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$NONCE" --aad 001100 <"$TMP/with" + assert_stderr_has "authentication failed" +} + +# The file is raw bytes, never hex-decoded: a file holding `ca fe ba be` is the same AAD as +# `--aad cafebabe`, while one holding the eight ASCII characters "cafebabe" is a different AAD. +# And the file wins if both flags are given, as for aes*-gcm. +test_aad_file_is_raw_bytes_and_takes_precedence() { + unhex cafebabe >"$TMP/aad.bin" + printf 'cafebabe' >"$TMP/aad.txt" + rng 36 >"$TMP/pt" + local enc=("$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE") + + "${enc[@]}" --aad cafebabe <"$TMP/pt" >"$TMP/with_hex" + "${enc[@]}" --aad-file "$TMP/aad.bin" <"$TMP/pt" >"$TMP/with_file" + assert_same "$TMP/with_hex" "$TMP/with_file" "raw bytes in a file are the same AAD as the hex flag" + + "${enc[@]}" --aad-file "$TMP/aad.txt" <"$TMP/pt" >"$TMP/with_text" + assert_differs "$TMP/with_hex" "$TMP/with_text" "the file is not hex-decoded" + + "${enc[@]}" --aad 00 --aad-file "$TMP/aad.bin" <"$TMP/pt" >"$TMP/both" + assert_same "$TMP/with_file" "$TMP/both" "--aad-file takes precedence over --aad" + + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$NONCE" --aad-file "$TMP/aad.bin" <"$TMP/with_file" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "decrypting with the file" +} + +# A file with no size to declare -- /dev/null, a character device -- is read whole rather than +# streamed, and an empty one is the same as no AAD at all. +test_a_non_regular_aad_file_is_read_whole() { + rng 18 >"$TMP/pt" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/pt" >"$TMP/without" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" --aad-file /dev/null <"$TMP/pt" >"$TMP/dev_null" + assert_same "$TMP/without" "$TMP/dev_null" "an empty AAD file is no AAD" +} + +test_a_missing_aad_file_is_reported() { + rng 4 >"$TMP/pt" + expect_fail "a missing AAD file" \ + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" --aad-file "$TMP/does-not-exist/aad.bin" <"$TMP/pt" + assert_stderr_has "couldn't read file" + assert_stderr_has "aad.bin" +} + +# ---- the tag -------------------------------------------------------------------------------- + +# A failed tag check must exit non-zero AND write nothing: SP 800-38C Sec 6.2 requires that on +# INVALID "the payload P and the MAC T shall not be revealed". +test_a_tampered_ciphertext_produces_no_output_at_all() { + local pos + rng 256 >"$TMP/pt" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/pt" >"$TMP/ct" + # A flipped bit in the first and last ciphertext bytes, then the first and last tag bytes. + for pos in 0 255 256 271; do + cp "$TMP/ct" "$TMP/bad" + flip_byte "$TMP/bad" "$pos" + if "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/bad" >"$TMP/out" 2>"$LAST_STDERR"; then + fail "a flipped bit at $pos must fail" + fi + assert_size "$TMP/out" 0 "no plaintext may be written when the tag check fails (flipped byte $pos)" + assert_stderr_has "authentication failed" + done +} + +# `--tag-len` changes the output length and must match on both sides, and only A.1's values are +# accepted. +test_tag_len_is_validated_and_must_match() { + local t + rng 18 >"$TMP/pt" + for t in 4 6 8 10 12 14 16; do + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" --tag-len $t <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((18 + t)) "tag-len $t" + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$NONCE" --tag-len $t <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "tag-len $t round trip" + done + # A.1: t is an element of {4, 6, 8, 10, 12, 14, 16}. Odd values and out-of-range are refused. + for t in 0 2 5 15 17 32; do + expect_fail "tag-len $t" \ + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" --tag-len $t <"$TMP/pt" + assert_stderr_has "tag-len" + assert_stderr_has "A.1" + done + # A tag-len mismatch between the two sides is caught rather than silently truncating. + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" --tag-len 16 <"$TMP/pt" >"$TMP/ct" + expect_fail "16 on one side, 8 on the other" \ + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$NONCE" --tag-len 8 <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +# An invalid tag length is a command-line error, so it must be rejected without waiting for EOF on +# the payload pipe: stdin here is held open by a sleeping writer, and `timeout` reports 124 if the +# command waited for it. +test_invalid_tag_len_is_rejected_before_stdin_is_read() { + local status=0 + timeout 2 "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" --tag-len 5 \ + < <(sleep 5) >/dev/null 2>"$LAST_STDERR" || status=$? + [ "$status" -ne 124 ] || fail "invalid --tag-len waited for stdin EOF" + [ "$status" -ne 0 ] || fail "an invalid --tag-len must be rejected" + assert_stderr_has "tag-len" + assert_stderr_has "A.1" +} + +# Sec 6.2 step 1: a C too short to contain a tag is rejected before anything else; exactly the tag +# length is an empty payload plus its tag, which is valid (Sec 5.3 footnote). +test_an_input_shorter_than_the_tag_is_rejected() { + local len + for len in 0 1 15; do + rng $len >"$TMP/short" + expect_fail "a $len-byte input cannot carry a 16-byte tag" \ + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/short" + assert_stderr_has "shorter than" + done + : >"$TMP/empty" + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/empty" >"$TMP/ct" + assert_size "$TMP/ct" 16 "an empty payload encrypts to just the tag" + "$BC_RUST" aes128-ccm -d decrypt --key "$KEY_128" --nonce "$NONCE" <"$TMP/ct" >"$TMP/rec" + assert_size "$TMP/rec" 0 "and round trips to nothing" +} + +# ---- keys ----------------------------------------------------------------------------------- + +# Key loading is shared with aes*-cbc; checked here so the CCM commands are not assumed to inherit +# it. +test_a_key_of_the_wrong_length_is_rejected() { + rng 4 >"$TMP/pt" + expect_fail "a 32-byte key is not an AES-128 key" \ + "$BC_RUST" aes128-ccm -d encrypt --key "$KEY_256" --nonce "$NONCE" <"$TMP/pt" + assert_stderr_has "16-byte key" + expect_fail "neither --key nor --key-file" \ + "$BC_RUST" aes128-ccm -d encrypt --nonce "$NONCE" <"$TMP/pt" + assert_stderr_has "key" +} + +# ---- help ----------------------------------------------------------------------------------- + +# The subcommands are listed in --help, and their own help documents what differs from the other +# modes: the supplied nonce, the non-streaming behaviour, and the nonce-reuse hazard. +test_the_subcommands_are_documented_in_help() { + local cmd + "$BC_RUST" --help >"$TMP/help" + for cmd in aes128-ccm aes192-ccm aes256-ccm; do + grep -q "$cmd" "$TMP/help" || fail "$cmd should be listed in --help" + done + "$BC_RUST" aes128-ccm --help >"$TMP/cmd_help" + grep -q -e "NOT GENERATED" -e "SUPPLIED" "$TMP/cmd_help" || fail "the help should say the nonce is supplied" + grep -qi "does not stream" "$TMP/cmd_help" || fail "the help should say it does not stream" + grep -q "never reuse a nonce" "$TMP/cmd_help" || fail "the help should warn about nonce reuse" + grep -q -- "--tag-len" "$TMP/cmd_help" && grep -q "defaults to 16" "$TMP/cmd_help" \ + || fail "the help should identify the option that has a default" + ! grep -q "usual choice and the default" "$TMP/cmd_help" \ + || fail "the help must not claim the required nonce has a default" +} + +run_all diff --git a/cli/tests/test_aes_cfb.sh b/cli/tests/test_aes_cfb.sh new file mode 100755 index 00000000..041d3fdf --- /dev/null +++ b/cli/tests/test_aes_cfb.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +# The aes128-cfb / aes192-cfb / aes256-cfb subcommands, end to end through the binary. +# +# Framing: encrypt writes a fresh IV as the first 16 bytes of its output and decrypt reads it back +# from the first 16 bytes of its input, as for CBC -- but CFB is a stream cipher, so input of any +# length is accepted and the ciphertext body is exactly as long as the plaintext. Keys and data +# come from `bc-rust rng`; the fixed inputs are the SP 800-38A Appendix F.3 known-answer set and +# the F.2.1 CBC ciphertext used for the cross-mode guard. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# One subcommand per key length: `cfb 128` prints "aes128-cfb". +cfb() { echo "aes$1-cfb"; } + +# ---- round trips -------------------------------------------------------------------------- + +test_round_trip_through_files() { + local bits + for bits in 128 192 256; do + rng "$(keylen $bits)" >"$TMP/key" + rng 1024 >"$TMP/pt" + + "$BC_RUST" "$(cfb $bits)" -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((16 + 1024)) "$bits: ciphertext is the IV plus the plaintext length" + assert_differs "$TMP/pt" "$TMP/ct" "$bits: the data must actually be encrypted" + + "$BC_RUST" "$(cfb $bits)" -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$bits: decrypt must recover the plaintext" + done +} + +test_round_trip_through_a_pipe_larger_than_the_pipe_buffer() { + rng 16 >"$TMP/key" + rng $((1024 * 1024)) >"$TMP/pt" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" <"$TMP/pt" \ + | "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "1 MiB must survive encrypt | decrypt with no file in between" +} + +# Every length from empty to just past two blocks: CFB pads nothing and rejects nothing, and the +# body of the ciphertext is exactly as long as the plaintext. +test_any_input_length_is_accepted_and_round_trips() { + rng 16 >"$TMP/key" + local len + for len in $(seq 0 33); do + rng "$len" >"$TMP/pt" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((16 + len)) "len $len: IV plus a body as long as the plaintext" + "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "len $len: round trip" + done +} + +# Sizes that straddle the 1 KiB streaming chunk and the block boundary: 1024 is one chunk, 1040 a +# chunk plus a block, 4112 four chunks plus a block; the odd sizes leave a partial final segment +# and put a chunk boundary in the middle of one. +test_round_trips_across_chunk_boundaries() { + rng 16 >"$TMP/key" + local size + for size in 16 32 1023 1024 1025 1040 4096 4112 65535 65536; do + rng "$size" >"$TMP/pt" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" <"$TMP/pt" \ + | "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$size bytes should round trip" + done +} + +test_hex_output_composes_through_hex_decode() { + rng 16 >"$TMP/key" + rng 4097 >"$TMP/pt" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" -x <"$TMP/pt" >"$TMP/ct.hex" + assert_size "$TMP/ct.hex" $((2 * (16 + 4097) + 1)) "-x emits two hex characters per byte, then a newline" + "$BC_RUST" hex-decode <"$TMP/ct.hex" \ + | "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "-x output must decrypt after hex-decode" +} + +# A fresh IV per invocation matters even more for CFB than for CBC: a repeated key-and-IV pair +# leaks the XOR of the two plaintexts outright. +test_each_invocation_uses_a_fresh_iv() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct1" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct2" + slice "$TMP/ct1" 0 16 >"$TMP/iv1" + slice "$TMP/ct2" 0 16 >"$TMP/iv2" + assert_differs "$TMP/iv1" "$TMP/iv2" "two encryptions must draw different IVs" + slice "$TMP/ct1" 16 64 >"$TMP/body1" + slice "$TMP/ct2" 16 64 >"$TMP/body2" + assert_differs "$TMP/body1" "$TMP/body2" "and the bodies differ too, not just the IV" + "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key" <"$TMP/ct2" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "the second ciphertext still decrypts" +} + +test_empty_input_produces_only_the_iv() { + rng 16 >"$TMP/key" + : >"$TMP/empty" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" <"$TMP/empty" >"$TMP/ct" + assert_size "$TMP/ct" 16 "an empty message encrypts to just the IV" + "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_size "$TMP/rec" 0 "and decrypts back to nothing" +} + +# Anything past the IV is ciphertext, whatever its length. +test_decrypt_accepts_an_unaligned_body() { + rng 16 >"$TMP/key" + rng $((16 + 20)) >"$TMP/ct" + "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_size "$TMP/rec" 20 "the plaintext is exactly as long as the ciphertext body" +} + +# ---- keys ----------------------------------------------------------------------------------- + +test_key_file_accepts_binary_hex_and_a_trailing_newline() { + rng 16 >"$TMP/key.bin" + hex "$TMP/key.bin" >"$TMP/key.hex" + { cat "$TMP/key.hex"; printf '\n'; } >"$TMP/key.hex.nl" + { cat "$TMP/key.bin"; printf '\n'; } >"$TMP/key.bin.nl" + rng 256 >"$TMP/pt" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" + + local form + for form in key.hex key.hex.nl key.bin.nl; do + "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" + done +} + +test_key_on_the_command_line_matches_the_key_file() { + rng 16 >"$TMP/key" + rng 256 >"$TMP/pt" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" aes128-cfb -d decrypt --key "$(hex "$TMP/key")" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "--key in hex must decrypt what --key-file encrypted" +} + +# CFB is unauthenticated: a wrong key succeeds and produces garbage of the same length, never the +# plaintext -- which is exactly why the crate docs insist on authenticating separately. +test_the_wrong_key_gives_the_wrong_plaintext() { + rng 16 >"$TMP/key" + rng 16 >"$TMP/other" + rng 256 >"$TMP/pt" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/other" <"$TMP/ct" >"$TMP/rec" + assert_differs "$TMP/pt" "$TMP/rec" "a different key must not recover the plaintext" + assert_size "$TMP/rec" 256 "but the length is unchanged" +} + +test_an_all_zero_key_warns_but_proceeds() { + head -c 16 /dev/zero >"$TMP/key" + rng 64 >"$TMP/pt" + expect_ok "an all-zero key is accepted" \ + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_stderr_has "arning" + assert_size "$TMP/ct" $((16 + 64)) "and the output is complete" +} + +# ---- known answers -------------------------------------------------------------------------- + +# SP 800-38A Appendix F.3.13, F.3.15 and F.3.17 (CFB128 at each key length), transcribed in the +# Rust suite this file replaces, plus F.2.1 (CBC-AES128) for the cross-mode guard. The CLI takes +# the IV as the first 16 bytes of its input, so the input is IV || ciphertext, and the output must +# be the appendix's four plaintext blocks. +F3_IV=000102030405060708090a0b0c0d0e0f +F3_PLAINTEXT=6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710 +F3_KEY_128=2b7e151628aed2a6abf7158809cf4f3c +F3_KEY_192=8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b +F3_KEY_256=603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4 +F3_CT_128=3b3fd92eb72dad20333449f8e83cfb4ac8a64537a0b3a93fcde3cdad9f1ce58b26751f67a3cbb140b1808cf187a4f4dfc04b05357c5d1c0eeac4c66f9ff7f2e6 +F3_CT_192=cdc80d6fddf18cab34c25909c99a417467ce7f7f81173621961a2b70171d3d7a2e1e8a1dd59b88b1c8e60fed1efac4c9c05f9f9ca9834fa042ae8fba584b09ff +F3_CT_256=dc7e84bfda79164b7ecd8486985d386039ffed143b28b1c832113c6331e5407bdf10132415e54b92a13ed0a8267ae2f975a385741ab9cef82031623d55b1e471 +F2_CBC_CT_128=7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b273bed6b8e3c1743b7116e69e222295163ff1caa1681fac09120eca307586e1a7 + +test_decrypt_matches_sp800_38a_f3_vectors() { + local bits key ct got + for bits in 128 192 256; do + key="F3_KEY_$bits" + ct="F3_CT_$bits" + unhex "$F3_IV${!ct}" >"$TMP/ct" + got=$("$BC_RUST" "$(cfb $bits)" -d decrypt --key "${!key}" <"$TMP/ct" | "$BC_RUST" hex-encode) + assert_eq "$got" "$F3_PLAINTEXT" "$bits: F.3 decrypt vector" + done +} + +# -x gives the identical answer in hex, plus a trailing newline. +test_hex_output_matches_binary_output() { + unhex "$F3_IV$F3_CT_128" >"$TMP/ct" + local got + got=$("$BC_RUST" aes128-cfb -d decrypt --key "$F3_KEY_128" -x <"$TMP/ct") + assert_eq "$got" "$F3_PLAINTEXT" "-x decrypt of the F.3.13 vector" +} + +# Appendix D, Table D.2 for CFB: a bit error in Cj gives *specific* bit errors in the decryption of +# Cj -- the very same bit position -- plus random bit errors in Cj+1, and nothing beyond that (with +# s = b, b/s is 1). This is what makes CFB tampering directly exploitable, and it is also a sharp +# check that the CLI is running CFB rather than CBC: under CBC the controlled flip would land in +# Pj+1, not Pj. +test_a_ciphertext_bit_flip_flips_the_same_plaintext_bit() { + # Byte 3 of the second ciphertext block: input is IV | C1 | C2 | C3 | C4, so C2 starts at 32. + local offset=$((32 + 3)) mask=$((0x20)) + unhex "$(flip_hex "$F3_IV$F3_CT_128" $offset $mask)" >"$TMP/ct" + "$BC_RUST" aes128-cfb -d decrypt --key "$F3_KEY_128" <"$TMP/ct" >"$TMP/out" + assert_size "$TMP/out" 64 "four plaintext blocks" + + unhex "$F3_PLAINTEXT" >"$TMP/pt" + # Plaintext byte 19 is byte 3 of P2. + unhex "$(flip_hex "$F3_PLAINTEXT" 19 $mask)" >"$TMP/pt_flipped" + + slice "$TMP/out" 0 16 >"$TMP/p1"; slice "$TMP/pt" 0 16 >"$TMP/e1" + assert_same "$TMP/p1" "$TMP/e1" "P1 depends only on the IV, so it is unaffected" + slice "$TMP/out" 16 16 >"$TMP/p2"; slice "$TMP/pt_flipped" 16 16 >"$TMP/e2" + assert_same "$TMP/p2" "$TMP/e2" "P2 should show exactly the flipped bit" + slice "$TMP/out" 32 16 >"$TMP/p3"; slice "$TMP/pt" 32 16 >"$TMP/e3" + assert_differs "$TMP/p3" "$TMP/e3" "P3 is randomised: C2 feeds the next cipher call" + slice "$TMP/out" 48 16 >"$TMP/p4"; slice "$TMP/pt" 48 16 >"$TMP/e4" + assert_same "$TMP/p4" "$TMP/e4" "P4 is unaffected: with s = b, damage stops at P3" +} + +# CFB and CBC take the same arguments and produce the same-shaped output, so nothing but this +# stops a caller pairing them up by mistake. Both spec ciphertexts are for the same key, IV and +# plaintext, so each mode must reproduce the plaintext only from its own ciphertext; neither is +# authenticated, so the mismatch is silent garbage rather than an error. +test_cfb_and_cbc_are_not_interchangeable() { + unhex "$F3_IV$F3_CT_128" >"$TMP/cfb_ct" + unhex "$F3_IV$F2_CBC_CT_128" >"$TMP/cbc_ct" + unhex "$F3_PLAINTEXT" >"$TMP/pt" + + "$BC_RUST" aes128-cfb -d decrypt --key "$F3_KEY_128" <"$TMP/cfb_ct" >"$TMP/own_cfb" + assert_same "$TMP/own_cfb" "$TMP/pt" "CFB decrypts its own ciphertext" + "$BC_RUST" aes128-cbc -d decrypt --key "$F3_KEY_128" <"$TMP/cbc_ct" >"$TMP/own_cbc" + assert_same "$TMP/own_cbc" "$TMP/pt" "CBC decrypts its own ciphertext" + + "$BC_RUST" aes128-cfb -d decrypt --key "$F3_KEY_128" <"$TMP/cbc_ct" >"$TMP/cross_cfb" + assert_differs "$TMP/cross_cfb" "$TMP/pt" "CFB must not decrypt a CBC ciphertext" + "$BC_RUST" aes128-cbc -d decrypt --key "$F3_KEY_128" <"$TMP/cfb_ct" >"$TMP/cross_cbc" + assert_differs "$TMP/cross_cbc" "$TMP/pt" "CBC must not decrypt a CFB ciphertext" +} + +# ---- rejected inputs ------------------------------------------------------------------------ + +test_decrypt_input_shorter_than_the_iv_is_rejected() { + rng 16 >"$TMP/key" + local len + for len in 0 1 15; do + rng "$len" >"$TMP/short" + expect_fail "$len bytes cannot hold a 16-byte IV" \ + "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key" <"$TMP/short" + assert_stderr_has "IV" + done +} + +test_a_key_of_the_wrong_length_is_rejected() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + expect_fail "a 16-byte key is not an AES-256 key" \ + "$BC_RUST" aes256-cfb -d encrypt --key-file "$TMP/key" <"$TMP/pt" + assert_stderr_has "32-byte key" + assert_stderr_has "16 bytes" +} + +test_a_missing_key_is_rejected() { + rng 64 >"$TMP/pt" + expect_fail "neither --key nor --key-file" \ + "$BC_RUST" aes128-cfb -d encrypt <"$TMP/pt" + assert_stderr_has "key" +} + +test_a_missing_direction_is_rejected() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + expect_fail "--direction is required" \ + "$BC_RUST" aes128-cfb --key-file "$TMP/key" <"$TMP/pt" + assert_stderr_has "direction" +} + +# ---- discoverability ------------------------------------------------------------------------ + +test_the_subcommands_are_listed_in_help() { + "$BC_RUST" --help >"$TMP/help" + local cmd + for cmd in aes128-cfb aes192-cfb aes256-cfb; do + grep -q "$cmd" "$TMP/help" || fail "--help should list $cmd" + done +} + +# Each subcommand's own help names the two directions, the IV convention, and -- because CFB8 and +# CFB1 are different, non-interoperable modes -- the segment size. +test_per_command_help_documents_the_iv_convention_and_the_segment_size() { + "$BC_RUST" aes128-cfb --help >"$TMP/help" + grep -q "encrypt" "$TMP/help" || fail "help should list the encrypt direction" + grep -q "decrypt" "$TMP/help" || fail "help should list the decrypt direction" + grep -qi "first 16 bytes" "$TMP/help" || fail "help should explain where the IV goes" + grep -q "CFB128" "$TMP/help" || fail "help should say which CFB variant this is" +} + +run_all diff --git a/cli/tests/test_aes_cfb8.sh b/cli/tests/test_aes_cfb8.sh new file mode 100755 index 00000000..5592ae80 --- /dev/null +++ b/cli/tests/test_aes_cfb8.sh @@ -0,0 +1,293 @@ +#!/usr/bin/env bash +# The aes128-cfb8 / aes192-cfb8 / aes256-cfb8 subcommands, end to end through the binary. +# +# Framing: encrypt writes a fresh IV as the first 16 bytes of its output and decrypt reads it back +# from the first 16 bytes of its input. CFB8's segment is one byte, so any input length is +# accepted and the ciphertext is exactly as long as the plaintext. Keys and data come from +# `bc-rust rng`; the fixed inputs are the SP 800-38A Appendix F.3 known-answer set. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# One subcommand per key length: `cfb8 128` prints "aes128-cfb8". +cfb8() { echo "aes$1-cfb8"; } + +# ---- round trips -------------------------------------------------------------------------- + +test_round_trip_through_files() { + local bits + for bits in 128 192 256; do + rng "$(keylen $bits)" >"$TMP/key" + rng 1000 >"$TMP/pt" + + "$BC_RUST" "$(cfb8 $bits)" -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((16 + 1000)) "$bits: ciphertext is the IV plus an equal-length body" + assert_differs "$TMP/pt" "$TMP/ct" "$bits: the data must actually be encrypted" + + "$BC_RUST" "$(cfb8 $bits)" -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$bits: decrypt must recover the plaintext" + done +} + +# Smaller than the other modes' pipe test because CFB8 spends a full AES call per byte. +test_round_trip_through_a_pipe_larger_than_the_pipe_buffer() { + rng 16 >"$TMP/key" + rng $((256 * 1024)) >"$TMP/pt" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/pt" \ + | "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "256 KiB must survive encrypt | decrypt with no file in between" +} + +test_any_input_length_is_accepted_and_round_trips() { + rng 16 >"$TMP/key" + local len + for len in $(seq 0 33); do + rng "$len" >"$TMP/pt" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((16 + len)) "len $len: IV plus an equal-length ciphertext" + "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "len $len: round trip" + done +} + +# Sizes that straddle the 1 KiB streaming chunk, including ones that leave the chunk boundary in +# the middle of the batch the decryptor uses. +test_round_trips_across_chunk_boundaries() { + rng 16 >"$TMP/key" + local size + for size in 1 8 9 1023 1024 1025 4096 4099; do + rng "$size" >"$TMP/pt" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/pt" \ + | "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$size bytes should round trip" + done +} + +test_hex_output_composes_through_hex_decode() { + rng 16 >"$TMP/key" + rng 777 >"$TMP/pt" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" -x <"$TMP/pt" >"$TMP/ct.hex" + assert_size "$TMP/ct.hex" $((2 * (16 + 777) + 1)) "-x emits two hex characters per byte and a newline" + "$BC_RUST" hex-decode <"$TMP/ct.hex" \ + | "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "-x output must decrypt after hex-decode" +} + +test_each_invocation_uses_a_fresh_iv() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct1" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct2" + head -c 16 "$TMP/ct1" >"$TMP/iv1" + head -c 16 "$TMP/ct2" >"$TMP/iv2" + assert_differs "$TMP/iv1" "$TMP/iv2" "two encryptions must draw different IVs" + "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/key" <"$TMP/ct2" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "the second ciphertext still decrypts" +} + +test_empty_input_produces_only_the_iv() { + rng 16 >"$TMP/key" + : >"$TMP/empty" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/empty" >"$TMP/ct" + assert_size "$TMP/ct" 16 "an empty message encrypts to just the IV" + "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_size "$TMP/rec" 0 "and decrypts back to nothing" +} + +# ---- keys ----------------------------------------------------------------------------------- + +test_key_file_accepts_binary_hex_and_a_trailing_newline() { + rng 16 >"$TMP/key.bin" + hex "$TMP/key.bin" >"$TMP/key.hex" + { cat "$TMP/key.hex"; printf '\n'; } >"$TMP/key.hex.nl" + { cat "$TMP/key.bin"; printf '\n'; } >"$TMP/key.bin.nl" + rng 200 >"$TMP/pt" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" + + local form + for form in key.hex key.hex.nl key.bin.nl; do + "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" + done +} + +test_key_on_the_command_line_matches_the_key_file() { + rng 16 >"$TMP/key" + rng 200 >"$TMP/pt" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" aes128-cfb8 -d decrypt --key "$(hex "$TMP/key")" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "--key in hex must decrypt what --key-file encrypted" +} + +test_a_wrong_key_does_not_recover_the_plaintext() { + rng 16 >"$TMP/key" + rng 16 >"$TMP/other" + rng 200 >"$TMP/pt" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + # CFB8 is unauthenticated: a wrong key succeeds and produces garbage of the same length. + "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/other" <"$TMP/ct" >"$TMP/rec" + assert_differs "$TMP/pt" "$TMP/rec" "a different key must not recover the plaintext" + assert_size "$TMP/rec" 200 "but the length is unchanged" +} + +test_an_all_zero_key_warns_but_proceeds() { + head -c 16 /dev/zero >"$TMP/key" + rng 18 >"$TMP/pt" + expect_ok "an all-zero key is accepted" \ + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_stderr_has "arning" + assert_size "$TMP/ct" $((16 + 18)) "and the output is complete" +} + +# ---- known answers -------------------------------------------------------------------------- + +# SP 800-38A Appendix F.3.7, F.3.9 and F.3.11 (CFB8 encrypt at each key length), transcribed in +# the Rust suite this file replaces. `decrypt` is the direction that can be pinned, since +# `encrypt` draws its own IV; the input here is IV || ciphertext, and the output must be the +# appendix's 18 plaintext bytes. +F3_IV=000102030405060708090a0b0c0d0e0f +F3_PLAINTEXT=6bc1bee22e409f96e93d7e117393172aae2d +F3_KEY_128=2b7e151628aed2a6abf7158809cf4f3c +F3_KEY_192=8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b +F3_KEY_256=603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4 +F3_CT_128=3b79424c9c0dd436bace9e0ed4586a4f32b9 +F3_CT_192=cda2521ef0a905ca44cd057cbf0d47a0678a +F3_CT_256=dc1f1a8520a64db55fcc8ac554844e889700 +# F.3.13 CFB128-AES128.Encrypt, first 18 bytes: same key, IV and plaintext as F3_CT_128, for the +# cross-mode guard. +F3_CFB128_CT_128=3b3fd92eb72dad20333449f8e83cfb4ac8a6 + +test_decrypt_matches_sp800_38a_f3_vectors() { + local bits key ct got + for bits in 128 192 256; do + key="F3_KEY_$bits" + ct="F3_CT_$bits" + unhex "$F3_IV${!ct}" >"$TMP/ct" + got=$("$BC_RUST" "$(cfb8 $bits)" -d decrypt --key "${!key}" <"$TMP/ct" | "$BC_RUST" hex-encode) + assert_eq "$got" "$F3_PLAINTEXT" "$bits: F.3 decrypt vector" + done +} + +test_hex_output_matches_binary_output() { + unhex "$F3_IV$F3_CT_128" >"$TMP/ct" + local binary_as_hex hex_out + binary_as_hex=$("$BC_RUST" aes128-cfb8 -d decrypt --key "$F3_KEY_128" <"$TMP/ct" | "$BC_RUST" hex-encode) + hex_out=$("$BC_RUST" aes128-cfb8 -d decrypt --key "$F3_KEY_128" -x <"$TMP/ct") + assert_eq "$hex_out" "$binary_as_hex" "-x must be the hex of the binary output" + assert_eq "$hex_out" "$F3_PLAINTEXT" "-x must be the F.3.7 plaintext" +} + +# Both spec ciphertexts are for the same key, IV and plaintext, so each mode must reproduce the +# plaintext only from its own ciphertext. They agree on the first byte -- P1 XOR MSB_8(CIPH_K(IV)) +# in both -- and diverge immediately after; neither mode is authenticated, so the mismatch is +# silent. +test_cfb8_and_cfb128_are_not_interchangeable() { + unhex "$F3_PLAINTEXT" >"$TMP/pt" + unhex "$F3_IV$F3_CT_128" >"$TMP/cfb8.ct" + unhex "$F3_IV$F3_CFB128_CT_128" >"$TMP/cfb128.ct" + + "$BC_RUST" aes128-cfb8 -d decrypt --key "$F3_KEY_128" <"$TMP/cfb8.ct" >"$TMP/own8" + assert_same "$TMP/pt" "$TMP/own8" "CFB8 decrypts its own ciphertext" + "$BC_RUST" aes128-cfb -d decrypt --key "$F3_KEY_128" <"$TMP/cfb128.ct" >"$TMP/own128" + assert_same "$TMP/pt" "$TMP/own128" "CFB128 decrypts its own ciphertext" + + "$BC_RUST" aes128-cfb8 -d decrypt --key "$F3_KEY_128" <"$TMP/cfb128.ct" >"$TMP/cross8" + assert_differs "$TMP/pt" "$TMP/cross8" "CFB8 must not decrypt a CFB128 ciphertext" + assert_eq "$(byte_at "$TMP/cross8" 0)" "$(byte_at "$TMP/pt" 0)" "...though the first byte necessarily agrees" + + "$BC_RUST" aes128-cfb -d decrypt --key "$F3_KEY_128" <"$TMP/cfb8.ct" >"$TMP/cross128" + assert_differs "$TMP/pt" "$TMP/cross128" "CFB128 must not decrypt a CFB8 ciphertext" +} + +# ---- SP 800-38A Appendix D ------------------------------------------------------------------ + +# Table D.2 for CFB: "SBE in the decryption of Cj" plus "RBE in the decryption of Cj+1,...,Cj+b/s". +# With s = 8 on a 16-byte block, b/s is 16: a flipped ciphertext bit flips the same bit of the same +# plaintext byte, corrupts the next 16 bytes, and then decryption resynchronises exactly. That is +# also a sharp check that the CLI is running CFB8 and not CFB128, whose window is one block. +test_a_ciphertext_bit_flip_damages_exactly_sixteen_following_bytes() { + rng 16 >"$TMP/key" + rng 48 >"$TMP/pt" + "$BC_RUST" aes128-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + + # Byte 8 of the body, which starts after the 16-byte IV; flip bit 5. + local j=8 mask=32 + cp "$TMP/ct" "$TMP/corrupt" + flip_byte "$TMP/corrupt" $((16 + j)) $mask + "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/key" <"$TMP/corrupt" >"$TMP/out" + assert_size "$TMP/out" 48 "the output length is unchanged" + + head -c $j "$TMP/pt" >"$TMP/pt.head" + head -c $j "$TMP/out" >"$TMP/out.head" + assert_same "$TMP/pt.head" "$TMP/out.head" "earlier bytes are unaffected" + + assert_eq "$(byte_at "$TMP/out" $j)" "$(( $(byte_at "$TMP/pt" $j) ^ mask ))" \ + "SBE: exactly the flipped bit, in the targeted byte" + + tail -c +$((j + 2)) "$TMP/pt" | head -c 16 >"$TMP/pt.window" + tail -c +$((j + 2)) "$TMP/out" | head -c 16 >"$TMP/out.window" + assert_differs "$TMP/pt.window" "$TMP/out.window" "the next b/s = 16 bytes should be randomised" + + tail -c +$((j + 18)) "$TMP/pt" >"$TMP/pt.tail" + tail -c +$((j + 18)) "$TMP/out" >"$TMP/out.tail" + assert_same "$TMP/pt.tail" "$TMP/out.tail" "byte j + 17 onwards must be exactly right again" +} + +# ---- rejected inputs ------------------------------------------------------------------------ + +test_decrypt_input_shorter_than_the_iv_is_rejected() { + rng 16 >"$TMP/key" + local len + for len in 0 1 15; do + rng "$len" >"$TMP/short" + expect_fail "$len bytes cannot hold a 16-byte IV" \ + "$BC_RUST" aes128-cfb8 -d decrypt --key-file "$TMP/key" <"$TMP/short" + assert_stderr_has "IV" + done +} + +test_a_key_of_the_wrong_length_is_rejected() { + rng 16 >"$TMP/key" + rng 18 >"$TMP/pt" + expect_fail "a 16-byte key is not an AES-256 key" \ + "$BC_RUST" aes256-cfb8 -d encrypt --key-file "$TMP/key" <"$TMP/pt" + assert_stderr_has "32-byte key" + assert_stderr_has "16 bytes" +} + +test_a_missing_key_is_rejected() { + rng 18 >"$TMP/pt" + expect_fail "neither --key nor --key-file" \ + "$BC_RUST" aes128-cfb8 -d encrypt <"$TMP/pt" + assert_stderr_has -- "--key" +} + +# A rejected key with a large stdin behind it: the error must still be the CLI's own, not a pipe +# failure. +test_a_large_payload_on_an_error_path_is_still_reported() { + rng $((256 * 1024)) >"$TMP/pt" + expect_fail "no key, large input" \ + "$BC_RUST" aes128-cfb8 -d encrypt <"$TMP/pt" + assert_stderr_has -- "--key" +} + +# ---- discoverability ------------------------------------------------------------------------ + +test_the_subcommands_are_listed_in_help() { + local help cmd + help=$("$BC_RUST" --help) + for cmd in aes128-cfb8 aes192-cfb8 aes256-cfb8; do + echo "$help" | grep -q "$cmd" || fail "--help should list $cmd" + done +} + +test_per_command_help_documents_the_segment_size_and_the_cost() { + local help + help=$("$BC_RUST" aes128-cfb8 --help) + echo "$help" | grep -q "encrypt" || fail "help should list the encrypt direction" + echo "$help" | grep -q "decrypt" || fail "help should list the decrypt direction" + echo "$help" | grep -qi "first 16 bytes" || fail "help should explain where the IV goes" + echo "$help" | grep -q "CFB8" || fail "help should say which CFB variant this is" + echo "$help" | grep -qi "non-interoperable" || fail "help should warn that CFB8 is not CFB128" +} + +run_all diff --git a/cli/tests/test_aes_ctr.sh b/cli/tests/test_aes_ctr.sh new file mode 100755 index 00000000..1806b0d1 --- /dev/null +++ b/cli/tests/test_aes_ctr.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash +# The aes128-ctr / aes192-ctr / aes256-ctr subcommands, end to end through the binary. +# +# Framing: encrypt writes a fresh 12-byte nonce (not the 16-byte IV the other modes write) as the +# first bytes of its output, decrypt reads it back from the first 12 bytes of its input, and CTR +# accepts any input length with a ciphertext body exactly as long as the plaintext. Keys and data +# come from `bc-rust rng`; the one fixed input is the OpenSSL-generated vector set, the same one +# `crypto/aes/tests/ctr_vector_tests.rs` uses. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# One subcommand per key length: `ctr 128` prints "aes128-ctr". +ctr() { echo "aes$1-ctr"; } +NONCE_LEN=12 + +# ---- the OpenSSL vectors --------------------------------------------------------------------- + +# `openssl enc -aes-*-ctr -K -iv 000102030405060708090a0b00000000`, OpenSSL 3.0.13, +# transcribed in the Rust suite this file replaces. The nonce is the leading 12 bytes of that +# initial counter block, and the message is four SP 800-38A Appendix F blocks plus five bytes: +# five counter blocks, the last partial. +V_NONCE=000102030405060708090a0b +V_PLAINTEXT=6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c37100011223344 +V_KEY_128=2b7e151628aed2a6abf7158809cf4f3c +V_KEY_192=8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b +V_KEY_256=603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4 +V_CT_128=ffd8816338abebca17491bc67fe6751c093833c279e946d49804c6b03df09f9d6b0727101b346a530523d59fb883e678fda525b39296cfc5a821d4dcda5a622706efd63405 +V_CT_192=c85f24d60a6fd4593209730ecd1ed507deae5f770708a1e162d04d42fe3dd6e6acf360f5c5f25e53a09396547d8b7f9b9d12dc684df141cd0b5462450a8d19004a271f6e8e +V_CT_256=b66c7ac8885c5ff473855203b36048ff5e7e0746b6e3ad4c2b84aaf440b1b98738a9ad1527187f6f435b83b09734cb04b3e3a2a77d2a02c4759cbd9b8fc822b31223c7e590 + +test_decrypt_matches_the_openssl_vectors() { + local bits key ct got + for bits in 128 192 256; do + key="V_KEY_$bits" + ct="V_CT_$bits" + unhex "$V_NONCE${!ct}" >"$TMP/ct" + got=$("$BC_RUST" "$(ctr $bits)" -d decrypt --key "${!key}" <"$TMP/ct" | "$BC_RUST" hex-encode) + assert_eq "$got" "$V_PLAINTEXT" "$bits: OpenSSL decrypt vector" + done +} + +test_hex_output_matches_binary_output() { + unhex "$V_NONCE$V_CT_128" >"$TMP/ct" + "$BC_RUST" aes128-ctr -d decrypt --key "$V_KEY_128" <"$TMP/ct" >"$TMP/bin" + "$BC_RUST" aes128-ctr -d decrypt --key "$V_KEY_128" -x <"$TMP/ct" >"$TMP/hex" + assert_eq "$(tr -d '\n' <"$TMP/hex")" "$(hex "$TMP/bin")" "-x output is the hex of the binary output" + assert_eq "$(tr -d '\n' <"$TMP/hex")" "$V_PLAINTEXT" "and both are the vector's plaintext" +} + +# ---- the nonce is 12 bytes ------------------------------------------------------------------- + +test_the_nonce_is_twelve_bytes_not_sixteen() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((69 + 12)) "output is a 12-byte nonce plus a body as long as the plaintext" + "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "decrypt consumes exactly 12 bytes of nonce" +} + +test_decrypt_input_shorter_than_the_nonce_is_rejected() { + rng 16 >"$TMP/key" + local len + for len in 0 1 11; do + rng "$len" >"$TMP/short" + expect_fail "$len bytes cannot hold a 12-byte nonce" \ + "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" <"$TMP/short" + assert_stderr_has "IV" + done +} + +test_empty_input_produces_only_the_nonce() { + rng 16 >"$TMP/key" + : >"$TMP/empty" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/empty" >"$TMP/ct" + assert_size "$TMP/ct" $NONCE_LEN "an empty message encrypts to just the nonce" + "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_size "$TMP/rec" 0 "and decrypts back to nothing" +} + +# ---- round trips ----------------------------------------------------------------------------- + +test_round_trip_through_files() { + local bits + for bits in 128 192 256; do + rng "$(keylen $bits)" >"$TMP/key" + rng 1000 >"$TMP/pt" + "$BC_RUST" "$(ctr $bits)" -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((NONCE_LEN + 1000)) "$bits: nonce plus ciphertext" + assert_differs "$TMP/pt" "$TMP/ct" "$bits: the data must actually be encrypted" + "$BC_RUST" "$(ctr $bits)" -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$bits: round trip" + done +} + +test_round_trip_through_a_pipe_larger_than_the_pipe_buffer() { + rng 16 >"$TMP/key" + rng $((1024 * 1024)) >"$TMP/pt" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" \ + | "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "1 MiB must survive encrypt | decrypt with no file in between" +} + +test_any_input_length_is_accepted_and_round_trips() { + rng 16 >"$TMP/key" + local len + for len in $(seq 0 33); do + rng "$len" >"$TMP/pt" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((len + NONCE_LEN)) "len $len: nonce plus an equal-length body" + "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "len $len: round trip" + done +} + +test_round_trips_across_chunk_boundaries() { + rng 16 >"$TMP/key" + local size + for size in 16 1023 1024 1025 4096 4099 65536; do + rng "$size" >"$TMP/pt" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$size bytes should round trip" + done +} + +test_hex_output_composes_through_hex_decode() { + rng 16 >"$TMP/key" + rng 4099 >"$TMP/pt" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" -x <"$TMP/pt" >"$TMP/ct.hex" + assert_size "$TMP/ct.hex" $((2 * (NONCE_LEN + 4099) + 1)) "-x emits two hex characters per byte, then a newline" + "$BC_RUST" hex-decode <"$TMP/ct.hex" \ + | "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "-x output must decrypt after hex-decode" +} + +# A fresh nonce per invocation. For CTR this is the whole security argument: a repeated nonce +# under one key repeats the keystream and leaks the XOR of the two messages. +test_each_invocation_uses_a_fresh_nonce() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + local i + : >"$TMP/nonces" + for i in $(seq 1 8); do + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + head -c $NONCE_LEN "$TMP/ct" | "$BC_RUST" hex-encode >>"$TMP/nonces" + echo >>"$TMP/nonces" + "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "run $i still decrypts" + done + assert_eq "$(sort -u "$TMP/nonces" | wc -l)" 8 "eight encryptions must draw eight distinct nonces" +} + +# ---- keys ------------------------------------------------------------------------------------ + +test_key_file_accepts_binary_hex_and_a_trailing_newline() { + rng 16 >"$TMP/key.bin" + hex "$TMP/key.bin" >"$TMP/key.hex" + { cat "$TMP/key.hex"; printf '\n'; } >"$TMP/key.hex.nl" + { cat "$TMP/key.bin"; printf '\n'; } >"$TMP/key.bin.nl" + rng 256 >"$TMP/pt" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key.bin" <"$TMP/pt" >"$TMP/ct" + + local form + for form in key.hex key.hex.nl key.bin.nl; do + "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$form must load as the same key as key.bin" + done +} + +test_key_on_the_command_line_matches_the_key_file() { + rng 16 >"$TMP/key" + rng 256 >"$TMP/pt" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" aes128-ctr -d decrypt --key "$(hex "$TMP/key")" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "--key in hex must decrypt what --key-file encrypted" +} + +test_a_key_of_the_wrong_length_is_rejected() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + expect_fail "a 16-byte key is not an AES-256 key" \ + "$BC_RUST" aes256-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" + assert_stderr_has "32-byte key" + assert_stderr_has "16 bytes" +} + +test_a_missing_key_is_rejected() { + rng 64 >"$TMP/pt" + expect_fail "neither --key nor --key-file" \ + "$BC_RUST" aes128-ctr -d encrypt <"$TMP/pt" + assert_stderr_has -- "--key" +} + +test_an_all_zero_key_warns_but_proceeds() { + head -c 16 /dev/zero >"$TMP/key" + rng 69 >"$TMP/pt" + expect_ok "an all-zero key is accepted" \ + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_stderr_has "arning" + assert_size "$TMP/ct" $((NONCE_LEN + 69)) "nonce plus the 69 ciphertext bytes" +} + +# ---- CTR-specific behaviour ------------------------------------------------------------------ + +# Encryption and decryption are the same operation (SP 800-38A Sec 6.5): the keystream depends on +# nothing but key and nonce, so presenting the nonce followed by a *plaintext* to `decrypt` gives +# exactly the ciphertext body that `encrypt` produced under that nonce. +test_encrypt_and_decrypt_are_the_same_operation() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + head -c $NONCE_LEN "$TMP/ct" >"$TMP/nonce" + tail -c +$((NONCE_LEN + 1)) "$TMP/ct" >"$TMP/body" + cat "$TMP/nonce" "$TMP/pt" | "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/key" >"$TMP/again" + assert_same "$TMP/body" "$TMP/again" "decrypt of nonce || plaintext must be the ciphertext body" +} + +# Appendix D, Table D.2 for CTR: "SBE in the decryption of Cj", and nothing else affected. A +# flipped ciphertext bit flips exactly the corresponding plaintext bit, with no garbling anywhere +# to signal the tampering. +test_a_ciphertext_bit_flip_flips_exactly_that_plaintext_bit_and_nothing_else() { + unhex "$V_NONCE$V_CT_128" >"$TMP/ct" + unhex "$V_PLAINTEXT" >"$TMP/expected" + # Byte 3 of the second block. The body starts after the 12-byte nonce. + flip_byte "$TMP/ct" $((12 + 16 + 3)) 32 + flip_byte "$TMP/expected" $((16 + 3)) 32 + "$BC_RUST" aes128-ctr -d decrypt --key "$V_KEY_128" <"$TMP/ct" >"$TMP/got" + assert_same "$TMP/expected" "$TMP/got" "exactly one plaintext bit should change, and nothing else" +} + +test_a_wrong_key_does_not_recover_the_plaintext() { + rng 16 >"$TMP/key" + rng 16 >"$TMP/other" + rng 69 >"$TMP/pt" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + # CTR is unauthenticated: a wrong key succeeds, with output the same length and wrong. + "$BC_RUST" aes128-ctr -d decrypt --key-file "$TMP/other" <"$TMP/ct" >"$TMP/rec" + assert_differs "$TMP/pt" "$TMP/rec" "a wrong key must not recover the plaintext" + assert_size "$TMP/rec" 69 "but the length is unchanged" +} + +test_ctr_and_cfb_are_not_interchangeable() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" aes128-ctr -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ctr" + "$BC_RUST" aes128-cfb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/cfb" + assert_size "$TMP/ctr" $((69 + 12)) "CTR prepends 12 bytes" + assert_size "$TMP/cfb" $((69 + 16)) "CFB prepends 16" + "$BC_RUST" aes128-cfb -d decrypt --key-file "$TMP/key" <"$TMP/ctr" >"$TMP/rec" + assert_differs "$TMP/pt" "$TMP/rec" "CFB must not decrypt a CTR ciphertext" +} + +# ---- discoverability ------------------------------------------------------------------------- + +test_the_subcommands_are_listed_in_help() { + "$BC_RUST" --help >"$TMP/help" + local cmd + for cmd in aes128-ctr aes192-ctr aes256-ctr; do + grep -q "$cmd" "$TMP/help" || fail "--help should list $cmd" + done +} + +# The per-command help must state the 12-byte nonce, the counter limit and the malleability +# warning, because all three differ from the other modes. +test_per_command_help_documents_the_nonce_and_the_counter() { + "$BC_RUST" aes128-ctr --help >"$TMP/help" + grep -q "encrypt" "$TMP/help" || fail "help should list the encrypt direction" + grep -q "decrypt" "$TMP/help" || fail "help should list the decrypt direction" + grep -qi "first 12 bytes" "$TMP/help" || fail "help should say the nonce is 12 bytes" + grep -q "counter" "$TMP/help" || fail "help should mention the counter" + grep -qiE "malleable|flipping" "$TMP/help" || fail "help should warn about malleability" +} + +run_all diff --git a/cli/tests/test_aes_ecb.sh b/cli/tests/test_aes_ecb.sh new file mode 100755 index 00000000..57d9ee52 --- /dev/null +++ b/cli/tests/test_aes_ecb.sh @@ -0,0 +1,275 @@ +#!/usr/bin/env bash +# The aes128-ecb / aes192-ecb / aes256-ecb subcommands, end to end through the binary. +# +# Framing: none. ECB writes no IV, so output is exactly as long as input in both directions, and +# with nothing to vary it `encrypt` is reproducible -- which is why the SP 800-38A Appendix F.1 +# vectors can be pinned in both directions here, and why the help text warns against using ECB for +# data. Neither direction applies padding, so input must be a whole number of 16-byte blocks. Keys +# and data come from `bc-rust rng`; the fixed inputs are the F.1 vectors and, for the cross-mode +# guard, the F.2.1 CBC vector. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# One subcommand per key length: `ecb 128` prints "aes128-ecb". +ecb() { echo "aes$1-ecb"; } + +# ---- known answers -------------------------------------------------------------------------- + +# SP 800-38A Appendix F.1: the four plaintext blocks, the three keys, and the F.1.1 / F.1.3 / +# F.1.5 ciphertexts (F.1.2 / F.1.4 / F.1.6 are the same pairs decrypted). Transcribed in the Rust +# suite this file replaces. +F1_PLAINTEXT=6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710 +F1_KEY_128=2b7e151628aed2a6abf7158809cf4f3c +F1_KEY_192=8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b +F1_KEY_256=603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4 +F1_CT_128=3ad77bb40d7a3660a89ecaf32466ef97f5d3d58503b9699de785895a96fdbaaf43b1cd7f598ece23881b00e3ed0306887b0c785e27e8ad3f8223207104725dd4 +F1_CT_192=bd334f1d6e45f25ff712a214571fa5cc974104846d0ad3ad7734ecb3ecee4eefef7afd2270e2e60adce0ba2face6444e9a4b41ba738d6c72fb16691603c18e0e +F1_CT_256=f3eed1bdb5d2a03c064b5a7e3db181f8591ccb10d410ed26dc5ba74a31362870b6ed21b99ca6f4f9f153e7b1beafed1d23304b7a39f9f3ff067d8d8f9e24ecc7 + +# SP 800-38A Appendix F.2.1, CBC-AES128.Encrypt: the IV and ciphertext, for the cross-mode guard. +F2_CBC_IV=000102030405060708090a0b0c0d0e0f +F2_CBC_CT_128=7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b273bed6b8e3c1743b7116e69e222295163ff1caa1681fac09120eca307586e1a7 + +test_both_directions_match_sp800_38a_f1_vectors() { + local bits key ct got + for bits in 128 192 256; do + key="F1_KEY_$bits" + ct="F1_CT_$bits" + got=$(unhex "$F1_PLAINTEXT" | "$BC_RUST" "$(ecb $bits)" -d encrypt --key "${!key}" | "$BC_RUST" hex-encode) + assert_eq "$got" "${!ct}" "$bits: F.1 encrypt vector" + got=$(unhex "${!ct}" | "$BC_RUST" "$(ecb $bits)" -d decrypt --key "${!key}" | "$BC_RUST" hex-encode) + assert_eq "$got" "$F1_PLAINTEXT" "$bits: F.1 decrypt vector" + done +} + +test_hex_output_matches_binary_output() { + local hex_out + unhex "$F1_PLAINTEXT" >"$TMP/pt" + "$BC_RUST" aes128-ecb -d encrypt --key "$F1_KEY_128" <"$TMP/pt" >"$TMP/ct" + hex_out=$("$BC_RUST" aes128-ecb -d encrypt --key "$F1_KEY_128" -x <"$TMP/pt") + assert_eq "$hex_out" "$(hex "$TMP/ct")" "-x must be the hex of the binary output" + assert_eq "$hex_out" "$F1_CT_128" "and both are the F.1.1 ciphertext" +} + +# ---- round trips and framing ---------------------------------------------------------------- + +test_round_trip_through_files_with_no_iv() { + local bits + for bits in 128 192 256; do + rng $((bits / 8)) >"$TMP/key" + rng 1024 >"$TMP/pt" + + "$BC_RUST" "$(ecb $bits)" -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" 1024 "$bits: no IV is written, so the ciphertext is the plaintext length" + assert_differs "$TMP/pt" "$TMP/ct" "$bits: the data must actually be encrypted" + + "$BC_RUST" "$(ecb $bits)" -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$bits: decrypt must recover the plaintext" + done +} + +test_round_trip_through_a_pipe_larger_than_the_pipe_buffer() { + rng 16 >"$TMP/key" + rng $((4 * 1024 * 1024)) >"$TMP/pt" + "$BC_RUST" aes128-ecb -d encrypt --key-file "$TMP/key" <"$TMP/pt" \ + | "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "4 MiB must survive encrypt | decrypt with no file in between" +} + +# Sizes that straddle the 1 KiB streaming chunk, the four-block batch and the block boundary: 128 +# is two fours; 144 is two fours plus one block; 1040 is a chunk plus a block. +test_round_trips_across_chunk_and_batch_boundaries() { + local size + rng 16 >"$TMP/key" + for size in 16 32 128 144 1024 1040 4096 4112 65536; do + rng "$size" >"$TMP/pt" + "$BC_RUST" aes128-ecb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" "$size" "$size bytes: ciphertext length" + "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$size bytes should round trip" + done +} + +test_hex_output_composes_through_hex_decode() { + rng 16 >"$TMP/key" + rng 4096 >"$TMP/pt" + "$BC_RUST" aes128-ecb -d encrypt --key-file "$TMP/key" -x <"$TMP/pt" >"$TMP/ct.hex" + assert_size "$TMP/ct.hex" $((2 * 4096 + 1)) "-x emits two hex characters per byte, then a newline" + "$BC_RUST" hex-decode <"$TMP/ct.hex" \ + | "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "-x output must decrypt after hex-decode" +} + +test_empty_input_produces_empty_output() { + rng 16 >"$TMP/key" + : >"$TMP/empty" + "$BC_RUST" aes128-ecb -d encrypt --key-file "$TMP/key" <"$TMP/empty" >"$TMP/ct" + assert_size "$TMP/ct" 0 "no IV to emit: an empty message encrypts to nothing" + "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/key" <"$TMP/empty" >"$TMP/rec" + assert_size "$TMP/rec" 0 "no IV to require: an empty ciphertext decrypts to nothing" +} + +# ---- the codebook property, visible on the wire --------------------------------------------- + +# SP 800-38A Sec 6.1: the same plaintext block under the same key always gives the same +# ciphertext block. Across invocations the output is identical (no IV to vary it), and within a +# message equal blocks stay equal -- the reason the help text warns against using ECB for data. +test_ecb_is_deterministic_and_shows_repeated_blocks() { + rng 16 >"$TMP/key" + unhex 00112233445566778899aabbccddeeffffeeddccbbaa9988776655443322110000112233445566778899aabbccddeeff >"$TMP/pt" + "$BC_RUST" aes128-ecb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct1" + "$BC_RUST" aes128-ecb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct2" + assert_same "$TMP/ct1" "$TMP/ct2" "the same input gives the same output every time" + slice "$TMP/ct1" 0 16 >"$TMP/c1" + slice "$TMP/ct1" 16 16 >"$TMP/c2" + slice "$TMP/ct1" 32 16 >"$TMP/c3" + assert_same "$TMP/c1" "$TMP/c3" "equal plaintext blocks give equal ciphertext blocks" + assert_differs "$TMP/c1" "$TMP/c2" "different plaintext blocks give different ciphertext blocks" +} + +# ---- keys ----------------------------------------------------------------------------------- + +test_key_file_accepts_hex_and_binary() { + local form + printf '%s' "$F1_KEY_128" >"$TMP/key.hex" + unhex "$F1_KEY_128" >"$TMP/key.bin" + unhex "$F1_CT_128" >"$TMP/ct" + unhex "$F1_PLAINTEXT" >"$TMP/pt" + for form in key.hex key.bin; do + "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/$form" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "--key-file $form must decrypt the F.1.1 ciphertext" + done +} + +test_a_key_of_the_wrong_length_is_rejected() { + unhex "$F1_PLAINTEXT" >"$TMP/pt" + expect_fail "a 16-byte key is not an AES-256 key" \ + "$BC_RUST" aes256-ecb -d encrypt --key "$F1_KEY_128" <"$TMP/pt" + assert_stderr_has "32-byte key" + assert_stderr_has "16 bytes" +} + +test_a_missing_key_is_rejected() { + unhex "$F1_PLAINTEXT" >"$TMP/pt" + expect_fail "neither --key nor --key-file" \ + "$BC_RUST" aes128-ecb -d encrypt <"$TMP/pt" + assert_stderr_has "\-\-key" +} + +# A rejected key must still be reported when stdin is far larger than any pipe buffer: the +# command exits before reading, and the harness must not hang on the write. +test_a_large_payload_on_an_error_path_is_still_rejected() { + rng $((4 * 1024 * 1024)) >"$TMP/pt" + expect_fail "no key, 4 MiB of stdin" \ + "$BC_RUST" aes128-ecb -d encrypt <"$TMP/pt" + assert_stderr_has "\-\-key" +} + +test_an_all_zero_key_warns_but_proceeds() { + unhex "$F1_PLAINTEXT" >"$TMP/pt" + expect_ok "an all-zero key is accepted" \ + "$BC_RUST" aes128-ecb -d encrypt --key "$(printf '0%.0s' {1..32})" <"$TMP/pt" >"$TMP/ct" + assert_stderr_has "arning" + assert_size "$TMP/ct" 64 "four ciphertext blocks and no IV" +} + +# ---- rejected inputs ------------------------------------------------------------------------ + +test_unaligned_input_is_rejected_in_both_directions() { + local extra direction + rng 16 >"$TMP/key" + for extra in 1 7 15; do + rng $((32 + extra)) >"$TMP/data" + for direction in encrypt decrypt; do + expect_fail "$((32 + extra)) bytes is not a whole number of blocks ($direction)" \ + "$BC_RUST" aes128-ecb -d "$direction" --key-file "$TMP/key" <"$TMP/data" + assert_stderr_has "whole number of 16-byte blocks" + assert_stderr_has "ECB" + done + done +} + +# ---- SP 800-38A Appendix D, through the CLI ------------------------------------------------- + +# Table D.2 for ECB: a bit error in `Cj` gives "RBE in the decryption of Cj" -- random bit errors +# in that block -- and Appendix D adds that ECB bit errors "do not affect the decryption of any +# other blocks". So the corrupted block is randomised and every other block is intact. This is +# also an end-to-end check that the CLI is running ECB and not CBC (where the next block would +# show the flipped bit) or CFB (where the same block would). +test_a_ciphertext_bit_flip_randomises_only_its_own_block() { + unhex "$F1_PLAINTEXT" >"$TMP/pt" + unhex "$F1_CT_128" >"$TMP/ct" + flip_byte "$TMP/ct" $((16 + 3)) 32 # bit 5 of byte 3 of C2 + "$BC_RUST" aes128-ecb -d decrypt --key "$F1_KEY_128" <"$TMP/ct" >"$TMP/rec" + assert_size "$TMP/rec" 64 "the length is unchanged" + + local i + for i in 0 32 48; do + slice "$TMP/pt" $i 16 >"$TMP/want" + slice "$TMP/rec" $i 16 >"$TMP/got" + assert_same "$TMP/want" "$TMP/got" "the block at $i is unaffected: nothing chains" + done + slice "$TMP/pt" 16 16 >"$TMP/p2" + slice "$TMP/rec" 16 16 >"$TMP/got" + assert_differs "$TMP/p2" "$TMP/got" "P2 must change" + flip_byte "$TMP/p2" 3 32 + assert_differs "$TMP/p2" "$TMP/got" "P2 must be randomised, not flipped in place as CBC would" +} + +# ---- cross-variant and cross-mode behaviour ------------------------------------------------- + +test_a_wrong_key_does_not_recover_the_plaintext() { + rng 16 >"$TMP/key" + rng 16 >"$TMP/other" + rng 256 >"$TMP/pt" + "$BC_RUST" aes128-ecb -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + # ECB is unauthenticated: a wrong key succeeds and produces garbage of the same length. + "$BC_RUST" aes128-ecb -d decrypt --key-file "$TMP/other" <"$TMP/ct" >"$TMP/rec" + assert_differs "$TMP/pt" "$TMP/rec" "a different key must not recover the plaintext" + assert_size "$TMP/rec" 256 "but the length is unchanged" +} + +# ECB and CBC ciphertexts are not interchangeable. The CBC command frames an IV and the ECB +# command does not: the CBC body run through ECB is not the plaintext, and the ECB ciphertext run +# through CBC (its first block consumed as an IV) is neither the plaintext nor the right length. +test_ecb_and_cbc_are_not_interchangeable() { + unhex "$F1_PLAINTEXT" >"$TMP/pt" + unhex "$F1_CT_128" >"$TMP/ecb_ct" + unhex "$F2_CBC_CT_128" >"$TMP/cbc_body" + unhex "$F2_CBC_IV$F2_CBC_CT_128" >"$TMP/cbc_input" + + "$BC_RUST" aes128-ecb -d decrypt --key "$F1_KEY_128" <"$TMP/ecb_ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "ECB decrypts its own ciphertext" + "$BC_RUST" aes128-cbc -d decrypt --key "$F1_KEY_128" <"$TMP/cbc_input" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "CBC decrypts its own ciphertext" + + "$BC_RUST" aes128-ecb -d decrypt --key "$F1_KEY_128" <"$TMP/cbc_body" >"$TMP/rec" + assert_differs "$TMP/pt" "$TMP/rec" "ECB must not decrypt a CBC ciphertext" + + "$BC_RUST" aes128-cbc -d decrypt --key "$F1_KEY_128" <"$TMP/ecb_ct" >"$TMP/rec" + assert_size "$TMP/rec" 48 "CBC consumes the first block as an IV" + tail -c 48 "$TMP/pt" >"$TMP/pt_tail" + assert_differs "$TMP/pt_tail" "$TMP/rec" "CBC must not decrypt an ECB ciphertext" +} + +# ---- discoverability ------------------------------------------------------------------------ + +test_the_subcommands_are_listed_in_help() { + local cmd + "$BC_RUST" --help >"$TMP/help" + for cmd in aes128-ecb aes192-ecb aes256-ecb; do + grep -q "$cmd" "$TMP/help" || fail "--help should list $cmd" + done +} + +# Each subcommand's own help names the two directions, says there is no IV, and carries the +# warning that ECB is not for data. +test_per_command_help_warns_and_documents_the_missing_iv() { + local word + "$BC_RUST" aes128-ecb --help >"$TMP/help" + for word in encrypt decrypt "NO IV" WARNING ECB; do + grep -q "$word" "$TMP/help" || fail "aes128-ecb --help should mention '$word'" + done +} + +run_all diff --git a/cli/tests/test_aes_gcm.sh b/cli/tests/test_aes_gcm.sh new file mode 100755 index 00000000..ca718ca5 --- /dev/null +++ b/cli/tests/test_aes_gcm.sh @@ -0,0 +1,235 @@ +#!/usr/bin/env bash +# The aes128-gcm / aes192-gcm / aes256-gcm subcommands, end to end through the binary. +# +# Framing: encrypt writes a fresh 12-byte nonce, then the ciphertext (as long as the plaintext), +# then the 16-byte tag; decrypt reads the same layout back. `--aad` (hex) or `--aad-file` (raw +# bytes) is authenticated but not encrypted and must match on both sides. GCM's algorithm +# correctness is pinned by the known-answer suites in the aes crate; what is tested here is the +# wiring: that AAD reaches the tag, that tampering is rejected with a non-zero exit, and that +# decrypt still writes whatever plaintext it had released before the failure. Keys, AAD and data +# all come from `bc-rust rng`. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +NONCE_LEN=12 +TAG_LEN=16 + +# One subcommand per key length: `gcm 128` prints "aes128-gcm". +gcm() { echo "aes$1-gcm"; } + +# ---- round trips -------------------------------------------------------------------------- + +test_round_trip_with_aad_at_every_key_length() { + local bits aad + for bits in 128 192 256; do + rng "$(keylen $bits)" >"$TMP/key" + rng 8 >"$TMP/aad" + aad=$(hex "$TMP/aad") + rng 69 >"$TMP/pt" + + "$BC_RUST" "$(gcm $bits)" -d encrypt --key-file "$TMP/key" --aad "$aad" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((69 + NONCE_LEN + TAG_LEN)) "$bits: nonce, ciphertext and tag" + "$BC_RUST" "$(gcm $bits)" -d decrypt --key-file "$TMP/key" --aad "$aad" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$bits: round trip" + done +} + +test_round_trips_with_no_aad() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "AAD is optional on both sides" +} + +test_any_input_length_is_accepted_and_round_trips() { + rng 16 >"$TMP/key" + local len + for len in $(seq 0 33); do + rng "$len" >"$TMP/pt" + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((len + NONCE_LEN + TAG_LEN)) "len $len: nonce, equal-length body, tag" + "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "len $len: round trip" + done +} + +test_round_trips_across_chunk_boundaries() { + rng 16 >"$TMP/key" + local size + for size in 0 1 15 16 17 1023 1024 1025 4096 4099 65536; do + rng "$size" >"$TMP/pt" + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" \ + | "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" --aad deadbeef >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "$size bytes should round trip through a pipe" + done +} + +test_each_invocation_uses_a_fresh_nonce() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + local i + : >"$TMP/nonces" + for i in $(seq 1 8); do + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + head -c $NONCE_LEN "$TMP/ct" | "$BC_RUST" hex-encode >>"$TMP/nonces" + echo >>"$TMP/nonces" + "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "run $i still decrypts" + done + assert_eq "$(sort -u "$TMP/nonces" | wc -l)" 8 "eight encryptions must draw eight distinct nonces" +} + +test_hex_output_composes_through_hex_decode() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" -x <"$TMP/pt" >"$TMP/ct.hex" + assert_size "$TMP/ct.hex" $((2 * (69 + NONCE_LEN + TAG_LEN) + 1)) "-x emits two hex characters per byte, then a newline" + "$BC_RUST" hex-decode <"$TMP/ct.hex" \ + | "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "-x output must decrypt after hex-decode" +} + +# ---- AAD ------------------------------------------------------------------------------------ + +test_wrong_aad_fails_authentication() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" + expect_fail "a different AAD must not verify" \ + "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" --aad 00112233 <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +test_missing_aad_on_one_side_fails_authentication() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" + expect_fail "AAD on encrypt but none on decrypt must not verify" \ + "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +# `--aad-file` is raw bytes, never hex-or-raw guessed like `--key-file`: the file's exact bytes +# are what any other GCM implementation would authenticate. Each case encrypts with the file and +# decrypts with `--aad` set to the hex of those bytes. +test_aad_file_is_raw_bytes_not_hex_decoded() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + # ASCII that is also valid hex text: hex-decoding would authenticate 2 bytes, not 4. + printf 'cafe' >"$TMP/ascii_hex" + # Sixteen zero bytes, which the hex decoder skips entirely: decoding would authenticate nothing. + head -c 16 /dev/zero >"$TMP/zeros" + # A trailing backslash, which once sent the hex decoder's `\x` handling past the end. + printf 'header\\' >"$TMP/trailing_backslash" + + local name + for name in ascii_hex zeros trailing_backslash; do + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" --aad-file "$TMP/$name" <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" --aad "$(hex "$TMP/$name")" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "case $name: the file's bytes are the AAD" + done +} + +# ---- tamper detection ----------------------------------------------------------------------- + +test_a_tampered_ciphertext_byte_is_rejected() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" + flip_byte "$TMP/ct" $NONCE_LEN + expect_fail "a flipped ciphertext bit must not verify" \ + "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +test_a_tampered_tag_byte_is_rejected() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" + flip_byte "$TMP/ct" $(($(wc -c <"$TMP/ct") - 1)) + expect_fail "a flipped tag bit must not verify" \ + "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +# The streaming trade-off: on a tag failure, whatever plaintext the decryptor had already released +# before the tag check stands on stdout. For a message longer than the tag that is everything but +# at most the last 16 bytes, and it must be the genuine plaintext, not garbage. The exit code is +# the signal a script must check. +test_decrypt_still_writes_the_plaintext_it_had_released_on_forgery() { + rng 16 >"$TMP/key" + rng 4096 >"$TMP/pt" + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/pt" >"$TMP/ct" + flip_byte "$TMP/ct" $(($(wc -c <"$TMP/ct") - 1)) # the tag only; the body is intact + if "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" --aad deadbeef <"$TMP/ct" >"$TMP/out" 2>/dev/null; then + fail "a corrupted tag must be rejected" + fi + local released + released=$(wc -c <"$TMP/out") + [ "$released" -ge $((4096 - TAG_LEN)) ] \ + || fail "most of the plaintext should already have reached stdout: got $released of 4096 bytes" + cmp -s -n "$released" "$TMP/out" "$TMP/pt" || fail "the released bytes must be the genuine plaintext" +} + +# ---- short input ---------------------------------------------------------------------------- + +test_decrypt_input_shorter_than_the_nonce_is_rejected() { + rng 16 >"$TMP/key" + local len + for len in 0 1 11; do + rng "$len" >"$TMP/short" + expect_fail "$len bytes cannot hold a 12-byte nonce" \ + "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" <"$TMP/short" + assert_stderr_has "12-byte nonce" + done +} + +# A nonce but not a full tag: there is nothing to check the tag against, so it is an +# authentication failure. +test_decrypt_input_with_a_nonce_but_no_full_tag_is_rejected() { + rng 16 >"$TMP/key" + : >"$TMP/empty" + "$BC_RUST" aes128-gcm -d encrypt --key-file "$TMP/key" <"$TMP/empty" >"$TMP/ct" + assert_size "$TMP/ct" $((NONCE_LEN + TAG_LEN)) "an empty message encrypts to nonce || tag" + head -c $((NONCE_LEN + TAG_LEN - 1)) "$TMP/ct" >"$TMP/short" + expect_fail "one tag byte short must not verify" \ + "$BC_RUST" aes128-gcm -d decrypt --key-file "$TMP/key" <"$TMP/short" + assert_stderr_has "authentication failed" +} + +# ---- keys ----------------------------------------------------------------------------------- + +test_a_key_of_the_wrong_length_is_rejected() { + rng 16 >"$TMP/key" + rng 69 >"$TMP/pt" + expect_fail "a 16-byte key is not an AES-256 key" \ + "$BC_RUST" aes256-gcm -d encrypt --key-file "$TMP/key" <"$TMP/pt" + assert_stderr_has "32-byte key" + assert_stderr_has "16 bytes" +} + +test_a_missing_key_is_rejected() { + rng 69 >"$TMP/pt" + expect_fail "neither --key nor --key-file" \ + "$BC_RUST" aes128-gcm -d encrypt <"$TMP/pt" + assert_stderr_has -- "--key" +} + +# ---- discoverability ------------------------------------------------------------------------ + +test_the_subcommands_are_listed_in_help() { + "$BC_RUST" --help >"$TMP/help" + local cmd + for cmd in aes128-gcm aes192-gcm aes256-gcm; do + grep -q "$cmd" "$TMP/help" || fail "--help should list $cmd" + done +} + +test_per_command_help_documents_aad_and_authentication() { + "$BC_RUST" aes128-gcm --help >"$TMP/help" + grep -q "aad" "$TMP/help" || fail "help should mention AAD" + grep -qi "authenticat" "$TMP/help" || fail "help should mention authentication" +} + +run_all diff --git a/cli/tests/test_all.sh b/cli/tests/test_all.sh new file mode 100755 index 00000000..a117d41d --- /dev/null +++ b/cli/tests/test_all.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Builds the CLI, then runs every cli/tests/test_*.sh against target/debug/bc-rust and reports +# the totals. +# +# cli/tests/test_all.sh +# +# Each test file is independent and exits non-zero if any of its tests failed; this script only +# counts files. Set BC_RUST to test a binary somewhere else, in which case nothing is built. + +set -u + +TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$TESTS_DIR/../.." && pwd)" + +if [ -z "${BC_RUST:-}" ]; then + echo "=== cargo build -p cli" + cargo build -p cli --manifest-path "$REPO_ROOT/Cargo.toml" || exit 2 + echo +fi + +passed=0 +failed=0 +failed_files=() + +for file in "$TESTS_DIR"/test_*.sh; do + [ "$(basename "$file")" = "test_all.sh" ] && continue + echo "=== $(basename "$file")" + if bash "$file"; then + passed=$((passed + 1)) + else + failed=$((failed + 1)) + failed_files+=("$(basename "$file")") + fi + echo +done + +echo "test files: $passed passed, $failed failed" +if [ "$failed" -ne 0 ]; then + printf ' failed: %s\n' "${failed_files[@]}" + exit 1 +fi diff --git a/cli/tests/test_ascon.sh b/cli/tests/test_ascon.sh new file mode 100755 index 00000000..c3a6f575 --- /dev/null +++ b/cli/tests/test_ascon.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# The ascon-hash256 / ascon-xof128 / ascon-cxof128 / ascon-aead128 subcommands, end to end +# through the binary. +# +# Framing for the AEAD: encrypt writes a fresh 16-byte nonce as the first bytes of its output +# unless `--nonce` supplies one (in which case the nonce is not written), the 16-byte tag rides +# at the end of the ciphertext, and `--ad` is authenticated but not encrypted. Keys, nonces and +# data come from `bc-rust rng`; the fixed inputs are the NIST LWC known-answer values already +# pinned in `crypto/ascon/tests/*.rs`, copied from the Rust suite this file replaces. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +NONCE_LEN=16 +TAG_LEN=16 + +# The NIST LWC AEAD KAT convention uses key == nonce for the embedded vector. +KAT_KEY=000102030405060708090a0b0c0d0e0f + +# ---- ascon-hash256 ------------------------------------------------------------------------ + +# LWC_HASH_KAT_256.txt Count 1: the digest of the empty message. +test_hash256_matches_the_kat_for_the_empty_message() { + local got + got=$(hex_out "$BC_RUST" ascon-hash256 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" ascon-hash256 -x <"$TMP/msg") + assert_eq "$got" "b88e497ae8e6fb641b87ef622eb8f2fca0ed95383f7ffebe167acf1099ba764f" "8-byte message digest" +} + +# ---- ascon-xof128 -------------------------------------------------------------------------- + +# LWC_XOF_KAT_128_512.txt Count 1: 64 bytes squeezed after absorbing the empty message. +test_xof128_matches_the_kat_for_the_empty_message() { + local got + got=$(hex_out "$BC_RUST" ascon-xof128 64 -x "$TMP/msg" + got=$(hex_out "$BC_RUST" ascon-cxof128 64 --customization 10 -x <"$TMP/msg") + assert_eq "$got" \ + "63fa8ba86382f2d544580f51322d080424b42c556eb74503cd73cf052bb993bd6f5210984c71c9c445f43ccc5b158226e509bd339cd634414377f79411aa8d5c" \ + "customized squeeze" +} + +# No `--customization` at all must give the same output as an empty one: the CLI's optional +# argument must treat "absent" and "empty" identically. +test_cxof128_with_no_customization_matches_an_empty_one() { + local without with_empty + without=$(hex_out "$BC_RUST" ascon-cxof128 64 -x "$TMP/key.bin" + unhex $KAT_KEY >"$TMP/nonce.bin" + got=$(hex_out "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key.bin" --nonce-file "$TMP/nonce.bin" -x "$TMP/key" + rng 4096 >"$TMP/pt" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + assert_size "$TMP/ct" $((4096 + NONCE_LEN + TAG_LEN)) "ciphertext is nonce plus plaintext plus tag" + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "decrypt must recover the plaintext" +} + +test_aead128_associated_data_round_trips() { + rng 16 >"$TMP/key" + rng 256 >"$TMP/pt" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" --ad deadbeef <"$TMP/pt" >"$TMP/ct" + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --ad deadbeef <"$TMP/ct" >"$TMP/rec" + assert_same "$TMP/pt" "$TMP/rec" "the same AD on both sides must round-trip" +} + +test_aead128_each_invocation_uses_a_fresh_nonce() { + rng 16 >"$TMP/key" + rng 32 >"$TMP/pt" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct1" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct2" + assert_size "$TMP/ct1" $((32 + NONCE_LEN + TAG_LEN)) "first ciphertext length" + assert_size "$TMP/ct2" $((32 + NONCE_LEN + TAG_LEN)) "second ciphertext length" + head -c $NONCE_LEN "$TMP/ct1" >"$TMP/n1" + head -c $NONCE_LEN "$TMP/ct2" >"$TMP/n2" + assert_differs "$TMP/n1" "$TMP/n2" "two encryptions must draw different nonces" +} + +# ---- ascon-aead128: rejected inputs -------------------------------------------------------- + +test_aead128_wrong_associated_data_is_rejected() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" --ad deadbeef <"$TMP/pt" >"$TMP/ct" + expect_fail "different AD must fail the tag check" \ + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --ad cafebabe <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +test_aead128_a_flipped_ciphertext_byte_is_rejected() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + flip_byte "$TMP/ct" $NONCE_LEN + expect_fail "a flipped ciphertext byte must fail the tag check" \ + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +test_aead128_a_flipped_tag_byte_is_rejected() { + rng 16 >"$TMP/key" + rng 64 >"$TMP/pt" + "$BC_RUST" ascon-aead128 -d encrypt --key-file "$TMP/key" <"$TMP/pt" >"$TMP/ct" + flip_byte "$TMP/ct" $(($(wc -c <"$TMP/ct") - 1)) + expect_fail "a flipped tag byte must fail the tag check" \ + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" <"$TMP/ct" + assert_stderr_has "authentication failed" +} + +# Decrypt input shorter than the generated 16-byte nonce is rejected before any tag check is +# attempted, including the empty-input case. +test_aead128_decrypt_input_shorter_than_the_nonce_is_rejected() { + local len + rng 16 >"$TMP/key" + for len in 0 1 15; do + rng "$len" >"$TMP/short" + expect_fail "$len bytes cannot hold a 16-byte nonce" \ + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" <"$TMP/short" + assert_stderr_has "shorter than the 16-byte nonce" + done +} + +# With `--nonce` supplied there is no nonce in the stream, so the floor is the tag instead. +test_aead128_explicit_nonce_decrypt_input_shorter_than_the_tag_is_rejected() { + local len + rng 16 >"$TMP/key" + rng 16 >"$TMP/nonce" + for len in 0 1 15; do + rng "$len" >"$TMP/short" + expect_fail "$len bytes cannot hold a 16-byte tag" \ + "$BC_RUST" ascon-aead128 -d decrypt --key-file "$TMP/key" --nonce-file "$TMP/nonce" <"$TMP/short" + assert_stderr_has "shorter than the 16-byte tag" + done +} + +# ---- help ----------------------------------------------------------------------------------- + +test_the_subcommands_are_listed_in_help() { + local name + "$BC_RUST" --help >"$TMP/help" + for name in ascon-hash256 ascon-xof128 ascon-cxof128 ascon-aead128; do + grep -q -- "$name" "$TMP/help" || fail "--help should list $name" + done +} + +run_all diff --git a/cli/tests/test_hkdf.sh b/cli/tests/test_hkdf.sh new file mode 100755 index 00000000..fcd81330 --- /dev/null +++ b/cli/tests/test_hkdf.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# The hkdf-* subcommands, end to end through the binary. HKDF has no inverse to round-trip +# through, so each variant is pinned to one published known answer, and its file inputs must give +# the same output as the same values passed as hex. +# +# The known answers: HKDF-SHA256 is RFC 5869 Appendix A.1; HKDF-SHA384 and HKDF-SHA512 are +# tcId 11 of Wycheproof's testvectors_v1/hkdf_sha384_test.json and hkdf_sha512_test.json. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# ---- known answers ------------------------------------------------------------------------ + +test_hkdf_sha256_matches_rfc5869_a1() { + local got + got=$(hex_out "$BC_RUST" hkdf-sha256 \ + --salt 000102030405060708090a0b0c \ + --ikm 0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b \ + --additional-input f0f1f2f3f4f5f6f7f8f9 \ + --len 42 -x) + assert_eq "$got" \ + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865" \ + "RFC 5869 A.1 OKM" +} + +test_hkdf_sha384_matches_wycheproof_tc11() { + local got + got=$(hex_out "$BC_RUST" hkdf-sha384 \ + --salt 2614d80275b08a1cf90bae0eb607d4d5 \ + --ikm 0cbd136d66d15a4ffefde1303b430821 \ + --additional-input ee991de21aeb6baa6a5f683dbb755e6f80db1c1d \ + --len 42 -x) + assert_eq "$got" \ + "e618b91d9f3d10c007958d025841a3347947eb41b23ec35a3d7927aad74f293c50405a56911d8158e74f" \ + "Wycheproof hkdf_sha384 tcId 11 OKM" +} + +test_hkdf_sha512_matches_wycheproof_tc11() { + local got + got=$(hex_out "$BC_RUST" hkdf-sha512 \ + --salt 2614d80275b08a1cf90bae0eb607d4d5 \ + --ikm 0cbd136d66d15a4ffefde1303b430821 \ + --additional-input ee991de21aeb6baa6a5f683dbb755e6f80db1c1d \ + --len 42 -x) + assert_eq "$got" \ + "e51c3bfe5f4e9b4fb0d3c3a67bb33a20c288800e03707621cf143e8581d422dfec3fe658ba8fa2e35c2c" \ + "Wycheproof hkdf_sha512 tcId 11 OKM" +} + +# ---- file inputs -------------------------------------------------------------------------- + +# `hkdf_files_match_hex SUBCOMMAND` derives from random inputs passed once as hex and once as +# binary files, and requires the same output. +hkdf_files_match_hex() { + local sub=$1 from_hex from_files + rng 32 >"$TMP/salt" + rng 32 >"$TMP/ikm" + rng 16 >"$TMP/info" + from_hex=$(hex_out "$BC_RUST" "$sub" --salt "$(hex "$TMP/salt")" --ikm "$(hex "$TMP/ikm")" \ + --additional-input "$(hex "$TMP/info")" --len 100 -x) + from_files=$(hex_out "$BC_RUST" "$sub" -s "$TMP/salt" -i "$TMP/ikm" -a "$TMP/info" --len 100 -x) + assert_eq "${#from_hex}" 200 "$sub output length" + assert_eq "$from_files" "$from_hex" "$sub: file inputs must give the hex inputs' output" +} + +test_hkdf_sha256_files_match_hex() { + hkdf_files_match_hex hkdf-sha256 +} + +test_hkdf_sha384_files_match_hex() { + hkdf_files_match_hex hkdf-sha384 +} + +test_hkdf_sha512_files_match_hex() { + hkdf_files_match_hex hkdf-sha512 +} + +# ---- output length ------------------------------------------------------------------------ + +# `hkdf_short_output_is_a_prefix SUBCOMMAND LEN`: RFC 5869's output is the first L octets of the +# expanded stream, so `--len LEN` must be a prefix of `--len 100`. The lengths used are one octet +# past a whole block, which the library once refused. +hkdf_short_output_is_a_prefix() { + local sub=$1 len=$2 long short + long=$(hex_out "$BC_RUST" "$sub" --salt 00 --ikm 0b0b0b0b --additional-input 01 --len 100 -x) + short=$(hex_out "$BC_RUST" "$sub" --salt 00 --ikm 0b0b0b0b --additional-input 01 --len "$len" -x) + assert_eq "$short" "${long:0:$((2 * len))}" "$sub --len $len" +} + +test_hkdf_sha256_one_octet_past_a_block() { + hkdf_short_output_is_a_prefix hkdf-sha256 33 +} + +test_hkdf_sha384_one_octet_past_a_block() { + hkdf_short_output_is_a_prefix hkdf-sha384 49 +} + +test_hkdf_sha512_one_octet_past_a_block() { + hkdf_short_output_is_a_prefix hkdf-sha512 65 +} + +run_all diff --git a/cli/tests/test_hmac.sh b/cli/tests/test_hmac.sh new file mode 100755 index 00000000..a932f783 --- /dev/null +++ b/cli/tests/test_hmac.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# The hmac-* subcommands, end to end through the binary: for each variant, a MAC computed over +# random data under a random key must verify with `-v`, and must stop verifying when the tag, the +# message or the key changes. `-v` reports through the exit status alone. + +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +# `hmac_round_trip SUBCOMMAND TAG_LEN` runs the round trip for one variant. +hmac_round_trip() { + local sub=$1 tag_len=$2 tag + rng 32 >"$TMP/key" + rng 32 >"$TMP/other_key" + rng 100 >"$TMP/msg" + cp "$TMP/msg" "$TMP/other_msg" + flip_byte "$TMP/other_msg" 50 + + tag=$(hex_out "$BC_RUST" "$sub" -k "$TMP/key" -x <"$TMP/msg") + assert_eq "${#tag}" $((2 * tag_len)) "$sub tag length" + + expect_ok "the tag must verify" "$BC_RUST" "$sub" -k "$TMP/key" -v "$tag" <"$TMP/msg" + expect_fail "a modified tag must not verify" \ + "$BC_RUST" "$sub" -k "$TMP/key" -v "$(flip_hex "$tag" 0 1)" <"$TMP/msg" + expect_fail "a modified message must not verify" \ + "$BC_RUST" "$sub" -k "$TMP/key" -v "$tag" <"$TMP/other_msg" + expect_fail "another key must not verify" \ + "$BC_RUST" "$sub" -k "$TMP/other_key" -v "$tag" <"$TMP/msg" +} + +test_hmac_sha256_round_trip() { + hmac_round_trip hmac-sha256 32 +} + +test_hmac_sha512_round_trip() { + hmac_round_trip hmac-sha512 64 +} + +test_hmac_sha512_224_round_trip() { + hmac_round_trip hmac-sha512-224 28 +} + +test_hmac_sha512_256_round_trip() { + hmac_round_trip hmac-sha512-256 32 +} + +test_hmac_sm3_round_trip() { + hmac_round_trip hmac-sm3 32 +} + +run_all diff --git a/crypto/aes/Cargo.toml b/crypto/aes/Cargo.toml new file mode 100644 index 00000000..1bd18ab0 --- /dev/null +++ b/crypto/aes/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bouncycastle-aes" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true +bouncycastle-cipher.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +bouncycastle-rng.workspace = true +criterion.workspace = true + +[[bench]] +name = "aes_benches" +harness = false + +[[bench]] +name = "aes_modes_benches" +harness = false diff --git a/crypto/aes/benches/aes_benches.rs b/crypto/aes/benches/aes_benches.rs new file mode 100644 index 00000000..c1f1838d --- /dev/null +++ b/crypto/aes/benches/aes_benches.rs @@ -0,0 +1,161 @@ +//! Criterion benchmarks for the bit-sliced AES permutation. +//! +//! The comparison that matters here is `encrypt_block` against `encrypt_2blocks` and +//! `encrypt_4blocks` over the same number of bytes. The engine runs on `u16`, `u32` or `u64` +//! bit-planes for one, two or four blocks, and a round costs about the same at every width on a +//! 64-bit machine, so the two- and four-block paths should approach twice and four times the +//! throughput of the single-block one; what they achieve in practice is what these benches record +//! (on x86-64: 1.75x and 3.0x for encryption, 1.95x and 3.7x for decryption). That multiplier is +//! the argument for modes of operation using the batched entry points wherever their blocks are +//! independent (CTR, ECB, and the decrypt direction of CBC and CFB). +//! +//! The data benches work in place on one buffer across iterations, so a `clone` never sits inside +//! the timed closure. The permutation is a bijection, so the buffer stays random whichever +//! direction ran last, and the contents never influence the timing of a constant-time cipher. + +use bouncycastle_aes::AES_BLOCK_LEN; +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::RNG; +use bouncycastle_rng as rng; +use criterion::measurement::WallTime; +use criterion::{BenchmarkGroup, Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +/// 16 KiB of data, i.e. 1024 AES blocks. +const NUM_BLOCKS: usize = 1024; +const DATA_LEN: usize = NUM_BLOCKS * AES_BLOCK_LEN; + +fn random_blocks() -> Vec<[u8; AES_BLOCK_LEN]> { + let mut blocks = vec![[0u8; AES_BLOCK_LEN]; NUM_BLOCKS]; + let mut generator = rng::DefaultRNG::default(); + for block in blocks.iter_mut() { + generator.next_bytes_out(block).unwrap(); + } + blocks +} + +fn key() -> KeyMaterial { + let mut bytes = [0u8; N]; + rng::DefaultRNG::default().next_bytes_out(&mut bytes).unwrap(); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +fn bench_key_expansion(c: &mut Criterion) { + let mut group = c.benchmark_group("aes::key expansion"); + + let key128 = key::<16>(); + group.throughput(Throughput::Bytes(16)); + group.bench_function("AES128Internal::new()", |b| { + b.iter(|| black_box(AES128Internal::new(black_box(&key128)).unwrap())) + }); + + let key192 = key::<24>(); + group.throughput(Throughput::Bytes(24)); + group.bench_function("AES192Internal::new()", |b| { + b.iter(|| black_box(AES192Internal::new(black_box(&key192)).unwrap())) + }); + + let key256 = key::<32>(); + group.throughput(Throughput::Bytes(32)); + group.bench_function("AES256Internal::new()", |b| { + b.iter(|| black_box(AES256Internal::new(black_box(&key256)).unwrap())) + }); + + group.finish(); +} + +/// The six data benches every key length gets: 16 KiB through the one-, two- and four-block +/// entry points, in each direction. +fn bench_data_paths>( + group: &mut BenchmarkGroup<'_, WallTime>, + aes: &C, +) { + let mut blocks = random_blocks(); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x1024", |b| { + b.iter(|| { + for block in blocks.iter_mut() { + aes.encrypt_block(black_box(block)); + } + black_box(&blocks); + }) + }); + + group.bench_function("16KiB -- .encrypt_2blocks() x512", |b| { + b.iter(|| { + for pair in blocks.chunks_exact_mut(2) { + // `try_into` cannot fail: `chunks_exact_mut(2)` yields slices of length 2. + let pair: &mut [[u8; AES_BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.encrypt_2blocks(black_box(pair)); + } + black_box(&blocks); + }) + }); + + group.bench_function("16KiB -- .encrypt_4blocks() x256", |b| { + b.iter(|| { + for four in blocks.chunks_exact_mut(4) { + // `try_into` cannot fail: `chunks_exact_mut(4)` yields slices of length 4. + let four: &mut [[u8; AES_BLOCK_LEN]; 4] = four.try_into().unwrap(); + aes.encrypt_4blocks(black_box(four)); + } + black_box(&blocks); + }) + }); + + group.bench_function("16KiB -- .decrypt_block() x1024", |b| { + b.iter(|| { + for block in blocks.iter_mut() { + aes.decrypt_block(black_box(block)); + } + black_box(&blocks); + }) + }); + + group.bench_function("16KiB -- .decrypt_2blocks() x512", |b| { + b.iter(|| { + for pair in blocks.chunks_exact_mut(2) { + let pair: &mut [[u8; AES_BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.decrypt_2blocks(black_box(pair)); + } + black_box(&blocks); + }) + }); + + group.bench_function("16KiB -- .decrypt_4blocks() x256", |b| { + b.iter(|| { + for four in blocks.chunks_exact_mut(4) { + let four: &mut [[u8; AES_BLOCK_LEN]; 4] = four.try_into().unwrap(); + aes.decrypt_4blocks(black_box(four)); + } + black_box(&blocks); + }) + }); +} + +fn bench_aes128(c: &mut Criterion) { + let aes = AES128Internal::new(&key::<16>()).unwrap(); + let mut group = c.benchmark_group("aes::AES128Internal"); + bench_data_paths(&mut group, &aes); + group.finish(); +} + +fn bench_aes192(c: &mut Criterion) { + let aes = AES192Internal::new(&key::<24>()).unwrap(); + let mut group = c.benchmark_group("aes::AES192Internal"); + bench_data_paths(&mut group, &aes); + group.finish(); +} + +fn bench_aes256(c: &mut Criterion) { + let aes = AES256Internal::new(&key::<32>()).unwrap(); + let mut group = c.benchmark_group("aes::AES256Internal"); + bench_data_paths(&mut group, &aes); + group.finish(); +} + +criterion_group!(benches, bench_key_expansion, bench_aes128, bench_aes192, bench_aes256); +criterion_main!(benches); diff --git a/crypto/aes/benches/aes_modes_benches.rs b/crypto/aes/benches/aes_modes_benches.rs new file mode 100644 index 00000000..40abd3ef --- /dev/null +++ b/crypto/aes/benches/aes_modes_benches.rs @@ -0,0 +1,1150 @@ +//! Criterion benchmarks for the modes. +//! +//! The number to watch is the **decrypt/encrypt throughput ratio at N >= 2**. Encryption in both +//! CBC and CFB is serial by construction (SP 800-38A Sec 6.2 and Sec 6.3: each forward cipher input +//! depends on the previous output), so it can only ever use the single-block path. *Decryption* in +//! both is parallel, and this implementation hands blocks to the permutation's batch methods -- +//! fours first, then pairs, then the remainder singly: for CBC that is `decrypt_4blocks` / +//! `decrypt_2blocks`, for CFB it is `encrypt_4blocks` / `encrypt_2blocks`, since CFB uses the +//! forward function in both directions. AES's natural unit is a pair, so its fours are two +//! pairs. With the bit-sliced AES, whose two-block path costs barely more than one block, +//! decryption should therefore run at roughly twice the throughput of encryption. That gap is the +//! entire justification for the batch methods on `ElectronicCodeBook`, so if it disappears, +//! something has stopped taking the pair path. +//! +//! `N = 1` is included to show the effect vanishing: with one block there is no pair to form, so +//! decryption falls back to the single-block path and the ratio should be about 1. +//! +//! CFB is a stream cipher (`StreamCipherEncryptor` / `StreamCipherDecryptor`), so `N` there is +//! simply the call length in blocks; the same 16 KiB goes through `do_encrypt_inplace` / +//! `do_decrypt_inplace` as `16 * N`-byte slices. Two extra CFB measurements use calls that are +//! *not* a whole number of blocks: every such call ends mid-segment and the next one starts by +//! finishing it byte by byte, so they show what the byte path costs relative to the block path at +//! a comparable call length. +//! +//! The `modes::cfb8::AES_128` group measures the other thing worth knowing about CFB8: it spends one +//! full forward cipher per *byte*, so on a 16-byte block it should come out at roughly **1/16** the +//! throughput of CFB over the same 16 KiB. That ratio, against `modes::cfb::AES_128`, is the number +//! to watch; it is inherent to `s = 8` (Sec 6.3 discards `b - s` bits of every output block), not a +//! property of this implementation. Decryption should still beat encryption, because CFB8 +//! decryption builds its input blocks in series and then batches the ciphers four at a time while +//! encryption cannot. +//! +//! The `modes::gcm::AES_128` group is GCM against CTR: GCM is CTR plus GHASH over the ciphertext +//! and AAD (SP 800-38D Sec 7.1), and its CTR half batches exactly as `modes::ctr::AES_128` does, so +//! the difference between the two groups' 16 KiB encrypt figures is the cost of the table-free +//! GF(2^128) multiply, one per block. The AAD-only measurement is GMAC (Sec 5.2), which is that +//! multiply with no cipher calls at all beyond the two for `H` and `J0`. +//! +//! The cipher works in place, so each measurement runs on a fresh copy of the data made in +//! criterion's untimed setup (`iter_batched`); the copy is not part of the timing. +//! +//! The `modes::cbc::AES_128` and `modes::cfb::AES_128` groups are directly comparable -- same cipher, +//! same data, same call granularity -- so the difference between them is the cost of the mode. CFB +//! never calls the inverse cipher, so on an engine whose inverse is slower than its forward +//! direction, CFB decryption is expected to come out ahead of CBC decryption. + +use bouncycastle_aes::hazmat::{AES128Internal, AES256Internal}; +use bouncycastle_cipher::modes::hazmat::Ecb; +use bouncycastle_cipher::modes::{Cbc, Ccm, CcmEncryptor, Cfb, Cfb8, Ctr, GCM_NONCE_LEN, Gcm}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, BlockCipherDecryptor, + BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +const BLOCK_LEN: usize = 16; +/// 16 KiB, i.e. 1024 AES blocks. +const NUM_BLOCKS: usize = 1024; +const DATA_LEN: usize = NUM_BLOCKS * BLOCK_LEN; + +type Aes128Cbc = Cbc; +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 takes its frame size at compile time. Its one-shots are not bound by it, +/// but using the same 4 KiB message keeps this comparison representative of the public alias a +/// packet protocol would choose. +const CCM_BUFFER_LEN: usize = 4096; +const CCM_AAD_LEN: usize = 64; +type Aes128CcmEncryptor = CcmEncryptor< + AES128Internal, + 16, + BLOCK_LEN, + CCM_NONCE_LEN, + CCM_TAG_LEN, + CCM_AAD_LEN, + CCM_BUFFER_LEN, +>; +/// GCM with the full 16-byte tag, as the `AES_GCM_*` aliases fix it. +const GCM_TAG_LEN: usize = 16; +type Aes128Gcm = Gcm; +type Aes256Gcm = Gcm; +type Aes128Ctr = Ctr; +type Aes256Ctr = Ctr; +type Aes128Ecb = Ecb; + +/// AES-128 with the batch methods implemented as single-block loops instead of AES's bit-sliced +/// pair. +/// +/// This exists purely to isolate the value of the pair path. Comparing `Cbc` against +/// `Cbc` at the *same* `N` holds everything else fixed -- same cipher, same +/// call granularity, same amount of data movement -- so the difference is attributable to +/// `decrypt_2blocks` and nothing else. +/// +/// Comparing `N = 1` against `N = 8` does *not* isolate it: encryption, which can never pair, also +/// speeds up substantially between those two, so call granularity dominates that comparison. +struct UnpairedAes128(AES128Internal); + +impl Algorithm for UnpairedAes128 { + const ALG_NAME: &'static str = "AES-128 (unpaired)"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl ElectronicCodeBook<16, BLOCK_LEN> for UnpairedAes128 { + fn new(key: &KeyMaterial<16>) -> Result { + Ok(Self(>::new(key)?)) + } + fn encrypt_block(&self, block: &mut [u8; BLOCK_LEN]) { + >::encrypt_block(&self.0, block) + } + fn decrypt_block(&self, block: &mut [u8; BLOCK_LEN]) { + >::decrypt_block(&self.0, block) + } + // Deliberately single-block loops, as a cipher with no unit wider than a block would write + // them, bypassing AES's bit-sliced pair. + fn encrypt_2blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { + for block in blocks.iter_mut() { + self.encrypt_block(block); + } + } + fn decrypt_2blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { + for block in blocks.iter_mut() { + self.decrypt_block(block); + } + } + fn encrypt_4blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 4]) { + for block in blocks.iter_mut() { + self.encrypt_block(block); + } + } + fn decrypt_4blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 4]) { + for block in blocks.iter_mut() { + self.decrypt_block(block); + } + } +} + +type UnpairedAes128Cbc = Cbc; +type UnpairedAes128Cfb = Cfb; +type UnpairedAes128Ecb = Ecb; + +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).unwrap() +} + +fn data() -> Vec<[u8; BLOCK_LEN]> { + (0..NUM_BLOCKS) + .map(|i| core::array::from_fn(|j| (i.wrapping_mul(31).wrapping_add(j)) as u8)) + .collect() +} + +fn bench_aes128(c: &mut Criterion) { + let k = key::<16>(); + let blocks = data(); + + let mut group = c.benchmark_group("modes::cbc::AES_128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + // ---- encryption: serial, one block at a time is all it can do ---- + group.bench_function("16KiB encrypt -- N=1", |b| { + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); + for block in scratch.iter_mut() { + enc.do_encrypt_inplace(block).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB encrypt -- N=8", |b| { + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + enc.do_encrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // ---- decryption: parallel, uses decrypt_2blocks for every pair ---- + let (mut enc, iv) = Aes128Cbc::::do_encrypt_init(&k).unwrap(); + let mut ciphertext = blocks.clone(); + for chunk in ciphertext.chunks_exact_mut(8) { + enc.do_encrypt_blocks_inplace(chunk).unwrap(); + } + + // N=1 never forms a pair, so this is the single-block path: the ratio against encrypt should + // be about 1. + group.bench_function("16KiB decrypt -- N=1 (no pairing)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for block in scratch.iter_mut() { + dec.do_decrypt_inplace(block).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // N=2 is one pair and N=8 two fours (four pairs, for AES), so every block goes through + // decrypt_2blocks. + group.bench_function("16KiB decrypt -- N=2 (all pairs)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(2) { + let arr: &mut [u8; 2 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB decrypt -- N=8 (all fours)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // N=9 is four pairs plus a one-block remainder, so it exercises the tail path too. + group.bench_function("16KiB decrypt -- N=9 (pairs + remainder)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(9) { + let arr: &mut [u8; 9 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // The controlled comparison: identical N, identical cipher, bit-sliced pair vs single-block loops. + // This pair of numbers -- and only this pair -- measures what `decrypt_2blocks` buys. + group.bench_function("16KiB decrypt -- N=8, pair path (2blocks overridden)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB decrypt -- N=8, no pair path (single-block loops)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = UnpairedAes128Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +fn bench_aes256(c: &mut Criterion) { + let k = key::<32>(); + let blocks = data(); + + let mut group = c.benchmark_group("modes::cbc::AES_256"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB encrypt -- N=8", |b| { + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes256Cbc::::do_encrypt_init(&k).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + enc.do_encrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + let (mut enc, iv) = Aes256Cbc::::do_encrypt_init(&k).unwrap(); + let mut ciphertext = blocks.clone(); + for chunk in ciphertext.chunks_exact_mut(8) { + enc.do_encrypt_blocks_inplace(chunk).unwrap(); + } + + group.bench_function("16KiB decrypt -- N=8 (all fours)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes256Cbc::::do_decrypt_init(&k, &iv).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +/// Runs the 16 KiB through a stream-cipher encryptor in `call_len`-byte calls. Used by the CFB, +/// CFB8 and CTR groups: it is generic over the trait, not over the mode. +fn cfb_encrypt_in_calls< + E: StreamCipherEncryptor, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, +>( + k: &KeyMaterial, + scratch: &mut [u8], + call_len: usize, +) { + let (mut enc, _) = E::do_encrypt_init(k).unwrap(); + for piece in scratch.chunks_mut(call_len) { + enc.do_encrypt_inplace(piece).unwrap(); + } +} + +/// Runs the 16 KiB through a stream-cipher decryptor in `call_len`-byte calls. Shared as above. +fn cfb_decrypt_in_calls< + D: StreamCipherDecryptor, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, +>( + k: &KeyMaterial, + iv: &[u8; INIT_DATA_LEN], + scratch: &mut [u8], + call_len: usize, +) { + let mut dec = D::do_decrypt_init(k, iv).unwrap(); + for piece in scratch.chunks_mut(call_len) { + dec.do_decrypt_inplace(piece).unwrap(); + } +} + +fn bench_cfb_aes128(c: &mut Criterion) { + let k = key::<16>(); + let blocks = data(); + let flat: Vec = blocks.as_flattened().to_vec(); + + let mut group = c.benchmark_group("modes::cfb::AES_128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + // ---- encryption: serial. Oj+1 = CIPH_K(Cj), and Cj is the previous call's output ---- + for (name, call_len) in [ + ("16KiB encrypt -- N=1", BLOCK_LEN), + ("16KiB encrypt -- N=8", 8 * BLOCK_LEN), + // 125 bytes: 7 blocks and 13 bytes, so every call finishes the segment the previous one + // left open, then does whole blocks, then opens a new segment. Compare with N=8. + ("16KiB encrypt -- 125-byte calls (byte path at both ends)", 125), + ] { + group.bench_function(name, |b| { + b.iter_batched( + || flat.clone(), + |mut scratch| { + cfb_encrypt_in_calls::, 16, BLOCK_LEN>( + &k, &mut scratch, call_len, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + } + + // ---- decryption: parallel, and uses `encrypt_4blocks` / `encrypt_2blocks` -- the FORWARD + // batch methods ---- + let (mut enc, iv) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); + let mut ciphertext = flat.clone(); + enc.do_encrypt_inplace(&mut ciphertext).unwrap(); + + for (name, call_len) in [ + // N=1 never forms a pair, so this is the single-block path: the ratio against encrypt + // should be about 1. + ("16KiB decrypt -- N=1 (no pairing)", BLOCK_LEN), + // N=2 and N=8 are all batches (N=8 two fours), so every block goes through a batch method. + ("16KiB decrypt -- N=2 (all pairs)", 2 * BLOCK_LEN), + ("16KiB decrypt -- N=8 (all fours)", 8 * BLOCK_LEN), + // N=9 is two fours plus a one-block remainder, so it exercises the tail path too. + ("16KiB decrypt -- N=9 (pairs + remainder)", 9 * BLOCK_LEN), + // As for encryption: 7 blocks plus 13 bytes per call. Compare with N=8. + ("16KiB decrypt -- 125-byte calls (byte path at both ends)", 125), + ] { + group.bench_function(name, |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + cfb_decrypt_in_calls::, 16, BLOCK_LEN>( + &k, &iv, &mut scratch, call_len, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + } + + // The controlled comparison: identical N, identical cipher, bit-sliced pair vs single-block loops. + // This pair of numbers -- and only this pair -- measures what `encrypt_2blocks` buys CFB. + group.bench_function("16KiB decrypt -- N=8, pair path (2blocks overridden)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + cfb_decrypt_in_calls::, 16, BLOCK_LEN>( + &k, + &iv, + &mut scratch, + 8 * BLOCK_LEN, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB decrypt -- N=8, no pair path (single-block loops)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + cfb_decrypt_in_calls::, 16, BLOCK_LEN>( + &k, + &iv, + &mut scratch, + 8 * BLOCK_LEN, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +fn bench_cfb_aes256(c: &mut Criterion) { + let k = key::<32>(); + let flat: Vec = data().as_flattened().to_vec(); + + let mut group = c.benchmark_group("modes::cfb::AES_256"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB encrypt -- N=8", |b| { + b.iter_batched( + || flat.clone(), + |mut scratch| { + cfb_encrypt_in_calls::, 32, BLOCK_LEN>( + &k, + &mut scratch, + 8 * BLOCK_LEN, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + let (mut enc, iv) = Aes256Cfb::::do_encrypt_init(&k).unwrap(); + let mut ciphertext = flat.clone(); + enc.do_encrypt_inplace(&mut ciphertext).unwrap(); + + group.bench_function("16KiB decrypt -- N=8 (all fours)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + cfb_decrypt_in_calls::, 32, BLOCK_LEN>( + &k, + &iv, + &mut scratch, + 8 * BLOCK_LEN, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +/// CFB8: one forward cipher per byte, so ~1/16 of CFB's throughput on a 16-byte block. +/// +/// Encryption is strictly serial. Decryption builds its input blocks in series and then runs them +/// through `encrypt_4blocks` / `encrypt_2blocks` (SP 800-38A Sec 6.3's parallel decryption), so it +/// should be substantially faster than encryption -- the same batch effect CBC and CFB show, at +/// byte granularity. +fn bench_cfb8_aes128(c: &mut Criterion) { + let k = key::<16>(); + let flat: Vec = data().as_flattened().to_vec(); + + let mut group = c.benchmark_group("modes::cfb8::AES_128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + // Serial by construction: I_{j+1} needs Cj, which this call just produced. + group.bench_function("16KiB encrypt -- whole message in one call", |b| { + b.iter_batched( + || flat.clone(), + |mut scratch| { + cfb_encrypt_in_calls::, 16, BLOCK_LEN>( + &k, &mut scratch, DATA_LEN, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + let (mut enc, iv) = Aes128Cfb8::::do_encrypt_init(&k).unwrap(); + let mut ciphertext = flat.clone(); + enc.do_encrypt_inplace(&mut ciphertext).unwrap(); + + for (name, call_len) in [ + // One call: fours, then pairs, then the tail. This is the batched path. + ("16KiB decrypt -- whole message in one call (batched)", DATA_LEN), + // 8-byte calls: exactly two four-block batches per call. + ("16KiB decrypt -- 8-byte calls (two batches each)", 8), + // 1-byte calls: never batches, so this is the cost of the serial path on the decrypt side + // and the controlled comparison for what batching buys. + ("16KiB decrypt -- 1-byte calls (no batching)", 1), + ] { + group.bench_function(name, |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + cfb_decrypt_in_calls::, 16, BLOCK_LEN>( + &k, &iv, &mut scratch, call_len, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + } + + group.finish(); +} + +/// CTR: the only mode here whose **encryption** is parallel too. +/// +/// Counter blocks depend on nothing but the nonce and the index (SP 800-38A Sec 6.5), so unlike CBC +/// and CFB there is no serial direction: encryption should show the same `N >= 2` speed-up that only +/// decryption shows for the feedback modes, and the two directions should measure the same, since +/// they are the same operation. That symmetry is the number to watch here. +fn bench_ctr_aes128(c: &mut Criterion) { + let k = key::<16>(); + let flat: Vec = data().as_flattened().to_vec(); + + let mut group = c.benchmark_group("modes::ctr::AES_128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + for (name, call_len) in [ + // N=1 never forms a pair: the single-block path, and the baseline for the batch effect. + ("16KiB encrypt -- N=1 (no batching)", BLOCK_LEN), + ("16KiB encrypt -- N=2 (all pairs)", 2 * BLOCK_LEN), + ("16KiB encrypt -- N=8 (two fours per call)", 8 * BLOCK_LEN), + // Calls that are not a whole number of blocks, so each end goes byte by byte. + ("16KiB encrypt -- 125-byte calls (byte path at both ends)", 125), + ] { + group.bench_function(name, |b| { + b.iter_batched( + || flat.clone(), + |mut scratch| { + cfb_encrypt_in_calls::, 16, 12>( + &k, &mut scratch, call_len, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + } + + let (mut enc, nonce) = Aes128Ctr::::do_encrypt_init(&k).unwrap(); + let mut ciphertext = flat.clone(); + enc.do_encrypt_inplace(&mut ciphertext).unwrap(); + + for (name, call_len) in [ + ("16KiB decrypt -- N=1 (no batching)", BLOCK_LEN), + ("16KiB decrypt -- N=8 (two fours per call)", 8 * BLOCK_LEN), + ] { + group.bench_function(name, |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + cfb_decrypt_in_calls::, 16, 12>( + &k, &nonce, &mut scratch, call_len, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + } + + group.finish(); +} + +/// AES-256 CTR, for the same key-length comparison the other modes carry. +fn bench_ctr_aes256(c: &mut Criterion) { + let k = key::<32>(); + let flat: Vec = data().as_flattened().to_vec(); + + let mut group = c.benchmark_group("modes::ctr::AES_256"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB encrypt -- N=8", |b| { + b.iter_batched( + || flat.clone(), + |mut scratch| { + cfb_encrypt_in_calls::, 32, 12>( + &k, + &mut scratch, + 8 * BLOCK_LEN, + ); + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +/// ECB has no chaining, so *both* directions batch (SP 800-38A Sec 6.1: forward and inverse +/// cipher functions "can be computed in parallel"). Encryption should therefore show the same +/// N >= 2 speed-up that only decryption shows for CBC and CFB, and the encrypt/decrypt gap should be +/// just the permutation's own forward/inverse cost difference. +fn bench_ecb_aes128(c: &mut Criterion) { + let k = key::<16>(); + let blocks = data(); + + let mut group = c.benchmark_group("modes::ecb::AES_128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB encrypt -- N=1 (no batching)", |b| { + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes128Ecb::::do_encrypt_init(&k).unwrap(); + for block in scratch.iter_mut() { + enc.do_encrypt_inplace(block).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB encrypt -- N=8 (fours)", |b| { + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes128Ecb::::do_encrypt_init(&k).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + enc.do_encrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB decrypt -- N=8 (fours)", |b| { + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let mut dec = Aes128Ecb::::do_decrypt_init(&k, &[]).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // The controlled comparison: identical N, identical cipher, bit-sliced pair vs single-block loops. + group.bench_function("16KiB encrypt -- N=8, no pair path (single-block loops)", |b| { + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = UnpairedAes128Ecb::::do_encrypt_init(&k).unwrap(); + for chunk in scratch.chunks_exact_mut(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + enc.do_encrypt_inplace(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +/// `do_*_init` includes a key expansion, and for encryption also an IV draw from the OS-backed +/// DRBG. Worth its own measurement, because for short messages it dominates. +fn bench_init(c: &mut Criterion) { + let k128 = key::<16>(); + let k256 = key::<32>(); + let iv = [0u8; BLOCK_LEN]; + + let mut group = c.benchmark_group("modes::init"); + + group.bench_function("AES_128 do_encrypt_init (key schedule + IV)", |b| { + b.iter(|| black_box(Aes128Cbc::::do_encrypt_init(black_box(&k128)).unwrap().1)) + }); + group.bench_function("AES_128 do_decrypt_init (key schedule only)", |b| { + b.iter(|| { + black_box(Aes128Cbc::::do_decrypt_init(black_box(&k128), &iv).unwrap()) + }) + }); + group.bench_function("AES_256 do_decrypt_init (key schedule only)", |b| { + b.iter(|| { + black_box(Aes256Cbc::::do_decrypt_init(black_box(&k256), &iv).unwrap()) + }) + }); + + // CFB does exactly the same work here -- one key expansion, plus an IV draw when encrypting -- + // so these should match the CBC numbers. A divergence would mean one mode is doing something + // extra at construction time. + group.bench_function("AES_128 do_encrypt_init, CFB (key schedule + IV)", |b| { + b.iter(|| black_box(Aes128Cfb::::do_encrypt_init(black_box(&k128)).unwrap().1)) + }); + group.bench_function("AES_128 do_decrypt_init, CFB (key schedule only)", |b| { + b.iter(|| { + black_box(Aes128Cfb::::do_decrypt_init(black_box(&k128), &iv).unwrap()) + }) + }); + + 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_out( + 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_out(&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_out( + 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_out( + 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_out( + 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's provided one-shot runs the streaming adapter over one 4 KiB frame and 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_detached_rng_out 4KiB", |b| { + b.iter_batched_ref( + || [0u8; CCM_BUFFER_LEN], + |out| { + black_box( + Aes128CcmEncryptor::encrypt_detached_rng_out( + 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_out 4KiB", |b| { + b.iter_batched_ref( + || [0u8; CCM_BUFFER_LEN], + |out| { + black_box( + Aes128CcmEnc::encrypt_detached_out( + black_box(&key), + &nonce, + &no_aad, + black_box(&data), + out, + ) + .unwrap(), + ) + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +/// GCM over the same 16 KiB as the CTR and CCM groups; see the module docs for what to compare +/// it against. The nonce comes from a cheap deterministic RNG created outside the timed loop, so +/// the figures measure the mode and not OS entropy or DRBG construction. +fn bench_gcm_aes128(c: &mut Criterion) { + let key = key::<16>(); + let data = [0xA5u8; DATA_LEN]; + let no_aad: [u8; 0] = []; + let mut rng = FixedSeedRNG::::new([0x24u8; GCM_NONCE_LEN]); + + let mut group = c.benchmark_group("modes::gcm::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( + Aes128Gcm::::encrypt_detached_rng_out( + black_box(&key), + &mut rng, + &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 (nonce, _, tag) = Aes128Gcm::::encrypt_detached_rng_out( + &key, &mut rng, &no_aad, &data, &mut ciphertext, + ) + .unwrap(); + + // The one-shot verifies the tag before it decrypts (SP 800-38D Sec 7's preamble permits the + // reordering), so this is GHASH over the whole ciphertext and then CTR over it: the same work + // as encryption in the other order. + group.bench_function("decrypt 16KiB, no AAD", |b| { + b.iter_batched_ref( + || [0u8; DATA_LEN], + |out| { + black_box( + Aes128Gcm::::decrypt_detached_out( + black_box(&key), + &nonce, + &no_aad, + black_box(&ciphertext), + &tag, + out, + ) + .unwrap(), + ) + }, + BatchSize::LargeInput, + ) + }); + + // The same payload with 16 KiB of AAD alongside it: one extra GHASH multiply per AAD block + // and no extra cipher calls, so the increment over the no-AAD case is the GHASH half alone. + group.bench_function("encrypt 16KiB with 16KiB AAD", |b| { + b.iter_batched_ref( + || [0u8; DATA_LEN], + |out| { + black_box( + Aes128Gcm::::encrypt_detached_rng_out( + black_box(&key), + &mut rng, + black_box(&data), + black_box(&data), + out, + ) + .unwrap(), + ) + }, + BatchSize::LargeInput, + ) + }); + + // AAD only: GMAC (Sec 5.2). GHASH over 16 KiB plus the two cipher calls for `H` and `J0`, + // which is the GHASH cost on its own and the number the CTR comparison needs. + group.bench_function("authenticate 16KiB AAD, empty payload (GMAC)", |b| { + b.iter(|| { + let mut out: [u8; 0] = []; + black_box( + Aes128Gcm::::encrypt_detached_rng_out( + black_box(&key), + &mut rng, + black_box(&data), + &no_aad, + &mut out, + ) + .unwrap(), + ) + }) + }); + + // Streaming in 128-byte pieces, the same call length as the N=8 CTR measurement, so the + // per-call overhead of the AEAD streaming path shows against the one-shot above. + group.bench_function("encrypt 16KiB -- N=8 streaming", |b| { + b.iter_batched( + || data.to_vec(), + |plaintext| { + let (mut enc, _nonce) = + Aes128Gcm::::do_encrypt_init_rng(black_box(&key), &mut rng) + .unwrap(); + let mut out = [0u8; 8 * BLOCK_LEN]; + for piece in plaintext.chunks(8 * BLOCK_LEN) { + enc.do_encrypt_out(piece, &mut out).unwrap(); + black_box(&out); + } + black_box(enc.do_encrypt_final().unwrap()) + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +/// AES-256 GCM, for the same key-length comparison the other modes carry. +fn bench_gcm_aes256(c: &mut Criterion) { + let key = key::<32>(); + let data = [0xA5u8; DATA_LEN]; + let no_aad: [u8; 0] = []; + let mut rng = FixedSeedRNG::::new([0x24u8; GCM_NONCE_LEN]); + + let mut group = c.benchmark_group("modes::gcm::AES_256"); + 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( + Aes256Gcm::::encrypt_detached_rng_out( + black_box(&key), + &mut rng, + &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_ccm_aes128, + bench_ccm_one_shot_pair, bench_gcm_aes128, bench_gcm_aes256, bench_init +); +criterion_main!(benches); diff --git a/crypto/aes/src/bitslice.rs b/crypto/aes/src/bitslice.rs new file mode 100644 index 00000000..dbf2fb6d --- /dev/null +++ b/crypto/aes/src/bitslice.rs @@ -0,0 +1,480 @@ +//! Conversion between AES blocks and the bit-sliced representation the round functions act on. +//! +//! # What "bit-sliced" means here +//! +//! The round functions in [`crate::round`] and the S-box in [`crate::sbox`] do not operate on +//! bytes. They operate on eight *bit-planes*, `q[0]..q[7]`, where plane `q[k]` collects bit `k` +//! of every byte of the state. That is what lets the S-box be a Boolean circuit: one `&` or `^` +//! on a plane applies that gate to all sixteen byte positions at once, and no memory access is +//! ever indexed by a secret value. +//! +//! # The plane width is the number of blocks +//! +//! A block is 16 bytes, so a plane needs 16 bits per block. The planes are therefore generic +//! over their word type: `u16` planes hold one block, `u32` planes hold two and `u64` planes hold +//! four, and [`PlaneWord`] is the trait over those three widths. Block `b` occupies bits +//! `16b..16b + 16` of every plane -- its own 16-bit **lane** -- so a wider state is literally +//! several one-block states side by side, and every transformation written for one width serves +//! all three. The extra blocks come for free: the S-box circuit costs the same 113 gates on a +//! `u64` as on a `u16`, which is why [`crate::hazmat::AESInternal`] gives four blocks for the price of one. +//! +//! # The layout +//! +//! Within a block's lane, **bit `4r + c` of plane `q[k]` is bit `k` of `s[r,c]`**, with `r` and +//! `c` the row and column of FIPS 197 Eq (3.6) (`s[r,c] = in[r + 4c]`). Row `r` is the nibble +//! `r` of the lane, and the column is the bit within that nibble: +//! +//! ```text +//! c=0 c=1 c=2 c=3 +//! r=0 | 0 1 2 3 +//! r=1 | 4 5 6 7 (bit position within the lane; +//! r=2 | 8 9 10 11 add 16b for block b) +//! r=3 | 12 13 14 15 +//! ``` +//! +//! Three things follow from this, and every mask in the crate is derived from one of them: +//! +//! * SHIFTROWS(), which only permutes within a row, becomes a rotation *within each nibble*, and +//! MIXCOLUMNS(), which combines the four rows of a column, becomes rotations of each lane by 4 +//! (one row) and 8 (two rows). Both are derived from the table in [`crate::round`]. +//! * A mask is a 16-bit pattern replicated into every lane, which is what +//! [`PlaneWord::splat`] does; a rotation is one applied to every lane independently, which is +//! [`PlaneWord::rotate_lanes_right`]. Those two methods are the only width-specific arithmetic +//! the round functions need. +//! * A round key is the same for every block, so the round key at any width is the one-block +//! `u16` form `splat` into every lane. That is why [`crate::schedule`] stores the schedule at +//! `u16` width and why widening it costs one replication per plane. +//! +//! `test_layout_matches_the_documented_table` below pins the table exhaustively at every width; +//! every mask in this crate is only correct relative to it. +//! +//! # How the transpose produces it +//! +//! [`ortho`] transposes, within each *byte*-lane of the eight words, the 8x8 bit matrix indexed +//! by (word number, bit number within the byte): +//! +//! ```text +//! after ortho: q[k] bit (8L + i) == before ortho: q[i] bit (8L + k) +//! ``` +//! +//! So the byte at byte-lane `L` of word `i` before the transpose lands at bit `8L + i` of every +//! plane after it. Solving `8L + i = 16b + 4r + c` gives `L = 2b + (r div 2)` and +//! `i = 4 (r mod 2) + c`: word `i` must carry, in the two byte-lanes of block `b`'s 16-bit lane, +//! `s[r,c]` with `r = i div 4` and `c = i mod 4` in the low byte and `s[r + 2, c]` in the high +//! byte. [`block_to_words`] makes that placement for one block as eight `u16`s, and each width's +//! [`PlaneWord::pack`] puts every block's words into its lane and transposes all lanes at once. +//! +//! # Provenance +//! +//! The three-stage masked-swap transpose is translated from BearSSL `src/symcipher/aes_ct.c` +//! (`br_aes_ct_ortho`), by Thomas Pornin, MIT licensed. The block placement is not BearSSL's: +//! `aes_ct.c` interleaves its two blocks bit by bit (block A in the even bit positions of every +//! byte-lane, block B in the odd), which ties every mask to one width. Keeping each block in its +//! own lane instead is what lets one set of `u16` patterns serve `u16`, `u32` and `u64` planes. + +use core::ops::{BitAnd, BitOr, BitXor, BitXorAssign, Not, Shl, Shr}; + +/// One 16-byte AES block, in the order of FIPS 197 Eq (3.6): `block[r + 4c] == s[r,c]`. +pub type Block = [u8; crate::AES_BLOCK_LEN]; + +/// The eight bit-planes holding one, two or four blocks, by the width of `T`. See the module +/// docs for the layout. +/// T impls PlaneWord, which is defined for u16 (1 block), u32 (2 blocks), and u64 (4 blocks). +pub(crate) type Planes = [T; 8]; + +/// A plane word: `u16`, `u32` or `u64`, holding one, two or four blocks. +/// +/// The operator bounds are what the S-box circuit and the round functions use; the methods are +/// the width-specific parts, which are exactly the four things that know a block is 16 bits wide. +/// Each width is implemented longhand below rather than through a macro, so that every mask and +/// shift is visible to `cargo mutants` and to a reviewer. +pub(crate) trait PlaneWord: + Copy + + Eq + + core::fmt::Debug + + BitAnd + + BitOr + + BitXor + + BitXorAssign + + Not + + Shl + + Shr +{ + /// The blocks a state of this width holds: `[Block; N]` with `N` the word width divided by + /// 16, so one, two or four. + type Blocks: AsRef<[Block]> + AsMut<[Block]> + Default; + + /// Replicates a 16-bit pattern into every block lane: the mask that applies `pattern` to one + /// block, applied to all of them. + fn splat(pattern: u16) -> Self; + + /// Rotates every 16-bit lane right by `n` bits, each lane independently, for `0 < n < 16`. + /// + /// A whole-word rotation would carry the bottom of one block's lane into the top of the + /// next block's; this keeps each block's bits inside its own lane. The bits that stay inside + /// their lane move down by `n` and are the low `16 - n` bits of it; the `n` bits that would + /// fall out of the bottom of each lane re-enter as its top `n` bits. Each mask also discards + /// what the shift carried in from the neighbouring lane. The two masks are complementary, so + /// the operands are disjoint and `|` and `^` agree here (a known surviving `cargo mutants`). + /// + /// Provided for every width; `u16`, having a single lane, overrides it with the word + /// rotation. + #[inline(always)] + fn rotate_lanes_right(self, n: u32) -> Self { + debug_assert!(0 < n && n < 16); + ((self >> n) & Self::splat(0xFFFF >> n)) + | ((self << (16 - n)) & Self::splat(0xFFFF << (16 - n))) + } + + /// Loads the blocks into bit-planes, block `b` into lane `b`. + fn pack(blocks: &Self::Blocks) -> Planes; + + /// Reads the blocks back out of the bit-planes, in place; the exact inverse of + /// [`PlaneWord::pack`]. Takes the planes mutably so the untranspose needs no copy of them. + fn unpack(q: &mut Planes, blocks: &mut Self::Blocks); +} + +/// The eight pre-transpose words of one block. +/// +/// Word `i` holds `s[r,c]` in its low byte and `s[r + 2, c]` in its high byte, with `r = i div 4` +/// and `c = i mod 4`; after [`ortho`] that puts `s[r,c]` at bit `4r + c`. See the module docs +/// for the derivation. +#[inline(always)] +fn block_to_words(block: &Block) -> [u16; 8] { + core::array::from_fn(|i| { + let (r, c) = (i / 4, i % 4); + u16::from_le_bytes([block[r + 4 * c], block[(r + 2) + 4 * c]]) + }) +} + +/// The inverse of [`block_to_words`]. +#[inline(always)] +fn words_to_block(words: &[u16; 8], block: &mut Block) { + for (i, word) in words.iter().enumerate() { + let (r, c) = (i / 4, i % 4); + let [lo, hi] = word.to_le_bytes(); + block[r + 4 * c] = lo; + block[(r + 2) + 4 * c] = hi; + } +} + +impl PlaneWord for u16 { + type Blocks = [Block; 1]; + + #[inline(always)] + fn splat(pattern: u16) -> Self { + pattern + } + + #[inline(always)] + fn rotate_lanes_right(self, n: u32) -> Self { + // One lane, so this is the word rotation. + self.rotate_right(n) + } + + #[inline(always)] + fn pack(blocks: &[Block; 1]) -> Planes { + let mut q = block_to_words(&blocks[0]); + ortho(&mut q); + q + } + + #[inline(always)] + fn unpack(q: &mut Planes, blocks: &mut [Block; 1]) { + ortho(q); + words_to_block(q, &mut blocks[0]); + } +} + +impl PlaneWord for u32 { + type Blocks = [Block; 2]; + + /// Shift-and-or rather than the equivalent `u32::from(pattern) * 0x0001_0001`, because + /// because integer multiplication is not constant-time on all architectures. + /// A compiler may still emit a multiply where its cost model + /// prefers one, as LLVM does for the `u64` version on x86-64, where `imul` is fixed-latency. + #[inline(always)] + fn splat(pattern: u16) -> Self { + let x = u32::from(pattern); + x | (x << 16) + } + + #[inline(always)] + fn pack(blocks: &[Block; 2]) -> Planes { + let a = block_to_words(&blocks[0]); + let b = block_to_words(&blocks[1]); + // Each block's words go into their own lane: the shifted operands are disjoint, so `|` + // and `^` agree, which is why `cargo mutants` reports the `| -> ^` mutants here (and in + // the `u64` version) as surviving. + let mut q: Planes = + core::array::from_fn(|i| u32::from(a[i]) | (u32::from(b[i]) << 16)); + ortho(&mut q); + q + } + + #[inline(always)] + fn unpack(q: &mut Planes, blocks: &mut [Block; 2]) { + ortho(q); + // `as u16` truncates to the low lane, which is the intent. + words_to_block(&core::array::from_fn(|i| q[i] as u16), &mut blocks[0]); + words_to_block(&core::array::from_fn(|i| (q[i] >> 16) as u16), &mut blocks[1]); + } +} + +impl PlaneWord for u64 { + type Blocks = [Block; 4]; + + /// Shift-and-or equivalent of `u64::from(pattern) * 0x0001_0001_0001_0001`, + /// because integer multiplication is not constant-time on all architectures. + /// A compiler may still emit a multiply where its cost model + /// prefers one, as LLVM does for the `u64` version on x86-64, where `imul` is fixed-latency. + #[inline(always)] + fn splat(pattern: u16) -> Self { + let x = u64::from(pattern); + x | (x << 16) | (x << 32) | (x << 48) + } + + #[inline(always)] + fn pack(blocks: &[Block; 4]) -> Planes { + let a = block_to_words(&blocks[0]); + let b = block_to_words(&blocks[1]); + let c = block_to_words(&blocks[2]); + let d = block_to_words(&blocks[3]); + let mut q: Planes = core::array::from_fn(|i| { + u64::from(a[i]) + | (u64::from(b[i]) << 16) + | (u64::from(c[i]) << 32) + | (u64::from(d[i]) << 48) + }); + ortho(&mut q); + q + } + + #[inline(always)] + fn unpack(q: &mut Planes, blocks: &mut [Block; 4]) { + ortho(q); + // `as u16` truncates to the low lane, which is the intent. + words_to_block(&core::array::from_fn(|i| q[i] as u16), &mut blocks[0]); + words_to_block(&core::array::from_fn(|i| (q[i] >> 16) as u16), &mut blocks[1]); + words_to_block(&core::array::from_fn(|i| (q[i] >> 32) as u16), &mut blocks[2]); + words_to_block(&core::array::from_fn(|i| (q[i] >> 48) as u16), &mut blocks[3]); + } +} + +/// Transposes bytes into bit-planes, and back -- it is its own inverse. +/// +/// Three stages of masked swaps exchange bit-fields of width 1, 2 and 4 between pairs of words, +/// which together transpose the 8x8 bit matrix inside each byte-lane. The width of the words does +/// not matter: the masks are byte patterns replicated across the word, and every byte-lane is +/// transposed at once. See the module docs for the resulting layout. +/// +/// Translated from BearSSL `aes_ct.c:br_aes_ct_ortho` (the `SWAP2`/`SWAP4`/`SWAP8` macros). +pub(crate) fn ortho(q: &mut Planes) { + /// One masked swap: exchanges the `cl`-selected fields of `y` into `x` and the `ch`-selected + /// fields of `x` into `y`, moving them by `s` bit positions. + /// + /// `cl` and `ch` are complementary, and `s` is exactly the field width, so in each returned + /// word the two combined operands occupy disjoint bits: `(x & cl)` and `(y & cl) << s` cannot + /// both be set in the same position. `|` and `^` therefore compute the same function here, + /// which is why `cargo mutants` reports the `| -> ^` mutants in this function as surviving -- + /// they are equivalent programs. `test_ortho_is_an_involution` and + /// `test_layout_matches_the_documented_table` are what actually pin this code. + #[inline(always)] + fn swap(cl: T, ch: T, s: u32, x: T, y: T) -> (T, T) { + ((x & cl) | ((y & cl) << s), ((x & ch) >> s) | (y & ch)) + } + + // Stage 1: swap single bits between adjacent words (0x55 = even bits, 0xAA = odd bits). + for (a, b) in [(0, 1), (2, 3), (4, 5), (6, 7)] { + (q[a], q[b]) = swap(T::splat(0x5555), T::splat(0xAAAA), 1, q[a], q[b]); + } + // Stage 2: swap 2-bit fields between words two apart. + for (a, b) in [(0, 2), (1, 3), (4, 6), (5, 7)] { + (q[a], q[b]) = swap(T::splat(0x3333), T::splat(0xCCCC), 2, q[a], q[b]); + } + // Stage 3: swap nibbles between words four apart. + for (a, b) in [(0, 4), (1, 5), (2, 6), (3, 7)] { + (q[a], q[b]) = swap(T::splat(0x0F0F), T::splat(0xF0F0), 4, q[a], q[b]); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A deterministic byte generator, so the tests do not depend on an RNG crate. + fn pseudo_random_block(seed: u32) -> Block { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + let mut out = [0u8; 16]; + for byte in out.iter_mut() { + // xorshift32; quality is irrelevant, only that it varies every bit position. + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + *byte = (state >> 24) as u8; + } + out + } + + /// One distinct pseudo-random block per lane. + fn pseudo_random_blocks(seed: u32) -> T::Blocks { + let mut blocks = T::Blocks::default(); + for (b, block) in blocks.as_mut().iter_mut().enumerate() { + *block = pseudo_random_block(seed + 1000 * b as u32); + } + blocks + } + + /// The number of blocks a state of width `T` holds. + fn num_blocks() -> usize { + T::Blocks::default().as_ref().len() + } + + /// The mask of block `b`'s 16-bit lane. + fn lane(b: usize) -> T { + // Lane 0 is every lane minus every lane but the first. The 16-bit shift is done in two + // steps of 8, because a shift by 16 is the whole width of a `u16` and would panic. + let lane0 = T::splat(0xFFFF) ^ ((T::splat(0xFFFF) << 8) << 8); + lane0 << (16 * b as u32) + } + + fn check_layout_matches_the_documented_table() { + // Pins the module doc table: bit (16b + 4r + c) of plane k is bit k of s[r,c] of block b. + // Every mask in `round` depends on it. Done one bit at a time -- a block that is zero + // except for bit k of byte j -- so a set bit must land as exactly one bit in exactly one + // plane, which also rules out any leakage between lanes. + for b in 0..num_blocks::() { + for j in 0..16 { + let (r, c) = (j % 4, j / 4); + for k in 0..8 { + let mut blocks = T::Blocks::default(); + blocks.as_mut()[b][j] = 1 << k; + let q = T::pack(&blocks); + let expected = T::splat(1 << (4 * r + c)) & lane::(b); + for (plane, &got) in q.iter().enumerate() { + let want = if plane == k { expected } else { T::splat(0) }; + assert_eq!( + got, want, + "block {b}, byte {j} (r={r}, c={c}), bit {k}: plane {plane}" + ); + } + } + } + } + } + + #[test] + fn test_layout_matches_the_documented_table() { + check_layout_matches_the_documented_table::(); + check_layout_matches_the_documented_table::(); + check_layout_matches_the_documented_table::(); + } + + fn check_ortho_is_an_involution() { + let original = T::pack(&pseudo_random_blocks::(3)); + let mut q = original; + ortho(&mut q); + assert_ne!(q, original, "ortho should actually move bits"); + ortho(&mut q); + assert_eq!(q, original); + } + + #[test] + fn test_ortho_is_an_involution() { + check_ortho_is_an_involution::(); + check_ortho_is_an_involution::(); + check_ortho_is_an_involution::(); + } + + fn check_unpack_inverts_pack() { + for seed in 0..64 { + let blocks = pseudo_random_blocks::(seed); + let mut out = T::Blocks::default(); + T::unpack(&mut T::pack(&blocks), &mut out); + assert_eq!(out.as_ref(), blocks.as_ref()); + } + } + + #[test] + fn test_unpack_inverts_pack() { + check_unpack_inverts_pack::(); + check_unpack_inverts_pack::(); + check_unpack_inverts_pack::(); + } + + fn check_the_blocks_are_independent() { + // Each block's lane must depend on that block alone: packing all the blocks together + // gives, lane by lane, exactly what packing each block on its own gives, and a block + // packed on its own puts nothing outside its lane. This pins that the side-by-side + // placement really is side by side and not overlapping. + let blocks = pseudo_random_blocks::(7); + let all = T::pack(&blocks); + for b in 0..num_blocks::() { + let mut only = T::Blocks::default(); + only.as_mut()[b] = blocks.as_ref()[b]; + let alone = T::pack(&only); + for k in 0..8 { + assert_eq!(all[k] & lane::(b), alone[k], "block {b}, plane {k}"); + assert_eq!(alone[k] & !lane::(b), T::splat(0), "block {b} leaked, plane {k}"); + } + } + } + + #[test] + fn test_the_blocks_are_independent() { + check_the_blocks_are_independent::(); + check_the_blocks_are_independent::(); + check_the_blocks_are_independent::(); + } + + #[test] + fn test_lane_helper_selects_one_lane() { + assert_eq!(lane::(0), 0xFFFF); + assert_eq!(lane::(0), 0x0000_FFFF); + assert_eq!(lane::(1), 0xFFFF_0000); + assert_eq!(lane::(2), 0x0000_FFFF_0000_0000); + assert_eq!(lane::(3), 0xFFFF_0000_0000_0000); + assert_eq!(num_blocks::(), 1); + assert_eq!(num_blocks::(), 2); + assert_eq!(num_blocks::(), 4); + } + + #[test] + fn test_splat_replicates_into_every_lane() { + assert_eq!(u16::splat(0x1234), 0x1234); + assert_eq!(u32::splat(0x1234), 0x1234_1234); + assert_eq!(u64::splat(0x1234), 0x1234_1234_1234_1234); + assert_eq!(u64::splat(0xFFFF), u64::MAX); + assert_eq!(u64::splat(0), 0); + } + + #[test] + fn test_rotate_lanes_right_rotates_each_lane_on_its_own() { + // Rotating the sole lane of a u16 is a word rotation. + assert_eq!(0x1234u16.rotate_lanes_right(4), 0x4123); + assert_eq!(0x1234u16.rotate_lanes_right(8), 0x3412); + // In a wider word, each lane rotates separately: nothing crosses the lane boundary. + assert_eq!(0x1234_5678u32.rotate_lanes_right(4), 0x4123_8567); + assert_eq!(0x1234_5678u32.rotate_lanes_right(8), 0x3412_7856); + assert_eq!(0x1234_5678_9ABC_DEF0u64.rotate_lanes_right(4), 0x4123_8567_C9AB_0DEF); + assert_eq!(0x1234_5678_9ABC_DEF0u64.rotate_lanes_right(8), 0x3412_7856_BC9A_F0DE); + // Amounts 4 and 8 are the ones MIXCOLUMNS() uses, but the contract is any 0 < n < 16. + assert_eq!(0x8001_0001u32.rotate_lanes_right(1), 0xC000_8000); + assert_eq!(0x0001_0001_0001_0001u64.rotate_lanes_right(15), 0x0002_0002_0002_0002); + } + + #[test] + fn test_block_to_words_places_bytes_as_documented() { + // Word i: s[i div 4, i mod 4] in the low byte, s[i div 4 + 2, i mod 4] in the high byte, + // with s[r,c] = block[r + 4c]. + let block: Block = core::array::from_fn(|j| j as u8); + let words = block_to_words(&block); + for (i, &word) in words.iter().enumerate() { + let (r, c) = (i / 4, i % 4); + assert_eq!(word.to_le_bytes(), [(r + 4 * c) as u8, (r + 2 + 4 * c) as u8], "word {i}"); + } + let mut back = [0u8; 16]; + words_to_block(&words, &mut back); + assert_eq!(back, block); + } +} diff --git a/crypto/aes/src/cbc.rs b/crypto/aes/src/cbc.rs new file mode 100644 index 00000000..f853644e --- /dev/null +++ b/crypto/aes/src/cbc.rs @@ -0,0 +1,217 @@ +//! Type aliases for AES in CBC mode (NIST SP 800-38A §6.2), with padding. +//! +//! See [`bouncycastle_cipher::modes::cbc`] for details on the CipherBlockChaining construction. +//! +//! The aliases here are padded block ciphers that accept input of any size; `NoPadding` accepts +//! only whole blocks but goes through the same adapter. The unpadded mode underneath them, which +//! implements the block-cipher traits directly, is [`Cbc`] and is not re-exported from this crate. +//! +//! # Usage Examples +//! +//! ## One-shot API +//! +//! Basic usage can be obtained via the [`SymmetricCipherEncryptor`] and [`SymmetricCipherDecryptor`] API: +//! +//! ``` +//! use bouncycastle_aes::AES_CBC_256; +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_cipher::padding::PKCS7; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CBC_256; +//! type AESDec = AES_CBC_256; +//! +//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! +//! // An arbitrary plaintext to encrypt +//! // Any length: PKCS#7 pads it out to whole blocks, so 50 bytes is as good as 48. +//! let plaintext = [0x5Au8; 50]; +//! +//! // The IV is generated for you and returned; there is no API for supplying one. +//! let (iv, ciphertext) = AESEnc::encrypt(&key, &plaintext).expect("encryption"); +//! assert_eq!(ciphertext.len(), 64, "50 bytes padded out to four blocks"); +//! +//! let recovered = AESDec::decrypt(&key, &iv, &ciphertext).expect("decryption"); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! ## Streaming API +//! +//! For data that arrives in pieces, the following APIs can be used: +//! +//! ``` +//! use bouncycastle_aes::{AES_CBC_128, AES_BLOCK_LEN}; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_cipher::padding::PKCS7; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CBC_128; +//! type AESDec = AES_CBC_128; +//! +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // An arbitrary plaintext to encrypt +//! let plaintext = [0x5Au8; 50]; +//! +//! // The streaming (chunked) API allows for data to be handed to the cipher as it arrives, in chunks +//! // of any length, but it will only be processed once a full block has been received. +//! // Here, we will use 7-byte chunks +//! let (mut encryptor, iv) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! +//! let mut ciphertext = Vec::new(); +//! +//! for piece in plaintext.chunks(7) { +//! let mut out = [0u8; AES_BLOCK_LEN]; +//! let bytes_written = encryptor.do_encrypt_out(piece, &mut out).expect("encryption"); +//! +//! // If that doesn't complete a block, then nothing is written. +//! if bytes_written != 0 { +//! ciphertext.extend_from_slice(&out[..bytes_written]); +//! } +//! } +//! let (last_block, last_len) = encryptor.do_encrypt_final().expect("padding the final block"); +//! ciphertext.extend_from_slice(&last_block[..last_len]); +//! assert_eq!(ciphertext.len(), 64, "50 bytes padded out to four blocks"); +//! +//! // Decrypt the ciphertext in 19-byte chunks. +//! let mut decryptor = AESDec::do_decrypt_init(&key, &iv).expect("decrypt init"); +//! let mut recovered = Vec::new(); +//! for piece in ciphertext.chunks(19) { +//! let mut out = [0u8; AES_BLOCK_LEN]; +//! let bytes_written = decryptor.do_decrypt_out(piece, &mut out).expect("decryption"); +//! if bytes_written != 0 { +//! recovered.extend_from_slice(&out[..bytes_written]); +//! } +//! } +//! let (last_block, last_len) = decryptor.do_decrypt_final().expect("a valid final block"); +//! recovered.extend_from_slice(&last_block[..last_len]); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! ## With no padding scheme +//! +//! With [`NoPadding`] nothing is added, and a message that is not a whole number of blocks is an +//! error at `do_encrypt_final` rather than something silently padded: +//! +//! ``` +//! use bouncycastle_aes::AES_CBC_128; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::SymmetricCipherEncryptor; +//! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_cipher::padding::NoPadding; +//! +//! // Define ourselves a convenience type for the encryption direction with no padding. +//! type Enc = AES_CBC_128; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! +//! // A whole block is fine, and comes out the same length. +//! let mut out = [0u8; 16]; +//! let (_iv, written) = Enc::encrypt_out(&key, &[0u8; 16], &mut out).expect("aligned"); +//! assert_eq!(written, 16); +//! +//! // Five bytes is not, and is refused rather than padded. +//! let mut out = [0u8; 16]; +//! assert!(Enc::encrypt_out(&key, b"hello", &mut out).is_err()); +//! ``` +//! +//! The padding scheme is part of the type, so the two schemes are different types and cannot be +//! interchanged. A value built with one will not satisfy a binding annotated with the other. +//! +//! ```compile_fail +//! use bouncycastle_aes::AES_CBC_128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::SymmetricCipherEncryptor; +//! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! +//! // Built as NoPadding, annotated as PKCS7: mismatched types. +//! let (enc, _iv) = AES_CBC_128::::do_encrypt_init(&key).unwrap(); +//! let _mismatched: AES_CBC_128 = enc; +//! ``` +//! +//! # 🚨 Security Considerations 🚨 +//! +//! All security considerations from [`bouncycastle_cipher::modes::cbc`] apply. + +use crate::AES_BLOCK_LEN; +use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::Direction; +use bouncycastle_cipher::modes::Cbc; +use bouncycastle_cipher::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::modes::cbc; +#[allow(unused_imports)] +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +#[allow(unused_imports)] +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +// end of imports needed for docs + +/// AES-128 in CBC mode with a padding scheme. +#[allow(non_camel_case_types)] +pub type AES_CBC_128 = ::Select< + PaddedBlockCipherEncryptor< + Cbc, + Pad, + 16, + AES_BLOCK_LEN, + AES_BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Cbc, + Pad, + 16, + AES_BLOCK_LEN, + AES_BLOCK_LEN, + >, +>; + +/// AES-192 in CBC mode with a padding scheme. +#[allow(non_camel_case_types)] +pub type AES_CBC_192 = ::Select< + PaddedBlockCipherEncryptor< + Cbc, + Pad, + 24, + AES_BLOCK_LEN, + AES_BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Cbc, + Pad, + 24, + AES_BLOCK_LEN, + AES_BLOCK_LEN, + >, +>; + +/// AES-256 in CBC mode with a padding scheme. See [`AES_CBC_128`]. +#[allow(non_camel_case_types)] +pub type AES_CBC_256 = ::Select< + PaddedBlockCipherEncryptor< + Cbc, + Pad, + 32, + AES_BLOCK_LEN, + AES_BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Cbc, + Pad, + 32, + AES_BLOCK_LEN, + AES_BLOCK_LEN, + >, +>; diff --git a/crypto/aes/src/ccm.rs b/crypto/aes/src/ccm.rs new file mode 100644 index 00000000..90e483d8 --- /dev/null +++ b/crypto/aes/src/ccm.rs @@ -0,0 +1,325 @@ +//! Type aliases for AES in CCM mode (NIST SP 800-38C). +//! +//! See [`bouncycastle_cipher::modes::ccm`] for details on the Counter with CBC-MAC construction. +//! +//! The aliases here are authenticated ciphers: encryption produces a tag as well as a ciphertext, +//! and decryption either returns the plaintext or fails the tag check. `Dir` is [`Encrypting`] or +//! [`Decrypting`]; the wrong direction is a compile error, not a runtime check. +//! +//! There are two families, because CCM must know the payload length before it starts (Sec 3), +//! and the two learn that length from different places: +//! +//! * [`AES_CCM_128`] and friends are told the AAD and payload lengths per message: the one-shots +//! read them off the slices they are given, and the streaming API takes both totals up front in +//! `new` or `new_with_lengths`. The nonce is **supplied**, which CCM permits because it requires +//! the nonce to be unique but not random (Sec 5.3), so a caller with a counter can do better +//! than a draw from a DRBG. +//! * [`AES_CCM_128_Packet`] and friends fix the payload length in the type, as the const parameter +//! `DATA_LEN`, and so implement [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] like every +//! other mode in this crate: they stream, holding back nothing but up to `AAD_LEN` bytes of +//! AAD, and their one-shots generate the nonce. Every entry point accepts exactly `DATA_LEN` +//! bytes of payload, the fixed frame of a packet protocol, and refuses any other amount. +//! +//! # The nonce and tag length are parametrizable +//! +//! Unlike the other aliases in this crate, these do not pin everything: `NONCE_LEN` and `TAG_LEN` +//! are exposed as parameters. +//! +//! * **`NONCE_LEN` (the spec's `n`) fixes the maximum payload.** NIST SP 800-38C 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. +//! * **`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`] are the 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 +//! // 12-byte nonce, 16-byte tag, < 16 MiB +//! AES_CCM_128 +//! ``` +//! +//! Though some alternative choices do exist, for example: +//! ```text +//! // IEEE 802.11 CCMP's 13 byte nonce and 8 byte tag +//! AES_CCM_128 +//! ``` +//! +//! # Usage Examples +//! +//! ## Generic AEAD API for a fixed packet size +//! +//! For code written against [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], the fixed-frame +//! pair generates the nonce and returns it. Every entry point, one-shots included, takes exactly +//! `DATA_LEN` bytes of payload: +//! +//! ``` +//! use bouncycastle_aes::AES_CCM_128_Packet; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Up to 64 bytes of AAD, and frames of exactly 2 KiB -- comfortably above an 802.11 frame, +//! // the packet size CCM was designed for. +//! type AESEnc = AES_CCM_128_Packet; +//! type AESDec = AES_CCM_128_Packet; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! let frame = [0x5Au8; 2048]; +//! +//! // The one-shots: one frame. +//! let (nonce, ciphertext, tag) = AESEnc::encrypt_detached(&key, b"header", &frame).expect("encryption"); +//! let plaintext = AESDec::decrypt_detached(&key, &nonce, b"header", &ciphertext, &tag).expect("decryption"); +//! assert_eq!(&plaintext[..], &frame[..]); +//! // ...and nothing but a frame. +//! assert!(AESEnc::encrypt_detached(&key, b"header", b"message").is_err()); +//! +//! // The streaming methods: the same frame, released as it is processed. +//! let (mut enc, nonce) = AESEnc::do_encrypt_init(&key).expect("init"); +//! enc.do_update_aad(b"header").expect("aad"); +//! let mut sealed = vec![0u8; 2048]; +//! let n = enc.do_encrypt_out(&frame, &mut sealed).expect("the whole frame comes out"); +//! assert_eq!(n, 2048); +//! let (_, _, tag) = enc.do_encrypt_final_detachedtag().expect("the tag"); +//! +//! let mut dec = AESDec::do_decrypt_init(&key, &nonce).expect("init"); +//! dec.do_update_aad(b"header").expect("aad"); +//! let mut opened = vec![0u8; 2048]; +//! dec.do_decrypt_out(&sealed, &mut opened).expect("released, but not yet authenticated"); +//! dec.do_decrypt_final_detachedtag(&tag).expect("...until the tag verifies"); +//! assert_eq!(opened, frame); +//! ``` +//! +//! ## One-shot API +//! +//! [`Ccm`]'s inherent `encrypt_out` / `decrypt_out`, expose the CCM-specific parameters, specifically +//! the ability to provide the nonce, and to produce and consume the spec's own ciphertext layout, +//! `ciphertext || tag` (Sec 6.1 step 8): +//! +//! ``` +//! use bouncycastle_aes::{AES_CCM_256, CCM_NONCE_LEN, CCM_TAG_LEN}; +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CCM_256; +//! type AESDec = AES_CCM_256; +//! +//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! +//! // Supplied, not generated. It is the caller's responsibility that it never repeat under this key. +//! let nonce = [0x01u8; CCM_NONCE_LEN]; +//! +//! // The associated data is authenticated but not encrypted; the message is both. +//! let aad = b"header, sent in the clear"; +//! let message = b"a message of no particular length"; +//! +//! let mut sealed = vec![0u8; message.len() + CCM_TAG_LEN]; +//! let n = AESEnc::encrypt_out(&key, &nonce, aad, message, &mut sealed).expect("encryption"); +//! assert_eq!(n, sealed.len(), "the ciphertext plus the tag"); +//! +//! let mut opened = vec![0u8; message.len()]; +//! let n = AESDec::decrypt_out(&key, &nonce, aad, &sealed, &mut opened).expect("decryption"); +//! assert_eq!(&opened[..n], message); +//! +//! // Tampering with either the ciphertext or the associated data fails the tag check. +//! let mut tampered = sealed.clone(); +//! tampered[0] ^= 1; +//! assert!(AESDec::decrypt_out(&key, &nonce, aad, &tampered, &mut opened).is_err()); +//! assert!(AESDec::decrypt_out(&key, &nonce, b"other header", &sealed, &mut opened).is_err()); +//! ``` +//! +//! ## Detached tag +//! +//! For a wire format that carries the tag separately, `encrypt_detached_out` / `decrypt_detached_out` +//! return and take it on its own: +//! +//! ``` +//! use bouncycastle_aes::{AES_CCM_128, CCM_NONCE_LEN, CCM_TAG_LEN}; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CCM_128; +//! type AESDec = AES_CCM_128; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let nonce = [0x02u8; CCM_NONCE_LEN]; +//! let message = b"a short packet"; +//! +//! let mut ciphertext = vec![0u8; message.len()]; +//! let (n, tag) = AESEnc::encrypt_detached_out(&key, &nonce, &[], message, &mut ciphertext).expect("encryption"); +//! assert_eq!(n, message.len(), "CCM never expands the payload"); +//! +//! let mut plaintext = vec![0u8; message.len()]; +//! AESDec::decrypt_detached_out(&key, &nonce, &[], &ciphertext, &tag, &mut plaintext).expect("decryption"); +//! assert_eq!(&plaintext[..], message); +//! ``` +//! +//! ## Streaming API +//! +//! CCM authenticates the payload length before any payload, so it cannot stream indefinitely +//! (SP 800-38C Sec 3). It can still process data that arrives in pieces, provided the total length +//! is declared up front to `new`; each piece is then encrypted in place, and the tag comes from +//! `do_encrypt_final`: +//! +//! ``` +//! use bouncycastle_aes::{AES_CCM_128, CCM_NONCE_LEN, CCM_TAG_LEN}; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CCM_128; +//! type AESDec = AES_CCM_128; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let nonce = [0x03u8; CCM_NONCE_LEN]; +//! let aad = b"header"; +//! let plaintext = [0x5Au8; 50]; +//! +//! // Encrypt in 7-byte pieces, each in place. The total length is declared up front. +//! let mut encryptor = AESEnc::new(&key, &nonce, aad, plaintext.len()).expect("encrypt init"); +//! let mut ciphertext = plaintext; +//! for piece in ciphertext.chunks_mut(7) { +//! encryptor.do_encrypt(piece).expect("encryption"); +//! } +//! // The tag is computed over everything, so it is the last thing out. +//! let tag = encryptor.do_encrypt_final().expect("the tag"); +//! +//! // Decrypt in 19-byte pieces: the boundaries need not match the encryptor's. The bytes +//! // written are not authenticated until `do_decrypt_final` accepts the tag. +//! let mut decryptor = AESDec::new(&key, &nonce, aad, ciphertext.len()).expect("decrypt init"); +//! let mut recovered = ciphertext; +//! for piece in recovered.chunks_mut(19) { +//! decryptor.do_decrypt_update(piece).expect("decryption"); +//! } +//! decryptor.do_decrypt_final(&tag).expect("a valid tag"); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! # Memory Usage +//! +//! The value held between calls: +//! +//! | Type | AES-128 | AES-192 | AES-256 | +//! |---|---|---|---| +//! | [`AES_CCM_128`] and friends, either direction | 264 B | 296 B | 328 B | +//! | [`AES_CCM_128_Packet`] and friends, encrypting, `AAD_LEN = 64` | 344 B | 376 B | 408 B | +//! | [`AES_CCM_128_Packet`] and friends, decrypting, `AAD_LEN = 64` | 368 B | 400 B | 432 B | +//! +//! The difference between the key sizes is the key schedule; the `_Packet` pair adds the +//! `AAD_LEN`-byte AAD buffer and, on the decrypting side, the held-back tag. Peak stack over a +//! 16 KiB frame with AES-128, measured with massif on x86-64 in release mode by +//! `mem_usage_benches/src/bench_ccm_mem_usage.rs`, including the caller's own 16 KiB arrays: +//! +//! | Path | Peak stack | +//! |---|---| +//! | process start-up alone | 7 696 B | +//! | `AES_CCM_128` one-shot, two arrays (message, ciphertext) | 35 944 B | +//! | `AES_CCM_128` streaming, one array encrypted in place | 19 112 B | +//! | `AES_CCM_128_Packet` streaming encrypt, two arrays | 37 400 B | +//! | `AES_CCM_128_Packet` streaming decrypt, two arrays | 36 168 B | +//! | `AES_CCM_128_Packet` one-shot, two arrays | 37 576 B | +//! +//! # 🚨 Security Considerations 🚨 +//! +//! All security considerations from [`bouncycastle_cipher::modes::ccm`] apply. Above all, the nonce that +//! [`AES_CCM_128`] and friends take must never repeat under one key. + +use crate::AES_BLOCK_LEN; +use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::Direction; +use bouncycastle_cipher::modes::{Ccm, CcmDecryptor, CcmEncryptor}; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::{Decrypting, Encrypting}; +#[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 with a `NONCE_LEN`-byte nonce and a `TAG_LEN`-byte tag. The AAD and +/// payload lengths are supplied per message: the one-shots read them off the slices they are +/// given, along with a caller-supplied nonce, and the streaming API takes both totals up front in +/// `new` or `new_with_lengths`. For a payload length fixed by the type, and the generic AEAD +/// traits, see [`AES_CCM_128_Packet`]. +/// +/// `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. +#[allow(non_camel_case_types)] +pub type AES_CCM_128 = + Ccm; + +/// AES-192 in CCM mode with a `NONCE_LEN`-byte nonce and a `TAG_LEN`-byte tag. See [`AES_CCM_128`]. +#[allow(non_camel_case_types)] +pub type AES_CCM_192 = + Ccm; + +/// AES-256 in CCM mode with a `NONCE_LEN`-byte nonce and a `TAG_LEN`-byte tag. See [`AES_CCM_128`]. +#[allow(non_camel_case_types)] +pub type AES_CCM_256 = + Ccm; + +/// AES-128 in CCM mode, as an [`AEADCipherEncryptor`] or [`AEADCipherDecryptor`] by `Dir`, for +/// frames of exactly `DATA_LEN` payload bytes. +/// +/// This is the fixed-frame pair, for code written against the generic AEAD traits: every entry +/// point accepts exactly `DATA_LEN` bytes of payload and up to `AAD_LEN` of AAD, and the +/// one-shots generate the nonce. `NONCE_LEN` must be at least 12 here, and `TAG_LEN` is as for +/// [`AES_CCM_128`]. See [`CcmEncryptor`] for the rules. +#[allow(non_camel_case_types)] +pub type AES_CCM_128_Packet< + Dir, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> = ::Select< + CcmEncryptor, + CcmDecryptor, +>; + +/// AES-192 in CCM mode, as an [`AEADCipherEncryptor`] or [`AEADCipherDecryptor`] by `Dir`. See [`AES_CCM_128_Packet`]. +#[allow(non_camel_case_types)] +pub type AES_CCM_192_Packet< + Dir, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> = ::Select< + CcmEncryptor, + CcmDecryptor, +>; + +/// AES-256 in CCM mode, as an [`AEADCipherEncryptor`] or [`AEADCipherDecryptor`] by `Dir`. See [`AES_CCM_128_Packet`]. +#[allow(non_camel_case_types)] +pub type AES_CCM_256_Packet< + Dir, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> = ::Select< + CcmEncryptor, + CcmDecryptor, +>; diff --git a/crypto/aes/src/cfb.rs b/crypto/aes/src/cfb.rs new file mode 100644 index 00000000..c7260629 --- /dev/null +++ b/crypto/aes/src/cfb.rs @@ -0,0 +1,112 @@ +//! Type aliases for AES in CFB mode (NIST SP 800-38A Sec 6.3). +//! +//! See [`bouncycastle_cipher::modes::cfb`] for details on the CipherFeedback construction. +//! +//! The aliases here are stream ciphers: the data is a `&mut [u8]` of any length, encrypted or +//! decrypted in place, and the ciphertext is exactly as long as the plaintext. The IV is generated +//! by encryption and returned; there is no API for supplying one. `Dir` is [`Encrypting`] or +//! [`Decrypting`]; the wrong direction is a compile error, not a runtime check. +//! +//! The segment size is the full block, so the constructions used here are equivalent to **CFB128**. +//! SP 800-38A's `s = 8` variant is a different, non-interoperable mode with its own aliases, +//! [`AES_CFB8_128`](crate::AES_CFB8_128) and friends, and `s = 1` is not implemented. +//! +//! # Usage Examples +//! +//! ## One-shot API +//! +//! Basic usage can be obtained via the [`StreamCipherEncryptor`] and [`StreamCipherDecryptor`] API: +//! +//! ``` +//! use bouncycastle_aes::AES_CFB_256; +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CFB_256; +//! type AESDec = AES_CFB_256; +//! +//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! +//! // An arbitrary plaintext to encrypt. +//! // Any length: a stream cipher does not need a whole number of blocks. +//! let plaintext = [0x5Au8; 47]; +//! +//! // Encryption works in place. The IV is generated for you and returned; there is no API for +//! // supplying one. +//! let mut data = plaintext; +//! let (_, iv) = AESEnc::encrypt_inplace(&key, &mut data).expect("encryption"); +//! +//! AESDec::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); +//! assert_eq!(data, plaintext); +//! ``` +//! +//! ## Streaming API +//! +//! For data that arrives in pieces, the following APIs can be used. A stream cipher processes +//! every byte it is given, so nothing is held back between calls and the pieces can be of any +//! length: +//! +//! ``` +//! use bouncycastle_aes::AES_CFB_128; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{ +//! StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, +//! SymmetricCipherEncryptor, +//! }; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CFB_128; +//! type AESDec = AES_CFB_128; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // An arbitrary plaintext to encrypt +//! let plaintext = [0x5Au8; 50]; +//! +//! // Encrypt in 7-byte pieces, each in place. +//! let (mut encryptor, iv) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! let mut ciphertext = plaintext; +//! for piece in ciphertext.chunks_mut(7) { +//! encryptor.do_encrypt_inplace(piece).expect("encryption"); +//! } +//! +//! // Decrypt in 19-byte pieces: the boundaries need not match the encryptor's. +//! let mut decryptor = AESDec::do_decrypt_init(&key, &iv).expect("decrypt init"); +//! let mut recovered = ciphertext; +//! for piece in recovered.chunks_mut(19) { +//! decryptor.do_decrypt_inplace(piece).expect("decryption"); +//! } +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! # 🚨 Security Considerations 🚨 +//! +//! All security considerations from [`bouncycastle_cipher::modes::cfb`] apply. + +use crate::AES_BLOCK_LEN; +use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Cfb; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::{Decrypting, Encrypting}; +#[allow(unused_imports)] +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +// end of imports needed for docs + +/// AES-128 in CFB128 mode. +#[allow(non_camel_case_types)] +pub type AES_CFB_128 = Cfb; + +/// AES-192 in CFB128 mode. +#[allow(non_camel_case_types)] +pub type AES_CFB_192 = Cfb; + +/// AES-256 in CFB128 mode. See [`AES_CFB_128`]. +#[allow(non_camel_case_types)] +pub type AES_CFB_256 = Cfb; diff --git a/crypto/aes/src/cfb8.rs b/crypto/aes/src/cfb8.rs new file mode 100644 index 00000000..c4ec0757 --- /dev/null +++ b/crypto/aes/src/cfb8.rs @@ -0,0 +1,135 @@ +//! Type aliases for AES in CFB8 mode (NIST SP 800-38A Sec 6.3, `s = 8`). +//! +//! See [`bouncycastle_cipher::modes::cfb8`] for details on the CipherFeedback construction with an 8-bit segment. +//! +//! The aliases here are stream ciphers: the data is a `&mut [u8]` of any +//! length, encrypted or decrypted in-place since the ciphertext is exactly as long as the plaintext. +//! The IV is generated by encryption and returned; there is no API for supplying one. `Dir` is +//! [`Encrypting`] or [`Decrypting`]; the wrong direction is a compile error, not a runtime check. +//! +//! CFB8 is a **different, non-interoperable mode** from CFB, not a variant of it: their +//! ciphertexts differ from the second byte, and it costs a full AES call per byte, sixteen times +//! the work of [`AES_CFB_128`](crate::AES_CFB_128). See [`bouncycastle_cipher::modes::cfb8`] for when +//! that is the right trade. +//! +//! # Usage Examples +//! +//! ## One-shot API +//! +//! Basic usage can be obtained via the [`StreamCipherEncryptor`] and [`StreamCipherDecryptor`] API: +//! +//! ``` +//! use bouncycastle_aes::AES_CFB8_256; +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CFB8_256; +//! type AESDec = AES_CFB8_256; +//! +//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! +//! // An arbitrary plaintext to encrypt. +//! // 5 bytes: CFB8's segment is one byte, so any length at all is fine. +//! let plaintext = *b"hello"; +//! +//! // Encryption works in place. The IV is generated for you and returned; there is no API for +//! // supplying one. +//! let mut data = plaintext; +//! let (_, iv) = AESEnc::encrypt_inplace(&key, &mut data).expect("encryption"); +//! +//! AESDec::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); +//! assert_eq!(data, plaintext); +//! ``` +//! +//! ## Streaming API +//! +//! For data that arrives in pieces, the following APIs can be used. A stream cipher processes +//! every byte it is given, so nothing is held back between calls and the pieces can be of any +//! length: +//! +//! ``` +//! use bouncycastle_aes::AES_CFB8_128; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{ +//! StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, +//! SymmetricCipherEncryptor, +//! }; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CFB8_128; +//! type AESDec = AES_CFB8_128; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // An arbitrary plaintext to encrypt +//! let plaintext = [0x5Au8; 50]; +//! +//! // Encrypt in 3-byte pieces, each in place. +//! let (mut encryptor, iv) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! let mut ciphertext = plaintext; +//! for piece in ciphertext.chunks_mut(3) { +//! encryptor.do_encrypt_inplace(piece).expect("encryption"); +//! } +//! +//! // Decrypt in 20-byte pieces: the boundaries need not match the encryptor's. +//! let mut decryptor = AESDec::do_decrypt_init(&key, &iv).expect("decrypt init"); +//! let mut recovered = ciphertext; +//! for piece in recovered.chunks_mut(20) { +//! decryptor.do_decrypt_inplace(piece).expect("decryption"); +//! } +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! ## Not interoperable with CFB +//! +//! CFB and CFB8 are not interchangeable: the same key and IV give a different ciphertext, so +//! a message encrypted with one does not decrypt with the other. +//! +//! ``` +//! use bouncycastle_aes::{AES_CFB8_128, AES_CFB_128}; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let plaintext = *b"hello"; +//! +//! let mut data = plaintext; +//! let (_, iv) = AES_CFB8_128::::encrypt_inplace(&key, &mut data).unwrap(); +//! +//! // Decrypting CFB8 output as CFB128 does not recover the plaintext. +//! AES_CFB_128::::decrypt_inplace(&key, &iv, &mut data).unwrap(); +//! assert_ne!(data, plaintext); +//! ``` +//! +//! # 🚨 Security Considerations 🚨 +//! +//! All security considerations from [`bouncycastle_cipher::modes::cfb8`] apply. + +use crate::AES_BLOCK_LEN; +use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Cfb8; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::{Decrypting, Encrypting}; +#[allow(unused_imports)] +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +// end of imports needed for docs + +/// AES-128 in CFB8 mode. +#[allow(non_camel_case_types)] +pub type AES_CFB8_128 = Cfb8; + +/// AES-192 in CFB8 mode. +#[allow(non_camel_case_types)] +pub type AES_CFB8_192 = Cfb8; + +/// AES-256 in CFB8 mode. See [`AES_CFB8_128`]. +#[allow(non_camel_case_types)] +pub type AES_CFB8_256 = Cfb8; diff --git a/crypto/aes/src/ctr.rs b/crypto/aes/src/ctr.rs new file mode 100644 index 00000000..c2456c47 --- /dev/null +++ b/crypto/aes/src/ctr.rs @@ -0,0 +1,120 @@ +//! Type aliases for AES in CTR mode (NIST SP 800-38A Sec 6.5). +//! +//! See [`bouncycastle_cipher::modes::ctr`] for details on the Counter construction. +//! +//! The aliases here are stream ciphers: the data is a `&mut [u8]` of any length, encrypted or +//! decrypted in place since ciphertext is exactly as long as the plaintext. The nonce is generated +//! by encryption and returned; there is no API for supplying one. `Dir` is [`Encrypting`] or [`Decrypting`]; +//! the wrong direction is a compile error, not a runtime check. +//! +//! # Nonce and counter length +//! +//! **The nonce length is 12 bytes, the counter is 4 bytes.** +//! +//! These aliases fix a **12-byte nonce** ([`CTR_NONCE_LEN`]), leaving the remainder of each block to +//! be a 4-byte counter. That allows 2^32 blocks -- 64 GiB -- in one message, and past it the +//! mode errors rather than repeating the keystream. A shorter message limit in exchange for more nonce +//! bits is available by using `Ctr` directly with a 13, 14 or 15-byte nonce. +//! +//! # Usage Examples +//! +//! ## One-shot API +//! +//! Basic usage can be obtained via the [`StreamCipherEncryptor`] and [`StreamCipherDecryptor`] API: +//! +//! ``` +//! use bouncycastle_aes::AES_CTR_256; +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CTR_256; +//! type AESDec = AES_CTR_256; +//! +//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! +//! // An arbitrary plaintext to encrypt. +//! // Any length: a stream cipher does not need a whole number of blocks. +//! let plaintext = [0x5Au8; 47]; +//! +//! // Encryption works in place. The nonce is generated for you and returned; there is no API for +//! // supplying one. +//! let mut data = plaintext; +//! let (_, nonce) = AESEnc::encrypt_inplace(&key, &mut data).expect("encryption"); +//! +//! AESDec::decrypt_inplace(&key, &nonce, &mut data).expect("decryption"); +//! assert_eq!(data, plaintext); +//! ``` +//! +//! ## Streaming API +//! +//! For data that arrives in pieces, the following APIs can be used. A stream cipher processes +//! every byte it is given, so nothing is held back between calls and the pieces can be of any +//! length: +//! +//! ``` +//! use bouncycastle_aes::AES_CTR_128; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{ +//! StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, +//! SymmetricCipherEncryptor, +//! }; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_CTR_128; +//! type AESDec = AES_CTR_128; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // An arbitrary plaintext to encrypt +//! let plaintext = [0x5Au8; 50]; +//! +//! // Encrypt in 7-byte pieces, each in place. +//! let (mut encryptor, nonce) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! let mut ciphertext = plaintext; +//! for piece in ciphertext.chunks_mut(7) { +//! encryptor.do_encrypt_inplace(piece).expect("encryption"); +//! } +//! +//! // Decrypt in 19-byte pieces: the boundaries need not match the encryptor's. +//! let mut decryptor = AESDec::do_decrypt_init(&key, &nonce).expect("decrypt init"); +//! let mut recovered = ciphertext; +//! for piece in recovered.chunks_mut(19) { +//! decryptor.do_decrypt_inplace(piece).expect("decryption"); +//! } +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! # 🚨 Security Considerations 🚨 +//! +//! All security considerations from [`bouncycastle_cipher::modes::ctr`] apply. + +use crate::AES_BLOCK_LEN; +use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Ctr; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::{Decrypting, Encrypting}; +#[allow(unused_imports)] +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +// end of imports needed for docs + +/// The nonce length these aliases use, leaving a 4-byte counter. +pub const CTR_NONCE_LEN: usize = 12; + +/// AES-128 in CTR mode with a 12-byte nonce. +#[allow(non_camel_case_types)] +pub type AES_CTR_128 = Ctr; + +/// AES-192 in CTR mode with a 12-byte nonce. +#[allow(non_camel_case_types)] +pub type AES_CTR_192 = Ctr; + +/// AES-256 in CTR mode with a 12-byte nonce. See [`AES_CTR_128`]. +#[allow(non_camel_case_types)] +pub type AES_CTR_256 = Ctr; diff --git a/crypto/aes/src/gcm.rs b/crypto/aes/src/gcm.rs new file mode 100644 index 00000000..294dd56b --- /dev/null +++ b/crypto/aes/src/gcm.rs @@ -0,0 +1,167 @@ +//! Type aliases for AES in GCM (NIST SP 800-38D). +//! +//! See [`bouncycastle_cipher::modes::gcm`] for details on the abstract Galois/Counter Mode construction. +//! +//! The aliases here are authenticated ciphers: encryption produces a tag as well as a ciphertext, +//! and decryption either returns the plaintext or fails the tag check. Both directions implement +//! [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], which keep the tag detached, and +//! [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`], which carry it inline as +//! `ciphertext || tag`. The nonce is generated by encryption and returned; there is no API for +//! supplying one. `Dir` is [`Encrypting`] or [`Decrypting`]; the wrong direction is a compile +//! error, not a runtime check. +//! +//! The tag is fixed 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 [`Gcm`] directly with the desired `TAG_LEN`. The nonce is +//! always [`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. +//! +//! # Usage Examples +//! +//! ## One-shot API +//! +//! Basic usage can be obtained via the [`AEADCipherEncryptor`] and [`AEADCipherDecryptor`] API, +//! which returns the tag separately from the ciphertext: +//! +//! ``` +//! use bouncycastle_aes::AES_GCM_256; +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_GCM_256; +//! type AESDec = AES_GCM_256; +//! +//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! +//! // The associated data is authenticated but not encrypted; the message is both. +//! let aad = b"header, sent in the clear"; +//! let message = b"a message of no particular length"; +//! +//! // The nonce is generated for you and returned; there is no API for supplying one. +//! let (nonce, ciphertext, tag) = AESEnc::encrypt_detached(&key, aad, message).expect("encryption"); +//! assert_eq!(ciphertext.len(), message.len(), "GCM never expands the payload"); +//! +//! let recovered = AESDec::decrypt_detached(&key, &nonce, aad, &ciphertext, &tag).expect("decryption"); +//! assert_eq!(recovered, message); +//! +//! // Tampering with the ciphertext, the tag or the associated data fails the tag check. +//! let mut tampered = ciphertext.clone(); +//! tampered[0] ^= 1; +//! assert!(AESDec::decrypt_detached(&key, &nonce, aad, &tampered, &tag).is_err()); +//! assert!(AESDec::decrypt_detached(&key, &nonce, b"other header", &ciphertext, &tag).is_err()); +//! ``` +//! +//! ## Inline `ciphertext || tag` +//! +//! Through the [`SymmetricCipherEncryptor`] and [`SymmetricCipherDecryptor`] API the tag is +//! appended to the ciphertext, so the output is 16 bytes longer than the input: +//! +//! ``` +//! use bouncycastle_aes::AES_GCM_128; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_GCM_128; +//! type AESDec = AES_GCM_128; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let message = b"a message of no particular length at all"; +//! +//! let mut ciphertext = vec![0u8; AESEnc::encrypt_out_len(message.len())]; +//! let (nonce, written) = AESEnc::encrypt_out(&key, message, &mut ciphertext).expect("encryption"); +//! assert_eq!(written, message.len() + 16, "the ciphertext plus the tag"); +//! +//! let mut plaintext = vec![0u8; AESDec::decrypt_out_len(ciphertext.len())]; +//! let n = AESDec::decrypt_out(&key, &nonce, &ciphertext, &mut plaintext).expect("decryption"); +//! assert_eq!(&plaintext[..n], message); +//! ``` +//! +//! ## Streaming API +//! +//! For data that arrives in pieces, the following APIs can be used. All associated data must be +//! given via `do_update_aad` before the first piece of data, and the tag comes from +//! `do_encrypt_final`: +//! +//! ``` +//! use bouncycastle_aes::AES_GCM_128; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{ +//! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +//! }; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_GCM_128; +//! type AESDec = AES_GCM_128; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let aad = b"header"; +//! let plaintext = [0x5Au8; 50]; +//! +//! // Encrypt in 7-byte pieces. A piece is encrypted as soon as it is given, so each call writes +//! // exactly as many bytes as it was handed. +//! let (mut encryptor, nonce) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! encryptor.do_update_aad(aad).expect("aad"); +//! let mut ciphertext = Vec::new(); +//! for piece in plaintext.chunks(7) { +//! let mut out = [0u8; 7]; +//! let bytes_written = encryptor.do_encrypt_out(piece, &mut out).expect("encryption"); +//! ciphertext.extend_from_slice(&out[..bytes_written]); +//! } +//! // The tag is computed over everything, so it is the last thing out. +//! let (tag, tag_len) = encryptor.do_encrypt_final().expect("the tag"); +//! ciphertext.extend_from_slice(&tag[..tag_len]); +//! assert_eq!(ciphertext.len(), plaintext.len() + 16); +//! +//! // Decrypt in 19-byte pieces. The decryptor holds back the last 16 bytes it has seen, since +//! // those may be the tag, so a call can write fewer bytes than it was handed; `do_decrypt_final` +//! // checks the tag and releases whatever is still held back. +//! let mut decryptor = AESDec::do_decrypt_init(&key, &nonce).expect("decrypt init"); +//! decryptor.do_update_aad(aad).expect("aad"); +//! let mut recovered = Vec::new(); +//! for piece in ciphertext.chunks(19) { +//! let mut out = [0u8; 19]; +//! let bytes_written = decryptor.do_decrypt_out(piece, &mut out).expect("decryption"); +//! recovered.extend_from_slice(&out[..bytes_written]); +//! } +//! let (last, last_len) = decryptor.do_decrypt_final().expect("a valid tag"); +//! recovered.extend_from_slice(&last[..last_len]); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! # 🚨 Security Considerations 🚨 +//! +//! All security considerations from [`bouncycastle_cipher::modes::gcm`] apply. + +use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Gcm; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::modes::GCM_NONCE_LEN; +#[allow(unused_imports)] +use bouncycastle_cipher::{Decrypting, Encrypting}; +#[allow(unused_imports)] +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +// end of imports needed for docs + +/// AES-128 in GCM with a 128-bit tag. +#[allow(non_camel_case_types)] +pub type AES_GCM_128 = Gcm; + +/// AES-192 in GCM with a 128-bit tag. +#[allow(non_camel_case_types)] +pub type AES_GCM_192 = Gcm; + +/// AES-256 in GCM with a 128-bit tag. See [`AES_GCM_128`]. +#[allow(non_camel_case_types)] +pub type AES_GCM_256 = Gcm; diff --git a/crypto/aes/src/hazmat/aes_internal.rs b/crypto/aes/src/hazmat/aes_internal.rs new file mode 100644 index 00000000..fc706adb --- /dev/null +++ b/crypto/aes/src/hazmat/aes_internal.rs @@ -0,0 +1,310 @@ +//! The raw AES permutation: CIPHER() and INVCIPHER() (FIPS 197 Sec 5.1 and Sec 5.3). +//! +//! Under [`hazmat`](crate::hazmat) because [`AESInternal`] transforms exactly one block: it is +//! the primitive under the modes in this crate, not a cipher for data. +//! +//! # Usage Examples +//! +//! ## Encrypting and decrypting a single block +//! +//! ``` +//! use bouncycastle_aes::hazmat::AES128Internal; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type( +//! &[0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, +//! 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c], +//! KeyType::SymmetricCipherKey, +//! ).expect("a 16-byte symmetric cipher key"); +//! +//! let aes = AES128Internal::new(&key).expect("a valid AES-128 key"); +//! +//! // Sample plaintext from FIPS 197 Appendix B. +//! let mut block: [u8; 16] = [0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, +//! 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, 0x34]; +//! aes.encrypt_block(&mut block); +//! +//! // `block` now contains the ciphertext. +//! // Double-check it against the sample ciphertext from FIPS 197 Appdx B. +//! assert_eq!(block, [0x39, 0x25, 0x84, 0x1d, 0x02, 0xdc, 0x09, 0xfb, +//! 0xdc, 0x11, 0x85, 0x97, 0x19, 0x6a, 0x0b, 0x32]); +//! +//! // The same value decrypts, from the same instantiated aes object. +//! aes.decrypt_block(&mut block); +//! +//! // `block` now contains the original plaintext again. +//! assert_eq!(block, [0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, +//! 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, 0x34]); +//! ``` +//! +//! ## Two or four blocks at a time +//! +//! The bit-sliced state is generic over its word width, and each 16 bits of width holds one +//! block: `u16` planes hold one block, `u32` planes two and `u64` planes four (see the +//! `bitslice` module in the source). The round functions cost about the same whatever the +//! width, so on a 64-bit machine four independent blocks cost little more than one. Where a +//! caller has them, [`ElectronicCodeBook::encrypt_4blocks`] is 3.0x the throughput of four +//! [`ElectronicCodeBook::encrypt_block`] calls on x86-64 (3.7x for decryption), and +//! [`ElectronicCodeBook::encrypt_2blocks`] 1.75x that of two (1.95x for decryption); the crate +//! docs have the table and the benches record the numbers: +//! +//! ``` +//! use bouncycastle_aes::hazmat::AES256Internal; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::hazmat::ElectronicCodeBook; +//! +//! let key = KeyMaterial::<32>::from_bytes_as_type(&[0x01; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! let aes = AES256Internal::new(&key).expect("a valid AES-256 key"); +//! +//! let mut pair = [[0u8; 16], [1u8; 16]]; +//! aes.encrypt_2blocks(&mut pair); +//! aes.decrypt_2blocks(&mut pair); +//! assert_eq!(pair, [[0u8; 16], [1u8; 16]]); +//! +//! let mut four = [[0u8; 16], [1u8; 16], [2u8; 16], [3u8; 16]]; +//! aes.encrypt_4blocks(&mut four); +//! aes.decrypt_4blocks(&mut four); +//! assert_eq!(four, [[0u8; 16], [1u8; 16], [2u8; 16], [3u8; 16]]); +//! ``` + +use crate::bitslice::{Block, PlaneWord, Planes}; +use crate::round::{add_round_key, inv_mix_columns, inv_shift_rows, mix_columns, shift_rows}; +use crate::sbox::{inv_sbox, sbox}; +use crate::schedule::{AES128Params, AES192Params, AES256Params, AESParams, expand, round_key}; +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_utils::secret::Secret; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::hazmat::ElectronicCodeBook; +// End imports needed for docs + +/// The AES keyed permutation, parameterised by key length. +/// +/// Use the aliases [`AES128Internal`], [`AES192Internal`] and [`AES256Internal`] rather than naming this directly. +/// `P` is sealed to the three parameter sets of FIPS 197 Table 3, so no fourth instantiation +/// exists. +/// +/// The only state is the key schedule, held in a [`Secret`] so that it is zeroized on drop and +/// redacted from `Debug`. There is no direction flag and no initialisation state: both directions +/// work from the same schedule (see the `inv_cipher` method), and a constructed value is always +/// ready to use, so there is no `init()` or `reset()`. +#[derive(Clone)] +pub struct AESInternal { + schedule: Secret, +} + +/// AES-128: 16-byte key, 10 rounds (FIPS 197 Table 3). +#[allow(non_camel_case_types)] +pub type AES128Internal = AESInternal; +/// AES-192: 24-byte key, 12 rounds (FIPS 197 Table 3). +#[allow(non_camel_case_types)] +pub type AES192Internal = AESInternal; +/// AES-256: 32-byte key, 14 rounds (FIPS 197 Table 3). +#[allow(non_camel_case_types)] +pub type AES256Internal = AESInternal; + +impl AESInternal

{ + /// Checks a key is fit to use before it is expanded. + /// + /// The key must be tagged [`KeyType::SymmetricCipherKey`], must be exactly `P::KEY_LEN` bytes + /// of the buffer, and must carry a [`SecurityStrength`] at least equal to its own length -- + /// which is what a key of this length from a correctly-instantiated RNG or KDF will have. + /// The checks exist to catch a key that arrived from somewhere it should not have: a seed + /// reused as a cipher key, or a 32-byte buffer holding material only derived at the 128-bit + /// strength. + /// + /// Takes `&dyn KeyMaterialTrait` so the three constructors, whose `KeyMaterial` capacities + /// differ, can share one implementation. + fn validate(key: &dyn KeyMaterialTrait) -> Result<(), SymmetricCipherError> { + if key.key_type() != KeyType::SymmetricCipherKey { + return Err(KeyMaterialError::InvalidKeyType( + "AES requires a key of type KeyType::SymmetricCipherKey.", + ) + .into()); + } + if key.key_len() != P::KEY_LEN { + return Err(KeyMaterialError::InvalidLength.into()); + } + if key.security_strength() < SecurityStrength::from_bytes(P::KEY_LEN) { + return Err(KeyMaterialError::SecurityStrength( + "The provided key has a lower security strength than the AES key length implies.", + ) + .into()); + } + Ok(()) + } + + /// CIPHER() on every block in the state at once (FIPS 197 Sec 5.1, Algorithm 1). + /// + /// `T` is the plane width, and so the number of blocks: one, two or four. The body is the + /// same at every width; see [`crate::bitslice`]. + /// + /// Algorithm 1 line by line: line 3 is the initial ADDROUNDKEY() with `w[0..3]`; lines 4-9 are + /// the `Nr - 1` full rounds; lines 10-12 are the final round, which omits MIXCOLUMNS(); line + /// 13 returns the state. + fn cipher(&self, q: &mut Planes) { + // line 3: state = state XOR w[0..3] + add_round_key(q, &round_key::(&self.schedule, 0)); + + // lines 4-9: for round from 1 to Nr - 1 + for round in 1..P::NR { + sbox(q); // line 5, SUBBYTES() + shift_rows(q); // line 6, SHIFTROWS() + mix_columns(q); // line 7, MIXCOLUMNS() + add_round_key(q, &round_key::(&self.schedule, round)); // line 8 + } + + // lines 10-12: the final round has no MIXCOLUMNS() + sbox(q); + shift_rows(q); + add_round_key(q, &round_key::(&self.schedule, P::NR)); + } + + /// INVCIPHER() on every block in the state at once (FIPS 197 Sec 5.3, Algorithm 3). + /// + /// This is the **straight** inverse cipher of Algorithm 3, not the equivalent inverse cipher + /// of Sec 5.3.5. That matters: Algorithm 3 applies INVMIXCOLUMNS() *after* ADDROUNDKEY(), + /// which lets it use the ordinary key schedule, whereas Sec 5.3.5 reorders the round to put + /// the two the other way round and needs a separate schedule with INVMIXCOLUMNS() applied to + /// each round key (Algorithm 5, KEYEXPANSIONEIC()). + /// + /// Following Algorithm 3 is therefore what allows one [`AESInternal`] value to encrypt *and* decrypt + /// from a single stored schedule, with no second copy and no transformation at construction + /// time -- which is the whole reason this crate can offer both directions at 176-240 bytes of + /// state. + /// + /// Line by line: line 3 is ADDROUNDKEY() with the last round key; lines 4-9 are the + /// `Nr - 1` full inverse rounds; lines 10-12 are the final one, which omits INVMIXCOLUMNS(); + /// line 13 returns the state. + fn inv_cipher(&self, q: &mut Planes) { + // line 3: state = state XOR w[4*Nr .. 4*Nr+3] + add_round_key(q, &round_key::(&self.schedule, P::NR)); + + // lines 4-9: for round from Nr - 1 down to 1 + for round in (1..P::NR).rev() { + inv_shift_rows(q); // line 5, INVSHIFTROWS() + inv_sbox(q); // line 6, INVSUBBYTES() + add_round_key(q, &round_key::(&self.schedule, round)); // line 7 + inv_mix_columns(q); // line 8, INVMIXCOLUMNS() + } + + // lines 10-12: the final inverse round has no INVMIXCOLUMNS() + inv_shift_rows(q); + inv_sbox(q); + add_round_key(q, &round_key::(&self.schedule, 0)); + } + + /// Encrypts the blocks a `T`-wide state holds, in place: transpose in, [`Self::cipher`], + /// transpose out. + #[inline(always)] + fn encrypt(&self, blocks: &mut T::Blocks) { + let mut q = T::pack(blocks); + self.cipher(&mut q); + T::unpack(&mut q, blocks); + } + + /// Decrypts the blocks a `T`-wide state holds, in place: transpose in, [`Self::inv_cipher`], + /// transpose out. + #[inline(always)] + fn decrypt(&self, blocks: &mut T::Blocks) { + let mut q = T::pack(blocks); + self.inv_cipher(&mut q); + T::unpack(&mut q, blocks); + } + + /// Encrypts one block in place, on `u16` planes. + /// + /// This is the right call when only one block is available -- CBC and CFB encryption, whose + /// blocks are serially dependent -- and it does no wasted work: the `u16` state holds + /// exactly one block. Where two or four independent blocks are available, which for a mode + /// of operation means CTR or the decryption direction of CBC and CFB, prefer + /// [`Self::encrypt_2blocks`] or [`Self::encrypt_4blocks`], which cost little more per call. + /// + /// Infallible: a constructed [`AESInternal`] is always usable and every input length is fixed. + pub(crate) fn encrypt_block(&self, block: &mut Block) { + self.encrypt::(core::array::from_mut(block)); + } + + /// Decrypts one block in place, on `u16` planes. See [`Self::encrypt_block`]. + pub(crate) fn decrypt_block(&self, block: &mut Block) { + self.decrypt::(core::array::from_mut(block)); + } + + /// Encrypts two independent blocks in place, on `u32` planes, for about the cost of one. + /// See [`Self::encrypt_block`] for when to use which. + pub(crate) fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + self.encrypt::(blocks); + } + + /// Decrypts two independent blocks in place, on `u32` planes. See [`Self::encrypt_block`]. + pub(crate) fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + self.decrypt::(blocks); + } + + /// Encrypts four independent blocks in place, on `u64` planes, for about the cost of one. + /// See [`Self::encrypt_block`] for when to use which. + pub(crate) fn encrypt_4blocks(&self, blocks: &mut [Block; 4]) { + self.encrypt::(blocks); + } + + /// Decrypts four independent blocks in place, on `u64` planes. See [`Self::encrypt_block`]. + pub(crate) fn decrypt_4blocks(&self, blocks: &mut [Block; 4]) { + self.decrypt::(blocks); + } +} + +// The three constructors and `Algorithm` impls below are written out longhand rather than +// generated with `macro_rules!`: `cargo mutants` cannot see into macro bodies, so a macro would +// hide the key checks and the security-strength constants from mutation testing (see CLAUDE.md). +// Each `new` differs only in the `KeyMaterial` capacity it accepts, which is what makes a +// wrong-length key a compile error at the call site rather than a runtime error. + +impl AES128Internal { + /// Expands a 16-byte key into an AES-128 schedule. + /// + /// # Errors + /// * [`KeyMaterialError::InvalidKeyType`] if the key is not [`KeyType::SymmetricCipherKey`]. + /// * [`KeyMaterialError::InvalidLength`] if the key is not 16 bytes long. + /// * [`KeyMaterialError::SecurityStrength`] if the key carries a strength below 128 bits. + pub(crate) fn new(key: &KeyMaterial<16>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl AES192Internal { + /// Expands a 24-byte key into an AES-192 schedule. See [`AES128Internal::new`] for the error cases. + pub(crate) fn new(key: &KeyMaterial<24>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl AES256Internal { + /// Expands a 32-byte key into an AES-256 schedule. See [`AES128Internal::new`] for the error cases. + pub(crate) fn new(key: &KeyMaterial<32>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl Algorithm for AES128Internal { + const ALG_NAME: &'static str = AES128Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl Algorithm for AES192Internal { + const ALG_NAME: &'static str = AES192Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; +} + +impl Algorithm for AES256Internal { + const ALG_NAME: &'static str = AES256Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +} diff --git a/crypto/aes/src/hazmat/ecb.rs b/crypto/aes/src/hazmat/ecb.rs new file mode 100644 index 00000000..5afcf0cb --- /dev/null +++ b/crypto/aes/src/hazmat/ecb.rs @@ -0,0 +1,328 @@ +//! Type aliases for AES in ECB mode (NIST SP 800-38A Sec 6.1), with padding. +//! +//! **🚨 Security note: 🚨 ECB is not a confidentiality mode for data.** That is why these are +//! under [`hazmat`](crate::hazmat); see [`bouncycastle_core::hazmat`] for the supported uses. +//! +//! See [`bouncycastle_cipher::modes::hazmat::Ecb`] for details on the ElectronicCodebook construction. +//! +//! The aliases here are padded block ciphers that accept input of any size; `NoPadding` accepts +//! only whole blocks but goes through the same adapter. The unpadded mode underneath them, which +//! implements the block-cipher traits directly, is [`Ecb`] and is not re-exported from this crate. +//! +//! ECB has no IV, so its `INIT_DATA_LEN` is 0: encryption returns an empty array, decryption takes +//! one, and the ciphertext is exactly the padded plaintext with nothing prepended. The RNG-taking +//! constructors, `do_encrypt_init_rng` and `encrypt_rng_out`, panic, as +//! [`SymmetricCipherEncryptor::do_encrypt_init_rng`] requires of a cipher with no init data to +//! generate; use the plain `do_encrypt_init` / `encrypt_out`. +//! +//! # Usage Examples +//! +//! ## One-shot API +//! +//! Basic usage can be obtained via the [`SymmetricCipherEncryptor`] and [`SymmetricCipherDecryptor`] API: +//! +//! ``` +//! use bouncycastle_aes::hazmat::AES_ECB_256; +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_cipher::padding::PKCS7; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_ECB_256; +//! type AESDec = AES_ECB_256; +//! +//! let key = KeyMaterial256::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! +//! // An arbitrary plaintext to encrypt +//! // Any length: PKCS#7 pads it out to whole blocks, so 50 bytes is as good as 48. +//! let plaintext = [0x5Au8; 50]; +//! +//! // ECB has no IV, so the init data that comes back is empty. +//! let (no_iv, ciphertext) = AESEnc::encrypt(&key, &plaintext).expect("encryption"); +//! assert_eq!(no_iv, [0u8; 0]); +//! assert_eq!(ciphertext.len(), 64, "50 bytes padded out to four blocks"); +//! +//! let recovered = AESDec::decrypt(&key, &no_iv, &ciphertext).expect("decryption"); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! ## Streaming API +//! +//! For data that arrives in pieces, the following APIs can be used: +//! +//! ``` +//! use bouncycastle_aes::AES_BLOCK_LEN; +//! use bouncycastle_aes::hazmat::AES_ECB_128; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! use bouncycastle_cipher::padding::PKCS7; +//! +//! // Define ourselves convenience types. +//! type AESEnc = AES_ECB_128; +//! type AESDec = AES_ECB_128; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // An arbitrary plaintext to encrypt +//! let plaintext = [0x5Au8; 50]; +//! +//! // The streaming (chunked) API allows for data to be handed to the cipher as it arrives, in chunks +//! // of any length, but it will only be processed once a full block has been received. +//! // Here, we will use 7-byte chunks +//! let (mut encryptor, no_iv) = AESEnc::do_encrypt_init(&key).expect("encrypt init"); +//! +//! let mut ciphertext = Vec::new(); +//! +//! for piece in plaintext.chunks(7) { +//! let mut out = [0u8; AES_BLOCK_LEN]; +//! let bytes_written = encryptor.do_encrypt_out(piece, &mut out).expect("encryption"); +//! +//! // If that doesn't complete a block, then nothing is written. +//! if bytes_written != 0 { +//! ciphertext.extend_from_slice(&out[..bytes_written]); +//! } +//! } +//! let (last_block, last_len) = encryptor.do_encrypt_final().expect("padding the final block"); +//! ciphertext.extend_from_slice(&last_block[..last_len]); +//! assert_eq!(ciphertext.len(), 64, "50 bytes padded out to four blocks"); +//! +//! // Decrypt the ciphertext in 19-byte chunks. +//! let mut decryptor = AESDec::do_decrypt_init(&key, &no_iv).expect("decrypt init"); +//! let mut recovered = Vec::new(); +//! for piece in ciphertext.chunks(19) { +//! let mut out = [0u8; AES_BLOCK_LEN]; +//! let bytes_written = decryptor.do_decrypt_out(piece, &mut out).expect("decryption"); +//! if bytes_written != 0 { +//! recovered.extend_from_slice(&out[..bytes_written]); +//! } +//! } +//! let (last_block, last_len) = decryptor.do_decrypt_final().expect("a valid final block"); +//! recovered.extend_from_slice(&last_block[..last_len]); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! ## With no padding scheme +//! +//! With [`NoPadding`] nothing is added, and a message that is not a whole number of blocks is an +//! error rather than something silently padded: +//! +//! ``` +//! use bouncycastle_aes::hazmat::AES_ECB_128; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::SymmetricCipherEncryptor; +//! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_cipher::padding::NoPadding; +//! +//! // Define ourselves a convenience type for the encryption direction with no padding. +//! type Enc = AES_ECB_128; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! +//! // A whole block is fine, and comes out the same length. +//! let mut out = [0u8; 16]; +//! let (_no_iv, written) = Enc::encrypt_out(&key, &[0u8; 16], &mut out).expect("aligned"); +//! assert_eq!(written, 16); +//! +//! // Five bytes is not, and is refused rather than padded. +//! let mut out = [0u8; 16]; +//! assert!(Enc::encrypt_out(&key, b"hello", &mut out).is_err()); +//! ``` +//! +//! The padding scheme is part of the type, so the two schemes are different types and cannot be +//! interchanged. A value built with one will not satisfy a binding annotated with the other. +//! +//! ```compile_fail +//! use bouncycastle_aes::hazmat::AES_ECB_128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::SymmetricCipherEncryptor; +//! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! +//! // Built as NoPadding, annotated as PKCS7: mismatched types. +//! let (enc, _no_iv) = AES_ECB_128::::do_encrypt_init(&key).unwrap(); +//! let _mismatched: AES_ECB_128 = enc; +//! ``` +//! +//! # 🚨 Security Considerations 🚨 +//! +//! All security considerations from [`bouncycastle_cipher::modes::hazmat::Ecb`] apply. Above all, **ECB is not a +//! confidentiality mode for data**: under a given key every plaintext block maps to the same +//! ciphertext block, so the structure of the plaintext shows through, and padding does not change +//! that in the least. It makes ECB accept any length; it does not make it safe. +//! +//! ``` +//! use bouncycastle_aes::hazmat::AES_ECB_128; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::SymmetricCipherEncryptor; +//! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_cipher::padding::NoPadding; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! +//! // Two identical blocks in... +//! let (_, ciphertext) = +//! AES_ECB_128::::encrypt(&key, &[0x5Au8; 32]).expect("encryption"); +//! // ...two identical blocks out. Nothing here chains, so nothing hides the repetition. +//! assert_eq!(ciphertext[..16], ciphertext[16..]); +//! ``` + +use crate::AES_BLOCK_LEN; +use crate::bitslice::Block; +use crate::hazmat::{AES128Internal, AES192Internal, AES256Internal, AESInternal}; +use crate::schedule::AESParams; +use bouncycastle_cipher::Direction; +use bouncycastle_cipher::modes::hazmat::Ecb; +use bouncycastle_cipher::padding::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::KeyMaterial; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +#[allow(unused_imports)] +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +// end of imports needed for docs + +/// AES-128 in ECB mode with a padding scheme. +#[allow(non_camel_case_types)] +pub type AES_ECB_128 =

::Select< + PaddedBlockCipherEncryptor< + Ecb, + Pad, + 16, + 0, + AES_BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Ecb, + Pad, + 16, + 0, + AES_BLOCK_LEN, + >, +>; + +/// AES-192 in ECB mode with a padding scheme. +#[allow(non_camel_case_types)] +pub type AES_ECB_192 = ::Select< + PaddedBlockCipherEncryptor< + Ecb, + Pad, + 24, + 0, + AES_BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Ecb, + Pad, + 24, + 0, + AES_BLOCK_LEN, + >, +>; + +/// AES-256 in ECB mode with a padding scheme. See [`AES_ECB_128`]. +#[allow(non_camel_case_types)] +pub type AES_ECB_256 = ::Select< + PaddedBlockCipherEncryptor< + Ecb, + Pad, + 32, + 0, + AES_BLOCK_LEN, + >, + PaddedBlockCipherDecryptor< + Ecb, + Pad, + 32, + 0, + AES_BLOCK_LEN, + >, +>; + +impl ElectronicCodeBook<16, AES_BLOCK_LEN> for AES128Internal { + fn new(key: &KeyMaterial<16>) -> Result { + AES128Internal::new(key) + } + fn encrypt_block(&self, block: &mut Block) { + Self::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + Self::decrypt_block(self, block) + } + fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + Self::encrypt_2blocks(self, blocks) + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + Self::decrypt_2blocks(self, blocks) + } + fn encrypt_4blocks(&self, blocks: &mut [Block; 4]) { + Self::encrypt_4blocks(self, blocks) + } + fn decrypt_4blocks(&self, blocks: &mut [Block; 4]) { + Self::decrypt_4blocks(self, blocks) + } +} + +impl ElectronicCodeBook<24, AES_BLOCK_LEN> for AES192Internal { + fn new(key: &KeyMaterial<24>) -> Result { + AES192Internal::new(key) + } + fn encrypt_block(&self, block: &mut Block) { + Self::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + Self::decrypt_block(self, block) + } + fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + Self::encrypt_2blocks(self, blocks) + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + Self::decrypt_2blocks(self, blocks) + } + fn encrypt_4blocks(&self, blocks: &mut [Block; 4]) { + Self::encrypt_4blocks(self, blocks) + } + fn decrypt_4blocks(&self, blocks: &mut [Block; 4]) { + Self::decrypt_4blocks(self, blocks) + } +} + +impl ElectronicCodeBook<32, AES_BLOCK_LEN> for AES256Internal { + fn new(key: &KeyMaterial<32>) -> Result { + AES256Internal::new(key) + } + fn encrypt_block(&self, block: &mut Block) { + Self::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + Self::decrypt_block(self, block) + } + fn encrypt_2blocks(&self, blocks: &mut [Block; 2]) { + Self::encrypt_2blocks(self, blocks) + } + fn decrypt_2blocks(&self, blocks: &mut [Block; 2]) { + Self::decrypt_2blocks(self, blocks) + } + fn encrypt_4blocks(&self, blocks: &mut [Block; 4]) { + Self::encrypt_4blocks(self, blocks) + } + fn decrypt_4blocks(&self, blocks: &mut [Block; 4]) { + Self::decrypt_4blocks(self, blocks) + } +} + +impl core::fmt::Debug for AESInternal

{ + /// Prints the algorithm name only. The key schedule is secret and is never formatted. + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(P::ALG_NAME) + } +} diff --git a/crypto/aes/src/hazmat/mod.rs b/crypto/aes/src/hazmat/mod.rs new file mode 100644 index 00000000..4ba527e8 --- /dev/null +++ b/crypto/aes/src/hazmat/mod.rs @@ -0,0 +1,23 @@ +//! Raw primitives whose safe use is the caller's responsibility. +//! +//! An item lives under a `hazmat` module when it is a correct, tested primitive whose +//! *composition* is the caller's responsibility, or that otherwise carry non-trivial +//! Security Considerations which are the caller's responsibility. +//! +//! Part of the design intention is to allow static code analyzers to easily find and flag +//! such uses with a simple search such as +//! +//! ```text +//! grep -rnE --include='*.rs' 'use .*::hazmat::' +//! ``` +//! +//! [`AESInternal`] is the keyed permutation: it transforms exactly one block and is the primitive +//! under every mode in this crate, not a cipher for data. [`AES_ECB_128`] and friends are that +//! permutation applied block by block, with padding; equal plaintext blocks give equal ciphertext +//! blocks, so they are here for interoperability and test vectors. + +mod aes_internal; +mod ecb; + +pub use aes_internal::{AES128Internal, AES192Internal, AES256Internal, AESInternal}; +pub use ecb::{AES_ECB_128, AES_ECB_192, AES_ECB_256}; diff --git a/crypto/aes/src/lib.rs b/crypto/aes/src/lib.rs new file mode 100644 index 00000000..a22061f9 --- /dev/null +++ b/crypto/aes/src/lib.rs @@ -0,0 +1,193 @@ +//! A constant-time, table-free AES block cipher engine (NIST FIPS 197). +//! +//! This crate provides the raw AES keyed permutation implemented as a Boolean circuit over bit-planes +//! rather than as byte substitutions through a lookup table, which makes it both smaller and constant-time; +//! see [Design](#design). +//! +//! This crate also provides various ready-to-use AES-based modes of operation. +//! +//! # Usage Examples +//! +//! The raw AES permutation, [`AESInternal`](hazmat::AESInternal), lives under [`hazmat`] because it +//! is not secure to use by itself; see +//! [A block permutation is not a cipher](#a-block-permutation-is-not-a-cipher) below. +//! +//! 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_GCM](crate::gcm) +//! +//! AES in ECB mode, [`AES_ECB_128`](hazmat::AES_ECB_128) and friends, is under [`hazmat`] because +//! it is a building block for other modes, not itself a confidentiality mode for data. +//! +//! # Design +//! +//! ## No lookup table +//! +//! FIPS 197 Sec 5.1.1 presents the S-box as a table (Table 4), and almost every AES +//! implementation stores it as one -- 256 bytes, or 2-8 KiB for the "T-table" variants that fold +//! MIXCOLUMNS() in. The trouble is that a table indexed by a byte of the state is indexed by +//! secret data, so on any CPU with a data cache the memory access pattern, and hence the timing, +//! depends on the key. That is a practical, repeatedly-demonstrated attack, and it is not fixable +//! with a lookup-table-based implementation. +//! +//! ## Bit-slicing +//! +//! The SBox implementation is borrowed from J. Boyar and R. Peralta, +//! "A new combinational logic minimization technique with applications to cryptology", +//! and the accompanying `SLP_AES_113.txt`. +//! +//! It is "bit-sliced" in the sense that the state is transposed so that each of eight words holds +//! one *bit position* of every byte: word `q[k]` collects bit `k` of all the bytes. In that form +//! the S-box becomes a fixed Boolean circuit -- 32 AND, 77 XOR and 4 XNOR gates, the 113-gate +//! straight-line program of Boyar and Peralta -- and one `&` or `^` applies a gate to every byte +//! position at once. Nothing is ever indexed by a secret, and nothing branches on one. +//! +//! Multiple blocks at once: +//! A single block bitslices into a `[u16; 8]` planes object. Since XOR and XNOR of two u16's, two u32's, or two u64's +//! is still a single operation (at least on a 64-bit machine), we can process two blocks at a time as a `[u32; 8]` +//! or 4 blocks at a time as a `[u64; 8]` for approximately the same cost as a single block. +//! The circuit and the masks cost about the same at every width, so the batched entry points +//! multiply throughput. Measured with the crate's criterion benches on x86-64, 16 KiB per run, +//! relative to the single-block entry point: +//! +//! | Entry point | Encrypt | Decrypt | +//! |---|---|---| +//! | `encrypt_block` / `decrypt_block` (`u16` planes) | 1.0x | 1.0x | +//! | `encrypt_2blocks` / `decrypt_2blocks` (`u32` planes) | 1.75x | 1.95x | +//! | `encrypt_4blocks` / `decrypt_4blocks` (`u64` planes) | 3.0x | 3.7x | +//! +//! The ratios hold for all three key lengths to within a few percent; in absolute terms AES-128 +//! single-block encryption is about 240 us per 16 KiB and decryption about 330 us. That multiplier +//! is what the modes of operation batch through wherever their blocks are independent: both +//! directions of ECB and CTR (and so GCM's CTR half), and the decryption direction of CBC, CFB and +//! CFB8. CBC and CFB *encryption* cannot, because each forward cipher input depends on the previous +//! output (SP 800-38A Sec 6.2 and 6.3), so those two paths run one block at a time. +//! +//! SHIFTROWS() and MIXCOLUMNS() become masks and rotations in the same representation, and the +//! key schedule is stored bit-sliced too, so no transposition happens inside the round loop. The +//! exact bit layout, and the derivation of every mask from it, is documented in the source code of the `bitslice` +//! and `round` modules. +//! +//! Decryption follows FIPS 197 Algorithm 3, the straight inverse cipher, rather than the +//! equivalent inverse cipher of Sec 5.3.5. Algorithm 3 puts INVMIXCOLUMNS() after ADDROUNDKEY(), +//! so it uses the *unmodified* key schedule; the equivalent inverse cipher would need a second +//! schedule with each round key transformed. One [`AES128Internal`](hazmat::AES128Internal) value therefore encrypts and decrypts +//! from one stored schedule. +//! +//! # Memory Usage +//! +//! There are no lookup tables and no heap allocation. The only persistent state is the key +//! schedule, which is `4 * (Nr + 1)` words -- exactly the size FIPS 197 Sec 5.2 defines, with the +//! bit-sliced form stored at the one-block width so that bit-slicing costs nothing in space: +//! +//! | | [`AES128Internal`](hazmat::AES128Internal) | [`AES192Internal`](hazmat::AES192Internal) | [`AES256Internal`](hazmat::AES256Internal) | +//! |---|---|---|---| +//! | Key | 16 B | 24 B | 32 B | +//! | Rounds, `Nr` | 10 | 12 | 14 | +//! | Key schedule, held between calls | 176 B | 208 B | 240 B | +//! | Lookup tables | 0 B | 0 B | 0 B | +//! +//! Per-call stack usage is set by the plane width: 16, 32 or 64 bytes of bit-sliced state for +//! one, two or four blocks, the same again for the round key widened from its stored one-block +//! form, plus the S-box circuit's spills. Only key expansion depends on the key length. Measured +//! as the deepest frame chain below each entry point in the release build (x86-64, return +//! addresses included): +//! +//! | Entry point | AES-128 | AES-192 | AES-256 | +//! |---|---|---|---| +//! | `new` (key expansion) | 312 B | 344 B | 376 B | +//! | `encrypt_block` (`u16` planes) | 208 B | 208 B | 208 B | +//! | `decrypt_block` (`u16` planes) | 208 B | 208 B | 208 B | +//! | `encrypt_2blocks` (`u32` planes) | 240 B | 240 B | 240 B | +//! | `decrypt_2blocks` (`u32` planes) | 224 B | 224 B | 224 B | +//! | `encrypt_4blocks` (`u64` planes) | 320 B | 320 B | 320 B | +//! | `decrypt_4blocks` (`u64` planes) | 352 B | 352 B | 352 B | +//! +//! # 🚨 Security Considerations 🚨 +//! +//! ## A block permutation is not a cipher +//! +//! [`AES128Internal`](hazmat::AES128Internal) and friends transform exactly 16 bytes. +//! Using them directly on data is equivalent to the [Electronic Code Book (ECB)](hazmat::AES_ECB_128) mode, +//! which does not provide proper confidentiality in most contexts since the same plaintext block +//! will produce the same ciphertext block every time, so structure in the plaintext survives encryption. +//! **Do not do it.** Use a ready-to-use mode of operation, and +//! prefer an authenticated one (AEAD) so that ciphertext tampering is detected. That is why the +//! permutation and the ECB aliases live under [`hazmat`]; [`bouncycastle_core::hazmat`] lists the +//! supported uses. +//! +//! ## Constant-time properties +//! +//! By construction there is no secret-dependent memory access and no secret-dependent branch, +//! in the cipher (sbox) *or* in the key load (key schedule expansion) +//! The only branches are the round loops, which count over the public `Nr`. +//! +//! This guarantee is "by construction" at the source code level only since no guarantees can be made +//! against the compiler optimizing the provided code into non-constant time assembly. For uses that +//! require constant-time guarantees that strong, then a library that uses inline assembly for critical +//! sections might be more appropriate. +//! +//! Caveats worth stating plainly: +//! +//! * The Rust compiler makes no guarantee it will preserve this. The code is written so that the +//! natural code generation is straight-line, and `#![forbid(unsafe_code)]` rules out the usual +//! ways of forcing the issue, but the property is not contractual. +//! * The working state (16, 32 or 64 bytes, by the entry point) is not scrubbed after a call. +//! Only the key schedule is wrapped in `Secret`, and so only it is guaranteed to be zeroized on +//! drop. +//! * Constant-time execution says nothing about power or electromagnetic side channels. +//! +//! # Provenance +//! +//! * Normative reference: **NIST FIPS 197** (Advanced Encryption Standard), including Update 1. +//! Every transformation cites its section, algorithm and equation numbers. +//! * The S-box circuit is the 113-gate straight-line program `SLP_AES_113.txt` from Peralta's +//! circuit collection, described in J. Boyar and R. Peralta, "A new combinational logic +//! minimization technique with applications to cryptology", +//! . +//! * The bit-sliced structure, the transpose, and the shape of the SHIFTROWS()/MIXCOLUMNS() +//! mask-and-rotation code are translated from BearSSL's `aes_ct` implementation by Thomas +//! Pornin (MIT licence). The bit layout is not BearSSL's -- blocks sit side by side in 16-bit +//! lanes rather than interleaved bit by bit, so that one set of masks serves every width -- and +//! every constant is derived from the documented layout in the comments and pinned by a test +//! against a byte-wise reference written from the FIPS 197 equations. +//! * Verified against FIPS 197 Appendix A (all three key expansions, every word), FIPS 197 +//! Appendix B, NIST SP 800-38A Appendix F.1 (ECB, all three key lengths, both directions), and +//! the NIST ACVP `ACVP-AES-ECB` vectors. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] +// `AESParams` is deliberately sealed with a private supertrait so that no fourth parameter set can +// be added outside this crate; that is what triggers this lint. +#![allow(private_bounds)] + +mod bitslice; +pub mod cbc; +pub mod ccm; +pub mod cfb; +pub mod cfb8; +pub mod ctr; +pub mod gcm; +pub mod hazmat; +mod round; +mod sbox; +mod schedule; + +/// The AES block length in bytes: 16 (FIPS 197 Sec 3.4, `Nb` = 4 words). +pub const AES_BLOCK_LEN: usize = 16; + +pub use cbc::{AES_CBC_128, AES_CBC_192, AES_CBC_256}; +pub use ccm::{ + AES_CCM_128, AES_CCM_128_Packet, AES_CCM_192, AES_CCM_192_Packet, AES_CCM_256, + AES_CCM_256_Packet, 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 gcm::{AES_GCM_128, AES_GCM_192, AES_GCM_256}; diff --git a/crypto/aes/src/round.rs b/crypto/aes/src/round.rs new file mode 100644 index 00000000..a344693d --- /dev/null +++ b/crypto/aes/src/round.rs @@ -0,0 +1,214 @@ +//! The three linear round transformations, on bit-planes. +//! +//! | Function | FIPS 197 | Inverse | FIPS 197 | +//! |---|---|---|---| +//! | [`add_round_key`] | Sec 5.1.4, Eq 5.9 | itself (XOR) | Sec 5.3.4 | +//! | [`shift_rows`] | Sec 5.1.2, Eq 5.5 | [`inv_shift_rows`] | Sec 5.3.1, Eq 5.12 | +//! | [`mix_columns`] | Sec 5.1.3, Eq 5.8 | [`inv_mix_columns`] | Sec 5.3.3, Eq 5.15 | +//! +//! SUBBYTES() is in [`crate::sbox`], because it is the only non-linear step and the only one that +//! needs a circuit rather than masks and rotations. +//! +//! Everything here is XOR, AND with a constant mask, and rotation by a constant. No operation +//! depends on the data, so all of it is inherently constant-time. +//! +//! # How the layout turns row and column arithmetic into shifts +//! +//! From the layout derived in [`crate::bitslice`], within every plane the bit holding `s[r,c]` +//! sits at bit position `4r + c` of the block's 16-bit lane. Two consequences drive every +//! constant below: +//! +//! * **A row is a nibble.** All of row `r` lives in bits `4r..4r+4` of the lane, and stepping one +//! column along that row is a step of one bit position. So SHIFTROWS(), which only permutes +//! within rows, is a rotation *inside* each nibble, by `r` positions. +//! * **Rotating a lane by 4 changes the row.** `x.rotate_lanes_right(4)` brings the contents of +//! nibble `r+1` into nibble `r`, so a rotation by 4 reads "the next row down" and one by 8 reads +//! "two rows down". MIXCOLUMNS(), which combines the four rows of a column, is therefore +//! expressible with those two rotations and no shuffling at all. +//! +//! Both are the same at every plane width, because a wider plane is just more 16-bit lanes: the +//! masks are 16-bit patterns [`PlaneWord::splat`] replicates into every lane, and the rotations +//! are [`PlaneWord::rotate_lanes_right`], which rotates each lane on its own. That is the whole +//! of what these functions know about the width. +//! +//! Provenance: the structure of each function -- seven masked terms for SHIFTROWS(), the `p`/`r` +//! and rotate-by-two-rows shape of MIXCOLUMNS() and the per-plane term lists of INVMIXCOLUMNS() +//! -- is translated from BearSSL `src/symcipher/aes_ct_enc.c` and `aes_ct_dec.c` (MIT, Thomas +//! Pornin). The mask and rotation constants are not BearSSL's, because the bit layout is not (see +//! the provenance note in [`crate::bitslice`]); each is derived from the layout in the comments +//! below. There are no unit tests here: the FIPS 197 Appendix B, SP 800-38A F.1 and ACVP +//! known-answer tests in `tests/` reach every mask and rotation through the public API, and +//! `cargo mutants` confirms they kill every mutant in this file that is not an equivalent program. + +use crate::bitslice::{PlaneWord, Planes}; + +/// ADDROUNDKEY(): XORs a round key into the state (FIPS 197 Sec 5.1.4, Eq 5.9). +/// +/// Eq 5.9 XORs word `w[4*round + c]` into column `c`. Here the round key has already been +/// bit-sliced into the same plane layout as the state, and replicated into every block's lane, by +/// [`crate::schedule::round_key`], so the whole transformation -- all four columns of every block +/// -- is eight XORs. +/// +/// This is its own inverse, which is why FIPS 197 Sec 5.3.4 needs no separate INVADDROUNDKEY(). +#[inline(always)] +pub(crate) fn add_round_key(q: &mut Planes, round_key: &Planes) { + for (plane, key_plane) in q.iter_mut().zip(round_key.iter()) { + *plane ^= *key_plane; + } +} + +/// SHIFTROWS(): cyclically shifts row `r` left by `r` columns (FIPS 197 Sec 5.1.2, Eq 5.5). +/// +/// Eq 5.5 is `s'[r,c] = s[r,(c + r) mod 4]`. Row `r` occupies nibble `r` of every lane and one +/// column is one bit position, so the new column `c` must take what is `r` positions further up +/// the nibble: a **rotate right by `r` within nibble `r`**. Rotating right, not left, because +/// taking from a higher column index means pulling data down towards bit 0. +/// +/// Written out per nibble rather than as a loop, so the shift amounts stay compile-time +/// constants: +/// +/// * nibble 0 (`r = 0`): rotate by 0, so bits `0..4` pass through untouched. +/// * nibble 1 (`r = 1`): rotate right by 1. Bits 5..8 drop to 4..7; bit 4 wraps to 7. +/// * nibble 2 (`r = 2`): rotate right by 2. Bits 10..12 drop to 8..10; bits 8..10 wrap up. +/// * nibble 3 (`r = 3`): rotate right by 3. Bit 15 drops to 12; bits 12..15 wrap up. +/// +/// The masks are 16-bit patterns, `splat` into every lane, so every block moves together and no +/// bit crosses from one block's lane into another's. +/// +/// The seven masks have pairwise disjoint destination ranges that together cover all 16 bits of +/// a lane, so the `|`s combine disjoint operands and `|` and `^` compute the same function. That +/// is why `cargo mutants` reports every `| -> ^` mutant here and in [`inv_shift_rows`] as +/// surviving: they are equivalent programs, not a gap in the tests. Masks that overlapped or +/// failed to cover would be a bug, and the known-answer tests would catch it. +/// +/// Translated from BearSSL `aes_ct_enc.c:shift_rows`, with the constants re-derived as above. +#[inline(always)] +pub(crate) fn shift_rows(q: &mut Planes) { + for plane in q.iter_mut() { + let x = *plane; + *plane = (x & T::splat(0x000F)) + | ((x & T::splat(0x00E0)) >> 1) + | ((x & T::splat(0x0010)) << 3) + | ((x & T::splat(0x0C00)) >> 2) + | ((x & T::splat(0x0300)) << 2) + | ((x & T::splat(0x8000)) >> 3) + | ((x & T::splat(0x7000)) << 1); + } +} + +/// INVSHIFTROWS(): cyclically shifts row `r` right by `r` columns +/// (FIPS 197 Sec 5.3.1, Eq 5.12). +/// +/// Eq 5.12 is `s'[r,c] = s[r,(c - r) mod 4]`, so this is [`shift_rows`] with every nibble rotation +/// reversed: **rotate left by `r` within nibble `r`**. The masks are the complementary halves of +/// the forward ones. +/// +/// Translated from BearSSL `aes_ct_dec.c:inv_shift_rows`, with the constants re-derived as above. +#[inline(always)] +pub(crate) fn inv_shift_rows(q: &mut Planes) { + for plane in q.iter_mut() { + let x = *plane; + *plane = (x & T::splat(0x000F)) + | ((x & T::splat(0x0070)) << 1) + | ((x & T::splat(0x0080)) >> 3) + | ((x & T::splat(0x0300)) << 2) + | ((x & T::splat(0x0C00)) >> 2) + | ((x & T::splat(0x1000)) << 3) + | ((x & T::splat(0xE000)) >> 1); + } +} + +/// MIXCOLUMNS(): multiplies every column by the fixed matrix of Eq 5.7 +/// (FIPS 197 Sec 5.1.3). +/// +/// # Derivation +/// +/// Eq 5.8 gives each output byte of a column. Collecting the four rows, and writing `s[r]` for +/// the byte in row `r` of the column being processed, every row obeys the same rule: +/// +/// ```text +/// s'[r] = {02}.s[r] ^ {03}.s[r+1] ^ s[r+2] ^ s[r+3] (rows mod 4) +/// = {02}.(s[r] ^ s[r+1]) ^ s[r+1] ^ s[r+2] ^ s[r+3] +/// ``` +/// +/// using `{03} = {02} ^ {01}`. Because "the next row" is a lane rotation by 4 and "two rows +/// down" is one by 8 (see the module docs), with `p` the state planes and `r` = `p` rotated by 4: +/// +/// * `p[k]` is bit `k` of `s[r]`, `r[k]` is bit `k` of `s[r+1]`, +/// * rotating those two by 8 gives bit `k` of `s[r+2]` and of `s[r+3]`. +/// +/// So `s[r+2] ^ s[r+3]` is `(p[k] ^ r[k]).rotate_lanes_right(8)`, which is the rotated term in +/// every line below, and `s[r+1]` is the bare `r[k]`. +/// +/// The remaining `{02}.(s[r] ^ s[r+1])` is XTIMES() (Eq 4.5) in the plane basis. Multiplying by +/// `x` shifts every bit up one plane, and the degree-8 term that falls off the top is reduced by +/// XOR-ing `{1b} = 0b0001_1011` -- bits 0, 1, 3 and 4. So with `v[k] = p[k] ^ r[k]`, plane `k` of +/// `{02}.v` is: +/// +/// * `v[k-1]` from the shift, for `k >= 1` (plane 0 gets nothing from the shift), and +/// * `v[7]`, the reduction, for `k` in {0, 1, 3, 4} only. +/// +/// That is exactly where the extra `p[7] ^ r[7]` terms appear below: in the lines for planes 0, 1, +/// 3 and 4, and nowhere else. Plane 0 is the one line with no `p[k-1] ^ r[k-1]` term. +/// +/// Translated from BearSSL `aes_ct_enc.c:mix_columns`; the equivalence to Eq 5.8 is pinned by +/// the known-answer tests in `tests/`. +#[inline(always)] +pub(crate) fn mix_columns(q: &mut Planes) { + let p = *q; + // r[k] holds the same bit position of the next row down. + let r: Planes = core::array::from_fn(|k| p[k].rotate_lanes_right(4)); + // Two rows down. + let rr = |v: T| v.rotate_lanes_right(8); + + // The `p[7] ^ r[7]` term is the {1b} reduction, present only in planes 0, 1, 3 and 4. + q[0] = p[7] ^ r[7] ^ r[0] ^ rr(p[0] ^ r[0]); + q[1] = p[0] ^ r[0] ^ p[7] ^ r[7] ^ r[1] ^ rr(p[1] ^ r[1]); + q[2] = p[1] ^ r[1] ^ r[2] ^ rr(p[2] ^ r[2]); + q[3] = p[2] ^ r[2] ^ p[7] ^ r[7] ^ r[3] ^ rr(p[3] ^ r[3]); + q[4] = p[3] ^ r[3] ^ p[7] ^ r[7] ^ r[4] ^ rr(p[4] ^ r[4]); + q[5] = p[4] ^ r[4] ^ r[5] ^ rr(p[5] ^ r[5]); + q[6] = p[5] ^ r[5] ^ r[6] ^ rr(p[6] ^ r[6]); + q[7] = p[6] ^ r[6] ^ r[7] ^ rr(p[7] ^ r[7]); +} + +/// INVMIXCOLUMNS(): multiplies every column by the inverse matrix of Eq 5.14 +/// (FIPS 197 Sec 5.3.3). +/// +/// The same shape as [`mix_columns`] -- `r` is the next row down, a lane rotation by 8 reaches two +/// rows further -- but the defining word of Sec 4.3 is `[{0e},{09},{0d},{0b}]` (Eq 5.13) instead +/// of `[{02},{01},{01},{03}]` (Eq 5.6). Those have degree up to 3, so expanding each product +/// through XTIMES() +/// in the plane basis produces many more terms than the forward direction, and the per-plane term +/// lists below are that expansion of Eq 5.15 rather than something readable line by line. +/// +/// The reduction terms are not confined to planes 0, 1, 3 and 4 here, because the higher-degree +/// coefficients feed carries into every plane. +/// +/// Translated from BearSSL `aes_ct_dec.c:inv_mix_columns`. Rather than trust the expansion by +/// inspection, the decryption known-answer tests in `tests/` (SP 800-38A F.1.2/4/6, ACVP) pin it, +/// and the ECB conformance suite checks the two directions are inverses. +#[inline(always)] +#[rustfmt::skip] +pub(crate) fn inv_mix_columns(q: &mut Planes) { + let p = *q; + let r: Planes = core::array::from_fn(|k| p[k].rotate_lanes_right(4)); + let rr = |v: T| v.rotate_lanes_right(8); + + q[0] = p[5] ^ p[6] ^ p[7] ^ r[0] ^ r[5] ^ r[7] + ^ rr(p[0] ^ p[5] ^ p[6] ^ r[0] ^ r[5]); + q[1] = p[0] ^ p[5] ^ r[0] ^ r[1] ^ r[5] ^ r[6] ^ r[7] + ^ rr(p[1] ^ p[5] ^ p[7] ^ r[1] ^ r[5] ^ r[6]); + q[2] = p[0] ^ p[1] ^ p[6] ^ r[1] ^ r[2] ^ r[6] ^ r[7] + ^ rr(p[0] ^ p[2] ^ p[6] ^ r[2] ^ r[6] ^ r[7]); + q[3] = p[0] ^ p[1] ^ p[2] ^ p[5] ^ p[6] ^ r[0] ^ r[2] ^ r[3] ^ r[5] + ^ rr(p[0] ^ p[1] ^ p[3] ^ p[5] ^ p[6] ^ p[7] ^ r[0] ^ r[3] ^ r[5] ^ r[7]); + q[4] = p[1] ^ p[2] ^ p[3] ^ p[5] ^ r[1] ^ r[3] ^ r[4] ^ r[5] ^ r[6] ^ r[7] + ^ rr(p[1] ^ p[2] ^ p[4] ^ p[5] ^ p[7] ^ r[1] ^ r[4] ^ r[5] ^ r[6]); + q[5] = p[2] ^ p[3] ^ p[4] ^ p[6] ^ r[2] ^ r[4] ^ r[5] ^ r[6] ^ r[7] + ^ rr(p[2] ^ p[3] ^ p[5] ^ p[6] ^ r[2] ^ r[5] ^ r[6] ^ r[7]); + q[6] = p[3] ^ p[4] ^ p[5] ^ p[7] ^ r[3] ^ r[5] ^ r[6] ^ r[7] + ^ rr(p[3] ^ p[4] ^ p[6] ^ p[7] ^ r[3] ^ r[6] ^ r[7]); + q[7] = p[4] ^ p[5] ^ p[6] ^ r[4] ^ r[6] ^ r[7] + ^ rr(p[4] ^ p[5] ^ p[7] ^ r[4] ^ r[7]); +} diff --git a/crypto/aes/src/sbox.rs b/crypto/aes/src/sbox.rs new file mode 100644 index 00000000..e4df6d75 --- /dev/null +++ b/crypto/aes/src/sbox.rs @@ -0,0 +1,261 @@ +//! SUBBYTES() and INVSUBBYTES() as a Boolean circuit (FIPS 197 Sec 5.1.1 and Sec 5.3.2). +//! +//! # Why a circuit and not a table +//! +//! FIPS 197 Sec 5.1.1 presents the S-box as a 256-entry lookup table (Table 4). A table lookup +//! indexed by a byte of the state is indexed by *secret data*, and on any CPU with a data cache +//! the access pattern -- hence the timing -- depends on that secret. That is the standard AES +//! cache-timing side channel, and it cannot be closed while keeping the lookup. +//! +//! So this module does not have a table. It computes the same function as Table 4 with AND, XOR +//! and XNOR gates applied to the bit-planes described in [`crate::bitslice`]. Every operation is +//! a straight-line word operation on public *positions*, so there is no secret-dependent memory +//! access and no secret-dependent branch. The two functions here are the only place in the crate +//! where secret data meets non-linear logic; everything else is XOR, rotate and mask. +//! +//! Because the planes hold all sixteen byte positions of every block in the state at once -- one, +//! two or four blocks, by the plane width (see [`crate::bitslice`]) -- one pass of the circuit is +//! the whole SUBBYTES() transformation of all of them, rather than one byte. The circuit is the +//! same gates whatever the width: nothing in it knows where one block ends and the next begins. +//! +//! # What the circuit computes +//! +//! FIPS 197 Sec 5.1.1 defines the S-box as inversion in GF(2^8) followed by an affine map +//! (Eq. 5.2), tabulated in Table 4. The circuit below is the 113-gate straight-line program of +//! Boyar and Peralta -- 32 AND, 77 XOR and 4 XNOR gates -- which computes exactly that, +//! including the affine map and its `{63}` constant (the constant is folded into the four XNORs +//! at the end of the bottom linear transformation). +//! +//! Sources: +//! * The straight-line program `SLP_AES_113.txt`, from Peralta's circuit collection. +//! * J. Boyar and R. Peralta, "A new combinational logic minimization technique with +//! applications to cryptology", . +//! * The same circuit appears in BearSSL `aes_ct.c:br_aes_ct_bitslice_Sbox` (MIT, Thomas +//! Pornin), whose variable naming is kept here so the two can be diffed. BearSSL re-associates +//! two gates in the non-linear section (its `t17`/`t21` differ from the SLP file, computing the +//! same `t21`) and uses a different but equivalent bottom linear transformation; where they +//! disagree this file follows `SLP_AES_113.txt`. +//! +//! The gate list is a mechanical transcription of `SLP_AES_113.txt`: `+` became `^`, `x` became +//! `&`, `#` became `!(.. ^ ..)`, and the SLP variable names are unchanged apart from case. It is +//! not independently meaningful line by line and should not be "tidied"; it is verified as a +//! whole by the known-answer tests in `tests/` (FIPS 197 Appendix B, SP 800-38A F.1 and the ACVP +//! vectors), which push every one of the 256 byte values through it many times over, and +//! `cargo mutants` confirms those tests kill every gate mutation but the one noted at `t37`. +//! There are no unit tests in this file for that reason. +//! +//! # Bit numbering +//! +//! The SLP numbers its inputs `U0..U7` and outputs `S0..S7` with **`U0` as the most significant +//! bit** of the byte, which is the reverse of the plane index. So `U0` is plane `q[7]` and `U7` +//! is plane `q[0]`, and likewise for the outputs. The known-answer tests are what pin this down +//! -- reversing it produces a wrong S-box, not a subtly different one. + +use crate::bitslice::{PlaneWord, Planes}; + +/// SUBBYTES(): applies the AES S-box to every byte position of every block in `q` +/// (FIPS 197 Sec 5.1.1, the transformation tabulated in Table 4). +/// +/// The 113-gate Boyar-Peralta circuit, transcribed from `SLP_AES_113.txt`. See the module docs. +// `#[inline(always)]` is a measured choice: +// Inlining lets the planes live in registers across the whole round. +// On x86-64 that is worth about 15-20%. +#[inline(always)] +pub(crate) fn sbox(q: &mut Planes) { + // SLP inputs U0..U7, most-significant bit first, so U0 is the highest plane. + let u0 = q[7]; + let u1 = q[6]; + let u2 = q[5]; + let u3 = q[4]; + let u4 = q[3]; + let u5 = q[2]; + let u6 = q[1]; + let u7 = q[0]; + + // Top linear transformation (23 gates): the input basis change. + let y14 = u3 ^ u5; + let y13 = u0 ^ u6; + let y9 = u0 ^ u3; + let y8 = u0 ^ u5; + let t0 = u1 ^ u2; + let y1 = t0 ^ u7; + let y4 = y1 ^ u3; + let y12 = y13 ^ y14; + let y2 = y1 ^ u0; + let y5 = y1 ^ u6; + let y3 = y5 ^ y8; + let t1 = u4 ^ y12; + let y15 = t1 ^ u5; + let y20 = t1 ^ u1; + let y6 = y15 ^ u7; + let y10 = y15 ^ t0; + let y11 = y20 ^ y9; + let y7 = u7 ^ y11; + let y17 = y10 ^ y11; + let y19 = y10 ^ y8; + let y16 = t0 ^ y11; + let y21 = y13 ^ y16; + let y18 = u0 ^ y16; + + // Non-linear section (62 gates): the GF(2^8) inversion, and the only ANDs in the circuit. + let t2 = y12 & y15; + let t3 = y3 & y6; + let t4 = t3 ^ t2; + let t5 = y4 & u7; + let t6 = t5 ^ t2; + let t7 = y13 & y16; + let t8 = y5 & y1; + let t9 = t8 ^ t7; + let t10 = y2 & y7; + let t11 = t10 ^ t7; + let t12 = y9 & y11; + let t13 = y14 & y17; + let t14 = t13 ^ t12; + let t15 = y8 & y10; + let t16 = t15 ^ t12; + let t17 = t4 ^ y20; + let t18 = t6 ^ t16; + let t19 = t9 ^ t14; + let t20 = t11 ^ t16; + let t21 = t17 ^ t14; + let t22 = t18 ^ y19; + let t23 = t19 ^ y21; + let t24 = t20 ^ y18; + let t25 = t21 ^ t22; + let t26 = t21 & t23; + let t27 = t24 ^ t26; + let t28 = t25 & t27; + let t29 = t28 ^ t22; + let t30 = t23 ^ t24; + let t31 = t22 ^ t26; + let t32 = t31 & t30; + let t33 = t32 ^ t24; + let t34 = t23 ^ t33; + let t35 = t27 ^ t33; + let t36 = t24 & t35; + // `cargo mutants` reports the `^ -> |` mutant on the next line as surviving. That is a true + // equivalence, not a gap: `t36` and `t34` are never both 1 for any of the 256 possible input + // bytes, so XOR and OR agree here. It is the only one of the circuit's 77 XOR gates with that + // property -- every other `^ -> |` mutant is killed by the known-answer tests in `tests/`. + let t37 = t36 ^ t34; + let t38 = t27 ^ t36; + let t39 = t29 & t38; + let t40 = t25 ^ t39; + let t41 = t40 ^ t37; + let t42 = t29 ^ t33; + let t43 = t29 ^ t40; + let t44 = t33 ^ t37; + let t45 = t42 ^ t41; + let z0 = t44 & y15; + let z1 = t37 & y6; + let z2 = t33 & u7; + let z3 = t43 & y16; + let z4 = t40 & y1; + let z5 = t29 & y7; + let z6 = t42 & y11; + let z7 = t45 & y17; + let z8 = t41 & y10; + let z9 = t44 & y12; + let z10 = t37 & y3; + let z11 = t33 & y4; + let z12 = t43 & y13; + let z13 = t40 & y5; + let z14 = t29 & y2; + let z15 = t42 & y9; + let z16 = t45 & y14; + let z17 = t41 & y8; + + // Bottom linear transformation (28 gates): the output basis change and the affine map of + // Eq. 5.2, whose `{63}` constant is the four XNORs below. + let tc1 = z15 ^ z16; + let tc2 = z10 ^ tc1; + let tc3 = z9 ^ tc2; + let tc4 = z0 ^ z2; + let tc5 = z1 ^ z0; + let tc6 = z3 ^ z4; + let tc7 = z12 ^ tc4; + let tc8 = z7 ^ tc6; + let tc9 = z8 ^ tc7; + let tc10 = tc8 ^ tc9; + let tc11 = tc6 ^ tc5; + let tc12 = z3 ^ z5; + let tc13 = z13 ^ tc1; + let tc14 = tc4 ^ tc12; + let s3 = tc3 ^ tc11; + let tc16 = z6 ^ tc8; + let tc17 = z14 ^ tc10; + let tc18 = tc13 ^ tc14; + let s7 = !(z12 ^ tc18); + let tc20 = z15 ^ tc16; + let tc21 = tc2 ^ z11; + let s0 = tc3 ^ tc16; + let s6 = !(tc10 ^ tc18); + let s4 = tc14 ^ s3; + let s1 = !(s3 ^ tc16); + let tc26 = tc17 ^ tc20; + let s2 = !(tc26 ^ z17); + let s5 = tc21 ^ tc17; + + // SLP outputs S0..S7, most-significant bit first, mirroring the input mapping. + q[7] = s0; + q[6] = s1; + q[5] = s2; + q[4] = s3; + q[3] = s4; + q[2] = s5; + q[1] = s6; + q[0] = s7; +} + +/// INVSUBBYTES(): applies the inverse AES S-box to every byte position of every block in `q` +/// (FIPS 197 Sec 5.3.2, the transformation tabulated in Table 6). +/// +/// Rather than a second 113-gate circuit, this reuses [`sbox`] by conjugating it with the +/// inverse of its affine layer. Writing the S-box of Eq. 5.2 as `S(x) = A(I(x)) ^ {63}`, where +/// `I` is inversion in GF(2^8) and `A` the linear part, and letting `B` be the inverse of `A`: +/// +/// ```text +/// iS(x) = B(S(B(x ^ {63})) ^ {63}) +/// ``` +/// +/// which holds because `I` is an involution: +/// `iS(S(y)) = B(A(I(B(A(I(y)) ^ {63} ^ {63}))) ^ {63} ^ {63}) = y`. +/// +/// So applying [`inv_affine`], then the forward circuit, then [`inv_affine`] again yields the +/// inverse S-box, at the cost of 16 extra XORs and 8 complements instead of a whole second +/// circuit. Verified against Table 6 by the decryption known-answer tests in `tests/` +/// (SP 800-38A F.1.2/4/6 and the ACVP decrypt vectors), and against [`sbox`] by the ECB +/// conformance suite's inverse checks. +/// +/// The derivation and the layer below are from BearSSL `aes_ct_dec.c` +/// (`br_aes_ct_bitslice_invSbox`). +#[inline(always)] +pub(crate) fn inv_sbox(q: &mut Planes) { + inv_affine(q); + sbox(q); + inv_affine(q); +} + +/// `B(x ^ {63})`: the inverse of the affine layer of Eq. 5.2, composed with the constant. +/// +/// The complements on planes 0, 1, 5 and 6 are the `^ {63}`; the eight three-term XORs are `B`. +/// Translated from BearSSL `aes_ct_dec.c:br_aes_ct_bitslice_invSbox`. +#[inline(always)] +fn inv_affine(q: &mut Planes) { + let q0 = !q[0]; + let q1 = !q[1]; + let q2 = q[2]; + let q3 = q[3]; + let q4 = q[4]; + let q5 = !q[5]; + let q6 = !q[6]; + let q7 = q[7]; + q[7] = q1 ^ q4 ^ q6; + q[6] = q0 ^ q3 ^ q5; + q[5] = q7 ^ q2 ^ q4; + q[4] = q6 ^ q1 ^ q3; + q[3] = q5 ^ q0 ^ q2; + q[2] = q4 ^ q7 ^ q1; + q[1] = q3 ^ q6 ^ q0; + q[0] = q2 ^ q5 ^ q7; +} diff --git a/crypto/aes/src/schedule.rs b/crypto/aes/src/schedule.rs new file mode 100644 index 00000000..8fb7176e --- /dev/null +++ b/crypto/aes/src/schedule.rs @@ -0,0 +1,495 @@ +//! KEYEXPANSION() (FIPS 197 Sec 5.2, Algorithm 2) and the per-key-length parameters. +//! +//! # Storage +//! +//! The schedule is `4 * (Nr + 1)` words -- 44, 52 or 60 -- exactly as FIPS 197 Sec 5.2 defines +//! it, so 176, 208 or 240 bytes. It is stored **bit-sliced at the one-block width**: each +//! 16-byte round key is transposed into eight `u16` planes exactly as a block is (see +//! [`crate::bitslice`]), and two planes are kept per `u32` word of the array, so bit-slicing +//! changes nothing about the size. Every block in a wider state is encrypted under the same key, +//! so the round key at `u32` or `u64` width is the `u16` form replicated into every block's lane; +//! [`round_key`] does that replication onto the stack when the round loop needs it, at whatever +//! width the round loop is running. +//! +//! The alternative -- storing a round key per width, or the widest form -- would multiply the +//! size, and holding the classical schedule *and* a bit-sliced copy would be worse still. Since +//! low memory is the point of this crate, neither is done: [`expand`] writes the classical +//! schedule into the final array and then rewrites it in place, one round key at a time, using +//! a few words of stack. In particular, it does not mirror BearSSL's `uint32_t skey[120]` +//! (480-byte) scratch buffer. +//! +//! # Constant-time +//! +//! The key is secret, so SUBWORD() in the expansion has the same table-lookup problem as +//! SUBBYTES() in the cipher, and gets the same treatment: [`sub_word`] routes the word through +//! the bit-sliced circuit in [`crate::sbox`]. A table-driven "light" AES that only removes the +//! tables from the cipher, and not from the key schedule, still leaks through the schedule. + +use crate::bitslice::{Block, PlaneWord, Planes, ortho}; +use crate::sbox::sbox; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; + +/// FIPS 197 Sec 5.2, Table 5: the round constants, `Rcon[j]` for `1 <= j <= 10`. +/// +/// Table 5 gives each as the word `[x, 00, 00, 00]`; only the leftmost byte is ever non-zero, and +/// words are held little-endian here, so the word `Rcon[j]` is just this byte. Indexing is shifted +/// by one against the spec: `Rcon[j - 1]` here is the spec's `Rcon[j]`, since the spec counts from 1. +#[allow(non_upper_case_globals)] +const Rcon: [u32; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; + +/// Prevents a fourth parameter set from being added outside this crate. +/// +/// FIPS 197 Table 3 lists exactly three Key-Block-Round combinations -- AES-128, AES-192 and +/// AES-256 -- and Sec 5 adds that "No other configurations of Rijndael conform to this +/// Standard". Because [`AESParams`] +/// has this private supertrait, only the three types in this module can implement it, so no +/// downstream crate can instantiate the cipher with an unapproved key length or round count. +trait AESParamsInternalTrait {} + +/// The per-key-length constants of FIPS 197 Table 3. +/// +/// This is a trait rather than const generic parameters because the schedule length +/// `4 * (Nr + 1)` cannot be written as an expression over another const parameter on stable +/// const-generics; each implementation spells its own array type out instead. The same pattern is +/// used by the `HashDRBG80090AParams_*` types in `bouncycastle-rng`. +/// +/// Sealed via a private supertrait, so the three types below are the only implementations. The +/// supertrait is named `*InternalTrait` after the pattern of `MLKEMPrivateKeyInternalTrait` in +/// `bouncycastle-mlkem`, which seals its key types the same way. +pub trait AESParams: AESParamsInternalTrait { + /// Key length in bytes: 16, 24 or 32 (FIPS 197 Table 3). + const KEY_LEN: usize; + /// `Nk`, the key length in 32-bit words: 4, 6 or 8 (FIPS 197 Table 3). + const NK: usize; + /// `Nr`, the number of rounds: 10, 12 or 14 (FIPS 197 Table 3). + const NR: usize; + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + const ALG_NAME: &'static str; + /// `[u32; 4 * (NR + 1)]` -- the bit-sliced schedule. See the module docs. + type Schedule: ZeroizablePrimitive + AsRef<[u32]> + AsMut<[u32]>; +} + +/// AES-128 parameters: 16-byte key, `Nk` = 4, `Nr` = 10 (FIPS 197 Table 3). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AES128Params; +/// AES-192 parameters: 24-byte key, `Nk` = 6, `Nr` = 12 (FIPS 197 Table 3). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AES192Params; +/// AES-256 parameters: 32-byte key, `Nk` = 8, `Nr` = 14 (FIPS 197 Table 3). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AES256Params; + +impl AESParamsInternalTrait for AES128Params {} +impl AESParamsInternalTrait for AES192Params {} +impl AESParamsInternalTrait for AES256Params {} + +impl AESParams for AES128Params { + const KEY_LEN: usize = 16; + const NK: usize = 4; + const NR: usize = 10; + const ALG_NAME: &'static str = "AES-128"; + type Schedule = [u32; 44]; // 4 * (10 + 1) +} + +impl AESParams for AES192Params { + const KEY_LEN: usize = 24; + const NK: usize = 6; + const NR: usize = 12; + const ALG_NAME: &'static str = "AES-192"; + type Schedule = [u32; 52]; // 4 * (12 + 1) +} + +impl AESParams for AES256Params { + const KEY_LEN: usize = 32; + const NK: usize = 8; + const NR: usize = 14; + const ALG_NAME: &'static str = "AES-256"; + type Schedule = [u32; 60]; // 4 * (14 + 1) +} + +/// ROTWORD(): `[a0,a1,a2,a3] -> [a1,a2,a3,a0]` (FIPS 197 Sec 5.2, Eq 5.10). +/// +/// Words are held little-endian, so `a0` is the low byte. Moving `a1` down into the low byte and +/// wrapping `a0` to the top is a rotate right by 8 of the whole word. +#[inline(always)] +fn rot_word(word: u32) -> u32 { + word.rotate_right(8) +} + +/// [`sbox`] is `#[inline(always)]` for the round loop's performance benefit. +/// Here, that would inline the circuit into `sub_word`'s stack frame, which sits on top of the +/// schedule being built. +/// `#[inline(never)]` here trades 120 bytes lower stack usage of running the key schedule generation +/// against roughly 2 percent performance for this one-off operation. +#[inline(never)] +fn sbox_out_of_line(q: &mut Planes) { + sbox(q) +} + +/// SUBWORD(): applies the S-box to each of the four bytes of a word +/// (FIPS 197 Sec 5.2, Eq 5.11). +/// +/// The key is secret, so this must not be a table lookup. It reuses the bit-sliced circuit +/// instead, by replicating `word` into all eight planes before transposing: +/// +/// after [`ortho`], plane `q[k]` bit `8L + i` equals bit `8L + k` of the *input* word `q[i]` -- +/// and every input word is the same `word`, so that bit is bit `k` of byte `L` of `word` +/// regardless of `i`. So the transposed state holds byte `L` of `word` in all eight positions of +/// byte-lane `L`; since the S-box circuit acts on each bit position independently, it does not +/// matter that this is not the block layout of [`crate::bitslice`]. One S-box pass substitutes +/// all four bytes (eight times over, redundantly), and transposing back reassembles the word. All +/// eight planes then hold the same result, so `q[0]` is SUBWORD(`word`); +/// `test_sub_word_fills_every_plane` checks that. +/// +/// It costs a full 113-gate S-box evaluation to substitute four bytes, which is wasteful, but it +/// happens `Nr` or so times per key rather than per block. Translated from BearSSL +/// `aes_ct.c:sub_word`. +fn sub_word(word: u32) -> u32 { + let mut q: Planes = [word; 8]; + ortho(&mut q); + sbox_out_of_line(&mut q); + ortho(&mut q); + // The word was broadcast into all eight planes, so all eight must carry the same answer. + debug_assert!(q.iter().all(|&plane| plane == q[0]), "the eight broadcast planes must agree"); + q[0] +} + +/// KEYEXPANSION() (FIPS 197 Sec 5.2, Algorithm 2), returning the bit-sliced schedule. +/// +/// `key` must be exactly `P::KEY_LEN` bytes; [`crate::hazmat::AESInternal`] checks that before calling, so this +/// cannot fail and takes no `Result`. +/// +/// Algorithm 2 is followed literally -- lines 2-6 copy the key into `w[0..Nk]`, lines 7-16 derive +/// the rest -- and then the finished schedule is rewritten in place into the storage form +/// described in the module docs. Verified against the worked expansions in FIPS 197 +/// Appendix A.1, A.2 and A.3 by the tests at the bottom of this file, which unpack the stored +/// schedule and compare every `w[i]`. +pub(crate) fn expand(key: &[u8]) -> Secret { + debug_assert_eq!(key.len(), P::KEY_LEN); + + let mut schedule = Secret::::new(); + let w = (*schedule).as_mut(); + + // Algorithm 2 lines 2-6: w[i] = key[4i .. 4i+3] for i < Nk. + for i in 0..P::NK { + // Cannot fail: `key` is P::KEY_LEN == 4 * P::NK bytes, so this window is in bounds. + w[i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().unwrap()); + } + + // Algorithm 2 lines 7-16. + let mut temp = w[P::NK - 1]; // line 8, hoisted: w[i-1] is the temp from the previous pass + for i in P::NK..w.len() { + if i % P::NK == 0 { + // line 10: temp = SUBWORD(ROTWORD(temp)) XOR Rcon[i / Nk] + temp = sub_word(rot_word(temp)) ^ Rcon[i / P::NK - 1]; + } else if P::NK > 6 && i % P::NK == 4 { + // lines 11-12: the extra substitution that only AES-256 reaches + temp = sub_word(temp); + } + // line 14: w[i] = w[i - Nk] XOR temp + temp ^= w[i - P::NK]; + w[i] = temp; + } + + // Rewrite in place into the bit-sliced form, one 4-word round key at a time. A round key is + // the 16 bytes of w[4*round .. 4*round + 4], which by Eq 3.6 is a block with s[r,c] the byte + // `r` of word `c`, so it is transposed exactly as a block is, at the one-block width. The + // eight `u16` planes go back into the same four `u32` slots, two per word. + for base in (0..w.len()).step_by(4) { + let mut block: Block = [0; crate::AES_BLOCK_LEN]; + for c in 0..4 { + block[4 * c..4 * c + 4].copy_from_slice(&w[base + c].to_le_bytes()); + } + // `from_ref` rather than `&[block]`, so the round key is transposed where it was built + // and never copied into a one-element array first. + let q = u16::pack(core::array::from_ref(&block)); + for j in 0..4 { + // The two halves are disjoint, so `|` and `^` agree here; that is why `cargo mutants` + // reports the `| -> ^` mutant on this line as surviving. + w[base + j] = u32::from(q[2 * j]) | (u32::from(q[2 * j + 1]) << 16); + } + } + + schedule +} + +/// Widens round key `round` of the schedule into its eight-plane form at plane width `T`. +/// +/// The inverse of the packing at the end of [`expand`], followed by the replication: each stored +/// `u16` plane is unpacked from its half of a `u32` word and [`PlaneWord::splat`] into every +/// block's lane, which is the round key [`crate::round::add_round_key`] expects, since every +/// block is under the same key. Eight words of stack at the width of the state, built fresh each +/// round rather than stored. +/// +/// Corresponds to BearSSL `aes_ct.c:br_aes_ct_skey_expand`, which does the same job for its own +/// (interleaved) layout. +#[inline(always)] +pub(crate) fn round_key( + schedule: &P::Schedule, + round: usize, +) -> Planes { + debug_assert!(round <= P::NR); + let w = schedule.as_ref(); + let mut sk: Planes = [T::splat(0); 8]; + for j in 0..4 { + let packed = w[4 * round + j]; + // `as u16` truncates to the low half, which is the intent: plane 2j is in the low half + // and plane 2j + 1 in the high half. + sk[2 * j] = T::splat(packed as u16); + sk[2 * j + 1] = T::splat((packed >> 16) as u16); + } + sk +} + +#[cfg(test)] +mod tests { + use super::*; + + /// FIPS 197 Appendix A.1: every w[i] of the AES-128 key expansion, as printed + /// (i.e. the byte sequence [a0,a1,a2,a3] read left to right). + #[rustfmt::skip] + const APPENDIX_A1_WORDS: [u32; 44] = [ + 0x2b7e1516, 0x28aed2a6, 0xabf71588, 0x09cf4f3c, + 0xa0fafe17, 0x88542cb1, 0x23a33939, 0x2a6c7605, + 0xf2c295f2, 0x7a96b943, 0x5935807a, 0x7359f67f, + 0x3d80477d, 0x4716fe3e, 0x1e237e44, 0x6d7a883b, + 0xef44a541, 0xa8525b7f, 0xb671253b, 0xdb0bad00, + 0xd4d1c6f8, 0x7c839d87, 0xcaf2b8bc, 0x11f915bc, + 0x6d88a37a, 0x110b3efd, 0xdbf98641, 0xca0093fd, + 0x4e54f70e, 0x5f5fc9f3, 0x84a64fb2, 0x4ea6dc4f, + 0xead27321, 0xb58dbad2, 0x312bf560, 0x7f8d292f, + 0xac7766f3, 0x19fadc21, 0x28d12941, 0x575c006e, + 0xd014f9a8, 0xc9ee2589, 0xe13f0cc8, 0xb6630ca6, + ]; + + /// FIPS 197 Appendix A.2: every w[i] of the AES-192 key expansion, as printed. + #[rustfmt::skip] + const APPENDIX_A2_WORDS: [u32; 52] = [ + 0x8e73b0f7, 0xda0e6452, 0xc810f32b, 0x809079e5, + 0x62f8ead2, 0x522c6b7b, 0xfe0c91f7, 0x2402f5a5, + 0xec12068e, 0x6c827f6b, 0x0e7a95b9, 0x5c56fec2, + 0x4db7b4bd, 0x69b54118, 0x85a74796, 0xe92538fd, + 0xe75fad44, 0xbb095386, 0x485af057, 0x21efb14f, + 0xa448f6d9, 0x4d6dce24, 0xaa326360, 0x113b30e6, + 0xa25e7ed5, 0x83b1cf9a, 0x27f93943, 0x6a94f767, + 0xc0a69407, 0xd19da4e1, 0xec1786eb, 0x6fa64971, + 0x485f7032, 0x22cb8755, 0xe26d1352, 0x33f0b7b3, + 0x40beeb28, 0x2f18a259, 0x6747d26b, 0x458c553e, + 0xa7e1466c, 0x9411f1df, 0x821f750a, 0xad07d753, + 0xca400538, 0x8fcc5006, 0x282d166a, 0xbc3ce7b5, + 0xe98ba06f, 0x448c773c, 0x8ecc7204, 0x01002202, + ]; + + /// FIPS 197 Appendix A.3: every w[i] of the AES-256 key expansion, as printed. + #[rustfmt::skip] + const APPENDIX_A3_WORDS: [u32; 60] = [ + 0x603deb10, 0x15ca71be, 0x2b73aef0, 0x857d7781, + 0x1f352c07, 0x3b6108d7, 0x2d9810a3, 0x0914dff4, + 0x9ba35411, 0x8e6925af, 0xa51a8b5f, 0x2067fcde, + 0xa8b09c1a, 0x93d194cd, 0xbe49846e, 0xb75d5b9a, + 0xd59aecb8, 0x5bf3c917, 0xfee94248, 0xde8ebe96, + 0xb5a9328a, 0x2678a647, 0x98312229, 0x2f6c79b3, + 0x812c81ad, 0xdadf48ba, 0x24360af2, 0xfab8b464, + 0x98c5bfc9, 0xbebd198e, 0x268c3ba7, 0x09e04214, + 0x68007bac, 0xb2df3316, 0x96e939e4, 0x6c518d80, + 0xc814e204, 0x76a9fb8a, 0x5025c02d, 0x59c58239, + 0xde136967, 0x6ccc5a71, 0xfa256395, 0x9674ee15, + 0x5886ca5d, 0x2e2f31d7, 0x7e0af1fa, 0x27cf73c3, + 0x749c47ab, 0x18501dda, 0xe2757e4f, 0x7401905a, + 0xcafaaae3, 0xe4d59b34, 0x9adf6ace, 0xbd10190d, + 0xfe4890d1, 0xe6188d0b, 0x046df344, 0x706c631e, + ]; + + /// Recovers the classical `w[i]` from a stored schedule. + /// + /// [`round_key`] at the one-block width gives the round key as eight `u16` planes, and + /// unpacking those as a block undoes the bit-slicing, leaving the round key's 16 bytes with + /// `w[4*round + c]` at bytes `4c..4c+4`. This is what lets the Appendix A vectors test the + /// real [`expand`] output rather than a reimplementation of it. + fn classical_word(schedule: &P::Schedule, i: usize) -> u32 { + let q = round_key::(schedule, i / 4); + let mut block = [[0u8; 16]]; + let mut q = q; + u16::unpack(&mut q, &mut block); + let c = i % 4; + u32::from_le_bytes(block[0][4 * c..4 * c + 4].try_into().unwrap()) + } + + /// Compares a whole expansion against an Appendix A table. + /// + /// Appendix A prints a word as the byte sequence `[a0,a1,a2,a3]` left to right, so the + /// tabulated `u32` has `a0` in its *most* significant byte; words are held little-endian + /// here, so `swap_bytes` is the conversion. + fn assert_expansion_matches(key: &[u8], expected: &[u32], label: &str) { + let schedule = expand::

(key); + assert_eq!(expected.len(), 4 * (P::NR + 1), "{label}: table length"); + for (i, &want) in expected.iter().enumerate() { + let got = classical_word::

(&schedule, i).swap_bytes(); + assert_eq!(got, want, "{label}: w[{i}] should be {want:#010x}, got {got:#010x}"); + } + } + + #[test] + fn test_key_expansion_matches_fips197_appendix_a1() { + let key = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, + 0x4f, 0x3c, + ]; + assert_expansion_matches::(&key, &APPENDIX_A1_WORDS, "Appendix A.1"); + } + + #[test] + fn test_key_expansion_matches_fips197_appendix_a2() { + let key = [ + 0x8e, 0x73, 0xb0, 0xf7, 0xda, 0x0e, 0x64, 0x52, 0xc8, 0x10, 0xf3, 0x2b, 0x80, 0x90, + 0x79, 0xe5, 0x62, 0xf8, 0xea, 0xd2, 0x52, 0x2c, 0x6b, 0x7b, + ]; + assert_expansion_matches::(&key, &APPENDIX_A2_WORDS, "Appendix A.2"); + } + + #[test] + fn test_key_expansion_matches_fips197_appendix_a3() { + let key = [ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, + 0x77, 0x81, 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, + 0x09, 0x14, 0xdf, 0xf4, + ]; + assert_expansion_matches::(&key, &APPENDIX_A3_WORDS, "Appendix A.3"); + } + + #[test] + fn test_the_first_nk_schedule_words_are_the_key_itself() { + // Algorithm 2 lines 2-6, and a check that the expansion is reading the key + // little-endian consistently with how Appendix A prints it. + let key = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, + 0x4f, 0x3c, + ]; + let schedule = expand::(&key); + for i in 0..AES128Params::NK { + let got = classical_word::(&schedule, i); + assert_eq!(got.to_le_bytes(), key[4 * i..4 * i + 4]); + } + } + + #[test] + fn test_rot_word_matches_equation_5_10() { + // FIPS 197 Eq 5.10 on the byte sequence [a0,a1,a2,a3] = [0x09,0xcf,0x4f,0x3c], which is + // the temp at i = 4 of Appendix A.1, whose ROTWORD() the appendix gives as cf4f3c09. + let word = u32::from_le_bytes([0x09, 0xcf, 0x4f, 0x3c]); + assert_eq!(rot_word(word).to_le_bytes(), [0xcf, 0x4f, 0x3c, 0x09]); + } + + #[test] + fn test_sub_word_matches_the_appendix_a1_example() { + // Appendix A.1, i = 4: "After ROTWORD()" is cf4f3c09 and "After SUBWORD()" is 8a84eb01. + // The appendix prints a word as the byte sequence [a0,a1,a2,a3]; words are held + // little-endian here, so `a0` is the low byte. + let after_rot = u32::from_le_bytes([0xcf, 0x4f, 0x3c, 0x09]); + assert_eq!(sub_word(after_rot).to_le_bytes(), [0x8a, 0x84, 0xeb, 0x01]); + } + + #[test] + fn test_sub_word_fills_every_plane() { + // The doc comment claims all eight planes end up holding SUBWORD(word); if that ever + // stopped being true, picking q[0] would be an arbitrary choice rather than a correct one. + let word = 0x1234_5678u32; + let mut q: Planes = [word; 8]; + ortho(&mut q); + sbox(&mut q); + ortho(&mut q); + assert!(q.iter().all(|&plane| plane == q[0])); + assert_eq!(q[0], sub_word(word)); + } + + #[test] + fn test_round_key_is_the_bit_sliced_round_key_at_every_width() { + // Round-tripping a known schedule: expand(), then round_key() for every round and width, + // and check the recovered planes match bit-slicing the classical round key directly as + // a block -- one copy of it per lane. + let key = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, + 0x4f, 0x3c, + ]; + let schedule = expand::(&key); + + // Recompute the classical schedule without the compression step. + let mut w = [0u32; 44]; + for i in 0..4 { + w[i] = u32::from_le_bytes(key[4 * i..4 * i + 4].try_into().unwrap()); + } + let mut temp = w[3]; + for i in 4..44 { + if i % 4 == 0 { + temp = sub_word(rot_word(temp)) ^ Rcon[i / 4 - 1]; + } + temp ^= w[i - 4]; + w[i] = temp; + } + + for round in 0..=AES128Params::NR { + let mut block = [0u8; 16]; + for c in 0..4 { + block[4 * c..4 * c + 4].copy_from_slice(&w[4 * round + c].to_le_bytes()); + } + assert_eq!( + round_key::(&schedule, round), + u16::pack(&[block]), + "round {round}, u16" + ); + assert_eq!( + round_key::(&schedule, round), + u32::pack(&[block; 2]), + "round {round}, u32" + ); + assert_eq!( + round_key::(&schedule, round), + u64::pack(&[block; 4]), + "round {round}, u64" + ); + } + } + + #[test] + fn test_schedule_lengths_match_four_times_nr_plus_one() { + // FIPS 197 Sec 5.2: the schedule is 4 * (Nr + 1) words. The array types are written out + // by hand per parameter set, so this guards against a typo in one of them. + assert_eq!( + size_of::<::Schedule>() / 4, + 4 * (AES128Params::NR + 1) + ); + assert_eq!( + size_of::<::Schedule>() / 4, + 4 * (AES192Params::NR + 1) + ); + assert_eq!( + size_of::<::Schedule>() / 4, + 4 * (AES256Params::NR + 1) + ); + } + + #[test] + fn test_key_len_is_four_times_nk() { + // FIPS 197 Sec 6.1 ties the two together; both are declared independently above. + assert_eq!(AES128Params::KEY_LEN, 4 * AES128Params::NK); + assert_eq!(AES192Params::KEY_LEN, 4 * AES192Params::NK); + assert_eq!(AES256Params::KEY_LEN, 4 * AES256Params::NK); + } + + #[test] + fn test_rcon_table_5_values() { + // FIPS 197 Sec 5.2: "for j > 0, these bytes may be generated by successively applying + // XTIMES() to the byte represented by x^(j-1)". Derive the table and compare, so a typo + // in the transcription of Table 5 shows up here. + let mut expected = [0u32; 10]; + let mut v: u8 = 0x01; + for slot in expected.iter_mut() { + *slot = u32::from(v); + v = (v << 1) ^ if v & 0x80 != 0 { 0x1b } else { 0 }; + } + assert_eq!(Rcon, expected); + // Spot-check the two values from Table 5 that are not plain powers of two. + assert_eq!(Rcon[8], 0x1b); + assert_eq!(Rcon[9], 0x36); + } +} diff --git a/crypto/aes/tests/aes_internal_tests.rs b/crypto/aes/tests/aes_internal_tests.rs new file mode 100644 index 00000000..b7fc5632 --- /dev/null +++ b/crypto/aes/tests/aes_internal_tests.rs @@ -0,0 +1,33 @@ +//! The contract of the three `AESInternal` engines that is neither a known-answer value nor a +//! trait conformance property: their size, name and strength. +//! +//! The "Memory Usage" table in the crate docs quotes the sizes, and the whole point of the crate +//! is that they are this small: `4 * (Nr + 1)` words of schedule (FIPS 197 Sec 5.2), nothing +//! else, and no tables anywhere. A size that matches `4 * (Nr + 1) * 4` bytes exactly also shows +//! there is no round counter, direction flag or initialised marker alongside the schedule, which +//! is what lets both directions run from one value. If the representation grows, the docs are +//! wrong -- fix both. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; + +/// One check per engine: the size is exactly the schedule, the name is the FIPS 197 name, and +/// the strength is the key length (FIPS 197 Sec 6.1 ties the three key lengths to 128, 192 and +/// 256 bits). +fn check_engine(nr: usize, key_len: usize, name: &str) { + assert_eq!(size_of::(), 4 * (nr + 1) * 4, "{name}: 4 * (Nr + 1) words, nothing else"); + assert_eq!(A::ALG_NAME, name); + assert_eq!(A::MAX_SECURITY_STRENGTH, SecurityStrength::from_bytes(key_len)); +} + +#[test] +fn the_engines_match_the_documented_memory_table_names_and_strengths() { + check_engine::(10, 16, "AES-128"); + check_engine::(12, 24, "AES-192"); + check_engine::(14, 32, "AES-256"); + // The literal figures the crate docs' table quotes, so a wrong `nr` above cannot hide one. + assert_eq!(size_of::(), 176); + assert_eq!(size_of::(), 208); + assert_eq!(size_of::(), 240); +} diff --git a/crypto/aes/tests/cbc_alias_tests.rs b/crypto/aes/tests/cbc_alias_tests.rs new file mode 100644 index 00000000..5441cbf7 --- /dev/null +++ b/crypto/aes/tests/cbc_alias_tests.rs @@ -0,0 +1,198 @@ +//! Tests for the padded AES-CBC aliases. +//! +//! The aliases are only type aliases, so what is worth testing is that they name the *right* types +//! and that both parameters actually select: the direction picks the encryptor or the decryptor, and +//! the padding scheme changes the behaviour rather than being decorative. The mode and the padding +//! layer are tested in their own crates; this checks the wiring between them. + +use bouncycastle_aes::hazmat::AES128Internal; +use bouncycastle_aes::{AES_CBC_128, AES_CBC_192, AES_CBC_256}; +use bouncycastle_cipher::modes::Cbc; +use bouncycastle_cipher::padding::{ + NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, +}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; + +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 aliases must resolve to exactly the adapters they claim to, at both directions. +/// +/// A type alias that quietly resolved to something else -- the wrong padding, the wrong direction, +/// the wrong key length -- would still compile everywhere it is used, so this pins the projection +/// itself by asserting the layouts coincide with the fully spelled-out types. +#[test] +fn the_aliases_name_the_expected_types() { + use core::mem::size_of; + + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherEncryptor, PKCS7, 16, 16, 16>, + >() + ); + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherDecryptor, PKCS7, 16, 16, 16>, + >() + ); + + // The two directions are genuinely different types, so the encryptor and the decryptor do not + // have to agree in size -- and here they do not, which is itself evidence the projection + // selected two different adapters rather than one. + assert_ne!( + size_of::>(), + size_of::>() + ); +} + +/// Every key length round-trips through its alias, at a length that needs padding and one that does +/// not. +#[test] +fn every_key_length_round_trips() { + fn check(name: &str) + where + Enc: SymmetricCipherEncryptor, + Dec: SymmetricCipherDecryptor, + { + for len in [0usize, 1, 15, 16, 17, 63, 64] { + let plaintext: Vec = (0..len).map(|i| (i * 11 + 3) as u8).collect(); + let (iv, ciphertext) = Enc::encrypt(&key::(), &plaintext).expect("encryption"); + + // PKCS#7 always adds at least one byte, and rounds up to a whole block. + assert_eq!( + ciphertext.len(), + (len / 16 + 1) * 16, + "{name}, len {len}: PKCS7 pads up to the next whole block" + ); + + let recovered = Dec::decrypt(&key::(), &iv, &ciphertext).expect("decryption"); + assert_eq!(recovered, plaintext, "{name}, len {len}: round trip"); + } + } + + check::<16, AES_CBC_128, AES_CBC_128>("AES-128"); + check::<24, AES_CBC_192, AES_CBC_192>("AES-192"); + check::<32, AES_CBC_256, AES_CBC_256>("AES-256"); +} + +/// The padding parameter must actually select the scheme, not merely be carried around. +/// +/// `PKCS7` accepts any length and always grows the message; `NoPadding` accepts only whole blocks +/// and never grows it. Checking both against the same alias, key and plaintext is what proves the +/// parameter reaches the behaviour. +#[test] +fn the_padding_parameter_selects_the_scheme() { + type Pkcs7Enc = AES_CBC_128; + type NoPadEnc = AES_CBC_128; + + // A whole block: both schemes accept it, and they disagree about the length. + let aligned = [0x5Au8; 16]; + let (_, pkcs7) = Pkcs7Enc::encrypt(&key::<16>(), &aligned).expect("PKCS7 accepts aligned data"); + let (_, nopad) = NoPadEnc::encrypt(&key::<16>(), &aligned).expect("NoPadding accepts it too"); + assert_eq!(pkcs7.len(), 32, "PKCS7 adds a whole block of padding to aligned data"); + assert_eq!(nopad.len(), 16, "NoPadding adds nothing"); + + // Five bytes: PKCS7 pads it, NoPadding refuses rather than silently padding. + let unaligned = b"hello"; + assert!(Pkcs7Enc::encrypt(&key::<16>(), unaligned).is_ok(), "PKCS7 pads a partial block"); + assert!( + NoPadEnc::encrypt(&key::<16>(), unaligned).is_err(), + "NoPadding must refuse a message that is not a whole number of blocks" + ); +} + +/// A ciphertext made under one scheme must not decrypt cleanly under the other. +/// +/// This is the practical reason the scheme is named in the type: the two are not interchangeable, +/// and without the type parameter nothing would stop a caller pairing them. +#[test] +fn the_two_schemes_are_not_interchangeable() { + let aligned = [0x5Au8; 16]; + let (iv, pkcs7) = + AES_CBC_128::::encrypt(&key::<16>(), &aligned).expect("encryption"); + + // NoPadding will hand back the padded block as if it were data, so it "succeeds" with the + // wrong answer -- exactly the silent mismatch the type parameter is there to prevent. + let as_nopad = AES_CBC_128::::decrypt(&key::<16>(), &iv, &pkcs7) + .expect("NoPadding cannot tell that the trailing block is padding"); + assert_ne!(as_nopad, aligned, "the recovered data must not match the original"); + assert_eq!(as_nopad.len(), 32, "it keeps the padding block as data"); + + // ...and the matching scheme gets it right. + let correct = + AES_CBC_128::::decrypt(&key::<16>(), &iv, &pkcs7).expect("decryption"); + assert_eq!(correct, aligned); +} + +/// The IV is generated per encryption, so the same plaintext gives different ciphertext. +#[test] +fn each_encryption_gets_a_fresh_iv() { + let plaintext = [0x77u8; 32]; + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..16 { + let (iv, ct) = AES_CBC_128::::encrypt(&key::<16>(), &plaintext).unwrap(); + assert!(seen.insert(iv), "IV repeated across encryptions"); + let back = AES_CBC_128::::decrypt(&key::<16>(), &iv, &ct).unwrap(); + assert_eq!(back, plaintext); + } +} + +/// The allocating streaming wrappers `do_encrypt` / `do_decrypt` return exactly what their `_out` +/// counterparts write -- `do_*_out_len` bytes, including none when a piece is wholly buffered or +/// held back -- and a message streamed through them round-trips. +#[test] +fn the_allocating_streaming_wrappers_match_the_out_versions() { + type Enc = AES_CBC_128; + type Dec = AES_CBC_128; + + let plaintext: Vec = (0u8..40).collect(); + // Piece lengths 5, 11, 1, 20, 0, 3: CBC releases a block only once it is full, so these + // release 0, 16, 0, 16, 0, 0 bytes, leaving 8 buffered for `do_encrypt_final` to pad. + let pieces = [0..5, 5..16, 16..17, 17..37, 37..37, 37..40]; + let expected_released = [0, 16, 0, 16, 0, 0]; + + let (mut enc, iv) = Enc::do_encrypt_init(&key::<16>()).unwrap(); + let mut ciphertext = Vec::new(); + for (range, expected) in pieces.into_iter().zip(expected_released) { + let piece = &plaintext[range]; + let mut via_out = vec![0u8; enc.do_encrypt_out_len(piece.len())]; + let n = enc.clone().do_encrypt_out(piece, &mut via_out).unwrap(); + assert_eq!(n, via_out.len()); + + let released = enc.do_encrypt(piece).unwrap(); + assert_eq!(released.len(), expected, "piece of {} bytes", piece.len()); + assert_eq!(released, via_out, "do_encrypt must match do_encrypt_out"); + ciphertext.extend_from_slice(&released); + } + let (last, last_len) = enc.do_encrypt_final().unwrap(); + ciphertext.extend_from_slice(&last[..last_len]); + assert_eq!(ciphertext.len(), 48); + assert_eq!(Dec::decrypt(&key::<16>(), &iv, &ciphertext).unwrap(), plaintext); + + // Decrypt the same ciphertext through `do_decrypt` in uneven pieces: the decryptor holds back + // the block that might carry the padding, so some pieces release nothing. + let mut dec = Dec::do_decrypt_init(&key::<16>(), &iv).unwrap(); + let mut recovered = Vec::new(); + let mut saw_empty = false; + for range in [0..16, 16..17, 17..48] { + let piece = &ciphertext[range]; + let mut via_out = vec![0u8; dec.do_decrypt_out_len(piece.len())]; + let n = dec.clone().do_decrypt_out(piece, &mut via_out).unwrap(); + assert_eq!(n, via_out.len()); + + let released = dec.do_decrypt(piece).unwrap(); + assert_eq!(released, via_out, "do_decrypt must match do_decrypt_out"); + saw_empty |= released.is_empty(); + recovered.extend_from_slice(&released); + } + assert!(saw_empty, "the piece lengths must exercise the held-back case"); + let (last, data_len) = dec.do_decrypt_final().unwrap(); + recovered.extend_from_slice(&last[..data_len]); + assert_eq!(recovered, plaintext); +} diff --git a/crypto/aes/tests/cbc_bc-test-data.rs b/crypto/aes/tests/cbc_bc-test-data.rs new file mode 100644 index 00000000..64f5ddf7 --- /dev/null +++ b/crypto/aes/tests/cbc_bc-test-data.rs @@ -0,0 +1,259 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-CBC` 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 ML-KEM, ML-DSA and `aes` suites -- `cargo test` +//! must stay green for someone who has only cloned this repository. +//! +//! These are the counterpart to `ecb_bc-test-data.rs`, which consumes the +//! `ACVP-AES-ECB` file to test the raw permutation. CBC is a mode, so its vectors belong here. +//! +//! # Joining the request and response files +//! +//! Unlike the ECB response file, which echoes `key`, `pt` and `ct` for every case, the CBC response +//! file carries **only the answer** (`ct` for an encrypt group, `pt` for a decrypt group) against a +//! `tcId`. The key, IV and input live in the request file, and the group metadata that says which +//! direction a case is -- `direction` and `keyLen` -- lives only there too. So both files are read +//! and joined on `tcId`; there is no way to drive this from the response file alone. +//! +//! # Coverage +//! +//! 2150 AFT (Algorithm Functional Test) cases across all three key lengths and both directions, +//! including 60 whose payload spans 2 to 10 blocks. Every case is run **twice**: once block by +//! block, and once in pairs with a one-block remainder for odd lengths. The second pass is what +//! puts the multi-block cases through `ElectronicCodeBook::decrypt_2blocks`, so the pair path is +//! exercised against real vectors and not only against the toy in `cbc_tests.rs`. +//! +//! The 6 MCT (Monte Carlo Test) groups are **not** implemented: their expected output is a +//! `resultsArray` produced by a chained update rule defined in the ACVP AES specification rather +//! than in SP 800-38A, and implementing it from anything else would be guesswork. The test reports +//! how many it skipped so the gap stays visible. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Cbc; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; +use std::collections::BTreeMap; + +#[path = "common/acvp_helpers.rs"] +mod acvp_helpers; +use acvp_helpers::cipher_key; + +const BLOCK_LEN: usize = 16; + +const TEST_DATA_DIR: &str = "crypto/aes_tdes_vectors/AES"; +const REQUEST_FILE: &str = "ACVP-AES-CBC.4014528.req.json"; +const RESPONSE_FILE: &str = "ACVP-AES-CBC.4014528.rsp.json"; + +/// How to walk the blocks of one case. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Grouping { + /// One block per call. Never forms a pair. + Single, + /// Two blocks per call, with a one-block remainder for odd lengths. Uses the pair path. + Pairs, +} + +/// Runs one CBC case in one direction, for a given permutation, under the given grouping. +/// +/// Encryption is driven through `do_encrypt_init_rng` with a `FixedSeedRNG` emitting the vector's +/// IV, and the returned init data is checked against that IV before any ciphertext is compared -- +/// so a change that ignored the RNG could not pass silently. +fn run_case( + key_bytes: &[u8], + iv: [u8; BLOCK_LEN], + input: &[[u8; BLOCK_LEN]], + encrypt: bool, + grouping: Grouping, +) -> Vec<[u8; BLOCK_LEN]> +where + P: ElectronicCodeBook, +{ + let key = cipher_key::(key_bytes); + let mut out: Vec<[u8; BLOCK_LEN]> = Vec::with_capacity(input.len()); + + if encrypt { + let (mut enc, got_iv) = Cbc::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .expect("encrypt init"); + assert_eq!(got_iv, iv, "the pinned RNG should reproduce the vector's IV"); + + match grouping { + Grouping::Single => { + for block in input { + let mut c = *block; + enc.do_encrypt_inplace(&mut c).unwrap(); + out.push(c); + } + } + Grouping::Pairs => { + let (pairs, tail) = input.as_chunks::<2>(); + for pair in pairs { + let mut c = *pair; + enc.do_encrypt_blocks_inplace(&mut c).unwrap(); + out.extend_from_slice(&c); + } + for block in tail { + let mut c = *block; + enc.do_encrypt_inplace(&mut c).unwrap(); + out.push(c); + } + } + } + } else { + let mut dec = + Cbc::::do_decrypt_init(&key, &iv).expect("dec init"); + + match grouping { + Grouping::Single => { + for block in input { + let mut p = *block; + dec.do_decrypt_inplace(&mut p).unwrap(); + out.push(p); + } + } + Grouping::Pairs => { + let (pairs, tail) = input.as_chunks::<2>(); + for pair in pairs { + let mut p = *pair; + dec.do_decrypt_blocks_inplace(&mut p).unwrap(); + out.extend_from_slice(&p); + } + for block in tail { + let mut p = *block; + dec.do_decrypt_inplace(&mut p).unwrap(); + out.push(p); + } + } + } + } + + out +} + +/// Dispatches on key length, which is what selects the AES parameter set. +fn run_case_for_key_len( + key_bytes: &[u8], + iv: [u8; BLOCK_LEN], + input: &[[u8; BLOCK_LEN]], + encrypt: bool, + grouping: Grouping, +) -> Vec<[u8; BLOCK_LEN]> { + match key_bytes.len() { + 16 => run_case::(key_bytes, iv, input, encrypt, grouping), + 24 => run_case::(key_bytes, iv, input, encrypt, grouping), + 32 => run_case::(key_bytes, iv, input, encrypt, grouping), + other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), + } +} + +fn to_blocks(bytes: &[u8]) -> Vec<[u8; BLOCK_LEN]> { + assert_eq!(bytes.len() % BLOCK_LEN, 0, "ACVP CBC payloads are block-aligned"); + bytes.chunks(BLOCK_LEN).map(|c| c.try_into().unwrap()).collect() +} + +#[test] +fn acvp_aes_cbc_known_answer_tests() { + let (Some(req), Some(rsp)) = ( + bc_test_data_json(TEST_DATA_DIR, REQUEST_FILE), + bc_test_data_json(TEST_DATA_DIR, RESPONSE_FILE), + ) else { + return; + }; + + // 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 checked = 0usize; + let mut multi_block = 0usize; + let mut skipped_mct = 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"); + 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}"), + }; + + 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"); + + if test_type == "MCT" { + skipped_mct += 1; + continue; + } + + let answer = answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + if answer.get("resultsArray").is_some() { + skipped_mct += 1; + continue; + } + + let key_bytes = hex_field(test, "key", tc_id); + let iv: [u8; BLOCK_LEN] = + hex_field(test, "iv", tc_id).try_into().expect("a 16-byte IV"); + + // Input comes from the request, expected output from the response. + let (input_field, output_field) = if encrypt { ("pt", "ct") } else { ("ct", "pt") }; + let input = to_blocks(&hex_field(test, input_field, tc_id)); + let expected = to_blocks(&hex_field(answer, output_field, tc_id)); + + assert_eq!(input.len(), expected.len(), "tcId {tc_id}: length mismatch"); + if input.len() > 1 { + multi_block += 1; + } + + for grouping in [Grouping::Single, Grouping::Pairs] { + let got = run_case_for_key_len(&key_bytes, iv, &input, encrypt, grouping); + assert_eq!( + got, + expected, + "tcId {tc_id}: AES-{} CBC {direction}, {} blocks, {grouping:?} grouping", + key_bytes.len() * 8, + input.len() + ); + } + + *per_kind.entry(format!("AES-{} {direction}", key_bytes.len() * 8)).or_default() += 1; + checked += 1; + } + } + + for (kind, n) in &per_kind { + println!("ACVP AES-CBC {kind}: {n} cases"); + } + println!( + "ACVP AES-CBC: {checked} AFT cases checked in two groupings each \ + ({multi_block} of them multi-block); {skipped_mct} MCT cases skipped" + ); + + // Guard against a silently-empty or partial run. + assert!(checked > 2000, "expected the full ACVP AFT set, only checked {checked}"); + assert!(multi_block >= 60, "expected the multi-block cases, found {multi_block}"); + assert_eq!(per_kind.len(), 6, "expected all three key lengths in both directions"); +} diff --git a/crypto/aes/tests/cbc_wycheproof.rs b/crypto/aes/tests/cbc_wycheproof.rs new file mode 100644 index 00000000..1b7d55fd --- /dev/null +++ b/crypto/aes/tests/cbc_wycheproof.rs @@ -0,0 +1,215 @@ +//! Known-answer tests against Project Wycheproof's `testvectors_v1/aes_cbc_pkcs5_test.json`. +//! +//! Requires the Wycheproof repository (https://github.com/C2SP/wycheproof) to be cloned alongside +//! this repository, i.e. at `../wycheproof` 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 vector +//! suites in this crate. +//! +//! # PKCS #5 is PKCS #7 at a 16-byte block +//! +//! The file's "PKCS #5" is the padding of RFC 5652 Sec 6.3 (`k - (lth mod k)` octets of value +//! `k - (lth mod k)`), which the cipher crate provides as [`PKCS7`]; the two names differ only in +//! that PKCS #5 was written for 8-byte blocks. So these vectors drive the padded aliases +//! `AES_CBC_128<_, PKCS7>` and friends, i.e. CBC through the `PaddedBlockCipherEncryptor` / +//! `PaddedBlockCipherDecryptor` adapters, where `cbc_bc-test-data.rs` and `sp800_38a_cbc_tests.rs` +//! drive the unpadded [`Cbc`](bouncycastle_cipher::modes::Cbc) underneath them. +//! +//! # Why this set is worth having alongside the ACVP one +//! +//! Two thirds of the file is `result: "invalid"`: ciphertexts of a message padded with zeros, with +//! `0xff`, with the wrong count, with a count of 0 or above 16, and so on (`BadPadding`, 141 +//! cases), plus an empty ciphertext (`NoPadding`, 3 cases). The ACVP set has no padding at all, +//! so this is the only external check that [`PKCS7::unpad`] rejects every malformed block rather +//! than accepting an alternative padding, and that the adapter refuses a ciphertext too short to +//! carry one. See the file's own `"notes"` object for what each `flags` entry is checking. +//! +//! The IV is supplied through a `FixedSeedRNG`, as in `cbc_bc-test-data.rs`, and the returned IV is +//! asserted to be the vector's. + +use bouncycastle_aes::{AES_CBC_128, AES_CBC_192, AES_CBC_256}; +use bouncycastle_cipher::padding::PKCS7; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{PaddingError, SymmetricCipherError}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; + +const BLOCK_LEN: usize = 16; + +/// 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 vector 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("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 +} + +/// What an invalid case must fail with, from its `flags`. +/// +/// `BadPadding` is a well-formed ciphertext whose final block does not unpad, so the adapter +/// surfaces [`PKCS7::unpad`]'s single undifferentiated [`PaddingError::InvalidPadding`]. +/// `NoPadding` is an empty ciphertext: there is no final block to unpad at all, which the adapter +/// reports as [`SymmetricCipherError::DecryptionFailed`] before any padding is looked at. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Expected { + Valid, + BadPadding, + NoFinalBlock, +} + +/// Runs one case through the padded `AES_CBC_*<_, PKCS7>` pair at one key length. +/// +/// For a valid case, `msg` must encrypt to exactly `expected_ct` under the vector's IV, and +/// `expected_ct` must decrypt back to `msg`. For an invalid case only the decrypt direction is +/// checked -- re-encrypting `msg` with correct padding has no reason to reproduce a deliberately +/// mis-padded `ct` -- and it must fail with the variant the case's flags predict. +fn run_case( + tc_id: u64, + key_bytes: &[u8], + iv: [u8; BLOCK_LEN], + msg: &[u8], + expected_ct: &[u8], + expected: Expected, +) where + E: SymmetricCipherEncryptor, + D: SymmetricCipherDecryptor, +{ + let key = cipher_key::(key_bytes); + + if expected == Expected::Valid { + let mut ct = vec![0u8; E::encrypt_out_len(msg.len())]; + let (got_iv, written) = + E::encrypt_rng_out(&key, &mut FixedSeedRNG::::new(iv), msg, &mut ct) + .unwrap_or_else(|e| panic!("tcId {tc_id}: valid case failed to encrypt: {e:?}")); + assert_eq!(got_iv, iv, "tcId {tc_id}: the seeded RNG must reproduce the vector's IV"); + ct.truncate(written); + assert_eq!(ct, expected_ct, "tcId {tc_id}: ciphertext mismatch"); + } + + let mut plaintext = vec![0u8; D::decrypt_out_len(expected_ct.len())]; + let outcome = D::decrypt_out(&key, &iv, expected_ct, &mut plaintext); + match (expected, outcome) { + (Expected::Valid, Ok(n)) => { + plaintext.truncate(n); + assert_eq!(plaintext, msg, "tcId {tc_id}: decrypted plaintext mismatch"); + } + (Expected::BadPadding, Err(SymmetricCipherError::PaddingError(e))) => { + assert_eq!( + e, + PaddingError::InvalidPadding, + "tcId {tc_id}: bad padding must be refused" + ); + } + (Expected::NoFinalBlock, Err(SymmetricCipherError::DecryptionFailed)) => {} + (expected, outcome) => { + panic!("tcId {tc_id}: expected {expected:?}, got {outcome:?}") + } + } +} + +/// Dispatches on the key length to the matching `AES_CBC_*` alias pair. +fn dispatch( + tc_id: u64, + key_bytes: &[u8], + iv: [u8; BLOCK_LEN], + msg: &[u8], + expected_ct: &[u8], + expected: Expected, +) { + match key_bytes.len() { + 16 => run_case::, AES_CBC_128, 16>( + tc_id, key_bytes, iv, msg, expected_ct, expected, + ), + 24 => run_case::, AES_CBC_192, 24>( + tc_id, key_bytes, iv, msg, expected_ct, expected, + ), + 32 => run_case::, AES_CBC_256, 32>( + tc_id, key_bytes, iv, msg, expected_ct, expected, + ), + other => panic!("tcId {tc_id}: AES keys are 16, 24 or 32 bytes, got {other}"), + } +} + +#[test] +fn wycheproof_aes_cbc_pkcs7_known_answer_tests() { + let Some(doc) = wycheproof_json("aes_cbc_pkcs5_test.json") else { + return; + }; + + assert_eq!( + doc.get("algorithm").and_then(Value::as_str), + Some("AES-CBC-PKCS5"), + "this is the AES-CBC-PKCS5 vector file" + ); + + let groups = doc.get("testGroups").and_then(Value::as_array).expect("testGroups"); + + let mut valid_count = 0usize; + let mut bad_padding_count = 0usize; + let mut no_final_block_count = 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"); + assert_eq!(iv_size_bits as usize, 8 * BLOCK_LEN, "CBC's IV is one block"); + assert_eq!(key_size_bits % 8, 0, "keySize must be a whole number of octets"); + + let tests = group.get("tests").and_then(Value::as_array).expect("tests"); + + for test in tests { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + let key_bytes = hex_field(test, "key", tc_id); + let iv: [u8; BLOCK_LEN] = hex_field(test, "iv", tc_id) + .try_into() + .unwrap_or_else(|_| panic!("tcId {tc_id}: the IV must be one block")); + let msg = hex_field(test, "msg", tc_id); + let ct = hex_field(test, "ct", tc_id); + let result = test.get("result").and_then(Value::as_str).expect("result"); + let flags: Vec<&str> = test + .get("flags") + .and_then(Value::as_array) + .expect("flags") + .iter() + .map(|f| f.as_str().expect("flag")) + .collect(); + + // The flags say *how* an invalid case is invalid, and so which error it must produce. + let expected = match result { + "valid" => Expected::Valid, + "invalid" if flags.contains(&"BadPadding") => Expected::BadPadding, + "invalid" if flags.contains(&"NoPadding") => Expected::NoFinalBlock, + other => panic!("tcId {tc_id}: unexpected result/flags {other} {flags:?}"), + }; + + dispatch(tc_id, &key_bytes, iv, &msg, &ct, expected); + + match expected { + Expected::Valid => valid_count += 1, + Expected::BadPadding => bad_padding_count += 1, + Expected::NoFinalBlock => no_final_block_count += 1, + } + } + } + + println!( + "Wycheproof AES-CBC-PKCS5: {valid_count} valid, {bad_padding_count} bad-padding and \ + {no_final_block_count} empty-ciphertext cases run" + ); + + // Guards against a silently-vacuous run. + assert!(valid_count > 0, "expected valid cases"); + assert!(bad_padding_count > 0, "expected bad-padding cases, which are the point of this set"); + assert!(no_final_block_count > 0, "expected the empty-ciphertext cases"); +} diff --git a/crypto/aes/tests/ccm_bc-test-data.rs b/crypto/aes/tests/ccm_bc-test-data.rs new file mode 100644 index 00000000..5200bf4a --- /dev/null +++ b/crypto/aes/tests/ccm_bc-test-data.rs @@ -0,0 +1,323 @@ +//! 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_out`] / [`Ccm::decrypt_out`], +//! 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::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Ccm; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; +use std::collections::BTreeMap; + +#[path = "common/acvp_helpers.rs"] +mod acvp_helpers; +use acvp_helpers::cipher_key; + +/// Every group in this set has `ivLen: 96`. +const NONCE_LEN: usize = 12; + +const TEST_DATA_DIR: &str = "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"; + +/// 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_out` 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_out( + 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(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_out( + 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(req), Some(rsp)) = ( + bc_test_data_json(TEST_DATA_DIR, REQUEST_FILE), + bc_test_data_json(TEST_DATA_DIR, RESPONSE_FILE), + ) else { + return; + }; + + // 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 = hex_field(test, "key", tc_id); + let nonce_bytes = hex_field(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 = hex_field(test, "aad", tc_id); + + // Input comes from the request, expected output from the response. + let input = hex_field(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 = hex_field(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 = hex_field(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/aes/tests/ccm_wycheproof.rs b/crypto/aes/tests/ccm_wycheproof.rs new file mode 100644 index 00000000..8df8e451 --- /dev/null +++ b/crypto/aes/tests/ccm_wycheproof.rs @@ -0,0 +1,276 @@ +//! Known-answer tests against Project Wycheproof's `testvectors_v1/aes_ccm_test.json`. +//! +//! Requires the Wycheproof repository (https://github.com/C2SP/wycheproof) to be cloned alongside +//! this repository, i.e. at `../wycheproof` 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 vector +//! suites in this crate. +//! +//! # Why this set is worth having alongside the ACVP one +//! +//! `ccm_bc-test-data.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 +//! `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_out`] / [`Ccm::decrypt_detached_out`], not +//! the inline pair `ccm_bc-test-data.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 not supported 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::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Ccm; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; + +/// 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_out`]), and `expected_ct`/`expected_tag` must decrypt back to `msg` +/// ([`Ccm::decrypt_detached_out`]). 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_out( + &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_out( + &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(doc) = wycheproof_json("aes_ccm_test.json") else { return }; + + 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 unsupported_groups = 0usize; + let mut unsupported_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 unsupported 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)) + { + unsupported_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 = hex_field(test, "key", tc_id); + let nonce_bytes = hex_field(test, "iv", tc_id); + let aad = hex_field(test, "aad", tc_id); + let msg = hex_field(test, "msg", tc_id); + let ct = hex_field(test, "ct", tc_id); + let tag = hex_field(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 { + unsupported_cases += 1; + } + } + } + + println!( + "Wycheproof AES-CCM: {run} cases run ({valid_count} valid, {invalid_count} invalid), \ + {unsupported_cases} cases in {unsupported_groups} groups not supported \ + (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!( + unsupported_groups > 0, + "expected most of this adversarial set to be outside A.1's sets" + ); +} diff --git a/crypto/aes/tests/cfb8_bc-test-data.rs b/crypto/aes/tests/cfb8_bc-test-data.rs new file mode 100644 index 00000000..a037138e --- /dev/null +++ b/crypto/aes/tests/cfb8_bc-test-data.rs @@ -0,0 +1,238 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-CFB8` 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 ML-KEM, ML-DSA, `aes` and AES-CBC suites -- +//! `cargo test` must stay green for someone who has only cloned this repository. +//! +//! This is the CFB8 counterpart to `cfb_bc-test-data.rs` (AES-CFB128), `cbc_bc-test-data.rs` +//! (AES-CBC) and `ecb_bc-test-data.rs` (AES-ECB, the raw permutation). `ACVP-AES-CFB1` is +//! the one remaining segment size, which this crate does not implement, and is not read. +//! +//! # Joining the request and response files +//! +//! As with CBC, the response file carries **only the answer** (`ct` for an encrypt group, `pt` for a +//! decrypt group) against a `tcId`. The key, IV and input live in the request file, and the group +//! metadata that says which direction a case is -- `direction` and `keyLen` -- lives only there too. +//! So both files are read and joined on `tcId`. +//! +//! # Coverage +//! +//! 2138 AFT (Algorithm Functional Test) cases across all three key lengths and both directions. +//! Most are a single byte -- CFB8's segment -- and 60 carry 16 to 160 bytes, which are the ones +//! that reach the batch paths. Every case is run **four times**: as one call over the whole +//! payload, byte by byte, in 8-byte calls, and in 3-byte calls that never line up with the +//! 8-byte batch. Between them those put the multi-byte cases through +//! [`ElectronicCodeBook::encrypt_4blocks`] and [`ElectronicCodeBook::encrypt_2blocks`] -- the +//! *forward* function, even on the decrypt side -- and through the single-byte path, with the +//! shift register carried across calls at every alignment. So all of that is exercised against real +//! vectors and not only against the toys in `cfb8_tests.rs`. +//! +//! The 6 MCT (Monte Carlo Test) groups are **not** implemented: their expected output is a +//! `resultsArray` produced by a chained update rule defined in the ACVP AES specification rather +//! than in SP 800-38A, and implementing it from anything else would be guesswork. The test reports +//! how many it skipped so the gap stays visible. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Cfb8; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::traits::{ + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; +use std::collections::BTreeMap; + +#[path = "common/acvp_helpers.rs"] +mod acvp_helpers; +use acvp_helpers::cipher_key; + +const BLOCK_LEN: usize = 16; + +const TEST_DATA_DIR: &str = "crypto/aes_tdes_vectors/AES"; +const REQUEST_FILE: &str = "ACVP-AES-CFB8.4014529.req.json"; +const RESPONSE_FILE: &str = "ACVP-AES-CFB8.4014529.rsp.json"; + +/// How to walk the bytes of one case. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Grouping { + /// The whole payload in one call: fours, then pairs, then the remaining bytes singly. + Whole, + /// One byte per call. Never batches. + Bytes, + /// Four bytes per call: every call is exactly one `encrypt_4blocks` batch. + Fours, + /// Three bytes per call, so no call lines up with the 8-byte batch and the shift register has + /// to carry across calls at every alignment. + Threes, +} + +impl Grouping { + fn chunk_len(self, payload_len: usize) -> usize { + match self { + Grouping::Whole => payload_len.max(1), + Grouping::Bytes => 1, + Grouping::Fours => 4, + Grouping::Threes => 3, + } + } +} + +/// Runs one CFB8 case in one direction, for a given permutation, under the given grouping. +/// +/// Encryption is driven through `do_encrypt_init_rng` with a `FixedSeedRNG` emitting the vector's +/// IV, and the returned init data is checked against that IV before any ciphertext is compared -- +/// so a change that ignored the RNG could not pass silently. +fn run_case( + key_bytes: &[u8], + iv: [u8; BLOCK_LEN], + input: &[u8], + encrypt: bool, + grouping: Grouping, +) -> Vec +where + P: ElectronicCodeBook, +{ + let key = cipher_key::(key_bytes); + let mut data = input.to_vec(); + let chunk = grouping.chunk_len(data.len()); + + if encrypt { + let (mut enc, got_iv) = Cfb8::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .expect("encrypt init"); + assert_eq!(got_iv, iv, "the pinned RNG should reproduce the vector's IV"); + for piece in data.chunks_mut(chunk) { + enc.do_encrypt_inplace(piece).unwrap(); + } + } else { + let mut dec = Cfb8::::do_decrypt_init(&key, &iv) + .expect("dec init"); + for piece in data.chunks_mut(chunk) { + dec.do_decrypt_inplace(piece).unwrap(); + } + } + + data +} + +/// Dispatches on key length, which is what selects the AES parameter set. +fn run_case_for_key_len( + key_bytes: &[u8], + iv: [u8; BLOCK_LEN], + input: &[u8], + encrypt: bool, + grouping: Grouping, +) -> Vec { + match key_bytes.len() { + 16 => run_case::(key_bytes, iv, input, encrypt, grouping), + 24 => run_case::(key_bytes, iv, input, encrypt, grouping), + 32 => run_case::(key_bytes, iv, input, encrypt, grouping), + other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), + } +} + +#[test] +fn acvp_aes_cfb8_known_answer_tests() { + let (Some(req), Some(rsp)) = ( + bc_test_data_json(TEST_DATA_DIR, REQUEST_FILE), + bc_test_data_json(TEST_DATA_DIR, RESPONSE_FILE), + ) else { + return; + }; + + // 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 checked = 0usize; + let mut multi_block = 0usize; + let mut skipped_mct = 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"); + 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}"), + }; + + 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"); + + if test_type == "MCT" { + skipped_mct += 1; + continue; + } + + let answer = answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + if answer.get("resultsArray").is_some() { + skipped_mct += 1; + continue; + } + + let key_bytes = hex_field(test, "key", tc_id); + let iv: [u8; BLOCK_LEN] = + hex_field(test, "iv", tc_id).try_into().expect("a 16-byte IV"); + + // Input comes from the request, expected output from the response. + let (input_field, output_field) = if encrypt { ("pt", "ct") } else { ("ct", "pt") }; + let input = hex_field(test, input_field, tc_id); + let expected = hex_field(answer, output_field, tc_id); + + assert_eq!(input.len(), expected.len(), "tcId {tc_id}: length mismatch"); + if input.len() > 1 { + multi_block += 1; + } + + for grouping in [Grouping::Whole, Grouping::Bytes, Grouping::Fours, Grouping::Threes] { + let got = run_case_for_key_len(&key_bytes, iv, &input, encrypt, grouping); + assert_eq!( + got, + expected, + "tcId {tc_id}: AES-{} CFB8 {direction}, {} bytes, {grouping:?} grouping", + key_bytes.len() * 8, + input.len() + ); + } + + *per_kind.entry(format!("AES-{} {direction}", key_bytes.len() * 8)).or_default() += 1; + checked += 1; + } + } + + for (kind, n) in &per_kind { + println!("ACVP AES-CFB8 {kind}: {n} cases"); + } + println!( + "ACVP AES-CFB8: {checked} AFT cases checked in four groupings each \ + ({multi_block} of them multi-byte); {skipped_mct} MCT cases skipped" + ); + + // Guard against a silently-empty or partial run. + assert!(checked > 2000, "expected the full ACVP AFT set, only checked {checked}"); + assert!(multi_block >= 50, "expected the multi-byte cases, found {multi_block}"); + assert_eq!(per_kind.len(), 6, "expected all three key lengths in both directions"); +} diff --git a/crypto/aes/tests/cfb_bc-test-data.rs b/crypto/aes/tests/cfb_bc-test-data.rs new file mode 100644 index 00000000..e4ae6f69 --- /dev/null +++ b/crypto/aes/tests/cfb_bc-test-data.rs @@ -0,0 +1,248 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-CFB128` 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 ML-KEM, ML-DSA, `aes` and AES-CBC suites -- +//! `cargo test` must stay green for someone who has only cloned this repository. +//! +//! This is the CFB128 counterpart to `cbc_bc-test-data.rs` (AES-CBC) and to +//! `ecb_bc-test-data.rs` (AES-ECB, the raw permutation). The `CFB128` file is +//! the one that matches [`Cfb`]; `ACVP-AES-CFB8` matches `Cfb8` and is read by +//! `cfb8_bc-test-data.rs`. `ACVP-AES-CFB1` is the one segment size this crate does not implement, +//! and is deliberately not read. +//! +//! # Joining the request and response files +//! +//! As with CBC, the response file carries **only the answer** (`ct` for an encrypt group, `pt` for a +//! decrypt group) against a `tcId`. The key, IV and input live in the request file, and the group +//! metadata that says which direction a case is -- `direction` and `keyLen` -- lives only there too. +//! So both files are read and joined on `tcId`. +//! +//! # Coverage +//! +//! 2138 AFT (Algorithm Functional Test) cases across all three key lengths and both directions, +//! including 54 whose payload spans 2 to 10 blocks. Every case is run **four times**: block by +//! block, in pairs with a one-block remainder for odd lengths, as one call over the whole payload, +//! and in 5-byte calls that never line up with a block. The second and third passes are what put +//! the multi-block cases through the pair and four-block paths -- which for CFB are +//! [`ElectronicCodeBook::encrypt_2blocks`] and [`ElectronicCodeBook::encrypt_4blocks`], the +//! *forward* function, even on the decrypt side -- and the fourth is what puts them through the +//! byte path with segments left open between calls. So all of that is exercised against real +//! vectors and not only against the toys in `cfb_tests.rs`. Every ACVP CFB128 payload is a whole +//! number of blocks, so the short final segment is not covered here (it is not covered by any +//! official vector); `cfb_tests.rs` pins it against the raw permutation. +//! +//! The 6 MCT (Monte Carlo Test) groups are **not** implemented: their expected output is a +//! `resultsArray` produced by a chained update rule defined in the ACVP AES specification rather +//! than in SP 800-38A, and implementing it from anything else would be guesswork. The test reports +//! how many it skipped so the gap stays visible. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Cfb; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::traits::{ + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; +use std::collections::BTreeMap; + +#[path = "common/acvp_helpers.rs"] +mod acvp_helpers; +use acvp_helpers::cipher_key; + +const BLOCK_LEN: usize = 16; + +const TEST_DATA_DIR: &str = "crypto/aes_tdes_vectors/AES"; +const REQUEST_FILE: &str = "ACVP-AES-CFB128.4014530.req.json"; +const RESPONSE_FILE: &str = "ACVP-AES-CFB128.4014530.rsp.json"; + +/// How to walk the bytes of one case. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Grouping { + /// One block per call. Never forms a pair. + Single, + /// Two blocks per call, with a one-block remainder for odd lengths. Uses the pair path. + Pairs, + /// The whole payload in one call: fours, then pairs, then the remaining block. The cases + /// of four or more blocks are the ones that reach `encrypt_4blocks`. + Whole, + /// Five bytes per call, so every call but the first starts mid-segment and none is a whole + /// block: the byte path, with the unused keystream carried between calls. + Bytes, +} + +impl Grouping { + fn chunk_len(self, payload_len: usize) -> usize { + match self { + Grouping::Single => BLOCK_LEN, + Grouping::Pairs => 2 * BLOCK_LEN, + Grouping::Whole => payload_len.max(1), + Grouping::Bytes => 5, + } + } +} + +/// Runs one CFB128 case in one direction, for a given permutation, under the given grouping. +/// +/// Encryption is driven through `do_encrypt_init_rng` with a `FixedSeedRNG` emitting the vector's +/// IV, and the returned init data is checked against that IV before any ciphertext is compared -- +/// so a change that ignored the RNG could not pass silently. +fn run_case( + key_bytes: &[u8], + iv: [u8; BLOCK_LEN], + input: &[u8], + encrypt: bool, + grouping: Grouping, +) -> Vec +where + P: ElectronicCodeBook, +{ + let key = cipher_key::(key_bytes); + let mut data = input.to_vec(); + let chunk = grouping.chunk_len(data.len()); + + if encrypt { + let (mut enc, got_iv) = Cfb::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .expect("encrypt init"); + assert_eq!(got_iv, iv, "the pinned RNG should reproduce the vector's IV"); + for piece in data.chunks_mut(chunk) { + enc.do_encrypt_inplace(piece).unwrap(); + } + } else { + let mut dec = + Cfb::::do_decrypt_init(&key, &iv).expect("dec init"); + for piece in data.chunks_mut(chunk) { + dec.do_decrypt_inplace(piece).unwrap(); + } + } + + data +} + +/// Dispatches on key length, which is what selects the AES parameter set. +fn run_case_for_key_len( + key_bytes: &[u8], + iv: [u8; BLOCK_LEN], + input: &[u8], + encrypt: bool, + grouping: Grouping, +) -> Vec { + match key_bytes.len() { + 16 => run_case::(key_bytes, iv, input, encrypt, grouping), + 24 => run_case::(key_bytes, iv, input, encrypt, grouping), + 32 => run_case::(key_bytes, iv, input, encrypt, grouping), + other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), + } +} + +#[test] +fn acvp_aes_cfb128_known_answer_tests() { + let (Some(req), Some(rsp)) = ( + bc_test_data_json(TEST_DATA_DIR, REQUEST_FILE), + bc_test_data_json(TEST_DATA_DIR, RESPONSE_FILE), + ) else { + return; + }; + + // 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 checked = 0usize; + let mut multi_block = 0usize; + let mut skipped_mct = 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"); + 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}"), + }; + + 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"); + + if test_type == "MCT" { + skipped_mct += 1; + continue; + } + + let answer = answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + if answer.get("resultsArray").is_some() { + skipped_mct += 1; + continue; + } + + let key_bytes = hex_field(test, "key", tc_id); + let iv: [u8; BLOCK_LEN] = + hex_field(test, "iv", tc_id).try_into().expect("a 16-byte IV"); + + // Input comes from the request, expected output from the response. + let (input_field, output_field) = if encrypt { ("pt", "ct") } else { ("ct", "pt") }; + let input = hex_field(test, input_field, tc_id); + let expected = hex_field(answer, output_field, tc_id); + + assert_eq!(input.len(), expected.len(), "tcId {tc_id}: length mismatch"); + assert_eq!( + input.len() % BLOCK_LEN, + 0, + "tcId {tc_id}: ACVP CFB128 payloads are block-aligned" + ); + if input.len() > BLOCK_LEN { + multi_block += 1; + } + + for grouping in [Grouping::Single, Grouping::Pairs, Grouping::Whole, Grouping::Bytes] { + let got = run_case_for_key_len(&key_bytes, iv, &input, encrypt, grouping); + assert_eq!( + got, + expected, + "tcId {tc_id}: AES-{} CFB128 {direction}, {} blocks, {grouping:?} grouping", + key_bytes.len() * 8, + input.len() / BLOCK_LEN + ); + } + + *per_kind.entry(format!("AES-{} {direction}", key_bytes.len() * 8)).or_default() += 1; + checked += 1; + } + } + + for (kind, n) in &per_kind { + println!("ACVP AES-CFB128 {kind}: {n} cases"); + } + println!( + "ACVP AES-CFB128: {checked} AFT cases checked in four groupings each \ + ({multi_block} of them multi-block); {skipped_mct} MCT cases skipped" + ); + + // Guard against a silently-empty or partial run. + assert!(checked > 2000, "expected the full ACVP AFT set, only checked {checked}"); + assert!(multi_block >= 50, "expected the multi-block cases, found {multi_block}"); + assert_eq!(per_kind.len(), 6, "expected all three key lengths in both directions"); +} diff --git a/crypto/aes/tests/common/acvp_gcm_helpers.rs b/crypto/aes/tests/common/acvp_gcm_helpers.rs new file mode 100644 index 00000000..9941e34c --- /dev/null +++ b/crypto/aes/tests/common/acvp_gcm_helpers.rs @@ -0,0 +1,186 @@ +//! Shared plumbing for the ACVP AES-GCM and AES-GMAC known-answer test files +//! (`gcm_bc-test-data.rs`, `gmac_bc-test-data.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::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Gcm; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; + +/// The nonce length these vectors use; every group in the ACVP AES-GCM/GMAC sets has `ivLen = 96`. +#[path = "acvp_helpers.rs"] +mod acvp_helpers; +pub use acvp_helpers::cipher_key; + +pub const GCM_NONCE_LEN: usize = 12; + +/// 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::hazmat::ElectronicCodeBook, +{ + let mut ct = vec![0u8; data.len()]; + let (got_iv, written, tag) = Gcm::::encrypt_detached_rng_out( + 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::hazmat::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_detached_out( + key, &iv, aad, ct, &tag_arr, &mut data, + ); + + // The inline `SymmetricCipherDecryptor` streaming view, `ciphertext || tag` through + // `do_update_out`/`do_decrypt_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, so it + // cannot run the cases that carry AAD, and most do): 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.do_decrypt_out_len(inline_ct.len()); + let mut inline_pt = vec![0u8; expect_written]; + let written = dec + .do_decrypt_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_decrypt_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_decrypt_final, got {inline_result:?}" + ); + } + } +} diff --git a/crypto/aes/tests/common/acvp_helpers.rs b/crypto/aes/tests/common/acvp_helpers.rs new file mode 100644 index 00000000..8abeb19d --- /dev/null +++ b/crypto/aes/tests/common/acvp_helpers.rs @@ -0,0 +1,28 @@ +//! Shared plumbing for every known-answer suite in this crate that reads `bc-test-data`'s ACVP +//! JSON: building a `KeyMaterial` from the raw key bytes. The per-mode suites differ only in how +//! they run a case, so that part stays with each of them. + +#![allow(dead_code)] + +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; + +/// Builds a `KeyMaterial` from raw ACVP key bytes, including the all-zero keys. +/// +/// The ACVP sets deliberately include an all-zero key. `KeyMaterial` tags an all-zero buffer as +/// `KeyType::Zeroized` and will not promote it outside a `do_hazardous_operations` closure, which +/// is the right default -- so this opts in explicitly rather than the engine weakening its guard. +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 +} diff --git a/crypto/aes/tests/ctr_bc-test-data.rs b/crypto/aes/tests/ctr_bc-test-data.rs new file mode 100644 index 00000000..e8732c4c --- /dev/null +++ b/crypto/aes/tests/ctr_bc-test-data.rs @@ -0,0 +1,258 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-CTR` 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. +//! +//! # Only the zero-counter cases apply, and that is most of them +//! +//! ACVP gives each case a full 16-byte `iv`, which for CTR is the **initial counter block**. +//! [`Ctr`] takes a *nonce* and starts its counter at zero, so a case is expressible through this +//! API exactly when its initial counter block ends in `CTR_LEN` zero bytes: then the nonce is the +//! leading bytes and the counter is already where this mode starts. +//! +//! With the 12-byte nonce used here (a 4-byte counter), **1853 of the 2138** functional cases +//! qualify. The other 285 begin at a non-zero counter and are skipped with the count reported, so +//! the gap stays visible; they test the cipher and the XOR, both of which the qualifying cases +//! already cover, and not the counter construction, which `ctr_tests.rs` pins against the spec. +//! +//! # Joining the request and response files +//! +//! As with the other AES sets, the response file carries **only the answer** (`ct` for an encrypt +//! group, `pt` for a decrypt group) against a `tcId`. The key, IV and input live in the request +//! file, and the group metadata that says which direction a case is lives only there too. So both +//! files are read and joined on `tcId`. +//! +//! # Coverage +//! +//! Every qualifying case is run in four groupings -- the whole payload in one call, block by block, +//! in 8-byte calls and in 3-byte calls -- so the batch paths and the byte path are both exercised +//! against real vectors. The payloads are a single block each, so counter *increment* is not +//! covered here; `ctr_tests.rs` covers it against the raw permutation across a 255-to-256 carry, +//! and the OpenSSL cross-check in `cli/tests/aes_ctr_cli_tests.rs` covers it end to end. +//! +//! The 6 MCT (Monte Carlo Test) groups are **not** implemented: their expected output is a +//! `resultsArray` produced by a chained update rule defined in the ACVP AES specification rather +//! than in SP 800-38A, and implementing it from anything else would be guesswork. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Ctr; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::traits::{ + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; +use std::collections::BTreeMap; + +#[path = "common/acvp_helpers.rs"] +mod acvp_helpers; +use acvp_helpers::cipher_key; + +const BLOCK_LEN: usize = 16; +/// The nonce length under test; the remaining 4 bytes of the block are the counter. +const NONCE_LEN: usize = 12; + +const TEST_DATA_DIR: &str = "crypto/aes_tdes_vectors/AES"; +const REQUEST_FILE: &str = "ACVP-AES-CTR.4014537.req.json"; +const RESPONSE_FILE: &str = "ACVP-AES-CTR.4014537.rsp.json"; + +/// How to walk the bytes of one case. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Grouping { + /// The whole payload in one call: fours, then pairs, then the remaining bytes singly. + Whole, + /// One whole block per call. + Blocks, + /// Eight bytes per call, so no call is a whole block and the keystream carries across calls. + Eights, + /// Three bytes per call, a size that lines up with neither the block nor the batch. + Threes, +} + +impl Grouping { + fn chunk_len(self, payload_len: usize) -> usize { + match self { + Grouping::Whole => payload_len.max(1), + Grouping::Blocks => BLOCK_LEN, + Grouping::Eights => 8, + Grouping::Threes => 3, + } + } +} + +/// Runs one CTR case in one direction, for a given permutation, under the given grouping. +/// +/// Encryption is driven through `do_encrypt_init_rng` with a `FixedSeedRNG` emitting the vector's +/// IV, and the returned init data is checked against that IV before any ciphertext is compared -- +/// so a change that ignored the RNG could not pass silently. +fn run_case( + key_bytes: &[u8], + nonce: [u8; NONCE_LEN], + input: &[u8], + encrypt: bool, + grouping: Grouping, +) -> Vec +where + P: ElectronicCodeBook, +{ + let key = cipher_key::(key_bytes); + let mut data = input.to_vec(); + let chunk = grouping.chunk_len(data.len()); + + if encrypt { + let (mut enc, got_iv) = + Ctr::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(nonce), + ) + .expect("encrypt init"); + assert_eq!(got_iv, nonce, "the pinned RNG should reproduce the vector's nonce"); + for piece in data.chunks_mut(chunk) { + enc.do_encrypt_inplace(piece).unwrap(); + } + } else { + let mut dec = + Ctr::::do_decrypt_init(&key, &nonce) + .expect("dec init"); + for piece in data.chunks_mut(chunk) { + dec.do_decrypt_inplace(piece).unwrap(); + } + } + + data +} + +/// Dispatches on key length, which is what selects the AES parameter set. +fn run_case_for_key_len( + key_bytes: &[u8], + nonce: [u8; NONCE_LEN], + input: &[u8], + encrypt: bool, + grouping: Grouping, +) -> Vec { + match key_bytes.len() { + 16 => run_case::(key_bytes, nonce, input, encrypt, grouping), + 24 => run_case::(key_bytes, nonce, input, encrypt, grouping), + 32 => run_case::(key_bytes, nonce, input, encrypt, grouping), + other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), + } +} + +#[test] +fn acvp_aes_ctr_known_answer_tests() { + let (Some(req), Some(rsp)) = ( + bc_test_data_json(TEST_DATA_DIR, REQUEST_FILE), + bc_test_data_json(TEST_DATA_DIR, RESPONSE_FILE), + ) else { + return; + }; + + // 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 checked = 0usize; + let mut skipped_mct = 0usize; + let mut skipped_nonzero_counter = 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"); + 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}"), + }; + + 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"); + + // Anything that is not a functional test is a Monte Carlo group. This file labels + // those "CTR" rather than "MCT", unlike the CBC and CFB sets, so the test is written + // against what an AFT case *is* rather than against one spelling of what it is not. + if test_type != "AFT" { + skipped_mct += 1; + continue; + } + + let answer = answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + if answer.get("resultsArray").is_some() { + skipped_mct += 1; + continue; + } + + let key_bytes = hex_field(test, "key", tc_id); + let iv: [u8; BLOCK_LEN] = + hex_field(test, "iv", tc_id).try_into().expect("a 16-byte IV"); + + // Only an initial counter block whose counter is already zero is expressible through + // this API; see the module docs. + if iv[NONCE_LEN..] != [0u8; BLOCK_LEN - NONCE_LEN] { + skipped_nonzero_counter += 1; + continue; + } + let nonce: [u8; NONCE_LEN] = iv[..NONCE_LEN].try_into().expect("the nonce"); + + // Input comes from the request, expected output from the response. + let (input_field, output_field) = if encrypt { ("pt", "ct") } else { ("ct", "pt") }; + let input = hex_field(test, input_field, tc_id); + let expected = hex_field(answer, output_field, tc_id); + + assert_eq!(input.len(), expected.len(), "tcId {tc_id}: length mismatch"); + for grouping in [Grouping::Whole, Grouping::Blocks, Grouping::Eights, Grouping::Threes] + { + let got = run_case_for_key_len(&key_bytes, nonce, &input, encrypt, grouping); + assert_eq!( + got, + expected, + "tcId {tc_id}: AES-{} CTR {direction}, {} bytes, {grouping:?} grouping", + key_bytes.len() * 8, + input.len() + ); + } + + *per_kind.entry(format!("AES-{} {direction}", key_bytes.len() * 8)).or_default() += 1; + checked += 1; + } + } + + for (kind, n) in &per_kind { + println!("ACVP AES-CTR {kind}: {n} cases"); + } + println!( + "ACVP AES-CTR: {checked} AFT cases checked in four groupings each; \ + {skipped_nonzero_counter} skipped for a non-zero initial counter, \ + {skipped_mct} MCT cases skipped" + ); + + // Guard against a silently-empty or partial run. + assert!(checked > 1800, "expected the zero-counter ACVP AFT cases, only checked {checked}"); + assert_eq!( + checked + skipped_nonzero_counter, + 2138, + "every AFT case should be either checked or explicitly skipped for its counter" + ); + assert_eq!(skipped_mct, 6, "the six Monte Carlo groups should be skipped, and only those"); + assert_eq!(per_kind.len(), 6, "expected all three key lengths in both directions"); +} diff --git a/crypto/aes/tests/ctr_bc_java_tests.rs b/crypto/aes/tests/ctr_bc_java_tests.rs new file mode 100644 index 00000000..2f680da8 --- /dev/null +++ b/crypto/aes/tests/ctr_bc_java_tests.rs @@ -0,0 +1,171 @@ +//! Cross-implementation tests for CTR against **BC Java's `SICBlockCipher`**. +//! +//! # Why this is the closest comparison available +//! +//! `ctr_vector_tests.rs` checks against OpenSSL, but OpenSSL's `-aes-*-ctr` takes the whole 16-byte +//! initial counter block as its IV: it has no notion of a nonce, and its counter is always the full +//! block. It can therefore only ever agree with this type at the one width where the two coincide, +//! and it cannot exercise a **narrow** counter at all. +//! +//! BC Java's `SICBlockCipher` (Segmented Integer Counter, its name for CTR) is built the same way +//! this type is. Given an IV shorter than the block it +//! +//! * copies the IV into the leading bytes and **zero-fills the rest**, so the counter starts at 0 +//! (`reset()`); +//! * increments the trailing bytes big-endian with carry (`incrementCounter()`); +//! * and **throws** `IllegalStateException("Counter in CTR/SIC mode out of range.")` once the carry +//! would reach the IV, which `checkCounter()` detects by comparing the leading bytes back against +//! the IV. +//! +//! That is the same construction, the same starting value and the same overflow rule, so it can +//! check the counter widths OpenSSL cannot reach. The one difference is the cap: BC Java allows a +//! counter up to `min(8, blockSize / 2)` bytes, which is 8 for AES, where this type stops at 4. Ours +//! is a subset, and on the overlap (nonce 12 to 15 bytes) the two agree exactly. +//! +//! # Provenance +//! +//! The blocks below are the **keystream**, i.e. `Oj = CIPH_K(N | j)`, obtained by encrypting zeros +//! with `SICBlockCipher.newInstance(AESEngine.newInstance())` under AES-128 key +//! `2b7e151628aed2a6abf7158809cf4f3c`, from the working tree of `bc-java` at +//! `core/src/main/java/org/bouncycastle/crypto/modes/SICBlockCipher.java`. Encrypting zeros is used +//! so the values are the keystream itself rather than a keystream XORed with something, which makes +//! a mismatch point straight at the counter block that produced it. +//! +//! Whole-message agreement with BC Java was also checked while these were generated -- the 69-byte +//! vectors of `ctr_vector_tests.rs` and a 5000-byte message across the 255-to-256 carry, at all +//! three key lengths -- and it is exact. Those cases are covered there and by the ACVP suite, so +//! what is pinned here is specifically the part neither of them reaches: the narrow counters. + +use bouncycastle_aes::hazmat::AES128Internal; +use bouncycastle_cipher::Encrypting; +use bouncycastle_cipher::modes::Ctr; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{StreamCipherEncryptor, SymmetricCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; + +/// The AES-128 key used for every vector in this file: SP 800-38A Appendix F's first key. +const KEY: &str = "2b7e151628aed2a6abf7158809cf4f3c"; + +fn key() -> KeyMaterial<16> { + let raw = hex::decode(KEY).expect("valid hex"); + KeyMaterial::<16>::from_bytes_as_type(&raw, KeyType::SymmetricCipherKey).expect("a valid key") +} + +/// Produces `blocks` blocks of keystream by encrypting zeros under the given nonce. +fn keystream(nonce_hex: &str, blocks: usize) -> Vec { + let nonce: [u8; NONCE_LEN] = + hex::decode(nonce_hex).expect("valid hex").try_into().expect("nonce length"); + let (mut enc, got) = Ctr::::do_encrypt_init_rng( + &key(), + &mut FixedSeedRNG::::new(nonce), + ) + .expect("encrypt init"); + assert_eq!(got, nonce, "the pinned RNG should reproduce the nonce"); + + let mut data = vec![0u8; blocks * 16]; + enc.do_encrypt_inplace(&mut data).expect("encryption"); + data +} + +/// Checks the numbered keystream blocks against BC Java's. +fn check(name: &str, keystream: &[u8], expected: &[(usize, &str)]) { + for (j, want) in expected { + let got = &keystream[j * 16..(j + 1) * 16]; + let got_hex: String = got.iter().map(|b| format!("{b:02x}")).collect(); + assert_eq!( + &got_hex, want, + "{name}: keystream block {j} must match BC Java's SICBlockCipher" + ); + } +} + +/// A **1-byte** counter (15-byte nonce): the narrowest this type allows, and a width OpenSSL cannot +/// express at all. Blocks 254 and 255 are the last two the counter can produce, so this pins the top +/// of the range as well as the bottom. +#[test] +fn one_byte_counter_matches_bc_java() { + const NONCE: &str = "5a5b5c5d5e5f606162636465666768"; + let ks = keystream::<15>(NONCE, 256); + check( + "1-byte counter", + &ks, + &[ + (0, "419c915d236c793736311df5d96395aa"), + (1, "23af650ed9d051ac2d5ed6365ff36b1e"), + (2, "1e1723bab8f7a67f152ae5bf5e0a6156"), + (254, "da78aa259930654dec5fd7b1bd194ee9"), + (255, "3e0caa53956c10ee5c3959d588b79cf3"), + ], + ); +} + +/// A **2-byte** counter (14-byte nonce), spanning the 255-to-256 boundary. +/// +/// That boundary is the carry from one counter byte into the next, and it is the case a per-byte +/// increment that forgot to carry, or one that wrote the counter little-endian, would get wrong. +/// BC Java carries the same way, so agreement across blocks 255 and 256 pins it. +#[test] +fn two_byte_counter_matches_bc_java_across_the_carry() { + const NONCE: &str = "3c3d3e3f40414243444546474849"; + let ks = keystream::<14>(NONCE, 260); + check( + "2-byte counter", + &ks, + &[ + (0, "2f79f802e5baf1eea03e079c55fa43ff"), + (254, "7ef19c2ab2e750a19741a653edabd4e2"), + (255, "a30a0d0c6c2c58bb04befb8aa32675ee"), + (256, "580080107847864b8589e21a9fb3cdff"), + (257, "fd85537add6a73476e13928f49eba5ee"), + ], + ); +} + +/// A **3-byte** counter (13-byte nonce), the remaining width between the two above and the 4-byte +/// counter the ACVP and OpenSSL suites cover. +#[test] +fn three_byte_counter_matches_bc_java() { + const NONCE: &str = "0102030405060708090a0b0c0d"; + let ks = keystream::<13>(NONCE, 3); + check( + "3-byte counter", + &ks, + &[ + (0, "e24be69cfe7c13dd7a94807fb91f95a7"), + (1, "234790f73eb542c18dbfc2a6f7a06795"), + (2, "95e5a3963bcdf6183357da61878861bc"), + ], + ); +} + +/// The counter limit falls in the same place as BC Java's. +/// +/// BC Java throws `IllegalStateException("Counter in CTR/SIC mode out of range.")` on the byte after +/// the counter's last value; this type returns `SymmetricCipherError::DataLimitExceeded` on the same +/// byte. +/// Checked here at the same 15-byte nonce as above, where the boundary is 256 blocks -- 4096 bytes +/// exactly -- and confirmed against BC Java at the 14-byte nonce too, where it is 1 MiB. +#[test] +fn the_counter_limit_falls_where_bc_java_throws() { + let nonce: [u8; 15] = + hex::decode("5a5b5c5d5e5f606162636465666768").unwrap().try_into().unwrap(); + let (mut enc, _) = Ctr::::do_encrypt_init_rng( + &key(), + &mut FixedSeedRNG::<15>::new(nonce), + ) + .unwrap(); + + // BC Java encrypts 4096 bytes under this IV without complaint. + let mut data = vec![0u8; 4096]; + enc.do_encrypt_inplace(&mut data) + .expect("4096 bytes must be accepted, as BC Java accepts them"); + + // ...and throws on the next byte. + let mut one = [0u8; 1]; + assert!( + matches!(enc.do_encrypt_inplace(&mut one), Err(SymmetricCipherError::DataLimitExceeded)), + "byte 4097 must be refused, where BC Java throws IllegalStateException" + ); +} diff --git a/crypto/aes/tests/ctr_vector_tests.rs b/crypto/aes/tests/ctr_vector_tests.rs new file mode 100644 index 00000000..1a895429 --- /dev/null +++ b/crypto/aes/tests/ctr_vector_tests.rs @@ -0,0 +1,186 @@ +//! Multi-block known-answer tests for CTR, generated with OpenSSL. +//! +//! # Why these exist alongside the ACVP suite +//! +//! `ctr_bc-test-data.rs` runs 1853 official NIST vectors, but **every one of them is a single +//! block**, so all of them use counter 0 and none exercises the increment. A counter that never +//! advanced -- or advanced the wrong way, or wrote its bytes little-endian -- would pass the entire +//! ACVP set. (That is not hypothetical: a deliberately little-endian counter was checked against +//! the ACVP suite while these tests were written, and it passed.) +//! +//! `ctr_tests.rs` covers the increment against the raw permutation, which is sound because that +//! permutation is itself ACVP-validated, but it is our own code on both sides of the comparison. +//! These vectors close that gap with an **independent implementation**: the ciphertexts below were +//! produced by OpenSSL 3.0.13, following the same convention the SM3 and HMAC suites use for +//! openssl-sourced values. They span five counter blocks, so they pin the increment end to end, +//! and their last block is partial, so they also pin Sec 6.5's `MSB_u(On)` handling. +//! +//! # How they were generated +//! +//! ```text +//! openssl enc -aes-128-ctr -K -iv 000102030405060708090a0b00000000 -in plaintext.bin +//! ``` +//! +//! OpenSSL takes the whole 16-byte initial counter block as its `-iv`. Ours is a 12-byte nonce with +//! the counter starting at zero, so the two line up exactly when the IV's low four bytes are zero, +//! which is why the IV above ends in `00000000`. See the [`Ctr`] module docs. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Ctr; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; + +const BLOCK_LEN: usize = 16; +const NONCE_LEN: usize = 12; + +/// The nonce: the leading 12 bytes of the OpenSSL IV `000102030405060708090a0b00000000`. +const NONCE: &str = "000102030405060708090a0b"; + +/// The four SP 800-38A Appendix F plaintext blocks followed by five more bytes, so the message is +/// 69 bytes: five counter blocks, the last of them partial. +const PLAINTEXT: &str = concat!( + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", + "0011223344", +); + +/// The three keys used throughout SP 800-38A Appendix F. +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + +/// `openssl enc -aes-128-ctr`, OpenSSL 3.0.13. +const CT_128: &str = concat!( + "ffd8816338abebca17491bc67fe6751c", + "093833c279e946d49804c6b03df09f9d", + "6b0727101b346a530523d59fb883e678", + "fda525b39296cfc5a821d4dcda5a6227", + "06efd63405", +); +/// `openssl enc -aes-192-ctr`, OpenSSL 3.0.13. +const CT_192: &str = concat!( + "c85f24d60a6fd4593209730ecd1ed507", + "deae5f770708a1e162d04d42fe3dd6e6", + "acf360f5c5f25e53a09396547d8b7f9b", + "9d12dc684df141cd0b5462450a8d1900", + "4a271f6e8e", +); +/// `openssl enc -aes-256-ctr`, OpenSSL 3.0.13. +const CT_256: &str = concat!( + "b66c7ac8885c5ff473855203b36048ff", + "5e7e0746b6e3ad4c2b84aaf440b1b987", + "38a9ad1527187f6f435b83b09734cb04", + "b3e3a2a77d2a02c4759cbd9b8fc822b3", + "1223c7e590", +); + +fn unhex(s: &str) -> Vec { + hex::decode(s).expect("valid hex") +} + +fn key_material(hex_str: &str) -> KeyMaterial { + let raw = unhex(hex_str); + assert_eq!(raw.len(), N, "key length"); + KeyMaterial::::from_bytes_as_type(&raw, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +/// Chunk sizes that cut across the block and the four-block batch, so the vectors are reproduced +/// through every path rather than only the batched one. +const CHUNKINGS: [usize; 6] = [1, 5, 16, 17, 33, 69]; + +fn check(name: &str, key_hex: &str, expected_hex: &str) +where + P: ElectronicCodeBook, +{ + let key = key_material::(key_hex); + let nonce: [u8; NONCE_LEN] = unhex(NONCE).try_into().expect("a 12-byte nonce"); + let plaintext = unhex(PLAINTEXT); + let expected = unhex(expected_hex); + assert_eq!(plaintext.len(), 69, "the message should be five counter blocks, the last partial"); + assert_eq!(expected.len(), plaintext.len(), "CTR does not change the length"); + + // Encryption, in one call and in every chunking. + for chunk in [plaintext.len()].into_iter().chain(CHUNKINGS) { + let (mut enc, got) = + Ctr::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(nonce), + ) + .expect("encrypt init"); + assert_eq!(got, nonce, "{name}: the pinned RNG should reproduce the nonce"); + + let mut data = plaintext.clone(); + for piece in data.chunks_mut(chunk) { + enc.do_encrypt_inplace(piece).expect("encryption"); + } + assert_eq!(data, expected, "{name}: encrypting in {chunk}-byte calls"); + } + + // Decryption, likewise. + for chunk in [expected.len()].into_iter().chain(CHUNKINGS) { + let mut dec = + Ctr::::do_decrypt_init(&key, &nonce) + .expect("decrypt init"); + let mut data = expected.clone(); + for piece in data.chunks_mut(chunk) { + dec.do_decrypt_inplace(piece).expect("decryption"); + } + assert_eq!(data, plaintext, "{name}: decrypting in {chunk}-byte calls"); + } + + // ...and the one-shot. + let mut data = expected.clone(); + Ctr::::decrypt_inplace(&key, &nonce, &mut data) + .expect("one-shot decryption"); + assert_eq!(data, plaintext, "{name}: one-shot"); +} + +#[test] +fn aes128_ctr_matches_openssl() { + check::("AES-128", KEY_128, CT_128); +} + +#[test] +fn aes192_ctr_matches_openssl() { + check::("AES-192", KEY_192, CT_192); +} + +#[test] +fn aes256_ctr_matches_openssl() { + check::("AES-256", KEY_256, CT_256); +} + +/// The vectors must actually depend on the counter advancing: the second block of ciphertext must +/// differ from what a mode that reused counter 0 would produce. +/// +/// Without this, a vector could in principle be satisfied by a stuck counter if the plaintext +/// happened to cooperate. Here the first two plaintext blocks differ, so `C1 XOR C2` would equal +/// `P1 XOR P2` if the keystream were the same for both -- and it must not. +#[test] +fn the_vectors_depend_on_the_counter_advancing() { + let plaintext = unhex(PLAINTEXT); + let ciphertext = unhex(CT_128); + + let ks_xor: Vec = ciphertext[..BLOCK_LEN] + .iter() + .zip(ciphertext[BLOCK_LEN..2 * BLOCK_LEN].iter()) + .zip(plaintext[..BLOCK_LEN].iter().zip(plaintext[BLOCK_LEN..2 * BLOCK_LEN].iter())) + .map(|((c1, c2), (p1, p2))| c1 ^ c2 ^ p1 ^ p2) + .collect(); + + assert_ne!( + ks_xor, + vec![0u8; BLOCK_LEN], + "O1 and O2 must differ, i.e. the counter must have advanced between them" + ); +} diff --git a/crypto/aes/tests/ecb_alias_tests.rs b/crypto/aes/tests/ecb_alias_tests.rs new file mode 100644 index 00000000..b0dc247e --- /dev/null +++ b/crypto/aes/tests/ecb_alias_tests.rs @@ -0,0 +1,118 @@ +//! Tests for the padded AES-ECB aliases. +//! +//! As with the CBC aliases, these are only type aliases, so what is worth testing is that both +//! parameters select: the direction picks the encryptor or the decryptor, and the padding scheme +//! reaches the behaviour. ECB's own properties are tested in `bouncycastle_cipher::modes`; what is specific +//! here is that its `INIT_DATA_LEN` is 0, so the projection must carry a different value than CBC's +//! and the aliases must still resolve correctly. + +use bouncycastle_aes::hazmat::AES128Internal; +use bouncycastle_aes::hazmat::{AES_ECB_128, AES_ECB_192, AES_ECB_256}; +use bouncycastle_cipher::modes::hazmat::Ecb; +use bouncycastle_cipher::padding::{ + NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, +}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; + +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 aliases must resolve to exactly the adapters they claim to, with `INIT_DATA_LEN = 0`. +#[test] +fn the_aliases_name_the_expected_types() { + use core::mem::size_of; + + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherEncryptor, PKCS7, 16, 0, 16>, + >() + ); + assert_eq!( + size_of::>(), + size_of::< + PaddedBlockCipherDecryptor, PKCS7, 16, 0, 16>, + >() + ); +} + +/// ECB has no IV, so the init data is an empty array and the ciphertext is exactly the padded +/// plaintext with nothing prepended. That is the difference from the CBC aliases, and it comes from +/// the `INIT_DATA_LEN = 0` the projection is given. +#[test] +fn there_is_no_iv() { + let (no_iv, ciphertext) = + AES_ECB_128::::encrypt(&key::<16>(), b"hello").expect("encryption"); + assert_eq!(no_iv, [0u8; 0], "ECB has no IV, so the init data is empty"); + assert_eq!(ciphertext.len(), 16, "five bytes padded to one block, nothing prepended"); + + let recovered = + AES_ECB_128::::decrypt(&key::<16>(), &no_iv, &ciphertext).unwrap(); + assert_eq!(recovered, b"hello"); +} + +/// Every key length round-trips through its alias, at lengths that need padding and lengths that do +/// not. +#[test] +fn every_key_length_round_trips() { + fn check(name: &str) + where + Enc: SymmetricCipherEncryptor, + Dec: SymmetricCipherDecryptor, + { + for len in [0usize, 1, 15, 16, 17, 64] { + let plaintext: Vec = (0..len).map(|i| (i * 11 + 3) as u8).collect(); + let (no_iv, ciphertext) = Enc::encrypt(&key::(), &plaintext).expect("encryption"); + assert_eq!(no_iv, [0u8; 0], "{name}: no IV"); + assert_eq!( + ciphertext.len(), + (len / 16 + 1) * 16, + "{name}, len {len}: PKCS7 pads up to the next whole block" + ); + + let recovered = Dec::decrypt(&key::(), &no_iv, &ciphertext).expect("decryption"); + assert_eq!(recovered, plaintext, "{name}, len {len}: round trip"); + } + } + + check::<16, AES_ECB_128, AES_ECB_128>("AES-128"); + check::<24, AES_ECB_192, AES_ECB_192>("AES-192"); + check::<32, AES_ECB_256, AES_ECB_256>("AES-256"); +} + +/// The padding parameter must select the scheme here too. +#[test] +fn the_padding_parameter_selects_the_scheme() { + let aligned = [0x5Au8; 16]; + let (_, pkcs7) = + AES_ECB_128::::encrypt(&key::<16>(), &aligned).expect("PKCS7"); + let (_, nopad) = + AES_ECB_128::::encrypt(&key::<16>(), &aligned).expect("NoPadding"); + assert_eq!(pkcs7.len(), 32, "PKCS7 adds a whole block to aligned data"); + assert_eq!(nopad.len(), 16, "NoPadding adds nothing"); + + assert!( + AES_ECB_128::::encrypt(&key::<16>(), b"hello").is_err(), + "NoPadding must refuse a partial block" + ); +} + +/// Padding does not fix ECB: identical plaintext blocks still give identical ciphertext blocks, and +/// the same message under the same key always gives the same ciphertext. The aliases carry the +/// warning; this is the test that it is warranted. +#[test] +fn padding_does_not_hide_the_codebook_property() { + // Two identical blocks give two identical ciphertext blocks. + let (_, ciphertext) = + AES_ECB_128::::encrypt(&key::<16>(), &[0x5Au8; 32]).unwrap(); + assert_eq!(ciphertext[..16], ciphertext[16..], "ECB is a codebook, padded or not"); + + // ...and encryption is deterministic, there being no IV to vary. + let (_, a) = AES_ECB_128::::encrypt(&key::<16>(), b"hello").unwrap(); + let (_, b) = AES_ECB_128::::encrypt(&key::<16>(), b"hello").unwrap(); + assert_eq!(a, b, "the same message encrypts the same way every time"); +} diff --git a/crypto/aes/tests/ecb_bc-test-data.rs b/crypto/aes/tests/ecb_bc-test-data.rs new file mode 100644 index 00000000..e1fae0de --- /dev/null +++ b/crypto/aes/tests/ecb_bc-test-data.rs @@ -0,0 +1,263 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-ECB` vectors from the `bc-test-data` repo. +//! +//! Requires `bc-test-data` to be cloned alongside this repository, i.e. at `../bc-test-data` +//! relative to the root of this git project. If it is absent the tests print a warning and pass, +//! matching the convention used by the ML-KEM and ML-DSA test suites -- `cargo test` must stay +//! green for someone who has only cloned this repository. +//! +//! # Why ECB, and where the other ACVP AES files are used +//! +//! ECB applies the raw permutation to each block independently, so an ECB test vector *is* a +//! 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 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: +//! +//! | Vector set | Consumed by | +//! |---|---| +//! | `ACVP-AES-ECB` | this file (the permutation; the `Ecb` mode's own tests are toy-driven, in `crypto/cipher/tests/modes/ecb_tests.rs`) | +//! | `ACVP-AES-CBC` | `cbc_bc-test-data.rs` | +//! | `ACVP-AES-CBC-CS1` / `-CS2` / `-CS3` | nothing yet (ciphertext stealing is unimplemented) | +//! | `ACVP-AES-CCM` | `ccm_bc-test-data.rs` | +//! | `ACVP-AES-CFB128` | `cfb_bc-test-data.rs` | +//! | `ACVP-AES-CFB8` | `cfb8_bc-test-data.rs` | +//! | `ACVP-AES-OFB` | nothing yet (OFB is unimplemented) | +//! | `ACVP-AES-CTR` | `ctr_bc-test-data.rs` | +//! | `ACVP-AES-GCM` / `-GMAC` | `gcm_bc-test-data.rs` / `gmac_bc-test-data.rs` | +//! | `ACVP-AES-KW` / `-KWP` | nothing yet (key wrap is unimplemented) | +//! | `ACVP-AES-FF1` / `-FF3-1` | nothing yet (format-preserving encryption is unimplemented) | +//! +//! So an unused vector set here means an unimplemented mode, not an untested one. Adding a mode +//! should include wiring up its file. +//! +//! The response file records `key`, `pt` and `ct` for every test case regardless of the group's +//! declared direction, so each case is checked in **both** directions: encrypting `pt` must give +//! `ct` and decrypting `ct` must give `pt`. That is strictly stronger than honouring the declared +//! direction, and it means the group metadata in the request file is not needed. +//! +//! # Coverage and one gap +//! +//! The AFT (Algorithm Functional Test) groups cover all three key lengths in both directions, +//! including cases whose plaintext spans several blocks. The six MCT (Monte Carlo Test) groups +//! are **not** implemented: their expected output is a `resultsArray` produced by a chained +//! key/plaintext update rule defined in the ACVP AES specification rather than in FIPS 197, and +//! implementing it from anything other than that specification would be guesswork. The test +//! reports how many it skipped so the gap is visible rather than silent. + +use bouncycastle_aes::AES_BLOCK_LEN; +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json}; +use bouncycastle_hex as hex; + +const TEST_DATA_DIR: &str = "crypto/aes_tdes_vectors/AES"; +const RESPONSE_FILE: &str = "ACVP-AES-ECB.4014527.rsp.json"; + +/// Builds a `KeyMaterial` from raw ACVP key bytes, including the all-zero keys. +/// +/// The ACVP set deliberately includes an all-zero key (the GFSbox-style groups vary only the +/// plaintext under a zero key). `KeyMaterial` tags an all-zero buffer as [`KeyType::Zeroized`] +/// and will not promote it outside a [`do_hazardous_operations`] closure, which is the right +/// default -- an all-zero key normally means a broken RNG, and `AESInternal128::new` rejecting it is +/// tested in `fips197_tests.rs`. Here the zero key is deliberate and comes from NIST, so this +/// opts in explicitly rather than the library weakening its guard. +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 +} + +/// A single-block transformation, resolved once per test case rather than per block. +type BlockTransform = Box; + +/// Encrypts or decrypts `data` block by block, i.e. ECB, dispatching on the key length. +fn ecb(key: &[u8], data: &[u8], encrypt: bool) -> Vec { + assert_eq!(data.len() % AES_BLOCK_LEN, 0, "ACVP ECB data must be block-aligned"); + + let transform: BlockTransform = match key.len() { + 16 => { + let km = cipher_key::<16>(key); + let aes = AES128Internal::new(&km).expect("valid AES-128 key"); + if encrypt { + Box::new(move |b| aes.encrypt_block(b)) + } else { + Box::new(move |b| aes.decrypt_block(b)) + } + } + 24 => { + let km = cipher_key::<24>(key); + let aes = AES192Internal::new(&km).expect("valid AES-192 key"); + if encrypt { + Box::new(move |b| aes.encrypt_block(b)) + } else { + Box::new(move |b| aes.decrypt_block(b)) + } + } + 32 => { + let km = cipher_key::<32>(key); + let aes = AES256Internal::new(&km).expect("valid AES-256 key"); + if encrypt { + Box::new(move |b| aes.encrypt_block(b)) + } else { + Box::new(move |b| aes.decrypt_block(b)) + } + } + other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), + }; + + let mut out = Vec::with_capacity(data.len()); + for chunk in data.chunks(AES_BLOCK_LEN) { + // Cannot fail: the length is asserted block-aligned above. + let mut block: [u8; AES_BLOCK_LEN] = chunk.try_into().unwrap(); + transform(&mut block); + out.extend_from_slice(&block); + } + out +} + +/// The same, using the two-block entry points where a pair is available. +fn ecb_pairwise(key: &[u8], data: &[u8], encrypt: bool) -> Vec { + assert_eq!(data.len() % AES_BLOCK_LEN, 0, "ACVP ECB data must be block-aligned"); + let mut blocks: Vec<[u8; AES_BLOCK_LEN]> = + data.chunks(AES_BLOCK_LEN).map(|c| c.try_into().unwrap()).collect(); + + match key.len() { + 16 => { + let km = cipher_key::<16>(key); + let aes = AES128Internal::new(&km).unwrap(); + run_pairwise(&mut blocks, encrypt, |p, e| { + if e { aes.encrypt_2blocks(p) } else { aes.decrypt_2blocks(p) } + }); + } + 24 => { + let km = cipher_key::<24>(key); + let aes = AES192Internal::new(&km).unwrap(); + run_pairwise(&mut blocks, encrypt, |p, e| { + if e { aes.encrypt_2blocks(p) } else { aes.decrypt_2blocks(p) } + }); + } + 32 => { + let km = cipher_key::<32>(key); + let aes = AES256Internal::new(&km).unwrap(); + run_pairwise(&mut blocks, encrypt, |p, e| { + if e { aes.encrypt_2blocks(p) } else { aes.decrypt_2blocks(p) } + }); + } + other => panic!("ACVP AES vectors should only use 16, 24 or 32 byte keys, got {other}"), + } + + blocks.concat() +} + +/// Walks `blocks` two at a time, leaving a trailing odd block to a duplicated pair. +fn run_pairwise( + blocks: &mut [[u8; AES_BLOCK_LEN]], + encrypt: bool, + transform: impl Fn(&mut [[u8; AES_BLOCK_LEN]; 2], bool), +) { + let mut chunks = blocks.chunks_exact_mut(2); + for pair in &mut chunks { + // Cannot fail: `chunks_exact_mut(2)` yields slices of length 2. + let pair: &mut [[u8; AES_BLOCK_LEN]; 2] = pair.try_into().unwrap(); + transform(pair, encrypt); + } + // An odd trailing block still has to go through the two-block path. + if let [last] = chunks.into_remainder() { + let mut pair = [*last, *last]; + transform(&mut pair, encrypt); + *last = pair[0]; + } +} + +#[test] +fn acvp_aes_ecb_known_answer_tests() { + let Some(parsed) = bc_test_data_json(TEST_DATA_DIR, RESPONSE_FILE) else { return }; + + // The ACVP file is an array: element 0 is the version header, element 1 the vector set. + let groups = parsed + .get(1) + .and_then(|set| set.get("testGroups")) + .and_then(Value::as_array) + .expect("testGroups array"); + + let mut checked = 0usize; + let mut skipped_mct = 0usize; + let mut by_key_len = [0usize; 3]; // 128, 192, 256 + + for group in groups { + let tests = group.get("tests").and_then(Value::as_array).expect("tests array"); + for test in tests { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + + // Monte Carlo groups carry a chained resultsArray instead of a single pt/ct pair. + if test.get("resultsArray").is_some() { + skipped_mct += 1; + continue; + } + + let get = |name: &str| -> Vec { + let s = test + .get(name) + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("tcId {tc_id}: missing field {name}")); + hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc_id}: bad hex in {name}")) + }; + + let key = get("key"); + let pt = get("pt"); + let ct = get("ct"); + + assert_eq!(pt.len(), ct.len(), "tcId {tc_id}: pt and ct differ in length"); + + assert_eq!(ecb(&key, &pt, true), ct, "tcId {tc_id}: AES-{} encrypt", key.len() * 8); + assert_eq!(ecb(&key, &ct, false), pt, "tcId {tc_id}: AES-{} decrypt", key.len() * 8); + + // The two-block path must agree with the single-block path on real vectors too. + assert_eq!( + ecb_pairwise(&key, &pt, true), + ct, + "tcId {tc_id}: AES-{} encrypt via encrypt_2blocks", + key.len() * 8 + ); + assert_eq!( + ecb_pairwise(&key, &ct, false), + pt, + "tcId {tc_id}: AES-{} decrypt via decrypt_2blocks", + key.len() * 8 + ); + + by_key_len[match key.len() { + 16 => 0, + 24 => 1, + _ => 2, + }] += 1; + checked += 1; + } + } + + println!( + "ACVP AES-ECB: {checked} test cases checked in both directions \ + (AES-128: {}, AES-192: {}, AES-256: {}); {skipped_mct} MCT cases skipped", + by_key_len[0], by_key_len[1], by_key_len[2] + ); + + // Guard against a silently-empty run: the published vector set has thousands of AFT cases + // across all three key lengths. + assert!(checked > 1000, "expected the full ACVP AFT set, only checked {checked}"); + assert!(by_key_len.iter().all(|&n| n > 0), "every key length should be covered"); +} diff --git a/crypto/aes/tests/electronic_code_book_tests.rs b/crypto/aes/tests/electronic_code_book_tests.rs new file mode 100644 index 00000000..b4f03bf4 --- /dev/null +++ b/crypto/aes/tests/electronic_code_book_tests.rs @@ -0,0 +1,27 @@ +//! `ElectronicCodeBook` trait conformance, via the shared test framework. +//! +//! The framework checks the properties every implementor must have -- both directions are +//! inverses, the permutation is injective, the two- and four-block methods are indistinguishable +//! from two or four single-block calls *including their order*, and the key checks behave. That +//! batching property matters here specifically: this crate overrides `encrypt_2blocks`, +//! `decrypt_2blocks`, `encrypt_4blocks` and `decrypt_4blocks` with its `u32` and `u64` plane +//! paths, so the default implementations are not what runs. + +use bouncycastle_aes::AES_BLOCK_LEN; +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; + +#[test] +fn aes128_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<16, AES_BLOCK_LEN, AES128Internal>(); +} + +#[test] +fn aes192_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<24, AES_BLOCK_LEN, AES192Internal>(); +} + +#[test] +fn aes256_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<32, AES_BLOCK_LEN, AES256Internal>(); +} diff --git a/crypto/aes/tests/fips197_tests.rs b/crypto/aes/tests/fips197_tests.rs new file mode 100644 index 00000000..df62bc68 --- /dev/null +++ b/crypto/aes/tests/fips197_tests.rs @@ -0,0 +1,244 @@ +//! Known-answer tests from NIST FIPS 197 itself. +//! +//! Appendix B -- the worked single-block AES-128 encryption -- plus its inverse, the two- and +//! four-block paths, and key-handling behaviour. +//! +//! The Appendix A key expansions are **not** tested here. The key schedule is deliberately not +//! public API (it is a `Secret` field), and a round-trip through the cipher cannot check it: a +//! wrong `w[i]` is used by encryption and decryption alike, so the round trip still succeeds. +//! Every word of all three expansions is instead checked against Appendix A inside +//! `src/schedule.rs`, where the stored schedule can be unpacked and compared directly. +//! +//! Known-answer coverage for AES-192 and AES-256, which Appendix B does not reach, is in +//! `sp800_38a_ecb_tests.rs` and `ecb_bc-test-data.rs`. +//! +//! All values here are transcribed from the published FIPS 197 (Update 1) PDF. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; + +/// Appendix A.1 / Appendix B key: `2b7e151628aed2a6abf7158809cf4f3c`. +const KEY_128: [u8; 16] = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, +]; + +/// Appendix A.2 key: `8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b`. +const KEY_192: [u8; 24] = [ + 0x8e, 0x73, 0xb0, 0xf7, 0xda, 0x0e, 0x64, 0x52, 0xc8, 0x10, 0xf3, 0x2b, 0x80, 0x90, 0x79, 0xe5, + 0x62, 0xf8, 0xea, 0xd2, 0x52, 0x2c, 0x6b, 0x7b, +]; + +/// Appendix A.3 key: +/// `603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4`. +const KEY_256: [u8; 32] = [ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, 0x77, 0x81, + 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4, +]; + +fn key_material(bytes: &[u8; N]) -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +#[test] +fn appendix_b_encrypts_the_documented_block() { + // Appendix B: Input = 32 43 f6 a8 88 5a 30 8d 31 31 98 a2 e0 37 07 34 + // Key = 2b 7e 15 16 28 ae d2 a6 ab f7 15 88 09 cf 4f 3c + // The final state printed as "output" reads, column by column (Eq 3.7): + // 39 25 84 1d 02 dc 09 fb dc 11 85 97 19 6a 0b 32 + let aes = AES128Internal::new(&key_material(&KEY_128)).unwrap(); + + let mut block = [ + 0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, + 0x34, + ]; + aes.encrypt_block(&mut block); + assert_eq!( + block, + [ + 0x39, 0x25, 0x84, 0x1d, 0x02, 0xdc, 0x09, 0xfb, 0xdc, 0x11, 0x85, 0x97, 0x19, 0x6a, + 0x0b, 0x32 + ] + ); +} + +#[test] +fn appendix_b_decrypts_back_to_the_documented_input() { + let aes = AES128Internal::new(&key_material(&KEY_128)).unwrap(); + + let mut block = [ + 0x39, 0x25, 0x84, 0x1d, 0x02, 0xdc, 0x09, 0xfb, 0xdc, 0x11, 0x85, 0x97, 0x19, 0x6a, 0x0b, + 0x32, + ]; + aes.decrypt_block(&mut block); + assert_eq!( + block, + [ + 0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, + 0x07, 0x34 + ] + ); +} + +#[test] +fn appendix_b_two_block_path_agrees_with_the_single_block_path() { + let aes = AES128Internal::new(&key_material(&KEY_128)).unwrap(); + let input = [ + 0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, + 0x34, + ]; + let expected = [ + 0x39, 0x25, 0x84, 0x1d, 0x02, 0xdc, 0x09, 0xfb, 0xdc, 0x11, 0x85, 0x97, 0x19, 0x6a, 0x0b, + 0x32, + ]; + + // Batching the Appendix B block with unrelated ones must not disturb any of them. + let other = [0xAAu8; 16]; + let mut other_alone = other; + aes.encrypt_block(&mut other_alone); + + let mut pair = [input, other]; + aes.encrypt_2blocks(&mut pair); + assert_eq!(pair[0], expected); + assert_eq!(pair[1], other_alone); + + // ...and in the other slot, which is a different lane of the bit-planes. + let mut pair = [other, input]; + aes.encrypt_2blocks(&mut pair); + assert_eq!(pair[0], other_alone); + assert_eq!(pair[1], expected); + + // ...and in each of the four lanes of the four-block path. + for slot in 0..4 { + let mut four = [other; 4]; + four[slot] = input; + aes.encrypt_4blocks(&mut four); + for (i, block) in four.iter().enumerate() { + let want = if i == slot { expected } else { other_alone }; + assert_eq!(*block, want, "slot {slot}, block {i}"); + } + } +} + +/// Encryption and decryption are inverses, under each Appendix A key. +/// +/// This checks `decrypt_block` really inverts `encrypt_block` from the same stored schedule, +/// which is the load-bearing claim of following FIPS 197 Algorithm 3 rather than Sec 5.3.5. It +/// deliberately makes no claim about the schedule being *correct* -- see the module docs. +#[test] +fn encryption_and_decryption_are_inverses_for_all_three_key_lengths() { + let aes128 = AES128Internal::new(&key_material(&KEY_128)).unwrap(); + let aes192 = AES192Internal::new(&key_material(&KEY_192)).unwrap(); + let aes256 = AES256Internal::new(&key_material(&KEY_256)).unwrap(); + + for block in [[0u8; 16], [0xFFu8; 16], core::array::from_fn(|i| i as u8)] { + let mut b = block; + aes128.encrypt_block(&mut b); + assert_ne!(b, block, "AES-128 must actually transform the block"); + aes128.decrypt_block(&mut b); + assert_eq!(b, block, "AES-128 round trip with the Appendix A.1 key"); + + let mut b = block; + aes192.encrypt_block(&mut b); + assert_ne!(b, block, "AES-192 must actually transform the block"); + aes192.decrypt_block(&mut b); + assert_eq!(b, block, "AES-192 round trip with the Appendix A.2 key"); + + let mut b = block; + aes256.encrypt_block(&mut b); + assert_ne!(b, block, "AES-256 must actually transform the block"); + aes256.decrypt_block(&mut b); + assert_eq!(b, block, "AES-256 round trip with the Appendix A.3 key"); + } +} + +/// The three key lengths must give different results for the same input. +/// +/// Guards against a parameter set silently using another set's `Nr` or `Nk`. +#[test] +fn the_three_key_lengths_are_distinct_permutations() { + // A key whose first 16 bytes are shared, so only Nk/Nr and the extra key bytes differ. + let shared = [0x11u8; 32]; + let aes128 = + AES128Internal::new(&key_material::<16>(&shared[..16].try_into().unwrap())).unwrap(); + let aes192 = + AES192Internal::new(&key_material::<24>(&shared[..24].try_into().unwrap())).unwrap(); + let aes256 = AES256Internal::new(&key_material(&shared)).unwrap(); + + let block = [0x42u8; 16]; + let mut b128 = block; + let mut b192 = block; + let mut b256 = block; + aes128.encrypt_block(&mut b128); + aes192.encrypt_block(&mut b192); + aes256.encrypt_block(&mut b256); + + assert_ne!(b128, b192); + assert_ne!(b192, b256); + assert_ne!(b128, b256); +} + +// ---- key handling ----------------------------------------------------------------------- + +#[test] +fn a_key_of_the_wrong_type_is_rejected() { + // KeyType::Seed is not a cipher key: a seed reused directly as an AES key is a real mistake + // and the type system tracks enough to catch it. + let key = KeyMaterial::<16>::from_bytes_as_type(&[0x01; 16], KeyType::Seed).unwrap(); + assert!(AES128Internal::new(&key).is_err()); + + let key = KeyMaterial::<16>::from_bytes_as_type(&[0x01; 16], KeyType::MACKey).unwrap(); + assert!(AES128Internal::new(&key).is_err()); +} + +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + // The capacity is right but only part of it is populated, so `key_len()` disagrees with the + // parameter set. This is the one length error the const generic cannot catch by itself. + let key = + KeyMaterial::<32>::from_bytes_as_type(&[0x01; 16], KeyType::SymmetricCipherKey).unwrap(); + assert!(AES256Internal::new(&key).is_err()); +} + +#[test] +fn a_key_carrying_too_low_a_security_strength_is_rejected() { + // A full-length key whose material was only ever derived at a lower security strength must + // not be usable at the strength its length implies. `from_bytes_as_type` tags a 32-byte key + // as 256-bit, so lower it deliberately -- lowering does not need a hazardous closure, only + // raising does. + let mut key = + KeyMaterial::<32>::from_bytes_as_type(&[0x01; 32], KeyType::SymmetricCipherKey).unwrap(); + assert_eq!(key.security_strength(), SecurityStrength::_256bit); + + key.set_security_strength(SecurityStrength::_128bit).unwrap(); + assert!( + AES256Internal::new(&key).is_err(), + "AES-256 must reject a 32-byte key only derived at the 128-bit strength" + ); + + // The same key at its full strength is fine, so the rejection is about the strength tag and + // not about anything else having gone wrong with the key. + let good = + KeyMaterial::<32>::from_bytes_as_type(&[0x01; 32], KeyType::SymmetricCipherKey).unwrap(); + assert!(AES256Internal::new(&good).is_ok()); +} + +#[test] +fn a_correctly_typed_key_of_each_length_is_accepted() { + assert!(AES128Internal::new(&key_material(&KEY_128)).is_ok()); + assert!(AES192Internal::new(&key_material(&KEY_192)).is_ok()); + assert!(AES256Internal::new(&key_material(&KEY_256)).is_ok()); +} + +#[test] +fn debug_does_not_print_the_key_schedule() { + // The schedule is secret; `Debug` must not be a way to leak it. + let aes = AES128Internal::new(&key_material(&KEY_128)).unwrap(); + let rendered = format!("{aes:?}"); + assert_eq!(rendered, "AES-128"); + // No byte of the key should appear as hex in the output. + assert!(!rendered.contains("2b")); + assert!(!rendered.contains("7e")); +} diff --git a/crypto/aes/tests/gcm_bc-test-data.rs b/crypto/aes/tests/gcm_bc-test-data.rs new file mode 100644 index 00000000..13c132a5 --- /dev/null +++ b/crypto/aes/tests/gcm_bc-test-data.rs @@ -0,0 +1,270 @@ +//! Known-answer tests against the NIST ACVP and CAVP 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. +//! +//! # ACVP +//! +//! 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_helpers::run_decrypt_case`). +//! +//! # CAVP +//! +//! The six `GCM/cavp/` files (`gcmEncryptExtIV{128,192,256}.rsp`, `gcmDecrypt{128,192,256}.rsp`) +//! have 7875 cases each: IVs of 8, 96 and 1024 bits, tags of 32 to 128 bits, and payload and AAD +//! lengths that include empty inputs and partial blocks. `Gcm` fixes the nonce at 96 bits and +//! accepts tags of 96 to 128 bits, so 1875 cases per file can run; the rest are counted as not +//! supported and both counts asserted, so a change in the files' shape is visible. In the decrypt +//! files a record with `FAIL` in place of `PT` is a forgery, which must be rejected. +//! +//! The Wycheproof `aes_gcm_test.json` set, which `bc-test-data` also carries, is run by +//! `gcm_wycheproof.rs`. + +#[path = "common/acvp_gcm_helpers.rs"] +mod acvp_gcm_helpers; + +use acvp_gcm_helpers::{GCM_NONCE_LEN, run_decrypt_case, run_encrypt_case}; +use bouncycastle_core_test_framework::test_data_loaders::{ + Value, bc_test_data, bc_test_data_json, hex_field, +}; +use bouncycastle_hex as hex; +use std::collections::BTreeMap; + +const TEST_DATA_DIR: &str = "crypto/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(req), Some(rsp)) = ( + bc_test_data_json(TEST_DATA_DIR, REQUEST_FILE), + bc_test_data_json(TEST_DATA_DIR, RESPONSE_FILE), + ) else { + return; + }; + + // 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 = hex_field(test, "key", tc_id); + let aad = hex_field(test, "aad", tc_id); + let iv_bytes = hex_field(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 = hex_field(test, "pt", tc_id); + let answer = + answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + let ct = hex_field(answer, "ct", tc_id); + let tag = hex_field(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, adding the + // encrypt groups' inputs to the valid-decrypt coverage that the non-forged + // decrypt cases (below) give. + run_decrypt_case(&key_bytes, iv, &aad, &ct, &tag, Some(&pt)); + encrypt_checked += 1; + } + "decrypt" => { + let ct = hex_field(test, "ct", tc_id); + let tag = hex_field(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 = hex_field(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"); +} + +// --------------------------------------------------------------------------------------------- +// NIST CAVP (`GCM/cavp/`) +// --------------------------------------------------------------------------------------------- + +const CAVP_DIR: &str = "crypto/aes_tdes_vectors/GCM/cavp"; +const CAVP_CASES_PER_FILE: usize = 7875; +/// The 96-bit-IV sections with a tag of 96 bits or more: 5 tag lengths x 5 payload lengths x 5 AAD +/// lengths x 75 cases. +const CAVP_RUNNABLE_PER_FILE: usize = 1875; + +/// One `Count` record of a CAVP GCM `.rsp` file, with its section's `[IVlen]` and `[Taglen]`. +struct CavpCase { + iv_bits: usize, + tag_bits: usize, + key: Vec, + iv: Vec, + aad: Vec, + ct: Vec, + tag: Vec, + /// `None` for a decrypt record marked `FAIL`. + pt: Option>, +} + +/// Parses a CAVP GCM `.rsp` file: blank-line-separated blocks of `[Name = bits]` section headers +/// and `Count = ` records of `Key`/`IV`/`PT`/`AAD`/`CT`/`Tag`, where a decrypt record has `FAIL` +/// in place of `PT`. +fn parse_cavp_file(content: &str) -> Vec { + let content = content.replace('\r', ""); + let (mut iv_bits, mut tag_bits) = (None, None); + let mut cases = Vec::new(); + for block in content.split("\n\n") { + let mut fields: BTreeMap<&str, &str> = BTreeMap::new(); + let mut fail = false; + for line in block.lines().map(str::trim) { + if line.starts_with('#') { + continue; + } else if let Some(header) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) { + let (k, v) = header.split_once(" = ").expect("a [Name = value] header"); + let v: usize = v.parse().expect("a header value in bits"); + match k { + "IVlen" => iv_bits = Some(v), + "Taglen" => tag_bits = Some(v), + _ => {} + } + } else if line == "FAIL" { + fail = true; + } else if let Some((k, v)) = line.split_once(" =") { + fields.insert(k, v.trim()); + } + } + let Some(count) = fields.get("Count") else { continue }; + let field = |k: &str| { + let v = fields.get(k).unwrap_or_else(|| panic!("Count = {count}: no {k}")); + hex::decode(v).unwrap_or_else(|e| panic!("Count = {count}: {k} is not hex: {e:?}")) + }; + assert!(!(fail && fields.contains_key("PT")), "Count = {count}: both PT and FAIL"); + cases.push(CavpCase { + iv_bits: iv_bits.expect("a record before [IVlen]"), + tag_bits: tag_bits.expect("a record before [Taglen]"), + key: field("Key"), + iv: field("IV"), + aad: field("AAD"), + ct: field("CT"), + tag: field("Tag"), + pt: if fail { None } else { Some(field("PT")) }, + }); + } + cases +} + +/// Runs every case in one CAVP file that `Gcm` can express, and counts the rest as not supported. +fn run_cavp_file(filename: &str) { + let Some(content) = bc_test_data(CAVP_DIR, filename) else { return }; + let cases = parse_cavp_file(&content); + let encrypt = filename.starts_with("gcmEncrypt"); + + let (mut checked, mut forgeries, mut unsupported) = (0usize, 0usize, 0usize); + for c in &cases { + // `Gcm` fixes the nonce at 96 bits and takes 96- to 128-bit tags. + if c.iv_bits != 96 || c.tag_bits < 96 { + unsupported += 1; + continue; + } + assert_eq!(c.tag.len() * 8, c.tag_bits, "{filename}: Tag length vs [Taglen]"); + let iv: [u8; GCM_NONCE_LEN] = c.iv.as_slice().try_into().expect("a 96-bit IV"); + if encrypt { + let pt = c.pt.as_deref().expect("encrypt records carry PT"); + run_encrypt_case(&c.key, iv, &c.aad, pt, c.tag.len(), &c.ct, &c.tag); + } else { + forgeries += usize::from(c.pt.is_none()); + run_decrypt_case(&c.key, iv, &c.aad, &c.ct, &c.tag, c.pt.as_deref()); + } + checked += 1; + } + + println!( + "CAVP {filename}: {checked} cases checked ({forgeries} forgeries), \ + {unsupported} not supported" + ); + assert_eq!(cases.len(), CAVP_CASES_PER_FILE, "{filename}: cases in the file"); + assert_eq!(checked, CAVP_RUNNABLE_PER_FILE, "{filename}: cases Gcm can run"); + if !encrypt { + assert!( + 0 < forgeries && forgeries < checked, + "{filename}: expected valid and forged cases" + ); + } +} + +#[test] +fn cavp_aes_gcm_encrypt_128() { + run_cavp_file("gcmEncryptExtIV128.rsp"); +} + +#[test] +fn cavp_aes_gcm_encrypt_192() { + run_cavp_file("gcmEncryptExtIV192.rsp"); +} + +#[test] +fn cavp_aes_gcm_encrypt_256() { + run_cavp_file("gcmEncryptExtIV256.rsp"); +} + +#[test] +fn cavp_aes_gcm_decrypt_128() { + run_cavp_file("gcmDecrypt128.rsp"); +} + +#[test] +fn cavp_aes_gcm_decrypt_192() { + run_cavp_file("gcmDecrypt192.rsp"); +} + +#[test] +fn cavp_aes_gcm_decrypt_256() { + run_cavp_file("gcmDecrypt256.rsp"); +} diff --git a/crypto/aes/tests/gcm_bc_java_tests.rs b/crypto/aes/tests/gcm_bc_java_tests.rs new file mode 100644 index 00000000..b3382d45 --- /dev/null +++ b/crypto/aes/tests/gcm_bc_java_tests.rs @@ -0,0 +1,248 @@ +//! 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::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Gcm; +use bouncycastle_cipher::{Decrypting, Encrypting}; +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; + +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::hazmat::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::hazmat::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_detached_rng_out( + &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_detached_out( + &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/aes/tests/gcm_tests.rs b/crypto/aes/tests/gcm_tests.rs new file mode 100644 index 00000000..4cdd23e9 --- /dev/null +++ b/crypto/aes/tests/gcm_tests.rs @@ -0,0 +1,106 @@ +//! AES-GCM through the shared conformance suites, plus the alias checks. +//! +//! 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), that the inline +//! `ciphertext || tag` layout holds at every byte-boundary edge (`TestFrameworkAEADTaggedLayout`), +//! and that a fresh nonce is generated per encryption. Algorithm correctness itself is pinned by +//! the ACVP and bc-java known-answer suites beside this file. + +use bouncycastle_aes::hazmat::AES128Internal; +use bouncycastle_aes::{AES_GCM_128, AES_GCM_192, AES_GCM_256}; +use bouncycastle_cipher::modes::Gcm; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +use bouncycastle_core_test_framework::aead::TestFrameworkAEADCipher; +use bouncycastle_core_test_framework::aead::TestFrameworkAEADTaggedLayout; + +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 inline `ciphertext || tag` layout -- where the tag lands, what the decryptor holds back, +/// and how an input shorter than the tag is refused -- at every length across a few multiples of +/// the tag and under every chunking, through the shared runner, at both ends of the key-length +/// range. +#[test] +fn the_inline_tag_layout_conforms_at_every_edge() { + let framework = TestFrameworkAEADTaggedLayout::new(); + framework.test::<16, 12, 16, 16, AES_GCM_128, AES_GCM_128>(); + framework.test::<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_detached_out(&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_detached_out( + &key::<16>(), + &nonce, + b"aad", + &ct, + &tag, + &mut pt, + ) + .unwrap(); + assert_eq!(pt, data); + } +} diff --git a/crypto/aes/tests/gcm_wycheproof.rs b/crypto/aes/tests/gcm_wycheproof.rs new file mode 100644 index 00000000..70922aec --- /dev/null +++ b/crypto/aes/tests/gcm_wycheproof.rs @@ -0,0 +1,245 @@ +//! Known-answer tests against Project Wycheproof's `testvectors_v1/aes_gcm_test.json`. +//! +//! Requires the Wycheproof repository (https://github.com/C2SP/wycheproof) to be cloned alongside +//! this repository, i.e. at `../wycheproof` 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 vector +//! suites in this crate. +//! +//! # Why this set is worth having alongside the ACVP one +//! +//! `gcm_bc-test-data.rs` covers the NIST set, whose only failures are tag-check failures on an +//! otherwise well-formed message. Wycheproof's set is adversarial in the ways ACVP is not: a tag +//! with every one of a chosen set of bits flipped (`ModifiedTag`, 81 cases), so that a comparison +//! which checks only part of the tag is caught; IV lengths from 0 to 2056 bits (`ZeroLengthIv`, +//! `SmallIv`, `LongIv`); IVs chosen so that the 32-bit counter wraps (`CounterWrap`); and +//! pseudorandom sizes meant to catch an implementation that only handles the common cases. See +//! `aes_gcm_test.json`'s own `"notes"` object for exactly what each +//! `flags` entry is checking. +//! +//! # Ciphertext and tag are separate fields +//! +//! Wycheproof's AEAD schema (`aead_test_schema_v1`) carries `ct` and `tag` as distinct fields, so +//! these cases go through the detached pair, [`AEADCipherEncryptor::encrypt_detached_rng_out`] / +//! [`AEADCipherDecryptor::decrypt_detached_out`]. [`Gcm`] generates its own nonce, so the vector's +//! `iv` is supplied through a `FixedSeedRNG` and the returned nonce is asserted to be exactly that +//! IV, the same technique as `gcm_bc-test-data.rs`. +//! +//! # Only the 96-bit-IV groups can be dispatched to, by design +//! +//! [`Gcm`] fixes the nonce at [`GCM_NONCE_LEN`] (SP 800-38D Sec 5.2.1.1 recommends restricting +//! support to 96 bits) and does not implement the `len(IV) != 96` branch of Algorithm 4 step 2. A +//! group with any other `ivSize` therefore has no instantiation to dispatch to: not a case that +//! can fail, but a shape the library never sees. That is most of the file's groups -- the point +//! of `ZeroLengthIv`, `SmallIv`, `LongIv` and `CounterWrap` is to probe exactly that boundary -- +//! and they are counted as not supported rather than silently dropped, with the counts asserted +//! at the end so a change in the vector file's shape is visible. Every group in the file uses a +//! 128-bit tag, which is within the `12..=16` bytes `Gcm` accepts, so the tag size never rules a +//! case out. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::{GCM_NONCE_LEN, Gcm}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; + +/// 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 and CCM 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("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, TAG_LEN, P)`. +/// +/// For a `result: "valid"` case, `msg` must encrypt to exactly `expected_ct`/`expected_tag` under +/// the vector's IV, and `expected_ct`/`expected_tag` must decrypt back to `msg`. For +/// `result: "invalid"`, only the decrypt direction is checked -- re-encrypting `msg` has no +/// reason to reproduce a deliberately corrupted tag -- and it must fail the tag check rather than +/// return a payload, leaving the caller's buffer zeroized as the trait contract requires. +#[allow(clippy::too_many_arguments)] +fn run_case( + tc_id: u64, + key_bytes: &[u8], + iv: [u8; GCM_NONCE_LEN], + aad: &[u8], + msg: &[u8], + expected_ct: &[u8], + expected_tag: &[u8], + valid: bool, +) where + P: ElectronicCodeBook, +{ + let key = cipher_key::(key_bytes); + 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 (got_iv, written, got_tag) = + Gcm::::encrypt_detached_rng_out( + &key, + &mut FixedSeedRNG::::new(iv), + aad, + msg, + &mut ct, + ) + .unwrap_or_else(|e| panic!("tcId {tc_id}: valid case failed to encrypt: {e:?}")); + assert_eq!(got_iv, iv, "tcId {tc_id}: the seeded RNG must reproduce the vector's IV"); + 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 Gcm::::decrypt_detached_out( + &key, &iv, 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"); + assert!( + plaintext.iter().all(|&b| b == 0), + "tcId {tc_id}: a failed tag check must leave the output buffer zeroized" + ); + } + Err(e) => panic!("tcId {tc_id}: unexpected GCM error: {e:?}"), + } +} + +/// Dispatches to one of the three key lengths at the 96-bit nonce and 128-bit tag `Gcm` and the +/// vector file share, or reports that the case's IV or tag size has no instantiation to dispatch +/// to at all. +#[allow(clippy::too_many_arguments)] +fn dispatch( + tc_id: u64, + key_bytes: &[u8], + iv_bytes: &[u8], + aad: &[u8], + msg: &[u8], + expected_ct: &[u8], + expected_tag: &[u8], + valid: bool, +) -> bool { + // The nonce length is fixed by the type, so a case is dispatched on its actual `iv` length, + // not the group's declared `ivSize`. + let Ok(iv) = <[u8; GCM_NONCE_LEN]>::try_from(iv_bytes) else { return false }; + // Every group in the file is a 128-bit tag; anything else would need its own `TAG_LEN` + // instantiation, and `Gcm` accepts only 12..=16 bytes, so report rather than guess. + if expected_tag.len() != 16 { + return false; + } + match key_bytes.len() { + 16 => run_case::<16, 16, AES128Internal>( + tc_id, key_bytes, iv, aad, msg, expected_ct, expected_tag, valid, + ), + 24 => run_case::<24, 16, AES192Internal>( + tc_id, key_bytes, iv, aad, msg, expected_ct, expected_tag, valid, + ), + 32 => run_case::<32, 16, AES256Internal>( + tc_id, key_bytes, iv, aad, msg, expected_ct, expected_tag, valid, + ), + _ => return false, + } + true +} + +#[test] +fn wycheproof_aes_gcm_known_answer_tests() { + let Some(doc) = wycheproof_json("aes_gcm_test.json") else { return }; + + assert_eq!( + doc.get("algorithm").and_then(Value::as_str), + Some("AES-GCM"), + "this is the AES-GCM vector file" + ); + + 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 unsupported_groups = 0usize; + let mut unsupported_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 per-group tally for the printout; the per-case counts below come from `dispatch`, + // which is the authority on what it can run. + if iv_size_bits as usize != 8 * GCM_NONCE_LEN || tag_size_bits != 128 { + unsupported_groups += 1; + } + + let tests = group.get("tests").and_then(Value::as_array).expect("tests"); + + for test in tests { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + let key_bytes = hex_field(test, "key", tc_id); + let iv_bytes = hex_field(test, "iv", tc_id); + let aad = hex_field(test, "aad", tc_id); + let msg = hex_field(test, "msg", tc_id); + let ct = hex_field(test, "ct", tc_id); + let tag = hex_field(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, &iv_bytes, &aad, &msg, &ct, &tag, valid); + + if ran { + run += 1; + if valid { + valid_count += 1; + } else { + invalid_count += 1; + } + } else { + unsupported_cases += 1; + } + } + } + + println!( + "Wycheproof AES-GCM: {run} cases run ({valid_count} valid, {invalid_count} invalid), \ + {unsupported_cases} cases in {unsupported_groups} groups not supported \ + (no 96-bit-IV instantiation)" + ); + + // Guards against a silently-vacuous run: the three 96-bit-IV groups must have been dispatched + // to and must have included both valid and tag-modified cases. + assert!(run > 0, "expected the 96-bit-IV groups to be dispatchable"); + 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!( + unsupported_groups > 0, + "expected the other-IV-length groups to be outside Gcm's shape" + ); +} diff --git a/crypto/aes/tests/gmac_bc-test-data.rs b/crypto/aes/tests/gmac_bc-test-data.rs new file mode 100644 index 00000000..a68907cc --- /dev/null +++ b/crypto/aes/tests/gmac_bc-test-data.rs @@ -0,0 +1,103 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-GMAC` vectors from the `bc-test-data` repo. +//! +//! Same joiner and shape as `gcm_bc-test-data.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. + +#[path = "common/acvp_gcm_helpers.rs"] +mod acvp_gcm_helpers; + +use acvp_gcm_helpers::{GCM_NONCE_LEN, run_decrypt_case, run_encrypt_case}; +use bouncycastle_core_test_framework::test_data_loaders::{Value, bc_test_data_json, hex_field}; +use std::collections::BTreeMap; + +const TEST_DATA_DIR: &str = "crypto/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(req), Some(rsp)) = ( + bc_test_data_json(TEST_DATA_DIR, REQUEST_FILE), + bc_test_data_json(TEST_DATA_DIR, RESPONSE_FILE), + ) else { + return; + }; + + 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 = hex_field(test, "key", tc_id); + let aad = hex_field(test, "aad", tc_id); + let iv_bytes = hex_field(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 = hex_field(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 = hex_field(test, "tag", tc_id); + let answer = + answers.get(&tc_id).unwrap_or_else(|| panic!("tcId {tc_id}: no answer")); + // See `gcm_bc-test-data.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/aes/tests/gmac_wycheproof.rs b/crypto/aes/tests/gmac_wycheproof.rs new file mode 100644 index 00000000..db220788 --- /dev/null +++ b/crypto/aes/tests/gmac_wycheproof.rs @@ -0,0 +1,70 @@ +//! Known-answer tests against Project Wycheproof's `testvectors_v1/aes_gmac_test.json`. +//! +//! Requires the Wycheproof repository (https://github.com/C2SP/wycheproof) to be cloned alongside +//! this repository, i.e. at `../wycheproof` 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 vector +//! suites in this crate. +//! +//! GMAC is GCM with an empty plaintext (SP 800-38D Sec 5.2): the vector's `msg` is the AAD, and the +//! tag is the whole output. Cases run through the same helpers as `gmac_bc-test-data.rs`. A `valid` +//! case must produce exactly `tag`, which must then verify; an `invalid` case must fail the tag +//! check. +//! +//! `Gcm` fixes the IV at 96 bits, so the file's 128-bit-IV groups are counted as not supported, as +//! in `gcm_wycheproof.rs`. + +#[path = "common/acvp_gcm_helpers.rs"] +mod acvp_gcm_helpers; + +use acvp_gcm_helpers::{GCM_NONCE_LEN, run_decrypt_case, run_encrypt_case}; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; + +#[test] +fn wycheproof_aes_gmac() { + let Some(doc) = wycheproof_json("aes_gmac_test.json") else { return }; + + assert_eq!(doc.get("algorithm").and_then(Value::as_str), Some("AES-GMAC")); + + let (mut valid_count, mut invalid_count) = (0usize, 0usize); + let (mut unsupported_groups, mut unsupported_cases) = (0usize, 0usize); + for group in doc.get("testGroups").and_then(Value::as_array).expect("testGroups") { + let tests = group.get("tests").and_then(Value::as_array).expect("tests"); + let iv_bits = group.get("ivSize").and_then(Value::as_u64).expect("ivSize"); + if iv_bits as usize != 8 * GCM_NONCE_LEN { + unsupported_groups += 1; + unsupported_cases += tests.len(); + continue; + } + + for test in tests { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + let key = hex_field(test, "key", tc_id); + let iv: [u8; GCM_NONCE_LEN] = + hex_field(test, "iv", tc_id).try_into().expect("a 96-bit IV"); + let aad = hex_field(test, "msg", tc_id); + let tag = hex_field(test, "tag", tc_id); + + match test.get("result").and_then(Value::as_str).expect("result") { + "valid" => { + run_encrypt_case(&key, iv, &aad, &[], tag.len(), &[], &tag); + run_decrypt_case(&key, iv, &aad, &[], &tag, Some(&[])); + valid_count += 1; + } + "invalid" => { + run_decrypt_case(&key, iv, &aad, &[], &tag, None); + invalid_count += 1; + } + other => panic!("tcId {tc_id}: unexpected result {other}"), + } + } + } + + println!( + "Wycheproof AES-GMAC: {} cases run ({valid_count} valid, {invalid_count} invalid), \ + {unsupported_cases} cases in {unsupported_groups} groups not supported \ + (no 96-bit-IV instantiation)", + valid_count + invalid_count + ); + assert!(valid_count > 0 && invalid_count > 0, "expected both valid and invalid cases"); + assert!(unsupported_groups > 0, "expected the 128-bit-IV groups to be outside Gcm's shape"); +} diff --git a/crypto/aes/tests/sp800_38a_cbc_tests.rs b/crypto/aes/tests/sp800_38a_cbc_tests.rs new file mode 100644 index 00000000..9224be97 --- /dev/null +++ b/crypto/aes/tests/sp800_38a_cbc_tests.rs @@ -0,0 +1,243 @@ +//! Known-answer tests from NIST SP 800-38A Appendix F.2, "CBC Example Vectors". +//! +//! Sections F.2.1 through F.2.6: CBC-AES128, CBC-AES192 and CBC-AES256, Encrypt and Decrypt. All +//! six share the same IV and the same four plaintext blocks (Appendix F preamble); only the key and +//! the resulting ciphertext differ. The three keys are the same three used by FIPS 197 Appendix A +//! and SP 800-38A F.1, so these vectors also re-check each AES key expansion through a second +//! construction. +//! +//! Transcribed from the published SP 800-38A PDF (2001 edition). +//! +//! # Driving the IV +//! +//! There is no API for supplying an IV -- see the crate docs. Encryption is therefore driven +//! through [`BlockCipherEncryptor::do_encrypt_init_rng`] with a [`FixedSeedRNG`] whose stream is +//! the vector's IV, and the test asserts the returned init data really is that IV before comparing +//! any ciphertext. Decryption takes the IV directly, as init data. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Cbc; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; + +const BLOCK_LEN: usize = 16; + +/// The IV shared by every Appendix F.2 subsection. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// The four plaintext blocks shared by every Appendix F subsection (Appendix F preamble). +const PLAINTEXTS: [&str; 4] = [ + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", +]; + +/// F.2.1 / F.2.2 key. +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +/// F.2.1 CBC-AES128.Encrypt output blocks. +const CIPHERTEXTS_128: [&str; 4] = [ + "7649abac8119b246cee98e9b12e9197d", + "5086cb9b507219ee95db113a917678b2", + "73bed6b8e3c1743b7116e69e22229516", + "3ff1caa1681fac09120eca307586e1a7", +]; + +/// F.2.3 / F.2.4 key. +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +/// F.2.3 CBC-AES192.Encrypt output blocks. +const CIPHERTEXTS_192: [&str; 4] = [ + "4f021db243bc633d7178183a9fa071e8", + "b4d9ada9ad7dedf4e5e738763f69145a", + "571b242012fb7ae07fa9baac3df102e0", + "08b0e27988598881d920a9e64f5615cd", +]; + +/// F.2.5 / F.2.6 key. +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; +/// F.2.5 CBC-AES256.Encrypt output blocks. +const CIPHERTEXTS_256: [&str; 4] = [ + "f58c4c04d6e5f1ba779eabfb5f7bfbd6", + "9cfc4e967edb808d679f777bc6702c7d", + "39f23369a9d9bacfa530e26304231461", + "b2eb05e2c39be9fcda6c19078c6a9d1b", +]; + +fn block(hex_str: &str) -> [u8; BLOCK_LEN] { + hex::decode(hex_str).expect("valid hex").try_into().expect("16 bytes") +} + +fn blocks(hex_strs: &[&str; 4]) -> [[u8; BLOCK_LEN]; 4] { + core::array::from_fn(|i| block(hex_strs[i])) +} + +/// The same four blocks as 64 contiguous bytes, for the flat streaming and one-shot methods. +fn flat(hex_strs: &[&str; 4]) -> [u8; 4 * BLOCK_LEN] { + blocks(hex_strs).as_flattened().try_into().expect("4 blocks = 64 bytes") +} + +fn key_material(hex_str: &str) -> KeyMaterial { + let bytes = hex::decode(hex_str).expect("valid hex"); + assert_eq!(bytes.len(), N, "key length"); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +/// Runs one Appendix F.2 encrypt subsection. +/// +/// Checks the whole message in one call, then again one block at a time, then again through the +/// implementor hook -- the vector should not care how the calls are grouped. +fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) +where + P: ElectronicCodeBook, +{ + let key = key_material::(key_hex); + let iv = block(IV); + let pt = blocks(&PLAINTEXTS); + let ct = blocks(expected); + + // All four blocks in one call. + let (mut enc, got_iv) = Cbc::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .unwrap(); + assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); + let mut data = flat(&PLAINTEXTS); + enc.do_encrypt_inplace(&mut data).unwrap(); + assert_eq!(data, flat(expected), "{section}: four blocks in one call"); + + // One block at a time. + let (mut enc, _) = Cbc::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .unwrap(); + for (i, (p, c)) in pt.iter().zip(ct.iter()).enumerate() { + let mut got = *p; + enc.do_encrypt_inplace(&mut got).unwrap(); + assert_eq!(&got, c, "{section}: block #{}", i + 1); + } + + // Through the implementor hook, `do_*_blocks`. + let (mut enc, _) = Cbc::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .unwrap(); + let mut blocks = pt; + enc.do_encrypt_blocks_inplace(&mut blocks).unwrap(); + assert_eq!(blocks, ct, "{section}: implementor hook"); +} + +/// Runs one Appendix F.2 decrypt subsection. +/// +/// Checks one call, one block at a time, and the odd grouping `3 + 1` -- which is the grouping that +/// leaves a one-block remainder after the pair loop in `do_decrypt_blocks_inplace`. +fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) +where + P: ElectronicCodeBook, +{ + let key = key_material::(key_hex); + let iv = block(IV); + let pt = blocks(&PLAINTEXTS); + let ct = blocks(ciphertext); + + type Dec = Cbc; + + // All four blocks in one call (two pairs, no remainder). + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut data = flat(ciphertext); + dec.do_decrypt_inplace(&mut data).unwrap(); + assert_eq!(data, flat(&PLAINTEXTS), "{section}: four blocks in one call"); + + // One block at a time (never takes the pair path). + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + for (i, (c, p)) in ct.iter().zip(pt.iter()).enumerate() { + let mut got = *c; + dec.do_decrypt_inplace(&mut got).unwrap(); + assert_eq!(&got, p, "{section}: block #{}", i + 1); + } + + // 3 + 1: one pair plus a remainder, then a lone block. + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); + dec.do_decrypt_inplace(&mut three).unwrap(); + let mut one = ct[3]; + dec.do_decrypt_inplace(&mut one).unwrap(); + assert_eq!(&three[..], pt[..3].as_flattened(), "{section}: blocks 1-3"); + assert_eq!(one, pt[3], "{section}: block 4"); + + // Through the implementor hook, `do_*_blocks`. + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut blocks = ct; + dec.do_decrypt_blocks_inplace(&mut blocks).unwrap(); + assert_eq!(blocks, pt, "{section}: implementor hook"); +} + +#[test] +fn f_2_1_cbc_aes128_encrypt() { + check_encrypt::("F.2.1", KEY_128, &CIPHERTEXTS_128); +} + +#[test] +fn f_2_2_cbc_aes128_decrypt() { + check_decrypt::("F.2.2", KEY_128, &CIPHERTEXTS_128); +} + +#[test] +fn f_2_3_cbc_aes192_encrypt() { + check_encrypt::("F.2.3", KEY_192, &CIPHERTEXTS_192); +} + +#[test] +fn f_2_4_cbc_aes192_decrypt() { + check_decrypt::("F.2.4", KEY_192, &CIPHERTEXTS_192); +} + +#[test] +fn f_2_5_cbc_aes256_encrypt() { + check_encrypt::("F.2.5", KEY_256, &CIPHERTEXTS_256); +} + +#[test] +fn f_2_6_cbc_aes256_decrypt() { + check_decrypt::("F.2.6", KEY_256, &CIPHERTEXTS_256); +} + +/// The one-shot API must agree with the vectors too, on the decrypt side where the IV is an input. +/// The one-shots take flat arrays and work in place, so the four ciphertext blocks are presented +/// as 64 contiguous bytes and become the four plaintext blocks. +/// The IV really is what distinguishes CBC from ECB here: the same key and plaintext under the +/// F.1 (ECB) conditions gives the F.1 ciphertext, and under F.2 gives a different one. +/// +/// F.1.1 block #1 for this key is `3ad77bb40d7a3660a89ecaf32466ef97`; F.2.1 block #1 is +/// `7649abac8119b246cee98e9b12e9197d`. They differ solely because CBC XORs the IV in first. +#[test] +fn cbc_differs_from_ecb_by_the_iv() { + let key = key_material::<16>(KEY_128); + let iv = block(IV); + + // The raw permutation on P1 alone is the ECB answer from F.1.1. + let mut ecb = block(PLAINTEXTS[0]); + >::encrypt_block( + &>::new(&key).unwrap(), + &mut ecb, + ); + assert_eq!(ecb, block("3ad77bb40d7a3660a89ecaf32466ef97"), "F.1.1 block #1"); + + // CBC's C1 = CIPH_K(P1 XOR IV) is the F.2.1 answer, and differs. + let (mut enc, _) = Cbc::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::<16>::new(iv), + ) + .unwrap(); + let mut cbc = block(PLAINTEXTS[0]); + enc.do_encrypt_inplace(&mut cbc).unwrap(); + assert_eq!(cbc, block(CIPHERTEXTS_128[0]), "F.2.1 block #1"); + assert_ne!(cbc, ecb); +} diff --git a/crypto/aes/tests/sp800_38a_cfb8_tests.rs b/crypto/aes/tests/sp800_38a_cfb8_tests.rs new file mode 100644 index 00000000..97992001 --- /dev/null +++ b/crypto/aes/tests/sp800_38a_cfb8_tests.rs @@ -0,0 +1,307 @@ +//! Known-answer tests from NIST SP 800-38A Appendix F.3, "CFB Example Vectors". +//! +//! Sections **F.3.7 through F.3.12**: CFB8-AES128, CFB8-AES192 and CFB8-AES256, Encrypt and +//! Decrypt. These are the `s = 8` subsections, the ones [`Cfb8`] implements. The `s = b` +//! subsections F.3.13-F.3.18 belong to [`Cfb`](bouncycastle_cipher::modes::Cfb) and are in +//! `sp800_38a_cfb_tests.rs`; F.3.1-F.3.6 are CFB1, which this crate does not provide. +//! +//! All six share the same IV. The plaintext is the **first 18 bytes** of the Appendix F plaintext: +//! the preamble notes that the CFB1 and CFB8 subsections truncate it, and each of these tabulates +//! 18 one-byte segments. Only the key and the resulting ciphertext differ between key lengths, and +//! the three keys are the same three used throughout Appendix F. +//! +//! Transcribed from the published SP 800-38A PDF (2001 edition). +//! +//! # The shift register is checked against the spec's own table +//! +//! Each F.3 subsection tabulates the **input block** and the **output block** for every segment. +//! For CFB8 those columns are the whole mechanism: the input block is the shift register, and the +//! output block is what `MSB_8` takes its byte from. `the_tabulated_blocks_are_the_shift_register` +//! transcribes all 18 of each for F.3.7 and checks them three ways -- that each input block is the +//! previous one shifted left by a byte with the ciphertext byte appended, that each output block is +//! the raw permutation applied to it, and that the ciphertext is the plaintext XOR its first byte. +//! A mode that produced the right ciphertext by some other route would still have to match them. +//! That check is key-independent, so it is done once rather than for all three key lengths. +//! +//! # Driving the IV +//! +//! There is no API for supplying an IV -- see the crate docs. Encryption is therefore driven +//! through [`SymmetricCipherEncryptor::do_encrypt_init_rng`] with a [`FixedSeedRNG`] whose stream is +//! the vector's IV, and the test asserts the returned init data really is that IV before comparing +//! any ciphertext. Decryption takes the IV directly, as init data. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Cfb8; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; + +const BLOCK_LEN: usize = 16; + +/// The IV shared by every Appendix F.3 subsection. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// The 18 one-byte plaintext segments shared by every CFB8 subsection: the first 18 bytes of the +/// Appendix F plaintext, which the CFB1 and CFB8 subsections truncate to. +const PLAINTEXT: &str = "6bc1bee22e409f96e93d7e117393172aae2d"; + +/// F.3.7 / F.3.8 key. +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +/// F.3.7 CFB8-AES128.Encrypt ciphertext segments. +const CIPHERTEXT_128: &str = "3b79424c9c0dd436bace9e0ed4586a4f32b9"; + +/// F.3.9 / F.3.10 key. +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +/// F.3.9 CFB8-AES192.Encrypt ciphertext segments. +const CIPHERTEXT_192: &str = "cda2521ef0a905ca44cd057cbf0d47a0678a"; + +/// F.3.11 / F.3.12 key. +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; +/// F.3.11 CFB8-AES256.Encrypt ciphertext segments. +const CIPHERTEXT_256: &str = "dc1f1a8520a64db55fcc8ac554844e889700"; + +/// F.3.7 CFB8-AES128.Encrypt, the "Input Block" column: the shift register at each segment. +const INPUT_BLOCKS_128: [&str; 18] = [ + "000102030405060708090a0b0c0d0e0f", + "0102030405060708090a0b0c0d0e0f3b", + "02030405060708090a0b0c0d0e0f3b79", + "030405060708090a0b0c0d0e0f3b7942", + "0405060708090a0b0c0d0e0f3b79424c", + "05060708090a0b0c0d0e0f3b79424c9c", + "060708090a0b0c0d0e0f3b79424c9c0d", + "0708090a0b0c0d0e0f3b79424c9c0dd4", + "08090a0b0c0d0e0f3b79424c9c0dd436", + "090a0b0c0d0e0f3b79424c9c0dd436ba", + "0a0b0c0d0e0f3b79424c9c0dd436bace", + "0b0c0d0e0f3b79424c9c0dd436bace9e", + "0c0d0e0f3b79424c9c0dd436bace9e0e", + "0d0e0f3b79424c9c0dd436bace9e0ed4", + "0e0f3b79424c9c0dd436bace9e0ed458", + "0f3b79424c9c0dd436bace9e0ed4586a", + "3b79424c9c0dd436bace9e0ed4586a4f", + "79424c9c0dd436bace9e0ed4586a4f32", +]; + +/// F.3.7 CFB8-AES128.Encrypt, the "Output Block" column: `Oj = CIPH_K(Ij)`, of which CFB8 uses +/// only the first byte. +const OUTPUT_BLOCKS_128: [&str; 18] = [ + "50fe67cc996d32b6da0937e99bafec60", + "b8eb865a2b026381abb1d6560ed20f68", + "fce6033b4edce64cbaed3f61ff5b927c", + "ae4e5e7ffe805f7a4395b180004f8ca8", + "b205eb89445b62116f1deb988a81e6dd", + "4d21d456a5e239064fff4be0c0f85488", + "4b2f5c3895b9efdc85ee0c5178c7fd33", + "a0976d856da260a34104d1a80953db4c", + "53674e5890a2c71b0f6a27a094e5808c", + "f34cd32ffed495f8bc8adba194eccb7a", + "e08cf2407d7ed676c9049586f1d48ba6", + "1f5c88a19b6ca28e99c9aeb8982a6dd8", + "a70e63df781cf395a208bd2365c8779b", + "cbcfe8b3bcf9ac202ce18420013319ab", + "7d9fac6604b3c8c5b1f8c5a00956cf56", + "65c3fa64bf0343986825c636f4a1efd2", + "9cff5e5ff4f554d56c924b9d6a6de21d", + "946c3dc1584cc18400ecd8c6052c44b1", +]; + +fn block(hex_str: &str) -> [u8; BLOCK_LEN] { + hex::decode(hex_str).expect("valid hex").try_into().expect("16 bytes") +} + +fn bytes(hex_str: &str) -> Vec { + hex::decode(hex_str).expect("valid hex") +} + +fn key_material(hex_str: &str) -> KeyMaterial { + let raw = hex::decode(hex_str).expect("valid hex"); + assert_eq!(raw.len(), N, "key length"); + KeyMaterial::::from_bytes_as_type(&raw, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +/// Chunk sizes that cut across the four-byte batch and the 16-byte block: 1 is the single-byte +/// path only, 4 is exactly the batch, 8 is two, and the rest leave a different remainder each call. +const CHUNKINGS: [usize; 7] = [1, 3, 4, 8, 9, 17, 18]; + +/// Runs one Appendix F.3 CFB8 encrypt subsection. +/// +/// Checks the whole message in one call, then in every chunking above -- the vector should not care +/// how the calls are grouped. +fn check_encrypt(section: &str, key_hex: &str, expected_hex: &str) +where + P: ElectronicCodeBook, +{ + let key = key_material::(key_hex); + let iv = block(IV); + let plaintext = bytes(PLAINTEXT); + let expected = bytes(expected_hex); + assert_eq!(plaintext.len(), 18, "{section}: the CFB8 subsections use 18 one-byte segments"); + + for chunk in [plaintext.len()].into_iter().chain(CHUNKINGS) { + let (mut enc, got_iv) = Cfb8::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .unwrap(); + assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); + + let mut data = plaintext.clone(); + for piece in data.chunks_mut(chunk) { + enc.do_encrypt_inplace(piece).unwrap(); + } + assert_eq!(data, expected, "{section}: {chunk}-byte calls"); + } +} + +/// Runs one Appendix F.3 CFB8 decrypt subsection. +fn check_decrypt(section: &str, key_hex: &str, ciphertext_hex: &str) +where + P: ElectronicCodeBook, +{ + let key = key_material::(key_hex); + let iv = block(IV); + let plaintext = bytes(PLAINTEXT); + let ciphertext = bytes(ciphertext_hex); + + for chunk in [ciphertext.len()].into_iter().chain(CHUNKINGS) { + let mut dec = + Cfb8::::do_decrypt_init(&key, &iv).unwrap(); + let mut data = ciphertext.clone(); + for piece in data.chunks_mut(chunk) { + dec.do_decrypt_inplace(piece).unwrap(); + } + assert_eq!(data, plaintext, "{section}: {chunk}-byte calls"); + } + + // ...and the one-shot, where the IV is an input. + let mut data = ciphertext.clone(); + Cfb8::::decrypt_inplace(&key, &iv, &mut data).unwrap(); + assert_eq!(data, plaintext, "{section}: one-shot"); +} + +#[test] +fn f_3_7_cfb8_aes128_encrypt() { + check_encrypt::("F.3.7", KEY_128, CIPHERTEXT_128); +} + +#[test] +fn f_3_8_cfb8_aes128_decrypt() { + check_decrypt::("F.3.8", KEY_128, CIPHERTEXT_128); +} + +#[test] +fn f_3_9_cfb8_aes192_encrypt() { + check_encrypt::("F.3.9", KEY_192, CIPHERTEXT_192); +} + +#[test] +fn f_3_10_cfb8_aes192_decrypt() { + check_decrypt::("F.3.10", KEY_192, CIPHERTEXT_192); +} + +#[test] +fn f_3_11_cfb8_aes256_encrypt() { + check_encrypt::("F.3.11", KEY_256, CIPHERTEXT_256); +} + +#[test] +fn f_3_12_cfb8_aes256_decrypt() { + check_decrypt::("F.3.12", KEY_256, CIPHERTEXT_256); +} + +/// The spec's tabulated **Input Blocks** are the shift register and its **Output Blocks** are +/// `CIPH_K` of them. Both fall straight out of Sec 6.3 with `s = 8`: +/// +/// ```text +/// I1 = IV; Ij = LSB_{b-8}(I_{j-1}) | C_{j-1}; Oj = CIPH_K(Ij); Cj = Pj XOR MSB_8(Oj) +/// ``` +/// +/// Checking all three relations against F.3.7's own table pins the mode's internals rather than +/// just its final output, and it confirms the transcription: the input, output, plaintext and +/// ciphertext columns are related by a shift, a cipher call and an XOR, none of which would survive +/// a typo in any of them. +#[test] +fn the_tabulated_blocks_are_the_shift_register() { + let key = key_material::<16>(KEY_128); + let perm = + >::new(&key).expect("a valid key"); + let plaintext = bytes(PLAINTEXT); + let ciphertext = bytes(CIPHERTEXT_128); + + for j in 0..18 { + let input_block = block(INPUT_BLOCKS_128[j]); + let output_block = block(OUTPUT_BLOCKS_128[j]); + + // I1 = IV, and Ij = LSB_{b-8}(I_{j-1}) | C_{j-1} thereafter. + if j == 0 { + assert_eq!(input_block, block(IV), "F.3.7: I1 must be the IV"); + } else { + let previous = block(INPUT_BLOCKS_128[j - 1]); + let mut expected = [0u8; BLOCK_LEN]; + expected[..BLOCK_LEN - 1].copy_from_slice(&previous[1..]); + expected[BLOCK_LEN - 1] = ciphertext[j - 1]; + assert_eq!( + input_block, + expected, + "F.3.7: I{} should be I{} shifted left one byte with C{} appended", + j + 1, + j, + j + ); + } + + // Oj = CIPH_K(Ij) -- the *forward* cipher function, which is all CFB ever uses. + let mut computed = input_block; + perm.encrypt_block(&mut computed); + assert_eq!( + computed, + output_block, + "F.3.7: tabulated output block #{} should be CIPH_K of input block #{}", + j + 1, + j + 1 + ); + + // Cj = Pj XOR MSB_8(Oj): the first byte of the output block, the rest discarded. + assert_eq!( + ciphertext[j], + plaintext[j] ^ output_block[0], + "F.3.7: Cj = Pj XOR MSB_8(Oj) for segment #{}", + j + 1 + ); + } +} + +/// CFB8 and CFB128 agree on the **first** byte and on nothing after it. +/// +/// Both set `I1 = IV` and `O1 = CIPH_K(IV)`, and both XOR the leading byte of `O1` into the first +/// plaintext byte, so `C1` is necessarily the same. They diverge immediately after, because CFB128 +/// replaces the whole input block with the ciphertext block while CFB8 shifts one byte in. +/// +/// The values below are quoted from **F.3.13 (CFB128-AES128.Encrypt)**, a different subsection from +/// the ones this file is testing, so agreement on byte 1 is an independent check that the F.3.7 +/// transcription is right, and disagreement on byte 2 is a check that [`Cfb8`] is CFB8 and not +/// CFB128. +#[test] +fn cfb8_agrees_with_cfb128_on_the_first_byte_only() { + /// F.3.13 CFB128-AES128.Encrypt, ciphertext segment #1 (16 bytes). + const CFB128_C1: &str = "3b3fd92eb72dad20333449f8e83cfb4a"; + + let cfb128_c1 = bytes(CFB128_C1); + let cfb8_ct = bytes(CIPHERTEXT_128); + + assert_eq!( + cfb8_ct[0], cfb128_c1[0], + "F.3.7 and F.3.13 must agree on the first byte: both are P1 XOR MSB_8(CIPH_K(IV))" + ); + assert_ne!( + cfb8_ct[1], cfb128_c1[1], + "the second byte must differ: CFB8 shifts the register, CFB128 replaces it" + ); +} diff --git a/crypto/aes/tests/sp800_38a_cfb_tests.rs b/crypto/aes/tests/sp800_38a_cfb_tests.rs new file mode 100644 index 00000000..d89ec164 --- /dev/null +++ b/crypto/aes/tests/sp800_38a_cfb_tests.rs @@ -0,0 +1,367 @@ +//! Known-answer tests from NIST SP 800-38A Appendix F.3, "CFB Example Vectors". +//! +//! Sections **F.3.13 through F.3.18**: CFB128-AES128, CFB128-AES192 and CFB128-AES256, Encrypt and +//! Decrypt. These are the `s = b` subsections, the ones [`Cfb`] implements. The rest of Appendix F.3 +//! -- F.3.1-F.3.6 (CFB1) and F.3.7-F.3.12 (CFB8) -- covers segment sizes this crate does not +//! provide, and is deliberately not transcribed; see the [`Cfb`] module docs. +//! +//! [`Cfb`] is a stream cipher, so besides the segment-at-a-time and whole-message calls the vectors +//! are also driven in chunks that do not line up with the segments at all. The expected output is +//! the same: the chunking of the calls is not visible in the ciphertext. +//! +//! All six share the same IV and the same four plaintext blocks (Appendix F preamble: the plaintext +//! is the same for every subsection except the CFB1 and CFB8 ones, which truncate it); only the key +//! and the resulting ciphertext differ. The three keys are the same three used by SP 800-38A F.1 +//! (ECB) and F.2 (CBC), so these vectors also re-check each AES key expansion through a third +//! construction. +//! +//! Transcribed from the published SP 800-38A PDF (2001 edition). +//! +//! # The intermediate values are checked too +//! +//! Unlike Appendix F.2, whose "Input Block" is just `Pj XOR Cj-1`, the F.3 subsections tabulate the +//! CFB **output blocks** -- the keystream `Oj` -- alongside the input blocks. Those are the mode's +//! internals, so `the_tabulated_output_blocks_are_the_keystream` checks them against the raw +//! permutation rather than only comparing final ciphertext. A mode that produced the right +//! ciphertext by a different route would still have to match them. +//! +//! # Driving the IV +//! +//! There is no API for supplying an IV -- see the crate docs. Encryption is therefore driven +//! through [`SymmetricCipherEncryptor::do_encrypt_init_rng`] with a [`FixedSeedRNG`] whose stream is +//! the vector's IV, and the test asserts the returned init data really is that IV before comparing +//! any ciphertext. Decryption takes the IV directly, as init data. + +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::Cfb; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; + +const BLOCK_LEN: usize = 16; + +/// The IV shared by every Appendix F.3 subsection. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// The four plaintext blocks shared by every Appendix F subsection (Appendix F preamble). +const PLAINTEXTS: [&str; 4] = [ + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", +]; + +/// F.3.13 / F.3.14 key. +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +/// F.3.13 CFB128-AES128.Encrypt ciphertext segments. +const CIPHERTEXTS_128: [&str; 4] = [ + "3b3fd92eb72dad20333449f8e83cfb4a", + "c8a64537a0b3a93fcde3cdad9f1ce58b", + "26751f67a3cbb140b1808cf187a4f4df", + "c04b05357c5d1c0eeac4c66f9ff7f2e6", +]; +/// F.3.13 CFB128-AES128.Encrypt output blocks, i.e. the keystream `Oj`. +const OUTPUT_BLOCKS_128: [&str; 4] = [ + "50fe67cc996d32b6da0937e99bafec60", + "668bcf60beb005a35354a201dab36bda", + "16bd032100975551547b4de89daea630", + "36d42170a312871947ef8714799bc5f6", +]; + +/// F.3.15 / F.3.16 key. +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +/// F.3.15 CFB128-AES192.Encrypt ciphertext segments. +const CIPHERTEXTS_192: [&str; 4] = [ + "cdc80d6fddf18cab34c25909c99a4174", + "67ce7f7f81173621961a2b70171d3d7a", + "2e1e8a1dd59b88b1c8e60fed1efac4c9", + "c05f9f9ca9834fa042ae8fba584b09ff", +]; +/// F.3.15 CFB128-AES192.Encrypt output blocks. +const OUTPUT_BLOCKS_192: [&str; 4] = [ + "a609b38df3b1133dddff2718ba09565e", + "c9e3f5289f149abd08ad44dc52b2b32b", + "1ed6965b76c76ca02d1dcef404f09626", + "36c0bbd976ccd4b7ef85cec1be273eef", +]; + +/// F.3.17 / F.3.18 key. +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; +/// F.3.17 CFB128-AES256.Encrypt ciphertext segments. +const CIPHERTEXTS_256: [&str; 4] = [ + "dc7e84bfda79164b7ecd8486985d3860", + "39ffed143b28b1c832113c6331e5407b", + "df10132415e54b92a13ed0a8267ae2f9", + "75a385741ab9cef82031623d55b1e471", +]; +/// F.3.17 CFB128-AES256.Encrypt output blocks. +const OUTPUT_BLOCKS_256: [&str; 4] = [ + "b7bf3a5df43989dd97f0fa97ebce2f4a", + "97d26743252b1d54aca653cf744ace2a", + "efd80f62b6b9af8344c511b13c70b016", + "833ca131c5f655ef8d1a2346b3ddd361", +]; + +fn block(hex_str: &str) -> [u8; BLOCK_LEN] { + hex::decode(hex_str).expect("valid hex").try_into().expect("16 bytes") +} + +fn blocks(hex_strs: &[&str; 4]) -> [[u8; BLOCK_LEN]; 4] { + core::array::from_fn(|i| block(hex_strs[i])) +} + +/// The same four blocks as 64 contiguous bytes, for the flat streaming and one-shot methods. +fn flat(hex_strs: &[&str; 4]) -> [u8; 4 * BLOCK_LEN] { + blocks(hex_strs).as_flattened().try_into().expect("4 blocks = 64 bytes") +} + +fn key_material(hex_str: &str) -> KeyMaterial { + let bytes = hex::decode(hex_str).expect("valid hex"); + assert_eq!(bytes.len(), N, "key length"); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +/// Chunk sizes that never line up with a 16-byte segment, for the stream-cipher checks. +const ODD_CHUNKS: [usize; 3] = [5, 23, 63]; + +/// Runs one Appendix F.3 encrypt subsection. +/// +/// Checks the whole message in one call, then again one segment at a time, then again in chunks +/// that straddle the segments -- the vector should not care how the calls are grouped. +fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) +where + P: ElectronicCodeBook, +{ + let key = key_material::(key_hex); + let iv = block(IV); + let pt = blocks(&PLAINTEXTS); + let ct = blocks(expected); + + let init = || { + let (enc, got_iv) = Cfb::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .unwrap(); + assert_eq!(got_iv, iv, "{section}: the pinned RNG should produce the vector's IV"); + enc + }; + + // All four segments in one call. + let mut enc = init(); + let mut data = flat(&PLAINTEXTS); + enc.do_encrypt_inplace(&mut data).unwrap(); + assert_eq!(data, flat(expected), "{section}: four segments in one call"); + + // One segment at a time. + let mut enc = init(); + for (i, (p, c)) in pt.iter().zip(ct.iter()).enumerate() { + let mut got = *p; + enc.do_encrypt_inplace(&mut got).unwrap(); + assert_eq!(&got, c, "{section}: segment #{}", i + 1); + } + + // In chunks that cut across the segments. + for chunk in ODD_CHUNKS { + let mut enc = init(); + let mut data = flat(&PLAINTEXTS); + for piece in data.chunks_mut(chunk) { + enc.do_encrypt_inplace(piece).unwrap(); + } + assert_eq!(data, flat(expected), "{section}: {chunk}-byte calls"); + } +} + +/// Runs one Appendix F.3 decrypt subsection. +/// +/// Checks one call, one segment at a time, the odd grouping `3 + 1` -- which is the grouping that +/// leaves a one-block remainder after the pair loop in `do_decrypt_inplace` -- and chunks that +/// straddle the segments. +fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) +where + P: ElectronicCodeBook, +{ + let key = key_material::(key_hex); + let iv = block(IV); + let pt = blocks(&PLAINTEXTS); + let ct = blocks(ciphertext); + + type Dec = Cfb; + + // All four segments in one call (two pairs, no remainder). + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut data = flat(ciphertext); + dec.do_decrypt_inplace(&mut data).unwrap(); + assert_eq!(data, flat(&PLAINTEXTS), "{section}: four segments in one call"); + + // One segment at a time (never takes the pair path). + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + for (i, (c, p)) in ct.iter().zip(pt.iter()).enumerate() { + let mut got = *c; + dec.do_decrypt_inplace(&mut got).unwrap(); + assert_eq!(&got, p, "{section}: segment #{}", i + 1); + } + + // 3 + 1: one pair plus a remainder, then a lone block. + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); + dec.do_decrypt_inplace(&mut three).unwrap(); + let mut one = ct[3]; + dec.do_decrypt_inplace(&mut one).unwrap(); + assert_eq!(&three[..], pt[..3].as_flattened(), "{section}: segments 1-3"); + assert_eq!(one, pt[3], "{section}: segment 4"); + + // In chunks that cut across the segments. + for chunk in ODD_CHUNKS { + let mut dec = Dec::::do_decrypt_init(&key, &iv).unwrap(); + let mut data = flat(ciphertext); + for piece in data.chunks_mut(chunk) { + dec.do_decrypt_inplace(piece).unwrap(); + } + assert_eq!(data, flat(&PLAINTEXTS), "{section}: {chunk}-byte calls"); + } +} + +#[test] +fn f_3_13_cfb128_aes128_encrypt() { + check_encrypt::("F.3.13", KEY_128, &CIPHERTEXTS_128); +} + +#[test] +fn f_3_14_cfb128_aes128_decrypt() { + check_decrypt::("F.3.14", KEY_128, &CIPHERTEXTS_128); +} + +#[test] +fn f_3_15_cfb128_aes192_encrypt() { + check_encrypt::("F.3.15", KEY_192, &CIPHERTEXTS_192); +} + +#[test] +fn f_3_16_cfb128_aes192_decrypt() { + check_decrypt::("F.3.16", KEY_192, &CIPHERTEXTS_192); +} + +#[test] +fn f_3_17_cfb128_aes256_encrypt() { + check_encrypt::("F.3.17", KEY_256, &CIPHERTEXTS_256); +} + +#[test] +fn f_3_18_cfb128_aes256_decrypt() { + check_decrypt::("F.3.18", KEY_256, &CIPHERTEXTS_256); +} + +/// The one-shot API must agree with the vectors too, on the decrypt side where the IV is an input. +/// The one-shots work in place, so the four ciphertext segments are presented as 64 contiguous +/// bytes and become the four plaintext blocks. +/// The spec's tabulated **Output Blocks** are the CFB keystream, and its **Input Blocks** are the +/// IV followed by the ciphertext segments. Both fall straight out of Sec 6.3 with `s = b`: +/// +/// ```text +/// I1 = IV; Ij = C_{j-1} (j >= 2); Oj = CIPH_K(Ij); Cj = Pj XOR Oj +/// ``` +/// +/// So each `Oj` in the table must equal the raw permutation applied to the previous ciphertext +/// segment (or to the IV, for `j = 1`), and XOR-ing it with the plaintext must give the ciphertext. +/// Checking this pins the mode's internals against the spec, not just its final output -- and in +/// particular it is what distinguishes CFB from a mode that happens to agree on the ciphertext. +/// +/// It also confirms the transcription: the ciphertext and output-block columns above are related by +/// an XOR that would not survive a typo in either. +fn check_output_blocks( + section: &str, + key_hex: &str, + ciphertexts: &[&str; 4], + output_blocks: &[&str; 4], +) where + P: ElectronicCodeBook, +{ + let key = key_material::(key_hex); + let perm = P::new(&key).expect("a valid key"); + let pt = blocks(&PLAINTEXTS); + let ct = blocks(ciphertexts); + let o = blocks(output_blocks); + + for j in 0..4 { + // Ij: the IV for j = 1, otherwise the previous ciphertext segment. + let input_block = if j == 0 { block(IV) } else { ct[j - 1] }; + + // Oj = CIPH_K(Ij) -- the *forward* cipher function, which is all CFB ever uses. + let mut computed = input_block; + perm.encrypt_block(&mut computed); + assert_eq!( + computed, + o[j], + "{section}: tabulated output block #{} should be CIPH_K of input block #{}", + j + 1, + j + 1 + ); + + // Cj = Pj XOR Oj. + let xored: [u8; BLOCK_LEN] = core::array::from_fn(|k| pt[j][k] ^ o[j][k]); + assert_eq!(xored, ct[j], "{section}: Cj = Pj XOR Oj for segment #{}", j + 1); + } +} + +#[test] +fn the_tabulated_output_blocks_are_the_keystream() { + check_output_blocks::( + "F.3.13", KEY_128, &CIPHERTEXTS_128, &OUTPUT_BLOCKS_128, + ); + check_output_blocks::( + "F.3.15", KEY_192, &CIPHERTEXTS_192, &OUTPUT_BLOCKS_192, + ); + check_output_blocks::( + "F.3.17", KEY_256, &CIPHERTEXTS_256, &OUTPUT_BLOCKS_256, + ); +} + +/// CFB128 and OFB must agree on the **first** block and on nothing after it. +/// +/// Both modes set `I1 = IV` and `O1 = CIPH_K(IV)`, and both then XOR that into the plaintext, so +/// `C1` is necessarily the same. They diverge from the second block, because OFB feeds back the +/// output block `Oj` (Sec 6.4) while CFB feeds back the ciphertext `Cj` (Sec 6.3). +/// +/// Appendix F bears this out, and the values below are quoted from **F.4.1 (OFB-AES128.Encrypt)**, +/// a different subsection from the ones this file is testing. Agreement on block 1 is therefore an +/// independent check that the F.3.13 transcription is right; disagreement on block 2 is a check +/// that [`Cfb`] is CFB and not OFB. +#[test] +fn cfb128_agrees_with_ofb_on_the_first_block_only() { + /// F.4.1 OFB-AES128.Encrypt, Block #1 Output Block. Same key and IV, so the same `O1`. + const OFB_OUTPUT_BLOCK_1: &str = "50fe67cc996d32b6da0937e99bafec60"; + /// F.4.1 OFB-AES128.Encrypt, Block #1 and Block #2 Ciphertext. + const OFB_CIPHERTEXT_1: &str = "3b3fd92eb72dad20333449f8e83cfb4a"; + const OFB_CIPHERTEXT_2: &str = "7789508d16918f03f53c52dac54ed825"; + + assert_eq!( + OUTPUT_BLOCKS_128[0], OFB_OUTPUT_BLOCK_1, + "F.3.13 and F.4.1 must tabulate the same O1 = CIPH_K(IV)" + ); + + let key = key_material::<16>(KEY_128); + let iv = block(IV); + let (mut enc, got_iv) = Cfb::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::<16>::new(iv), + ) + .unwrap(); + assert_eq!(got_iv, iv); + + let mut c1 = block(PLAINTEXTS[0]); + enc.do_encrypt_inplace(&mut c1).unwrap(); + assert_eq!(c1, block(OFB_CIPHERTEXT_1), "block 1 must match OFB, and F.3.13"); + + let mut c2 = block(PLAINTEXTS[1]); + enc.do_encrypt_inplace(&mut c2).unwrap(); + assert_eq!(c2, block(CIPHERTEXTS_128[1]), "block 2 must match F.3.13"); + assert_ne!(c2, block(OFB_CIPHERTEXT_2), "block 2 must NOT match OFB"); +} diff --git a/crypto/aes/tests/sp800_38a_ecb_tests.rs b/crypto/aes/tests/sp800_38a_ecb_tests.rs new file mode 100644 index 00000000..3f49390c --- /dev/null +++ b/crypto/aes/tests/sp800_38a_ecb_tests.rs @@ -0,0 +1,212 @@ +//! Known-answer tests from NIST SP 800-38A Appendix F.1, "ECB Example Vectors". +//! +//! These are the only NIST-published known-answer vectors for AES-192 and AES-256 that live in a +//! specification document rather than a separate vector file -- FIPS 197 Appendix B only covers +//! AES-128, and FIPS 197 (Update 1) removed the Appendix C example vectors in favour of a pointer +//! to the CSRC website. `ecb_bc-test-data.rs` covers far more cases, but only when the +//! `bc-test-data` repository is present, so these vectors are the always-available known-answer +//! floor. +//! +//! ECB applies the raw permutation to each block independently, so an ECB example vector *is* a +//! block-permutation test vector. (That is the only reason ECB appears in this crate; see the +//! crate docs on why you must not use it to encrypt anything.) +//! +//! The keys are the same three keys as FIPS 197 Appendix A.1, A.2 and A.3, so these vectors also +//! pin each key expansion against a NIST-published answer, in both directions. +//! +//! Transcribed from the published SP 800-38A PDF, sections F.1.1 through F.1.6. + +use bouncycastle_aes::AES_BLOCK_LEN; +use bouncycastle_aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_hex as hex; + +/// The four plaintext blocks shared by every F.1 subsection. +const PLAINTEXTS: [&str; 4] = [ + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", +]; + +/// F.1.1 / F.1.2 key. +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +/// F.1.1 ECB-AES128.Encrypt output blocks. +const CIPHERTEXTS_128: [&str; 4] = [ + "3ad77bb40d7a3660a89ecaf32466ef97", + "f5d3d58503b9699de785895a96fdbaaf", + "43b1cd7f598ece23881b00e3ed030688", + "7b0c785e27e8ad3f8223207104725dd4", +]; + +/// F.1.3 / F.1.4 key. +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +/// F.1.3 ECB-AES192.Encrypt output blocks. +const CIPHERTEXTS_192: [&str; 4] = [ + "bd334f1d6e45f25ff712a214571fa5cc", + "974104846d0ad3ad7734ecb3ecee4eef", + "ef7afd2270e2e60adce0ba2face6444e", + "9a4b41ba738d6c72fb16691603c18e0e", +]; + +/// F.1.5 / F.1.6 key. +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; +/// F.1.5 ECB-AES256.Encrypt output blocks. +const CIPHERTEXTS_256: [&str; 4] = [ + "f3eed1bdb5d2a03c064b5a7e3db181f8", + "591ccb10d410ed26dc5ba74a31362870", + "b6ed21b99ca6f4f9f153e7b1beafed1d", + "23304b7a39f9f3ff067d8d8f9e24ecc7", +]; + +fn block(hex_str: &str) -> [u8; AES_BLOCK_LEN] { + hex::decode(hex_str).expect("valid hex").try_into().expect("16 bytes") +} + +fn key_material(hex_str: &str) -> KeyMaterial { + let bytes = hex::decode(hex_str).expect("valid hex"); + assert_eq!(bytes.len(), N, "key length"); + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey) + .expect("a valid symmetric cipher key") +} + +// ---- F.1.1 / F.1.2 ECB-AES128 ------------------------------------------------------------- + +#[test] +fn f_1_1_ecb_aes128_encrypt() { + let aes = AES128Internal::new(&key_material::<16>(KEY_128)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_128.iter()).enumerate() { + let mut b = block(pt); + aes.encrypt_block(&mut b); + assert_eq!(b, block(ct), "F.1.1 block #{}", i + 1); + } +} + +#[test] +fn f_1_2_ecb_aes128_decrypt() { + let aes = AES128Internal::new(&key_material::<16>(KEY_128)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_128.iter()).enumerate() { + let mut b = block(ct); + aes.decrypt_block(&mut b); + assert_eq!(b, block(pt), "F.1.2 block #{}", i + 1); + } +} + +// ---- F.1.3 / F.1.4 ECB-AES192 ------------------------------------------------------------- + +#[test] +fn f_1_3_ecb_aes192_encrypt() { + let aes = AES192Internal::new(&key_material::<24>(KEY_192)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_192.iter()).enumerate() { + let mut b = block(pt); + aes.encrypt_block(&mut b); + assert_eq!(b, block(ct), "F.1.3 block #{}", i + 1); + } +} + +#[test] +fn f_1_4_ecb_aes192_decrypt() { + let aes = AES192Internal::new(&key_material::<24>(KEY_192)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_192.iter()).enumerate() { + let mut b = block(ct); + aes.decrypt_block(&mut b); + assert_eq!(b, block(pt), "F.1.4 block #{}", i + 1); + } +} + +// ---- F.1.5 / F.1.6 ECB-AES256 ------------------------------------------------------------- + +#[test] +fn f_1_5_ecb_aes256_encrypt() { + let aes = AES256Internal::new(&key_material::<32>(KEY_256)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_256.iter()).enumerate() { + let mut b = block(pt); + aes.encrypt_block(&mut b); + assert_eq!(b, block(ct), "F.1.5 block #{}", i + 1); + } +} + +#[test] +fn f_1_6_ecb_aes256_decrypt() { + let aes = AES256Internal::new(&key_material::<32>(KEY_256)).unwrap(); + for (i, (pt, ct)) in PLAINTEXTS.iter().zip(CIPHERTEXTS_256.iter()).enumerate() { + let mut b = block(ct); + aes.decrypt_block(&mut b); + assert_eq!(b, block(pt), "F.1.6 block #{}", i + 1); + } +} + +// ---- the two-block path against the same vectors ------------------------------------------- + +/// The two-block entry points must produce exactly the single-block answers. +/// +/// This is the test that pins the lane placement: a mistake in which 16-bit lane of the planes a +/// block's bits belong to shows up here and nowhere in the single-block tests, because a +/// single-block call has only one lane. +#[test] +fn two_block_path_matches_the_f_1_vectors() { + let aes = AES128Internal::new(&key_material::<16>(KEY_128)).unwrap(); + + // Blocks 1 and 2 as a pair, then 3 and 4. + for chunk in 0..2 { + let (i, j) = (chunk * 2, chunk * 2 + 1); + let mut pair = [block(PLAINTEXTS[i]), block(PLAINTEXTS[j])]; + aes.encrypt_2blocks(&mut pair); + assert_eq!(pair[0], block(CIPHERTEXTS_128[i]), "pair {chunk} slot 0"); + assert_eq!(pair[1], block(CIPHERTEXTS_128[j]), "pair {chunk} slot 1"); + + aes.decrypt_2blocks(&mut pair); + assert_eq!(pair[0], block(PLAINTEXTS[i])); + assert_eq!(pair[1], block(PLAINTEXTS[j])); + } +} + +/// Swapping the two slots must swap the two results, and nothing else. +#[test] +fn two_block_path_is_slot_symmetric() { + let aes = AES256Internal::new(&key_material::<32>(KEY_256)).unwrap(); + + let mut forward = [block(PLAINTEXTS[0]), block(PLAINTEXTS[1])]; + let mut reversed = [block(PLAINTEXTS[1]), block(PLAINTEXTS[0])]; + aes.encrypt_2blocks(&mut forward); + aes.encrypt_2blocks(&mut reversed); + + assert_eq!(forward[0], reversed[1]); + assert_eq!(forward[1], reversed[0]); + assert_eq!(forward[0], block(CIPHERTEXTS_256[0])); + assert_eq!(forward[1], block(CIPHERTEXTS_256[1])); +} + +// ---- the four-block path against the same vectors ------------------------------------------ + +/// The four-block entry points must produce exactly the single-block answers. +/// +/// F.1 has exactly four blocks, so one call covers the whole vector. As with the pair test, this +/// is what pins the four 16-bit lanes of the `u64` planes to the four slots, in order. +#[test] +fn four_block_path_matches_the_f_1_vectors() { + let aes = AES192Internal::new(&key_material::<24>(KEY_192)).unwrap(); + + let mut four = PLAINTEXTS.map(block); + aes.encrypt_4blocks(&mut four); + assert_eq!(four, CIPHERTEXTS_192.map(block)); + + aes.decrypt_4blocks(&mut four); + assert_eq!(four, PLAINTEXTS.map(block)); +} + +/// Permuting the four slots must permute the four results, and nothing else. +#[test] +fn four_block_path_is_slot_symmetric() { + let aes = AES256Internal::new(&key_material::<32>(KEY_256)).unwrap(); + + // Every cyclic rotation of the four F.1 plaintexts. + for shift in 0..4 { + let mut four: [_; 4] = core::array::from_fn(|i| block(PLAINTEXTS[(i + shift) % 4])); + aes.encrypt_4blocks(&mut four); + for i in 0..4 { + assert_eq!(four[i], block(CIPHERTEXTS_256[(i + shift) % 4]), "shift {shift}, slot {i}"); + } + } +} diff --git a/crypto/aes/tests/sp800_38c_tests.rs b/crypto/aes/tests/sp800_38c_tests.rs new file mode 100644 index 00000000..acf4e05c --- /dev/null +++ b/crypto/aes/tests/sp800_38c_tests.rs @@ -0,0 +1,1282 @@ +//! 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::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle_cipher::modes::{Ccm, CcmDecryptor, CcmEncryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +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::aead::TestFrameworkAEADCipher; +use bouncycastle_hex as hex; + +/// 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") +} + +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::hazmat::ElectronicCodeBook, +>( + name: &str, + key_hex: &str, + nonce_hex: &str, + aad: &[u8], + plaintext_hex: &str, + c_hex: &str, + // Whether to run the chunking sweep as well as the single-pass checks; `appendix_c4` says why + // it opts out. + sweep: bool, +) { + 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_out( + &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_out( + &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_out( + &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_out(&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 --- + if sweep { + // 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(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"); + } + } +} + +/// 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", + true, + ); +} + +/// 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", + true, + ); +} + +/// 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", + true, + ); +} + +/// 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", + // No chunking sweep here: with a 64 KiB AAD every extra pass through Sec 6.1 or 6.2 is a + // 4096-block CBC-MAC, and the sweep alone made this the slowest test in the crate. What it + // pins, chunking invisibility, is pinned on C.1 to C.3 above and exhaustively over the toy + // in `ccm_tests.rs`; what only C.4 can pin, the six-octet AAD length and `q = 2`, needs one + // pass. + false, + ); +} + +/// The shared framework, told the one payload length a fixed-frame pair's streaming methods +/// accept, `DATA_LEN`, so that it streams exactly that and checks that anything else is refused. +fn framework(data_len: usize) -> TestFrameworkAEADCipher { + let mut framework = TestFrameworkAEADCipher::new(); + framework.fixed_message_len = Some(data_len); + framework +} + +/// The whole [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] contract, through the shared +/// framework, for the fixed-frame [`CcmEncryptor`] / [`CcmDecryptor`] pair. +/// +/// `DATA_LEN` is 240, well above the few multiples of the tag the suite's one-shots try, so the +/// one-shots are exercised at every length up to it and the streaming methods at exactly it. +/// `AAD_LEN` is 64, above the suite's 20-byte AAD. `FINAL_LEN` is the tag. +#[test] +fn framework_streaming_contract() { + framework(240).test_encryptor_decryptor::< + 16, + 12, + 16, + 16, + 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(240).test_encryptor_decryptor::< + 24, + 12, + 16, + 16, + CcmEncryptor, + CcmDecryptor, + >(); + framework(240).test_encryptor_decryptor::< + 32, + 12, + 16, + 16, + 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(240).test_encryptor_decryptor::< + 16, + 13, + 8, + 8, + CcmEncryptor, + CcmDecryptor, + >(); + // The empty frame: a message that is nothing but its AAD and tag. + framework(0).test_encryptor_decryptor::< + 16, + 12, + 16, + 16, + CcmEncryptor, + CcmDecryptor, + >(); +} + +/// The fixed-frame pair must agree with the run-time-length [`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. C.3's payload is 24 bytes +/// and its AAD 20, so that is the frame. +#[test] +fn the_fixed_frame_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 every byte is released at once. + enc.do_update_aad(&aad[..5]).expect("aad 1"); + enc.do_update_aad(&aad[5..]).expect("aad 2"); + let mut ct = Vec::new(); + for piece in plaintext.chunks(7) { + assert_eq!(enc.do_encrypt_out_len(piece.len()), piece.len(), "nothing is held back"); + let mut buf = vec![0u8; piece.len()]; + assert_eq!(enc.do_encrypt_out(piece, &mut buf).expect("update"), piece.len()); + ct.extend_from_slice(&buf); + } + let mut flushed = [0xEEu8; 8]; + let (len, tag) = enc.do_encrypt_final_detachedtag_out(&mut flushed).expect("final"); + assert_eq!(len, 0, "the detached final flushes nothing"); + assert_eq!(flushed, [0u8; 8], "...and leaves the buffer zeroed"); + assert_eq!(&ct[..], 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"); + let mut pt = Vec::new(); + for piece in want_ct.chunks(5) { + let mut buf = vec![0u8; dec.do_decrypt_out_len(piece.len())]; + assert_eq!(dec.do_decrypt_out(piece, &mut buf).expect("update"), piece.len()); + pt.extend_from_slice(&buf); + } + let mut out = [0xEEu8; 8]; + let n = dec + .do_decrypt_final_detachedtag_out(want_tag.try_into().expect("8 bytes"), &mut out) + .expect("tag check"); + assert_eq!(n, 0, "the detached final releases nothing"); + assert_eq!(out, [0u8; 8], "...and leaves the buffer zeroed"); + assert_eq!(&pt[..], &plaintext[..], "C.3 plaintext via the trait"); + + // The inline layout through the inherited `SymmetricCipher*` methods: C.3's `C` is exactly + // `ciphertext || tag`, with the tag as the final's output, 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"); + let mut inline = vec![0u8; 24]; + enc.do_encrypt_out(&plaintext, &mut inline).expect("update"); + let (last, last_len) = enc.do_encrypt_final().expect("final"); + inline.extend_from_slice(&last[..last_len]); + assert_eq!(&inline[..], &c[..], "C.3 `C` via the inline do_encrypt_final"); + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_aad(&aad).expect("aad"); + let mut pt = Vec::new(); + for piece in c.chunks(5) { + let mut buf = vec![0u8; dec.do_decrypt_out_len(piece.len())]; + let n = dec.do_decrypt_out(piece, &mut buf).expect("update"); + pt.extend_from_slice(&buf[..n]); + } + let (_, n) = dec.do_decrypt_final().expect("tag check"); + assert_eq!(n, 0, "the inline final releases nothing: the payload already went out"); + assert_eq!(&pt[..], &plaintext[..], "C.3 plaintext via the inline do_decrypt_final"); +} + +/// More than the declared lengths is refused, and the refusal consumes nothing and writes +/// nothing: a payload past `DATA_LEN` at the update that would cross it, on both sides, and an +/// AAD past `AAD_LEN`. +#[test] +fn the_adapters_refuse_more_than_the_declared_lengths() { + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + let k = key::<16>(APPENDIX_C_KEY); + + let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + let mut out = [0xEEu8; 33]; + match enc.do_encrypt_out(&[0u8; 33], &mut out) { + Err(SymmetricCipherError::StateError(msg)) => assert!( + msg.contains("DATA_LEN"), + "the encryptor's refusal must name its bound, got: {msg}" + ), + other => panic!("expected StateError, got {other:?}"), + } + assert_eq!(out, [0u8; 33], "a refused update must leave the output buffer zeroed"); + + // 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_encrypt_out(&[0u8; 20], &mut out).expect("fits"), 20); + assert!(matches!( + enc.do_encrypt_out(&[0u8; 13], &mut out), + Err(SymmetricCipherError::StateError(_)) + )); + + let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + match enc.do_update_aad(&[0u8; 33]) { + Err(SymmetricCipherError::GenericError(msg)) => { + assert!(msg.contains("AAD_LEN"), "the refusal must name AAD_LEN, got: {msg}") + } + other => panic!("expected GenericError, got {other:?}"), + } + + // The decryptor's bound is `DATA_LEN + TAG_LEN`, the frame with an inline tag. + let nonce = [0x24u8; 12]; + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut pt = [0xEEu8; 49]; + match dec.do_decrypt_out(&[0u8; 49], &mut pt) { + Err(SymmetricCipherError::StateError(msg)) => assert!( + msg.contains("DATA_LEN + TAG_LEN"), + "the decryptor's refusal must name its bound, got: {msg}" + ), + other => panic!("expected StateError, got {other:?}"), + } + assert_eq!(pt, [0u8; 49], "a refused update must leave the output buffer zeroed"); + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + assert_eq!(dec.do_decrypt_out(&[0u8; 40], &mut pt).expect("fits"), 32); + assert!(matches!( + dec.do_decrypt_out(&[0u8; 9], &mut pt), + Err(SymmetricCipherError::StateError(_)) + )); + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + assert!(matches!(dec.do_update_aad(&[0u8; 33]), Err(SymmetricCipherError::GenericError(_)))); +} + +/// 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 aad = b"header"; + let message = b"payload"; + + let (mut enc, nonce) = Enc::do_encrypt_init(&k).expect("init"); + let mut sealed = [0u8; 7]; + enc.do_encrypt_out(&[], &mut sealed).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_encrypt_out(message, &mut sealed).expect("released"); + assert!( + matches!(enc.do_update_aad(aad), Err(SymmetricCipherError::StateError(_))), + "a non-empty update still closes the AAD phase" + ); + let (tag, tag_len) = enc.do_encrypt_final().expect("final"); + assert_eq!(tag_len, 16); + + // 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; 7 + 16]; + let n = Ccm::::encrypt_out( + &k, &nonce, aad, message, &mut expected, + ) + .expect("direct"); + assert_eq!(n, 23); + assert_eq!(&sealed[..], &expected[..7]); + assert_eq!(&tag[..], &expected[7..]); + + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut opened = [0u8; 7]; + dec.do_decrypt_out(&[], &mut opened).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_decrypt_out(&expected, &mut opened).expect("the payload and the inline tag"); + assert!( + matches!(dec.do_update_aad(aad), Err(SymmetricCipherError::StateError(_))), + "a non-empty update still closes the AAD phase" + ); + let (_, opened_len) = dec.do_decrypt_final().expect("tag check"); + assert_eq!(opened_len, 0); + assert_eq!(&opened[..], message); +} + +/// Exactly the declared lengths are accepted, in one call and split across two, on both sides: +/// the checks are `>`, so using all of `DATA_LEN` and `AAD_LEN` is legitimate and only one byte +/// more is not. The split for the decryptor straddles the payload/tag boundary, which is where +/// its own bookkeeping goes wrong. +#[test] +fn exact_lengths_are_accepted_whole_and_split() { + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + let k = key::<16>(APPENDIX_C_KEY); + let aad = [0x11u8; 32]; + let message = [0x5Au8; 32]; + let nonce_seed = [0x24u8; 12]; + + let (mut enc, nonce) = + Enc::do_encrypt_init_rng(&k, &mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); + enc.do_update_aad(&aad).expect("AAD exactly filling the capacity is accepted"); + let mut sealed = [0u8; 48]; + assert_eq!(enc.do_encrypt_out(&message, &mut sealed).expect("exactly DATA_LEN"), 32); + let (tag, _) = enc.do_encrypt_final().expect("final"); + sealed[32..].copy_from_slice(&tag); + + let (mut enc, _) = + Enc::do_encrypt_init_rng(&k, &mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); + enc.do_update_aad(&aad[..20]).expect("part"); + enc.do_update_aad(&aad[20..]).expect("exactly fills the remaining capacity"); + let mut split = [0u8; 48]; + assert_eq!(enc.do_encrypt_out(&message[..20], &mut split).expect("fits"), 20); + assert_eq!(enc.do_encrypt_out(&message[20..], &mut split[20..]).expect("the rest"), 12); + let (tag, _) = enc.do_encrypt_final().expect("final"); + split[32..].copy_from_slice(&tag); + assert_eq!(split, sealed, "the chunking must not change the answer"); + + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_aad(&aad).expect("aad"); + let mut opened = [0u8; 32]; + assert_eq!(dec.do_decrypt_out(&sealed, &mut opened).expect("the whole frame"), 32); + dec.do_decrypt_final().expect("tag check"); + assert_eq!(opened, message); + + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_aad(&aad[..20]).expect("part"); + dec.do_update_aad(&aad[20..]).expect("the rest"); + let mut opened = [0u8; 32]; + // 20 bytes of payload, then 12 of payload with 10 of tag, then the last 6 of tag. + assert_eq!(dec.do_decrypt_out_len(20), 20); + assert_eq!(dec.do_decrypt_out(&sealed[..20], &mut opened).expect("payload"), 20); + assert_eq!(dec.do_decrypt_out_len(22), 12, "only the payload part is released"); + assert_eq!(dec.do_decrypt_out(&sealed[20..42], &mut opened[20..]).expect("straddle"), 12); + assert_eq!(dec.do_decrypt_out_len(6), 0, "the rest is tag"); + assert_eq!(dec.do_decrypt_out(&sealed[42..], &mut []).expect("tag"), 0); + dec.do_decrypt_final().expect("tag check"); + assert_eq!(opened, message); +} + +/// `AAD_LEN` is a capacity and `DATA_LEN` an exact length, and each is enforced on its own: a +/// small AAD capacity next to a larger frame is the shape a packet protocol with a short header +/// wants, and the AAD may fall short of its capacity, including all the way to none. +#[test] +fn the_aad_capacity_and_the_payload_length_are_independent() { + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + let k = key::<16>(APPENDIX_C_KEY); + + // AAD past AAD_LEN is refused, although it would fit in DATA_LEN. + let (mut enc, _) = Enc::do_encrypt_init(&k).expect("init"); + assert!(matches!(enc.do_update_aad(&[0u8; 9]), Err(SymmetricCipherError::GenericError(_)))); + + // A full AAD_LEN of AAD and a full DATA_LEN of payload together, far more than AAD_LEN alone, + // round-trip through both sides; so does a frame with less AAD than the capacity, and with + // none, each agreeing with the direct API. + let message = [0x5Au8; 64]; + for aad in [&[0x11u8; 8][..], &[0x11u8; 3][..], &[][..]] { + let (mut enc, nonce) = Enc::do_encrypt_init(&k).expect("init"); + enc.do_update_aad(aad).expect("within AAD_LEN"); + let mut sealed = [0u8; 64]; + enc.do_encrypt_out(&message, &mut sealed).expect("exactly DATA_LEN"); + let (_, _, tag) = enc.do_encrypt_final_detachedtag().expect("final"); + + let mut direct = [0u8; 64]; + let (_, direct_tag) = + Ccm::::encrypt_detached_out( + &k, &nonce, aad, &message, &mut direct, + ) + .expect("direct"); + assert_eq!((sealed, tag), (direct, direct_tag), "aad of {} bytes", aad.len()); + + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_aad(aad).expect("within AAD_LEN"); + let mut opened = [0u8; 64]; + dec.do_decrypt_out(&sealed, &mut opened).expect("exactly DATA_LEN"); + dec.do_decrypt_final_detachedtag(&tag).expect("tag check"); + assert_eq!(opened, message); + } +} + +/// The decryptor knows where the payload ends, so it releases every payload byte as it arrives +/// and holds back only what follows: the inline tag, if the final says the layout is inline, and +/// excess ciphertext if it says detached. Nothing is released by either final. +#[test] +fn the_decryptor_releases_the_payload_and_holds_back_only_the_tag() { + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + let k = key::<16>(APPENDIX_C_KEY); + let message = [0x5Au8; 32]; + + let (mut enc, nonce) = Enc::do_encrypt_init(&k).expect("init"); + let mut inline = [0u8; 48]; + enc.do_encrypt_out(&message, &mut inline).expect("the frame"); + let (tag, _) = enc.do_encrypt_final().expect("final"); + inline[32..].copy_from_slice(&tag); + + // Inline: 40 bytes release the 32 of payload and hold 8 of tag; the last 8 release nothing. + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut out = [0u8; 32]; + assert_eq!(dec.do_decrypt_out_len(40), 32); + assert_eq!( + dec.do_decrypt_out(&inline[..40], &mut out).expect("payload and part of the tag"), + 32 + ); + assert_eq!(out, message, "the payload is out before the tag has been seen"); + assert_eq!(dec.do_decrypt_out(&inline[40..], &mut []).expect("the rest of the tag"), 0); + let (_, n) = dec.do_decrypt_final().expect("tag check"); + assert_eq!(n, 0, "nothing is left to release"); + + // One byte past the frame with its tag is refused, and the final still verifies. + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_decrypt_out(&inline, &mut out).expect("the whole frame"); + assert!(matches!( + dec.do_decrypt_out(&[0u8; 1], &mut []), + Err(SymmetricCipherError::StateError(_)) + )); + dec.do_decrypt_final().expect("a refused update must not disturb the state"); + + // Detached, the 16 bytes held back after the payload have nowhere to go: the frame is + // exactly DATA_LEN, so this `C` is malformed. + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_decrypt_out(&inline, &mut out).expect("the whole frame"); + let mut nothing = [0xEEu8; 16]; + assert!(matches!( + dec.do_decrypt_final_detachedtag_out(&tag, &mut nothing), + Err(SymmetricCipherError::DecryptionFailed) + )); + assert_eq!(nothing, [0u8; 16], "the detached final only zeroes its buffer"); + + // ...and exactly DATA_LEN is the detached frame. + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + let mut out = [0u8; 32]; + dec.do_decrypt_out(&inline[..32], &mut out).expect("the frame"); + assert_eq!(dec.do_decrypt_final_detachedtag_out(&tag, &mut nothing).expect("tag check"), 0); + assert_eq!(nothing, [0u8; 16], "the detached final only zeroes its buffer"); + assert_eq!(out, message); +} + +/// The trait one-shots are the trait's own, provided over the streaming methods, so `DATA_LEN` +/// and `AAD_LEN` bind them exactly as they bind the streaming calls: a frame of the declared +/// length goes through and agrees with the run-time-length [`Ccm`] byte for byte, and anything +/// else is refused with the streaming methods' own errors. +#[test] +fn trait_one_shots_are_bound_by_data_len() { + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + let k = key::<16>(APPENDIX_C_KEY); + let nonce_seed = [0x24u8; 12]; + let aad = [0x3Cu8; 48]; + let frame = [0xA5u8; 48]; + + let mut ciphertext = [0u8; 48]; + let (nonce, written, tag) = Enc::encrypt_detached_rng_out( + &k, + &mut FixedSeedRNG::<12>::new(nonce_seed), + &aad, + &frame, + &mut ciphertext, + ) + .expect("exactly DATA_LEN and AAD_LEN"); + assert_eq!(written, 48); + let mut direct = [0u8; 48]; + let (_, direct_tag) = Ccm::::encrypt_detached_out( + &k, &nonce, &aad, &frame, &mut direct, + ) + .expect("direct"); + assert_eq!((ciphertext, tag), (direct, direct_tag), "the two routes to Sec 6.1 agree"); + let mut opened = [0u8; 48]; + assert_eq!( + Dec::decrypt_detached_out(&k, &nonce, &aad, &ciphertext, &tag, &mut opened).expect("open"), + 48 + ); + assert_eq!(opened, frame); + + // One byte either side of the frame is refused, with the streaming methods' own variants. + let mut out = [0u8; 4096]; + assert!(matches!( + Enc::encrypt_detached_out(&k, &aad, &[0xA5u8; 47], &mut out), + Err(SymmetricCipherError::StateError(_)) + )); + assert!(matches!( + Enc::encrypt_detached_out(&k, &aad, &[0xA5u8; 49], &mut out), + Err(SymmetricCipherError::StateError(_)) + )); + assert!(matches!( + Enc::encrypt_detached_out(&k, &[0x3Cu8; 49], &frame, &mut out), + Err(SymmetricCipherError::GenericError(_)) + )); + assert!(matches!( + Dec::decrypt_detached_out(&k, &nonce, &aad, &ciphertext[..47], &tag, &mut out), + Err(SymmetricCipherError::DecryptionFailed) + )); + let mut long = [0u8; 49]; + long[..48].copy_from_slice(&ciphertext); + assert!(matches!( + Dec::decrypt_detached_out(&k, &nonce, &aad, &long, &tag, &mut out), + Err(SymmetricCipherError::DecryptionFailed) + )); +} + +/// `B0` commits to `DATA_LEN`, so every final must check that exactly that much payload was +/// supplied before it computes or checks a tag -- a tag over a shorter message would be one no +/// verifier could reproduce, and on the decrypting side a `C` of the wrong length is malformed. +/// Every one of the eight final entry points is asserted on its own, the provided forwarders +/// included, so that a forwarder that dropped the check could not hide behind the one it wraps. +/// +/// The encryptor refuses with [`SymmetricCipherError::StateError`], the caller's own sequencing +/// mistake; the decryptor with [`SymmetricCipherError::DecryptionFailed`], since the input is a +/// ciphertext, and a wrong-length one is malformed. Each refusal is paired with the same call +/// succeeding on the right amount, so a matcher that passed for the wrong reason would show. +#[test] +fn every_final_refuses_a_payload_of_the_wrong_length() { + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + let k = key::<16>(APPENDIX_C_KEY); + let aad = b"header"; + let frame = [0x5Au8; 8]; + let nonce_seed = [0x24u8; 12]; + + // An encryptor fed `supplied` of the 8 declared bytes. + let enc = |supplied: usize| { + let (mut enc, _) = + Enc::do_encrypt_init_rng(&k, &mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); + enc.do_update_aad(aad).expect("aad"); + let mut out = [0u8; 8]; + enc.do_encrypt_out(&frame[..supplied], &mut out).expect("update"); + enc + }; + for short in [0usize, 4, 7] { + assert!( + matches!(enc(short).do_encrypt_final(), Err(SymmetricCipherError::StateError(_))), + "do_encrypt_final after {short} of 8 bytes" + ); + let mut buf = [0u8; 16]; + assert!( + matches!( + enc(short).do_encrypt_final_out(&mut buf), + Err(SymmetricCipherError::StateError(_)) + ), + "do_encrypt_final_out after {short} of 8 bytes" + ); + assert!( + matches!( + enc(short).do_encrypt_final_detachedtag(), + Err(SymmetricCipherError::StateError(_)) + ), + "do_encrypt_final_detachedtag after {short} of 8 bytes" + ); + assert!( + matches!( + enc(short).do_encrypt_final_detachedtag_out(&mut buf), + Err(SymmetricCipherError::StateError(_)) + ), + "do_encrypt_final_detachedtag_out after {short} of 8 bytes" + ); + } + // The positive controls, which also produce the ciphertext for the decrypting side. + let (tag, n) = enc(8).do_encrypt_final().expect("do_encrypt_final on a whole frame"); + assert_eq!(n, 16); + let mut buf = [0u8; 16]; + assert_eq!( + enc(8).do_encrypt_final_out(&mut buf).expect("do_encrypt_final_out on a whole frame"), + 16 + ); + assert_eq!(buf, tag); + let (_, n, tag2) = enc(8) + .do_encrypt_final_detachedtag() + .expect("do_encrypt_final_detachedtag on a whole frame"); + assert_eq!((n, tag2), (0, tag)); + let (n, tag3) = enc(8) + .do_encrypt_final_detachedtag_out(&mut buf) + .expect("do_encrypt_final_detachedtag_out on a whole frame"); + assert_eq!((n, tag3), (0, tag)); + let mut ct = [0u8; 8]; + { + let (mut e, _) = + Enc::do_encrypt_init_rng(&k, &mut FixedSeedRNG::<12>::new(nonce_seed)).expect("init"); + e.do_update_aad(aad).expect("aad"); + e.do_encrypt_out(&frame, &mut ct).expect("update"); + e.do_encrypt_final().expect("final"); + } + let mut inline = [0u8; 24]; + inline[..8].copy_from_slice(&ct); + inline[8..].copy_from_slice(&tag); + + // A decryptor fed the first `supplied` bytes of `inline`. + let dec = |supplied: usize| { + let mut dec = Dec::do_decrypt_init(&k, &nonce_seed).expect("init"); + dec.do_update_aad(aad).expect("aad"); + let mut out = [0u8; 8]; + dec.do_decrypt_out(&inline[..supplied], &mut out).expect("update"); + dec + }; + // Inline: a short payload, and a whole payload with a short tag, are both a short `C`. + for short in [0usize, 4, 7, 8, 12, 23] { + assert!( + matches!(dec(short).do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed)), + "do_decrypt_final after {short} of 24 bytes" + ); + let mut buf = [0u8; 16]; + assert!( + matches!( + dec(short).do_decrypt_final_out(&mut buf), + Err(SymmetricCipherError::DecryptionFailed) + ), + "do_decrypt_final_out after {short} of 24 bytes" + ); + } + assert_eq!(dec(24).do_decrypt_final().expect("do_decrypt_final on a whole frame").1, 0); + assert_eq!( + dec(24).do_decrypt_final_out(&mut buf).expect("do_decrypt_final_out on a whole frame"), + 0 + ); + // Detached: a short payload, and bytes past it that this layout has no place for. + for wrong in [0usize, 4, 7, 9, 24] { + assert!( + matches!( + dec(wrong).do_decrypt_final_detachedtag(&tag), + Err(SymmetricCipherError::DecryptionFailed) + ), + "do_decrypt_final_detachedtag after {wrong} of 8 bytes" + ); + let mut buf = [0u8; 16]; + assert!( + matches!( + dec(wrong).do_decrypt_final_detachedtag_out(&tag, &mut buf), + Err(SymmetricCipherError::DecryptionFailed) + ), + "do_decrypt_final_detachedtag_out after {wrong} of 8 bytes" + ); + } + assert_eq!( + dec(8) + .do_decrypt_final_detachedtag(&tag) + .expect("do_decrypt_final_detachedtag on a whole frame") + .1, + 0 + ); + assert_eq!( + dec(8) + .do_decrypt_final_detachedtag_out(&tag, &mut buf) + .expect("do_decrypt_final_detachedtag_out on a whole frame"), + 0 + ); + // ...and a whole frame with the wrong tag is the tag check failing, not a length refusal. + let mut forged = tag; + forged[0] ^= 0xFF; + assert!(matches!( + dec(8).do_decrypt_final_detachedtag(&forged), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); +} + +/// 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 fixed-frame decryptor's +/// `do_decrypt_final` and its `decrypt_with_aad_out` -- must report the same malformed input with +/// the same variant, [`SymmetricCipherError::DecryptionFailed`], which is what +/// [`SymmetricCipherDecryptor::do_decrypt_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; + // The empty frame, so that every byte of a short `C` is a (missing) tag byte. + 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_out(&k, &nonce, &[], &short, &mut out), + Err(SymmetricCipherError::DecryptionFailed) + ), + "a {len}-byte C cannot carry a 16-byte tag (Ccm::decrypt_out)" + ); + assert!( + matches!( + StreamDec::decrypt_with_aad_out(&k, &nonce, &[], &short, &mut out), + Err(SymmetricCipherError::DecryptionFailed) + ), + "a {len}-byte C cannot carry a 16-byte tag (decrypt_with_aad_out)" + ); + let mut dec = StreamDec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_decrypt_out(&short, &mut nothing).expect("held back as a possible tag"); + assert!( + matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed)), + "a {len}-byte C cannot carry a 16-byte tag (do_decrypt_final)" + ); + } + + // Exactly TAG_LEN: an empty payload plus its tag, which must verify, on all three. + let mut inline = [0u8; 16]; + let n = Enc::encrypt_out(&k, &nonce, &[], &[], &mut inline).expect("encryption"); + assert_eq!(n, 16); + assert_eq!(Dec::decrypt_out(&k, &nonce, &[], &inline, &mut out).expect("decryption"), 0); + assert_eq!( + StreamDec::decrypt_with_aad_out(&k, &nonce, &[], &inline, &mut out).expect("decryption"), + 0 + ); + let mut dec = StreamDec::do_decrypt_init(&k, &nonce).expect("init"); + assert_eq!(dec.do_decrypt_out(&inline, &mut nothing).expect("the tag"), 0); + assert_eq!(dec.do_decrypt_final().expect("an empty frame still verifies").1, 0); +} + +/// The same agreement on a frame that is not empty, where the inline entry points can disagree +/// in a way the empty frame hides. `do_decrypt_out` releases the payload as it arrives, up to +/// `DATA_LEN`, and only then holds bytes back as the tag; so a `C` of fewer than +/// `DATA_LEN + TAG_LEN` bytes still asks for a `DATA_LEN`-byte buffer when it is longer than the +/// frame. `decrypt_out_len` is therefore `DATA_LEN` for any such `C`, not `C` less a tag: a +/// one-shot that sizes its buffer by it reaches the final, which reports the short `C` as +/// malformed, rather than refusing the buffer with `OutputBufferTooSmall` first. +#[test] +fn a_short_inline_ciphertext_is_rejected_the_same_way_for_a_non_empty_frame() { + const DATA_LEN: usize = 32; + type Enc = CcmEncryptor; + type Dec = CcmDecryptor; + let k = key::<16>(APPENDIX_C_KEY); + let frame = [0x5Au8; DATA_LEN]; + let mut sealed = vec![0u8; Enc::encrypt_out_len(DATA_LEN)]; + let (nonce, n) = Enc::encrypt_with_aad_out(&k, b"hdr", &frame, &mut sealed).expect("seal"); + assert_eq!(n, DATA_LEN + 16); + + // The whole frame plus its tag is the one accepted inline length, and the bound is exact. + assert_eq!(Dec::decrypt_out_len(DATA_LEN + 16), DATA_LEN); + assert_eq!(Dec::decrypt_out_len(DATA_LEN + 1), DATA_LEN, "the payload is DATA_LEN"); + assert_eq!(Dec::decrypt_out_len(5), 5, "...or all of a C shorter than the frame"); + + for len in 0..DATA_LEN + 16 { + let short = &sealed[..len]; + let mut pt = vec![0u8; Dec::decrypt_out_len(len)]; + assert!( + matches!( + Dec::decrypt_with_aad_out(&k, &nonce, b"hdr", short, &mut pt), + Err(SymmetricCipherError::DecryptionFailed) + ), + "a {len}-byte C is not a frame and its tag (decrypt_with_aad_out)" + ); + let mut pt = vec![0u8; Dec::decrypt_out_len(len)]; + assert!( + matches!( + Dec::decrypt_out(&k, &nonce, short, &mut pt), + Err(SymmetricCipherError::DecryptionFailed) + ), + "a {len}-byte C is not a frame and its tag (decrypt_out)" + ); + let mut dec = Dec::do_decrypt_init(&k, &nonce).expect("init"); + dec.do_update_aad(b"hdr").expect("aad"); + let mut pt = vec![0u8; dec.do_decrypt_out_len(len)]; + dec.do_decrypt_out(short, &mut pt).expect("the payload is released, the rest held"); + assert!( + matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed)), + "a {len}-byte C is not a frame and its tag (do_decrypt_final)" + ); + } + + // ...and the accepted length, through the same three, so the loop's bound is not off by one. + let mut pt = vec![0u8; Dec::decrypt_out_len(sealed.len())]; + assert_eq!(Dec::decrypt_with_aad_out(&k, &nonce, b"hdr", &sealed, &mut pt).expect("open"), 32); + assert_eq!(&pt[..], &frame[..]); +} + +/// 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_out(&k, &nonce, &[], &plaintext, &mut too_small)), + Some(24) + ); + + let mut too_small = [0u8; 39]; + assert_eq!( + buffer_len_error(Enc::encrypt_out(&k, &nonce, &[], &plaintext, &mut too_small)), + Some(40) + ); + + let mut ct = [0u8; 40]; + Enc::encrypt_out(&k, &nonce, &[], &plaintext, &mut ct).expect("encryption"); + let mut too_small = [0u8; 23]; + assert_eq!(buffer_len_error(Dec::decrypt_out(&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_out(&wrong, &[0u8; 12], &[], &[], &mut out), + Err(SymmetricCipherError::KeyMaterialError(_)) + )); + assert!(matches!( + Enc::new(&wrong, &[0u8; 12], &[], 0), + Err(SymmetricCipherError::KeyMaterialError(_)) + )); + assert!(matches!( + Dec::decrypt_out(&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(&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 sizes: `Ccm` is 264/296/328 B for AES-128/192/256, independent of +/// `NONCE_LEN`/`TAG_LEN`, and the fixed-frame pair is `Ccm` plus the `AAD_LEN` buffer and a few +/// words of bookkeeping, independent of `DATA_LEN`. +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::size_of; + + assert_eq!(size_of::>(), 264); + assert_eq!(size_of::>(), 296); + assert_eq!(size_of::>(), 328); + + // 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 fixed-frame pair holds no payload: a 4 KiB frame costs exactly what a 16-byte one does. + let enc_64 = size_of::>(); + assert_eq!(enc_64, size_of::>()); + assert_eq!(enc_64, 264 + 64 + 16, "Ccm, the AAD buffer, its length and the phase flag"); + assert_eq!( + size_of::>() - enc_64, + 4096 - 64, + "the value grows by exactly the AAD capacity" + ); + // The decryptor adds the tag it holds back and that tag's length. + let dec_64 = size_of::>(); + assert_eq!(dec_64, size_of::>()); + assert_eq!(dec_64, enc_64 + 16 + 8); +} + +// ---- moved from crypto/cipher/src/modes/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" + ); +} + +// ---- progressive AAD: new_with_lengths + do_update_aad ------------------------------------ + +/// Runs one Appendix C example through [`Ccm::new_with_lengths`], feeding the AAD in `chunk`-byte +/// pieces, in both directions, and checks the result against the example's `C`. +fn check_progressive_aad( + label: &str, + nonce: &str, + aad: &[u8], + plaintext: &str, + c: &str, + chunk: usize, +) { + let k = key::<16>(APPENDIX_C_KEY); + let nonce: [u8; NONCE_LEN] = hex::decode(nonce).unwrap().try_into().unwrap(); + let plaintext = hex::decode(plaintext).unwrap(); + let c = hex::decode(c).unwrap(); + let (want_ct, want_tag) = c.split_at(plaintext.len()); + + let mut enc = Ccm::::new_with_lengths( + &k, + &nonce, + aad.len(), + plaintext.len(), + ) + .unwrap(); + for piece in aad.chunks(chunk) { + enc.do_update_aad(piece).unwrap(); + } + let mut data = plaintext.clone(); + enc.do_encrypt(&mut data).unwrap(); + let tag = enc.do_encrypt_final().unwrap(); + assert_eq!(data, want_ct, "{label}: ciphertext, AAD in {chunk}-byte pieces"); + assert_eq!(&tag[..], want_tag, "{label}: tag, AAD in {chunk}-byte pieces"); + + let mut dec = Ccm::::new_with_lengths( + &k, + &nonce, + aad.len(), + plaintext.len(), + ) + .unwrap(); + for piece in aad.chunks(chunk) { + dec.do_update_aad(piece).unwrap(); + } + dec.do_decrypt_update(&mut data).unwrap(); + dec.do_decrypt_final(&tag).unwrap(); + assert_eq!(data, plaintext, "{label}: decryption, AAD in {chunk}-byte pieces"); +} + +/// Supplying the AAD in pieces gives Appendix C's answers, whatever the chunking: C.3's 20-byte +/// AAD, which needs padding, and C.4's 65536-byte one, which takes A.2.2's six-octet length +/// encoding. The chunk sizes straddle the 16-byte block, so pieces end part-way through a block. +#[test] +fn progressive_aad_matches_appendix_c() { + for chunk in [1, 3, 15, 16, 17, 20] { + check_progressive_aad::<12, 8>( + "C.3", + "101112131415161718191a1b", + &hex::decode("000102030405060708090a0b0c0d0e0f10111213").unwrap(), + "202122232425262728292a2b2c2d2e2f3031323334353637", + "e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5484392fbc1b09951", + chunk, + ); + } + let mut aad = Vec::with_capacity(65536); + for _ in 0..256 { + aad.extend(0u8..=255u8); + } + for chunk in [1, 7, 256, 1000, 65536] { + check_progressive_aad::<13, 14>( + "C.4", + "101112131415161718191a1b1c", + &aad, + "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + "69915dad1e84c6376a68c2967e4dab615ae0fd1faec44cc484828529463ccf72\ + b4ac6bec93e8598e7f0dadbcea5b", + chunk, + ); + } +} + +/// The declared AAD length is encoded in front of the AAD, so, as for the payload, any other +/// amount is refused -- more at the update, less at the final -- and the payload may not start +/// until the AAD is complete. A refused call consumes nothing. +#[test] +fn progressive_aad_enforces_the_declared_length_and_order() { + type Enc = Ccm; + type Dec = Ccm; + let k = key::<16>(APPENDIX_C_KEY); + let nonce = [0x24u8; 12]; + let aad = b"0123456789"; + let message = *b"payload"; + + let reference = { + let mut ccm = Enc::new(&k, &nonce, aad, message.len()).unwrap(); + let mut data = message; + ccm.do_encrypt(&mut data).unwrap(); + (data, ccm.do_encrypt_final().unwrap()) + }; + + // More than declared: refused, and the refused call absorbs nothing. + let mut ccm = Enc::new_with_lengths(&k, &nonce, aad.len(), message.len()).unwrap(); + ccm.do_update_aad(&aad[..4]).unwrap(); + assert!(matches!(ccm.do_update_aad(&[0u8; 7]), Err(SymmetricCipherError::StateError(_)))); + + // Payload before the AAD is complete: refused, and the data is left untouched. + let mut data = message; + assert!(matches!(ccm.do_encrypt(&mut data), Err(SymmetricCipherError::StateError(_)))); + assert_eq!(data, message, "a refused update must not touch the data"); + // An empty payload update is a no-op, not a refusal. + ccm.do_encrypt(&mut []).expect("an empty update is a no-op"); + + // Completing the AAD after both refusals gives the same answer as supplying it whole. + ccm.do_update_aad(&aad[4..]).unwrap(); + ccm.do_encrypt(&mut data).unwrap(); + assert_eq!((data, ccm.do_encrypt_final().unwrap()), reference); + + // Less than declared: refused at the final, in both directions. + let mut ccm = Enc::new_with_lengths(&k, &nonce, aad.len(), 0).unwrap(); + ccm.do_update_aad(&aad[..9]).unwrap(); + assert!(matches!(ccm.do_encrypt_final(), Err(SymmetricCipherError::StateError(_)))); + let mut dec = Dec::new_with_lengths(&k, &nonce, aad.len(), 0).unwrap(); + dec.do_update_aad(&aad[..9]).unwrap(); + assert!(matches!(dec.do_decrypt_final(&[0u8; 16]), Err(SymmetricCipherError::StateError(_)))); + + // The decryptor refuses payload before the AAD is complete too. + let mut dec = Dec::new_with_lengths(&k, &nonce, aad.len(), message.len()).unwrap(); + let mut ct = reference.0; + assert!(matches!(dec.do_decrypt_update(&mut ct), Err(SymmetricCipherError::StateError(_)))); + assert_eq!(ct, reference.0, "a refused update must not touch the data"); + dec.do_update_aad(aad).unwrap(); + dec.do_decrypt_update(&mut ct).unwrap(); + dec.do_decrypt_final(&reference.1).expect("tag check"); + assert_eq!(ct, message); + + // `new` declares exactly the AAD it is given, so any more afterwards is refused. + let mut ccm = Enc::new(&k, &nonce, aad, message.len()).unwrap(); + assert!(matches!(ccm.do_update_aad(b"x"), Err(SymmetricCipherError::StateError(_)))); + ccm.do_update_aad(&[]).expect("an empty AAD update is always a no-op"); + + // A declared AAD length of zero is the no-AAD flow: the payload may start at once. + let mut ccm = Enc::new_with_lengths(&k, &nonce, 0, message.len()).unwrap(); + let mut data = message; + ccm.do_encrypt(&mut data).unwrap(); + let no_aad = ccm.do_encrypt_final().unwrap(); + let mut ccm = Enc::new(&k, &nonce, &[], message.len()).unwrap(); + let mut data2 = message; + ccm.do_encrypt(&mut data2).unwrap(); + assert_eq!((data, no_aad), (data2, ccm.do_encrypt_final().unwrap())); +} diff --git a/crypto/aes/tests/suspend_tests.rs b/crypto/aes/tests/suspend_tests.rs new file mode 100644 index 00000000..2befe6d9 --- /dev/null +++ b/crypto/aes/tests/suspend_tests.rs @@ -0,0 +1,99 @@ +//! Suspend-and-resume round trips through the AES aliases. +//! +//! The impls live on the generic modes in `bouncycastle-cipher`, where they are tested over a +//! toy permutation; what is checked here is that each alias reaches them with the right key +//! type. The engine itself has no state and nothing to suspend: a resumed mode rebuilds it from +//! the re-supplied key. The test does part of an operation, suspends a clone, resumes it, and +//! finishes both the same way. + +use bouncycastle_aes::hazmat::AES_ECB_128; +use bouncycastle_aes::{ + AES_CBC_128, AES_CCM_128, AES_CFB_128, AES_CFB8_128, AES_CTR_128, AES_GCM_128, +}; +use bouncycastle_cipher::Encrypting; +use bouncycastle_cipher::padding::PKCS7; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + AEADCipherEncryptor, StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; + +fn key() -> KeyMaterial<16> { + KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap() +} + +fn round_trip(cipher: C, finish: impl Fn(C) -> Vec) -> Vec +where + C: SuspendableKeyed> + Clone, +{ + let key = key(); + TestFrameworkSuspendableKeyedState::new().test(&cipher, &key); + let resumed = C::from_suspended(cipher.clone().suspend(), &key).unwrap(); + let original_output = finish(cipher); + assert_eq!(original_output, finish(resumed), "the resumed cipher must continue identically"); + original_output +} + +#[test] +fn every_alias_family_is_suspendable() { + type CbcEnc = AES_CBC_128; + let (mut cbc, _) = CbcEnc::do_encrypt_init(&key()).unwrap(); + cbc.do_encrypt_out(&[0x11u8; 20], &mut [0u8; 16]).unwrap(); + round_trip::<{ CbcEnc::SUSPENDED_STATE_LEN }, _>(cbc, |mut e| { + let mut out = [0u8; 16]; + e.do_encrypt_out(&[0x22u8; 12], &mut out).unwrap(); + let (last, n) = e.do_encrypt_final().unwrap(); + [out.as_slice(), &last[..n]].concat() + }); + + type EcbEnc = AES_ECB_128; + let (mut ecb, _) = EcbEnc::do_encrypt_init(&key()).unwrap(); + ecb.do_encrypt_out(&[0x11u8; 20], &mut [0u8; 16]).unwrap(); + round_trip::<{ EcbEnc::SUSPENDED_STATE_LEN }, _>(ecb, |e| { + let (last, n) = e.do_encrypt_final().unwrap(); + last[..n].to_vec() + }); + + let (mut cfb, _) = AES_CFB_128::::do_encrypt_init(&key()).unwrap(); + cfb.do_encrypt_inplace(&mut [0x11u8; 7]).unwrap(); + round_trip::<{ AES_CFB_128::::SUSPENDED_STATE_LEN }, _>(cfb, |mut e| { + let mut data = [0x22u8; 25]; + e.do_encrypt_inplace(&mut data).unwrap(); + data.to_vec() + }); + + let (mut cfb8, _) = AES_CFB8_128::::do_encrypt_init(&key()).unwrap(); + cfb8.do_encrypt_inplace(&mut [0x11u8; 7]).unwrap(); + round_trip::<{ AES_CFB8_128::::SUSPENDED_STATE_LEN }, _>(cfb8, |mut e| { + let mut data = [0x22u8; 9]; + e.do_encrypt_inplace(&mut data).unwrap(); + data.to_vec() + }); + + let (mut ctr, _) = AES_CTR_128::::do_encrypt_init(&key()).unwrap(); + ctr.do_encrypt_inplace(&mut [0x11u8; 7]).unwrap(); + round_trip::<{ AES_CTR_128::::SUSPENDED_STATE_LEN }, _>(ctr, |mut e| { + let mut data = [0x22u8; 25]; + e.do_encrypt_inplace(&mut data).unwrap(); + data.to_vec() + }); + + let (mut gcm, _) = AES_GCM_128::::do_encrypt_init(&key()).unwrap(); + gcm.do_update_aad(b"header").unwrap(); + gcm.do_encrypt_out(&[0x11u8; 7], &mut [0u8; 7]).unwrap(); + round_trip::<{ AES_GCM_128::::SUSPENDED_STATE_LEN }, _>(gcm, |mut e| { + let mut out = [0u8; 25]; + e.do_encrypt_out(&[0x22u8; 25], &mut out).unwrap(); + let (tag, n) = e.do_encrypt_final().unwrap(); + [out.as_slice(), &tag[..n]].concat() + }); + + type CcmEnc = AES_CCM_128; + let mut ccm = CcmEnc::new(&key(), &[0x24u8; 12], b"header", 32).unwrap(); + ccm.do_encrypt(&mut [0x11u8; 7]).unwrap(); + round_trip::<{ CcmEnc::SUSPENDED_STATE_LEN }, _>(ccm, |mut e| { + let mut data = [0x22u8; 25]; + e.do_encrypt(&mut data).unwrap(); + [data.as_slice(), &e.do_encrypt_final().unwrap()].concat() + }); +} diff --git a/crypto/ascon/Cargo.toml b/crypto/ascon/Cargo.toml new file mode 100644 index 00000000..00b7aa76 --- /dev/null +++ b/crypto/ascon/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bouncycastle-ascon" +version.workspace = true +edition.workspace = true + +[features] +default = ["std"] +std = ["bouncycastle-core/std"] + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-cipher.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..e5cd65b2 --- /dev/null +++ b/crypto/ascon/src/ascon_aead128.rs @@ -0,0 +1,846 @@ +//! 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_cipher::Direction; +use bouncycastle_core::errors::{KeyMaterialError, SuspendableError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, SuspendableKeyed, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; + +use crate::ASCON_AEAD128_NAME; +use crate::permutation::{AsconState, load_u64_le, p8, p12, store_u64_le}; + +/*** Imports needed for docs ***/ +#[allow(unused_imports)] +use bouncycastle_cipher::{Decrypting, Encrypting}; + +/// 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_NAME; + 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_encrypt_final`] writes only the tag, and the detached +/// [`AEADCipherEncryptor::do_encrypt_final_detachedtag_out`] only zeroes its buffer. +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 do_encrypt_out_len(&self, input_len: usize) -> usize { + input_len + } + + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + 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_encrypt_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 zeroed. + fn do_encrypt_final_detachedtag_out( + self, + ciphertext: &mut [u8; TAG_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext.fill(0); + 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_decrypt_final`]) or ciphertext with the tag carried separately +/// ([`AEADCipherDecryptor::do_decrypt_final_detachedtag_out`]). 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 do_decrypt_out_len(&self, input_len: usize) -> usize { + (self.held_len + input_len).saturating_sub(TAG_LEN) + } + + fn do_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + let release = self.do_decrypt_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_decrypt_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_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_decrypt_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_decrypt_final_detachedtag_out( + mut self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; TAG_LEN], + ) -> Result { + plaintext.fill(0); + 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) + } +} + +/// 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. +/// +/// A plain alias cannot choose between two types, so this is a projection through the sealed +/// [`Direction`] trait, which only [`Encrypting`] and [`Decrypting`] implement. +/// +/// 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_cipher::{Decrypting, Encrypting}; +/// use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +/// +/// 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_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

= + ::Select; + +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..2abac895 --- /dev/null +++ b/crypto/ascon/src/ascon_cxof128.rs @@ -0,0 +1,383 @@ +//! 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::traits::{Algorithm, Hash, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; + +use crate::ASCON_CXOF128_NAME; +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_NAME; + 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_output_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_output_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..e8583c4f --- /dev/null +++ b/crypto/ascon/src/ascon_hash256.rs @@ -0,0 +1,178 @@ +//! 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::traits::{Algorithm, Hash, HashAlgParams, Suspendable}; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; + +use crate::ASCON_HASH256_NAME; +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, + ]), + } + } + + // 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_NAME; + 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..806a4c19 --- /dev/null +++ b/crypto/ascon/src/ascon_xof128.rs @@ -0,0 +1,333 @@ +//! 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::traits::{Algorithm, Hash, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; + +use crate::ASCON_XOF128_NAME; +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_NAME; + 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_output_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_output_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..d6d6e075 --- /dev/null +++ b/crypto/ascon/src/lib.rs @@ -0,0 +1,184 @@ +//! 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::new().hash(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_decrypt_final_detachedtag_out` 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_encrypt_out(plaintext, &mut ciphertext).unwrap(); +//! let mut final_buf = [0u8; 16]; +//! let (_, tag) = enc.do_encrypt_final_detachedtag_out(&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_decrypt_out(&ciphertext, &mut recovered).unwrap(); // 0: all 16 held back +//! let m = dec.do_decrypt_final_detachedtag_out(&tag, &mut final_buf).unwrap(); // 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_with_aad_out`] / +//! [`bouncycastle_core::traits::AEADCipherDecryptor::decrypt_with_aad_out`] 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_with_aad_out(&key, b"aad", plaintext, &mut inline).unwrap(); +//! let n = AsconAead128Decryptor::decrypt_with_aad_out(&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_detached_out`], +//! [`bouncycastle_core::traits::AEADCipherDecryptor::decrypt_with_aad_out`] 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_decrypt_final_detachedtag_out`] or +//! [`bouncycastle_core::traits::SymmetricCipherDecryptor::do_decrypt_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..4499834d --- /dev/null +++ b/crypto/ascon/tests/aead128_tests.rs @@ -0,0 +1,703 @@ +//! 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 `ascon_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::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core_test_framework::aead::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_cipher::{Decrypting, Encrypting}; + TestFrameworkAEADCipher::new().test_encryptor_decryptor::< + 16, + 16, + 16, + 16, + Ascon_AEAD128, + Ascon_AEAD128, + >(); +} + +/// The two tag layouts must agree byte for byte: `direct_ciphertext || direct_tag`, produced by +/// streaming [`AsconAead128Encryptor`] and taking the tag from `do_encrypt_final_detachedtag_out`, +/// must equal what the inline layout produces for the same key, nonce (driven by the same RNG +/// stream), AAD and message -- through both `encrypt_with_aad_out` and the inherited +/// `do_encrypt_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_encrypt_out(&pt, &mut direct_ct).unwrap(); + let mut unused = [0u8; 16]; + let (flushed, direct_tag) = + direct_enc.do_encrypt_final_detachedtag_out(&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_encrypt_out(&pt, &mut tagged_out).unwrap(); + let mut last = [0u8; 16]; + let last_len = tagged_enc.do_encrypt_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_with_aad_out(&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_len(one_len)]; + let one_n = AsconAead128Decryptor::decrypt_with_aad_out( + &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_decrypt_final_detachedtag_out` 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_decrypt_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_decrypt_final_detachedtag_out(&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_decrypt_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_decrypt_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_len(tagged_out.len())]; + let n = AsconAead128Decryptor::decrypt_with_aad_out( + &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_rng_out( + &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/ascon_bc-test-data.rs b/crypto/ascon/tests/ascon_bc-test-data.rs new file mode 100644 index 00000000..bd0474f8 --- /dev/null +++ b/crypto/ascon/tests/ascon_bc-test-data.rs @@ -0,0 +1,233 @@ +//! NIST SP 800-232 known-answer test (KAT) vectors for Ascon-AEAD128, Ascon-Hash256, Ascon-XOF128 +//! and Ascon-CXOF128. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data", under `crypto/ascon//`. If it is not +//! present the tests print a warning and pass vacuously. +//! +//! These full sweeps (1025–1089 cases each) complement the small embedded vector sets in the +//! per-primitive test files. + +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::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Hash, XOF}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use std::collections::BTreeMap; + +const TEST_DATA_DIR: &str = "crypto/ascon"; + +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 Some(contents) = bc_test_data(TEST_DATA_DIR, "asconaead128/LWC_AEAD_KAT_128_128.txt") + else { + 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 Some(contents) = bc_test_data(TEST_DATA_DIR, "asconhash256/LWC_HASH_KAT_256.txt") else { + 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::new().hash(&msg), + expected, + "Hash256 mismatch (Count {})", + field(case, &["Count"]) + ); + } + + println!("Ascon-Hash256: {} KAT cases passed", cases.len()); +} + +#[test] +fn ascon_xof128_kat() { + let Some(contents) = bc_test_data(TEST_DATA_DIR, "asconxof128/LWC_XOF_KAT_128_512.txt") else { + 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 Some(contents) = bc_test_data(TEST_DATA_DIR, "asconcxof128/LWC_CXOF_KAT_128_512.txt") + else { + 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/ascon_wycheproof.rs b/crypto/ascon/tests/ascon_wycheproof.rs new file mode 100644 index 00000000..ebdd9993 --- /dev/null +++ b/crypto/ascon/tests/ascon_wycheproof.rs @@ -0,0 +1,84 @@ +//! Known-answer tests against Project Wycheproof's +//! `testvectors_v1/ascon_sp800_232_aead128_test.json` (Ascon-AEAD128, NIST SP 800-232). +//! +//! Requires the Wycheproof repository (https://github.com/C2SP/wycheproof) to be cloned alongside +//! this repository, i.e. at `../wycheproof` 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 vector +//! suites in this crate. +//! +//! A `valid` case must encrypt to exactly `ct || tag` and decrypt back to `msg`. An `invalid` case +//! must be rejected with `AEADTagCheckFailed`, leaving the output buffer zeroized. + +use bouncycastle_ascon::ascon_aead128::{AsconAead128, KEY_LEN, NONCE_LEN, TAG_LEN}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; + +/// Wraps the vector's key bytes as a cipher key, promoting them if `KeyMaterial`'s entropy +/// heuristic declined to (as `ascon_bc-test-data.rs` does for the NIST KAT keys). +fn cipher_key(bytes: &[u8]) -> KeyMaterial { + let mut key = KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey) + .expect("a 16-byte key"); + do_hazardous_operations(&mut key, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::_128bit) + }) + .expect("promoting a wycheproof test key"); + key +} + +#[test] +fn wycheproof_ascon_aead128() { + let Some(doc) = wycheproof_json("ascon_sp800_232_aead128_test.json") else { return }; + + assert_eq!(doc.get("algorithm").and_then(Value::as_str), Some("ASCON-AEAD128")); + + let (mut valid_count, mut invalid_count) = (0usize, 0usize); + for group in doc.get("testGroups").and_then(Value::as_array).expect("testGroups") { + for (field, len) in [("keySize", KEY_LEN), ("ivSize", NONCE_LEN), ("tagSize", TAG_LEN)] { + let bits = group.get(field).and_then(Value::as_u64).expect(field); + assert_eq!(bits as usize, 8 * len, "every group uses Ascon-AEAD128's fixed {field}"); + } + + 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 key = cipher_key(&hex_field(test, "key", tc_id)); + let nonce: [u8; NONCE_LEN] = + hex_field(test, "iv", tc_id).try_into().expect("a 16-byte nonce"); + let aad = hex_field(test, "aad", tc_id); + let ad = if aad.is_empty() { None } else { Some(aad.as_slice()) }; + let msg = hex_field(test, "msg", tc_id); + let ct_and_tag = [hex_field(test, "ct", tc_id), hex_field(test, "tag", tc_id)].concat(); + + let mut pt = vec![0xEEu8; ct_and_tag.len().saturating_sub(TAG_LEN)]; + let decrypted = AsconAead128::decrypt(&key, &nonce, ad, &ct_and_tag, &mut pt); + + match test.get("result").and_then(Value::as_str).expect("result") { + "valid" => { + let mut out = vec![0u8; msg.len() + TAG_LEN]; + let n = AsconAead128::encrypt(&key, &nonce, ad, &msg, &mut out) + .unwrap_or_else(|e| panic!("tcId {tc_id}: encrypt failed: {e:?}")); + assert_eq!(&out[..n], &ct_and_tag[..], "tcId {tc_id}: ct || tag"); + + let n = decrypted.unwrap_or_else(|e| panic!("tcId {tc_id}: decrypt: {e:?}")); + assert_eq!(&pt[..n], &msg[..], "tcId {tc_id}: decrypted plaintext"); + valid_count += 1; + } + "invalid" => { + assert!( + matches!(decrypted, Err(SymmetricCipherError::AEADTagCheckFailed)), + "tcId {tc_id}: expected AEADTagCheckFailed, got {decrypted:?}" + ); + assert!(pt.iter().all(|&b| b == 0), "tcId {tc_id}: output not zeroized"); + invalid_count += 1; + } + other => panic!("tcId {tc_id}: unexpected result {other}"), + } + } + } + + println!("Wycheproof Ascon-AEAD128: {valid_count} valid and {invalid_count} invalid cases run"); + assert!(valid_count > 0 && invalid_count > 0, "expected both valid and invalid cases"); +} diff --git a/crypto/ascon/tests/cxof128_tests.rs b/crypto/ascon/tests/cxof128_tests.rs new file mode 100644 index 00000000..ecfbc2fd --- /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 `ascon_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..7f51802d --- /dev/null +++ b/crypto/ascon/tests/hash256_tests.rs @@ -0,0 +1,153 @@ +//! Ascon-Hash256 tests (NIST SP 800-232 §5.1). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `ascon_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::new().hash(&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 mut expected = [0u8; 32]; + assert_eq!(AsconHash256::new().hash_out(&msg, &mut expected), 32); + + // One-shot APIs agree. + assert_eq!(AsconHash256::new().hash(&msg), expected); + 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::new().hash(&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::new().hash(&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::new().hash(&data); + + // 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..9d5f6bfd --- /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 `ascon_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/base64/src/lib.rs b/crypto/base64/src/lib.rs index 4dc3a858..a6a4350f 100644 --- a/crypto/base64/src/lib.rs +++ b/crypto/base64/src/lib.rs @@ -285,11 +285,16 @@ impl Base64Decoder { } } if self.buf[self.vals_in_buf] == 0x81 { - // Error: we found padding. + // Padding. In `do_update` that is a contract violation: restore the state from + // the start of the call and report it, discarding whatever this call had already + // decoded, so that the caller can hand the *same* input to `do_final` and get all + // of it back. Returning `Ok` with the partial output here would hand the caller + // bytes the restored state is about to produce again. In `do_final` the padding + // simply ends the data, and the partial block is finished by the caller. if rollback_if_padding { - // Roll back and return Base64Error::NonFinalBlockContainsPadding. - self.buf = starting_state.clone(); + self.buf = starting_state; self.vals_in_buf = starting_vals_in_block; + return Err(Base64Error::PaddingEncounteredDuringDoUpdate); } return Ok(out); } diff --git a/crypto/base64/tests/base64_tests.rs b/crypto/base64/tests/base64_tests.rs index eba90363..aa700dd6 100644 --- a/crypto/base64/tests/base64_tests.rs +++ b/crypto/base64/tests/base64_tests.rs @@ -89,3 +89,36 @@ mod ctbase64_test { assert_eq!(LOREM_IPSUM, out); } } + +/// `do_update` must refuse a chunk containing padding as its docs say: the state is restored to +/// what it was at entry and nothing is returned, so passing the same chunk to `do_final` yields +/// every byte exactly once. It used to return `Ok` with the bytes decoded before the padding while +/// rolling the block state back, so a block held from the previous call was emitted twice and the +/// padded block itself was lost. +#[test] +fn do_update_refuses_padding_and_leaves_the_state_restorable() { + use bouncycastle_base64::{Base64Decoder, Base64Error}; + + // The padded block completes a quartet begun in the previous call. + let mut decoder = Base64Decoder::new(true); + assert_eq!(decoder.do_update("QUJ").unwrap(), b""); + assert!(matches!( + decoder.do_update("DRA=="), + Err(Base64Error::PaddingEncounteredDuringDoUpdate) + )); + assert_eq!(decoder.do_final("DRA==").unwrap(), b"ABCD"); + + // The padded block arrives whole, with complete blocks before it in the same chunk. + let mut decoder = Base64Decoder::new(true); + assert!(matches!( + decoder.do_update("QUJDRA=="), + Err(Base64Error::PaddingEncounteredDuringDoUpdate) + )); + assert_eq!(decoder.do_final("QUJDRA==").unwrap(), b"ABCD"); + + // Padding alone, after everything else went through `do_update`. + let mut decoder = Base64Decoder::new(true); + assert_eq!(decoder.do_update("QUJDRA").unwrap(), b"ABC"); + assert!(matches!(decoder.do_update("=="), Err(Base64Error::PaddingEncounteredDuringDoUpdate))); + assert_eq!(decoder.do_final("==").unwrap(), b"D"); +} diff --git a/crypto/cipher/Cargo.toml b/crypto/cipher/Cargo.toml new file mode 100644 index 00000000..3cab85c4 --- /dev/null +++ b/crypto/cipher/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "bouncycastle-cipher" +edition.workspace = true +rust-version.workspace = true +version.workspace = true + +[dependencies] +bouncycastle-core.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 + +[[test]] +name = "nopadding_tests" +path = "tests/padding/nopadding_tests.rs" + +[[test]] +name = "padded_tests" +path = "tests/padding/padded_tests.rs" + +[[test]] +name = "pkcs7_tests" +path = "tests/padding/pkcs7_tests.rs" + +[[test]] +name = "cbc_tests" +path = "tests/modes/cbc_tests.rs" + +[[test]] +name = "ccm_tests" +path = "tests/modes/ccm_tests.rs" + +[[test]] +name = "cfb8_tests" +path = "tests/modes/cfb8_tests.rs" + +[[test]] +name = "cfb_tests" +path = "tests/modes/cfb_tests.rs" + +[[test]] +name = "ctr_tests" +path = "tests/modes/ctr_tests.rs" + +[[test]] +name = "ecb_tests" +path = "tests/modes/ecb_tests.rs" + +[[test]] +name = "gcm_tests" +path = "tests/modes/gcm_tests.rs" + +[[test]] +name = "symmetric_cipher_api_tests" +path = "tests/modes/symmetric_cipher_api_tests.rs" + +[[bench]] +name = "padding_benches" +path = "benches/padding/padding_benches.rs" +harness = false diff --git a/crypto/cipher/benches/padding/padding_benches.rs b/crypto/cipher/benches/padding/padding_benches.rs new file mode 100644 index 00000000..931dc679 --- /dev/null +++ b/crypto/cipher/benches/padding/padding_benches.rs @@ -0,0 +1,27 @@ +use bouncycastle_cipher::padding::PKCS7; +use bouncycastle_core::traits::BlockCipherPadding; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +fn bench_pkcs7(c: &mut Criterion) { + let mut group = c.benchmark_group("padding::PKCS7"); + group.bench_function("pad/16", |b| { + let mut block = [0u8; 16]; + b.iter(|| { + >::pad(black_box(&mut block), black_box(5)).unwrap(); + black_box(&block); + }) + }); + group.bench_function("unpad/16", |b| { + let mut block = [0u8; 16]; + >::pad(&mut block, 5).unwrap(); + b.iter(|| { + let n = >::unpad(black_box(&block)).unwrap(); + black_box(n); + }) + }); + group.finish(); +} + +criterion_group!(benches, bench_pkcs7); +criterion_main!(benches); diff --git a/crypto/cipher/src/lib.rs b/crypto/cipher/src/lib.rs new file mode 100644 index 00000000..73ce8bc8 --- /dev/null +++ b/crypto/cipher/src/lib.rs @@ -0,0 +1,114 @@ +//! A utility crate for holding common building blocks for constructing symmetric ciphers on top of +//! different permutation functions, such as modes of operation and padding. +//! +//! * [`modes`] — block cipher modes of operation (NIST SP 800-38A, SP 800-38C and SP 800-38D). +//! * [`padding`] — block padding schemes, and the adapters that apply them to a block cipher mode. +//! * [`stream`] — a stream cipher over any keystream, and the helpers shared by stream ciphers that +//! cannot be built that way. +//! +//! # Usage Examples +//! +//! See the [`modes`], [`padding`] and [`stream`] module docs. +//! +//! # Suspending and resuming execution +//! +//! Every mode and adapter implements `SuspendableKeyed`, so a message in progress can be suspended +//! to a byte array and resumed later with the re-supplied key. The length of that array is the +//! type's `SUSPENDED_STATE_LEN`, and a wrong length is a compile error; the mechanism is +//! [`bouncycastle_utils::suspendable_state`]. A suspended state holds everything the message in +//! progress depends on except the key -- a chaining block, live keystream, a running MAC -- so +//! protect it as the plaintext it governs, and never resume one state twice. +//! +//! ``` +//! use bouncycastle_cipher::modes::Cbc; +//! use bouncycastle_cipher::Encrypting; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherEncryptor, SuspendableKeyed}; +//! use bouncycastle_core_test_framework::ToyBlockCipher; +//! +//! type ToyCbc = Cbc; +//! const STATE_LEN: usize = ToyCbc::SUSPENDED_STATE_LEN; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let (mut enc, _iv) = ToyCbc::do_encrypt_init(&key).unwrap(); +//! let mut first = [0x11u8; 16]; +//! enc.do_encrypt_inplace(&mut first).unwrap(); +//! +//! // Suspending consumes the cipher. The key is not in the state and is re-supplied to resume. +//! let state: [u8; STATE_LEN] = enc.suspend(); +//! let mut enc = ToyCbc::from_suspended(state, &key).unwrap(); +//! let mut second = [0x22u8; 16]; +//! enc.do_encrypt_inplace(&mut second).unwrap(); +//! ``` +//! +//! # Memory Usage +//! +//! See the "Memory Usage" section of each module. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! See the "Security Considerations" section of each module. + +#![forbid(unsafe_code)] +#![forbid(missing_docs)] +#![no_std] + +pub mod modes; +pub mod padding; +pub mod stream; + +/// Direction marker for a cipher value that encrypts. +/// +/// Zero-sized: encoding the direction in the type costs no memory. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Encrypting; + +/// Direction marker for a cipher value that decrypts. +/// +/// Zero-sized: encoding the direction in the type costs no memory. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Decrypting; + +mod sealed { + /// Private supertrait of [`Direction`](super::Direction): only this module can name it, so + /// only the two markers below can implement `Direction`. + pub trait Sealed {} + impl Sealed for super::Encrypting {} + impl Sealed for super::Decrypting {} +} + +/// Selects a type by direction: `Enc` for [`Encrypting`], `Dec` for [`Decrypting`]. +/// +/// A cipher whose two directions are distinct types cannot offer `Cipher` as a plain type +/// alias, because an alias cannot choose between two types from one of its parameters. It is +/// written as a projection through this trait instead: +/// +/// ```text +/// pub type Ascon_AEAD128 = +/// ::Select; +/// ``` +/// +/// Sealed: implemented for the two markers and for nothing else, so `Encrypting` and `Decrypting` +/// are the only values a `Dir` parameter can take, and a caller cannot project an alias onto a +/// type of their own: +/// +/// ```compile_fail +/// use bouncycastle_cipher::Direction; +/// struct Sideways; +/// // error: the supertrait is private to bouncycastle_cipher +/// impl Direction for Sideways { +/// type Select = Enc; +/// } +/// ``` +pub trait Direction: sealed::Sealed { + /// `Enc` for [`Encrypting`], `Dec` for [`Decrypting`]. + type Select; +} + +impl Direction for Encrypting { + type Select = Enc; +} + +impl Direction for Decrypting { + type Select = Dec; +} diff --git a/crypto/cipher/src/modes/cbc.rs b/crypto/cipher/src/modes/cbc.rs new file mode 100644 index 00000000..785dac96 --- /dev/null +++ b/crypto/cipher/src/modes/cbc.rs @@ -0,0 +1,349 @@ +//! The Cipher Block Chaining mode of operation (NIST SP 800-38A Sec 6.2). +//! +//! # Parallel decryption +//! +//! Sec 6.2 notes that in CBC decryption "the input blocks for the inverse cipher function, i.e., +//! the ciphertext blocks, are immediately available, so that multiple inverse cipher operations can +//! be performed in parallel", whereas in encryption "the input block to each forward cipher +//! operation (except the first) depends on the result of the previous forward cipher operation, so +//! the forward cipher operations cannot be performed in parallel". +//! +//! This implementation uses that: decryption walks the ciphertext four blocks at a time through +//! [`ElectronicCodeBook::decrypt_4blocks`], then any remaining pair through +//! [`ElectronicCodeBook::decrypt_2blocks`], then the last block singly. A bit-sliced engine +//! computes two or four blocks (AES, on `u32` or `u64` planes) for barely more than the cost of +//! one. Encryption cannot, and does not. +//! +//! # Usage Examples +//! +//! The direction is part of the type: [`Cbc`](Cbc) implements +//! [`BlockCipherEncryptor`] and nothing else, and [`Cbc`](Cbc) implements +//! [`BlockCipherDecryptor`] and nothing else. +//! The IV is generated and returned; there is no API for supplying one. +//! +//! ``` +//! use bouncycastle_core_test_framework::ToyBlockCipher; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +//! use bouncycastle_cipher::modes::Cbc; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! type ToyCbc = Cbc; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // 48 bytes: three whole blocks. A length that is not a multiple of 16 would not compile. +//! let plaintext: [u8; 48] = *b"The quick brown fox jumps over the lazy dog. OK!"; +//! +//! // One shot, in place: encrypts under a freshly generated IV, which is returned. +//! let mut data = plaintext; +//! let (_, iv) = ToyCbc::::encrypt_inplace(&key, &mut data).expect("encryption"); +//! assert_ne!(data, plaintext); +//! +//! ToyCbc::::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); +//! assert_eq!(data, plaintext); +//! ``` +//! +//! Streaming, for data that arrives in pieces. A sequence of calls is equivalent to one call over +//! the concatenation: +//! +//! ``` +//! use bouncycastle_core_test_framework::ToyBlockCipher; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +//! use bouncycastle_cipher::modes::Cbc; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! type ToyCbc = Cbc; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! let (mut encryptor, iv) = +//! ToyCbc::::do_encrypt_init(&key).expect("encrypt init"); +//! let mut first = [0xAAu8; 16]; +//! let mut rest = [0xBBu8; 32]; +//! encryptor.do_encrypt_inplace(&mut first).expect("block 1"); +//! encryptor.do_encrypt_inplace(&mut rest).expect("blocks 2-3"); +//! +//! let mut decryptor = ToyCbc::::do_decrypt_init(&key, &iv).expect("decrypt init"); +//! decryptor.do_decrypt_inplace(&mut first).unwrap(); +//! decryptor.do_decrypt_inplace(&mut rest).unwrap(); +//! assert_eq!(first, [0xAAu8; 16]); +//! assert_eq!(rest, [0xBBu8; 32]); +//! ``` +//! +//! # Suspending and resuming execution +//! +//! [`Cbc`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte +//! array and resumed later with the re-supplied key. The state is the chaining block; the +//! permutation is rebuilt from the key. The array length is `Cbc::SUSPENDED_STATE_LEN`; see [the +//! crate docs](crate#suspending-and-resuming-execution) for an example. +//! +//! # 🚨 Security Considerations 🚨 +//! ## IV integrity +//! +//! NIST SP 800-38A Appendix D: +//! +//! > "for the CBC mode, the decryption of the first ciphertext block is vulnerable to the +//! > (deliberate) introduction of bit errors in specific bit positions of the IV if the integrity of +//! > the IV is not protected". +//! +//! Under CBC a flipped IV bit flips exactly that bit of the first decrypted plaintext block. +//! +//! So, while the IV need not be secret, best-practice is to authenticate it along with the ciphertext, +//! or use an authenticated (AEAD) mode such as GCM. + +use crate::modes::iv::random_iv; +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, RNG, SuspendableKeyed, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::suspendable_state::{ + LIB_VERSION_LEN, SuspendableComponent, resume_component, suspend_component, +}; +use core::marker::PhantomData; + +/// CBC mode over any [`ElectronicCodeBook`], with the direction encoded in the type. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`]. [`BlockCipherEncryptor`] is implemented only for the +/// former and [`BlockCipherDecryptor`] only for the latter, so a `Cbc<_, Encrypting, _, _>` has no +/// decryption methods at all -- using one in the wrong direction is a compile error rather than a +/// runtime check. +/// +/// The initialization data is one block, so `INIT_DATA_LEN == BLOCK_LEN`. +/// +/// # State +/// +/// Two fields: the permutation (which owns the key schedule, and is responsible for keeping it in +/// a zeroize-on-drop wrapper) and one block of chaining value. The chaining value is an IV or a +/// ciphertext block, both of which are public, so it is deliberately not wrapped in a `Secret`. +#[derive(Clone)] +pub struct Cbc +where + P: ElectronicCodeBook, +{ + perm: P, + /// `Cj-1`, initialised to the IV. See the module docs on why there is only one field for both. + chain: [u8; BLOCK_LEN], + _dir: PhantomData, +} + +impl Cbc +where + P: ElectronicCodeBook, +{ + /// The `N` of this type's [`SuspendableKeyed`] impl: the version header and the chaining + /// block. See [`bouncycastle_utils::suspendable_state`]. + pub const SUSPENDED_STATE_LEN: usize = LIB_VERSION_LEN + BLOCK_LEN; + + /// `Cj = CIPH_K(Pj XOR Cj-1)` in place, then `Cj` becomes the next chaining value. + #[inline] + fn encrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { + for (b, chain) in block.iter_mut().zip(self.chain.iter()) { + *b ^= *chain; // Pj XOR Cj-1 + } + self.perm.encrypt_block(block); // Cj = CIPH_K(..) + self.chain = *block; + } + + /// `Pj = CIPH^-1_K(Cj) XOR Cj-1` in place, then `Cj` becomes the next chaining value. + /// + /// `Cj` is overwritten by `Pj`, so it is copied first: it is the next chaining value. + #[inline] + fn decrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { + let cj = *block; + self.perm.decrypt_block(block); // CIPH^-1_K(Cj) + for (b, chain) in block.iter_mut().zip(self.chain.iter()) { + *b ^= *chain; // XOR Cj-1 + } + self.chain = cj; + } + + /// Decrypts two consecutive blocks with one [`ElectronicCodeBook::decrypt_2blocks`] call. + /// + /// Writing the pair as `Cj, Cj+1` with `Cj-1` the incoming chaining value, Sec 6.2 gives + /// + /// ```text + /// Pj = CIPH^-1_K(Cj) XOR Cj-1 + /// Pj+1 = CIPH^-1_K(Cj+1) XOR Cj + /// ``` + /// + /// Neither inverse cipher depends on the other's *output* -- only on ciphertext, which is + /// already in hand -- so computing them together changes nothing. The two XOR operands do + /// differ, and the second one is `Cj`, so both ciphertext blocks are copied out before the + /// permutation overwrites them, and the chaining value is then advanced to `Cj+1`. + #[inline] + fn decrypt_pair(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { + let [cj, cj1] = *blocks; + self.perm.decrypt_2blocks(blocks); + + let [pj, pj1] = blocks; + for (b, chain) in pj.iter_mut().zip(self.chain.iter()) { + *b ^= *chain; // XOR Cj-1 + } + for (b, prev) in pj1.iter_mut().zip(cj.iter()) { + *b ^= *prev; // XOR Cj + } + + self.chain = cj1; + } + + /// Decrypts four consecutive blocks with one [`ElectronicCodeBook::decrypt_4blocks`] call. + /// + /// The same argument as [`Self::decrypt_pair`], four wide: `Pj+k = CIPH^-1_K(Cj+k) XOR Cj+k-1` + /// for `k = 0..4`, with `Cj-1` the incoming chaining value. No inverse cipher depends on + /// another's output, so all four run together; the ciphertexts are copied out first because + /// the permutation overwrites them and each is the next block's XOR operand, and the chaining + /// value advances to `Cj+3`. + #[inline] + fn decrypt_four(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 4]) { + let cts = *blocks; + self.perm.decrypt_4blocks(blocks); + + let mut prev = self.chain; + for (pj, cj) in blocks.iter_mut().zip(cts.iter()) { + for (b, chain) in pj.iter_mut().zip(prev.iter()) { + *b ^= *chain; // XOR Cj+k-1 + } + prev = *cj; + } + self.chain = prev; + } +} + +impl Algorithm + for Cbc +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; +} + +impl + BlockCipherEncryptor for Cbc +where + P: ElectronicCodeBook, +{ + /// Begins an encryption flow, generating the IV from the library's default OS-backed DRBG. + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + Self::do_encrypt_init_rng(key, &mut rng) + } + + /// As [`BlockCipherEncryptor::do_encrypt_init`], but takes the IV from the provided RNG. + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { + let perm = P::new(key)?; + let iv = random_iv::(rng)?; + Ok((Self { perm, chain: iv, _dir: PhantomData }, iv)) + } + + /// The implementor hook (the flat `do_encrypt_inplace` is provided over it). + /// + /// Strictly serial: `Cj` is the input to block `j + 1`, so there is no pair path here. See the + /// module docs. Never fails: CBC has no per-IV data limit. + fn do_encrypt_blocks_inplace( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result { + for block in blocks.iter_mut() { + self.encrypt_one(block); + } + Ok(blocks.len() * BLOCK_LEN) + } +} + +impl + BlockCipherDecryptor for Cbc +where + P: ElectronicCodeBook, +{ + /// Begins a decryption flow from the IV returned by + /// [`BlockCipherEncryptor::do_encrypt_init`]. + fn do_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; BLOCK_LEN], + ) -> Result { + let perm = P::new(key)?; + Ok(Self { perm, chain: *init_data, _dir: PhantomData }) + } + + /// The implementor hook (the flat `do_decrypt_inplace` is provided over it). + /// + /// Walks the input in fours through `decrypt_4blocks`, then pairs through `decrypt_2blocks`, + /// then the at-most-one block left over: Sec 6.2's parallelism, in the units the permutation + /// offers. `as_chunks_mut` splits into exactly those shapes with no runtime length check and no + /// indexing arithmetic. Never fails: CBC has no per-IV data limit. + fn do_decrypt_blocks_inplace( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result { + let len = blocks.len() * BLOCK_LEN; + let (fours, rest) = blocks.as_chunks_mut::<4>(); + for four in fours.iter_mut() { + self.decrypt_four(four); + } + let (pairs, tail) = rest.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.decrypt_pair(pair); + } + for block in tail.iter_mut() { + self.decrypt_one(block); + } + Ok(len) + } +} + +/// The suspended state is the chaining block `Cj-1`, in both directions; the permutation is +/// rebuilt from the re-supplied key. See [`bouncycastle_utils::suspendable_state`]. +impl SuspendableComponent + for Cbc +where + P: ElectronicCodeBook, +{ + const STATE_LEN: usize = BLOCK_LEN; + type Key = KeyMaterial; + + fn write_state(&self, out: &mut [u8]) { + out.copy_from_slice(&self.chain); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + let mut chain = [0u8; BLOCK_LEN]; + chain.copy_from_slice(state); + Ok(Self { perm, chain, _dir: PhantomData }) + } +} + +/// `N` must be [`Cbc::SUSPENDED_STATE_LEN`]; anything else is a compile error. +impl SuspendableKeyed + for Cbc +where + P: ElectronicCodeBook, +{ + type Key = KeyMaterial; + + fn suspend(self) -> [u8; N] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + resume_component(&state, key) + } +} diff --git a/crypto/cipher/src/modes/ccm.rs b/crypto/cipher/src/modes/ccm.rs new file mode 100644 index 00000000..6688ce97 --- /dev/null +++ b/crypto/cipher/src/modes/ccm.rs @@ -0,0 +1,1984 @@ +//! The CCM mode of operation: Counter with Cipher Block Chaining-Message Authentication Code +//! (NIST SP 800-38C, May 2004, errata update 07-20-2007). +//! Sec 6.1, the generation-encryption process, and Sec 6.2, the decryption-verification process. +//! +//! CCM is an *authenticated* mode: it produces a tag as well as a ciphertext, and decryption either +//! returns the plaintext or a [`SymmetricCipherError::AEADTagCheckFailed`]. +//! It is built from two mechanisms under a single key: (Sec 5.2): +//! "The same key, K, is used for both the CTR and CBC-MAC mechanisms within CCM". +//! +//! Only the forward cipher function is ever used, in both directions, so a +//! permutation that implements nothing but `encrypt_block` works here. +//! +//! # Usage Examples +//! 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 with [`SymmetricCipherError::AEADTagCheckFailed`] +//! -- it never returns plausible-looking rubbish the way the unauthenticated modes do when the +//! ciphertext has been altered. +//! +//! ``` +//! use bouncycastle_core_test_framework::ToyBlockCipher; +//! use bouncycastle_core::errors::SymmetricCipherError; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_cipher::modes::Ccm; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! type ToyCcm = Ccm; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // Supplied, not generated +//! // It is the caller's responsibility that it 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 ct_and_tag = vec![0u8; message.len() + 16]; +//! ToyCcm::::encrypt_out(&key, &nonce, header, message, &mut ct_and_tag).expect("encryption"); +//! +//! let mut recovered_plaintext = vec![0u8; message.len()]; +//! let n = ToyCcm::::decrypt_out(&key, &nonce, header, &ct_and_tag, &mut recovered_plaintext).expect("decryption"); +//! assert_eq!(&recovered_plaintext[..n], message); +//! +//! // If we tamper with any byte of the ciphertext, then this fails with a SymmetricCipherError::AEADTagCheckFailed +//! let mut tampered = ct_and_tag.clone(); +//! tampered[0] ^= 1; +//! assert_eq!(ToyCcm::::decrypt_out(&key, &nonce, header, &tampered, &mut recovered_plaintext).unwrap_err(), +//! SymmetricCipherError::AEADTagCheckFailed); +//! +//! // Same if we provide the correct ciphertext and tag, but change the authenticated data +//! assert_eq!(ToyCcm::::decrypt_out(&key, &nonce, b"other header", &ct_and_tag, &mut recovered_plaintext).unwrap_err(), +//! SymmetricCipherError::AEADTagCheckFailed); +//! ``` +//! +//! # CCM is not a stream cipher +//! +//! It does not have an indefinite-length streaming mode. +//! 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. +//! +//! 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. +//! +//! This is different from the `do_encrypt()` mode, often referred to as a "streaming mode" where +//! the content is processed in batches; so long as the total expected length is known up-front. +//! The AAD can be processed in batches the same way, if its length is declared up-front too; see +//! [`Ccm::new_with_lengths`]. +//! +//! # Suspending and resuming execution +//! +//! [`Ccm`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte +//! array and resumed later with the re-supplied key. The state is the CTR half, the CBC-MAC +//! chaining value and the AAD and payload still owed; the permutation is rebuilt from the key. The +//! array length is `Ccm::SUSPENDED_STATE_LEN`; see [the crate +//! docs](crate#suspending-and-resuming-execution) for an example. +//! +//! # 🚨 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, only unique, so +//! a counter is a valid and often better choice; every deterministic entry point here takes the +//! nonce from the caller, and should be drawn 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 bytes 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`. +//! +//! **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". + +use crate::modes::ctr::apply_counter_blocks; +use crate::modes::iv::random_iv; +use crate::stream::StreamCipher; +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{ElectronicCodeBook, KeyStream}; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, StreamCipherDecryptor, + StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, + suspend_component, +}; +use core::marker::PhantomData; + +use crate::{Decrypting, Encrypting}; + +/// CCM (SP 800-38C) over any [`ElectronicCodeBook`] with a 128-bit block. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`], `Ccm` has Sec 6.1's methods and +/// nothing else, and `Ccm` has Sec 6.2's. +/// +/// Asking an encryptor to verify a tag does not compile -- `do_decrypt_final` exists only on +/// `Ccm`: +/// +/// A nonce length A.1 does not permit does not compile: +/// +/// ```compile_fail +/// use bouncycastle_core_test_framework::ToyBlockCipher; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_cipher::modes::Ccm; +/// use bouncycastle_cipher::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_core_test_framework::ToyBlockCipher; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_cipher::modes::Ccm; +/// use bouncycastle_cipher::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); +/// ``` +#[derive(Clone)] +pub struct Ccm< + P, + Dir, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, +> where + P: ElectronicCodeBook, +{ + // The CTR half (Sec 6.1 steps 5-8): the payload keystream `S1 || S2 || ...`, and the key + // schedule, which the CBC-MAC half below shares (Sec 5.2). + ctr: StreamCipher< + CcmKeyStream, + Dir, + KEY_LEN, + NONCE_LEN, + BLOCK_LEN, + >, + // 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 the keystream 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, + // How much of the AAD length declared at construction has not yet been supplied. The length is + // encoded in front of the AAD (A.2.2), so, as for the payload below, a different amount is + // refused. While it is non-zero the AAD phase is open and no payload is accepted: A.2.3 puts + // the payload blocks after the AAD blocks. + aad_owed: usize, + // How much of the payload length declared at construction 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 = CcmKeyStream::::Q_LEN; + + /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the CTR state, + /// the CBC-MAC chaining block and three counts as `u64`s. See [`bouncycastle_utils::suspendable_state`]. + pub const SUSPENDED_STATE_LEN: usize = + LIB_VERSION_LEN + ::STATE_LEN; + + /// The CTR half's share of the suspended state. + const CTR_STATE_LEN: usize = , + Dir, + KEY_LEN, + NONCE_LEN, + BLOCK_LEN, + > as SuspendableComponent>::STATE_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 `DATA_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. + /// + /// `payload_len` is declared here because Appendix A.2.1 puts the payload length inside `B0`, + /// the first block the CBC-MAC absorbs. For an AAD that is not all in hand at once, see + /// [`Self::new_with_lengths`], which declares its length instead and takes it in pieces. + /// + /// * `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_with_lengths`, + // 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)?; + let mut ccm = Self::from_perm_with_lengths(perm, nonce, aad.len(), payload_len)?; + // Exactly the length just declared, so nothing is left owed. + ccm.absorb_aad(aad); + Ok(ccm) + } + + /// As [`Self::new`], but with the AAD's length declared rather than the AAD itself, so that the + /// AAD can then be supplied in pieces through [`Self::do_update_aad`]. + /// + /// Both lengths are needed up front, and only the lengths. `B0` carries the payload length + /// (A.2.1), and A.2.2 formats the AAD as "the encoding of a [...] concatenated with the + /// associated data A", so the CBC-MAC cannot absorb the first AAD byte until it has absorbed + /// `B0` and the encoding of `a`. The AAD bytes themselves then go through the CBC-MAC as they + /// arrive, in any chunking. + /// + /// The flow is: this constructor, exactly `aad_len` bytes of AAD through + /// [`Self::do_update_aad`], then exactly `payload_len` bytes of payload, then the final. The + /// AAD must be complete before any payload: A.2.3 puts the payload blocks after the AAD blocks. + /// + /// ``` + /// use bouncycastle_core_test_framework::ToyBlockCipher; + /// use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; + /// use bouncycastle_cipher::modes::Ccm; + /// use bouncycastle_cipher::Encrypting; + /// + /// type ToyCcm = Ccm; + /// + /// let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) + /// .expect("a 16-byte symmetric cipher key"); + /// let nonce = [0x01u8; 12]; + /// let header: [&[u8]; 2] = [b"version: 1; ", b"route: a->b"]; + /// let mut message = *b"attack at dawn"; + /// + /// let aad_len = header.iter().map(|part| part.len()).sum(); + /// let mut ccm = + /// ToyCcm::::new_with_lengths(&key, &nonce, aad_len, message.len()).unwrap(); + /// for part in header { + /// ccm.do_update_aad(part).unwrap(); + /// } + /// ccm.do_encrypt(&mut message).unwrap(); + /// let tag = ccm.do_encrypt_final().unwrap(); + /// + /// // The same as supplying the AAD whole. + /// let mut whole = *b"attack at dawn"; + /// let mut ccm = ToyCcm::::new(&key, &nonce, b"version: 1; route: a->b", 14).unwrap(); + /// ccm.do_encrypt(&mut whole).unwrap(); + /// assert_eq!((message, tag), (whole, ccm.do_encrypt_final().unwrap())); + /// ``` + /// + /// # Errors + /// As [`Self::new`]. + pub fn new_with_lengths( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad_len: usize, + payload_len: usize, + ) -> Result { + let perm = P::new(key)?; + Self::from_perm_with_lengths(perm, nonce, aad_len, payload_len) + } + + /// Supplies the next piece of the AAD declared to [`Self::new_with_lengths`]. A sequence of + /// calls is equivalent to one call over the concatenation. An empty `aad` is a no-op. + /// + /// When the last declared byte arrives, the AAD is zero-padded to a block boundary (A.2.2's + /// "minimum number of '0' bits"), which is what opens the payload phase. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if `aad` would take the total past the declared AAD + /// length -- which includes any non-empty AAD once the declared amount is complete, and so any + /// after [`Self::new`], which declares exactly the AAD it is given. Nothing is absorbed in that + /// case. + pub fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + if aad.len() > self.aad_owed { + return Err(SymmetricCipherError::StateError( + "CCM was given more AAD than the declared AAD length, which the AAD length \ + encoding commits to", + )); + } + self.absorb_aad(aad); + Ok(()) + } + + /// [`Self::do_update_aad`] without its check: `aad` must be at most the AAD still owed, which + /// the caller has established. An empty `aad` is a no-op. + fn absorb_aad(&mut self, aad: &[u8]) { + if aad.is_empty() { + return; + } + self.mac_absorb(aad); + self.aad_owed -= aad.len(); + if self.aad_owed == 0 { + // 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. + self.mac_pad(); + } + } + + /// As [`Self::new_with_lengths`], from a key schedule that has already been expanded: formats + /// and absorbs `B0` and, if there is any AAD, the encoding of its length, leaving the AAD + /// itself to [`Self::do_update_aad`]. + fn from_perm_with_lengths( + perm: P, + nonce: &[u8; NONCE_LEN], + aad_len: usize, + 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)", + )); + } + let mut ccm = Self::from_perm_unformatted(perm, nonce, payload_len); + ccm.format_header(aad_len); + Ok(ccm) + } + + /// The state *before* Sec 6.1 step 1: the keystream is positioned at `S1` and `payload_len` + /// is owed, but nothing has been absorbed into the CBC-MAC, not even `B0`. + /// + /// Crate-private, for [`CcmEncryptor`] / [`CcmDecryptor`]: the trait constructor they + /// implement is handed a key and no AAD, and `B0`'s Adata bit (A.2.2: "'0' if a=0 and '1' if + /// a>0") cannot be set until the AAD is complete. They call [`Self::format_header`] when it + /// is, and nothing else may touch the MAC before that. `payload_len` must already be at most + /// [`Self::MAX_PAYLOAD_LEN`]; the adapters assert theirs at compile time. + fn from_perm_unformatted(perm: P, nonce: &[u8; NONCE_LEN], payload_len: usize) -> Self { + Self::check_shape(); + Self { + ctr: StreamCipher::from_keystream(CcmKeyStream::from_perm(perm, nonce)), + // 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, + aad_owed: 0, + owed: payload_len, + _dir: PhantomData, + } + } + + /// Sec 6.1 step 1's formatting of `N`, `a` and `Plen`: absorbs `B0` (A.2.1) and, if `a > 0`, + /// the encoding of `a` (A.2.2), and opens the AAD phase for `aad_len` bytes. Called exactly + /// once, on a value from [`Self::from_perm_unformatted`] that has absorbed nothing yet, which + /// is why `B0`'s payload length is simply what is still owed. + fn format_header(&mut self, aad_len: usize) { + self.aad_owed = aad_len; + let nonce = self.ctr.keystream().nonce(); + self.mac_absorb(&Self::format_b0(&nonce, aad_len > 0, self.owed 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". The encoding goes in now; `A` and the pad + // follow through `do_update_aad`. If `a = 0` there are no AAD blocks at all, so nothing is + // absorbed and nothing is padded, and `B0` has already ended on a block boundary. + if aad_len > 0 { + let (encoded, encoded_len) = Self::encode_aad_len(aad_len as u64); + self.mac_absorb(&encoded[..encoded_len]); + } + } + + /// 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); + CcmKeyStream::::put_q_field(&mut b0, payload_len); + b0 + } + + /// 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.ctr.keystream().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.ctr.keystream().perm.encrypt_block(&mut self.y); + self.mac_pos = 0; + } + } + + /// Debits `len` bytes from the payload length declared to [`Self::new`], refusing any payload + /// while declared AAD is still outstanding. + #[inline] + fn take_owed(&mut self, len: usize) -> Result<(), SymmetricCipherError> { + if len > 0 && self.aad_owed != 0 { + return Err(SymmetricCipherError::StateError( + "CCM was given payload before all of the declared AAD; A.2.3 puts the payload \ + after the AAD", + )); + } + if len > self.owed { + return Err(SymmetricCipherError::StateError( + "CCM was given more payload than the declared payload length, 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 the payload keystream produces, so it gets the same `Secret` treatment + // rather than a plain local that outlives this function's stack frame unzeroed. + let keystream = self.ctr.keystream(); + let mut s0: Secret<[u8; BLOCK_LEN]> = Secret::new(); + *s0 = CcmKeyStream::::counter_block( + &keystream.ctr_template, 0, + ); + keystream.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, or if it is non-empty while AAD declared to [`Self::new_with_lengths`] is + /// still outstanding. Nothing is consumed in either case. + pub fn do_encrypt(&mut self, data: &mut [u8]) -> Result<(), SymmetricCipherError> { + self.take_owed(data.len())?; + self.mac_absorb(data); + self.ctr.do_encrypt_inplace(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 -- or less AAD than declared to [`Self::new_with_lengths`], for the + /// same reason. + pub fn do_encrypt_final(self) -> Result<[u8; TAG_LEN], SymmetricCipherError> { + if self.aad_owed != 0 { + return Err(SymmetricCipherError::StateError( + "CCM was given less AAD than the declared AAD length", + )); + } + if self.owed != 0 { + return Err(SymmetricCipherError::StateError( + "CCM was given less payload than the declared payload length, 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. The entire output buffer is zeroized before the ciphertext is written, so any bytes + /// past that count will be 0. For the spec's own inline `ciphertext || tag` string, use + /// [`Self::encrypt_out`]. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, plus + /// [`Self::new`]'s errors. + pub fn encrypt_detached_out( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext.fill(0); + 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(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. + /// The entire output buffer is zeroized before the output is written, so any bytes past that + /// count will be 0. + /// + /// # Errors + /// As [`Self::encrypt_detached_out`]. + pub fn encrypt_out( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + 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_out(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`] 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, or if it is non-empty while AAD declared to [`Self::new_with_lengths`] is + /// still outstanding. Nothing is consumed in either case. + pub fn do_decrypt_update(&mut self, data: &mut [u8]) -> Result<(), SymmetricCipherError> { + self.take_owed(data.len())?; + self.ctr.do_decrypt_inplace(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`], or less AAD than declared to [`Self::new_with_lengths`]. + pub fn do_decrypt_final(self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + if self.aad_owed != 0 { + return Err(SymmetricCipherError::StateError( + "CCM was given less AAD than the declared AAD length", + )); + } + if self.owed != 0 { + return Err(SymmetricCipherError::StateError( + "CCM was given less ciphertext than the declared payload length, 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). + /// + /// Returns the number of plaintext bytes written. The entire output buffer is zeroized before + /// the plaintext is written, so any bytes past that count will be 0. 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_out( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + 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)`. Returns the + /// number of plaintext bytes written. The entire output buffer is zeroized before the plaintext + /// is written, so any bytes past that count will be 0. + /// + /// # 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_decrypt_final`] specifies for a malformed ciphertext so that + /// every inline entry point -- this one, [`CcmDecryptor::do_decrypt_final`] and + /// [`CcmDecryptor::decrypt_with_aad_out`](AEADCipherDecryptor::decrypt_with_aad_out) -- agrees + /// on the same input. Otherwise as [`Self::decrypt_detached_out`]. + pub fn decrypt_out( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + let Some((data, tag)) = ciphertext.split_last_chunk::() else { + return Err(SymmetricCipherError::DecryptionFailed); + }; + Self::decrypt_detached_out(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; +} + +/// The CTR half of CCM: the keystream `Sj = CIPH_K(Ctrj)` for `j = 1, 2, ...` (Sec 6.1 steps 5-7), +/// over counter blocks formatted as A.3 specifies. +/// +/// Crate-private: CCM's CTR half alone is an unauthenticated cipher, and is only reachable +/// through [`Ccm`]. It shares its key schedule with the CBC-MAC half, which reaches it through +/// [`StreamCipher::keystream`]. +#[derive(Clone)] +struct CcmKeyStream +where + P: ElectronicCodeBook, +{ + perm: P, + // `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 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, +} + +impl + CcmKeyStream +where + P: ElectronicCodeBook, +{ + /// The spec's `q`: the octet length of the payload-length field `Q`, which is also the width + /// of the counter field. A.1 requires `n + q = 15`. + const Q_LEN: usize = 15 - NONCE_LEN; + + /// The largest counter value the `q`-octet counter field can hold, `2^8q - 1`; `q = 8` makes + /// that `u64::MAX`. + const MAX_COUNTER: u64 = + if Self::Q_LEN >= 8 { u64::MAX } else { (1u64 << (8 * Self::Q_LEN)) - 1 }; + + /// Formats the counter template and positions the keystream at `S1`. + fn from_perm(perm: P, nonce: &[u8; NONCE_LEN]) -> Self { + // 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); + Self { perm, ctr_template, next_ctr: 1 } + } + + /// The nonce `N`, read back out of the counter template: A.3 Table 3 puts it in octets + /// `1 ... 15-q`, which is `1 ... NONCE_LEN` since `n + q = 15`. It is the same `N` that + /// `B0` carries (A.2.1 Table 2), so [`Ccm::format_header`] needs no second copy of it. + fn nonce(&self) -> [u8; NONCE_LEN] { + let mut nonce = [0u8; NONCE_LEN]; + nonce.copy_from_slice(&self.ctr_template[1..1 + NONCE_LEN]); + nonce + } + + /// 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: [`Ccm::new`] refuses a payload above + /// [`Ccm::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..]); + } + + /// Builds `Ctrj` (A.3, Table 3) for counter index `j` from the template, without encrypting + /// it. An associated function rather than a method so that [`KeyStream::apply_blocks`] can call + /// it while it holds the counter mutably. + #[inline] + fn counter_block(template: &[u8; BLOCK_LEN], j: u64) -> [u8; BLOCK_LEN] { + let mut ctr = *template; + Self::put_q_field(&mut ctr, j); + ctr + } +} + +impl Algorithm + for CcmKeyStream +where + P: ElectronicCodeBook, +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +impl + KeyStream for CcmKeyStream +where + P: ElectronicCodeBook, +{ + fn new( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ) -> Result { + Ok(Self::from_perm(P::new(key)?, nonce)) + } + + /// Every counter value from `next_ctr` to `2^8q - 1`. Never the binding limit in practice: + /// [`Ccm::new`] caps the payload at `2^8q - 1` bytes, far fewer than that many blocks. + fn remaining_blocks(&self) -> u64 { + Self::MAX_COUNTER - self.next_ctr + 1 + } + + /// Step 8's `P XOR MSB_Plen(S)` and Sec 6.2 step 5's `MSB(C) XOR MSB(S)` -- the same operation + /// -- over whole blocks; see [`apply_counter_blocks`]. + fn apply_blocks(&mut self, blocks: &mut [[u8; BLOCK_LEN]]) { + let template = &self.ctr_template; + apply_counter_blocks( + &self.perm, + &mut self.next_ctr, + |j| Self::counter_block(template, j), + blocks, + ); + } +} + +/// What [`CcmEncryptor`] and [`CcmDecryptor`] share: the [`Ccm`] state, which the trait +/// constructor builds before it has seen any AAD, and the AAD itself, held back until it is +/// complete. +/// +/// The trait's AAD is optional and open-ended, and A.2.2 puts the encoding of its length `a` in +/// front of it -- and `B0`'s Adata bit before that -- so no AAD byte can reach the CBC-MAC until +/// the last one has arrived. The first non-empty payload update, or the final, is what says so; +/// [`Self::begin_data`] then runs Sec 6.1 step 1 over the whole of it at once. That is the only +/// buffer in either adapter, and `AAD_LEN` is its capacity: header-sized, by the caller's choice. +/// +/// `DATA_LEN` is an exact length, not a capacity, and is committed to `B0` here: the payload +/// then streams through [`Ccm`]'s own `owed` accounting, which refuses more and whose finals +/// refuse less. +#[derive(Clone)] +struct CcmAdapter< + P, + Dir, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> where + P: ElectronicCodeBook, +{ + ccm: Ccm, + // Associated data is authenticated but not encrypted, and travels in the clear, so it is not + // secret and is not wrapped. + aad: [u8; AAD_LEN], + aad_len: usize, + // Whether `begin_data` has run: Sec 6.1 step 1 has been absorbed and the AAD phase is over. + formatted: bool, +} + +impl< + P, + Dir, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> CcmAdapter +where + P: ElectronicCodeBook, +{ + /// The compile-time checks for the trait adapters, run from both [`CcmEncryptor`]'s and + /// [`CcmDecryptor`]'s constructors, which every entry point goes through, so that a parameter + /// set either names a usable adapter or does not compile at all. [`Ccm::check_shape`]'s own checks run + /// as well, from the [`Ccm`] constructor underneath. + /// + /// 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_shape() { + 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" + ); + // `B0` could not carry it (A.1's `p < 2^8q`), and this is the one place the length is + // known at compile time, so it is a compile error rather than `Ccm::new`'s `Err`. + assert!( + DATA_LEN as u64 + <= Ccm::::MAX_PAYLOAD_LEN, + "CCM: DATA_LEN exceeds the payload limit 2^8q - 1 that NONCE_LEN implies (A.1)" + ); + } + } + + /// Readies the keystream and commits `DATA_LEN` as the payload length; the CBC-MAC absorbs + /// nothing until [`Self::begin_data`]. + fn new(perm: P, nonce: &[u8; NONCE_LEN]) -> Self { + Self::check_shape(); + Self { + ccm: Ccm::from_perm_unformatted(perm, nonce, DATA_LEN), + aad: [0u8; AAD_LEN], + aad_len: 0, + formatted: false, + } + } + + /// Holds back `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 absorbed once all + /// of it is in hand. An empty `aad` is a no-op at any point. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] for a non-empty `aad` once the payload has begun, and + /// [`SymmetricCipherError::GenericError`] if the total would exceed `AAD_LEN`. Nothing is + /// held in either case. + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + if aad.is_empty() { + return Ok(()); + } + if self.formatted { + return Err(SymmetricCipherError::StateError( + "CCM: do_update_aad after the payload has begun; A.2.3 puts the AAD before the \ + payload", + )); + } + let end = self.aad_len + aad.len(); + if end > AAD_LEN { + return Err(SymmetricCipherError::GenericError( + "CCM: associated data longer than AAD_LEN", + )); + } + self.aad[self.aad_len..end].copy_from_slice(aad); + self.aad_len = end; + Ok(()) + } + + /// Ends the AAD phase, the first time it is called: runs Sec 6.1 step 1 -- `B0`, the encoding + /// of `a`, the AAD and its zero pad -- over the AAD held so far, which is now known to be all + /// of it. Called before any payload byte goes through [`Ccm`], and from every final, so a + /// message with no payload at all still gets its header. Later calls do nothing. + fn begin_data(&mut self) { + if !self.formatted { + self.formatted = true; + self.ccm.format_header(self.aad_len); + // Exactly the length just declared, so nothing is left owed. + self.ccm.absorb_aad(&self.aad[..self.aad_len]); + } + } +} + +/// Adapts [`Ccm`] to [`AEADCipherEncryptor`] and, through it, [`SymmetricCipherEncryptor`], for a +/// payload of exactly `DATA_LEN` bytes. +/// +/// [`SymmetricCipherEncryptor::do_encrypt_init`] is handed a key and nothing else, but CCM cannot +/// form `B0` -- and so cannot authenticate anything -- until it knows the payload length +/// (Appendix A.2.1; see the module docs). Rather than hold the message until the final call +/// reveals that length, this type fixes the payload length as the const parameter `DATA_LEN`. +/// The streaming +/// methods then stream: every ciphertext byte is released by the call that produces it, +/// [`do_encrypt_out_len`](SymmetricCipherEncryptor::do_encrypt_out_len) is the identity, and +/// `FINAL_LEN` is `TAG_LEN`, exactly as for GCM. The price is that they accept exactly +/// `DATA_LEN` bytes of payload -- the fixed frame of SP 800-38C Sec 3's "packet environment" -- +/// and refuse any other amount, more at the update that would exceed it and less at the final. +/// The one-shots are the trait's own, provided over those methods, so they take exactly +/// `DATA_LEN` bytes too: a frame of any other length is a `Ccm` one-shot's job, with the lengths +/// supplied per message. +/// +/// `AAD_LEN` is a capacity, not an exact length: the trait's AAD is optional and open-ended, and +/// the inherited [`SymmetricCipherEncryptor`] methods are this AEAD with none at all. Up to +/// `AAD_LEN` bytes of it are held until the first payload byte, or the final, marks it complete; +/// see `CcmAdapter`. More is refused. +/// +/// ``` +/// use bouncycastle_core_test_framework::ToyBlockCipher; +/// use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +/// use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +/// use bouncycastle_cipher::modes::{CcmDecryptor, CcmEncryptor}; +/// +/// // Frames of exactly 40 payload bytes, with up to 16 bytes of header. +/// type Enc = CcmEncryptor; +/// type Dec = CcmDecryptor; +/// +/// let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// let header = b"frame 7"; +/// let frame = [0x5Au8; 40]; +/// +/// let (mut enc, nonce) = Enc::do_encrypt_init(&key).expect("init"); +/// enc.do_update_aad(header).expect("within AAD_LEN"); +/// let mut ct = [0u8; 40]; +/// // Every byte comes straight out; the chunking is the caller's business. +/// let mut written = 0; +/// for piece in frame.chunks(7) { +/// written += enc.do_encrypt_out(piece, &mut ct[written..]).expect("within DATA_LEN"); +/// } +/// assert_eq!(written, 40); +/// let (_, _, tag) = enc.do_encrypt_final_detachedtag().expect("exactly DATA_LEN was supplied"); +/// +/// let mut dec = Dec::do_decrypt_init(&key, &nonce).expect("init"); +/// dec.do_update_aad(header).expect("within AAD_LEN"); +/// let mut pt = [0u8; 40]; +/// dec.do_decrypt_out(&ct, &mut pt).expect("released, but not yet authenticated"); +/// dec.do_decrypt_final_detachedtag(&tag).expect("...until the tag verifies"); +/// assert_eq!(pt, frame); +/// +/// // 39 bytes is not a frame: the final refuses rather than authenticate a length `B0` did not commit to. +/// let (mut short, _) = Enc::do_encrypt_init(&key).expect("init"); +/// short.do_encrypt_out(&frame[..39], &mut ct).expect("within DATA_LEN"); +/// assert!(short.do_encrypt_final().is_err()); +/// ``` +/// +/// # 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. A shorter nonce does not compile: +/// +/// ```compile_fail +/// use bouncycastle_core_test_framework::ToyBlockCipher; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SymmetricCipherEncryptor; +/// use bouncycastle_cipher::modes::CcmEncryptor; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// // n = 8 is permitted by A.1, but too short for a random draw. +/// let _ = CcmEncryptor::::do_encrypt_init(&key); +/// ``` +/// +/// Nor does a `DATA_LEN` that `B0` could not carry under this `NONCE_LEN` (A.1's `p < 2^8q`): +/// +/// ```compile_fail +/// use bouncycastle_core_test_framework::ToyBlockCipher; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SymmetricCipherEncryptor; +/// use bouncycastle_cipher::modes::CcmEncryptor; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// // n = 13 leaves q = 2, so the payload is at most 65535 bytes. +/// let _ = CcmEncryptor::::do_encrypt_init(&key); +/// ``` +/// +/// # Memory +/// +/// A value is the fixed-size [`Ccm`] state, the `AAD_LEN`-byte AAD buffer and two words of +/// bookkeeping, independent of `DATA_LEN`; the finals return and write only the tag. Nothing +/// scales with the message. +#[derive(Clone)] +pub struct CcmEncryptor< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +>(CcmAdapter) +where + P: ElectronicCodeBook; + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_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 AAD_LEN: usize, + const DATA_LEN: usize, +> CcmEncryptor +where + P: ElectronicCodeBook, +{ + /// Every final comes here: Sec 6.1 steps 4 and 8, the tag. The header goes in first if no + /// payload call put it there, which is the `DATA_LEN = 0` message. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if fewer than `DATA_LEN` payload bytes were supplied: + /// `B0` committed to `DATA_LEN`, so a shorter message would get a tag no verifier could + /// reproduce, and [`Ccm::do_encrypt_final`] refuses to produce one. + fn finish(mut self) -> Result<[u8; TAG_LEN], SymmetricCipherError> { + self.0.begin_data(); + self.0.ccm.do_encrypt_final() + } +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_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> { + // `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(CcmAdapter::new(perm, &nonce)), nonce)) + } + + /// The identity: nothing is held back, since `B0` is already committed to `DATA_LEN`. + fn do_encrypt_out_len(&self, input_len: usize) -> usize { + input_len + } + + /// Sec 6.1 steps 3 and 8 over `plaintext`, written to `ciphertext`: the plaintext goes + /// through the CBC-MAC and the CTR keystream, and every byte comes out. A non-empty call ends + /// the AAD phase; an empty one is a no-op that leaves it open. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is shorter than + /// `plaintext`, and [`SymmetricCipherError::StateError`] if `plaintext` would take the total + /// past `DATA_LEN`. Nothing is consumed in either case and `ciphertext` is left zeroed, as on + /// every call, though a non-empty call refused for its length has still ended the AAD phase. + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + if plaintext.is_empty() { + return Ok(0); + } + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::OutputBufferTooSmall(plaintext.len())); + } + // 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.0.begin_data(); + // `Ccm::do_encrypt` would refuse this too, but only after the plaintext had been copied + // into the caller's output buffer, and a refused call must not leave plaintext there. + if plaintext.len() > self.0.ccm.owed { + return Err(SymmetricCipherError::StateError( + "CCM: plaintext longer than DATA_LEN, the payload length the type declares", + )); + } + let out = &mut ciphertext[..plaintext.len()]; + out.copy_from_slice(plaintext); + self.0.ccm.do_encrypt(out)?; + Ok(plaintext.len()) + } + + /// The tag, and nothing else: all of the ciphertext has already been released. + /// + /// # Errors + /// As [`AEADCipherEncryptor::do_encrypt_final_detachedtag_out`]. + fn do_encrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + Ok((self.finish()?, 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 + } +} + +/// The AEAD view, with `FINAL_LEN = TAG_LEN`: the encryptor holds nothing back, so the detached +/// final flushes nothing and returns only the tag. +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> AEADCipherEncryptor + for CcmEncryptor +where + P: ElectronicCodeBook, +{ + /// Holds back `aad` until the payload begins; see `CcmAdapter`. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] for a non-empty `aad` after the first non-empty + /// [`do_encrypt_out`](SymmetricCipherEncryptor::do_encrypt_out), and + /// [`SymmetricCipherError::GenericError`] if the total would exceed `AAD_LEN`. + fn do_update_aad(&mut self, aad: &[u8]) -> Result<(), SymmetricCipherError> { + self.0.do_update_aad(aad) + } + + /// Sec 6.1 steps 4 and 8: the tag. Nothing is held back, so `ciphertext` is left zeroed. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if fewer than `DATA_LEN` payload bytes were supplied. + fn do_encrypt_final_detachedtag_out( + self, + ciphertext: &mut [u8; TAG_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext.fill(0); + Ok((0, self.finish()?)) + } +} + +/// Adapts [`Ccm`] to [`AEADCipherDecryptor`] and, through it, [`SymmetricCipherDecryptor`], for +/// a payload of exactly `DATA_LEN` bytes; the mirror of [`CcmEncryptor`], and see it for the +/// parameters, the nonce floor and the memory. +/// +/// Knowing the payload length has a consequence no other decryptor in this crate enjoys: there is +/// nothing to guess about where the tag starts. The first `DATA_LEN` bytes of the stream are +/// ciphertext and are decrypted and released by the call that brings them; anything after them +/// can only be an inline tag (Sec 6.2 step 6's `LSB_Tlen(C)`), and only those bytes -- at most +/// `TAG_LEN` -- are held back for the final to check. A `C` of any other length than `DATA_LEN` +/// (detached) or `DATA_LEN + TAG_LEN` (inline) is refused as malformed. +/// +/// # 🚨 Security Considerations 🚨 +/// +/// **The plaintext this releases is not authenticated until the final call returns `Ok`.** Sec 6.2 +/// recovers `P` (step 5) before it can verify it (step 10), and this type releases `P` as it is +/// recovered rather than hold the whole frame back, so a forged ciphertext yields attacker-chosen +/// bytes that only the final's [`SymmetricCipherError::AEADTagCheckFailed`] disowns. That is +/// [`AEADCipherDecryptor`]'s general streaming caveat, and the inherent +/// [`Ccm::do_decrypt_update`] has it too. Sec 6.2's "the payload P and the MAC T shall not be +/// revealed" on INVALID is honoured by the one-shots, which zeroize what they wrote before +/// returning the error. +#[derive(Clone)] +pub struct CcmDecryptor< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> where + P: ElectronicCodeBook, +{ + inner: CcmAdapter, + // The bytes past the `DATA_LEN`th, as they arrive: an inline tag, if the final says the + // layout is inline, and excess ciphertext if it says detached. Public either way (the tag + // travels in the clear), so not wrapped. + tag: [u8; TAG_LEN], + tag_len: usize, +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_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 AAD_LEN: usize, + const DATA_LEN: usize, +> CcmDecryptor +where + P: ElectronicCodeBook, +{ + /// Every final comes here once it has settled which bytes are the tag: Sec 6.2 step 10, + /// through [`Ccm::do_decrypt_final`]. The header goes in first if no payload call put it + /// there, which is the `DATA_LEN = 0` message. + /// + /// # Errors + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. The callers have + /// already refused a short payload, so [`Ccm::do_decrypt_final`]'s own refusal of one is not + /// reachable from here; it stays as the backstop it is for the inherent API. + fn finish(mut self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + self.inner.begin_data(); + self.inner.ccm.do_decrypt_final(tag) + } +} + +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_LEN: usize, +> SymmetricCipherDecryptor + for CcmDecryptor +where + P: ElectronicCodeBook, +{ + fn do_decrypt_init( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + ) -> Result { + // `P::new`'s own checks are the only key validation needed; see the encryptor. + let perm = P::new(key)?; + Ok(Self { inner: CcmAdapter::new(perm, nonce), tag: [0u8; TAG_LEN], tag_len: 0 }) + } + + /// Every byte of `input_len` that is still inside the declared payload; the rest can only be + /// the tag, and is held back. + fn do_decrypt_out_len(&self, input_len: usize) -> usize { + input_len.min(self.inner.ccm.owed) + } + + /// Sec 6.2 steps 5 and 7 over the payload part of `ciphertext`, written to `plaintext` + /// **unauthenticated** (see the type's security considerations), with whatever follows the + /// `DATA_LEN`th byte held back as the possible tag. A non-empty call ends the AAD phase; an + /// empty one is a no-op that leaves it open. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is shorter than + /// [`do_decrypt_out_len`](Self::do_decrypt_out_len), and [`SymmetricCipherError::StateError`] + /// if `ciphertext` would take the total past `DATA_LEN + TAG_LEN`, more than either layout + /// can be. Nothing is consumed in either case and `plaintext` is left zeroed, as on every + /// call, though a non-empty call refused for its length has still ended the AAD phase. + fn do_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + if ciphertext.is_empty() { + return Ok(0); + } + let release = self.do_decrypt_out_len(ciphertext.len()); + if plaintext.len() < release { + return Err(SymmetricCipherError::OutputBufferTooSmall(release)); + } + // Before the length check, as on the encryptor: a refused oversized call still closes the + // AAD phase. + self.inner.begin_data(); + let (data, tail) = ciphertext.split_at(release); + if tail.len() > TAG_LEN - self.tag_len { + return Err(SymmetricCipherError::StateError( + "CCM: ciphertext longer than DATA_LEN + TAG_LEN, the payload length the type \ + declares plus an inline tag", + )); + } + // `release` is within what is owed, so `Ccm` cannot refuse it. + plaintext[..release].copy_from_slice(data); + self.inner.ccm.do_decrypt_update(&mut plaintext[..release])?; + self.tag[self.tag_len..self.tag_len + tail.len()].copy_from_slice(tail); + self.tag_len += tail.len(); + Ok(release) + } + + /// The inline layout: the `TAG_LEN` bytes held back after the payload are the tag (Sec 6.2 + /// step 6's `LSB_Tlen(C)`), and Sec 6.2 runs over everything before them. Releases nothing: + /// every plaintext byte went out as it was recovered. + /// + /// # Errors + /// [`SymmetricCipherError::DecryptionFailed`] if fewer than `DATA_LEN + TAG_LEN` bytes were + /// supplied -- Sec 6.2 step 1's "If Clen <= Tlen, then return INVALID", for a `C` whose + /// length is fixed; [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. + fn do_decrypt_final(self) -> Result<([u8; TAG_LEN], usize), SymmetricCipherError> { + // Tag bytes are only held once the whole payload has been released, so a full tag means + // a full payload too; a short payload shows up here as no tag at all. + if self.tag_len < TAG_LEN { + return Err(SymmetricCipherError::DecryptionFailed); + } + let tag = self.tag; + self.finish(&tag)?; + Ok(([0u8; TAG_LEN], 0)) + } + + /// The payload, which is `DATA_LEN` whatever `ciphertext_len` claims. For the one `C` the + /// inline layout accepts that is `ciphertext_len - TAG_LEN`, as for any AEAD; for a shorter + /// `C` it is still what [`do_decrypt_out`](Self::do_decrypt_out) releases, so a one-shot that + /// sizes its buffer by this reaches the final and reports the short `C` as malformed, rather + /// than refusing the buffer first. + fn decrypt_out_len(ciphertext_len: usize) -> usize { + ciphertext_len.min(DATA_LEN) + } +} + +/// The AEAD view, with `FINAL_LEN = TAG_LEN`. The one-shots are the trait's own, so a `C` of +/// any length but the frame's is refused like any other wrong-length stream. +impl< + P, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const AAD_LEN: usize, + const DATA_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.inner.do_update_aad(aad) + } + + /// The detached layout: every byte of `C` is ciphertext, so `C` is exactly `DATA_LEN` long + /// and Sec 6.2 runs over all of it against `tag`. Releases nothing, so `plaintext` is left + /// zeroed: every plaintext byte went out as it was recovered. + /// + /// # Errors + /// [`SymmetricCipherError::DecryptionFailed`] if `C` was not exactly `DATA_LEN` bytes -- a + /// payload still owed, or bytes held back as a possible inline tag that this layout has no + /// place for; [`SymmetricCipherError::AEADTagCheckFailed`] if the tag does not verify. + fn do_decrypt_final_detachedtag_out( + self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; TAG_LEN], + ) -> Result { + plaintext.fill(0); + if self.tag_len != 0 || self.inner.ccm.owed != 0 { + return Err(SymmetricCipherError::DecryptionFailed); + } + self.finish(tag)?; + Ok(0) + } +} + +/// The suspended state is the CTR half, the CBC-MAC chaining value, how much of its current +/// block has gone in, and how much AAD and payload are still owed. The chaining value is +/// key-dependent MAC state, which is why the state must be protected; see [`bouncycastle_utils::suspendable_state`]. +impl< + P, + Dir, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, +> SuspendableComponent for Ccm +where + P: ElectronicCodeBook, +{ + const STATE_LEN: usize = Self::CTR_STATE_LEN + BLOCK_LEN + 8 + 8 + 8; + type Key = KeyMaterial; + + fn write_state(&self, out: &mut [u8]) { + let (ctr, rest) = out.split_at_mut(Self::CTR_STATE_LEN); + self.ctr.write_state(ctr); + let mut w = CursorMut::new(rest); + w.bytes(&*self.y); + w.u64(self.mac_pos as u64); + w.u64(self.aad_owed as u64); + w.u64(self.owed as u64); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + Self::check_shape(); + let (ctr, rest) = state.split_at(Self::CTR_STATE_LEN); + let ctr = StreamCipher::read_state(ctr, key)?; + let mut r = Cursor::new(rest); + let mut y: Secret<[u8; BLOCK_LEN]> = Secret::new(); + (*y).copy_from_slice(r.bytes(BLOCK_LEN)); + // A whole block is enciphered as soon as it is full, so `mac_pos` is always below + // `BLOCK_LEN`; the owed payload cannot exceed what `B0` could have committed to. + let mac_pos = bounded_usize(r.u64(), BLOCK_LEN - 1)?; + let aad_owed = bounded_usize(r.u64(), usize::MAX)?; + let owed = bounded_usize(r.u64(), usize::MAX)?; + if owed as u64 > Self::MAX_PAYLOAD_LEN { + return Err(SuspendableError::InvalidData); + } + debug_assert!(r.is_done()); + Ok(Self { ctr, y, mac_pos, aad_owed, owed, _dir: PhantomData }) + } +} + +/// `N` must be [`Ccm::SUSPENDED_STATE_LEN`]; anything else is a compile error. +impl< + P, + Dir, + const KEY_LEN: usize, + const BLOCK_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const N: usize, +> SuspendableKeyed for Ccm +where + P: ElectronicCodeBook, +{ + type Key = KeyMaterial; + + fn suspend(self) -> [u8; N] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + resume_component(&state, key) + } +} + +/// The suspended state is the counter template and the next counter index; the permutation is +/// rebuilt from the re-supplied key. Crate-private like the type, reachable only through +/// [`Ccm`]'s state. +impl SuspendableComponent + for CcmKeyStream +where + P: ElectronicCodeBook, +{ + const STATE_LEN: usize = BLOCK_LEN + 8; + type Key = KeyMaterial; + + fn write_state(&self, out: &mut [u8]) { + let mut w = CursorMut::new(out); + w.bytes(&self.ctr_template); + w.u64(self.next_ctr); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + let mut r = Cursor::new(state); + let ctr_template = r.array::(); + // A.3: the flags octet is `[q-1]_3` and nothing else, and the counter field is zero in + // the template; `next_ctr` starts at 1 (`S0` is the tag mask) and stops at `MAX_COUNTER`. + let next_ctr = r.u64(); + let flags_ok = ctr_template[0] == (Self::Q_LEN - 1) as u8; + let counter_field_zero = ctr_template[BLOCK_LEN - Self::Q_LEN..].iter().all(|&b| b == 0); + let ctr_ok = next_ctr >= 1 && next_ctr - 1 <= Self::MAX_COUNTER; + if !(flags_ok && counter_field_zero && ctr_ok) { + return Err(SuspendableError::InvalidData); + } + debug_assert!(r.is_done()); + Ok(Self { perm, ctr_template, next_ctr }) + } +} + +#[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 ks = CcmKeyStream::::from_perm(Identity, &nonce_c1); + // `Ctr0` is the template with a zero counter field. + let ctr0 = CcmKeyStream::::counter_block(&ks.ctr_template, 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 the first block the keystream XORs in must + // be `Ctr1`: under the identity permutation, XORed into zeros, that is `Ctr1` itself. + let mut s1 = [[0u8; 16]]; + ks.apply_blocks(&mut s1); + let mut ctr1 = ctr0; + ctr1[15] = 1; + assert_eq!(s1[0], 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 ks4 = CcmKeyStream::::from_perm(Identity, &nonce_c4); + assert_eq!( + CcmKeyStream::::counter_block(&ks4.ctr_template, 0), + [ + 0x01, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x00, 0x00 + ], + "C.4 Ctr0" + ); + } + + /// A fresh keystream has every counter value but `Ctr0` left: step 7's `S1 || S2 || ...` runs + /// from `j = 1` to the largest `q`-octet counter, `2^8q - 1`, and `S0` is the tag mask. Pinned + /// absolutely because the limit can never bind through the public API -- A.1 caps the payload + /// at `2^8q - 1` bytes, far fewer than that many blocks -- so nothing else would notice an + /// off-by-one here. + #[test] + fn a_fresh_keystream_has_every_counter_but_ctr0_left() { + // n = 13, so q = 2: counters 1 ..= 65535. + let ks = CcmKeyStream::::from_perm(Identity, &[0u8; 13]); + assert_eq!(ks.remaining_blocks(), 65535); + // n = 7, so q = 8: counters 1 ..= 2^64 - 1, which is `u64::MAX` of them. + let ks = CcmKeyStream::::from_perm(Identity, &[0u8; 7]); + assert_eq!(ks.remaining_blocks(), u64::MAX); + } + + /// CCM's keystream against the shared [`KeyStream`] conformance suite. A unit test rather than + /// an integration test because `CcmKeyStream` is crate-private. Over the framework's keyed + /// toy rather than the identity, which ignores the key and so could not pass the key-policy + /// checks. + #[test] + fn ccm_keystream_conforms_to_the_key_stream_framework() { + use bouncycastle_core_test_framework::ToyBlockCipher; + use bouncycastle_core_test_framework::key_stream::TestFrameworkKeyStream; + let framework = TestFrameworkKeyStream::new(); + framework.test::<16, 7, 16, CcmKeyStream>(); + framework.test::<16, 13, 16, CcmKeyStream>(); + } + + /// 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/cipher/src/modes/cfb.rs b/crypto/cipher/src/modes/cfb.rs new file mode 100644 index 00000000..c6d713be --- /dev/null +++ b/crypto/cipher/src/modes/cfb.rs @@ -0,0 +1,593 @@ +//! The Cipher Feedback mode of operation (NIST SP 800-38A §6.3), full-block segment, as a stream +//! cipher. +//! +//! CFB and CFB8 are the same construction at two segment sizes, resulting in different, +//! non-interoperable modes. See [`cfb8`] +//! +//! # A stream cipher, implemented over a block cipher +//! +//! CFB is a keystream mode: the cipher never touches the data, it produces a keystream, and the data +//! is XORed with it byte for byte. While this mode operates over a block permutation primitive, +//! the chunking is invisible in the caller because the state carries the unused part of a block +//! from one call to the next. +//! +//! Since this does not require the input data to be block-aligned (ie to be a length that is a +//! multiple of the block size of the underlying permutation), this implementation treats the final +//! bytes of the message as a short final block and only extracts as much key stream material from +//! the final block as required to encrypt it. This avoids needing padding, and avoids any ciphertext +//! expansion. +//! +//! Note that NIST SP 800-38A §5.2 "Representation of the Plaintext and the Ciphertext" only presents +//! definitions for block-aligned ciphertexts, and these are the only one the Appendix F.3 and ACVP +//! test vectors cover. +//! +//! # Decryption uses the *forward* cipher function +//! +//! As a stream cipher producing an XOR key stream, the forward (encryption) and reverse (decryption) +//! are the same: both directions apply `CIPH_K`. +//! +//! So [`Cfb`](Cfb) is implemented over a permutation that impls [`ElectronicCodeBook`], +//! but never calls [`ElectronicCodeBook::decrypt_block`]. A permutation that implements only the +//! forward direction would still work here. The two directions differ only in which of the two +//! values -- the byte that came in, or the byte that went out -- is the ciphertext to be fed back into +//! the next block. +//! +//! # Parallel decryption +//! +//! Sec 6.3: "In CFB encryption, like CBC encryption, the input block to each forward cipher +//! function (except the first) depends on the result of the previous forward cipher function; +//! therefore, multiple forward cipher operations cannot be performed in parallel. In CFB +//! decryption, the required forward cipher operations can be performed in parallel if the input +//! blocks are first constructed (in series) from the IV and the ciphertext." +//! +//! This implementation follows: encryption handles blocks singly via [`ElectronicCodeBook::encrypt_block`], +//! while decryption can batch-process two or four blocks at a time via +//! [`ElectronicCodeBook::encrypt_2blocks`] or [`ElectronicCodeBook::encrypt_4blocks`], which may +//! yield a performance gain, depending on the implementation of the underlying permutation. +//! +//! # Usage Examples +//! +//! The direction is part of the type: [`Cfb`](Cfb) implements +//! [`StreamCipherEncryptor`] and nothing else, and [`Cfb`](Cfb) implements +//! [`StreamCipherDecryptor`] and nothing else. +//! +//! They take a `&mut [u8]` of any length; there is no padding layer and the ciphertext is exactly +//! as long as the plaintext: +//! +//! ``` +//! use bouncycastle_core_test_framework::ToyBlockCipher; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +//! use bouncycastle_cipher::modes::{Cfb, Cfb8}; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! type ToyCfb = Cfb; +//! type ToyCfb8 = Cfb8; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // Start with the plaintext. +//! let plaintext = b"the quick brown fox!!"; +//! let mut data = *plaintext; +//! +//! let (bytes_written, iv) = ToyCfb::::encrypt_inplace(&key, &mut data).expect("encryption"); +//! assert_eq!(bytes_written, plaintext.len()); +//! +//! // `data` now contains the ciphertext +//! +//! ToyCfb::::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); +//! assert_eq!(data, *b"the quick brown fox!!"); +//! ``` +//! +//! Streaming works with chunks of any size: +//! +//! ``` +//! use bouncycastle_core_test_framework::ToyBlockCipher; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{ +//! StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor +//! }; +//! use bouncycastle_cipher::modes::Cfb; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! type ToyCfb = Cfb; +//! +//! let key = KeyMaterial128::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let mut data = [0x5Au8; 40]; +//! +//! let (mut encryptor, iv) = ToyCfb::::do_encrypt_init(&key).expect("init"); +//! +//! // Just to prove that this can handle arbitrary sizes, we'll feed in +//! // 7 bytes, then 33: neither is a whole block. +//! let bytes_written = encryptor.do_encrypt_inplace(&mut data[..7]).expect("first chunk"); +//! assert_eq!(bytes_written, 7); +//! +//! let bytes_written = encryptor.do_encrypt_inplace(&mut data[7..]).expect("the rest"); +//! assert_eq!(bytes_written, 33); +//! +//! // Decrypting in a different chunking must also agree. +//! let mut decryptor = ToyCfb::::do_decrypt_init(&key, &iv).expect("init"); +//! decryptor.do_decrypt_inplace(&mut data[..19]).expect("first chunk"); +//! decryptor.do_decrypt_inplace(&mut data[19..]).expect("the rest"); +//! assert_eq!(data, [0x5Au8; 40]); +//! ``` +//! +//! # Suspending and resuming execution +//! +//! [`Cfb`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte +//! array and resumed later with the re-supplied key. The state is the open segment and how much of +//! it is used; the permutation is rebuilt from the key. The array length is +//! `Cfb::SUSPENDED_STATE_LEN`; see [the crate docs](crate#suspending-and-resuming-execution) for an +//! example. +//! +//! # Memory Usage +//! +//! The state consists of the underlying permutation struct, one block, `buf`, and a byte count, `used`. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! ## IV integrity +//! +//! NIST SP 800-38A Appendix D: +//! +//! > "for the CBC mode, the decryption of the first ciphertext block is vulnerable to the +//! > (deliberate) introduction of bit errors in specific bit positions of the IV if the integrity of +//! > the IV is not protected". +//! +//! Under CBC a flipped IV bit flips exactly that bit of the first decrypted plaintext block. +//! +//! Tampered IVs in CFB damage the initial block too, but unpredictably rather than controllably: +//! the IV is the first thing fed to the cipher, so this results in random errors instead of +//! predictable ones. See NIST SP 800-38A Appendix D: Error Properties for more info. +//! +//! ## Key stream leakage +//! +//! Every keystream block is `CIPH_K` of a public input -- the IV, then the previous ciphertext +//! block (Sec 6.3) -- so leaked keystream reveals nothing a known-plaintext attacker could not +//! already compute, and recovering the key from it is the block cipher's problem, not the mode's. +//! That means leaking the key stream is likely catastrophic for the confidentiality of the message +//! being protected, but does not compromise the symmetric key. +//! +//! That said, CFB mode is not an RNG, hash function, XOF, or MAC, and primitives intended for those +//! purposes should be used. +//! +//! ## Key stream reuse +//! +//! Reusing the same key stream is equivalent to encrypting two messages with the same key and IV. +//! Two messages encrypted under one key with the same IV share their first keystream block, and +//! because each later input block is the previous ciphertext block (Sec 6.3), they keep sharing +//! keystream until the first block at which their plaintexts differ. +//! Even if the adversary cannot recover the plaintext, simply knowing that two messages are identical +//! on their first N blocks often constitutes a catastrophic loss of security for many applications. +//! For example, this is enough to know if two users have downloaded the same file or a different file, +//! or potentially how much a file was modified between versions. +//! +//! This reinforces the general advice to always generate cryptographically random IVs unique for +//! each encryption operation. + +use crate::modes::iv::random_iv; +use crate::stream::{stream_do_final, stream_update_out}; +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, + suspend_component, +}; +use core::marker::PhantomData; + +// Imports needed for docs +#[allow(unused_imports)] +use crate::modes::cfb8; +// End imports needed for docs + +/// CFB mode over any [`ElectronicCodeBook`], as a stream cipher, with the direction encoded in the +/// type. +/// +/// The segment size is the full block (`s = b`, i.e. CFB128 for AES); see the module docs for why +/// the other segment sizes are out of scope, and for how a message that is not a whole number of +/// blocks is handled. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`]. [`StreamCipherEncryptor`] is implemented only for the +/// former and [`StreamCipherDecryptor`] only for the latter, so a `Cfb<_, Encrypting, _, _>` has no +/// decryption methods at all -- using one in the wrong direction is a compile error rather than a +/// runtime check. +/// +/// The initialization data is one block, so `INIT_DATA_LEN == BLOCK_LEN`. +#[derive(Clone)] +pub struct Cfb +where + P: ElectronicCodeBook, +{ + perm: P, + /// `buf[..used]` is the ciphertext of the current segment so far, i.e. the head of `I_{j+1}`; + /// `buf[used..]` is the unused tail of `Oj`. When `used == BLOCK_LEN` the whole buffer is the + /// next input block (initially `I1 = IV`) and no keystream is pending. + // + // # One buffer, three roles + // + // Within segment `j`, `buf[..used]` holds the ciphertext bytes produced (or consumed) so far and + // `buf[used..]` holds the bytes of `Oj` not yet used. Both are needed and they fit in one block + // because each ciphertext byte is written over the keystream byte that produced it. + // When `used == BLOCK_LEN` the buffer holds `I_{j+1}`, and the next byte encrypts it in place + // into `O_{j+1}`. So the same 16 bytes are the input block, then the output block, then the + // next input block, and no copy is ever made. + // + // Since `buf` holds either plaintext or ciphertext, but never any keymaterial, it is not + // necessary to tag it as `Secret<>`. + buf: [u8; BLOCK_LEN], + /// Bytes of the current segment already processed, `0..=BLOCK_LEN`. + used: usize, + _dir: PhantomData, +} + +impl Cfb +where + P: ElectronicCodeBook, +{ + /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the segment + /// buffer and the `used` count as a `u64`. See [`bouncycastle_utils::suspendable_state`]. + pub const SUSPENDED_STATE_LEN: usize = LIB_VERSION_LEN + BLOCK_LEN + 8; + + /// `I1 = IV`, with no segment open: the first byte in either direction will compute `O1`. + #[inline] + fn start(perm: P, iv: [u8; BLOCK_LEN]) -> Self { + Self { perm, buf: iv, used: BLOCK_LEN, _dir: PhantomData } + } + + /// Makes the next keystream byte available: if the current segment is complete, `buf` is the + /// next input block, so `Oj = CIPH_K(Ij)` is computed in place and a new segment opened. + /// + /// The forward cipher function, in both directions -- see the module docs. + #[inline] + fn refill_if_used_up(&mut self) { + if self.used == BLOCK_LEN { + self.perm.encrypt_block(&mut self.buf); + self.used = 0; + } + } + + /// Encrypts fewer than a block's worth of bytes, byte by byte, within the open segment or + /// opening a new one: `Cj[i] = Pj[i] XOR Oj[i]`, then `Cj[i]` takes the place of `Oj[i]` in the + /// buffer as the `i`th byte of `I_{j+1}`. + /// + /// Correct for any length, but only called with what the block path cannot take: the bytes that + /// complete a segment left open by an earlier call, and the final short segment. + #[inline] + fn encrypt_bytes(&mut self, data: &mut [u8]) { + for byte in data.iter_mut() { + self.refill_if_used_up(); + *byte ^= self.buf[self.used]; + self.buf[self.used] = *byte; + self.used += 1; + } + } + + /// The decrypting counterpart of [`Self::encrypt_bytes`]: `Pj[i] = Cj[i] XOR Oj[i]`, and it is + /// the *ciphertext* byte `Cj[i]` -- the one that came in, not the one going out -- that is fed + /// back into the buffer. + #[inline] + fn decrypt_bytes(&mut self, data: &mut [u8]) { + for byte in data.iter_mut() { + self.refill_if_used_up(); + // `I_{j+1} = C#_j` of the spec equations: the ciphertext segment is what is fed back. + // Feeding back the plaintext instead would still decrypt the first block correctly and + // nothing after it, which is why `cfb_tests.rs` checks exactly that. + let c = *byte; + *byte ^= self.buf[self.used]; + self.buf[self.used] = c; + self.used += 1; + } + } + + /// Encrypts one whole block at a segment boundary (`used == BLOCK_LEN`, so `buf` is `Ij`): + /// `Oj = CIPH_K(Ij)` in place, `Cj = Pj XOR Oj`, then `Cj` becomes `I_{j+1}` -- which leaves + /// `used == BLOCK_LEN` again, so consecutive calls need no bookkeeping. + #[inline] + fn encrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { + debug_assert_eq!(self.used, BLOCK_LEN, "the block path needs a segment boundary"); + self.perm.encrypt_block(&mut self.buf); + for (b, o) in block.iter_mut().zip(self.buf.iter()) { + *b ^= *o; + } + // I_{j+1} = Cj. Serial: this is the input to the next cipher call. + self.buf = *block; + } + + /// Decrypts one whole block at a segment boundary. `Cj` is overwritten by `Pj`, so it is copied + /// first to become `I_{j+1}`. + #[inline] + fn decrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { + debug_assert_eq!(self.used, BLOCK_LEN, "the block path needs a segment boundary"); + let cj = *block; + self.perm.encrypt_block(&mut self.buf); + for (b, o) in block.iter_mut().zip(self.buf.iter()) { + *b ^= *o; + } + self.buf = cj; + } + + /// Decrypts two consecutive blocks with one [`ElectronicCodeBook::encrypt_2blocks`] call. + /// + /// Writing the pair as `Cj, Cj+1` with `Ij` the incoming input block, the `s = b` equations + /// give + /// + /// ```text + /// Ij = buf Oj = CIPH_K(Ij) Pj = Cj XOR Oj + /// Ij+1 = Cj Oj+1 = CIPH_K(Ij+1) Pj+1 = Cj+1 XOR Oj+1 + /// ``` + /// + /// Both input blocks are known before either cipher call -- `Ij` is already held and `Ij+1` is + /// just `Cj`, which the caller supplied -- so the two forward ciphers are independent and + /// computing them together changes nothing. This is precisely the parallelism Sec 6.3 describes, + /// with the input blocks "first constructed (in series) from the IV and the ciphertext". + /// + /// In place: the two input blocks are the keystream buffer, so the ciphertext is never + /// overwritten before it has been read, and only `Cj+1` needs copying for the next input block. + #[inline] + fn decrypt_pair(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { + debug_assert_eq!(self.used, BLOCK_LEN, "the block path needs a segment boundary"); + // The two input blocks, constructed in series: Ij (already held) and Ij+1 (= Cj). + let mut o = [self.buf, blocks[0]]; + self.perm.encrypt_2blocks(&mut o); + + // I_{j+2} = Cj+1, read before the XOR below turns it into Pj+1. + self.buf = blocks[1]; + + for (block, o) in blocks.iter_mut().zip(o.iter()) { + for (b, o) in block.iter_mut().zip(o.iter()) { + *b ^= *o; + } + } + } + + /// Decrypts four consecutive blocks with one [`ElectronicCodeBook::encrypt_4blocks`] call. + /// + /// The same construction as [`Self::decrypt_pair`] widened to four: the input blocks are the + /// incoming input block followed by the first three ciphertext blocks, all known before any + /// cipher call, so the four forward ciphers are independent (Sec 6.3's parallel decryption). + /// `I_{j+4} = Cj+3` is read before the XOR turns it into `Pj+3`. + #[inline] + fn decrypt_four(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 4]) { + debug_assert_eq!(self.used, BLOCK_LEN, "the block path needs a segment boundary"); + let mut o = [self.buf, blocks[0], blocks[1], blocks[2]]; + self.perm.encrypt_4blocks(&mut o); + self.buf = blocks[3]; + for (block, o) in blocks.iter_mut().zip(o.iter()) { + for (b, o) in block.iter_mut().zip(o.iter()) { + *b ^= *o; + } + } + } + + /// Splits `data` into the bytes that complete the currently open segment (none, if a segment + /// boundary has been reached), the whole blocks that follow, and the short tail that opens the + /// final segment. After the head has been processed `used == BLOCK_LEN`, which is what the + /// block path requires; the tail is shorter than a block, so it opens at most one segment. + #[inline] + fn split<'a>( + &self, + data: &'a mut [u8], + ) -> (&'a mut [u8], &'a mut [[u8; BLOCK_LEN]], &'a mut [u8]) { + let head_len = core::cmp::min(BLOCK_LEN - self.used, data.len()); + let (head, rest) = data.split_at_mut(head_len); + let (blocks, tail) = rest.as_chunks_mut::(); + (head, blocks, tail) + } +} + +impl Algorithm + for Cfb +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; +} + +impl + SymmetricCipherEncryptor for Cfb +where + P: ElectronicCodeBook, +{ + /// Begins an encryption flow, generating the IV from the library's default OS-backed DRBG. + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + Self::do_encrypt_init_rng(key, &mut rng) + } + + /// As [`SymmetricCipherEncryptor::do_encrypt_init`], but takes the IV from the provided RNG. + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { + let perm = P::new(key)?; + // `I1 = IV`. + let iv = random_iv::(rng)?; + Ok((Self::start(perm, iv), iv)) + } + + /// Every input byte produces exactly one output byte. + fn do_encrypt_out_len(&self, input_len: usize) -> usize { + input_len + } + + /// See [`stream_update_out`]. + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + stream_update_out(plaintext, ciphertext, |data| self.do_encrypt_inplace(data)) + } + + /// See [`stream_do_final`]. + fn do_encrypt_final(self) -> Result<([u8; 0], usize), SymmetricCipherError> { + stream_do_final() + } + + /// A stream cipher never changes the length of its data. + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + } +} + +impl StreamCipherEncryptor + for Cfb +where + P: ElectronicCodeBook, +{ + /// Encrypts `data`, of any length, in place. + /// + /// Strictly serial: `Oj+1 = CIPH_K(Cj)` and `Cj` is the *output* of the previous cipher call, so + /// there is no pair path here; the block-aligned middle goes one cipher call per block, and + /// only the bytes that complete an open segment or open the final short one go singly. See the + /// module docs. Never fails: CFB has no per-IV data limit. + /// + /// Infallible -- cannot produce an error. + fn do_encrypt_inplace(&mut self, data: &mut [u8]) -> Result { + let len = data.len(); + let (head, blocks, tail) = self.split(data); + self.encrypt_bytes(head); + for block in blocks.iter_mut() { + self.encrypt_one(block); + } + self.encrypt_bytes(tail); + Ok(len) + } +} + +impl + SymmetricCipherDecryptor for Cfb +where + P: ElectronicCodeBook, +{ + /// Begins a decryption flow from the IV returned by + /// [`SymmetricCipherEncryptor::do_encrypt_init`]. + fn do_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; BLOCK_LEN], + ) -> Result { + let perm = P::new(key)?; + // `I1 = IV`, exactly as on the encrypt side. + Ok(Self::start(perm, *init_data)) + } + + /// Nothing is held back, so every input byte can be released immediately. + fn do_decrypt_out_len(&self, input_len: usize) -> usize { + input_len + } + + /// See [`stream_update_out`]. + fn do_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + stream_update_out(ciphertext, plaintext, |data| self.do_decrypt_inplace(data)) + } + + /// See [`stream_do_final`]. + fn do_decrypt_final(self) -> Result<([u8; 0], usize), SymmetricCipherError> { + stream_do_final() + } + + /// Exact rather than an upper bound: a stream cipher never changes the length of its data. + fn decrypt_out_len(ciphertext_len: usize) -> usize { + ciphertext_len + } +} + +impl StreamCipherDecryptor + for Cfb +where + P: ElectronicCodeBook, +{ + /// Decrypts `data`, of any length, in place. + /// + /// Walks the block-aligned middle in fours through the permutation's *forward* four-block + /// path, then in pairs through its forward pair path, then the remaining block singly. + /// `as_chunks_mut` splits into exactly those shapes with no runtime length check and no + /// indexing arithmetic. The bytes that complete an open segment, and the final short segment, + /// go singly. Never fails: CFB has no per-IV data limit. + fn do_decrypt_inplace(&mut self, data: &mut [u8]) -> Result { + let len = data.len(); + let (head, blocks, tail) = self.split(data); + self.decrypt_bytes(head); + let (fours, rest) = blocks.as_chunks_mut::<4>(); + for four in fours.iter_mut() { + self.decrypt_four(four); + } + let (pairs, single) = rest.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.decrypt_pair(pair); + } + for block in single.iter_mut() { + self.decrypt_one(block); + } + self.decrypt_bytes(tail); + Ok(len) + } +} + +/// The suspended state is `buf` and `used` -- the open segment, which is public ciphertext and +/// the keystream not yet used against it -- in both directions; the permutation is rebuilt from +/// the re-supplied key. See [`bouncycastle_utils::suspendable_state`]. +impl SuspendableComponent + for Cfb +where + P: ElectronicCodeBook, +{ + const STATE_LEN: usize = BLOCK_LEN + 8; + type Key = KeyMaterial; + + fn write_state(&self, out: &mut [u8]) { + let mut w = CursorMut::new(out); + w.bytes(&self.buf); + w.u64(self.used as u64); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + let mut r = Cursor::new(state); + let buf = r.array::(); + // `used` is `0..=BLOCK_LEN`; anything past the buffer would index out of it. + let used = bounded_usize(r.u64(), BLOCK_LEN)?; + debug_assert!(r.is_done()); + Ok(Self { perm, buf, used, _dir: PhantomData }) + } +} + +/// `N` must be [`Cfb::SUSPENDED_STATE_LEN`]; anything else is a compile error. +impl SuspendableKeyed + for Cfb +where + P: ElectronicCodeBook, +{ + type Key = KeyMaterial; + + fn suspend(self) -> [u8; N] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + resume_component(&state, key) + } +} diff --git a/crypto/cipher/src/modes/cfb8.rs b/crypto/cipher/src/modes/cfb8.rs new file mode 100644 index 00000000..fe52daf0 --- /dev/null +++ b/crypto/cipher/src/modes/cfb8.rs @@ -0,0 +1,364 @@ +//! The Cipher Feedback mode of operation (NIST SP 800-38A Sec 6.3), 8-bit segment. +//! +//! CFB and CBF8 are the same construction at two segment sizes, resulting in different, +//! non-interoperable modes. See [`cfb`] for the primary docs on this mode. +//! +//! The difference is the segment size `s` of Sec 6.3. `Cfb` uses `s = b`: each cipher call yields a +//! whole block of keystream, and the next input block is simply the previous ciphertext block. +//! `Cfb8` uses `s = 8` bits: each call to the underlying block permutation yields one keystream byte, +//! the other `b - 8` are discarded, and the input block is a shift register. NIST SP 800-38A +//! Sec 6.3: +//! +//! > "the bits of the first input block circularly shift s positions to the left, and then the +//! > ciphertext segment replaces the s least significant bits of the result". +//! +//! The smaller segment is not a security gain, but it makes the mode self-synchronising +//! at byte granularity: after a dropped or inserted byte the shift register refills from ciphertext +//! and decryption recovers `b/s` bytes later on its own, where `Cfb` and every other mode need the +//! alignment "restored externally". +//! +//! # One cipher call per byte +//! +//! Using only one byte from each invocation of the underlying block cipher dramatically reduces +//! performance, so on a typical 16-byte block cipher it does **16 times** the cipher work of +//! [`cfb`] for the same data. That is inherent to the mode, not to this implementation. +//! +//! # Suspending and resuming execution +//! +//! [`Cfb8`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte +//! array and resumed later with the re-supplied key. The state is the shift register; the +//! permutation is rebuilt from the key. The array length is `Cfb8::SUSPENDED_STATE_LEN`; see [the +//! crate docs](crate#suspending-and-resuming-execution) for an example. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! CFB and CFB8 largely share their security considerations, with only a few differences. +//! Therefore, everything in the Security Considerations of [`crate::modes::cfb`] applies here as well. +//! +//! ## Increased attack precision +//! +//! In CFB, the security implications happen at a block granularity, whereas in CFB8 they happen at +//! a byte granularity. +//! This means, for example, key and IV reuse now tells a passive attacker at which exact byte +//! two messages begin to differ. +//! +//! ## Self-synchronization cuts both ways +//! +//! The self-synchronization property, while providing great robustness, also allows attackers to +//! drop or insert content, including content taken from other messages under the same key. This +//! results in `b/s` bytes of garbage (16 with AES) and then a fully recovered plaintext stream +//! thereafter, possibly now decrypting a different document than the one before the cut. +//! This means that, for example, a malicious cut right before a long random number, such as an account +//! number or ID number, could still yield a syntactically-correct message and therefore be completely +//! undetectable. + +use crate::modes::iv::random_iv; +use crate::stream::{stream_do_final, stream_update_out}; +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::suspendable_state::{ + LIB_VERSION_LEN, SuspendableComponent, resume_component, suspend_component, +}; +use core::marker::PhantomData; + +// Imports needed for docs +#[allow(unused_imports)] +use crate::modes::cfb; +// End imports needed for docs + +/// CFB8 mode over any [`ElectronicCodeBook`], with the direction encoded in the type. +/// +/// The segment size is one byte (`s = 8`); see the module docs, and note that this is **not** +/// interoperable with [`cfb`], which is `s = b`. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`]. [`StreamCipherEncryptor`] is implemented only for the +/// former and [`StreamCipherDecryptor`] only for the latter, so a `Cfb8<_, Encrypting, _, _>` has +/// no decryption methods at all -- using one in the wrong direction is a compile error rather than +/// a runtime check. +/// +/// The initialization data is one block, so `INIT_DATA_LEN == BLOCK_LEN`. +/// +/// # State +/// +/// Two fields, the same size as `Cbc`: the permutation (which owns the key schedule, and is +/// responsible for keeping it in a zeroize-on-drop wrapper) and one block holding the shift +/// register `Ij`. `Ij` is built from the IV and ciphertext bytes, both of which are public, so it +/// is deliberately not wrapped in a `Secret`. +/// +/// Note what is *not* stored: the output block `Oj`. It is recomputed from the register on each +/// byte and lives only in a local, so no keystream outlives the call that used it. No partial +/// segment is stored either, because a segment is one byte. +#[derive(Clone)] +pub struct Cfb8 +where + P: ElectronicCodeBook, +{ + perm: P, + /// `Ij`: the IV, then the shift register. See the module docs. + chain: [u8; BLOCK_LEN], + _dir: PhantomData, +} + +impl Cfb8 +where + P: ElectronicCodeBook, +{ + /// The `N` of this type's [`SuspendableKeyed`] impl: the version header and the shift + /// register. See [`bouncycastle_utils::suspendable_state`]. + pub const SUSPENDED_STATE_LEN: usize = LIB_VERSION_LEN + BLOCK_LEN; + + /// `I_{j+1} = LSB_{b-8}(Ij) | Cj`: shift the register one byte left and put the ciphertext byte + /// in the least significant position. + /// + /// This is Sec 6.3's alternative description verbatim -- "the bits of the first input block + /// circularly shift s positions to the left, and then the ciphertext segment replaces the s + /// least significant bits of the result" -- so the rotate is the spec's rotate, and overwriting + /// the last byte is what discards the byte the rotate carried round. + #[inline] + fn shift_in(&mut self, ciphertext_byte: u8) { + self.chain.rotate_left(1); + // BLOCK_LEN is non-zero for any permutation: a zero-length block has no cipher. + self.chain[BLOCK_LEN - 1] = ciphertext_byte; + } + + /// `MSB_8(Oj)`, the one keystream byte this segment uses: `Oj = CIPH_K(Ij)`, first byte kept, + /// the other `b - 8` discarded as Sec 6.3 requires. + /// + /// The forward cipher function, in both directions -- see the module docs. + #[inline] + fn keystream_byte(&self) -> u8 { + let mut o = self.chain; + self.perm.encrypt_block(&mut o); + o[0] + } + + /// Decrypts `N` consecutive bytes with one batched forward-cipher call. + /// + /// The input blocks are built in series first -- each is the previous one shifted with the + /// previous *ciphertext* byte appended, which decryption already has -- so the `N` forward + /// ciphers are independent. This is precisely the parallelism Sec 6.3 describes, with the input + /// blocks "first constructed (in series) from the IV and the ciphertext". + /// + /// `batch` is the permutation's `N`-block method; the scratch array holds the input blocks on + /// the way in and the output blocks on the way out. + #[inline] + fn decrypt_batch( + &mut self, + bytes: &mut [u8; N], + batch: impl Fn(&P, &mut [[u8; BLOCK_LEN]; N]), + ) { + let mut blocks = [[0u8; BLOCK_LEN]; N]; + for (block, c) in blocks.iter_mut().zip(bytes.iter()) { + *block = self.chain; + // I_{j+1} = LSB(Ij) | C#_j: the ciphertext byte is what is fed back, and on this side + // it is the byte that came in, before the XOR below turns it into plaintext. + self.shift_in(*c); + } + batch(&self.perm, &mut blocks); + for (byte, o) in bytes.iter_mut().zip(blocks.iter()) { + *byte ^= o[0]; + } + } +} + +impl Algorithm + for Cfb8 +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; +} + +impl + SymmetricCipherEncryptor for Cfb8 +where + P: ElectronicCodeBook, +{ + /// Begins an encryption flow, generating the IV from the library's default OS-backed DRBG. + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + Self::do_encrypt_init_rng(key, &mut rng) + } + + /// As [`SymmetricCipherEncryptor::do_encrypt_init`], but takes the IV from the provided RNG. + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; BLOCK_LEN]), SymmetricCipherError> { + let perm = P::new(key)?; + // `I1 = IV`. + let iv = random_iv::(rng)?; + Ok((Self { perm, chain: iv, _dir: PhantomData }, iv)) + } + + /// Every input byte produces exactly one output byte. + fn do_encrypt_out_len(&self, input_len: usize) -> usize { + input_len + } + + /// See [`stream_update_out`]. + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + stream_update_out(plaintext, ciphertext, |data| self.do_encrypt_inplace(data)) + } + + /// See [`stream_do_final`]. + fn do_encrypt_final(self) -> Result<([u8; 0], usize), SymmetricCipherError> { + stream_do_final() + } + + /// A stream cipher never changes the length of its data. + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + } +} + +impl StreamCipherEncryptor + for Cfb8 +where + P: ElectronicCodeBook, +{ + /// Encrypts `data`, of any length, in place: `Cj = Pj XOR MSB_8(CIPH_K(Ij))` for each byte, + /// then `Cj` shifts into the register. + /// + /// Strictly serial, one forward cipher per byte: `I_{j+1}` needs `Cj`, which is the result of + /// the XOR that the cipher call produced. See the module docs. Never fails: CFB has no per-IV + /// data limit. + fn do_encrypt_inplace(&mut self, data: &mut [u8]) -> Result { + for byte in data.iter_mut() { + *byte ^= self.keystream_byte(); + self.shift_in(*byte); + } + Ok(data.len()) + } +} + +impl + SymmetricCipherDecryptor for Cfb8 +where + P: ElectronicCodeBook, +{ + /// Begins a decryption flow from the IV returned by + /// [`SymmetricCipherEncryptor::do_encrypt_init`]. + fn do_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; BLOCK_LEN], + ) -> Result { + let perm = P::new(key)?; + // `I1 = IV`, exactly as on the encrypt side. + Ok(Self { perm, chain: *init_data, _dir: PhantomData }) + } + + /// Nothing is held back, so every input byte can be released immediately. + fn do_decrypt_out_len(&self, input_len: usize) -> usize { + input_len + } + + /// See [`stream_update_out`]. + fn do_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + stream_update_out(ciphertext, plaintext, |data| self.do_decrypt_inplace(data)) + } + + /// See [`stream_do_final`]. + fn do_decrypt_final(self) -> Result<([u8; 0], usize), SymmetricCipherError> { + stream_do_final() + } + + /// Exact rather than an upper bound: a stream cipher never changes the length of its data. + fn decrypt_out_len(ciphertext_len: usize) -> usize { + ciphertext_len + } +} + +impl StreamCipherDecryptor + for Cfb8 +where + P: ElectronicCodeBook, +{ + /// Decrypts `data`, of any length, in place: `Pj = Cj XOR MSB_8(CIPH_K(Ij))` for each byte, + /// with the *ciphertext* byte -- the one that came in, not the plaintext going out -- shifted + /// into the register. + /// + /// Walks the data in fours through the permutation's *forward* four-block path, then in pairs + /// through its forward pair path, then the remaining bytes singly (Sec 6.3's parallel + /// decryption; see the module docs). Never fails: CFB has no per-IV data limit. + fn do_decrypt_inplace(&mut self, data: &mut [u8]) -> Result { + let len = data.len(); + let (fours, rest) = data.as_chunks_mut::<4>(); + for four in fours.iter_mut() { + self.decrypt_batch(four, P::encrypt_4blocks); + } + let (pairs, tail) = rest.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.decrypt_batch(pair, P::encrypt_2blocks); + } + for byte in tail.iter_mut() { + let c = *byte; + *byte ^= self.keystream_byte(); + self.shift_in(c); + } + Ok(len) + } +} + +/// The suspended state is the shift register `Ij`, in both directions; the permutation is +/// rebuilt from the re-supplied key. See [`bouncycastle_utils::suspendable_state`]. +impl SuspendableComponent + for Cfb8 +where + P: ElectronicCodeBook, +{ + const STATE_LEN: usize = BLOCK_LEN; + type Key = KeyMaterial; + + fn write_state(&self, out: &mut [u8]) { + out.copy_from_slice(&self.chain); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + let mut chain = [0u8; BLOCK_LEN]; + chain.copy_from_slice(state); + Ok(Self { perm, chain, _dir: PhantomData }) + } +} + +/// `N` must be [`Cfb8::SUSPENDED_STATE_LEN`]; anything else is a compile error. +impl SuspendableKeyed + for Cfb8 +where + P: ElectronicCodeBook, +{ + type Key = KeyMaterial; + + fn suspend(self) -> [u8; N] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + resume_component(&state, key) + } +} diff --git a/crypto/cipher/src/modes/ctr.rs b/crypto/cipher/src/modes/ctr.rs new file mode 100644 index 00000000..cf1460de --- /dev/null +++ b/crypto/cipher/src/modes/ctr.rs @@ -0,0 +1,164 @@ +//! The Counter mode of operation (NIST SP 800-38A §6.5). +//! +//! CTR mode (SP 800-38A Sec 6.5) applies the forward cipher to a sequence of counter blocks T1, T2, …, Tn +//! and XORs the resulting output blocks with the plaintext, so encryption and decryption are the same +//! operation, every block can be computed in parallel or ahead of time, and the last block may be +//! partial with no padding. This makes it a stream cipher. +//! +//! # The counter is finite, and running out is an error +//! +//! A `CTR_LEN`-byte counter has `2^(8 * CTR_LEN)` distinct values, so a message can be at most +//! that many blocks: 2^32 blocks (64 GiB) for a 4-byte counter, down to 256 blocks (4 KiB) for a +//! 1-byte one. SP 800-38A Appendix B.1 is explicit that this is the bound -- counter blocks "satisfy the +//! uniqueness requirement within the given message provided that `n <= 2^m`" -- and past it the +//! counter would repeat, which for a keystream mode means reusing keystream: the two-time-pad +//! failure, within a single message. +//! +//! So [`Ctr`] **refuses** rather than wraps. [`CtrKeyStream`] reports how many counter values are +//! left, and a call that would need more keystream than that returns +//! [`SymmetricCipherError::DataLimitExceeded`] and consumes nothing -- [`StreamCipher`] makes the check up +//! front, against the whole call, so a message is never half-encrypted before the mode notices. +//! +//! # Everything is parallel +//! +//! Sec 6.5: "In both CTR encryption and CTR decryption, the forward cipher functions can be +//! performed in parallel". Counter blocks depend on nothing but the nonce and the index, so unlike +//! CBC and CFB there is no feed-forward between blocks at all. +//! As such, both directions walk the block-aligned part of the data in fours through +//! [`ElectronicCodeBook::encrypt_4blocks`], then in pairs through [`ElectronicCodeBook::encrypt_2blocks`]. +//! Only a leftover single block, and the keystream block for a short tail at the end, go one block at a time. +//! +//! Like the rest of CFB and CTR, only the **forward** cipher function is ever used, in both +//! directions, so a permutation that implements only `encrypt_block` works here. +//! +//! # Suspending and resuming execution +//! +//! [`Ctr`] implements [`SuspendableKeyed`](bouncycastle_core::traits::SuspendableKeyed), so a +//! message in progress can be suspended to a byte array and resumed later with the re-supplied key. +//! The state is the nonce, the next counter value and the partly used keystream block; the +//! permutation is rebuilt from the key. The array length is `Ctr::SUSPENDED_STATE_LEN`; see [the +//! crate docs](crate#suspending-and-resuming-execution) for an example. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! The one security requirement of CTR mode is that every counter block be distinct across all +//! messages ever encrypted under a key, since: +//! +//! > "if any plaintext block that is encrypted using a given counter block is known, then the output +//! > of the forward cipher function can be determined easily from the associated ciphertext block" +//! +//! and used to recover any other plaintext encrypted under that same counter. That is why [`Ctr`] +//! refuses to overflow the counter and returns a [`SymmetricCipherError::DataLimitExceeded`] +//! instead, and why the nonce is generated rather than accepted from the caller: within one +//! message the counter cannot repeat, and across messages a fresh random nonce is what keeps the +//! counter blocks distinct. + +use crate::modes::hazmat::CtrKeyStream; +use crate::stream::StreamCipher; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_utils::secret::Secret; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::errors::SymmetricCipherError; +#[allow(unused_imports)] +use bouncycastle_core::traits::{StreamCipherDecryptor, StreamCipherEncryptor}; +// end of imports needed for docs + +/// CTR mode over any [`ElectronicCodeBook`] permutation function, with the direction encoded in the type: the +/// [`CtrKeyStream`] wrapped in a [`StreamCipher`]. +/// +/// The counter block is the init data (the nonce) followed by a counter filling the rest of the +/// block, so `INIT_DATA_LEN` chooses the counter length; see the module docs. `Dir` is +/// [`Encrypting`](crate::Encrypting) or [`Decrypting`](crate::Decrypting). +/// +/// # The counter width is checked at compile time +/// +/// The counter must be at least one byte and at most four, so on a 16-byte block the nonce is 12, +/// 13, 14 or 15 bytes. Both bounds are inline `const` assertions in the constructors, so a nonce +/// length outside that range is a **compile** error at the call site rather than a runtime `Err`. +/// +/// The permitted lengths all work: +/// +/// ``` +/// use bouncycastle_core_test_framework::ToyBlockCipher; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::SymmetricCipherEncryptor; +/// use bouncycastle_cipher::modes::Ctr; +/// use bouncycastle_cipher::Encrypting; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// let _ = Ctr::::do_encrypt_init(&key).unwrap(); // 4-byte counter +/// let _ = Ctr::::do_encrypt_init(&key).unwrap(); // 1-byte counter +/// ``` +pub type Ctr = + StreamCipher< + CtrKeyStream, + Dir, + KEY_LEN, + INIT_DATA_LEN, + BLOCK_LEN, + >; + +/// XORs `Oj = CIPH_K(Tj)` into `blocks` for the next `blocks.len()` counter values, starting at +/// `*next` and advancing it past them, where `Tj = counter_block(j)`. +/// +/// Shared by [`CtrKeyStream`] and CCM's keystream (SP 800-38C Sec 6.1 steps 5-7), which differ +/// only in how a counter block is formatted. Walks the blocks in fours through +/// [`ElectronicCodeBook::encrypt_4blocks`], then pairs, then a single block: the counter blocks +/// depend only on `j`, not on the data or on each other's cipher output, so the forward ciphers in +/// a batch are independent. This is the parallelism SP 800-38A Sec 6.5 describes, and it applies +/// to both directions. +/// +/// The keystream scratch is one [`Secret`] per width and per call rather than per batch, so every +/// block of `Oj` is zeroized when this returns. +pub(crate) fn apply_counter_blocks( + perm: &P, + next: &mut u64, + counter_block: impl Fn(u64) -> [u8; BLOCK_LEN], + blocks: &mut [[u8; BLOCK_LEN]], +) where + P: ElectronicCodeBook, +{ + let (fours, rest) = blocks.as_chunks_mut::<4>(); + let mut ks4: Secret<[[u8; BLOCK_LEN]; 4]> = Secret::new(); + for four in fours.iter_mut() { + apply_batch(perm, next, &counter_block, four, &mut ks4, P::encrypt_4blocks); + } + let (pairs, single) = rest.as_chunks_mut::<2>(); + let mut ks2: Secret<[[u8; BLOCK_LEN]; 2]> = Secret::new(); + for pair in pairs.iter_mut() { + apply_batch(perm, next, &counter_block, pair, &mut ks2, P::encrypt_2blocks); + } + let mut ks1: Secret<[[u8; BLOCK_LEN]; 1]> = Secret::new(); + for block in single.iter_mut() { + apply_batch(perm, next, &counter_block, core::array::from_mut(block), &mut ks1, |p, b| { + p.encrypt_block(&mut b[0]) + }); + } +} + +/// One batch of [`apply_counter_blocks`]: builds `N` counter blocks into `keystream`, encrypts +/// them with one `batch` call, and XORs the result into `blocks`. +#[inline] +fn apply_batch( + perm: &P, + next: &mut u64, + counter_block: &impl Fn(u64) -> [u8; BLOCK_LEN], + blocks: &mut [[u8; BLOCK_LEN]; N], + keystream: &mut [[u8; BLOCK_LEN]; N], + batch: impl Fn(&P, &mut [[u8; BLOCK_LEN]; N]), +) where + P: ElectronicCodeBook, +{ + for slot in keystream.iter_mut() { + *slot = counter_block(*next); + *next += 1; + } + batch(perm, keystream); + for (block, o) in blocks.iter_mut().zip(keystream.iter()) { + for (b, o) in block.iter_mut().zip(o.iter()) { + *b ^= *o; + } + } +} diff --git a/crypto/cipher/src/modes/gcm.rs b/crypto/cipher/src/modes/gcm.rs new file mode 100644 index 00000000..b00551b6 --- /dev/null +++ b/crypto/cipher/src/modes/gcm.rs @@ -0,0 +1,802 @@ +//! Galois/Counter Mode (NIST SP 800-38D), the authenticated encryption mode built from CTR +//! and the GHASH universal hash. +//! +//! # Nonce and Tag +//! +//! [`Gcm`] fixes the nonce at 96 bits (12 bytes, [`GCM_NONCE_LEN`]): SP 800-38D Sec 5.2.1.1 +//! recommends that implementations "restrict support to the length of 96 bits", and the other IV +//! lengths are not implemented. The nonce is never taken from the caller: instead +//! [`SymmetricCipherEncryptor::do_encrypt_init`] and [`SymmetricCipherEncryptor::do_encrypt_init_rng`] +//! draw it from the default OS RNG or the provided RNG, respectively. +//! +//! 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. +//! +//! # Usage Examples +//! +//! [`Gcm`] is used through [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`], with +//! `FINAL_LEN = TAG_LEN`, and through the [`SymmetricCipherEncryptor`] / +//! [`SymmetricCipherDecryptor`] traits they extend: +//! +//! Used through the SymmetricCipher traits, there is no option to include additional associated data (aad), +//! and the tag is inlined into the ciphertext as `ciphertext || tag`. +//! +//! ``` +//! use bouncycastle_core_test_framework::ToyBlockCipher; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +//! use bouncycastle_core::errors::SymmetricCipherError; +//! use bouncycastle_cipher::modes::Gcm; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! type ToyGcm = Gcm; +//! +//! let key = KeyMaterial128::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: [u8; 16] = *b"attack at dawn!!"; +//! +//! let (nonce, ciphertext) = ToyGcm::::encrypt(&key, &plaintext).expect("encrypt"); +//! +//! let mut recovered = ToyGcm::::decrypt(&key, &nonce, &ciphertext).expect("decrypt"); +//! assert_eq!(recovered, plaintext); +//! +//! // A tampered ciphertext will be caught by the tag +//! let mut tampered_ct = ciphertext.clone(); +//! tampered_ct[1] ^= 0xFF; +//! match ToyGcm::::decrypt(&key, &nonce, &tampered_ct).unwrap_err() { +//! SymmetricCipherError::AEADTagCheckFailed => { /* good */ } +//! _ => { panic!() } +//! } +//! ``` +//! +//! The AEADCipher traits provide the AEAD-specific functionality, including accepting the aad, and +//! the `_detached()` methods handle the tag separately, instead of inlined into the ciphertext. +//! +//! ``` +//! use bouncycastle_core_test_framework::ToyBlockCipher; +//! use bouncycastle_core::key_material::{KeyMaterial128, KeyType}; +//! use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; +//! use bouncycastle_cipher::modes::Gcm; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! type ToyGcm = Gcm; +//! +//! let key = KeyMaterial128::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, _bytes_written, tag) = +//! ToyGcm::::encrypt_detached_out(&key, aad, &plaintext, &mut ciphertext).unwrap(); +//! +//! let mut recovered = [0u8; 16]; +//! ToyGcm::::decrypt_detached_out(&key, &nonce, aad, &ciphertext, &tag, &mut recovered) +//! .unwrap(); +//! assert_eq!(recovered, plaintext); +//! ``` +//! +//! There is also a streaming mode. +//! Note that the aad must be supplied before any plaintext or ciphertext; attempting to call +//! `do_update_aad()` after a `do_encrypt()` will result in a [`SymmetricCipherError::StateError`]. +//! +//! ``` +//! use bouncycastle_core_test_framework::ToyBlockCipher; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{ +//! AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +//! }; +//! use bouncycastle_cipher::modes::Gcm; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! type ToyGcm = Gcm; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x07; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let aad = b"some associated data"; +//! let message = b"a message that streams in over more than one call"; +//! +//! let (mut enc, nonce) = ToyGcm::::do_encrypt_init(&key).unwrap(); +//! enc.do_update_aad(aad).unwrap(); +//! let mut ct = vec![0u8; message.len()]; +//! enc.do_encrypt_out(message, &mut ct).unwrap(); +//! let (tag_block, tag_len) = enc.do_encrypt_final().unwrap(); +//! ct.extend_from_slice(&tag_block[..tag_len]); +//! +//! let mut dec = ToyGcm::::do_decrypt_init(&key, &nonce).unwrap(); +//! dec.do_update_aad(aad).unwrap(); +//! let mut pt = vec![0u8; ct.len()]; +//! let written = dec.do_decrypt_out(&ct, &mut pt).unwrap(); +//! let (_last, last_len) = dec.do_decrypt_final().unwrap(); +//! pt.truncate(written + last_len); +//! assert_eq!(pt, message); +//! ``` +//! +//! # Suspending and resuming execution +//! +//! [`Gcm`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte +//! array and resumed later with the re-supplied key. The state is the CTR half, the running GHASH, +//! the byte counts and whatever a decryptor is holding back as a possible tag; `H` and the tag mask +//! are re-derived from the key. The array length is `Gcm::SUSPENDED_STATE_LEN`; see [the crate +//! docs](crate#suspending-and-resuming-execution) for an example. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! ## Nonce uniqueness +//! +//! As with all symmetric cipher modes, repeated key and nonce for multiple messages is catastrophic +//! for security, which is why the nonce is always drawn from the library's default RNG or a provided +//! RNG and never accepted from the caller. +//! +//! ## Invocation limit +//! +//! NIST SP 800-38D Sec 8.3: +//! +//! > "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." +//! +//! ## Streaming decryption releases plaintext before the tag is checked +//! +//! [`SymmetricCipherDecryptor::do_decrypt_out`] hands back plaintext as it goes, which is +//! unauthenticated until the tag has been checked after the final block. +//! It is the application's responsibility not to take any action on the decrypted plaintext until +//! the end of the ciphertext has been reached, and the `do_decrypt_final` / +//! `do_decrypt_final_detachedtag` succeeds. +//! +//! The one-shots (`decrypt_out`, `decrypt_detached_out`, `decrypt_with_aad_out`) verify the +//! tag first and release nothing on failure, making them more robust. +//! +//! * **GMAC is GCM with no plaintext** (Sec 5.2): feed only AAD and call +//! `do_encrypt_final_detachedtag`: there is no separate `Gmac` type. + +use crate::modes::Ctr; +use crate::modes::ghash::{GHASH_STATE_LEN, Ghash}; +use crate::modes::hazmat::CtrKeyStream; +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, Algorithm, RNG, StreamCipherDecryptor, + StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, + suspend_component, +}; +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 = 0, + Data = 1, +} + +impl Phase { + /// The inverse of `as u8`, for a suspended state; anything but the two values is refused. + fn from_u8(v: u8) -> Result { + match v { + 0 => Ok(Phase::Aad), + 1 => Ok(Phase::Data), + _ => Err(SuspendableError::InvalidData), + } + } +} + +/// 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. +#[derive(Clone)] +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_decrypt_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 `N` of this type's [`SuspendableKeyed`] impl: the version header, the CTR state, + /// the GHASH state, the two byte counts, the phase, the held-back tail and its length. See + /// [`bouncycastle_utils::suspendable_state`]. + pub const SUSPENDED_STATE_LEN: usize = + LIB_VERSION_LEN + ::STATE_LEN; + + /// The CTR half's share of the suspended state. + const CTR_STATE_LEN: usize = + as SuspendableComponent>::STATE_LEN; + + /// 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::from_keystream(CtrKeyStream::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::DataLimitExceeded`] 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 + /// [`SymmetricCipherError::StateError`] 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_inplace(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::modes::iv::random_iv::(rng)?; + Ok((Self::setup(perm, nonce), nonce)) + } + + /// The identity: GCM's encryptor holds nothing back. + fn do_encrypt_out_len(&self, input_len: usize) -> usize { + input_len + } + + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + 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_encrypt_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; nothing is held back, so `ciphertext` is left zeroed. + fn do_encrypt_final_detachedtag_out( + self, + ciphertext: &mut [u8; TAG_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext.fill(0); + 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_inplace(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_detached_out`, + /// `decrypt_with_aad_out`): 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. The preamble of Sec 7 + /// explicitly permits this: "in Algorithm 5, the verification of the tag may precede the + /// computation of 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_inplace(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 do_decrypt_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_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + let release = self.do_decrypt_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_decrypt_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_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 { + plaintext.fill(0); + >::decrypt_with_aad_out( + 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_decrypt_final_detachedtag_out( + mut self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; TAG_LEN], + ) -> Result { + plaintext.fill(0); + 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_detached_out( + key: &KeyMaterial, + nonce: &[u8; GCM_NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + 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_with_aad_out( + key: &KeyMaterial, + nonce: &[u8; GCM_NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + let needed = Self::decrypt_out_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) + } +} + +/// The suspended state is the CTR half, the running GHASH, the two byte counts, the phase, and +/// the up-to-`TAG_LEN` bytes a decryptor holds back. `H` and `CIPH_K(J0)` are not in it: both +/// derive from the key and the nonce, and `setup` re-derives them on resume. See +/// [`bouncycastle_utils::suspendable_state`]. +impl SuspendableComponent + for Gcm +where + P: ElectronicCodeBook, +{ + const STATE_LEN: usize = Self::CTR_STATE_LEN + GHASH_STATE_LEN + 8 + 8 + 1 + TAG_LEN + 8; + type Key = KeyMaterial; + + fn write_state(&self, out: &mut [u8]) { + let (ctr, rest) = out.split_at_mut(Self::CTR_STATE_LEN); + self.ctr.write_state(ctr); + let (ghash, rest) = rest.split_at_mut(GHASH_STATE_LEN); + self.ghash.write_state(ghash); + let mut w = CursorMut::new(rest); + w.u64(self.aad_len); + w.u64(self.data_len); + w.u8(self.phase as u8); + w.bytes(&*self.tail); + w.u64(self.tail_len as u64); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + Self::check_shape(); + let (ctr, rest) = state.split_at(Self::CTR_STATE_LEN); + let (ghash, rest) = rest.split_at(GHASH_STATE_LEN); + + // `setup` re-derives `H` and `CIPH_K(J0)` from the key and the nonce, which is the + // leading part of the CTR state. Its fresh CTR and GHASH are then replaced by the + // suspended ones; the CTR read expands the key a second time, a one-off cost at resume. + let nonce = CtrKeyStream::::nonce_from_state(ctr); + let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + let mut gcm = Self::setup(perm, nonce); + gcm.ctr = as SuspendableComponent>::read_state( + ctr, key, + )?; + gcm.ghash.restore_state(ghash)?; + + let mut r = Cursor::new(rest); + gcm.aad_len = r.u64(); + gcm.data_len = r.u64(); + gcm.phase = Phase::from_u8(r.u8())?; + (*gcm.tail).copy_from_slice(r.bytes(TAG_LEN)); + gcm.tail_len = bounded_usize(r.u64(), TAG_LEN)?; + debug_assert!(r.is_done()); + Ok(gcm) + } +} + +/// `N` must be [`Gcm::SUSPENDED_STATE_LEN`]; anything else is a compile error. +impl SuspendableKeyed + for Gcm +where + P: ElectronicCodeBook, +{ + type Key = KeyMaterial; + + fn suspend(self) -> [u8; N] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + resume_component(&state, key) + } +} diff --git a/crypto/cipher/src/modes/ghash.rs b/crypto/cipher/src/modes/ghash.rs new file mode 100644 index 00000000..d93550dc --- /dev/null +++ b/crypto/cipher/src/modes/ghash.rs @@ -0,0 +1,460 @@ +//! 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 new cryptographic code `gcm.rs` needs; everything else there is +//! plumbing around this and [`crate::modes::Ctr`]. +//! +//! This is placed here and not exposed as a top-level Hash function because it is only used by the +//! GCM block cipher mode, and not as a standalone hash function. +//! +//! # 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. + +use bouncycastle_core::errors::SuspendableError; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{Cursor, CursorMut, bounded_usize}; + +/// 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. +/// Bytes [`Ghash::write_state`] writes: `Y` as two `u64`s, the pending block and its length. +pub(crate) const GHASH_STATE_LEN: usize = 16 + 16 + 8; + +#[derive(Clone)] +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()); + } +} + +impl Ghash { + /// Writes the running state -- `Y`, the pending partial block and its length -- into `out`, + /// exactly [`GHASH_STATE_LEN`] bytes. `H` is not written: it is `CIPH_K(0^128)`, which the + /// resuming side re-derives from the re-supplied key, so the state carries one secret fewer. + pub(crate) fn write_state(&self, out: &mut [u8]) { + let mut w = CursorMut::new(out); + w.u64(self.y[0]); + w.u64(self.y[1]); + w.bytes(&*self.pending); + w.u64(self.pending_len as u64); + debug_assert!(w.is_done()); + } + + /// The inverse of [`Self::write_state`], over a `Ghash` freshly built from `H` by + /// [`Self::new`]: replaces `Y` and the pending block. + /// + /// # Errors + /// [`SuspendableError::InvalidData`] if the pending length is a whole block or more: `update` + /// absorbs whole blocks immediately, so a legitimate state never holds one. + pub(crate) fn restore_state(&mut self, state: &[u8]) -> Result<(), SuspendableError> { + let mut r = Cursor::new(state); + self.y[0] = r.u64(); + self.y[1] = r.u64(); + (*self.pending).copy_from_slice(r.bytes(16)); + self.pending_len = bounded_usize(r.u64(), 15)?; + debug_assert!(r.is_done()); + Ok(()) + } +} + +#[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/cipher/src/modes/hazmat/ctr_key_stream.rs b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs new file mode 100644 index 00000000..588581f6 --- /dev/null +++ b/crypto/cipher/src/modes/hazmat/ctr_key_stream.rs @@ -0,0 +1,261 @@ +//! The CTR keystream, [`CtrKeyStream`]: a raw [`KeyStream`], used through [`Ctr`]. + +use crate::modes::ctr::apply_counter_blocks; +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; +use bouncycastle_core::hazmat::{ElectronicCodeBook, KeyStream}; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; +use bouncycastle_utils::suspendable_state::{Cursor, CursorMut, SuspendableComponent}; + +// Imports needed for docs +#[allow(unused_imports)] +use crate::modes::Ctr; +#[allow(unused_imports)] +use crate::stream::StreamCipher; +// end of imports needed for docs + +/// The CTR keystream `Oj = CIPH_K(Tj)` over any [`ElectronicCodeBook`], with `Tj = N | [j]m`; +/// see the module docs. Use it through [`Ctr`]. +/// +/// # 🚨 Security Considerations 🚨 +/// A raw [`KeyStream`]: constructed directly, it takes the nonce from the caller and does not +/// refuse to run past the counter. See [`KeyStream`]'s security notes and +/// [`bouncycastle_core::hazmat`] for the supported uses. +/// +/// # State +/// +/// The permutation, the nonce and the next counter value. The nonce and the counter are both +/// public, so they are plain fields; no keystream is kept between calls. +#[derive(Clone)] +pub struct CtrKeyStream +where + P: ElectronicCodeBook, +{ + perm: P, + /// `N`: the message nonce, the leading bytes of every counter block. + nonce: [u8; INIT_DATA_LEN], + /// The counter of the *next* block to use, as an integer: `Tj = N | [next_counter]m`. + /// + /// Held as a `u64` rather than as the counter bytes so that exhaustion is representable. The + /// counter field itself is at most 4 bytes, so it wraps to zero at `2^m` and a mode that read + /// its state back out of those bytes could not tell "just started" from "completely used up". + /// This counts to `BLOCK_LIMIT` and stops there. + next_counter: u64, +} + +impl + CtrKeyStream +where + P: ElectronicCodeBook, +{ + /// Bytes of counter at the end of each block: whatever the nonce leaves. + const CTR_LEN: usize = BLOCK_LEN - INIT_DATA_LEN; + + /// The number of counter blocks available, `2^(8 * CTR_LEN)`. + /// + /// `CTR_LEN <= 4` is asserted at construction, so this is at most `2^32` and cannot overflow + /// the `u64`. + const BLOCK_LIMIT: u64 = 1u64 << (8 * Self::CTR_LEN as u64); + + /// The compile-time shape check, run from both constructors. + /// + /// A zero-length counter could not count, and this type caps the counter at 4 bytes; see the + /// module docs. Both are properties of the const parameters, so both are compile errors at the + /// call site rather than a runtime `Err`. + #[inline] + fn check_shape() { + const { + assert!( + INIT_DATA_LEN < BLOCK_LEN, + "CTR needs at least one byte of counter: the nonce must be shorter than the block" + ); + assert!( + BLOCK_LEN - INIT_DATA_LEN <= 4, + "CTR counter is capped at 4 bytes: the nonce must be at least BLOCK_LEN - 4 bytes" + ); + }; + } + + /// `T1 = N | [0]m`: the nonce, then a zero counter. + #[inline] + fn start(perm: P, nonce: [u8; INIT_DATA_LEN]) -> Self { + Self::start_at(perm, nonce, 0) + } + + /// 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 } + } + + /// The nonce `N` of a suspended keystream, read out of the state [`SuspendableComponent`] + /// writes without rebuilding the keystream: it is the leading `INIT_DATA_LEN` bytes. For a + /// composite that needs the nonce before it can afford the key schedule (GCM derives `H` and + /// the tag mask from it). + pub(crate) fn nonce_from_state(state: &[u8]) -> [u8; INIT_DATA_LEN] { + Cursor::new(state).array::() + } + + /// `Tj = N | [j]m`: the nonce followed by the counter `j`, big-endian, in the trailing + /// `CTR_LEN` bytes. + /// + /// Taking the low `CTR_LEN` bytes of the big-endian `u64` is the `mod 2^m` of Appendix B.1's + /// standard incrementing function, though the truncation never actually discards anything: + /// [`StreamCipher`] refuses the call before the counter could reach `2^m`. + #[inline] + fn counter_block(nonce: &[u8; INIT_DATA_LEN], j: u64) -> [u8; BLOCK_LEN] { + let mut t = [0u8; BLOCK_LEN]; + t[..INIT_DATA_LEN].copy_from_slice(nonce); + let be = j.to_be_bytes(); + t[INIT_DATA_LEN..].copy_from_slice(&be[be.len() - Self::CTR_LEN..]); + t + } +} + +impl Algorithm + for CtrKeyStream +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; +} + +impl + KeyStream + for CtrKeyStream +where + P: ElectronicCodeBook, +{ + /// Expands the key; the keystream starts at `T1 = N | [0]m`. + fn new( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ) -> Result { + Self::check_shape(); + let perm = P::new(key)?; + Ok(Self::start(perm, *init_data)) + } + + /// A whole block for every counter value left. + fn remaining_blocks(&self) -> u64 { + Self::BLOCK_LIMIT - self.next_counter + } + + /// `Cj = Pj XOR CIPH_K(Tj)` (or `Pj = Cj XOR CIPH_K(Tj)`, the same operation) for the next + /// `blocks.len()` counter blocks; see `apply_counter_blocks`. + fn apply_blocks(&mut self, blocks: &mut [[u8; BLOCK_LEN]]) { + let nonce = &self.nonce; + apply_counter_blocks( + &self.perm, + &mut self.next_counter, + |j| Self::counter_block(nonce, j), + blocks, + ); + } +} + +/// The suspended state is the nonce and the next counter value; the permutation is rebuilt from +/// the re-supplied key. See [`bouncycastle_utils::suspendable_state`]. +impl + SuspendableComponent for CtrKeyStream +where + P: ElectronicCodeBook, +{ + const STATE_LEN: usize = INIT_DATA_LEN + 8; + type Key = KeyMaterial; + + fn write_state(&self, out: &mut [u8]) { + let mut w = CursorMut::new(out); + w.bytes(&self.nonce); + w.u64(self.next_counter); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + Self::check_shape(); + let perm = P::new(key).map_err(|_| SuspendableError::InvalidData)?; + let mut r = Cursor::new(state); + let nonce = r.array::(); + // The counter counts to `BLOCK_LIMIT` and stops there (that is the exhausted state, with + // `remaining_blocks() == 0`); anything past it is not a state this type produces. + let next_counter = r.u64(); + if next_counter > Self::BLOCK_LIMIT { + return Err(SuspendableError::InvalidData); + } + debug_assert!(r.is_done()); + Ok(Self { perm, nonce, next_counter }) + } +} + +#[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 crate::Encrypting; + use bouncycastle_core::hazmat::ElectronicCodeBook; + use bouncycastle_core::key_material::{KeyMaterial, KeyType}; + use bouncycastle_core::traits::StreamCipherEncryptor; + use bouncycastle_core_test_framework::ToyBlockCipher; + + type ToyKeyStream = CtrKeyStream; + type ToyCtr = Ctr; + + fn key() -> KeyMaterial<16> { + KeyMaterial::<16>::from_bytes_as_type(&[0x5Au8; 16], KeyType::SymmetricCipherKey) + .expect("a valid 16-byte 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::from_keystream(ToyKeyStream::start( + ToyBlockCipher::new(&key()).unwrap(), + nonce, + )); + let mut discarded = [0u8; 32]; + from_start.do_encrypt_inplace(&mut discarded).unwrap(); + + let mut from_start_at = ToyCtr::from_keystream(ToyKeyStream::start_at( + ToyBlockCipher::new(&key()).unwrap(), + nonce, + 2, + )); + + let mut a = [0x42u8; 48]; + let mut b = a; + from_start.do_encrypt_inplace(&mut a).unwrap(); + from_start_at.do_encrypt_inplace(&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 ks = ToyKeyStream::start_at(ToyBlockCipher::new(&key()).unwrap(), [0u8; 12], 2); + assert_eq!(ks.remaining_blocks(), ToyKeyStream::BLOCK_LIMIT - 2); + } +} diff --git a/crypto/cipher/src/modes/hazmat/ecb.rs b/crypto/cipher/src/modes/hazmat/ecb.rs new file mode 100644 index 00000000..6b38b622 --- /dev/null +++ b/crypto/cipher/src/modes/hazmat/ecb.rs @@ -0,0 +1,276 @@ +//! The Electronic Codebook mode of operation (NIST SP 800-38A Sec 6.1). +//! +//! **🚨 Security note: 🚨 ECB is not a confidentiality mode for data.** That is why it is under +//! [`hazmat`](crate::modes::hazmat); see [`bouncycastle_core::hazmat`] for the supported uses. +//! +//! "In ECB encryption, the forward cipher function is applied directly and independently to each +//! block of the plaintext. The resulting sequence of output blocks is the ciphertext. In ECB +//! decryption, the inverse cipher function is applied directly and independently to each block of +//! the ciphertext. The resulting sequence of output blocks is the plaintext." +//! +//! # Usage Examples +//! +//! ECB has the same shape with no IV: `encrypt` returns an empty array and `decrypt` takes one. +//! The codebook property that makes it unsuitable for data is visible in the ciphertext: +//! +//! ``` +//! use bouncycastle_core_test_framework::ToyBlockCipher; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +//! use bouncycastle_cipher::modes::hazmat::Ecb; +//! use bouncycastle_cipher::{Decrypting, Encrypting}; +//! +//! type ToyEcb = Ecb; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let mut data = [0x5Au8; 32]; // two equal blocks +//! +//! let (bytes_written, no_iv): (usize, [u8; 0]) = ToyEcb::::encrypt_inplace(&key, &mut data).expect("encryption"); +//! assert_eq!(no_iv.len(), 0, "ECB mode returns the IV as an empty array"); +//! assert_eq!(data[..16], data[16..], "equal plaintext blocks give equal ciphertext blocks"); +//! +//! ToyEcb::::decrypt_inplace(&key, &[], &mut data).expect("decryption"); +//! assert_eq!(data, [0x5Au8; 32]); +//! ``` +//! +//! # A mode with no state +//! +//! This mode is a fixed permutation determined by the key acting on a single block. +//! There is no IV and no chaining. +//! +//! # Both directions are parallel +//! +//! Sec 6.1: "In ECB encryption and ECB decryption, multiple forward cipher functions and inverse +//! cipher functions can be computed in parallel". +//! +//! To take advantage of this parallelism, this mode batches both directions through the +//! permutation's four-block and pair methods ([`ElectronicCodeBook::encrypt_4blocks`] / +//! [`ElectronicCodeBook::encrypt_2blocks`] and their inverses), which may represent a speed-up over +//! iterating one block at a time, depending on the implementation of the underlying cipher. +//! +//! # Suspending and resuming execution +//! +//! [`Ecb`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a byte +//! array and resumed later with the re-supplied key. The state is empty, since nothing carries over +//! between blocks, and resuming is re-expanding the key; it exists so the padded adapters over ECB +//! can be suspended. The array length is `Ecb::SUSPENDED_STATE_LEN`; see [the crate +//! docs](crate#suspending-and-resuming-execution) for an example. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! ## ECB is a building-block not a confidentiality mode for data +//! +//! SP 800-38A §6.1: +//! +//! > "In the ECB mode, under a given key, any given plaintext block always gets +//! > encrypted to the same ciphertext block. If this property is undesirable in a particular +//! > application, the ECB mode should not be used." +//! +//! While this _might_ be secure for encrypting plaintext that is cryptographically random, +//! it is certainly not ok for structured data (such as any file format with known and predictable +//! headers), or data with repeated blocks since it becomes trivial for an attacker to build lookup +//! tables of plaintext --> ciphertext pairs under this encryption key. ECB mode also does not prevent +//! an attacker from reordering, duplicating, or deleting blocks within a multi-block ciphertext or +//! between multiple messages encrypted under the same key. +//! +//! As such, ECB is exposed primarily as a building-block for the other, more secure, modes of +//! operation, and also for research and educational purposes. +//! +//! **ECB Mode should not be used in production!** + +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, RNG, SuspendableKeyed, +}; +use bouncycastle_utils::suspendable_state::{ + LIB_VERSION_LEN, SuspendableComponent, resume_component, suspend_component, +}; +use core::marker::PhantomData; + +/// ECB mode over any permutation that impls [`ElectronicCodeBook`], with the direction encoded in the type. +/// +/// **Not a confidentiality mode for data**: see the module docs and [`modes`](crate::modes)'s "Security +/// Considerations". Provided for interoperability and test vectors. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`]. [`BlockCipherEncryptor`] is implemented only for the +/// former and [`BlockCipherDecryptor`] only for the latter, so an `Ecb<_, Encrypting, _, _>` has no +/// decryption methods at all -- using one in the wrong direction is a compile error rather than a +/// runtime check. +/// +/// # State +/// Only the permutation, which owns the key schedule and is responsible for keeping it in a +/// zeroize-on-drop wrapper. Nothing chains from one block to the next, so unlike `Cbc` and `Cfb` +/// there is no block of chaining value: `size_of::>() == size_of::

()`. +#[derive(Clone)] +pub struct Ecb +where + P: ElectronicCodeBook, +{ + perm: P, + _dir: PhantomData

, +} + +impl Ecb +where + P: ElectronicCodeBook, +{ + /// The `N` of this type's [`SuspendableKeyed`] impl: the version header alone, since ECB + /// has no state between blocks. See [`bouncycastle_utils::suspendable_state`]. + pub const SUSPENDED_STATE_LEN: usize = LIB_VERSION_LEN; + + /// Expands the key. Both `_init` constructors are this; there is nothing else to set up. + fn new(key: &KeyMaterial) -> Result { + Ok(Self { perm: P::new(key)?, _dir: PhantomData }) + } +} + +impl Algorithm + for Ecb +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. (It does not make ECB + /// suitable for data either; strength is about the key, not about the codebook property.) + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +impl BlockCipherEncryptor + for Ecb +where + P: ElectronicCodeBook, +{ + /// Expands the key. ECB has no initialization data (SP 800-38A Table D.2 lists the IV column + /// as "Not applicable"), so the returned init data is the empty array. + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; 0]), SymmetricCipherError> { + Ok((Self::new(key)?, [])) + } + + /// Always panics: ECB generates no init data, so there is nothing for an RNG to do. + /// + /// # Panics + /// Unconditionally, as [`BlockCipherEncryptor::do_encrypt_init_rng`] requires of a mode whose + /// `INIT_DATA_LEN` is 0. Reaching for the RNG-taking constructor means the caller expects a + /// randomized mode, and ECB is not one -- SP 800-38A Table D.2 lists its IV column as "Not + /// applicable" -- so silently ignoring the RNG would leave that mistaken expectation + /// undisturbed. Use [`do_encrypt_init`](Self::do_encrypt_init), or a mode that has an IV. + fn do_encrypt_init_rng( + _key: &KeyMaterial, + _rng: &mut dyn RNG, + ) -> Result<(Self, [u8; 0]), SymmetricCipherError> { + unimplemented!( + "ECB has no initialization data, so it draws nothing from an RNG: use do_encrypt_init, \ + or a mode with an IV if a randomized ciphertext was wanted" + ) + } + + /// The implementor hook (the flat `do_encrypt_inplace` is provided over it): + /// `Cj = CIPH_K(Pj)` for every block, in place. + /// + /// Sec 6.1 allows the forward cipher functions to "be computed in parallel", so the blocks go + /// to the permutation in fours, then pairs, then the remaining block singly. `as_chunks_mut` + /// splits into exactly those shapes with no runtime length check. Never fails: ECB has no + /// per-initialization data limit. + fn do_encrypt_blocks_inplace( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result { + let len = blocks.len() * BLOCK_LEN; + let (fours, rest) = blocks.as_chunks_mut::<4>(); + for four in fours.iter_mut() { + self.perm.encrypt_4blocks(four); + } + let (pairs, tail) = rest.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.perm.encrypt_2blocks(pair); + } + for block in tail.iter_mut() { + self.perm.encrypt_block(block); + } + Ok(len) + } +} + +impl BlockCipherDecryptor + for Ecb +where + P: ElectronicCodeBook, +{ + /// Expands the key. The init data is the empty array [`BlockCipherEncryptor::do_encrypt_init`] + /// returned; there is nothing in it to use. + fn do_decrypt_init( + key: &KeyMaterial, + _init_data: &[u8; 0], + ) -> Result { + Self::new(key) + } + + /// The implementor hook (the flat `do_decrypt_inplace` is provided over it): + /// `Pj = CIPH^-1_K(Cj)` for every block, in place -- fours, then pairs, then the remaining + /// block, as on the encrypt side. Never fails. + fn do_decrypt_blocks_inplace( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result { + let len = blocks.len() * BLOCK_LEN; + let (fours, rest) = blocks.as_chunks_mut::<4>(); + for four in fours.iter_mut() { + self.perm.decrypt_4blocks(four); + } + let (pairs, tail) = rest.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.perm.decrypt_2blocks(pair); + } + for block in tail.iter_mut() { + self.perm.decrypt_block(block); + } + Ok(len) + } +} + +/// ECB carries nothing from one block to the next, so its suspended state is empty and resuming +/// is re-expanding the key. It is implemented so that the padded adapters over it, which do hold +/// a partial block, can be suspended. See [`bouncycastle_utils::suspendable_state`]. +impl SuspendableComponent + for Ecb +where + P: ElectronicCodeBook, +{ + const STATE_LEN: usize = 0; + type Key = KeyMaterial; + + fn write_state(&self, out: &mut [u8]) { + debug_assert!(out.is_empty()); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + debug_assert!(state.is_empty()); + Self::new(key).map_err(|_| SuspendableError::InvalidData) + } +} + +/// `N` must be [`Ecb::SUSPENDED_STATE_LEN`]; anything else is a compile error. +impl SuspendableKeyed + for Ecb +where + P: ElectronicCodeBook, +{ + type Key = KeyMaterial; + + fn suspend(self) -> [u8; N] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + resume_component(&state, key) + } +} diff --git a/crypto/cipher/src/modes/hazmat/mod.rs b/crypto/cipher/src/modes/hazmat/mod.rs new file mode 100644 index 00000000..6f272447 --- /dev/null +++ b/crypto/cipher/src/modes/hazmat/mod.rs @@ -0,0 +1,18 @@ +//! Raw primitives whose safe use is the caller's responsibility. +//! +//! An item lives under a `hazmat` module when it is a correct, tested primitive whose +//! *composition* is the caller's responsibility, or that otherwise carry non-trivial +//! Security Considerations which are the caller's responsibility. +//! +//! Part of the design intention is to allow static code analyzers to easily find and flag +//! such uses with a simple search such as +//! +//! ```text +//! grep -rnE --include='*.rs' 'use .*::hazmat::' +//! ``` + +mod ctr_key_stream; +mod ecb; + +pub use ctr_key_stream::CtrKeyStream; +pub use ecb::Ecb; diff --git a/crypto/cipher/src/modes/iv.rs b/crypto/cipher/src/modes/iv.rs new file mode 100644 index 00000000..a37733aa --- /dev/null +++ b/crypto/cipher/src/modes/iv.rs @@ -0,0 +1,25 @@ +//! Initialization-vector generation, shared by the modes that need one. + +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::traits::RNG; + +/// Generates a random initialization vector. +/// +/// NIST SP 800-38A Appendix C gives two recommended methods for producing the unpredictable IVs +/// that CBC and CFB require. This is the second one verbatim: "to generate a random data block +/// using a FIPS-approved random number generator". +/// +/// The first method -- applying the forward cipher function to a nonce under the same key -- is not +/// implemented, because it needs a nonce the caller has to guarantee unique, and the API +/// deliberately does not accept caller-supplied initialization data at all. +/// +/// Appendix C also notes the IV "need not be secret", so this is not wrapped in a `Secret`: it is +/// returned to the caller to transmit alongside the ciphertext. Its *integrity* is a different +/// matter -- see the `cbc` module docs about SP 800-38A Appendix D. +pub(crate) fn random_iv( + rng: &mut dyn RNG, +) -> Result<[u8; N], SymmetricCipherError> { + let mut iv = [0u8; N]; + rng.next_bytes_out(&mut iv)?; + Ok(iv) +} diff --git a/crypto/cipher/src/modes/mod.rs b/crypto/cipher/src/modes/mod.rs new file mode 100644 index 00000000..cba27d9f --- /dev/null +++ b/crypto/cipher/src/modes/mod.rs @@ -0,0 +1,169 @@ +//! Block cipher modes of operation (NIST SP 800-38A, SP 800-38C and SP 800-38D). +//! +//! The module is deliberately cipher-agnostic: it depends on no concrete block cipher, only on the +//! trait. +//! +//! A mode turns a keyed block permutation -- `bouncycastle-aes`'s `AES128Internal` and friends, +//! `bouncycastle_core_test_framework::ToyBlockCipher`, or anything else implementing +//! [`ElectronicCodeBook`] -- into something that can encrypt more than one block. +//! +//! This module provides: +//! +//! | Mode | Mod | Spec | Notes | +//! |---|---|---|---| +//! | CBC | [`cbc`] | SP 800-38A Sec 6.2 | Cipher Block Chaining | +//! | CCM | [`ccm`] | SP 800-38C | Counter with CBC-MAC. **Authenticated**: CTR plus CBC-MAC, with a tag and AAD | +//! | 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 | +//! | ECB | [`hazmat`] | SP 800-38A Sec 6.1 | Electronic Codebook. **Not confidential for data**; interoperability and test vectors only | +//! | GCM | [`gcm`] | SP 800-38D | **Authenticated**: 96-bit nonce, 96-128-bit tag, no padding; AAD before data | +//! +//! 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). +//! +//! **CCM and GCM are AEADs**: they authenticate the ciphertext to detect ciphertext tampering, and +//! can also take additional (non-encrypted) data (AAD) that is protected by the same +//! authentication tag. The traits above have nowhere to put the AAD or the tag, so both implement +//! [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] instead, and through them +//! [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] with no option to provide AAD, and +//! the tag inline. +//! +//! CBC, CFB, CFB8 and CTR all generate their own init data: an IV for the first three, a nonce for +//! CTR, which is shorter than a block because the rest of the counter block is the counter. ECB has +//! none at all (`INIT_DATA_LEN = 0`) and is the raw permutation applied block by block, which is +//! why it lives under [`hazmat`] -- see [`hazmat::Ecb`] and +//! [Choosing between the modes](#choosing-between-the-modes). +//! +//! [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. +//! +//! # Usage Examples +//! +//! These usage examples are for implementing a concrete cipher on top of a mode. They are intended +//! for library developers, not end-users. +//! +//! They are written over `bouncycastle_core_test_framework::ToyBlockCipher`, a deliberately +//! insecure stand-in with AES-128's key and block sizes that the test-framework crate exports for +//! exactly this purpose, so that this module's documentation does not depend on any real cipher +//! crate (which would be a dependency cycle: `bouncycastle-aes` and the like depend on this one). Substitute +//! any [`ElectronicCodeBook`] implementor, such as `bouncycastle_aes::hazmat::AES128Internal`; +//! the `bouncycastle-aes` crate's aliases carry runnable examples over the real thing. +//! +//! [`ElectronicCodeBook`]: bouncycastle_core::hazmat::ElectronicCodeBook +//! +//! ## Defining type aliases +//! +//! Define a one-line alias for the combination you use -- or use the ready-made +//! `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_core_test_framework::ToyBlockCipher; +//! use bouncycastle_cipher::modes::{Cbc, Ccm, Cfb, Cfb8, Ctr, Gcm}; +//! +//! // CBC, CFB, and CFB8 take a permutation, a direction, key length, and a block length. +//! type ToyCbc = Cbc; +//! type ToyCfb = Cfb; +//! type ToyCfb8 = Cfb8; +//! +//! // CTR takes one more parameter: the nonce length, which fixes the counter width at +//! // `BLOCK_LEN - NONCE_LEN`. 12 bytes of nonce leaves the maximum 4-byte counter. +//! type ToyCtr = Ctr; +//! +//! // CCM takes the permutation, a direction, key length, and a block length like the rest, +//! // plus the nonce length and the tag length -- both CCM-specific choices rather than cipher params. +//! // 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 ToyCcm = Ccm; +//! +//! // GCM mode is specified in NIST SP 800-38D. `Gcm` fixes the nonce at 12 bytes (Sec 5.2.1.1 +//! // recommends restricting support to 96 bits), and the block is always 16, so neither is a +//! // parameter. +//! type ToyGcm = Gcm; +//! ``` +//! +//! ## Encrypting and decrypting +//! +//! See each sub-module for usage docs. +//! +//! # Choosing between the modes +//! +//! **For a new design, use [`gcm`].** It is authenticated, and an unauthenticated +//! mode is almost never what a new protocol wants: the other five leave the ciphertext malleable in +//! specific, exploitable ways. While it is possible to bolt a MAC on afterwards, +//! this design has some subtleties that most people get wrong. +//! +//! [`ccm`] is also authenticated, however its design predates GCM. CCM must know the payload +//! length before it starts, so it suits a packet protocol whose frame is fixed or declared up +//! front and cannot stream a message of unknown length the way GCM can. +//! +//! ECB is not a candidate for data at all (below). Between the unauthenticated modes: +//! +//! The block cipher modes: [`cbc`], [`ctr`], [`cfb`] and [`cfb8`], while they do provide reasonable +//! confidentiality, do not provide ciphertext authentication, meaning that they do not protect against +//! ciphertext malleability attacks where an active attacker will manipulate the ciphertext and then +//! hand it to a decryption oracle to see how the oracle behaves. As such, these modes should be +//! considered antiquated and only used when a protocol requires them. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! See sub-modules for mode-specific security considerations. +//! +//! ## The IV must be unpredictable +//! +//! Modes that require an Initialisation Vector (IV) or a nonce typically require that it be +//! unpredictable -- ie not guessable by an attacker prior to the honest party performing the +//! encryption -- and that it be unique per encryption invocation. +//! +//! Generally, the best-practice is to pull it from a cryptographic RNG as part of the `encrypt()` +//! operation, which most of the provided modes offer and do automatically. However, some modes +//! allow the user to provide the IV / nonce, in which case they become responsible for its +//! randomness. +//! +//! ## Key, IV reuse and content limits +//! +//! In general, it is acceptable for the same key being used for many messages, which is fine provided +//! each encryption gets a fresh unpredictable IV / nonce. IV / nonce reuse often leads +//! to immediate total loss of security. +//! +//! Additionally, some ciphers and modes will specify a +//! maximum amount of data that can be encrypted under a given key / IV before there is a risk that +//! blocks start repeating. + +pub mod cbc; +pub mod ccm; +pub mod cfb; +pub mod cfb8; +pub mod ctr; +pub mod gcm; +mod ghash; +pub mod hazmat; +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 gcm::{GCM_NONCE_LEN, Gcm}; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::hazmat::ElectronicCodeBook; +#[allow(unused_imports)] +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +// end of imports needed for docs diff --git a/crypto/cipher/src/modes/possible_enhancements.md b/crypto/cipher/src/modes/possible_enhancements.md new file mode 100644 index 00000000..899a4d0c --- /dev/null +++ b/crypto/cipher/src/modes/possible_enhancements.md @@ -0,0 +1,16 @@ +Possible additional modes or features to be added to this crate: + +* **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 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. +* Add GMAC and CMAC as top-level [`MAC']'s. \ No newline at end of file diff --git a/crypto/cipher/src/padding/mod.rs b/crypto/cipher/src/padding/mod.rs new file mode 100644 index 00000000..8b33abec --- /dev/null +++ b/crypto/cipher/src/padding/mod.rs @@ -0,0 +1,188 @@ +//! Block padding schemes implementing [`bouncycastle_core::traits::BlockCipherPadding`]. +//! +//! The following padding schemes are provided: +//! +//! * [`NoPadding`] — adds nothing and refuses to: for data that must already be a whole number of +//! blocks, where a partial final block is a caller error rather than something to pad. +//! * [`PKCS7`] — the padding scheme of RFC 5652 §6.3. +//! +//! The following structs are provided: +//! +//! * [`PaddedBlockCipherEncryptor`] / [`PaddedBlockCipherDecryptor`] — adapt a block-aligned +//! [`BlockCipherEncryptor`](bouncycastle_core::traits::BlockCipherEncryptor) / +//! [`BlockCipherDecryptor`](bouncycastle_core::traits::BlockCipherDecryptor) to arbitrary-length +//! data, streaming or one-shot. With [`NoPadding`] they instead *enforce* block alignment: an +//! aligned message passes through unchanged in length, and an unaligned one fails at +//! `do_encrypt_final`. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_core::traits::BlockCipherPadding; +//! use bouncycastle_cipher::padding::PKCS7; +//! +//! // 5 data bytes in a 16-byte block: pad with 11 bytes of value 0x0b. +//! let mut block = [0u8; 16]; +//! block[..5].copy_from_slice(b"hello"); +//! >::pad(&mut block, 5).unwrap(); +//! assert_eq!(&block[..5], b"hello"); +//! assert_eq!(&block[5..], &[0x0b; 11]); +//! +//! // Unpadding recovers the data length. +//! let data_len = >::unpad(&block).unwrap(); +//! assert_eq!(data_len, 5); +//! +//! // A block that is not well-formed padding is rejected. +//! block[15] = 0x00; +//! assert!(>::unpad(&block).is_err()); +//! ``` +//! +//! `NoPadding` never writes a byte: asking it to is the error that tells the caller their data was +//! not block-aligned, and a "padded" block is all data. +//! +//! ``` +//! use bouncycastle_core::errors::PaddingError; +//! use bouncycastle_core::traits::BlockCipherPadding; +//! use bouncycastle_cipher::padding::NoPadding; +//! +//! let mut block = [0x42u8; 16]; +//! assert_eq!(>::pad(&mut block, 5), Err(PaddingError::PaddingNotPermitted)); +//! assert_eq!(block, [0x42u8; 16], "nothing was written"); +//! assert_eq!(>::unpad(&block), Ok(16)); +//! ``` +//! +//! # Suspending and resuming execution +//! +//! Each adapter implements [`SuspendableKeyed`](bouncycastle_core::traits::SuspendableKeyed), so a +//! message in progress can be suspended to a byte array and resumed later with the re-supplied key. +//! The state is the inner cipher's state and the buffered partial block, plus the withheld block on +//! the decryptor. The array length is `PaddedBlockCipherEncryptor::SUSPENDED_STATE_LEN`; see [the +//! crate docs](crate#suspending-and-resuming-execution) for an example. +//! +//! # Memory Usage +//! +//! | Operation | Stack (excluding the caller's buffers and the inner cipher) | +//! |------------------------------|-------------------------------------------------------------| +//! | `PKCS7::pad` | O(1) | +//! | `PKCS7::unpad` | O(1) | +//! | `NoPadding::pad` / `unpad` | O(1), touches no data | +//! | `PaddedBlockCipherEncryptor` | one `BLOCK_LEN` buffer (in a `Secret`) + a length | +//! | `PaddedBlockCipherDecryptor` | two `BLOCK_LEN` buffers + a length | +//! +//! # 🚨 Security Considerations 🚨 +//! +//! `unpad` is the classic padding-oracle site: if timing or the error depends on *which* byte was +//! malformed, an attacker who can submit ciphertexts can decrypt them byte by byte. [`PKCS7::unpad`] +//! inspects every byte with constant-time masks and returns a single undifferentiated +//! [`PaddingError::InvalidPadding`]. This does not make unauthenticated encryption safe: still +//! authenticate the ciphertext (MAC or AEAD) so the error is never reachable by an attacker. +//! +//! [`NoPadding`] has no padding to inspect and so no oracle of that kind; its `unpad` is a constant. +//! It does not make unauthenticated encryption safe either. + +mod padded_block_cipher; +pub use padded_block_cipher::{PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; + +use bouncycastle_core::errors::PaddingError; +use bouncycastle_core::traits::BlockCipherPadding; +use bouncycastle_utils::ct::Condition; + +/// RFC 5652 §6.3 padding (the CMS successor to PKCS #7): "the input shall be padded at the trailing +/// end with `k-(lth mod k)` octets all having value `k-(lth mod k)`". Defined only for block lengths +/// `0 < k < 256`, enforced at compile time. +#[derive(Debug, Clone, Copy)] +pub struct PKCS7; + +impl BlockCipherPadding for PKCS7 { + /// RFC 5652 §6.3 always adds at least one octet, so an aligned input gets a whole extra block + /// of padding (`pad(block, 0)`); otherwise the last block could not be unpadded unambiguously. + const ALWAYS_PADS: bool = true; + + fn pad(block: &mut [u8; BLOCK_LEN], data_len: usize) -> Result<(), PaddingError> { + const { + assert!( + BLOCK_LEN > 0 && BLOCK_LEN < 256, + "PKCS7 padding is only defined for block lengths 1..=255 (RFC 5652 §6.3)" + ) + } + if data_len >= BLOCK_LEN { + return Err(PaddingError::DataLengthTooLong(BLOCK_LEN - 1)); + } + // RFC 5652 §6.3: pad with k - (lth mod k) octets of value k - (lth mod k). Here the caller + // has already reduced lth mod k to data_len, so the value is simply BLOCK_LEN - data_len. + // `data_len < BLOCK_LEN < 256` so this fits in a u8. + let pad_byte = (BLOCK_LEN - data_len) as u8; + // Constant-time in data_len: every byte is visited, and a mask selects data vs padding. + for (i, b) in block.iter_mut().enumerate() { + let is_padding = Condition::::is_gte(i as i64, data_len as i64); + *b = is_padding.select(pad_byte as i64, *b as i64) as u8; + } + Ok(()) + } + + fn unpad(block: &[u8; BLOCK_LEN]) -> Result { + const { + assert!( + BLOCK_LEN > 0 && BLOCK_LEN < 256, + "PKCS7 padding is only defined for block lengths 1..=255 (RFC 5652 §6.3)" + ) + } + let k = BLOCK_LEN as i64; + // The last byte declares the padding length p; the block is valid iff 1 <= p <= k and the + // final p bytes all equal p. Every byte is examined regardless, so timing is independent of + // where (or whether) the padding is malformed. + let p = block[BLOCK_LEN - 1] as i64; + let mut valid = Condition::::is_within_range(p, 1, k); + for (i, b) in block.iter().enumerate() { + // Position i is a padding position iff i >= k - p. (If p is out of range this may select + // every position, but `valid` is already FALSE and cannot become TRUE again.) + let in_padding = Condition::::is_gte(i as i64, k - p); + let matches = Condition::::is_equal(*b as i64, p); + valid &= matches | !in_padding; + } + // Single public decision point: the caller learns only valid/invalid. + if valid.to_bool() { + // p is within 1..=k here, so k - p is in 0..k and the cast is lossless. + Ok((k - p) as usize) + } else { + Err(PaddingError::InvalidPadding) + } + } +} + +/// The absence of padding, as a [`BlockCipherPadding`] scheme: for data that must already be a +/// whole number of blocks. +/// +/// `pad` never writes anything -- it returns [`PaddingError::PaddingNotPermitted`] whenever it is +/// called, because being called means there was a partial block to pad -- and `unpad` reports the +/// whole block as data. Since [`ALWAYS_PADS`](BlockCipherPadding::ALWAYS_PADS) is `false`, a +/// [`PaddedBlockCipherEncryptor`] over it emits no final block for an aligned message and fails at +/// `do_encrypt_final` for an unaligned one, and a [`PaddedBlockCipherDecryptor`] releases every +/// block as data. The adapters thereby turn "the caller must supply whole blocks" into a checked +/// error instead of a silent assumption, which is what this scheme is for: interoperating with +/// formats that are defined on whole blocks (and, when used with ECB, with the raw block-by-block +/// operation they specify) while keeping the arbitrary-length API shape. +/// +/// It offers nothing that authentication would; see this module's "Security Considerations". +#[derive(Debug, Clone, Copy)] +pub struct NoPadding; + +impl BlockCipherPadding for NoPadding { + /// Adds nothing to aligned data: an aligned message is finished with no final block. + const ALWAYS_PADS: bool = false; + + /// Always an error: this scheme adds no bytes, so being asked to means the data was not a + /// whole number of blocks. `block` is left untouched. `data_len >= BLOCK_LEN` is reported as + /// [`PaddingError::DataLengthTooLong`], as for every scheme. + fn pad(_block: &mut [u8; BLOCK_LEN], data_len: usize) -> Result<(), PaddingError> { + if data_len >= BLOCK_LEN { + return Err(PaddingError::DataLengthTooLong(BLOCK_LEN - 1)); + } + Err(PaddingError::PaddingNotPermitted) + } + + /// The whole block is data. Constant, so trivially constant-time. + fn unpad(_block: &[u8; BLOCK_LEN]) -> Result { + Ok(BLOCK_LEN) + } +} diff --git a/crypto/cipher/src/padding/padded_block_cipher.rs b/crypto/cipher/src/padding/padded_block_cipher.rs new file mode 100644 index 00000000..9e80b60a --- /dev/null +++ b/crypto/cipher/src/padding/padded_block_cipher.rs @@ -0,0 +1,482 @@ +//! [`PaddedBlockCipherEncryptor`] / [`PaddedBlockCipherDecryptor`]: adapt a block-aligned [`BlockCipherEncryptor`] / +//! [`BlockCipherDecryptor`] to arbitrary-length data using a [`BlockCipherPadding`] scheme. +//! +//! The public API is the [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] traits, whose +//! shape was drawn from these two types; the one-shot methods are the traits' provided ones. +//! `FINAL_LEN` is `BLOCK_LEN`: the final output is the padded block -- or, under a scheme with +//! [`BlockCipherPadding::ALWAYS_PADS`] `false` (`NoPadding`) and an aligned message, nothing at +//! all, in which case `do_encrypt_final` reports 0 of the `FINAL_LEN` bytes as output. + +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, BlockCipherPadding, RNG, + SuspendableKeyed, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, + suspend_component, +}; +use core::array::from_mut; +use core::marker::PhantomData; + +/// Blocks per inner-cipher call on the bulk path; the remainder is processed one at a time. +const GROUP: usize = 8; + +/// Encrypts arbitrary-length data with a block cipher `E`, padding the final block with `P`. +/// +/// Stream with [`SymmetricCipherEncryptor::do_encrypt_out`] then +/// [`SymmetricCipherEncryptor::do_encrypt_final`], or use the one-shot +/// [`SymmetricCipherEncryptor::encrypt_out`]. Output is +/// `plaintext_len / BLOCK_LEN + 1` blocks for a scheme that always pads (PKCS7), and exactly the +/// input length for one that never does (`NoPadding`, which rejects an unaligned input at +/// `do_encrypt_final`). The buffered partial plaintext block is held in a [`Secret`]. +#[derive(Clone)] +pub struct PaddedBlockCipherEncryptor< + E, + P, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +> where + E: BlockCipherEncryptor, + P: BlockCipherPadding, +{ + encryptor: E, + _padding: PhantomData

, + /// Partial plaintext block; `buf_len < BLOCK_LEN` between calls. + buf: Secret<[u8; BLOCK_LEN]>, + buf_len: usize, +} + +impl Algorithm + for PaddedBlockCipherEncryptor +where + E: BlockCipherEncryptor, + P: BlockCipherPadding, +{ + /// The inner cipher's name; padding does not change what the algorithm is. + const ALG_NAME: &'static str = E::ALG_NAME; + /// Padding does not change the strength of the inner cipher. + const MAX_SECURITY_STRENGTH: SecurityStrength = E::MAX_SECURITY_STRENGTH; +} + +impl + SymmetricCipherEncryptor + for PaddedBlockCipherEncryptor +where + E: BlockCipherEncryptor, + P: BlockCipherPadding, +{ + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (encryptor, init_data) = E::do_encrypt_init(key)?; + Ok((Self { encryptor, _padding: PhantomData, buf: Secret::new(), buf_len: 0 }, init_data)) + } + + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (encryptor, init_data) = E::do_encrypt_init_rng(key, rng)?; + Ok((Self { encryptor, _padding: PhantomData, buf: Secret::new(), buf_len: 0 }, init_data)) + } + + /// Whole blocks among the buffered bytes plus `input_len`. + fn do_encrypt_out_len(&self, input_len: usize) -> usize { + (self.buf_len + input_len) / BLOCK_LEN * BLOCK_LEN + } + + /// Encrypts all whole blocks available (buffered + `plaintext`) into `ciphertext`, buffering the + /// remainder. + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + let out_len = self.do_encrypt_out_len(plaintext.len()); + if ciphertext.len() < out_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(out_len)); + } + // out_len is a multiple of BLOCK_LEN, so the remainder of this split is empty. + let (mut out_blocks, _) = ciphertext[..out_len].as_chunks_mut::(); + + // Turn this into a mutable pointer so that we can walk the pointer down the data. + // Note only the pointer is mut, the data remains immutable `&[u8]`. + let mut plaintext = plaintext; + + // 1. Top up a previously buffered partial block. + if self.buf_len > 0 { + let take = (BLOCK_LEN - self.buf_len).min(plaintext.len()); + self.buf[self.buf_len..self.buf_len + take].copy_from_slice(&plaintext[..take]); + self.buf_len += take; + plaintext = &plaintext[take..]; + if self.buf_len < BLOCK_LEN { + // All input absorbed into the partial block; nothing to emit (out_len == 0). + return Ok(0); + } + // Block completed. out_len >= BLOCK_LEN here, so `split_first_mut` always succeeds. + // The cipher works in place, so the block is encrypted inside the `Secret` and only + // ciphertext is copied out of it. + if let Some((first, rest)) = core::mem::take(&mut out_blocks).split_first_mut() { + self.encryptor.do_encrypt_blocks_inplace(from_mut(&mut *self.buf))?; + *first = *self.buf; + out_blocks = rest; + } + self.buf_len = 0; + } + + // 2. Bulk path: whole blocks are copied into the output and encrypted there, in place, in + // groups of GROUP then singly. + let (in_blocks, remainder) = plaintext.as_chunks::(); + debug_assert_eq!(in_blocks.len(), out_blocks.len()); + out_blocks.copy_from_slice(in_blocks); + let (out_groups, out_tail) = out_blocks.as_chunks_mut::(); + for group in out_groups.iter_mut() { + self.encryptor.do_encrypt_blocks_inplace(group)?; + } + for block in out_tail.iter_mut() { + self.encryptor.do_encrypt_blocks_inplace(from_mut(block))?; + } + + // 3. Buffer the trailing partial block (remainder.len() < BLOCK_LEN). + self.buf[..remainder.len()].copy_from_slice(remainder); + self.buf_len = remainder.len(); + Ok(out_len) + } + + /// Pads and encrypts the buffered partial block, returning the final ciphertext block and + /// `BLOCK_LEN` -- or, when the scheme adds nothing to aligned data and nothing is buffered, an + /// untouched buffer and 0: there is no final block. + /// + /// The block is padded and encrypted inside the `Secret`, so what is copied out is ciphertext. + /// A scheme that adds no padding turns a buffered partial block into + /// [`SymmetricCipherError::PaddingError`] here, which is the alignment check such a scheme + /// exists to provide. + fn do_encrypt_final(self) -> Result<([u8; BLOCK_LEN], usize), SymmetricCipherError> { + let Self { encryptor: mut inner, mut buf, buf_len, .. } = self; + if buf_len == 0 && !P::ALWAYS_PADS { + return Ok(([0u8; BLOCK_LEN], 0)); + } + P::pad(&mut buf, buf_len)?; + inner.do_encrypt_inplace(&mut buf)?; + Ok((*buf, BLOCK_LEN)) + } + + /// `(plaintext_len / BLOCK_LEN + 1) * BLOCK_LEN` -- always one extra block for the padding -- + /// for a scheme that always pads; `plaintext_len` itself for one that adds nothing (an + /// unaligned length is rejected by `do_encrypt_final`, so this is exact for every accepted + /// input). + fn encrypt_out_len(plaintext_len: usize) -> usize { + if P::ALWAYS_PADS { (plaintext_len / BLOCK_LEN + 1) * BLOCK_LEN } else { plaintext_len } + } +} + +/// Decrypts data produced by a [`PaddedBlockCipherEncryptor`] with the matching cipher and padding. +/// +/// Only the last block carries padding, so [`do_update_out`](Self::do_decrypt_out) always withholds +/// the most recent complete block and [`do_decrypt_final`](Self::do_decrypt_final) unpads it. +/// One-shot: [`decrypt_out`](Self::decrypt_out). +#[derive(Clone)] +pub struct PaddedBlockCipherDecryptor< + D, + P, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +> where + D: BlockCipherDecryptor, + P: BlockCipherPadding, +{ + decryptor: D, + _padding: PhantomData

, + /// Partial ciphertext block; `buf_len < BLOCK_LEN` between calls. + buf: [u8; BLOCK_LEN], + buf_len: usize, + /// Most recent complete ciphertext block, withheld in case it is the last. + held: Option<[u8; BLOCK_LEN]>, +} + +impl Algorithm + for PaddedBlockCipherDecryptor +where + D: BlockCipherDecryptor, + P: BlockCipherPadding, +{ + /// The inner cipher's name; padding does not change what the algorithm is. + const ALG_NAME: &'static str = D::ALG_NAME; + /// Padding does not change the strength of the inner cipher. + const MAX_SECURITY_STRENGTH: SecurityStrength = D::MAX_SECURITY_STRENGTH; +} + +impl + SymmetricCipherDecryptor + for PaddedBlockCipherDecryptor +where + D: BlockCipherDecryptor, + P: BlockCipherPadding, +{ + fn do_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ) -> Result { + Ok(Self { + decryptor: D::do_decrypt_init(key, init_data)?, + _padding: PhantomData, + buf: [0u8; BLOCK_LEN], + buf_len: 0, + held: None, + }) + } + + /// All complete blocks but the most recent one are released. + fn do_decrypt_out_len(&self, input_len: usize) -> usize { + let complete = self.held.is_some() as usize + (self.buf_len + input_len) / BLOCK_LEN; + complete.saturating_sub(1) * BLOCK_LEN + } + + /// Decrypts all complete blocks except the most recent into `plaintext`, buffering the remainder. + fn do_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + let out_len = self.do_decrypt_out_len(ciphertext.len()); + if plaintext.len() < out_len { + return Err(SymmetricCipherError::OutputBufferTooSmall(out_len)); + } + let (mut out_blocks, _) = plaintext[..out_len].as_chunks_mut::(); + let mut ciphertext = ciphertext; + + // 1. Top up a previously buffered partial block. + if self.buf_len > 0 { + let take = (BLOCK_LEN - self.buf_len).min(ciphertext.len()); + self.buf[self.buf_len..self.buf_len + take].copy_from_slice(&ciphertext[..take]); + self.buf_len += take; + ciphertext = &ciphertext[take..]; + if self.buf_len < BLOCK_LEN { + return Ok(0); + } + self.buf_len = 0; + // The completed block becomes the held block; the previously held block, if any, is + // now known not to be last and can be released. out_blocks has room for it by + // construction of out_len, so `split_first_mut` succeeds. + if let Some(prev) = self.held.replace(self.buf) + && let Some((first, rest)) = core::mem::take(&mut out_blocks).split_first_mut() + { + *first = prev; + self.decryptor.do_decrypt_blocks_inplace(from_mut(first))?; + out_blocks = rest; + } + } + + // 2. Bulk path. + let (in_blocks, remainder) = ciphertext.as_chunks::(); + if let Some((last, release)) = in_blocks.split_last() { + // Release the previously held block first (it precedes everything in `in_blocks`). + if let Some(prev) = self.held.replace(*last) + && let Some((first, rest)) = core::mem::take(&mut out_blocks).split_first_mut() + { + *first = prev; + self.decryptor.do_decrypt_blocks_inplace(from_mut(first))?; + out_blocks = rest; + } + // Then every block of this call except the new held one: copied into the output and + // decrypted there, in place. + debug_assert_eq!(release.len(), out_blocks.len()); + out_blocks.copy_from_slice(release); + let (out_groups, out_tail) = out_blocks.as_chunks_mut::(); + for group in out_groups.iter_mut() { + self.decryptor.do_decrypt_blocks_inplace(group)?; + } + for block in out_tail.iter_mut() { + self.decryptor.do_decrypt_blocks_inplace(from_mut(block))?; + } + } + + // 3. Buffer the trailing partial block. + self.buf[..remainder.len()].copy_from_slice(remainder); + self.buf_len = remainder.len(); + Ok(out_len) + } + + /// Decrypts and unpads the held final block. Returns the block and its data length; the rest is + /// padding. `DecryptionFailed` if the ciphertext was not block-aligned, or was empty under a + /// scheme that always pads (a padded message is at least one block); `PaddingError` if the + /// padding is malformed. Under a scheme that adds nothing, an empty ciphertext is the empty + /// message and every held block is entirely data. + fn do_decrypt_final(self) -> Result<([u8; BLOCK_LEN], usize), SymmetricCipherError> { + let Self { decryptor: mut inner, buf_len, held, .. } = self; + if buf_len != 0 { + return Err(SymmetricCipherError::DecryptionFailed); + } + let Some(mut block) = held else { + return if P::ALWAYS_PADS { + Err(SymmetricCipherError::DecryptionFailed) + } else { + Ok(([0u8; BLOCK_LEN], 0)) + }; + }; + inner.do_decrypt_inplace(&mut block)?; + let data_len = P::unpad(&block)?; + Ok((block, data_len)) + } + + /// `ciphertext_len - 1` for a scheme that always pads (at least one byte of the final block is + /// padding); `ciphertext_len` for one that adds nothing. + fn decrypt_out_len(ciphertext_len: usize) -> usize { + if P::ALWAYS_PADS { ciphertext_len.saturating_sub(1) } else { ciphertext_len } + } +} + +impl + PaddedBlockCipherEncryptor +where + E: BlockCipherEncryptor + SuspendableComponent, + P: BlockCipherPadding, +{ + /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the inner + /// cipher's state, the partial plaintext block and its length as a `u64`. See + /// [`bouncycastle_utils::suspendable_state`]. + pub const SUSPENDED_STATE_LEN: usize = + LIB_VERSION_LEN + ::STATE_LEN; +} + +/// The suspended state is the inner cipher's followed by the buffered partial block. That block +/// is plaintext, so the state must be protected; see [`bouncycastle_utils::suspendable_state`]. +impl + SuspendableComponent for PaddedBlockCipherEncryptor +where + E: BlockCipherEncryptor + SuspendableComponent, + P: BlockCipherPadding, +{ + const STATE_LEN: usize = E::STATE_LEN + BLOCK_LEN + 8; + type Key = E::Key; + + fn write_state(&self, out: &mut [u8]) { + let (inner, rest) = out.split_at_mut(E::STATE_LEN); + self.encryptor.write_state(inner); + let mut w = CursorMut::new(rest); + w.bytes(&*self.buf); + w.u64(self.buf_len as u64); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + let (inner, rest) = state.split_at(E::STATE_LEN); + let encryptor = E::read_state(inner, key)?; + let mut r = Cursor::new(rest); + let mut buf: Secret<[u8; BLOCK_LEN]> = Secret::new(); + (*buf).copy_from_slice(r.bytes(BLOCK_LEN)); + // A full block is encrypted as soon as it is full, so `buf_len < BLOCK_LEN` between calls. + let buf_len = bounded_usize(r.u64(), BLOCK_LEN - 1)?; + debug_assert!(r.is_done()); + Ok(Self { encryptor, _padding: PhantomData, buf, buf_len }) + } +} + +/// `N` must be [`PaddedBlockCipherEncryptor::SUSPENDED_STATE_LEN`]; anything else is a compile +/// error. +impl + SuspendableKeyed for PaddedBlockCipherEncryptor +where + E: BlockCipherEncryptor + SuspendableComponent, + P: BlockCipherPadding, +{ + type Key = E::Key; + + fn suspend(self) -> [u8; N] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + resume_component(&state, key) + } +} + +impl + PaddedBlockCipherDecryptor +where + D: BlockCipherDecryptor + SuspendableComponent, + P: BlockCipherPadding, +{ + /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the inner + /// cipher's state, the partial ciphertext block and its length as a `u64`, a flag for + /// whether a block is held back, and that block. See [`bouncycastle_utils::suspendable_state`]. + pub const SUSPENDED_STATE_LEN: usize = + LIB_VERSION_LEN + ::STATE_LEN; +} + +/// The suspended state is the inner cipher's, the buffered partial block, and the withheld +/// block if there is one (all ciphertext). See [`bouncycastle_utils::suspendable_state`]. +impl + SuspendableComponent for PaddedBlockCipherDecryptor +where + D: BlockCipherDecryptor + SuspendableComponent, + P: BlockCipherPadding, +{ + const STATE_LEN: usize = D::STATE_LEN + BLOCK_LEN + 8 + 1 + BLOCK_LEN; + type Key = D::Key; + + fn write_state(&self, out: &mut [u8]) { + let (inner, rest) = out.split_at_mut(D::STATE_LEN); + self.decryptor.write_state(inner); + let mut w = CursorMut::new(rest); + w.bytes(&self.buf); + w.u64(self.buf_len as u64); + match &self.held { + Some(block) => { + w.u8(1); + w.bytes(block); + } + None => { + w.u8(0); + w.bytes(&[0u8; BLOCK_LEN]); + } + } + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + let (inner, rest) = state.split_at(D::STATE_LEN); + let decryptor = D::read_state(inner, key)?; + let mut r = Cursor::new(rest); + let buf = r.array::(); + // A full block becomes the held block as soon as it is full, so `buf_len < BLOCK_LEN`. + let buf_len = bounded_usize(r.u64(), BLOCK_LEN - 1)?; + let held = match r.u8() { + 0 => { + r.bytes(BLOCK_LEN); + None + } + 1 => Some(r.array::()), + _ => return Err(SuspendableError::InvalidData), + }; + debug_assert!(r.is_done()); + Ok(Self { decryptor, _padding: PhantomData, buf, buf_len, held }) + } +} + +/// `N` must be [`PaddedBlockCipherDecryptor::SUSPENDED_STATE_LEN`]; anything else is a compile +/// error. +impl + SuspendableKeyed for PaddedBlockCipherDecryptor +where + D: BlockCipherDecryptor + SuspendableComponent, + P: BlockCipherPadding, +{ + type Key = D::Key; + + fn suspend(self) -> [u8; N] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + resume_component(&state, key) + } +} diff --git a/crypto/cipher/src/stream.rs b/crypto/cipher/src/stream.rs new file mode 100644 index 00000000..1971a6d3 --- /dev/null +++ b/crypto/cipher/src/stream.rs @@ -0,0 +1,406 @@ +//! Stream ciphers built from a [`KeyStream`], and the helpers a stream cipher that cannot be built +//! that way uses for the separate-output half of its API. +//! +//! [`StreamCipher`] turns any [`KeyStream`] into a [`StreamCipherEncryptor`] / +//! [`StreamCipherDecryptor`] pair, and with it the [`SymmetricCipherEncryptor`] / +//! [`SymmetricCipherDecryptor`] supertraits, as a block cipher mode turns an +//! [`ElectronicCodeBook`](bouncycastle_core::hazmat::ElectronicCodeBook) into a block cipher. A keystream +//! implementor writes the keystream; the nonce, the partly-used block held between calls and the +//! refusal to run past the end of the keystream are written once, here. +//! +//! A mode whose keystream depends on the data, such as CFB, implements the traits itself; the free +//! functions here are the parts of that implementation that are the same for every stream cipher. +//! +//! # Suspending and resuming execution +//! +//! [`StreamCipher`] implements [`SuspendableKeyed`], so a message in progress can be suspended to a +//! byte array and resumed later with the re-supplied key. The state is the keystream's own state +//! and the partly used keystream block, for any keystream that implements [`SuspendableComponent`]. +//! The array length is `StreamCipher::SUSPENDED_STATE_LEN`; see [the crate +//! docs](crate#suspending-and-resuming-execution) for an example. +//! + +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::{SuspendableError, SymmetricCipherError}; +use bouncycastle_core::hazmat::KeyStream; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + Algorithm, RNG, StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, + suspend_component, +}; +use core::marker::PhantomData; + +/// The separate-output `do_update_out` of a stream cipher, over its in-place data method: copies +/// `input` into `output` and applies `in_place` there, so the caller's input is left untouched. +/// Returns `input.len()`, since a stream cipher neither buffers nor changes the length of its data. +/// The whole of `output` is zeroized first, so any bytes past `input.len()` will be 0. +/// +/// # Errors +/// [`SymmetricCipherError::OutputBufferTooSmall`] if `output` is shorter than `input`, checked +/// before anything is consumed; otherwise whatever `in_place` returns. +pub fn stream_update_out( + input: &[u8], + output: &mut [u8], + in_place: impl FnOnce(&mut [u8]) -> Result, +) -> Result { + output.fill(0); + if output.len() < input.len() { + return Err(SymmetricCipherError::OutputBufferTooSmall(input.len())); + } + let out = &mut output[..input.len()]; + out.copy_from_slice(input); + in_place(out)?; + Ok(input.len()) +} + +/// The `do_encrypt_final` / `do_decrypt_final` of a stream cipher: nothing is held back, so there +/// is nothing to finish -- an empty buffer, none of it output, and no padding or tag to check. +/// +/// `cargo mutants` reports the `[]` here as a surviving mutant against `[0; 0]` and `[1; 0]`. +/// Those are the same value: a zero-length array has no element to differ in, so the three +/// spellings are indistinguishable and no test can separate them. The mutants that *do* change +/// behaviour -- returning 1 rather than 0 for the data length -- are caught. +pub fn stream_do_final() -> Result<([u8; 0], usize), SymmetricCipherError> { + Ok(([], 0)) +} + +/// A stream cipher over any [`KeyStream`], with the direction encoded in the type. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`]. +/// +/// `INIT_DATA_LEN` and `BLOCK_LEN` must both be non-zero, checked at compile time: a keystream +/// with no init data would repeat for every message under a key. +/// +/// # State +/// +/// The keystream, the current keystream block and how much of it has been used. A call can end +/// part-way through a keystream block, and the remainder is kept for the next call so the caller's +/// chunking is invisible in the output. Those bytes are live keystream for the next bytes of the +/// message, so the block is a [`Secret`] and is zeroized on drop. +/// +/// # The keystream is finite, and running out is an error +/// +/// A call that would need more keystream than [`KeyStream::remaining_blocks`] can still supply +/// returns [`SymmetricCipherError::DataLimitExceeded`] and consumes nothing: the check is made up front, +/// against the whole call, so a message is never half-processed before the cipher notices. Past +/// that point the keystream would repeat, which is the two-time-pad failure within one message. +#[derive(Clone)] +pub struct StreamCipher< + KS, + Dir, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +> where + KS: KeyStream, +{ + keystream: KS, + /// The keystream block currently being consumed. Meaningful only while `used < BLOCK_LEN`. + pending: Secret<[u8; BLOCK_LEN]>, + /// Bytes of `pending` already consumed, `0..=BLOCK_LEN`. `BLOCK_LEN` means none is pending + /// and the next byte needs a fresh keystream block. + used: usize, + _marker: PhantomData

, +} + +impl + StreamCipher +where + KS: KeyStream, +{ + /// Wraps a keystream that has already been constructed and positioned, such as one that + /// starts part-way into its counter space (GCM's GCTR starts at `inc32(J0)`). + /// + /// # 🚨 Security Considerations 🚨 + /// This bypasses init-data generation: the keystream's nonce is whatever it was constructed + /// with, and the caller is responsible for it never repeating under the key. The + /// [`SymmetricCipherEncryptor`] constructors are the safe path. + pub fn from_keystream(keystream: KS) -> Self { + Self::check_shape(); + Self { keystream, pending: Secret::new(), used: BLOCK_LEN, _marker: PhantomData } + } + + /// The wrapped keystream, for a construction that shares its key schedule with something + /// else (CCM's CBC-MAC). Shared access only: producing keystream takes `&mut`, so this cannot + /// be used to step the keystream behind this value's back. + pub fn keystream(&self) -> &KS { + &self.keystream + } + + /// The compile-time shape check, run from every constructor. + /// + /// A keystream with no init data would produce the same keystream for every message under a + /// key; the traits' init-data contract exists to prevent exactly that. A zero-length block + /// could not carry any keystream at all. + #[inline] + fn check_shape() { + const { + assert!( + INIT_DATA_LEN > 0, + "a stream cipher needs init data, or it repeats its keystream for every message" + ); + assert!(BLOCK_LEN > 0, "a keystream block must be at least one byte"); + }; + } + + /// The whole data path, shared by both directions: a keystream cipher's encryption and + /// decryption are the same XOR, so there is one implementation and the direction is only a + /// type. + /// + /// Splits into the bytes that finish an already-open keystream block, the whole blocks that + /// follow -- handed to [`KeyStream::apply_blocks`] in one call, so the keystream batches them + /// however suits it -- and the short tail, whose keystream block is generated into `pending` + /// and kept for the next call. + /// + /// # Errors + /// [`SymmetricCipherError::DataLimitExceeded`] if the keystream cannot cover the call; nothing + /// is consumed in that case. + fn apply(&mut self, data: &mut [u8]) -> Result { + let pending_len = BLOCK_LEN - self.used; + // Saturating: a keystream with no practical limit reports `u64::MAX` blocks. + let capacity = (pending_len as u64) + .saturating_add(self.keystream.remaining_blocks().saturating_mul(BLOCK_LEN as u64)); + if data.len() as u64 > capacity { + // Keystream exhausted: this call would need more keystream than remains for this init + // data, and continuing would repeat keystream. + return Err(SymmetricCipherError::DataLimitExceeded); + } + + let head_len = core::cmp::min(pending_len, data.len()); + let (head, rest) = data.split_at_mut(head_len); + for (b, k) in head.iter_mut().zip(self.pending[self.used..].iter()) { + *b ^= *k; + } + self.used += head_len; + + let (blocks, tail) = rest.as_chunks_mut::(); + self.keystream.apply_blocks(blocks); + + if !tail.is_empty() { + // `rest` is non-empty, so `head` used up every pending byte and `used == BLOCK_LEN`. + // The keystream block is generated in place inside the `Secret` -- XORed into zeros -- + // so no copy of it is left on the stack unzeroized. + *self.pending = [0u8; BLOCK_LEN]; + self.keystream.apply_blocks(core::slice::from_mut(&mut *self.pending)); + for (b, k) in tail.iter_mut().zip(self.pending.iter()) { + *b ^= *k; + } + self.used = tail.len(); + } + Ok(data.len()) + } +} + +impl Algorithm + for StreamCipher +where + KS: KeyStream, +{ + /// The keystream's name. + const ALG_NAME: &'static str = KS::ALG_NAME; + /// Wrapping a keystream does not change its strength. + const MAX_SECURITY_STRENGTH: SecurityStrength = KS::MAX_SECURITY_STRENGTH; +} + +impl + SymmetricCipherEncryptor + for StreamCipher +where + KS: KeyStream, +{ + /// Begins an encryption flow, drawing the init data from the library's default OS-backed DRBG. + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let mut rng = HashDRBG_SHA512::new_from_os(); + Self::do_encrypt_init_rng(key, &mut rng) + } + + /// As [`SymmetricCipherEncryptor::do_encrypt_init`], but draws the init data from `rng`. + /// Never panics: `INIT_DATA_LEN == 0` is ruled out at compile time; see [`StreamCipher`]. + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + Self::check_shape(); + let mut init_data = [0u8; INIT_DATA_LEN]; + rng.next_bytes_out(&mut init_data)?; + let keystream = KS::new(key, &init_data)?; + Ok((Self::from_keystream(keystream), init_data)) + } + + /// Every input byte produces exactly one output byte. + fn do_encrypt_out_len(&self, input_len: usize) -> usize { + input_len + } + + /// See [`stream_update_out`]. + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + stream_update_out(plaintext, ciphertext, |data| self.apply(data)) + } + + /// See [`stream_do_final`]. + fn do_encrypt_final(self) -> Result<([u8; 0], usize), SymmetricCipherError> { + stream_do_final() + } + + /// A stream cipher never changes the length of its data. + fn encrypt_out_len(plaintext_len: usize) -> usize { + plaintext_len + } +} + +impl + StreamCipherEncryptor + for StreamCipher +where + KS: KeyStream, +{ + /// XORs the next `data.len()` keystream bytes into `data`. + /// + /// # Errors + /// [`SymmetricCipherError::DataLimitExceeded`] if the keystream cannot cover the call. Nothing + /// is consumed in that case; see [`StreamCipher`]. + fn do_encrypt_inplace(&mut self, data: &mut [u8]) -> Result { + self.apply(data) + } +} + +impl + SymmetricCipherDecryptor + for StreamCipher +where + KS: KeyStream, +{ + /// Begins a decryption flow from the init data returned by + /// [`SymmetricCipherEncryptor::do_encrypt_init`]. + fn do_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ) -> Result { + Self::check_shape(); + Ok(Self::from_keystream(KS::new(key, init_data)?)) + } + + /// Nothing is held back, so every input byte can be released immediately. + fn do_decrypt_out_len(&self, input_len: usize) -> usize { + input_len + } + + /// See [`stream_update_out`]. + fn do_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + stream_update_out(ciphertext, plaintext, |data| self.apply(data)) + } + + /// See [`stream_do_final`]. + fn do_decrypt_final(self) -> Result<([u8; 0], usize), SymmetricCipherError> { + stream_do_final() + } + + /// Exact rather than an upper bound: a stream cipher never changes the length of its data. + fn decrypt_out_len(ciphertext_len: usize) -> usize { + ciphertext_len + } +} + +impl + StreamCipherDecryptor + for StreamCipher +where + KS: KeyStream, +{ + /// The same XOR as encryption. + /// + /// # Errors + /// As [`StreamCipherEncryptor::do_encrypt_inplace`]. + fn do_decrypt_inplace(&mut self, data: &mut [u8]) -> Result { + self.apply(data) + } +} + +impl + StreamCipher +where + KS: KeyStream + SuspendableComponent, +{ + /// The `N` of this type's [`SuspendableKeyed`] impl: the version header, the keystream's + /// state, the pending keystream block and the `used` count as a `u64`. See + /// [`bouncycastle_utils::suspendable_state`]. + pub const SUSPENDED_STATE_LEN: usize = + LIB_VERSION_LEN + ::STATE_LEN; +} + +/// The suspended state is the keystream's own state followed by the pending keystream block and +/// how much of it is used. The pending block is live keystream, which is why the whole state +/// must be protected and never resumed twice; see [`bouncycastle_utils::suspendable_state`]. +impl + SuspendableComponent for StreamCipher +where + KS: KeyStream + SuspendableComponent, +{ + const STATE_LEN: usize = KS::STATE_LEN + BLOCK_LEN + 8; + type Key = KS::Key; + + fn write_state(&self, out: &mut [u8]) { + let (ks, rest) = out.split_at_mut(KS::STATE_LEN); + self.keystream.write_state(ks); + let mut w = CursorMut::new(rest); + w.bytes(&*self.pending); + w.u64(self.used as u64); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], key: &Self::Key) -> Result { + Self::check_shape(); + let (ks, rest) = state.split_at(KS::STATE_LEN); + let keystream = KS::read_state(ks, key)?; + let mut r = Cursor::new(rest); + // Read straight into the `Secret`, so no copy of the keystream block sits on the stack. + let mut pending: Secret<[u8; BLOCK_LEN]> = Secret::new(); + (*pending).copy_from_slice(r.bytes(BLOCK_LEN)); + // `used` is `0..=BLOCK_LEN`, with `BLOCK_LEN` meaning nothing is pending. + let used = bounded_usize(r.u64(), BLOCK_LEN)?; + debug_assert!(r.is_done()); + Ok(Self { keystream, pending, used, _marker: PhantomData }) + } +} + +/// `N` must be [`StreamCipher::SUSPENDED_STATE_LEN`]; anything else is a compile error. +impl< + KS, + Dir, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, + const N: usize, +> SuspendableKeyed for StreamCipher +where + KS: KeyStream + SuspendableComponent, +{ + type Key = KS::Key; + + fn suspend(self) -> [u8; N] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; N], key: &Self::Key) -> Result { + resume_component(&state, key) + } +} diff --git a/crypto/cipher/tests/ccm_suspend_tests.rs b/crypto/cipher/tests/ccm_suspend_tests.rs new file mode 100644 index 00000000..7a26676b --- /dev/null +++ b/crypto/cipher/tests/ccm_suspend_tests.rs @@ -0,0 +1,135 @@ +//! What a resumed CCM state refuses: the bounds `Ccm` and its keystream check when they are +//! rebuilt from a suspended array, each tampered with on its own. +//! +//! The round trips in `suspend_tests.rs` show that a faithful state resumes. These show that an +//! unfaithful one does not, which is the other half of the contract and the half that pins each +//! check individually: with the flags octet, the counter field, the counter index, the CBC-MAC +//! position and the owed payload all validated in one `if`, a test that corrupts several at +//! once would still pass if any one check were dropped. +//! +//! The offsets are those of the layout the `SuspendableComponent` impls write, in order: the +//! library version, the keystream (counter template, counter index), the stream cipher's pending +//! block and its `used` count, then the chaining block, `mac_pos`, `aad_owed` and `owed`, every +//! integer a little-endian `u64`. Spec references are to NIST SP 800-38C (May 2004, errata +//! update 07-20-2007). + +use bouncycastle_cipher::modes::Ccm; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SuspendableError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::SuspendableKeyed; +use bouncycastle_core_test_framework::ToyBlockCipher; +use bouncycastle_utils::suspendable_state::LIB_VERSION_LEN; + +/// A 12-byte nonce, so `q = 3`: the counter field is the template's last three octets and the +/// payload limit is `2^24 - 1`. +const NONCE_LEN: usize = 12; +const BLOCK_LEN: usize = 16; +type ToyCcm = Ccm; +const N: usize = ToyCcm::::SUSPENDED_STATE_LEN; + +/// A.1's `2^8q - 1`, which bounds both the owed payload and, since there is one counter block per +/// payload block, the counter field; `MAX_COUNTER` is the same number for every `q < 8`. +const LIMIT: u64 = ToyCcm::::MAX_PAYLOAD_LEN; + +// Field offsets within the suspended array. +const TEMPLATE: usize = LIB_VERSION_LEN; +const NEXT_CTR: usize = TEMPLATE + BLOCK_LEN; +const MAC_POS: usize = N - 24; +const OWED: usize = N - 8; + +fn key() -> KeyMaterial<16> { + KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap() +} + +/// A freshly constructed encryptor's state: `next_ctr = 1`, `mac_pos = 0` after `B0`, `owed` as +/// declared. +fn fresh(payload_len: usize) -> [u8; N] { + ToyCcm::::new(&key(), &[0x24u8; NONCE_LEN], b"aad", payload_len).unwrap().suspend() +} + +fn with_u64(mut state: [u8; N], at: usize, value: u64) -> [u8; N] { + state[at..at + 8].copy_from_slice(&value.to_le_bytes()); + state +} + +fn resumes(state: [u8; N]) -> Result<(), SuspendableError> { + ToyCcm::::from_suspended(state, &key()).map(|_| ()) +} + +/// `mac_pos` is how much of the current CBC-MAC block has been XORed in, and a full block is +/// enciphered at once, so `BLOCK_LEN - 1` is the largest value a real state can hold. +#[test] +fn mac_pos_is_held_below_the_block_length() { + let state = fresh(32); + assert!(resumes(with_u64(state, MAC_POS, (BLOCK_LEN - 1) as u64)).is_ok()); + assert_eq!( + resumes(with_u64(state, MAC_POS, BLOCK_LEN as u64)), + Err(SuspendableError::InvalidData) + ); +} + +/// `owed` is what `B0` committed to minus what has been supplied, so it can never exceed A.1's +/// `2^8q - 1`; the limit itself is a legal value, since nothing has to have been supplied yet. +#[test] +fn owed_is_held_to_the_payload_limit() { + let state = fresh(32); + assert!(resumes(with_u64(state, OWED, LIMIT)).is_ok()); + assert_eq!(resumes(with_u64(state, OWED, LIMIT + 1)), Err(SuspendableError::InvalidData)); +} + +/// The counter index runs from 1 (`S0` is the tag mask, step 7 starts the payload keystream at +/// `S1`) to one past the last counter value, which is where it stands once every block has been +/// used; 0 and anything beyond are not states a `Ccm` can have been in. +#[test] +fn the_counter_index_is_held_to_its_range() { + let state = fresh(32); + assert!(resumes(with_u64(state, NEXT_CTR, 1)).is_ok()); + assert!(resumes(with_u64(state, NEXT_CTR, LIMIT + 1)).is_ok(), "every counter used"); + assert_eq!(resumes(with_u64(state, NEXT_CTR, 0)), Err(SuspendableError::InvalidData)); + assert_eq!(resumes(with_u64(state, NEXT_CTR, LIMIT + 2)), Err(SuspendableError::InvalidData)); +} + +/// A.3 Table 4 fixes the template's flags octet at `[q-1]_3` with every other bit zero, and the +/// counter field is zero in the template because the index is written over it per block. Each +/// is checked on its own: the flags octet with the right `q` but a stray bit, a wrong `q`, and a +/// non-zero byte in each position of the counter field. +#[test] +fn the_counter_template_is_checked_byte_by_byte() { + let state = fresh(32); + assert!(resumes(state).is_ok()); + assert_eq!(state[TEMPLATE], 2, "q - 1 for a 12-byte nonce"); + + let mut stray_bit = state; + stray_bit[TEMPLATE] |= 0x40; + assert_eq!(resumes(stray_bit), Err(SuspendableError::InvalidData)); + let mut wrong_q = state; + wrong_q[TEMPLATE] = 1; + assert_eq!(resumes(wrong_q), Err(SuspendableError::InvalidData)); + + for i in BLOCK_LEN - 3..BLOCK_LEN { + let mut counter_field = state; + counter_field[TEMPLATE + i] = 1; + assert_eq!( + resumes(counter_field), + Err(SuspendableError::InvalidData), + "counter field octet {i}" + ); + } + // The nonce octets before the counter field are the caller's: any value resumes. + let mut nonce_byte = state; + nonce_byte[TEMPLATE + 1] ^= 0xFF; + assert!(resumes(nonce_byte).is_ok()); +} + +/// The same checks run for the decrypting direction, which shares the impl. +#[test] +fn the_decrypting_direction_checks_the_same_bounds() { + let state = + ToyCcm::::new(&key(), &[0x24u8; NONCE_LEN], b"aad", 32).unwrap().suspend(); + assert!(ToyCcm::::from_suspended(state, &key()).is_ok()); + assert!( + ToyCcm::::from_suspended(with_u64(state, OWED, LIMIT + 1), &key()).is_err() + ); + assert!(ToyCcm::::from_suspended(with_u64(state, NEXT_CTR, 0), &key()).is_err()); +} diff --git a/crypto/cipher/tests/direction_tests.rs b/crypto/cipher/tests/direction_tests.rs new file mode 100644 index 00000000..d338a040 --- /dev/null +++ b/crypto/cipher/tests/direction_tests.rs @@ -0,0 +1,29 @@ +//! [`Direction`] must select `Enc` for [`Encrypting`] and `Dec` for [`Decrypting`]. Both checks +//! hold at compile time, so a regression fails the build of this test crate; the `#[test]` is the +//! runtime half that a test runner can report. + +use bouncycastle_cipher::{Decrypting, Direction, Encrypting}; + +/// Two types that cannot be confused with each other, or with anything else. +struct Enc([u8; 1]); +struct Dec([u8; 2]); + +/// Compiles only when both arguments are the same type. +const fn same_type(_: &T, _: &T) {} + +const _: () = { + let enc: ::Select = Enc([0]); + same_type(&enc, &Enc([0])); + let dec: ::Select = Dec([0; 2]); + same_type(&dec, &Dec([0; 2])); +}; + +#[test] +fn encrypting_selects_enc_and_decrypting_selects_dec() { + let enc: ::Select = Enc([7]); + assert_eq!(enc.0, [7]); + let dec: ::Select = Dec([8, 9]); + assert_eq!(dec.0, [8, 9]); + assert_eq!(size_of::<::Select>(), 1); + assert_eq!(size_of::<::Select>(), 2); +} diff --git a/crypto/cipher/tests/modes/cbc_tests.rs b/crypto/cipher/tests/modes/cbc_tests.rs new file mode 100644 index 00000000..61526c93 --- /dev/null +++ b/crypto/cipher/tests/modes/cbc_tests.rs @@ -0,0 +1,411 @@ +//! Structural tests for CBC, driven by a toy permutation. +//! +//! These check the properties of the *mode* -- chaining, call sequencing, the pair/remainder split, +//! direction typing, SP 800-38A Appendix D error propagation -- independently of any real cipher. +//! The known-answer tests against SP 800-38A Appendix F.2 are in the `aes` crate, `crypto/aes/tests/sp800_38a_cbc_tests.rs`. + +mod common; + +use bouncycastle_cipher::modes::Cbc; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; +use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; +use common::{SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; + +type ToyCbc = Cbc; +type SwappedCbc = Cbc; +type SwappedFourCbc = Cbc; + +/// The implementor hook `do_encrypt_blocks_inplace`, by value, for tests whose data is +/// block-shaped. +fn enc_blocks( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[[u8; TOY_LEN]; N], +) -> [[u8; TOY_LEN]; N] { + let mut blocks = *plaintext; + enc.do_encrypt_blocks_inplace(&mut blocks).unwrap(); + blocks +} + +/// The implementor hook `do_decrypt_blocks_inplace`, by value. +fn dec_blocks( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[[u8; TOY_LEN]; N], +) -> [[u8; TOY_LEN]; N] { + let mut blocks = *ciphertext; + dec.do_decrypt_blocks_inplace(&mut blocks).unwrap(); + blocks +} + +/// The flat streaming method `do_encrypt_inplace`, by value. +fn enc_flat( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *plaintext; + enc.do_encrypt_inplace(&mut data).unwrap(); + data +} + +/// The flat streaming method `do_decrypt_inplace`, by value. +fn dec_flat( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *ciphertext; + dec.do_decrypt_inplace(&mut data).unwrap(); + data +} + +// ---- the toy itself, and the mode, against the shared frameworks ------------------------- + +/// The toy must be a real permutation before any conclusion drawn from it is worth anything. +#[test] +fn the_toy_permutation_conforms_to_the_trait() { + TestFrameworkElectronicCodeBook::new().test::(); +} + +#[test] +fn cbc_conforms_to_the_block_cipher_framework() { + TestFrameworkBlockCipher::new() + .test::, ToyCbc>(); +} + +// ---- chaining and call sequencing -------------------------------------------------------- + +/// Encrypting `n` blocks must not depend on how the calls are grouped, and likewise for +/// decryption. This is the "a sequence of calls is equivalent to one call over the concatenation" +/// contract of the trait, and for CBC it is entirely about the chaining value surviving across +/// calls. +/// +/// The odd groupings matter for decryption specifically: `N = 3` and `N = 5` leave a one-block +/// remainder after the pair loop, and `N = 1` skips the pair loop altogether. +#[test] +fn call_grouping_does_not_change_the_result() { + let key = toy_key(); + let plaintext: [[u8; TOY_LEN]; 8] = + core::array::from_fn(|i| core::array::from_fn(|j| (i * TOY_LEN + j) as u8)); + + // Both encryption runs must use the same IV to be comparable, so pin it with the fixed RNG + // rather than letting `do_encrypt_init` generate a fresh one. + let iv: [u8; TOY_LEN] = core::array::from_fn(|i| 0xF0 ^ (i as u8)); + let pinned_rng = || bouncycastle_core_test_framework::FixedSeedRNG::::new(iv); + + // Reference: all eight blocks in one call (two fours). + let (mut enc, got_iv) = + ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); + assert_eq!(got_iv, iv, "the pinned RNG should reproduce the IV"); + let reference = enc_blocks(&mut enc, &plaintext); + + // The same eight blocks, grouped every way that exercises a different code path. + let (mut enc, _) = ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); + let mut got = [[0u8; TOY_LEN]; 8]; + let a = enc_flat(&mut enc, &plaintext[0]); // one block, flat + let b = enc_blocks(&mut enc, &[plaintext[1], plaintext[2]]); // N = 2 + let c = enc_blocks(&mut enc, &[plaintext[3], plaintext[4], plaintext[5]]); // N = 3 + let d = enc_blocks(&mut enc, &[plaintext[6], plaintext[7]]); // N = 2 + got[0] = a; + got[1..3].copy_from_slice(&b); + got[3..6].copy_from_slice(&c); + got[6..8].copy_from_slice(&d); + + assert_eq!(got, reference, "grouping must not change the ciphertext"); + + // Now the decrypt side: one call vs several groupings, all from the same ciphertext. + let ct = reference; + + let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + let all_at_once = dec_blocks(&mut dec, &ct); + assert_eq!(all_at_once, plaintext); + + for grouping in [1usize, 2, 4] { + let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + let mut out = [[0u8; TOY_LEN]; 8]; + let mut at = 0; + while at < 8 { + match grouping { + 1 => { + out[at] = dec_flat(&mut dec, &ct[at]); + } + 2 => { + let p = dec_blocks(&mut dec, &[ct[at], ct[at + 1]]); + out[at..at + 2].copy_from_slice(&p); + } + _ => { + let p = dec_blocks(&mut dec, &[ct[at], ct[at + 1], ct[at + 2], ct[at + 3]]); + out[at..at + 4].copy_from_slice(&p); + } + } + at += grouping; + } + assert_eq!(out, plaintext, "decrypting in groups of {grouping}"); + } + + // N = 3 and N = 5 both leave a one-block remainder after the pair loop. + let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + let three = dec_blocks(&mut dec, &[ct[0], ct[1], ct[2]]); + let five = dec_blocks(&mut dec, &[ct[3], ct[4], ct[5], ct[6], ct[7]]); + assert_eq!(three, [plaintext[0], plaintext[1], plaintext[2]]); + assert_eq!(five, [plaintext[3], plaintext[4], plaintext[5], plaintext[6], plaintext[7]]); +} + +/// The pair path in `do_decrypt_blocks_inplace` must actually be taken. +/// +/// [`SwappedPairToy`] returns its two pair results in the wrong order while its single-block +/// methods are correct. So a CBC decryptor that uses `decrypt_2blocks` gives the wrong answer for +/// even-length input, and the right answer for a single block. If both came out right, the pair +/// path would be dead code and every claim about it would be untested. +#[test] +fn the_pair_path_is_really_used() { + let key = toy_key(); + let plaintext = [[0xA5u8; TOY_LEN], [0x5Au8; TOY_LEN]]; + + // The correct toy round-trips. + let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &ct), plaintext); + + // The swapped-pair toy encrypts identically (encryption is serial and never pairs)... + let (mut enc, iv) = SwappedCbc::::do_encrypt_init(&key).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + + // ...but decrypting the pair together must now be wrong, because the pair path is used. + let mut dec = SwappedCbc::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!( + dec_blocks(&mut dec, &ct), + plaintext, + "decrypting a pair must go through decrypt_2blocks" + ); + + // Decrypting one block at a time avoids the pair path, so it is correct even for this toy. + let mut dec = SwappedCbc::::do_decrypt_init(&key, &iv).unwrap(); + let p0 = dec_flat(&mut dec, &ct[0]); + let p1 = dec_flat(&mut dec, &ct[1]); + assert_eq!([p0, p1], plaintext, "the single-block path must not pair"); +} + +/// The four-block path in `do_decrypt_blocks_inplace` must actually be taken, and only for full +/// fours. +/// +/// [`SwappedFourToy`] returns its four results rotated while its pair and single-block methods +/// are correct. So a CBC decryptor that uses `decrypt_4blocks` gives the wrong answer for four +/// blocks handed over together, and the right answer for the same four blocks handed over as +/// two pairs or one at a time. Five blocks are wrong too: four, then one. +#[test] +fn the_four_block_path_is_really_used() { + let key = toy_key(); + let plaintext: [[u8; TOY_LEN]; 5] = core::array::from_fn(|i| [0x10 * i as u8 + 1; TOY_LEN]); + + // The correct toy round-trips five blocks. + let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &ct), plaintext); + + // The rotated-four toy encrypts identically (encryption is serial and never batches)... + let (mut enc, iv) = SwappedFourCbc::::do_encrypt_init(&key).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + + // ...but decrypting five together must be wrong, because the first four take the four path. + let mut dec = SwappedFourCbc::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!(dec_blocks(&mut dec, &ct), plaintext, "four blocks must go through decrypt_4blocks"); + + // Exactly four together is wrong for the same reason. + let four: [[u8; TOY_LEN]; 4] = ct[..4].try_into().unwrap(); + let mut dec = SwappedFourCbc::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!(&dec_blocks(&mut dec, &four)[..], &plaintext[..4]); + + // Two pairs go through the pair path and are correct; so is the fifth block on its own. + let mut dec = SwappedFourCbc::::do_decrypt_init(&key, &iv).unwrap(); + let first: [[u8; TOY_LEN]; 2] = ct[..2].try_into().unwrap(); + let second: [[u8; TOY_LEN]; 2] = ct[2..4].try_into().unwrap(); + assert_eq!( + &dec_blocks(&mut dec, &first)[..], + &plaintext[..2], + "fewer than four must not batch" + ); + assert_eq!(&dec_blocks(&mut dec, &second)[..], &plaintext[2..4]); + assert_eq!(dec_flat(&mut dec, &ct[4]), plaintext[4]); +} + +/// The flat streaming method must agree with the block-shaped implementor hook. +#[test] +fn flat_streaming_agrees_with_the_block_hook() { + let key = toy_key(); + let plaintext = [[0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; + let flat_plaintext: [u8; 3 * TOY_LEN] = plaintext.as_flattened().try_into().unwrap(); + + let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); + let flat_ct = enc_flat(&mut enc, &flat_plaintext); + + let (mut enc, iv2) = ToyCbc::::do_encrypt_init_rng( + &key, + &mut bouncycastle_core_test_framework::FixedSeedRNG::::new(iv), + ) + .unwrap(); + assert_eq!(iv2, iv, "the pinned RNG should reproduce the IV"); + let block_ct = enc_blocks(&mut enc, &plaintext); + assert_eq!(*block_ct.as_flattened(), flat_ct, "flat streaming must equal the block hook"); + + let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &block_ct), plaintext); + let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_flat(&mut dec, &flat_ct), flat_plaintext); +} + +// ---- SP 800-38A Appendix D error propagation --------------------------------------------- + +/// Appendix D: "In the CBC mode, if bit errors occur in the IV, then the first ciphertext block +/// will be decrypted incorrectly, and bit errors will occur in exactly the same bit positions as +/// in the IV; the decryptions of the other ciphertext blocks are not affected." +/// +/// This is a property of the construction (`P1 = CIPH^-1(C1) XOR IV`), so it holds for any +/// permutation, and getting it wrong would mean the IV is not being XOR-ed where the spec says. +#[test] +fn an_iv_bit_error_flips_exactly_that_bit_of_the_first_block() { + let key = toy_key(); + let plaintext = [[0x00u8; TOY_LEN], [0x11u8; TOY_LEN], [0x22u8; TOY_LEN]]; + + let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + + for byte in 0..TOY_LEN { + for bit in 0..8 { + let mut corrupt_iv = iv; + corrupt_iv[byte] ^= 1 << bit; + + let mut dec = ToyCbc::::do_decrypt_init(&key, &corrupt_iv).unwrap(); + let got = dec_blocks(&mut dec, &ct); + + let mut expected = plaintext; + expected[0][byte] ^= 1 << bit; + assert_eq!( + got, expected, + "IV byte {byte} bit {bit}: only that bit of P1 should change" + ); + } + } +} + +/// Appendix D, the ciphertext half: bit errors in `Cj` randomise the decryption of `Cj` and flip +/// the same bit positions of `Cj+1`'s decryption, leaving later blocks alone. +#[test] +fn a_ciphertext_bit_error_affects_only_two_blocks() { + let key = toy_key(); + let plaintext = [[0x00u8; TOY_LEN], [0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; + + let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + + let mut corrupt = ct; + corrupt[1][3] ^= 0b0010_0000; + + let mut dec = ToyCbc::::do_decrypt_init(&key, &iv).unwrap(); + let got = dec_blocks(&mut dec, &corrupt); + + assert_eq!(got[0], plaintext[0], "P1 depends only on C1 and the IV"); + assert_ne!(got[1], plaintext[1], "P2 comes from the corrupted C2"); + // P3 = CIPH^-1(C3) XOR C2, so the flipped bit of C2 appears verbatim in P3. + let mut expected_p3 = plaintext[2]; + expected_p3[3] ^= 0b0010_0000; + assert_eq!(got[2], expected_p3, "P3 should show the same bit flipped, and nothing else"); + assert_eq!(got[3], plaintext[3], "P4 is unaffected"); +} + +// ---- IV handling ------------------------------------------------------------------------- + +/// Two encryption flows under the same key must not reuse an IV. The framework checks this too; +/// repeated here because for CBC it is the single most important operational requirement. +#[test] +fn each_encryption_gets_a_fresh_iv() { + let key = toy_key(); + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..64 { + let (_, iv) = ToyCbc::::do_encrypt_init(&key).unwrap(); + assert!(seen.insert(iv), "IV repeated across encryptions: {iv:02x?}"); + } +} + +/// Identical plaintext under the same key must give different ciphertext, because the IV differs. +/// This is the property ECB lacks and the reason CBC needs an IV at all. +#[test] +fn identical_plaintext_gives_different_ciphertext() { + let key = toy_key(); + let plaintext = [0x77u8; 2 * TOY_LEN]; + + let mut first = plaintext; + ToyCbc::::encrypt_inplace(&key, &mut first).unwrap(); + let mut second = plaintext; + ToyCbc::::encrypt_inplace(&key, &mut second).unwrap(); + assert_ne!(first, second); + + // ...and, within one message, two identical plaintext blocks must not give identical + // ciphertext blocks either, because the chaining value differs. + assert_ne!( + first[..TOY_LEN], + first[TOY_LEN..], + "chaining should break the ECB pattern within a message" + ); +} + +// ---- key handling ------------------------------------------------------------------------ + +#[test] +fn a_key_of_the_wrong_type_is_rejected() { + let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); + let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); + assert!(ToyCbc::::do_encrypt_init(&seed).is_err()); + assert!(ToyCbc::::do_decrypt_init(&seed, &[0u8; TOY_LEN]).is_err()); +} + +/// The one-shots (`encrypt` / `decrypt` on a `[u8; LEN]`, in place) must produce exactly what the +/// streaming API produces over the same blocks, for an odd block count (pairs plus a one-block +/// tail) and an even one (pairs only), in both directions. +#[test] +fn one_shots_agree_with_the_streaming_api() { + let key = toy_key(); + let iv: [u8; TOY_LEN] = core::array::from_fn(|i| 0x0F ^ (i as u8)); + let pinned_rng = || bouncycastle_core_test_framework::FixedSeedRNG::::new(iv); + + // 3 blocks = 48 bytes: one pair and a tail. + let flat3: [u8; 3 * TOY_LEN] = core::array::from_fn(|i| (i * 7) as u8); + let blocks3: [[u8; TOY_LEN]; 3] = + core::array::from_fn(|b| flat3[b * TOY_LEN..][..TOY_LEN].try_into().unwrap()); + let (iv_a, ct_blocks) = { + let (mut enc, iv) = + ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); + (iv, enc_blocks(&mut enc, &blocks3)) + }; + let mut buf = flat3; + let (_, iv_b) = + ToyCbc::::encrypt_rng_inplace(&key, &mut pinned_rng(), &mut buf).unwrap(); + assert_eq!(iv_a, iv_b); + assert_eq!(buf, *ct_blocks.as_flattened(), "3 blocks: one-shot must equal streaming"); + ToyCbc::::decrypt_inplace(&key, &iv, &mut buf).unwrap(); + assert_eq!(buf, flat3); + + // 4 blocks = 64 bytes: pairs only, no tail. + let flat4: [u8; 4 * TOY_LEN] = core::array::from_fn(|i| (i * 13 + 1) as u8); + let blocks4: [[u8; TOY_LEN]; 4] = + core::array::from_fn(|b| flat4[b * TOY_LEN..][..TOY_LEN].try_into().unwrap()); + let ct_blocks = { + let (mut enc, _) = + ToyCbc::::do_encrypt_init_rng(&key, &mut pinned_rng()).unwrap(); + enc_blocks(&mut enc, &blocks4) + }; + let mut buf = flat4; + ToyCbc::::encrypt_rng_inplace(&key, &mut pinned_rng(), &mut buf).unwrap(); + assert_eq!(buf, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); + ToyCbc::::decrypt_inplace(&key, &iv, &mut buf).unwrap(); + assert_eq!(buf, flat4); + + // The OS-RNG variant round-trips too. + let mut buf = flat3; + let (_, iv_fresh) = ToyCbc::::encrypt_inplace(&key, &mut buf).unwrap(); + assert_ne!(buf, flat3); + ToyCbc::::decrypt_inplace(&key, &iv_fresh, &mut buf).unwrap(); + assert_eq!(buf, flat3); +} diff --git a/crypto/cipher/tests/modes/ccm_tests.rs b/crypto/cipher/tests/modes/ccm_tests.rs new file mode 100644 index 00000000..103a08e1 --- /dev/null +++ b/crypto/cipher/tests/modes/ccm_tests.rs @@ -0,0 +1,502 @@ +//! Structural tests for CCM, driven by toy permutations. +//! +//! These check the properties of the *mode* -- that only the forward cipher function is ever +//! used, that the counter half batches while the CBC-MAC stays serial, that call chunking is +//! invisible in both directions, what `TAG_LEN` and `NONCE_LEN` do and do not change, that the +//! decryptor holds to the declared length, and which entry points release unauthenticated +//! plaintext -- independently of the known-answer vectors in the `aes` crate's `sp800_38c_tests.rs`, +//! `ccm_bc-test-data.rs` and `ccm_wycheproof.rs`. The Appendix C file also carries the +//! fixed-frame `CcmEncryptor` / `CcmDecryptor` pair's contract, the shared framework run and the +//! memory table, so none of those is repeated here. +//! +//! Spec references are to NIST SP 800-38C (May 2004, errata update 07-20-2007). + +mod common; + +use bouncycastle_cipher::modes::{Ccm, CcmDecryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::traits::{AEADCipherDecryptor, SymmetricCipherDecryptor}; +use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; + +/// The default shape under test: a 12-byte nonce, so `q = 3`, and a full 16-byte tag. +const NONCE_LEN: usize = 12; +const TAG_LEN: usize = 16; + +type ToyCcm = Ccm; +type SwappedCcm = Ccm; +type SwappedFourCcm = Ccm; +type ForwardOnlyCcm = Ccm; + +fn pinned_nonce() -> [u8; NONCE_LEN] { + core::array::from_fn(|i| 0xA0 ^ (i as u8)) +} + +fn message(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(3).wrapping_add(1)).collect() +} + +/// One-shot Sec 6.1 over the toy permutation `P`, detached: the ciphertext and the tag. +fn encrypt>( + nonce: &[u8; NONCE_LEN], + aad: &[u8], + plaintext: &[u8], +) -> (Vec, [u8; TAG_LEN]) { + let mut ct = vec![0u8; plaintext.len()]; + let (written, tag) = + Ccm::::encrypt_detached_out( + &toy_key(), + nonce, + aad, + plaintext, + &mut ct, + ) + .unwrap(); + assert_eq!(written, plaintext.len(), "CCM never expands the payload"); + (ct, tag) +} + +/// Sec 6.1 through the streaming API over `P`, `chunk` bytes per `do_encrypt_update`. +fn stream_encrypt>( + nonce: &[u8; NONCE_LEN], + aad: &[u8], + plaintext: &[u8], + chunk: usize, +) -> (Vec, [u8; TAG_LEN]) { + let mut ccm = Ccm::::new( + &toy_key(), + nonce, + aad, + plaintext.len(), + ) + .unwrap(); + let mut data = plaintext.to_vec(); + for piece in data.chunks_mut(chunk) { + ccm.do_encrypt(piece).unwrap(); + } + let tag = ccm.do_encrypt_final().unwrap(); + (data, tag) +} + +/// Sec 6.2 through the streaming API over `P`, `chunk` bytes per `do_decrypt_update`. +fn stream_decrypt>( + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + chunk: usize, +) -> Result, SymmetricCipherError> { + let mut ccm = Ccm::::new( + &toy_key(), + nonce, + aad, + ciphertext.len(), + )?; + let mut data = ciphertext.to_vec(); + for piece in data.chunks_mut(chunk) { + ccm.do_decrypt_update(piece)?; + } + ccm.do_decrypt_final(tag)?; + Ok(data) +} + +// ---- the forward-cipher-only rule --------------------------------------------------------- + +/// Sec 3: "Only the forward cipher function of the block cipher algorithm is used within these +/// primitives", and Sec 5.1: "the CCM mode does not require the inverse cipher function". So +/// neither direction may reach the inverse cipher: decryption is CTR against the same keystream +/// (Sec 6.2 steps 3 and 5) and a CBC-MAC over the recovered plaintext (step 9), all forward. +/// `ForwardOnlyToy` panics from every inverse entry point, and the message is long enough that +/// the four-block, pair, single-block and partial-block keystream paths all run. +#[test] +fn neither_direction_uses_the_inverse_cipher() { + let nonce = pinned_nonce(); + // Long enough to span two MAC blocks, so the AAD's own zero pad (A.2.2) runs as well. + let aad = b"a header long enough to run over into a second CBC-MAC block"; + let plaintext = message(11 * TOY_LEN + 5); + + let (ct, tag) = encrypt::(&nonce, aad, &plaintext); + let mut back = vec![0u8; plaintext.len()]; + ForwardOnlyCcm::::decrypt_detached_out( + &toy_key(), + &nonce, + aad, + &ct, + &tag, + &mut back, + ) + .unwrap(); + assert_eq!(back, plaintext, "all paths, forward cipher only"); + + // Byte by byte, so the partial-block keystream path runs in both directions too. + assert_eq!( + stream_encrypt::(&nonce, aad, &plaintext, 1), + (ct.clone(), tag), + "byte path, forward cipher only" + ); + assert_eq!(stream_decrypt::(&nonce, aad, &ct, &tag, 1).unwrap(), plaintext); + + // The forward-only toy must agree with the real one, or the above proves nothing. + assert_eq!(encrypt::(&nonce, aad, &plaintext), (ct, tag), "the two toys must agree"); +} + +// ---- the batched keystream paths ---------------------------------------------------------- + +/// The counter half batches, in both directions, and the CBC-MAC half does not. +/// +/// Sec 6.1 steps 5 and 6 encipher the counter blocks `Ctr_j`, which A.3 forms from `j` alone, so +/// they are independent of each other and may go through `encrypt_2blocks`; step 3's +/// `Yi = CIPH_K(Bi XOR Yi-1)` depends on the previous output and cannot. `SwappedPairToy` +/// returns its two pair results in the wrong order while its single-block method is correct, so +/// with it: the ciphertext of two whole blocks differs from `Toy`'s (the pair path is taken), the +/// tag is *identical* (the MAC never batches, and `S0` is one block), and one block per call +/// avoids the pair path and agrees with `Toy` entirely. +#[test] +fn the_pair_path_is_really_used_in_both_directions_and_the_mac_never_batches() { + let nonce = pinned_nonce(); + let plaintext = message(2 * TOY_LEN); + let (ct, tag) = encrypt::(&nonce, b"aad", &plaintext); + + let (swapped_ct, swapped_tag) = encrypt::(&nonce, b"aad", &plaintext); + assert_ne!(swapped_ct, ct, "CCM encryption must use the pair path"); + assert_eq!(swapped_tag, tag, "the CBC-MAC and S0 are single-block, so the tag must not change"); + + let (single_ct, single_tag) = + stream_encrypt::(&nonce, b"aad", &plaintext, TOY_LEN); + assert_eq!(single_ct, ct, "the single-block path must not pair"); + assert_eq!(single_tag, tag); + + // Decryption: the swapped keystream recovers the wrong plaintext (Sec 6.2 step 5), which is + // what the MAC then absorbs (step 9), so the tag check fails. + let mut dec = SwappedCcm::::new(&toy_key(), &nonce, b"aad", ct.len()).unwrap(); + let mut back = ct.clone(); + dec.do_decrypt_update(&mut back).unwrap(); + assert_ne!(back, plaintext, "CCM decryption must use the pair path"); + assert!(matches!(dec.do_decrypt_final(&tag), Err(SymmetricCipherError::AEADTagCheckFailed))); +} + +/// The four-block path must be taken, in both directions, and only for full fours. +/// `SwappedFourToy` rotates its four `encrypt_4blocks` results while its pair and single-block +/// methods are correct. +#[test] +fn the_four_block_path_is_really_used_in_both_directions() { + let nonce = pinned_nonce(); + let plaintext = message(5 * TOY_LEN); + let (ct, tag) = encrypt::(&nonce, b"aad", &plaintext); + + let (swapped_ct, swapped_tag) = encrypt::(&nonce, b"aad", &plaintext); + assert_ne!(swapped_ct, ct, "five blocks must go through encrypt_4blocks"); + assert_eq!(swapped_tag, tag, "the CBC-MAC and S0 are single-block, so the tag must not change"); + + // Two blocks per call uses pairs only, so the rotated-four toy is correct there. + let (pairs_ct, pairs_tag) = + stream_encrypt::(&nonce, b"aad", &plaintext, 2 * TOY_LEN); + assert_eq!(pairs_ct, ct, "pairs must not use the four path"); + assert_eq!(pairs_tag, tag); + + let mut dec = SwappedFourCcm::::new(&toy_key(), &nonce, b"aad", ct.len()).unwrap(); + let mut back = ct.clone(); + dec.do_decrypt_update(&mut back).unwrap(); + assert_ne!(back, plaintext, "decryption must batch fours too"); + assert!(matches!(dec.do_decrypt_final(&tag), Err(SymmetricCipherError::AEADTagCheckFailed))); +} + +// ---- call sequencing ---------------------------------------------------------------------- + +/// Call chunking must be invisible in both directions: every two-call split of a 101-byte +/// message, so that the second call resumes a keystream block and a CBC-MAC block left open at +/// every possible offset, gives the one-shot's ciphertext, tag and plaintext. +/// +/// 101 bytes is six blocks and a tail, so a small opening call leaves the second one at least a +/// four-block batch, a pair batch and a remainder: the batched blocks must line up with the +/// keystream the small call left partway through, not silently skip over it. None of the Appendix +/// C vectors is long enough for that -- the largest, C.4, is two blocks -- and every chunking +/// `sp800_38c_tests.rs` sweeps is uniform, so a call that resumes an open block there is always a +/// short final remainder, never one big enough to batch. +#[test] +fn every_split_agrees_with_the_one_shot_in_both_directions() { + let nonce = pinned_nonce(); + let aad = b"header"; + let plaintext = message(6 * TOY_LEN + 5); + let (ct, tag) = encrypt::(&nonce, aad, &plaintext); + + for split in 0..=plaintext.len() { + let mut enc = ToyCcm::::new(&toy_key(), &nonce, aad, plaintext.len()).unwrap(); + let mut streamed = plaintext.clone(); + let (head, rest) = streamed.split_at_mut(split); + enc.do_encrypt(head).unwrap(); + enc.do_encrypt(rest).unwrap(); + assert_eq!(enc.do_encrypt_final().unwrap(), tag, "tag, split at {split}"); + assert_eq!(streamed, ct, "ciphertext, split at {split}"); + + let mut dec = ToyCcm::::new(&toy_key(), &nonce, aad, ct.len()).unwrap(); + let mut back = ct.clone(); + let (head, rest) = back.split_at_mut(split); + dec.do_decrypt_update(head).unwrap(); + dec.do_decrypt_update(rest).unwrap(); + dec.do_decrypt_final(&tag).unwrap_or_else(|e| panic!("tag check, split at {split}: {e:?}")); + assert_eq!(back, plaintext, "plaintext, split at {split}"); + } +} + +/// The payload length declared to `new` is inside `B0` (A.2.1 Table 2's `Q`), so neither +/// direction may be given more data than declared, nor finalized with less: either would produce +/// or verify a tag against a `B0` no counterpart could reproduce. A refused update consumes +/// nothing, so the flow is still usable. +#[test] +fn both_directions_hold_to_the_declared_length() { + let nonce = pinned_nonce(); + let plaintext = message(8); + let (ct, tag) = encrypt::(&nonce, &[], &plaintext); + + // Encrypting: too much is refused, and finalizing short is refused. + let mut enc = ToyCcm::::new(&toy_key(), &nonce, &[], 8).unwrap(); + let mut too_much = [0x77u8; 9]; + assert!( + matches!(enc.do_encrypt(&mut too_much), Err(SymmetricCipherError::StateError(_))), + "9 bytes against a declared 8" + ); + assert_eq!(too_much, [0x77u8; 9], "a refused update must not touch the data"); + let mut some = plaintext[..4].to_vec(); + enc.do_encrypt(&mut some).expect("4 of the 8 declared bytes"); + assert!( + matches!(enc.do_encrypt_final(), Err(SymmetricCipherError::StateError(_))), + "finalizing 4 bytes short" + ); + + // Decrypting: the same two refusals. + let mut dec = ToyCcm::::new(&toy_key(), &nonce, &[], 8).unwrap(); + let mut too_much = [0x77u8; 9]; + assert!( + matches!(dec.do_decrypt_update(&mut too_much), Err(SymmetricCipherError::StateError(_))), + "9 bytes against a declared 8" + ); + assert_eq!(too_much, [0x77u8; 9], "a refused update must not touch the data"); + // ...and must not have debited the declared length either: the 8 genuine bytes still verify. + let mut back = ct.clone(); + dec.do_decrypt_update(&mut back).unwrap(); + dec.do_decrypt_final(&tag).unwrap(); + assert_eq!(back, plaintext); + + let mut dec = ToyCcm::::new(&toy_key(), &nonce, &[], 8).unwrap(); + let mut some = ct[..4].to_vec(); + dec.do_decrypt_update(&mut some).unwrap(); + assert!( + matches!(dec.do_decrypt_final(&tag), Err(SymmetricCipherError::StateError(_))), + "finalizing 4 bytes short" + ); +} + +// ---- the parameters ----------------------------------------------------------------------- + +/// `TAG_LEN` changes the tag and nothing else -- and, unlike GCM's `MSB_t` truncation, a shorter +/// CCM tag is **not** a prefix of a longer one. +/// +/// A.3 Table 4 keeps `t` out of the counter blocks (bits 3, 4 and 5 "shall also be set to 0"), +/// so the ciphertext is the same for every `t`. A.2.1 Table 1 puts `[(t-2)/2]_3` in `B0`'s flags +/// octet, so `Y0` and every `Yi` after it change with `t` (Sec 6.1 steps 2 and 3), and with them +/// the whole of `T = MSB_Tlen(Yr)`. With `Toy`, which permutes each byte independently, the +/// differing flags octet is guaranteed to propagate to byte 0 of every `Yi`, so the "not a +/// prefix" assertion holds by construction rather than by luck. All seven of A.1's `t` values +/// round-trip. +#[test] +fn tag_length_changes_the_tag_but_not_the_ciphertext_and_tags_do_not_nest() { + let nonce = pinned_nonce(); + let aad = b"associated"; + let plaintext = message(23); + let (ct16, tag16) = encrypt::(&nonce, aad, &plaintext); + + macro_rules! check_tag_len { + ($t:literal) => {{ + type Enc = Ccm; + type Dec = Ccm; + let mut ct = vec![0u8; plaintext.len()]; + let (_, tag) = + Enc::encrypt_detached_out(&toy_key(), &nonce, aad, &plaintext, &mut ct).unwrap(); + assert_eq!(ct, ct16, "ciphertext must not depend on TAG_LEN ({})", $t); + let mut pt = vec![0u8; plaintext.len()]; + Dec::decrypt_detached_out(&toy_key(), &nonce, aad, &ct, &tag, &mut pt).unwrap(); + assert_eq!(pt, plaintext, "TAG_LEN={} round trip", $t); + tag.to_vec() + }}; + } + let shorter = [ + check_tag_len!(4), + check_tag_len!(6), + check_tag_len!(8), + check_tag_len!(10), + check_tag_len!(12), + check_tag_len!(14), + ]; + assert_eq!(check_tag_len!(16), tag16.to_vec(), "the reference is TAG_LEN=16 itself"); + for tag in &shorter { + assert_ne!( + &tag[..], + &tag16[..tag.len()], + "a {}-byte tag must not be a prefix of the 16-byte tag: t is inside B0", + tag.len() + ); + } +} + +/// Every nonce length A.1 permits, `n` in `7..=13`, works, and each one implies its own payload +/// limit: `q = 15 - n` and "by definition, p < 2^8q", which `Ccm::MAX_PAYLOAD_LEN` exposes. +/// `sp800_38c_tests.rs` reaches `n` of 7, 8, 12 and 13 through Appendix C; 9, 10 and 11 are +/// reached only here. Over the toy alone, since where the nonce goes (A.2.1 Table 2, A.3 Table 3) +/// is the mode's business and not the permutation's. +#[test] +fn every_permitted_nonce_length_works() { + fn round_trip( + key: &KeyMaterial, + expected_max_payload: u64, + ) where + P: ElectronicCodeBook, + { + // The full 16-byte tag, deliberately. `Toy` permutes each byte independently, so under + // it the CBC-MAC is sixteen independent byte-chains and a `t`-byte tag witnesses only the + // first `t` of them; the last nonce octet flipped below sits at block octet `N`, which an + // 8-byte tag would never see once `N >= 8`. Real AES mixes every byte into every other, + // so this is a limit of the toy, not of the mode. + type Enc = Ccm; + type Dec = Ccm; + assert_eq!( + Enc::::MAX_PAYLOAD_LEN, + expected_max_payload, + "n = {N}: the payload limit 2^8q - 1 that q = 15 - n implies" + ); + + let nonce: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(11).wrapping_add(3)); + let plaintext = message(100); + let mut ct = vec![0u8; plaintext.len()]; + let (_, tag) = + Enc::::encrypt_detached_out(key, &nonce, b"aad", &plaintext, &mut ct) + .unwrap(); + assert_ne!(ct, plaintext, "nonce length {N}: must actually encrypt"); + + let mut back = vec![0u8; plaintext.len()]; + Dec::::decrypt_detached_out(key, &nonce, b"aad", &ct, &tag, &mut back) + .unwrap(); + assert_eq!(back, plaintext, "nonce length {N}: round trip"); + + // The last nonce octet sits right before `Q` in B0 (Table 2) and before the counter in + // every `Ctr_i` (Table 3); flipping it must change both and so fail the check. + let mut wrong = nonce; + wrong[N - 1] ^= 0x01; + assert!( + matches!( + Dec::::decrypt_detached_out( + key, &wrong, b"aad", &ct, &tag, &mut back + ), + Err(SymmetricCipherError::AEADTagCheckFailed) + ), + "nonce length {N}: the nonce is authenticated" + ); + } + + // `q = 8` makes `2^8q` exactly `2^64`, which does not fit a `u64`, so the bound is `u64::MAX`. + let toy = toy_key(); + round_trip::(&toy, u64::MAX); + round_trip::(&toy, (1 << 56) - 1); + round_trip::(&toy, (1 << 48) - 1); + round_trip::(&toy, (1 << 40) - 1); + round_trip::(&toy, (1 << 32) - 1); + round_trip::(&toy, (1 << 24) - 1); + round_trip::(&toy, (1 << 16) - 1); +} + +/// Which entry points release unauthenticated plaintext on a forgery, pinned side by side. +/// +/// Sec 6.2: "When the error message INVALID is returned, the payload P and the MAC T shall not +/// be revealed." The one-shots honour that -- the caller's buffer comes back zeroized -- because +/// they have the whole ciphertext before they start. Neither streaming path can: Sec 6.2 +/// recovers `P` (step 5) before it can verify it (step 10), and both the inherent +/// `do_decrypt_update` and the fixed-frame `CcmDecryptor` release `P` as it is recovered rather +/// than hold the frame back, so by the time the final rejects the tag the plaintext is already +/// in the caller's buffer, as their docs warn. Pinning the difference makes it a documented +/// property rather than an accident. +#[test] +fn one_shots_release_nothing_on_forgery_but_the_streams_do() { + let nonce = pinned_nonce(); + let plaintext = *b"do not trust me yet"; + let (ct, mut tag) = encrypt::(&nonce, b"aad", &plaintext); + tag[0] ^= 0xFF; // forge it + + // The inherent one-shot: verify-then-return, so a forged tag leaves nothing but zeros. + let mut one_shot = [0xEEu8; 19]; + assert!(matches!( + ToyCcm::::decrypt_detached_out( + &toy_key(), + &nonce, + b"aad", + &ct, + &tag, + &mut one_shot + ), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(one_shot, [0u8; 19], "the one-shot must zeroize its buffer on a forged tag"); + + // The inherent stream: the plaintext is in the buffer before the tag is ever looked at, and + // rejecting the tag cannot take it back. + let mut dec = ToyCcm::::new(&toy_key(), &nonce, b"aad", ct.len()).unwrap(); + let mut streamed = ct.clone(); + dec.do_decrypt_update(&mut streamed).unwrap(); + assert_eq!(&streamed[..], &plaintext[..], "the stream already produced plaintext"); + assert!(matches!(dec.do_decrypt_final(&tag), Err(SymmetricCipherError::AEADTagCheckFailed))); + assert_eq!(&streamed[..], &plaintext[..], "...and a rejected tag cannot take it back"); + + // The fixed-frame decryptor is the same stream behind the trait: the payload is released by + // the update that brings it, the finals release nothing, and a rejected tag cannot take it + // back. The detached final leaves its (unused) buffer alone rather than zeroizing it, since + // there is nothing of the plaintext in it to zeroize. + type Dec = CcmDecryptor; + + let mut dec = Dec::do_decrypt_init(&toy_key(), &nonce).unwrap(); + dec.do_update_aad(b"aad").unwrap(); + let mut streamed = [0u8; 19]; + assert_eq!(dec.do_decrypt_out(&ct, &mut streamed).unwrap(), 19, "released mid-stream"); + assert_eq!(&streamed[..], &plaintext[..], "the stream already produced plaintext"); + let mut detached = [0xEEu8; 16]; + assert!(matches!( + dec.do_decrypt_final_detachedtag_out(&tag, &mut detached), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(&streamed[..], &plaintext[..], "...and a rejected tag cannot take it back"); + assert_eq!(detached, [0u8; 16], "the detached final only zeroes its buffer"); + + let mut inline = ct.clone(); + inline.extend_from_slice(&tag); + let mut dec = Dec::do_decrypt_init(&toy_key(), &nonce).unwrap(); + dec.do_update_aad(b"aad").unwrap(); + let mut streamed = [0u8; 19]; + assert_eq!(dec.do_decrypt_out(&inline, &mut streamed).unwrap(), 19); + assert_eq!(&streamed[..], &plaintext[..]); + assert!(matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::AEADTagCheckFailed))); +} + +/// Tests a large payload that would blow the Linux stack limit if we try to hard-copy it. +/// Tests the inherent APIs on CCM. +#[test] +fn test_large_payload_inherent() { + // 5 mb payload + const LARGE_LEN: usize = 5 * 1024 * 1024; + let key = toy_key(); + let nonce = pinned_nonce(); + let aad = b"header"; + let plaintext = message(LARGE_LEN); + + // round-tripped though the inherent CCM interface + let mut ct = vec![0u8; LARGE_LEN]; + let (written, tag) = + ToyCcm::::encrypt_detached_out(&key, &nonce, aad, &plaintext, &mut ct).unwrap(); + assert_eq!(written, LARGE_LEN); + assert_ne!(ct, plaintext, "must actually encrypt"); + + let mut back = vec![0u8; LARGE_LEN]; + let n = ToyCcm::::decrypt_detached_out(&key, &nonce, aad, &ct, &tag, &mut back) + .unwrap(); + assert_eq!(n, LARGE_LEN); + assert_eq!(back, plaintext, "inherent round trip"); +} diff --git a/crypto/cipher/tests/modes/cfb8_tests.rs b/crypto/cipher/tests/modes/cfb8_tests.rs new file mode 100644 index 00000000..eac113f4 --- /dev/null +++ b/crypto/cipher/tests/modes/cfb8_tests.rs @@ -0,0 +1,682 @@ +//! Structural tests for CFB8, driven by a toy permutation. +//! +//! These check the properties of the *mode* -- the shift register, the one-byte segment, call +//! sequencing at arbitrary byte boundaries, the batch split on the decrypt side, direction typing, +//! SP 800-38A Appendix D error propagation, and the "forward cipher function only" rule of +//! Sec 6.3 -- independently of any real cipher. The known-answer tests against SP 800-38A +//! Appendix F.3.7-F.3.12 are in the `aes` crate, `crypto/aes/tests/sp800_38a_cfb8_tests.rs`, and +//! the ACVP CFB8 set in `cfb8_bc-test-data.rs` beside it. +//! +//! The toy's own conformance to [`ElectronicCodeBook`] is pinned once, by +//! `the_toy_permutation_conforms_to_the_trait` in `cbc_tests.rs`; it is the same `Toy` here, so it +//! is not re-run. + +mod common; + +use bouncycastle_cipher::modes::{Cfb, Cfb8}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkStreamCipher; +use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; + +type ToyCfb8 = Cfb8; +type SwappedCfb8 = Cfb8; +type ForwardOnlyCfb8 = Cfb8; +type SwappedFourCfb8 = Cfb8; + +/// `do_encrypt_inplace`, by value. +fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { + let mut data = plaintext.to_vec(); + e.do_encrypt_inplace(&mut data).unwrap(); + data +} + +/// `do_decrypt_inplace`, by value. +fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) -> Vec { + let mut data = ciphertext.to_vec(); + d.do_decrypt_inplace(&mut data).unwrap(); + data +} + +/// `do_decrypt_inplace` in `chunk`-byte calls, by value. The last call may be shorter. +fn dec_chunked( + d: &mut impl StreamCipherDecryptor, + ciphertext: &[u8], + chunk: usize, +) -> Vec { + let mut data = ciphertext.to_vec(); + for piece in data.chunks_mut(chunk) { + d.do_decrypt_inplace(piece).unwrap(); + } + data +} + +/// A pinned IV, so two runs are comparable. Encryption never accepts one, so it is fed through the +/// fixed-output RNG that `do_encrypt_init_rng` takes. +fn pinned_iv() -> [u8; TOY_LEN] { + core::array::from_fn(|i| 0xF0 ^ (i as u8)) +} + +fn pinned_rng(iv: [u8; TOY_LEN]) -> FixedSeedRNG { + FixedSeedRNG::::new(iv) +} + +fn pinned_encryptor(iv: [u8; TOY_LEN]) -> ToyCfb8 { + let (enc, got) = ToyCfb8::::do_encrypt_init_rng(&toy_key(), &mut pinned_rng(iv)) + .expect("encrypt init"); + assert_eq!(got, iv, "the pinned RNG should reproduce the IV"); + enc +} + +fn pinned_decryptor(iv: [u8; TOY_LEN]) -> ToyCfb8 { + ToyCfb8::::do_decrypt_init(&toy_key(), &iv).expect("decrypt init") +} + +/// A test message of `len` bytes with no repeating structure at the block size. +fn message(len: usize) -> Vec { + (0..len).map(|i| (i * 7 + (i / TOY_LEN) * 31 + 1) as u8).collect() +} + +/// The chunk sizes every "chunking must not matter" test uses: below, at, either side of and above +/// both the 8-byte batch and the 16-byte block. +const CHUNKINGS: [usize; 11] = [1, 2, 3, 7, 8, 9, 15, 16, 17, 32, 100]; + +// ---- the mode against the shared framework ------------------------------------------------ + +#[test] +fn cfb8_conforms_to_the_stream_cipher_framework() { + TestFrameworkStreamCipher::new() + .test::, ToyCfb8>(); +} + +// ---- the spec equations ------------------------------------------------------------------- + +/// CFB with `s = 8` from SP 800-38A Sec 6.3, written out longhand against the raw permutation: +/// +/// ```text +/// I1 = IV; Ij = LSB_{b-8}(I_{j-1}) | C_{j-1}; Oj = CIPH_K(Ij); Cj = Pj XOR MSB_8(Oj) +/// ``` +/// +/// The shift is written here as an explicit copy of `Ij[1..]` followed by the ciphertext byte, so +/// it is an independent statement of the rule rather than a second call to the same `rotate_left` +/// the implementation uses. +/// +/// This is the independent reference the mode is checked against below. It uses only +/// [`ElectronicCodeBook::encrypt_block`], because that is all the spec calls for. +fn reference_cfb8(perm: &Toy, iv: [u8; TOY_LEN], input: &[u8], encrypt: bool) -> Vec { + let mut chain = iv; // I1 = IV + let mut out = Vec::with_capacity(input.len()); + for &byte in input { + let mut o = chain; + perm.encrypt_block(&mut o); // Oj = CIPH_K(Ij) + let result = byte ^ o[0]; // Cj = Pj XOR MSB_8(Oj) + + // I_{j+1} = LSB_{b-8}(Ij) | C#_j -- always the *ciphertext* byte, whichever direction. + let cj = if encrypt { result } else { byte }; + let mut next = [0u8; TOY_LEN]; + next[..TOY_LEN - 1].copy_from_slice(&chain[1..]); + next[TOY_LEN - 1] = cj; + chain = next; + + out.push(result); + } + out +} + +/// The mode must reproduce the Sec 6.3 `s = 8` equations exactly, in both directions, at lengths +/// either side of the shift register's own width. +/// +/// A reference implementation is a weak test on its own -- both could be wrong the same way -- so +/// this also pins the anchors that follow directly from the equations and that no plausible +/// mistake preserves: `C1 = P1 XOR MSB_8(CIPH_K(IV))`, and the second input block. +#[test] +fn the_mode_matches_the_spec_equations() { + let key = toy_key(); + let iv = pinned_iv(); + let perm = >::new(&key).unwrap(); + + for len in [1, 2, TOY_LEN - 1, TOY_LEN, TOY_LEN + 1, 3 * TOY_LEN + 5] { + let plaintext = message(len); + + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + assert_eq!( + ct, + reference_cfb8(&perm, iv, &plaintext, true), + "len {len}: encryption must match the Sec 6.3 equations at s = 8" + ); + + let recovered = dec(&mut pinned_decryptor(iv), &ct); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + assert_eq!( + recovered, + reference_cfb8(&perm, iv, &ct, false), + "len {len}: decryption must match the Sec 6.3 equations at s = 8" + ); + } + + let plaintext = message(4); + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + + // Anchor 1: `O1 = CIPH_K(IV)` and `C1 = P1 XOR MSB_8(O1)` -- the *first* byte of the output + // block, the other b - 8 bits discarded. + let mut o1 = iv; + perm.encrypt_block(&mut o1); + assert_eq!(ct[0], plaintext[0] ^ o1[0], "C1 = P1 XOR MSB_8(CIPH_K(IV))"); + + // Anchor 2: `I2 = LSB_{b-8}(IV) | C1`, i.e. the IV without its leading byte, then C1. + let mut i2 = [0u8; TOY_LEN]; + i2[..TOY_LEN - 1].copy_from_slice(&iv[1..]); + i2[TOY_LEN - 1] = ct[0]; + let mut o2 = i2; + perm.encrypt_block(&mut o2); + assert_eq!(ct[1], plaintext[1] ^ o2[0], "C2 = P2 XOR MSB_8(CIPH_K(LSB(IV) | C1))"); + + // Anchor 3: with `P = 0`, the ciphertext is the keystream itself. + assert_eq!( + enc(&mut pinned_encryptor(iv), &[0u8; 2]), + vec![o1[0], { + let mut i = [0u8; TOY_LEN]; + i[..TOY_LEN - 1].copy_from_slice(&iv[1..]); + i[TOY_LEN - 1] = o1[0]; + let mut o = i; + perm.encrypt_block(&mut o); + o[0] + }], + "encrypting zero yields the keystream" + ); +} + +/// CFB8 and CFB128 are different, non-interoperable modes, and they differ from the very first +/// byte: with `s = b` the whole output block is used and the next input block is the ciphertext +/// block, whereas with `s = 8` one byte is used and the register shifts. +/// +/// The first byte of ciphertext is the same in both -- `P1 XOR MSB_8(CIPH_K(IV))` either way -- and +/// everything from the second byte differs. That is the sharp statement of "not a variant", and it +/// is what catches a CFB8 that has quietly become CFB128 or vice versa. +#[test] +fn cfb8_is_not_cfb128() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = message(2 * TOY_LEN); + + let cfb8 = enc(&mut pinned_encryptor(iv), &plaintext); + + let (mut cfb, got) = + Cfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)) + .unwrap(); + assert_eq!(got, iv); + let mut cfb128 = plaintext.clone(); + cfb.do_encrypt_inplace(&mut cfb128).unwrap(); + + assert_eq!(cfb8[0], cfb128[0], "both modes start O1 = CIPH_K(IV), so C1 agrees"); + assert_ne!(cfb8[1..], cfb128[1..], "everything after the first byte must differ"); + + // ...and neither can decrypt the other's ciphertext. + let mut wrong = cfb128.clone(); + ToyCfb8::::decrypt_inplace(&key, &iv, &mut wrong).unwrap(); + assert_ne!(wrong, plaintext, "CFB8 must not decrypt a CFB128 ciphertext"); + + let mut wrong = cfb8.clone(); + Cfb::::decrypt_inplace(&key, &iv, &mut wrong).unwrap(); + assert_ne!(wrong, plaintext, "CFB128 must not decrypt a CFB8 ciphertext"); +} + +/// A stream cipher's ciphertext for a prefix of the message is the prefix of the ciphertext. +#[test] +fn the_ciphertext_of_a_prefix_is_a_prefix_of_the_ciphertext() { + let iv = pinned_iv(); + let plaintext = message(2 * TOY_LEN + 3); + let full = enc(&mut pinned_encryptor(iv), &plaintext); + + for k in 0..=plaintext.len() { + assert_eq!( + enc(&mut pinned_encryptor(iv), &plaintext[..k]), + full[..k], + "encrypting the first {k} bytes" + ); + assert_eq!( + dec(&mut pinned_decryptor(iv), &full[..k]), + plaintext[..k], + "decrypting the first {k} bytes" + ); + } +} + +// ---- the forward-cipher-only rule --------------------------------------------------------- + +/// SP 800-38A Sec 6.3: "The *forward cipher* function is applied to each input block to produce the +/// output blocks" -- in CFB *decryption* as well as encryption. +/// +/// [`ForwardOnlyToy`] panics from `decrypt_block`, `decrypt_2blocks` and `decrypt_4blocks`, so this +/// test fails loudly if either direction of the mode ever reaches the inverse cipher. Every decrypt +/// path is exercised -- fours, pairs and single bytes -- and the result is required to agree with +/// the plain [`Toy`], otherwise the test could pass by not really encrypting anything. +#[test] +fn neither_direction_uses_the_inverse_cipher() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = message(19); + + let (mut e, _) = + ForwardOnlyCfb8::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc(&mut e, &plaintext); + + // One call: four fours, then a pair, then a single byte. + let mut d = ForwardOnlyCfb8::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec(&mut d, &ct), plaintext, "all paths, forward cipher only"); + + // Byte by byte: the single-byte path only. + let mut d = ForwardOnlyCfb8::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "single-byte path, forward cipher only"); + + // The forward-only toy must agree with the real one, or the above proves nothing. + assert_eq!( + enc(&mut pinned_encryptor(iv), &plaintext), + ct, + "the two toys must agree going forward" + ); +} + +/// The decryptor must shift the **ciphertext** byte into the register, not the plaintext it just +/// recovered. +/// +/// Getting this wrong is invisible in the first byte -- `O1 = CIPH_K(IV)` either way -- and wrong +/// from the second onwards. An encryptor run over ciphertext is exactly that mistake, so byte 1 +/// agreeing while byte 2 disagrees is the signature of the bug, and is what this asserts. +#[test] +fn the_decryptor_shifts_in_ciphertext_not_plaintext() { + let iv = pinned_iv(); + let plaintext = message(2 * TOY_LEN); + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + assert_ne!(ct[0], plaintext[0], "the two feedback choices must actually differ here"); + + let wrong = enc(&mut pinned_encryptor(iv), &ct); + assert_eq!(wrong[0], plaintext[0], "byte 1 cannot tell the two apart"); + assert_ne!(wrong[1..], plaintext[1..], "byte 2 onwards must, so the feedback source is pinned"); +} + +// ---- chaining and call sequencing -------------------------------------------------------- + +/// Encrypting a message must not depend on how the calls are chunked, and likewise for decryption, +/// at byte granularity. Every chunking in [`CHUNKINGS`] is checked against the one-call reference in +/// both directions, and every encrypt chunking against every decrypt chunking. +/// +/// For CFB8 the decrypt side is where this bites: chunk sizes that are not multiples of 4 leave the +/// four-byte batch loop with a different remainder each call, so the register has to carry across +/// calls correctly for every alignment. +#[test] +fn call_chunking_does_not_change_the_result() { + let iv = pinned_iv(); + let plaintext = message(3 * TOY_LEN + 7); + + let reference = enc(&mut pinned_encryptor(iv), &plaintext); + assert_eq!(dec(&mut pinned_decryptor(iv), &reference), plaintext); + + for &enc_chunk in &CHUNKINGS { + let mut ct = plaintext.clone(); + let mut e = pinned_encryptor(iv); + for piece in ct.chunks_mut(enc_chunk) { + e.do_encrypt_inplace(piece).unwrap(); + } + assert_eq!(ct, reference, "encrypting in {enc_chunk}-byte calls"); + + for &dec_chunk in &CHUNKINGS { + let pt = dec_chunked(&mut pinned_decryptor(iv), &ct, dec_chunk); + assert_eq!( + pt, plaintext, + "encrypted in {enc_chunk}-byte calls, decrypted in {dec_chunk}-byte calls" + ); + } + } + + // Empty calls anywhere are no-ops. + let mut e = pinned_encryptor(iv); + e.do_encrypt_inplace(&mut []).unwrap(); + let mut ct = plaintext.clone(); + e.do_encrypt_inplace(&mut ct[..5]).unwrap(); + e.do_encrypt_inplace(&mut []).unwrap(); + e.do_encrypt_inplace(&mut ct[5..]).unwrap(); + e.do_encrypt_inplace(&mut []).unwrap(); + assert_eq!(ct, reference, "empty calls must not disturb the state"); +} + +/// The same equivalence, generic over the permutation, at a length that leaves the decrypt-side +/// batch loop with a different remainder under every chunking. +/// +/// `call_chunking_does_not_change_the_result` proves the property over [`Toy`] at 55 bytes. This +/// repeats it at 171 bytes, which is 42 four-byte batches and a 3-byte tail, and runs it over +/// [`ForwardOnlyToy`] as well, so the chunked decryptions that reach the batch paths are shown to +/// do so without the inverse cipher. The AES coverage (the `aes` crate's `sp800_38a_cfb8_tests.rs` +/// and `cfb8_bc-test-data.rs`) chunks against *published* ciphertext; this is the direct +/// single-call-versus-chunked comparison, kept free of an AES dependency. +#[test] +fn chunking_matches_a_single_call_at_every_batch_remainder() { + fn check(name: &str) + where + P: ElectronicCodeBook, + { + let key_bytes: [u8; KEY_LEN] = + core::array::from_fn(|i| (i as u8).wrapping_mul(31).wrapping_add(7)); + let key = + KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .expect("a valid key"); + let iv: [u8; 16] = core::array::from_fn(|i| 0xC3 ^ (i as u8)); + let plaintext: Vec = (0..171).map(|i| (i * 7 + i / 16) as u8).collect(); + + let encryptor = || { + let (enc, got) = Cfb8::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::<16>::new(iv), + ) + .expect("encrypt init"); + assert_eq!(got, iv, "{name}: the pinned RNG should reproduce the IV"); + enc + }; + let decryptor = || { + Cfb8::::do_decrypt_init(&key, &iv).expect("decrypt init") + }; + + // The reference: the whole message in one call. + let mut reference = plaintext.clone(); + encryptor().do_encrypt_inplace(&mut reference).expect("one-call encryption"); + assert_ne!(reference, plaintext, "{name}: the data must actually be encrypted"); + + // ...and the round trip of that, also in one call. + let mut back = reference.clone(); + decryptor().do_decrypt_inplace(&mut back).expect("one-call decryption"); + assert_eq!(back, plaintext, "{name}: one-call round trip"); + + for &enc_chunk in &CHUNKINGS { + let mut ct = plaintext.clone(); + let mut e = encryptor(); + for piece in ct.chunks_mut(enc_chunk) { + e.do_encrypt_inplace(piece).expect("chunked encryption"); + } + assert_eq!(ct, reference, "{name}: encrypting in {enc_chunk}-byte calls"); + + for &dec_chunk in &CHUNKINGS { + let mut pt = ct.clone(); + let mut d = decryptor(); + for piece in pt.chunks_mut(dec_chunk) { + d.do_decrypt_inplace(piece).expect("chunked decryption"); + } + assert_eq!( + pt, plaintext, + "{name}: encrypted in {enc_chunk}-byte calls, decrypted in {dec_chunk}-byte calls" + ); + } + } + } + + check::("Toy"); + check::("ForwardOnlyToy"); +} + +/// The pair path in `do_decrypt_inplace` must actually be taken. +/// +/// [`SwappedPairToy`] returns its two pair results in the wrong order while its single-block method +/// is correct. CFB8 decryption batches through `encrypt_2blocks`, so with this permutation six +/// bytes handed over together come out wrong while the same bytes one at a time come out right. +/// +/// Two, not four: [`SwappedPairToy`]'s `encrypt_4blocks` is two `encrypt_2blocks` calls, so four +/// bytes would also be wrong and would not distinguish the two paths. +#[test] +fn the_pair_path_is_really_used() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = message(2); + + // The correct toy round-trips. + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + assert_eq!(dec(&mut pinned_decryptor(iv), &ct), plaintext); + + // The swapped-pair toy encrypts identically -- CFB8 encryption is serial and never batches. + let (mut e, _) = + SwappedCfb8::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(enc(&mut e, &plaintext), ct, "CFB8 encryption must not use the pair path"); + + // ...but decrypting two bytes together must now be wrong, because the pair path is used. + let mut d = SwappedCfb8::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!(dec(&mut d, &ct), plaintext, "a pair must go through encrypt_2blocks"); + + // One byte at a time avoids the pair path, so it is correct even for this toy. + let mut d = SwappedCfb8::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "the single-byte path must not pair"); +} + +/// The four-byte batch path in `do_decrypt_inplace` must actually be taken, and only for full +/// fours. +/// +/// [`SwappedFourToy`] returns its four `encrypt_4blocks` results rotated while its pair and +/// single-block methods are correct. So five bytes handed over together decrypt wrongly (four +/// batched, then one), while two bytes (a pair) or one at a time decrypt correctly. +#[test] +fn the_four_byte_path_is_really_used() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = message(5); + + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + assert_eq!(dec(&mut pinned_decryptor(iv), &ct), plaintext); + + // The rotated-four toy encrypts identically: CFB8 encryption is serial and never batches. + let (mut e, _) = + SwappedFourCfb8::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(enc(&mut e, &plaintext), ct, "CFB8 encryption must not use the four path"); + + // ...but five bytes together must now be wrong, because the first four go through + // encrypt_4blocks. + let mut d = SwappedFourCfb8::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!(dec(&mut d, &ct), plaintext, "five bytes must go through encrypt_4blocks"); + + // Two bytes use the pair path only, so they are correct even for this toy... + let two = &ct[..2]; + let mut d = SwappedFourCfb8::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec(&mut d, two), plaintext[..2], "pairs must not use the four path"); + + // ...and so is one byte at a time. + let mut d = SwappedFourCfb8::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "the single-byte path must not batch"); +} + +/// The one-shots must produce exactly what the streaming API produces. +#[test] +fn one_shots_agree_with_the_streaming_api() { + let key = toy_key(); + let iv = pinned_iv(); + + for len in [1, 9, 2 * TOY_LEN + 3] { + let plaintext = message(len); + let streamed = enc(&mut pinned_encryptor(iv), &plaintext); + + let mut buf = plaintext.clone(); + let (_, iv_b) = + ToyCfb8::::encrypt_rng_inplace(&key, &mut pinned_rng(iv), &mut buf) + .unwrap(); + assert_eq!(iv_b, iv); + assert_eq!(buf, streamed, "len {len}: one-shot must equal streaming"); + ToyCfb8::::decrypt_inplace(&key, &iv, &mut buf).unwrap(); + assert_eq!(buf, plaintext); + + // The OS-RNG variant round-trips too. Whether the ciphertext *differs* from the plaintext + // is only worth asserting once the message is long enough that coinciding with the + // keystream by chance is negligible -- see `every_length_round_trips_without_padding`. + let mut buf = plaintext.clone(); + let (_, iv_fresh) = ToyCfb8::::encrypt_inplace(&key, &mut buf).unwrap(); + if len >= 8 { + assert_ne!(buf, plaintext); + } + ToyCfb8::::decrypt_inplace(&key, &iv_fresh, &mut buf).unwrap(); + assert_eq!(buf, plaintext); + } +} + +// ---- SP 800-38A Appendix D error propagation --------------------------------------------- + +/// Appendix D, Table D.2 for CFB: a bit error in `Cj` gives "SBE in the decryption of `Cj`" plus +/// "RBE in the decryption of `Cj+1`,...,`Cj+b/s`". With `s = 8` on a 16-byte block, `b/s` is **16**: +/// the corrupted byte enters the shift register at its tail, moves one place per segment and +/// leaves after 16, so byte `j + 16` is the last one it can touch and byte `j + 17` onwards is +/// **exactly correct** again. That self-synchronisation is the property CFB8 is chosen for. +/// +/// `Toy` permutes each byte of the register independently, so a segment's keystream byte depends +/// on the register's *leading* byte alone. The damage is therefore not spread across the window -- +/// "RBE" is the block cipher's diffusion, not the mode's -- but lands entirely on byte `j + 16`, +/// where the corrupted byte has reached the front, and lands there as exactly `rotate_left(1)` of +/// the flipped bit, the toy's per-byte function. That makes the window's far edge exact arithmetic +/// rather than a statistical claim: `j + 16` is damaged and `j + 17` is not, so the width is `b/s` +/// and not one less. `a_ciphertext_bit_error_flips_exactly_that_bit_of_its_own_byte` pins the +/// near edge and the bound at several `j`; this one pins the far edge. +#[test] +fn a_ciphertext_bit_error_damages_exactly_sixteen_following_bytes() { + let iv = pinned_iv(); + let plaintext = message(3 * TOY_LEN); + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + + // Byte 8, so there is a clean prefix, the full 16-byte window and a clean tail. + const J: usize = 8; + for bit in 0..8 { + let flip = 1u8 << bit; + let mut corrupt = ct.clone(); + corrupt[J] ^= flip; + let got = dec(&mut pinned_decryptor(iv), &corrupt); + + assert_eq!(&got[..J], &plaintext[..J], "bit {bit}: earlier bytes are unaffected"); + assert_eq!( + got[J], + plaintext[J] ^ flip, + "bit {bit}: SBE -- exactly the flipped bit, in the targeted byte" + ); + // Bytes j + 1 ..= j + 15: the corrupted byte is in the register but not yet at its front, + // which is all the toy's keystream byte reads, so these come out untouched. A real cipher + // randomises them; the toy cannot show that, and this does not claim it. + assert_eq!( + &got[J + 1..J + TOY_LEN], + &plaintext[J + 1..J + TOY_LEN], + "bit {bit}: the byte-local toy damages nothing until the corrupted byte leads the register" + ); + // Byte j + 16: the corrupted byte is now the register's leading byte, so the keystream + // byte is off by exactly the toy's rotation of the flip. + assert_eq!( + got[J + TOY_LEN], + plaintext[J + TOY_LEN] ^ flip.rotate_left(1), + "bit {bit}: byte j + b/s is the last one damaged, by exactly rotate_left(1) of the flip" + ); + // ...and then it resynchronises, exactly. + assert_eq!( + &got[J + TOY_LEN + 1..], + &plaintext[J + TOY_LEN + 1..], + "bit {bit}: byte j + 17 onwards must be exactly right again" + ); + } +} + +/// The same claim in the direction that needs no cipher diffusion, and so holds for *any* +/// permutation: the damage window is bounded by `b/s` segments, and the SBE lands in the targeted +/// byte. With the toy this is exact arithmetic rather than a statistical argument. +#[test] +fn a_ciphertext_bit_error_flips_exactly_that_bit_of_its_own_byte() { + let iv = pinned_iv(); + let plaintext = message(3 * TOY_LEN); + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + + for j in [0usize, 1, 5, TOY_LEN, 2 * TOY_LEN] { + for bit in 0..8 { + let mut corrupt = ct.clone(); + corrupt[j] ^= 1 << bit; + let got = dec(&mut pinned_decryptor(iv), &corrupt); + + assert_eq!(&got[..j], &plaintext[..j], "byte {j} bit {bit}: earlier bytes unaffected"); + assert_eq!( + got[j], + plaintext[j] ^ (1 << bit), + "byte {j} bit {bit}: exactly that bit of that byte" + ); + // Damage cannot reach past b/s = TOY_LEN segments. + let resync = core::cmp::min(j + 1 + TOY_LEN, plaintext.len()); + assert_eq!( + &got[resync..], + &plaintext[resync..], + "byte {j} bit {bit}: must resynchronise after b/s = {TOY_LEN} segments" + ); + } + } +} + +// ---- IV handling ------------------------------------------------------------------------- + +/// Two encryption flows under the same key must not reuse an IV. +#[test] +fn each_encryption_gets_a_fresh_iv() { + let key = toy_key(); + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..64 { + let (_, iv) = ToyCfb8::::do_encrypt_init(&key).unwrap(); + assert!(seen.insert(iv), "IV repeated across encryptions: {iv:02x?}"); + } +} + +/// Identical plaintext under the same key must give different ciphertext, because the IV differs. +#[test] +fn identical_plaintext_gives_different_ciphertext() { + let key = toy_key(); + let plaintext = [0x77u8; 2 * TOY_LEN]; + + let mut first = plaintext; + ToyCfb8::::encrypt_inplace(&key, &mut first).unwrap(); + let mut second = plaintext; + ToyCfb8::::encrypt_inplace(&key, &mut second).unwrap(); + assert_ne!(first, second); + + // ...and, within one message, a run of identical plaintext bytes must not give a run of + // identical ciphertext bytes: the register changes on every byte. Compared a block at a time + // rather than byte against byte, because two single bytes coincide once in 256 runs by chance + // while two 16-byte halves do so once in 2^128. + assert_ne!( + first[..TOY_LEN], + first[TOY_LEN..], + "the shifting register should break the pattern within a message" + ); +} + +// ---- key handling ------------------------------------------------------------------------ + +#[test] +fn a_key_of_the_wrong_type_is_rejected() { + let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); + let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); + assert!(ToyCfb8::::do_encrypt_init(&seed).is_err()); + assert!(ToyCfb8::::do_decrypt_init(&seed, &[0u8; TOY_LEN]).is_err()); +} + +// ---- every length, no padding ------------------------------------------------------------ + +/// CFB8 is a stream cipher with a one-byte segment: every length round-trips, the ciphertext is +/// exactly as long as the plaintext, and no padding layer is involved. +#[test] +fn every_length_round_trips_without_padding() { + let key = toy_key(); + for len in 0..=(2 * TOY_LEN + 1) { + let plaintext = message(len); + let mut data = plaintext.clone(); + let (n, iv) = ToyCfb8::::encrypt_inplace(&key, &mut data).expect("encryption"); + assert_eq!(n, len, "len {len}: encrypt must report the number of bytes written"); + assert_eq!(data.len(), len, "len {len}: the ciphertext is as long as the plaintext"); + // Only meaningful once the message is long enough that agreeing with the keystream by + // chance is negligible: a 1-byte message coincides with its own ciphertext whenever the + // single keystream byte is zero, which a fresh random IV makes happen about once in 256 + // runs. At 8 bytes the odds are 2^-64. (This is why the assertion is guarded rather than + // dropped: it is worth making, just not at every length.) + if len >= 8 { + assert_ne!(data, plaintext, "len {len}: the data must actually be encrypted"); + } + ToyCfb8::::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); + assert_eq!(data, plaintext, "len {len}: round trip"); + } +} diff --git a/crypto/cipher/tests/modes/cfb_tests.rs b/crypto/cipher/tests/modes/cfb_tests.rs new file mode 100644 index 00000000..923f7591 --- /dev/null +++ b/crypto/cipher/tests/modes/cfb_tests.rs @@ -0,0 +1,772 @@ +//! Structural tests for CFB, driven by a toy permutation. +//! +//! These check the properties of the *mode* -- the keystream construction, chaining, call +//! sequencing at arbitrary byte boundaries, the short final segment, the pair/four-block split on +//! the decrypt side, direction typing, SP 800-38A Appendix D error propagation, and the "forward +//! cipher function only" rule of Sec 6.3 -- independently of any real cipher. The known-answer +//! tests against SP 800-38A Appendix F.3.13-F.3.18 are in the `aes` crate, +//! `crypto/aes/tests/sp800_38a_cfb_tests.rs`, and the ACVP CFB128 set in `cfb_bc-test-data.rs` +//! beside it. +//! +//! The toy's own conformance to [`ElectronicCodeBook`] is pinned once, by +//! `the_toy_permutation_conforms_to_the_trait` in `cbc_tests.rs`; it is the same `Toy` here, so it +//! is not re-run. + +mod common; + +use bouncycastle_cipher::modes::{Cbc, Cfb}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkStreamCipher; +use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; + +type ToyCfb = Cfb; +type SwappedCfb = Cfb; +type ForwardOnlyCfb = Cfb; +type SwappedFourCfb = Cfb; + +/// `do_encrypt_inplace`, by value. +fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { + let mut data = plaintext.to_vec(); + e.do_encrypt_inplace(&mut data).unwrap(); + data +} + +/// `do_decrypt_inplace`, by value. +fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) -> Vec { + let mut data = ciphertext.to_vec(); + d.do_decrypt_inplace(&mut data).unwrap(); + data +} + +/// `do_encrypt_inplace` in `chunk`-byte calls, by value. The last call may be shorter. +fn enc_chunked( + e: &mut impl StreamCipherEncryptor, + plaintext: &[u8], + chunk: usize, +) -> Vec { + let mut data = plaintext.to_vec(); + for piece in data.chunks_mut(chunk) { + e.do_encrypt_inplace(piece).unwrap(); + } + data +} + +/// `do_decrypt_inplace` in `chunk`-byte calls, by value. The last call may be shorter. +fn dec_chunked( + d: &mut impl StreamCipherDecryptor, + ciphertext: &[u8], + chunk: usize, +) -> Vec { + let mut data = ciphertext.to_vec(); + for piece in data.chunks_mut(chunk) { + d.do_decrypt_inplace(piece).unwrap(); + } + data +} + +/// A pinned IV, so two runs are comparable. Encryption never accepts one, so it is fed through the +/// fixed-output RNG that `do_encrypt_init_rng` takes. +fn pinned_iv() -> [u8; TOY_LEN] { + core::array::from_fn(|i| 0xF0 ^ (i as u8)) +} + +fn pinned_rng(iv: [u8; TOY_LEN]) -> FixedSeedRNG { + FixedSeedRNG::::new(iv) +} + +fn pinned_encryptor(iv: [u8; TOY_LEN]) -> ToyCfb { + let (enc, got) = ToyCfb::::do_encrypt_init_rng(&toy_key(), &mut pinned_rng(iv)) + .expect("encrypt init"); + assert_eq!(got, iv, "the pinned RNG should reproduce the IV"); + enc +} + +fn pinned_decryptor(iv: [u8; TOY_LEN]) -> ToyCfb { + ToyCfb::::do_decrypt_init(&toy_key(), &iv).expect("decrypt init") +} + +/// A test message of `len` bytes with no repeating structure at the block size. +fn message(len: usize) -> Vec { + (0..len).map(|i| (i * 7 + (i / TOY_LEN) * 31 + 1) as u8).collect() +} + +/// The chunk sizes every "chunking must not matter" test uses: below, at, just either side of, and +/// well above the block, plus primes that never line up with it. +const CHUNKINGS: [usize; 12] = [1, 3, 5, 7, 15, 16, 17, 31, 32, 33, 64, 100]; + +// ---- the mode against the shared framework ------------------------------------------------ + +#[test] +fn cfb_conforms_to_the_stream_cipher_framework() { + TestFrameworkStreamCipher::new() + .test::, ToyCfb>(); +} + +// ---- the spec equations ------------------------------------------------------------------- + +/// CFB with `s = b` from SP 800-38A Sec 6.3, written out longhand against the raw permutation: +/// +/// ```text +/// I1 = IV; Ij = C_{j-1} (j >= 2); Oj = CIPH_K(Ij); Cj = Pj XOR Oj +/// ``` +/// +/// extended to a message that is not a whole number of blocks by the rule in the [`Cfb`] docs: the +/// last `r` bytes are a short segment, `C#_n = P#_n XOR MSB_{8r}(On)`, and no input block is formed +/// after it. +/// +/// This is the independent reference the mode is checked against below. It uses only +/// [`ElectronicCodeBook::encrypt_block`], because that is all the spec calls for. +fn reference_cfb(perm: &Toy, iv: [u8; TOY_LEN], input: &[u8], encrypt: bool) -> Vec { + let mut chain = iv; // I1 = IV + let mut out = Vec::with_capacity(input.len()); + for segment in input.chunks(TOY_LEN) { + let mut o = chain; + perm.encrypt_block(&mut o); // Oj = CIPH_K(Ij) + // C#_j = P#_j XOR MSB_s(Oj): a whole block, or the leading bytes of Oj for a short segment. + let result: Vec = segment.iter().zip(o.iter()).map(|(d, o)| d ^ o).collect(); + if segment.len() == TOY_LEN { + // I_{j+1} is always the *ciphertext* block, whichever direction we are going. + let cj = if encrypt { &result[..] } else { segment }; + chain.copy_from_slice(cj); + } + out.extend_from_slice(&result); + } + out +} + +/// The mode must reproduce the Sec 6.3 equations exactly, in both directions, for whole blocks and +/// for a message ending in a short segment. +/// +/// A reference implementation is a weak test on its own -- both could be wrong the same way -- so +/// this also pins the two anchors that follow directly from the equations and that no plausible +/// mistake preserves: `C1 = P1 XOR CIPH_K(IV)`, and encrypting an all-zero block reveals the +/// keystream block itself. +#[test] +fn the_mode_matches_the_spec_equations() { + let key = toy_key(); + let iv = pinned_iv(); + let perm = >::new(&key).unwrap(); + + for len in [5 * TOY_LEN, 5 * TOY_LEN + 9, TOY_LEN - 1, 1] { + let plaintext = message(len); + + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + assert_eq!( + ct, + reference_cfb(&perm, iv, &plaintext, true), + "len {len}: encryption must match the Sec 6.3 equations" + ); + + let recovered = dec(&mut pinned_decryptor(iv), &ct); + assert_eq!(recovered, plaintext, "len {len}: round trip"); + assert_eq!( + recovered, + reference_cfb(&perm, iv, &ct, false), + "len {len}: decryption must match the Sec 6.3 equations" + ); + } + + let plaintext = message(3 * TOY_LEN); + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + + // Anchor 1: `O1 = CIPH_K(IV)` and `C1 = P1 XOR O1`. + let mut o1 = iv; + perm.encrypt_block(&mut o1); + let expected_c1: Vec = + plaintext[..TOY_LEN].iter().zip(o1.iter()).map(|(p, o)| p ^ o).collect(); + assert_eq!(&ct[..TOY_LEN], &expected_c1[..], "C1 = P1 XOR CIPH_K(IV)"); + + // Anchor 2: with `P1 = 0`, `C1 = O1`. CFB is a keystream mode, and this is what that means. + assert_eq!( + enc(&mut pinned_encryptor(iv), &[0u8; TOY_LEN]), + &o1[..], + "encrypting zero yields the keystream" + ); + + // ...and CFB is not CBC: CBC computes `CIPH_K(P1 XOR IV)`, CFB computes `P1 XOR CIPH_K(IV)`. + let (mut cbc, _) = + Cbc::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)) + .unwrap(); + let mut cbc_c1: [u8; TOY_LEN] = plaintext[..TOY_LEN].try_into().unwrap(); + cbc.do_encrypt_inplace(&mut cbc_c1).unwrap(); + assert_ne!(&cbc_c1[..], &ct[..TOY_LEN], "CFB must not agree with CBC"); +} + +// ---- the short final segment -------------------------------------------------------------- + +/// A message that is not a whole number of blocks ends in a short segment, and its ciphertext is +/// the plaintext XOR the *leading* bytes of the output block -- `MSB_{8r}(On)` -- for every `r`. +/// +/// Checked at the first segment (against `CIPH_K(IV)`) and after two whole blocks (against +/// `CIPH_K(C2)`), so both the "only segment" and "final segment" cases are covered. +#[test] +fn the_final_short_segment_is_xored_with_the_leading_keystream_bytes() { + let key = toy_key(); + let iv = pinned_iv(); + let perm = >::new(&key).unwrap(); + + let mut o1 = iv; + perm.encrypt_block(&mut o1); + + let two_blocks = message(2 * TOY_LEN); + let two_blocks_ct = enc(&mut pinned_encryptor(iv), &two_blocks); + let mut o3: [u8; TOY_LEN] = two_blocks_ct[TOY_LEN..].try_into().unwrap(); + perm.encrypt_block(&mut o3); + + for r in 1..TOY_LEN { + // The only segment. + let short = message(r); + let ct = enc(&mut pinned_encryptor(iv), &short); + let expected: Vec = short.iter().zip(o1.iter()).map(|(p, o)| p ^ o).collect(); + assert_eq!(ct, expected, "r = {r}: C#_1 = P#_1 XOR MSB(O1)"); + assert_eq!(dec(&mut pinned_decryptor(iv), &ct), short, "r = {r}: round trip"); + + // The final segment after two whole blocks. + let mut long = two_blocks.clone(); + long.extend_from_slice(&message(2 * TOY_LEN + r)[2 * TOY_LEN..]); + let ct = enc(&mut pinned_encryptor(iv), &long); + assert_eq!( + &ct[..2 * TOY_LEN], + &two_blocks_ct[..], + "r = {r}: the whole blocks are unchanged" + ); + let expected: Vec = + long[2 * TOY_LEN..].iter().zip(o3.iter()).map(|(p, o)| p ^ o).collect(); + assert_eq!(&ct[2 * TOY_LEN..], &expected[..], "r = {r}: C#_3 = P#_3 XOR MSB(O3)"); + assert_eq!(dec(&mut pinned_decryptor(iv), &ct), long, "r = {r}: round trip"); + } +} + +/// A stream cipher's ciphertext for a prefix of the message is the prefix of the ciphertext: the +/// bytes after position `k` cannot influence the bytes before it. For CFB that follows from the +/// equations -- `Oj` depends only on `C_{j-1}` -- and it is what makes the short final segment +/// well defined: truncating the message truncates the ciphertext, nothing more. +#[test] +fn the_ciphertext_of_a_prefix_is_a_prefix_of_the_ciphertext() { + let iv = pinned_iv(); + let plaintext = message(4 * TOY_LEN + 3); + let full = enc(&mut pinned_encryptor(iv), &plaintext); + + for k in 0..=plaintext.len() { + let ct = enc(&mut pinned_encryptor(iv), &plaintext[..k]); + assert_eq!(&ct[..], &full[..k], "encrypting the first {k} bytes"); + let pt = dec(&mut pinned_decryptor(iv), &full[..k]); + assert_eq!(&pt[..], &plaintext[..k], "decrypting the first {k} bytes"); + } +} + +// ---- the forward-cipher-only rule --------------------------------------------------------- + +/// SP 800-38A Sec 6.3: "The *forward cipher* function is applied to each input block to produce the +/// output blocks" -- in CFB *decryption* as well as encryption. +/// +/// [`ForwardOnlyToy`] panics from `decrypt_block`, `decrypt_2blocks` and `decrypt_4blocks`, so this +/// test fails loudly if either direction of the mode ever reaches the inverse cipher. Every +/// decrypt path is exercised -- the four-block, pair, single-block and byte paths -- and the result +/// is required to agree with the plain [`Toy`], otherwise the test could pass by not really +/// encrypting anything. +#[test] +fn neither_direction_uses_the_inverse_cipher() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = message(11 * TOY_LEN + 5); + + let (mut e, _) = + ForwardOnlyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc(&mut e, &plaintext); + + // One call: two fours, then a pair, then a single, then the short segment. + let mut d = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec(&mut d, &ct), plaintext, "all paths, forward cipher only"); + + // Byte by byte: the byte path only. + let mut d = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_chunked(&mut d, &ct, 1), plaintext, "byte path, forward cipher only"); + + // The forward-only toy must agree with the real one, or the above proves nothing. + assert_eq!( + enc(&mut pinned_encryptor(iv), &plaintext), + ct, + "the two toys must agree going forward" + ); +} + +/// The decryptor must feed the **ciphertext** back, not the plaintext it just recovered. +/// +/// Getting this wrong is invisible in the first block -- `O1 = CIPH_K(IV)` either way -- and wrong +/// from the second onwards. An encryptor run over ciphertext is exactly that mistake: it XORs the +/// right keystream into block 1 and then chains on its own output. So block 1 agreeing while +/// block 2 disagrees is the signature of the bug, and is what this asserts -- once for whole-block +/// calls and once byte by byte, since the two paths feed back separately. +#[test] +fn the_decryptor_chains_on_ciphertext_not_plaintext() { + let iv = pinned_iv(); + let plaintext = message(3 * TOY_LEN); + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + assert_ne!( + &ct[..TOY_LEN], + &plaintext[..TOY_LEN], + "the two feedback choices must actually differ here" + ); + + for chunk in [3 * TOY_LEN, 1] { + let wrong = enc_chunked(&mut pinned_encryptor(iv), &ct, chunk); + assert_eq!( + &wrong[..TOY_LEN], + &plaintext[..TOY_LEN], + "chunk {chunk}: block 1 cannot tell the two apart" + ); + assert_ne!( + &wrong[TOY_LEN..2 * TOY_LEN], + &plaintext[TOY_LEN..2 * TOY_LEN], + "chunk {chunk}: block 2 must, so the feedback source is pinned" + ); + } +} + +// ---- chaining and call sequencing -------------------------------------------------------- + +/// Encrypting a message must not depend on how the calls are chunked, and likewise for decryption, +/// at *byte* granularity. This is the "a sequence of calls is equivalent to one call over the +/// concatenation" contract of the trait, and for CFB it is about the input block surviving across +/// calls and, when a call ends mid-segment, the unused keystream surviving too. +/// +/// Every chunking in [`CHUNKINGS`] is checked against the one-call reference in both directions, +/// and every encrypt chunking against every decrypt chunking. Chunk sizes that are not multiples +/// of the block put every call through the head-blocks-tail split with all three parts non-empty at +/// some point; 1 never reaches the block path at all; 16 and 32 never leave it. +#[test] +fn call_chunking_does_not_change_the_result() { + let iv = pinned_iv(); + let plaintext = message(10 * TOY_LEN + 11); + + let reference = enc(&mut pinned_encryptor(iv), &plaintext); + assert_eq!(dec(&mut pinned_decryptor(iv), &reference), plaintext); + + for &enc_chunk in &CHUNKINGS { + let ct = enc_chunked(&mut pinned_encryptor(iv), &plaintext, enc_chunk); + assert_eq!(ct, reference, "encrypting in {enc_chunk}-byte calls"); + + for &dec_chunk in &CHUNKINGS { + let pt = dec_chunked(&mut pinned_decryptor(iv), &ct, dec_chunk); + assert_eq!( + pt, plaintext, + "encrypted in {enc_chunk}-byte calls, decrypted in {dec_chunk}-byte calls" + ); + } + } + + // Empty calls anywhere are no-ops, including mid-segment. + let mut e = pinned_encryptor(iv); + e.do_encrypt_inplace(&mut []).unwrap(); + let mut ct = plaintext.clone(); + e.do_encrypt_inplace(&mut ct[..5]).unwrap(); + e.do_encrypt_inplace(&mut []).unwrap(); + e.do_encrypt_inplace(&mut ct[5..]).unwrap(); + e.do_encrypt_inplace(&mut []).unwrap(); + assert_eq!(ct, reference, "empty calls must not disturb the state"); +} + +/// The same equivalence, generic over the permutation, at a length that runs the decryptor's +/// four-block batch several times over and ends every chunking on a short final segment. +/// +/// `call_chunking_does_not_change_the_result` proves the property over [`Toy`] at 55 bytes. This +/// repeats it at 171 bytes, which is not a whole number of blocks, and runs it over +/// [`ForwardOnlyToy`] as well, so the chunked decryptions that reach the batch paths are shown to +/// do so without the inverse cipher. The AES coverage (the `aes` crate's `sp800_38a_cfb_tests.rs` +/// and `cfb_bc-test-data.rs`) chunks against *published* ciphertext; this is the direct +/// single-call-versus-chunked comparison, kept free of an AES dependency. +#[test] +fn chunking_matches_a_single_call_over_several_batches() { + fn check(name: &str) + where + P: ElectronicCodeBook, + { + let key_bytes: [u8; KEY_LEN] = + core::array::from_fn(|i| (i as u8).wrapping_mul(31).wrapping_add(7)); + let key = + KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .expect("a valid key"); + let iv: [u8; 16] = core::array::from_fn(|i| 0xC3 ^ (i as u8)); + let plaintext: Vec = (0..171).map(|i| (i * 7 + i / 16) as u8).collect(); + + let encryptor = || { + let (enc, got) = Cfb::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::<16>::new(iv), + ) + .expect("encrypt init"); + assert_eq!(got, iv, "{name}: the pinned RNG should reproduce the IV"); + enc + }; + let decryptor = + || Cfb::::do_decrypt_init(&key, &iv).expect("decrypt init"); + + // The reference: the whole message in one call. + let mut reference = plaintext.clone(); + encryptor().do_encrypt_inplace(&mut reference).expect("one-call encryption"); + assert_ne!(reference, plaintext, "{name}: the data must actually be encrypted"); + + // ...and the round trip of that, also in one call. + let mut back = reference.clone(); + decryptor().do_decrypt_inplace(&mut back).expect("one-call decryption"); + assert_eq!(back, plaintext, "{name}: one-call round trip"); + + for &enc_chunk in &CHUNKINGS { + let mut ct = plaintext.clone(); + let mut e = encryptor(); + for piece in ct.chunks_mut(enc_chunk) { + e.do_encrypt_inplace(piece).expect("chunked encryption"); + } + assert_eq!(ct, reference, "{name}: encrypting in {enc_chunk}-byte calls"); + + for &dec_chunk in &CHUNKINGS { + let mut pt = ct.clone(); + let mut d = decryptor(); + for piece in pt.chunks_mut(dec_chunk) { + d.do_decrypt_inplace(piece).expect("chunked decryption"); + } + assert_eq!( + pt, plaintext, + "{name}: encrypted in {enc_chunk}-byte calls, decrypted in {dec_chunk}-byte calls" + ); + } + } + } + + check::("Toy"); + check::("ForwardOnlyToy"); +} + +/// The pair path in `do_decrypt_inplace` must actually be taken, and only where a pair of whole +/// blocks sits at a segment boundary. +/// +/// [`SwappedPairToy`] returns its two pair results in the wrong order while its single-block methods +/// are correct. CFB decryption pairs through `encrypt_2blocks`, so with this permutation two blocks +/// handed over together come out wrong, while the same bytes handed over one block at a time, or +/// offset by a partial segment so that no two whole blocks line up, come out right. If everything +/// came out right, the pair path would be dead code and every claim about it would be untested. +#[test] +fn the_pair_path_is_really_used() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = message(2 * TOY_LEN); + + // The correct toy round-trips. + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + assert_eq!(dec(&mut pinned_decryptor(iv), &ct), plaintext); + + // The swapped-pair toy encrypts identically -- CFB encryption is serial and never pairs, so its + // `encrypt_2blocks` override is not reached from the encryptor at all. + let (mut e, _) = + SwappedCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(enc(&mut e, &plaintext), ct, "CFB encryption must not use the pair path"); + + // ...but decrypting the pair together must now be wrong, because the pair path is used. + let mut d = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!(dec(&mut d, &ct), plaintext, "decrypting a pair must go through encrypt_2blocks"); + + // Decrypting one block at a time avoids the pair path, so it is correct even for this toy. + let mut d = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_chunked(&mut d, &ct, TOY_LEN), plaintext, "the single-block path must not pair"); + + // So does splitting the pair across a segment boundary: 5 bytes, then 27. The second call has + // an 11-byte head, one whole block and no tail, so there is no pair to form. + let mut d = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); + let mut got = ct.clone(); + d.do_decrypt_inplace(&mut got[..5]).unwrap(); + d.do_decrypt_inplace(&mut got[5..]).unwrap(); + assert_eq!(got, plaintext, "a pair not at a segment boundary is not a pair"); +} + +/// The four-block path in `do_decrypt_inplace` must actually be taken, and only for full fours. +/// +/// [`SwappedFourToy`] returns its four `encrypt_4blocks` results rotated while its pair and +/// single-block methods are correct. CFB decryption batches fours through the *forward* +/// `encrypt_4blocks`, so with this permutation five blocks handed over together decrypt wrongly +/// (four rotated, then one), while the same blocks handed over as two pairs or one at a time +/// decrypt correctly. Encryption is serial and never batches, so it is unaffected. +#[test] +fn the_four_block_path_is_really_used() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = message(5 * TOY_LEN); + + // The correct toy round-trips five blocks. + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + assert_eq!(dec(&mut pinned_decryptor(iv), &ct), plaintext); + + // The rotated-four toy encrypts identically: CFB encryption is serial and never batches. + let (mut e, _) = + SwappedFourCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(enc(&mut e, &plaintext), ct, "CFB encryption must not use the four path"); + + // ...but five blocks together must now be wrong, because the first four go through + // encrypt_4blocks. + let mut d = SwappedFourCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!(dec(&mut d, &ct), plaintext, "five blocks must go through encrypt_4blocks"); + + // Pairs use the pair path only, so they are correct even for this toy... + let mut d = SwappedFourCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!( + dec_chunked(&mut d, &ct, 2 * TOY_LEN), + plaintext, + "pairs must not use the four path" + ); + + // ...and so is one block at a time. + let mut d = SwappedFourCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!( + dec_chunked(&mut d, &ct, TOY_LEN), + plaintext, + "the single-block path must not batch" + ); +} + +/// The one-shots (`encrypt` / `decrypt`, in place) must produce exactly what the streaming API +/// produces, for a message ending in a short segment and one that does not, in both directions. +#[test] +fn one_shots_agree_with_the_streaming_api() { + let key = toy_key(); + let iv = pinned_iv(); + + for len in [3 * TOY_LEN + 7, 4 * TOY_LEN] { + let plaintext = message(len); + let streamed = enc(&mut pinned_encryptor(iv), &plaintext); + + let mut buf = plaintext.clone(); + let (_, iv_b) = + ToyCfb::::encrypt_rng_inplace(&key, &mut pinned_rng(iv), &mut buf).unwrap(); + assert_eq!(iv_b, iv); + assert_eq!(buf, streamed, "len {len}: one-shot must equal streaming"); + ToyCfb::::decrypt_inplace(&key, &iv, &mut buf).unwrap(); + assert_eq!(buf, plaintext); + + // The OS-RNG variant round-trips too. + let mut buf = plaintext.clone(); + let (_, iv_fresh) = ToyCfb::::encrypt_inplace(&key, &mut buf).unwrap(); + assert_ne!(buf, plaintext); + ToyCfb::::decrypt_inplace(&key, &iv_fresh, &mut buf).unwrap(); + assert_eq!(buf, plaintext); + } +} + +// ---- SP 800-38A Appendix D error propagation --------------------------------------------- + +/// The parts of Appendix D that follow from the equations and hold for *any* permutation. +/// +/// Table D.2 for CFB: a bit error in `Cj` gives "SBE in the decryption of `Cj`" -- specific bit +/// errors, i.e. the same bit positions -- because `Pj = Cj XOR Oj` and `Oj = CIPH_K(C_{j-1})` does +/// not depend on `Cj` at all. Earlier blocks are untouched, and with `s = b` the damage reaches +/// exactly one block further (`Cj+1`, since `b/s = 1`). A bit error in the short final segment is +/// the same story with nothing after it: the same bit of the same segment, and nothing else. +#[test] +fn a_ciphertext_bit_error_flips_exactly_that_bit_of_its_own_block() { + let iv = pinned_iv(); + let plaintext = message(4 * TOY_LEN + 5); + let ct = enc(&mut pinned_encryptor(iv), &plaintext); + + // Every bit of C2, so the SBE claim is checked exhaustively rather than at one position. + for byte in TOY_LEN..2 * TOY_LEN { + for bit in 0..8 { + let mut corrupt = ct.clone(); + corrupt[byte] ^= 1 << bit; + let got = dec(&mut pinned_decryptor(iv), &corrupt); + + assert_eq!(&got[..TOY_LEN], &plaintext[..TOY_LEN], "P1 depends only on the IV and C1"); + + let mut expected_p2 = plaintext[TOY_LEN..2 * TOY_LEN].to_vec(); + expected_p2[byte - TOY_LEN] ^= 1 << bit; + assert_eq!( + &got[TOY_LEN..2 * TOY_LEN], + &expected_p2[..], + "C2 byte {byte} bit {bit}: exactly that bit of P2 should change" + ); + + assert_ne!( + &got[2 * TOY_LEN..3 * TOY_LEN], + &plaintext[2 * TOY_LEN..3 * TOY_LEN], + "P3 comes from CIPH_K of the corrupted C2" + ); + assert_eq!( + &got[3 * TOY_LEN..], + &plaintext[3 * TOY_LEN..], + "P4 and the final segment are unaffected: b/s = 1, so damage stops at P3" + ); + } + } + + // Every bit of the short final segment. + for byte in 4 * TOY_LEN..plaintext.len() { + for bit in 0..8 { + let mut corrupt = ct.clone(); + corrupt[byte] ^= 1 << bit; + let got = dec(&mut pinned_decryptor(iv), &corrupt); + let mut expected = plaintext.clone(); + expected[byte] ^= 1 << bit; + assert_eq!( + got, expected, + "final segment byte {byte} bit {bit}: exactly that bit, and nothing else" + ); + } + } +} + +/// Appendix D for a corrupted IV under CFB with `s = b`: the damage is confined to `P1` -- "a bit +/// error in the ith most significant bit position affects the decryptions of the first i/s +/// (rounding up) ciphertext segments", which is one segment for every `i` -- and, unlike CBC, the +/// IV goes through the cipher before it reaches the plaintext, so the error is not flipped in +/// place. Table D.2 calls the result "RBE", random bit errors: that spread is the block cipher's +/// diffusion, not the mode's, and [`Toy`] (which permutes each byte independently) cannot show it +/// and this does not claim it. +/// +/// What the toy makes exact instead: the flipped IV bit comes out of `P1` in the same byte but +/// moved by the toy's `rotate_left(1)`, every other byte of `P1` is untouched, and `P2` onwards is +/// exactly right. Under CBC the same corruption flips *exactly* the corresponding bit of `P1` +/// (Appendix D, and `an_iv_bit_error_flips_exactly_that_bit_of_the_first_block` in +/// `cbc_tests.rs`); confusing the two would be a real bug, and the rotated bit is what catches it. +#[test] +fn an_iv_bit_error_damages_only_the_first_block_through_the_cipher() { + const LEN: usize = TOY_LEN; + let iv = pinned_iv(); + let plaintext = [[0x00u8; LEN], [0x11u8; LEN], [0x22u8; LEN]]; + + let mut ct = plaintext; + pinned_encryptor(iv).do_encrypt_inplace(ct.as_flattened_mut()).unwrap(); + + let mut first_blocks = std::collections::BTreeSet::new(); + + for byte in 0..LEN { + for bit in 0..8 { + let flip = 1u8 << bit; + let mut corrupt_iv = iv; + corrupt_iv[byte] ^= flip; + + let mut got = ct; + pinned_decryptor(corrupt_iv).do_decrypt_inplace(got.as_flattened_mut()).unwrap(); + + // Only P1 is affected: `I2 = C1`, which the corruption did not touch. + assert_eq!(got[1], plaintext[1], "IV byte {byte} bit {bit}: P2 must be unaffected"); + assert_eq!(got[2], plaintext[2], "IV byte {byte} bit {bit}: P3 must be unaffected"); + + // ...and within P1 the error went through the cipher: the toy's per-byte function + // rotates the flipped bit one place, so it lands in the same byte at a different + // position, which is exactly what CBC's in-place flip would not do. + let mut expected = plaintext[0]; + expected[byte] ^= flip.rotate_left(1); + assert_eq!( + got[0], expected, + "IV byte {byte} bit {bit}: P1 should carry the flip through the toy's rotation" + ); + let mut cbc_style = plaintext[0]; + cbc_style[byte] ^= flip; + assert_ne!(got[0], cbc_style, "CFB must not behave like CBC for a corrupted IV"); + + assert!(first_blocks.insert(got[0]), "distinct IVs should give distinct P1"); + } + } + + assert_eq!(first_blocks.len(), LEN * 8, "every corrupted IV should have been tried"); +} + +// ---- IV handling ------------------------------------------------------------------------- + +/// Two encryption flows under the same key must not reuse an IV. The framework checks this too; +/// repeated here because a repeated IV is worse for CFB than for CBC -- it leaks the XOR of the two +/// plaintexts, not merely their equality (see the crate docs, "Key and IV reuse"). +#[test] +fn each_encryption_gets_a_fresh_iv() { + let key = toy_key(); + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..64 { + let (_, iv) = ToyCfb::::do_encrypt_init(&key).unwrap(); + assert!(seen.insert(iv), "IV repeated across encryptions: {iv:02x?}"); + } +} + +/// Identical plaintext under the same key must give different ciphertext, because the IV differs. +#[test] +fn identical_plaintext_gives_different_ciphertext() { + let key = toy_key(); + let plaintext = [0x77u8; 2 * TOY_LEN]; + + let mut first = plaintext; + ToyCfb::::encrypt_inplace(&key, &mut first).unwrap(); + let mut second = plaintext; + ToyCfb::::encrypt_inplace(&key, &mut second).unwrap(); + assert_ne!(first, second); + + // ...and, within one message, two identical plaintext blocks must not give identical ciphertext + // blocks either, because the keystream block differs. + assert_ne!( + first[..TOY_LEN], + first[TOY_LEN..], + "feedback should break the ECB pattern within a message" + ); +} + +// ---- key handling ------------------------------------------------------------------------ + +#[test] +fn a_key_of_the_wrong_type_is_rejected() { + let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); + let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); + assert!(ToyCfb::::do_encrypt_init(&seed).is_err()); + assert!(ToyCfb::::do_decrypt_init(&seed, &[0u8; TOY_LEN]).is_err()); +} + +// ---- every length, no padding ------------------------------------------------------------ + +/// CFB is a stream cipher: every length round-trips, the ciphertext is exactly as long as the +/// plaintext, and no padding layer is involved. Every length from empty to just past three blocks +/// covers the empty message, a lone short segment, exact multiples and every partial final segment. +#[test] +fn every_length_round_trips_without_padding() { + let key = toy_key(); + for len in 0..=(3 * TOY_LEN + 1) { + let plaintext = message(len); + let mut data = plaintext.clone(); + let (n, iv) = ToyCfb::::encrypt_inplace(&key, &mut data).expect("encryption"); + assert_eq!(n, len, "len {len}: encrypt must report the number of bytes written"); + assert_eq!(data.len(), len, "len {len}: the ciphertext is as long as the plaintext"); + // Only meaningful once the message is long enough that agreeing with the keystream by + // chance is negligible: a 1-byte message coincides with its own ciphertext whenever the + // single keystream byte is zero, which a fresh random IV makes happen about once in 256 + // runs. At 8 bytes the odds are 2^-64. (This is why the assertion is guarded rather than + // dropped: it is worth making, just not at every length.) + if len >= 8 { + assert_ne!(data, plaintext, "len {len}: the data must actually be encrypted"); + } + ToyCfb::::decrypt_inplace(&key, &iv, &mut data).expect("decryption"); + assert_eq!(data, plaintext, "len {len}: round trip"); + } +} + +// ---- memory ------------------------------------------------------------------------------ + +/// Pins the "Memory Usage" statement in the module docs -- the state is the permutation, one +/// block and a byte count -- and the claim that CFB costs one `usize` more than CBC: the block that +/// is `Ij`, `Oj` and `I_{j+1}` in turn, plus the count of how much of it has been used. +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::size_of; + + // The general rule the docs state. + assert_eq!(size_of::>(), size_of::() + TOY_LEN + size_of::()); + + // The direction marker is free, and does not change the layout. + assert_eq!(size_of::>(), size_of::>()); + + // The docs say CFB is one `usize` bigger than CBC. + assert_eq!( + size_of::>(), + size_of::>() + size_of::() + ); +} diff --git a/crypto/cipher/tests/modes/common/mod.rs b/crypto/cipher/tests/modes/common/mod.rs new file mode 100644 index 00000000..8c935e5e --- /dev/null +++ b/crypto/cipher/tests/modes/common/mod.rs @@ -0,0 +1,273 @@ +//! Toy [`ElectronicCodeBook`] implementations, for testing the mode independently of any real cipher. +//! +//! These are **not** cryptography. They exist so the structural properties of a mode -- chaining, +//! sequencing, the pair/remainder split, direction typing -- can be tested without an AES +//! dependency and without a real cipher's vectors getting in the way. The real known-answer tests +//! are in the `aes` crate's `tests/sp800_38a_*_tests.rs`. +//! +//! # Why not XOR +//! +//! The obvious toy, `block[i] ^= key[i]`, is its own inverse. That would make `encrypt_block` and +//! `decrypt_block` the same function, which hides exactly the bugs these tests are for: a CBC +//! decryptor that called the forward function, or an encryptor that called the inverse, would still +//! round-trip. [`Toy`] is therefore asymmetric: it rotates before XOR-ing, so the two directions are +//! genuinely different functions. + +// Each test binary that includes this module uses a subset of it -- `cfb_tests.rs` needs +// `ForwardOnlyToy`, `cbc_tests.rs` does not -- and an unused item in an integration test's private +// module is otherwise a dead-code warning. +#![allow(dead_code)] + +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; + +/// Block and key length of the toy ciphers, chosen to match AES so the tests exercise the same +/// shapes the real thing will. +pub const TOY_LEN: usize = 16; + +/// Shared key validation, so the toys reject the same keys a real permutation would and the +/// framework's key-handling checks are meaningful. +fn validate(key: &dyn KeyMaterialTrait) -> Result<(), SymmetricCipherError> { + if key.key_type() != KeyType::SymmetricCipherKey { + return Err( + KeyMaterialError::InvalidKeyType("toy cipher needs a SymmetricCipherKey").into() + ); + } + if key.key_len() != TOY_LEN { + return Err(KeyMaterialError::InvalidLength.into()); + } + if key.security_strength() < SecurityStrength::_128bit { + return Err(KeyMaterialError::SecurityStrength("toy cipher needs a 128-bit key").into()); + } + Ok(()) +} + +/// An asymmetric toy permutation: `encrypt` is `rotate_left(1)` then XOR with the key byte. +/// +/// A true permutation on each byte, so it is a true permutation on the block, and its inverse is +/// distinctly different code (XOR then `rotate_right(1)`). +pub struct Toy { + key: [u8; TOY_LEN], +} + +impl Algorithm for Toy { + const ALG_NAME: &'static str = "Toy"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl ElectronicCodeBook for Toy { + fn new(key: &KeyMaterial) -> Result { + validate(key)?; + let mut bytes = [0u8; TOY_LEN]; + bytes.copy_from_slice(key.ref_to_bytes()); + Ok(Self { key: bytes }) + } + + fn encrypt_block(&self, block: &mut [u8; TOY_LEN]) { + for (b, k) in block.iter_mut().zip(self.key.iter()) { + *b = b.rotate_left(1) ^ *k; + } + } + + fn decrypt_block(&self, block: &mut [u8; TOY_LEN]) { + for (b, k) in block.iter_mut().zip(self.key.iter()) { + *b = (*b ^ *k).rotate_right(1); + } + } + + // The toy has no unit wider than a block, so the batch methods are single-block loops. + fn encrypt_2blocks(&self, blocks: &mut [[u8; TOY_LEN]; 2]) { + for block in blocks.iter_mut() { + self.encrypt_block(block); + } + } + + fn decrypt_2blocks(&self, blocks: &mut [[u8; TOY_LEN]; 2]) { + for block in blocks.iter_mut() { + self.decrypt_block(block); + } + } + + fn encrypt_4blocks(&self, blocks: &mut [[u8; TOY_LEN]; 4]) { + for block in blocks.iter_mut() { + self.encrypt_block(block); + } + } + + fn decrypt_4blocks(&self, blocks: &mut [[u8; TOY_LEN]; 4]) { + for block in blocks.iter_mut() { + self.decrypt_block(block); + } + } +} + +/// A deliberately broken toy whose pair methods **swap** their two results. +/// +/// Used to prove that the mode really does take the pair path: with this permutation, a CBC +/// decryptor that uses `decrypt_2blocks` must produce something other than the correct plaintext. +/// If a test using this still round-trips, the pair path is dead code and the coverage claimed for +/// it is false. +/// +/// Its single-block methods are identical to [`Toy`]'s, so the two agree on odd-length input. +pub struct SwappedPairToy { + inner: Toy, +} + +impl Algorithm for SwappedPairToy { + const ALG_NAME: &'static str = "SwappedPairToy"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl ElectronicCodeBook for SwappedPairToy { + fn new(key: &KeyMaterial) -> Result { + Ok(Self { inner: Toy::new(key)? }) + } + + fn encrypt_block(&self, block: &mut [u8; TOY_LEN]) { + self.inner.encrypt_block(block); + } + + fn decrypt_block(&self, block: &mut [u8; TOY_LEN]) { + self.inner.decrypt_block(block); + } + + fn encrypt_2blocks(&self, blocks: &mut [[u8; TOY_LEN]; 2]) { + self.inner.encrypt_block(&mut blocks[0]); + self.inner.encrypt_block(&mut blocks[1]); + blocks.swap(0, 1); + } + + fn decrypt_2blocks(&self, blocks: &mut [[u8; TOY_LEN]; 2]) { + self.inner.decrypt_block(&mut blocks[0]); + self.inner.decrypt_block(&mut blocks[1]); + blocks.swap(0, 1); + } + + // Two (swapped) pair calls, so four blocks are wrong too: the fault is in the pair path, and + // a mode that batches fours still goes through it. + fn encrypt_4blocks(&self, blocks: &mut [[u8; TOY_LEN]; 4]) { + let (pairs, _) = blocks.as_mut_slice().as_chunks_mut::<2>(); + for pair in pairs { + self.encrypt_2blocks(pair); + } + } + + fn decrypt_4blocks(&self, blocks: &mut [[u8; TOY_LEN]; 4]) { + let (pairs, _) = blocks.as_mut_slice().as_chunks_mut::<2>(); + for pair in pairs { + self.decrypt_2blocks(pair); + } + } +} + +/// A toy whose **inverse cipher function panics**. +/// +/// SP 800-38A Sec 6.3 applies the forward cipher function in both directions of CFB, so a correct +/// `Cfb` never touches `decrypt_block`, `decrypt_2blocks` or `decrypt_4blocks`. Running a full CFB round trip over this +/// permutation turns that claim into a test: if either decryption entry point is ever reached, the +/// test panics with the message below rather than quietly producing a right answer for the wrong +/// reason. +/// +/// This is deliberately not a valid [`ElectronicCodeBook`] -- it cannot pass +/// `TestFrameworkElectronicCodeBook`, which exercises both directions -- so it is only ever used with +/// `Cfb`. Its forward methods delegate to [`Toy`], including the pair and four-block methods, so a CFB round trip +/// over it must agree with one over `Toy`. +pub struct ForwardOnlyToy { + inner: Toy, +} + +impl Algorithm for ForwardOnlyToy { + const ALG_NAME: &'static str = "ForwardOnlyToy"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl ElectronicCodeBook for ForwardOnlyToy { + fn new(key: &KeyMaterial) -> Result { + Ok(Self { inner: Toy::new(key)? }) + } + + fn encrypt_block(&self, block: &mut [u8; TOY_LEN]) { + self.inner.encrypt_block(block); + } + + fn decrypt_block(&self, _block: &mut [u8; TOY_LEN]) { + panic!("CFB must never call the inverse cipher function (SP 800-38A Sec 6.3)"); + } + + fn encrypt_2blocks(&self, blocks: &mut [[u8; TOY_LEN]; 2]) { + self.inner.encrypt_2blocks(blocks); + } + + fn decrypt_2blocks(&self, _blocks: &mut [[u8; TOY_LEN]; 2]) { + panic!("CFB must never call the inverse cipher pair function (SP 800-38A Sec 6.3)"); + } + + fn encrypt_4blocks(&self, blocks: &mut [[u8; TOY_LEN]; 4]) { + self.inner.encrypt_4blocks(blocks); + } + + fn decrypt_4blocks(&self, _blocks: &mut [[u8; TOY_LEN]; 4]) { + panic!("CFB must never call the inverse cipher four-block function (SP 800-38A Sec 6.3)"); + } +} + +/// A [`Toy`] whose `encrypt_4blocks` / `decrypt_4blocks` return their four results rotated by one +/// slot, while every other method -- single block and pair -- is correct. +/// +/// The four-block analogue of [`SwappedPairToy`]: a CBC decryptor that uses `decrypt_4blocks` +/// must produce something other than the correct plaintext for four or more blocks, while fewer +/// than four, which go through the pair and single paths, still round-trip. +pub struct SwappedFourToy { + inner: Toy, +} + +impl Algorithm for SwappedFourToy { + const ALG_NAME: &'static str = "SwappedFourToy"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl ElectronicCodeBook for SwappedFourToy { + fn new(key: &KeyMaterial) -> Result { + Ok(Self { inner: Toy::new(key)? }) + } + + fn encrypt_block(&self, block: &mut [u8; TOY_LEN]) { + self.inner.encrypt_block(block); + } + + fn decrypt_block(&self, block: &mut [u8; TOY_LEN]) { + self.inner.decrypt_block(block); + } + + fn encrypt_2blocks(&self, blocks: &mut [[u8; TOY_LEN]; 2]) { + self.inner.encrypt_2blocks(blocks); + } + + fn decrypt_2blocks(&self, blocks: &mut [[u8; TOY_LEN]; 2]) { + self.inner.decrypt_2blocks(blocks); + } + + fn encrypt_4blocks(&self, blocks: &mut [[u8; TOY_LEN]; 4]) { + for block in blocks.iter_mut() { + self.inner.encrypt_block(block); + } + blocks.rotate_left(1); + } + + fn decrypt_4blocks(&self, blocks: &mut [[u8; TOY_LEN]; 4]) { + for block in blocks.iter_mut() { + self.inner.decrypt_block(block); + } + blocks.rotate_left(1); + } +} + +/// Builds a `KeyMaterial` for the toys from a fixed non-zero pattern. +pub fn toy_key() -> KeyMaterial { + let bytes: [u8; TOY_LEN] = 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 toy key") +} diff --git a/crypto/cipher/tests/modes/ctr_tests.rs b/crypto/cipher/tests/modes/ctr_tests.rs new file mode 100644 index 00000000..84748c8c --- /dev/null +++ b/crypto/cipher/tests/modes/ctr_tests.rs @@ -0,0 +1,771 @@ +//! Structural tests for CTR, driven by a toy permutation. +//! +//! These check the properties of the *mode* -- the counter block construction, the standard +//! incrementing function, the counter limit and its error, call sequencing at arbitrary byte +//! boundaries, the batch paths in both directions, direction typing, and the "forward cipher +//! function only" rule -- independently of any real cipher. The known-answer tests against the NIST +//! ACVP `ACVP-AES-CTR` set are in the `aes` crate, `crypto/aes/tests/ctr_bc-test-data.rs`. +//! +//! The toy's own conformance to [`ElectronicCodeBook`] is pinned once, by +//! `the_toy_permutation_conforms_to_the_trait` in `cbc_tests.rs`; it is the same `Toy` here, so it +//! is not re-run. +//! +//! # Why there is no SP 800-38A Appendix F.5 suite +//! +//! F.5 gives each vector a full 16-byte "Init. Counter" -- `f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff` -- +//! whose counter part starts at `0xfcfdfeff`, not at zero. [`Ctr`] takes a *nonce* as its init data +//! and always starts the counter at zero, so those vectors cannot be expressed through its API. +//! What the F.5 counter blocks do confirm is the shape of the split this type uses: across the four +//! blocks they increment only within the last four bytes (`fcfdfeff`, `fcfdff00`, `fcfdff01`, +//! `fcfdff02`), leaving the leading twelve fixed, which is exactly a 12-byte nonce and a 4-byte +//! counter. `the_f5_counter_blocks_have_the_shape_this_type_assumes` pins that reading, and the +//! ACVP suite supplies the actual known-answer coverage. + +mod common; + +use bouncycastle_cipher::modes::Ctr; +use bouncycastle_cipher::modes::hazmat::CtrKeyStream; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::key_stream::TestFrameworkKeyStream; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkStreamCipher; +use common::{ForwardOnlyToy, SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; + +/// The default shape under test: a 12-byte nonce, so a 4-byte counter. +const NONCE_LEN: usize = 12; +type ToyCtr = Ctr; +type SwappedCtr = Ctr; +type ForwardOnlyCtr = Ctr; +type SwappedFourCtr = Ctr; + +/// A 15-byte nonce leaves a **1-byte** counter, so the whole counter space is 256 blocks -- 4 KiB +/// of keystream. That makes the exhaustion behaviour reachable in a test. +const SHORT_CTR_NONCE_LEN: usize = 15; +type TinyCtr = Ctr; +/// Capacity of a 1-byte counter, in bytes. +const TINY_CAPACITY: usize = 256 * TOY_LEN; + +fn enc(e: &mut impl StreamCipherEncryptor, plaintext: &[u8]) -> Vec { + let mut data = plaintext.to_vec(); + e.do_encrypt_inplace(&mut data).unwrap(); + data +} + +fn dec(d: &mut impl StreamCipherDecryptor, ciphertext: &[u8]) -> Vec { + let mut data = ciphertext.to_vec(); + d.do_decrypt_inplace(&mut data).unwrap(); + data +} + +fn dec_chunked( + d: &mut impl StreamCipherDecryptor, + ciphertext: &[u8], + chunk: usize, +) -> Vec { + let mut data = ciphertext.to_vec(); + for piece in data.chunks_mut(chunk) { + d.do_decrypt_inplace(piece).unwrap(); + } + data +} + +fn pinned_nonce() -> [u8; NONCE_LEN] { + core::array::from_fn(|i| 0xA0 ^ (i as u8)) +} + +fn pinned_rng(nonce: [u8; NONCE_LEN]) -> FixedSeedRNG { + FixedSeedRNG::::new(nonce) +} + +fn pinned_encryptor(nonce: [u8; NONCE_LEN]) -> ToyCtr { + let (e, got) = + ToyCtr::::do_encrypt_init_rng(&toy_key(), &mut pinned_rng(nonce)).unwrap(); + assert_eq!(got, nonce, "the pinned RNG should reproduce the nonce"); + e +} + +fn pinned_decryptor(nonce: [u8; NONCE_LEN]) -> ToyCtr { + ToyCtr::::do_decrypt_init(&toy_key(), &nonce).unwrap() +} + +fn message(len: usize) -> Vec { + (0..len).map(|i| (i * 7 + (i / TOY_LEN) * 31 + 1) as u8).collect() +} + +const CHUNKINGS: [usize; 12] = [1, 3, 5, 7, 15, 16, 17, 31, 32, 33, 64, 100]; + +// ---- the mode against the shared framework ------------------------------------------------ + +#[test] +fn ctr_conforms_to_the_stream_cipher_framework() { + TestFrameworkStreamCipher::new() + .test::, ToyCtr>(); +} + +/// The keystream under the mode, on its own: over the toy at the default nonce length and at the +/// one-byte-counter length, and over [`ForwardOnlyToy`] so that whatever the framework drives +/// through the batch paths is shown to get there without the inverse cipher. +#[test] +fn ctr_keystream_conforms_to_the_key_stream_framework() { + let framework = TestFrameworkKeyStream::new(); + framework.test::>(); + framework.test::< + TOY_LEN, + SHORT_CTR_NONCE_LEN, + TOY_LEN, + CtrKeyStream, + >(); + framework.test::< + TOY_LEN, + NONCE_LEN, + TOY_LEN, + CtrKeyStream, + >(); +} + +// ---- the spec equations ------------------------------------------------------------------- + +/// CTR from SP 800-38A Sec 6.5, written out longhand against the raw permutation: +/// +/// ```text +/// Tj = N | [j - 1]m; Oj = CIPH_K(Tj); Cj = Pj XOR Oj; C*_n = P*_n XOR MSB_u(On) +/// ``` +/// +/// The counter block is built here from scratch on every block, from the nonce and the index, so it +/// is an independent statement of the construction rather than a second call to the same +/// incrementing code the implementation uses. +fn reference_ctr(perm: &Toy, nonce: [u8; NONCE_LEN], input: &[u8]) -> Vec { + let mut out = Vec::with_capacity(input.len()); + for (j, chunk) in input.chunks(TOY_LEN).enumerate() { + let mut t = [0u8; TOY_LEN]; + t[..NONCE_LEN].copy_from_slice(&nonce); + t[NONCE_LEN..].copy_from_slice(&(j as u32).to_be_bytes()); + let mut o = t; + perm.encrypt_block(&mut o); // Oj = CIPH_K(Tj) + // Cj = Pj XOR Oj, and for a short final block only its leading bytes: MSB_u(On). + out.extend(chunk.iter().zip(o.iter()).map(|(d, o)| d ^ o)); + } + out +} + +/// The mode must reproduce the Sec 6.5 equations exactly, for whole blocks and for a message ending +/// in a partial block. +/// +/// A reference implementation is a weak test on its own, so this also pins the anchors that follow +/// directly from the equations: the first counter block is the nonce with a zero counter, and +/// encrypting zeros reveals the keystream itself. +#[test] +fn the_mode_matches_the_spec_equations() { + let key = toy_key(); + let nonce = pinned_nonce(); + let perm = >::new(&key).unwrap(); + + for len in [1, TOY_LEN - 1, TOY_LEN, TOY_LEN + 1, 5 * TOY_LEN, 5 * TOY_LEN + 9] { + let plaintext = message(len); + let ct = enc(&mut pinned_encryptor(nonce), &plaintext); + assert_eq!( + ct, + reference_ctr(&perm, nonce, &plaintext), + "len {len}: encryption must match the Sec 6.5 equations" + ); + assert_eq!(dec(&mut pinned_decryptor(nonce), &ct), plaintext, "len {len}: round trip"); + } + + // Anchor 1: `T1 = N | 0`, so `O1 = CIPH_K(N | 0)` and encrypting a zero block yields it. + let mut t1 = [0u8; TOY_LEN]; + t1[..NONCE_LEN].copy_from_slice(&nonce); + let mut o1 = t1; + perm.encrypt_block(&mut o1); + assert_eq!( + enc(&mut pinned_encryptor(nonce), &[0u8; TOY_LEN]), + o1.to_vec(), + "encrypting a zero block yields O1 = CIPH_K(N | 0)" + ); + + // Anchor 2: the cipher never touches the data. The keystream depends only on the key and the + // counter blocks, so two messages encrypted under the same nonce satisfy + // `C XOR C' == P XOR P'` -- the defining property of a keystream mode, and the reason a nonce + // must never repeat. A mode that put the plaintext through the cipher could not satisfy it. + let p1 = message(3 * TOY_LEN + 4); + let p2: Vec = p1.iter().map(|b| b ^ 0x5A).collect(); + let c1 = enc(&mut pinned_encryptor(nonce), &p1); + let c2 = enc(&mut pinned_encryptor(nonce), &p2); + let ct_xor: Vec = c1.iter().zip(c2.iter()).map(|(a, b)| a ^ b).collect(); + let pt_xor: Vec = p1.iter().zip(p2.iter()).map(|(a, b)| a ^ b).collect(); + assert_eq!(ct_xor, pt_xor, "C XOR C' must equal P XOR P' under a repeated nonce"); +} + +/// **Encryption and decryption are the same operation** (Sec 6.5): both compute `CIPH_K(Tj)` and +/// XOR it in. Running the encryptor over ciphertext must therefore recover the plaintext, which is +/// the sharpest statement of that property and would fail for every other mode in this crate. +#[test] +fn encryption_and_decryption_are_the_same_operation() { + let nonce = pinned_nonce(); + let plaintext = message(3 * TOY_LEN + 5); + + let ct = enc(&mut pinned_encryptor(nonce), &plaintext); + assert_eq!(enc(&mut pinned_encryptor(nonce), &ct), plaintext, "the encryptor decrypts too"); + assert_eq!(dec(&mut pinned_decryptor(nonce), &ct), plaintext, "and so does the decryptor"); +} + +// ---- the counter --------------------------------------------------------------------------- + +/// The counter blocks are the nonce followed by a big-endian counter from zero, incremented by +/// Appendix B.1's standard incrementing function -- **at every permitted counter width**. +/// +/// Read out of the keystream rather than out of the mode's private state: encrypting zeros gives +/// `Oj`, and `Oj` must equal `CIPH_K(N | [j]m)` computed independently here from the nonce and the +/// index. +/// +/// Running this at all four widths matters more than it looks. The counter occupies the trailing +/// `CTR_LEN` bytes, so writing it involves a width-dependent slice, and getting that wrong is a bug +/// that **round-trip tests cannot see**: encryption and decryption would build the same wrong +/// counter block and still recover the plaintext, while producing ciphertext no other +/// implementation agrees with. Only checking the keystream against an independently built counter +/// block catches it. +/// +/// Where the counter is wide enough, the run crosses the 255 -> 256 boundary, which is the carry +/// between counter bytes that a per-byte increment could get wrong. +fn check_counter_blocks(blocks: usize) { + const fn ctr_len() -> usize { + TOY_LEN - N + } + + let key = toy_key(); + let perm = >::new(&key).unwrap(); + let nonce: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(13).wrapping_add(5)); + + let (mut e, got) = Ctr::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(nonce), + ) + .unwrap(); + assert_eq!(got, nonce); + let mut keystream = vec![0u8; blocks * TOY_LEN]; + e.do_encrypt_inplace(&mut keystream).expect("the run must fit in the counter space"); + + for j in 0..blocks { + let mut expected = [0u8; TOY_LEN]; + expected[..N].copy_from_slice(&nonce); + // The counter, big-endian, in the trailing CTR_LEN bytes: the low CTR_LEN bytes of the + // index written big-endian. + let be = (j as u64).to_be_bytes(); + expected[N..].copy_from_slice(&be[be.len() - ctr_len::()..]); + perm.encrypt_block(&mut expected); + assert_eq!( + &keystream[j * TOY_LEN..(j + 1) * TOY_LEN], + &expected[..], + "counter width {}: block {j} must be CIPH_K(nonce | {j} big-endian)", + ctr_len::() + ); + } +} + +#[test] +fn counter_blocks_are_the_nonce_then_a_big_endian_counter_from_zero() { + // A 1-byte counter has exactly 256 blocks, so that is the whole space and there is no internal + // carry to cross. The wider ones run past 256 so that the 255 -> 256 carry is exercised. + check_counter_blocks::<15>(256); // 1-byte counter, its entire space + check_counter_blocks::<14>(258); // 2-byte counter, across the carry + check_counter_blocks::<13>(258); // 3-byte counter, across the carry + check_counter_blocks::<12>(258); // 4-byte counter, across the carry +} + +/// SP 800-38A Appendix F.5's counter blocks increment only within their last four bytes +/// (`fcfdfeff`, `fcfdff00`, `fcfdff01`, `fcfdff02`), leaving the leading twelve fixed. +/// +/// That is the nonce-and-counter split this type is built on, so the spec's own example vectors +/// corroborate the shape even though their non-zero starting counter puts them out of reach of this +/// API. See the module docs. +#[test] +fn the_f5_counter_blocks_have_the_shape_this_type_assumes() { + /// F.5.1 CTR-AES128.Encrypt, the four tabulated "Input Block" values. + const F5_COUNTER_BLOCKS: [[u8; 16]; 4] = [ + [ + 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, + 0xfe, 0xff, + ], + [ + 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, + 0xff, 0x00, + ], + [ + 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, + 0xff, 0x01, + ], + [ + 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, + 0xff, 0x02, + ], + ]; + + // The leading 12 bytes are identical in all four: that is the nonce. + for (i, block) in F5_COUNTER_BLOCKS.iter().enumerate() { + assert_eq!( + &block[..12], + &F5_COUNTER_BLOCKS[0][..12], + "F.5 block {i}: the leading 12 bytes must be fixed, i.e. a nonce" + ); + } + + // ...and the trailing 4 are a big-endian counter incremented by one each time, carrying. + for (i, block) in F5_COUNTER_BLOCKS.iter().enumerate() { + let counter = u32::from_be_bytes(block[12..].try_into().unwrap()); + let first = u32::from_be_bytes(F5_COUNTER_BLOCKS[0][12..].try_into().unwrap()); + assert_eq!( + counter, + first.wrapping_add(i as u32), + "F.5 block {i}: the trailing 4 bytes must be the counter, incremented by one" + ); + } +} + +/// The mode must **error** rather than let the counter repeat, and it must do so without consuming +/// anything. +/// +/// Appendix B.1: counter blocks satisfy the uniqueness requirement "provided that `n <= 2^m`". With +/// a 1-byte counter that is 256 blocks, so exactly 4 KiB of keystream is available; the byte after +/// that would reuse `T1` and hence `O1`, which is keystream reuse within one message. +/// +/// `the_counter_limit_is_enforced_at_two_bytes_too` repeats the boundary one width up, where the +/// limit is 65536 blocks rather than 256, so the check is not tied to the one width whose counter +/// happens to be a single byte. +#[test] +fn the_counter_limit_is_enforced() { + let key = toy_key(); + let nonce: [u8; SHORT_CTR_NONCE_LEN] = core::array::from_fn(|i| 0x5A ^ (i as u8)); + + let encryptor = || { + let (e, got) = TinyCtr::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(nonce), + ) + .unwrap(); + assert_eq!(got, nonce); + e + }; + + // Exactly the capacity is allowed, in one call. + let mut data = vec![0u8; TINY_CAPACITY]; + encryptor().do_encrypt_inplace(&mut data).expect("the full counter space must be usable"); + + // One byte more is refused. + let mut data = vec![0u8; TINY_CAPACITY + 1]; + match encryptor().do_encrypt_inplace(&mut data) { + Err(SymmetricCipherError::DataLimitExceeded) => {} + other => panic!("expected DataLimitExceeded past the counter limit, got {other:?}"), + } + assert_eq!(data, vec![0u8; TINY_CAPACITY + 1], "a refused call must not touch the data"); + + // The same limit reached across many calls, not just one. + let mut e = encryptor(); + let mut sixteenth = vec![0u8; TINY_CAPACITY / 16]; + for i in 0..16 { + e.do_encrypt_inplace(&mut sixteenth) + .unwrap_or_else(|err| panic!("call {i} should fit: {err:?}")); + } + let mut one = [0u8; 1]; + assert!(e.do_encrypt_inplace(&mut one).is_err(), "the next byte must be refused"); + assert_eq!(one, [0u8; 1], "a refused call must not touch the data"); + + // ...and a refused call must not disturb the state either: the mode is exhausted, so it stays + // exhausted, and a smaller call is refused too rather than silently wrapping. + let mut one = [0u8; 1]; + assert!(e.do_encrypt_inplace(&mut one).is_err(), "still exhausted on a second attempt"); + + // A call refused part-way through the counter space leaves the state untouched, so the bytes + // that *do* fit are unchanged by the attempt. + let mut e = encryptor(); + let mut half = vec![0u8; TINY_CAPACITY / 2]; + e.do_encrypt_inplace(&mut half).unwrap(); + let mut too_big = vec![0u8; TINY_CAPACITY]; // more than the half that is left + assert!(e.do_encrypt_inplace(&mut too_big).is_err(), "must refuse what does not fit"); + assert_eq!(too_big, vec![0u8; TINY_CAPACITY], "refused call must not touch the data"); + // The remaining half still encrypts, and to exactly what an uninterrupted run would give. + let mut rest = vec![0u8; TINY_CAPACITY / 2]; + e.do_encrypt_inplace(&mut rest).expect("the untouched remainder must still be usable"); + let mut whole = vec![0u8; TINY_CAPACITY]; + encryptor().do_encrypt_inplace(&mut whole).unwrap(); + assert_eq!( + &rest[..], + &whole[TINY_CAPACITY / 2..], + "the refused call must not have advanced the counter" + ); +} + +/// The same boundary with a **2-byte** counter: 65536 blocks, so 1 MiB exactly. +/// +/// Cheap enough to run, and it shows the limit tracks the counter width rather than being a +/// property of the one-byte case. Three and four byte counters put the boundary at 256 MiB and +/// 64 GiB, which is why they are not tested here; the width-generic capacity arithmetic is shared, +/// and `check_counter_blocks` pins the counter construction at all four widths. +#[test] +fn the_counter_limit_is_enforced_at_two_bytes_too() { + const NONCE: usize = 14; + const CAPACITY: usize = 65536 * TOY_LEN; + let key = toy_key(); + let nonce: [u8; NONCE] = core::array::from_fn(|i| 0x3C ^ (i as u8)); + + let encryptor = || { + let (e, got) = Ctr::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(nonce), + ) + .unwrap(); + assert_eq!(got, nonce); + e + }; + + let mut data = vec![0u8; CAPACITY]; + encryptor() + .do_encrypt_inplace(&mut data) + .expect("the full 2-byte counter space must be usable"); + + let mut data = vec![0u8; CAPACITY + 1]; + assert!( + encryptor().do_encrypt_inplace(&mut data).is_err(), + "one byte past the limit must be refused" + ); + assert_eq!(data, vec![0u8; CAPACITY + 1], "a refused call must not touch the data"); +} + +/// The decryptor enforces the same limit: a ciphertext longer than the counter can cover is refused +/// rather than decrypted with repeated keystream. +#[test] +fn the_counter_limit_is_enforced_when_decrypting_too() { + let key = toy_key(); + let nonce: [u8; SHORT_CTR_NONCE_LEN] = core::array::from_fn(|i| 0x5A ^ (i as u8)); + let mut d = TinyCtr::::do_decrypt_init(&key, &nonce).unwrap(); + let mut data = vec![0u8; TINY_CAPACITY + 1]; + assert!( + d.do_decrypt_inplace(&mut data).is_err(), + "decryption must refuse past the counter limit" + ); + assert_eq!(data, vec![0u8; TINY_CAPACITY + 1], "a refused call must not touch the data"); +} + +// ---- the forward-cipher-only rule --------------------------------------------------------- + +/// CTR applies `CIPH_K` to counter blocks in both directions and never inverts anything, so neither +/// direction may reach the inverse cipher. [`ForwardOnlyToy`] panics from every inverse entry point. +#[test] +fn neither_direction_uses_the_inverse_cipher() { + let key = toy_key(); + let nonce = pinned_nonce(); + let plaintext = message(11 * TOY_LEN + 5); + + let (mut e, _) = ForwardOnlyCtr::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(nonce), + ) + .unwrap(); + let mut ct = plaintext.clone(); + e.do_encrypt_inplace(&mut ct).unwrap(); + + let mut d = ForwardOnlyCtr::::do_decrypt_init(&key, &nonce).unwrap(); + let mut back = ct.clone(); + d.do_decrypt_inplace(&mut back).unwrap(); + assert_eq!(back, plaintext, "all paths, forward cipher only"); + + // Byte by byte, so the single-block path runs too. + let mut d = ForwardOnlyCtr::::do_decrypt_init(&key, &nonce).unwrap(); + let mut back = ct.clone(); + for piece in back.chunks_mut(1) { + d.do_decrypt_inplace(piece).unwrap(); + } + assert_eq!(back, plaintext, "byte path, forward cipher only"); + + // The forward-only toy must agree with the real one, or the above proves nothing. + assert_eq!(enc(&mut pinned_encryptor(nonce), &plaintext), ct, "the two toys must agree"); +} + +// ---- call sequencing ----------------------------------------------------------------------- + +/// Chunking must not change the result, in either direction, at byte granularity -- and every +/// encrypt chunking must decrypt under every decrypt chunking. +#[test] +fn call_chunking_does_not_change_the_result() { + let nonce = pinned_nonce(); + let plaintext = message(10 * TOY_LEN + 11); + + let reference = enc(&mut pinned_encryptor(nonce), &plaintext); + assert_eq!(dec(&mut pinned_decryptor(nonce), &reference), plaintext); + + for &enc_chunk in &CHUNKINGS { + let mut ct = plaintext.clone(); + let mut e = pinned_encryptor(nonce); + for piece in ct.chunks_mut(enc_chunk) { + e.do_encrypt_inplace(piece).unwrap(); + } + assert_eq!(ct, reference, "encrypting in {enc_chunk}-byte calls"); + + for &dec_chunk in &CHUNKINGS { + assert_eq!( + dec_chunked(&mut pinned_decryptor(nonce), &ct, dec_chunk), + plaintext, + "encrypted in {enc_chunk}-byte calls, decrypted in {dec_chunk}-byte calls" + ); + } + } + + // Empty calls anywhere are no-ops, including mid-block. + let mut e = pinned_encryptor(nonce); + e.do_encrypt_inplace(&mut []).unwrap(); + let mut ct = plaintext.clone(); + e.do_encrypt_inplace(&mut ct[..5]).unwrap(); + e.do_encrypt_inplace(&mut []).unwrap(); + e.do_encrypt_inplace(&mut ct[5..]).unwrap(); + assert_eq!(ct, reference, "empty calls must not disturb the state"); +} + +/// The same equivalence, generic over the permutation, at 171 bytes -- several four-block batches +/// and a short tail -- and over [`ForwardOnlyToy`] as well as [`Toy`], so the chunked calls that +/// reach the batch paths are shown to do so without the inverse cipher; as for the other stream +/// modes, kept free of an AES dependency. +#[test] +fn chunking_matches_a_single_call_over_several_batches() { + fn check(name: &str) + where + P: ElectronicCodeBook, + { + let key_bytes: [u8; KEY_LEN] = + core::array::from_fn(|i| (i as u8).wrapping_mul(31).wrapping_add(7)); + let key = + KeyMaterial::::from_bytes_as_type(&key_bytes, KeyType::SymmetricCipherKey) + .expect("a valid key"); + let nonce: [u8; 12] = core::array::from_fn(|i| 0xC3 ^ (i as u8)); + let plaintext: Vec = (0..171).map(|i| (i * 7 + i / 16) as u8).collect(); + + let encryptor = || { + let (e, got) = Ctr::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::<12>::new(nonce), + ) + .expect("encrypt init"); + assert_eq!(got, nonce, "{name}: the pinned RNG should reproduce the nonce"); + e + }; + let decryptor = || { + Ctr::::do_decrypt_init(&key, &nonce) + .expect("decrypt init") + }; + + let mut reference = plaintext.clone(); + encryptor().do_encrypt_inplace(&mut reference).expect("one-call encryption"); + assert_ne!(reference, plaintext, "{name}: the data must actually be encrypted"); + + let mut back = reference.clone(); + decryptor().do_decrypt_inplace(&mut back).expect("one-call decryption"); + assert_eq!(back, plaintext, "{name}: one-call round trip"); + + for &enc_chunk in &CHUNKINGS { + let mut ct = plaintext.clone(); + let mut e = encryptor(); + for piece in ct.chunks_mut(enc_chunk) { + e.do_encrypt_inplace(piece).expect("chunked encryption"); + } + assert_eq!(ct, reference, "{name}: encrypting in {enc_chunk}-byte calls"); + + for &dec_chunk in &CHUNKINGS { + let mut pt = ct.clone(); + let mut d = decryptor(); + for piece in pt.chunks_mut(dec_chunk) { + d.do_decrypt_inplace(piece).expect("chunked decryption"); + } + assert_eq!( + pt, plaintext, + "{name}: encrypted in {enc_chunk}-byte, decrypted in {dec_chunk}-byte calls" + ); + } + } + } + + check::("Toy"); + check::("ForwardOnlyToy"); +} + +/// The pair path must be taken, **in both directions** -- unlike CBC and CFB, CTR encryption +/// batches too, because counter blocks do not depend on cipher output (Sec 6.5). +#[test] +fn the_pair_path_is_really_used_in_both_directions() { + let key = toy_key(); + let nonce = pinned_nonce(); + let plaintext = message(2 * TOY_LEN); + + let ct = enc(&mut pinned_encryptor(nonce), &plaintext); + assert_eq!(dec(&mut pinned_decryptor(nonce), &ct), plaintext); + + // Encryption: two blocks together must go through encrypt_2blocks, so the swapped toy differs. + let (mut e, _) = + SwappedCtr::::do_encrypt_init_rng(&key, &mut pinned_rng(nonce)).unwrap(); + let mut swapped = plaintext.clone(); + e.do_encrypt_inplace(&mut swapped).unwrap(); + assert_ne!(swapped, ct, "CTR encryption must use the pair path"); + + // ...but one block at a time avoids it, and then it agrees with the correct toy. + let (mut e, _) = + SwappedCtr::::do_encrypt_init_rng(&key, &mut pinned_rng(nonce)).unwrap(); + let mut single = plaintext.clone(); + for piece in single.chunks_mut(TOY_LEN) { + e.do_encrypt_inplace(piece).unwrap(); + } + assert_eq!(single, ct, "the single-block path must not pair"); + + // Decryption: the same, on the correct ciphertext. + let mut d = SwappedCtr::::do_decrypt_init(&key, &nonce).unwrap(); + let mut back = ct.clone(); + d.do_decrypt_inplace(&mut back).unwrap(); + assert_ne!(back, plaintext, "CTR decryption must use the pair path"); +} + +/// The four-block path must be taken, in both directions, and only for full fours. +#[test] +fn the_four_block_path_is_really_used_in_both_directions() { + let key = toy_key(); + let nonce = pinned_nonce(); + let plaintext = message(5 * TOY_LEN); + + let ct = enc(&mut pinned_encryptor(nonce), &plaintext); + + let (mut e, _) = + SwappedFourCtr::::do_encrypt_init_rng(&key, &mut pinned_rng(nonce)).unwrap(); + let mut swapped = plaintext.clone(); + e.do_encrypt_inplace(&mut swapped).unwrap(); + assert_ne!(swapped, ct, "five blocks must go through encrypt_4blocks"); + + // Two blocks at a time uses pairs only, so the rotated-four toy is correct there. + let (mut e, _) = + SwappedFourCtr::::do_encrypt_init_rng(&key, &mut pinned_rng(nonce)).unwrap(); + let mut pairs = plaintext.clone(); + for piece in pairs.chunks_mut(2 * TOY_LEN) { + e.do_encrypt_inplace(piece).unwrap(); + } + assert_eq!(pairs, ct, "pairs must not use the four path"); + + let mut d = SwappedFourCtr::::do_decrypt_init(&key, &nonce).unwrap(); + let mut back = ct.clone(); + d.do_decrypt_inplace(&mut back).unwrap(); + assert_ne!(back, plaintext, "decryption must batch fours too"); +} + +// ---- nonce handling ------------------------------------------------------------------------ + +/// Two encryption flows under the same key must not reuse a nonce. For CTR this is the whole +/// security argument: a repeated nonce repeats the counter blocks and so the keystream. +#[test] +fn each_encryption_gets_a_fresh_nonce() { + let key = toy_key(); + let mut seen = std::collections::BTreeSet::new(); + for _ in 0..64 { + let (_, nonce) = ToyCtr::::do_encrypt_init(&key).unwrap(); + assert!(seen.insert(nonce), "nonce repeated across encryptions: {nonce:02x?}"); + } +} + +#[test] +fn identical_plaintext_gives_different_ciphertext() { + let key = toy_key(); + let plaintext = [0x77u8; 2 * TOY_LEN]; + + let mut first = plaintext; + ToyCtr::::encrypt_inplace(&key, &mut first).unwrap(); + let mut second = plaintext; + ToyCtr::::encrypt_inplace(&key, &mut second).unwrap(); + assert_ne!(first, second); + + // ...and two identical plaintext blocks within one message differ, because the counter moves. + assert_ne!(first[..TOY_LEN], first[TOY_LEN..], "the counter should change the keystream"); +} + +// ---- key handling -------------------------------------------------------------------------- + +#[test] +fn a_key_of_the_wrong_type_is_rejected() { + let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); + let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); + assert!(ToyCtr::::do_encrypt_init(&seed).is_err()); + assert!(ToyCtr::::do_decrypt_init(&seed, &[0u8; NONCE_LEN]).is_err()); +} + +// ---- every length -------------------------------------------------------------------------- + +/// CTR is a stream cipher: every length round-trips and the ciphertext is exactly as long as the +/// plaintext. +#[test] +fn every_length_round_trips_without_padding() { + let key = toy_key(); + for len in 0..=(3 * TOY_LEN + 1) { + let plaintext = message(len); + let mut data = plaintext.clone(); + let (n, nonce) = + ToyCtr::::encrypt_inplace(&key, &mut data).expect("encryption"); + assert_eq!(n, len, "len {len}: encrypt must report the number of bytes written"); + assert_eq!(data.len(), len, "len {len}: the ciphertext is as long as the plaintext"); + if len >= 8 { + assert_ne!(data, plaintext, "len {len}: the data must actually be encrypted"); + } + ToyCtr::::decrypt_inplace(&key, &nonce, &mut data).expect("decryption"); + assert_eq!(data, plaintext, "len {len}: round trip"); + } +} + +// ---- nonce lengths ------------------------------------------------------------------------- + +/// Every permitted nonce length works and gives a different counter width. 12, 13, 14 and 15 bytes +/// on a 16-byte block are counters of 4, 3, 2 and 1 bytes; a 16-byte nonce (no counter) and an +/// 11-byte one (a 5-byte counter) are compile errors, so they cannot be tested here. +#[test] +fn every_permitted_nonce_length_works() { + fn round_trip() { + let key = toy_key(); + let nonce: [u8; N] = core::array::from_fn(|i| (i as u8).wrapping_mul(11).wrapping_add(3)); + let plaintext = (0..100u8).collect::>(); + + let (mut e, got) = Ctr::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(nonce), + ) + .unwrap(); + assert_eq!(got, nonce); + let mut ct = plaintext.clone(); + e.do_encrypt_inplace(&mut ct).unwrap(); + assert_ne!(ct, plaintext, "nonce length {N}: must actually encrypt"); + + Ctr::::decrypt_inplace(&key, &nonce, &mut ct) + .unwrap(); + assert_eq!(ct, plaintext, "nonce length {N}: round trip"); + } + + round_trip::<12>(); + round_trip::<13>(); + round_trip::<14>(); + round_trip::<15>(); +} + +// ---- memory --------------------------------------------------------------------------------- + +/// Pins the layout: permutation + nonce + counter (`u64`) + keystream block + the used offset, +/// rounded up to the `u64`'s alignment. +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::{align_of, size_of}; + + assert_eq!( + size_of::>(), + (size_of::() + NONCE_LEN + size_of::() + TOY_LEN + size_of::()) + .next_multiple_of(align_of::()) + ); + + // The direction marker is free, and the nonce length does not change the layout: the counter + // block is always a whole block. + assert_eq!(size_of::>(), size_of::>()); + // A longer nonce fits in the same padding, so the total is unchanged. + assert_eq!(size_of::>(), size_of::>()); +} diff --git a/crypto/cipher/tests/modes/ecb_tests.rs b/crypto/cipher/tests/modes/ecb_tests.rs new file mode 100644 index 00000000..20657177 --- /dev/null +++ b/crypto/cipher/tests/modes/ecb_tests.rs @@ -0,0 +1,416 @@ +//! Structural tests for ECB, driven by a toy permutation. +//! +//! These check the properties of the *mode* -- that it is the permutation applied block by block +//! with nothing chained, that both directions batch through the pair and four-block paths, call +//! sequencing, direction typing, the empty init data, SP 800-38A Appendix D error propagation, and +//! the codebook property that makes ECB unsuitable for data -- independently of any real cipher. The +//! known-answer tests against SP 800-38A Appendix F.1 are in the `aes` crate, +//! `crypto/aes/tests/sp800_38a_ecb_tests.rs`, and the ACVP set in `ecb_bc-test-data.rs` beside it. +//! +//! The toy's own conformance to [`ElectronicCodeBook`] is pinned once, by +//! `the_toy_permutation_conforms_to_the_trait` in `cbc_tests.rs`; it is the same `Toy` here. + +mod common; + +use bouncycastle_cipher::modes::Cbc; +use bouncycastle_cipher::modes::hazmat::Ecb; +use bouncycastle_cipher::padding::{PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; +use common::{SwappedFourToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; + +type ToyEcb = Ecb; +type SwappedEcb = Ecb; +type SwappedFourEcb = Ecb; + +/// The implementor hook `do_encrypt_blocks_inplace`, by value, for tests whose data is +/// block-shaped. +fn enc_blocks( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[[u8; TOY_LEN]; N], +) -> [[u8; TOY_LEN]; N] { + let mut blocks = *plaintext; + enc.do_encrypt_blocks_inplace(&mut blocks).unwrap(); + blocks +} + +/// The implementor hook `do_decrypt_blocks_inplace`, by value. +fn dec_blocks( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[[u8; TOY_LEN]; N], +) -> [[u8; TOY_LEN]; N] { + let mut blocks = *ciphertext; + dec.do_decrypt_blocks_inplace(&mut blocks).unwrap(); + blocks +} + +/// The flat streaming method `do_encrypt_inplace`, by value. +fn enc_flat( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *plaintext; + enc.do_encrypt_inplace(&mut data).unwrap(); + data +} + +/// The flat streaming method `do_decrypt_inplace`, by value. +fn dec_flat( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *ciphertext; + dec.do_decrypt_inplace(&mut data).unwrap(); + data +} + +fn encryptor() -> ToyEcb { + ToyEcb::::do_encrypt_init(&toy_key()).unwrap().0 +} + +fn decryptor() -> ToyEcb { + ToyEcb::::do_decrypt_init(&toy_key(), &[]).unwrap() +} + +// ---- the mode against the shared framework ------------------------------------------------ + +#[test] +fn ecb_conforms_to_the_block_cipher_framework() { + TestFrameworkBlockCipher::new() + .test::, ToyEcb>(); +} + +// ---- the spec equations ------------------------------------------------------------------- + +/// SP 800-38A Sec 6.1, written out longhand against the raw permutation: +/// +/// ```text +/// Cj = CIPH_K(Pj); Pj = CIPH^-1_K(Cj) for j = 1 ... n +/// ``` +/// +/// Each block is transformed "directly and independently", so this reference uses only the +/// single-block methods and never looks at a neighbouring block. +fn reference_ecb(perm: &Toy, input: &[[u8; TOY_LEN]], encrypt: bool) -> Vec<[u8; TOY_LEN]> { + input + .iter() + .map(|block| { + let mut b = *block; + if encrypt { + perm.encrypt_block(&mut b) + } else { + perm.decrypt_block(&mut b) + } + b + }) + .collect() +} + +/// The mode must reproduce the Sec 6.1 equations exactly, in both directions, and must therefore +/// agree with the raw permutation block for block. It must also *differ* from CBC from the very +/// first block, since CBC XORs the IV in before the cipher call. +#[test] +fn the_mode_matches_the_spec_equations() { + let key = toy_key(); + let perm = >::new(&key).unwrap(); + let plaintext: [[u8; TOY_LEN]; 5] = + core::array::from_fn(|i| core::array::from_fn(|j| (i * 31 + j * 7 + 1) as u8)); + + let (mut enc, init) = ToyEcb::::do_encrypt_init(&key).unwrap(); + assert_eq!(init, [0u8; 0], "ECB has no init data"); + let ct = enc_blocks(&mut enc, &plaintext); + assert_eq!( + ct.to_vec(), + reference_ecb(&perm, &plaintext, true), + "encryption is CIPH_K per block" + ); + + let mut dec = decryptor(); + let recovered = dec_blocks(&mut dec, &ct); + assert_eq!(recovered, plaintext, "round trip"); + assert_eq!( + recovered.to_vec(), + reference_ecb(&perm, &ct, false), + "decryption is CIPH^-1_K per block" + ); + + // Each block is exactly the permutation of that block, whatever surrounds it. + for (p, c) in plaintext.iter().zip(ct.iter()) { + let mut alone = *p; + perm.encrypt_block(&mut alone); + assert_eq!(&alone, c, "a block's ciphertext does not depend on its neighbours"); + } + + // ...and ECB is not CBC: CBC computes CIPH_K(P1 XOR IV), ECB computes CIPH_K(P1). + let iv: [u8; TOY_LEN] = core::array::from_fn(|i| 0xF0 ^ (i as u8)); + let (mut cbc, _) = Cbc::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .unwrap(); + let mut first = plaintext[0]; + cbc.do_encrypt_inplace(&mut first).unwrap(); + assert_ne!(first, ct[0], "ECB must not agree with CBC"); +} + +// ---- no state: determinism and the codebook property -------------------------------------- + +/// ECB is a function of the key and the block alone. Sec 6.1: "under a given key, any given +/// plaintext block always gets encrypted to the same ciphertext block." This is the property that +/// makes it unusable for data, and it is pinned here so the mode cannot quietly grow an IV or a +/// counter and stop being ECB. +#[test] +fn ecb_is_deterministic_and_leaks_equal_blocks() { + let key = toy_key(); + let block = [0x5Au8; TOY_LEN]; + let plaintext = [block, [0x11; TOY_LEN], block, block]; + + let ct_a = enc_blocks(&mut encryptor(), &plaintext); + let ct_b = enc_blocks(&mut encryptor(), &plaintext); + assert_eq!(ct_a, ct_b, "the same plaintext under the same key gives the same ciphertext"); + + assert_eq!(ct_a[0], ct_a[2], "equal plaintext blocks give equal ciphertext blocks"); + assert_eq!(ct_a[0], ct_a[3]); + assert_ne!(ct_a[0], ct_a[1], "different plaintext blocks give different ciphertext blocks"); + + // The one-shot sees the same thing: `encrypt` returns the empty init data and is repeatable. + // (The RNG-taking one-shot is not an alternative here -- it panics; see below.) + let flat: [u8; 4 * TOY_LEN] = plaintext.as_flattened().try_into().unwrap(); + let mut once = flat; + let (n_a, init_a): (usize, [u8; 0]) = + ToyEcb::::encrypt_inplace(&key, &mut once).unwrap(); + assert_eq!(n_a, once.len(), "encrypt must report the number of bytes written"); + let mut twice = flat; + let (n_b, init_b) = ToyEcb::::encrypt_inplace(&key, &mut twice).unwrap(); + assert_eq!(n_b, twice.len(), "encrypt must report the number of bytes written"); + assert_eq!(init_a, init_b); + assert_eq!(once, twice, "no init data and no randomness, so the one-shot is repeatable"); + assert_eq!(once, *ct_a.as_flattened()); +} + +/// The RNG-taking constructor must panic rather than quietly ignore the RNG. ECB has no init data +/// to generate (SP 800-38A Table D.2 lists its IV column as "Not applicable"), so a caller reaching +/// for `do_encrypt_init_rng` has mistaken ECB for a randomized mode; +/// [`BlockCipherEncryptor::do_encrypt_init_rng`]'s contract requires an implementation with +/// `INIT_DATA_LEN == 0` to say so. It is a programmer error, not bad input, hence a panic and not a +/// [`SymmetricCipherError`]. +#[test] +#[should_panic(expected = "ECB has no initialization data")] +fn the_rng_constructor_panics() { + let key = toy_key(); + let mut rng = FixedSeedRNG::<0>::new([]); + let _ = ToyEcb::::do_encrypt_init_rng(&key, &mut rng); +} + +/// ...and so does the one-shot provided over it: `encrypt_rng_inplace` is `do_encrypt_init_rng` +/// followed by `do_encrypt_inplace`, so it panics in the same case and for the same reason. Pinned +/// separately because it is the call a user is most likely to reach for. +#[test] +#[should_panic(expected = "ECB has no initialization data")] +fn the_rng_one_shot_panics() { + let key = toy_key(); + let mut block = [0x42u8; TOY_LEN]; + let _ = ToyEcb::::encrypt_rng_inplace( + &key, + &mut FixedSeedRNG::<0>::new([]), + &mut block, + ); +} + +// ---- batching: pairs and fours, in both directions ---------------------------------------- + +/// Sec 6.1: "multiple forward cipher functions and inverse cipher functions can be computed in +/// parallel" -- so, unlike CBC and CFB, *both* directions batch. [`SwappedPairToy`] swaps its two +/// pair results, so a pair handed over together comes out wrong in either direction, while blocks +/// handed over singly come out right. +#[test] +fn the_pair_path_is_used_in_both_directions() { + let key = toy_key(); + let plaintext = [[0xA5u8; TOY_LEN], [0x5Au8; TOY_LEN]]; + let ct = enc_blocks(&mut encryptor(), &plaintext); + + // Encryption: a pair goes through encrypt_2blocks, so the swapped toy returns them swapped. + let (mut enc, _) = SwappedEcb::::do_encrypt_init(&key).unwrap(); + let swapped_ct = enc_blocks(&mut enc, &plaintext); + assert_eq!(swapped_ct, [ct[1], ct[0]], "encrypting a pair must go through encrypt_2blocks"); + + // ...and one block at a time avoids the pair path. + let (mut enc, _) = SwappedEcb::::do_encrypt_init(&key).unwrap(); + assert_eq!([enc_flat(&mut enc, &plaintext[0]), enc_flat(&mut enc, &plaintext[1])], ct); + + // Decryption likewise. + let mut dec = SwappedEcb::::do_decrypt_init(&key, &[]).unwrap(); + assert_eq!( + dec_blocks(&mut dec, &ct), + [plaintext[1], plaintext[0]], + "decrypting a pair must go through decrypt_2blocks" + ); + let mut dec = SwappedEcb::::do_decrypt_init(&key, &[]).unwrap(); + assert_eq!([dec_flat(&mut dec, &ct[0]), dec_flat(&mut dec, &ct[1])], plaintext); +} + +/// The four-block path must be taken, and only for full fours, in both directions. +/// [`SwappedFourToy`] rotates its four results while its pair and single-block methods are +/// correct, so five blocks handed over together are wrong (four rotated, then one right) and the +/// same blocks as two pairs or singly are right. +#[test] +fn the_four_block_path_is_used_in_both_directions() { + let key = toy_key(); + let plaintext: [[u8; TOY_LEN]; 5] = core::array::from_fn(|i| [0x10 * i as u8 + 1; TOY_LEN]); + let ct = enc_blocks(&mut encryptor(), &plaintext); + assert_eq!(dec_blocks(&mut decryptor(), &ct), plaintext); + + let (mut enc, _) = SwappedFourEcb::::do_encrypt_init(&key).unwrap(); + let rotated = enc_blocks(&mut enc, &plaintext); + assert_ne!(rotated, ct, "five blocks must go through encrypt_4blocks"); + assert_eq!(rotated[4], ct[4], "the fifth block goes through the single path and is right"); + assert_eq!(&rotated[..4], &[ct[1], ct[2], ct[3], ct[0]], "four rotated"); + + let (mut enc, _) = SwappedFourEcb::::do_encrypt_init(&key).unwrap(); + let a = enc_blocks(&mut enc, &[plaintext[0], plaintext[1]]); + let b = enc_blocks(&mut enc, &[plaintext[2], plaintext[3]]); + assert_eq!([a, b].as_flattened(), &ct[..4], "pairs use the pair path only"); + + let mut dec = SwappedFourEcb::::do_decrypt_init(&key, &[]).unwrap(); + assert_ne!(dec_blocks(&mut dec, &ct), plaintext, "five blocks must go through decrypt_4blocks"); + let mut dec = SwappedFourEcb::::do_decrypt_init(&key, &[]).unwrap(); + for (c, p) in ct.iter().zip(plaintext.iter()) { + assert_eq!(&dec_flat(&mut dec, c), p, "the single-block path must not batch"); + } +} + +/// Grouping cannot matter -- there is no state to carry between calls -- but the contract is the +/// same as for the other modes and the batching paths differ per grouping, so it is pinned. +#[test] +fn call_grouping_does_not_change_the_result() { + let plaintext: [[u8; TOY_LEN]; 11] = + core::array::from_fn(|i| core::array::from_fn(|j| (i * TOY_LEN + j) as u8)); + let reference = enc_blocks(&mut encryptor(), &plaintext); + + let mut enc = encryptor(); + let mut got = [[0u8; TOY_LEN]; 11]; + got[0] = enc_flat(&mut enc, &plaintext[0]); + got[1..3].copy_from_slice(&enc_blocks(&mut enc, &[plaintext[1], plaintext[2]])); + let rest: [[u8; TOY_LEN]; 8] = plaintext[3..11].try_into().unwrap(); + got[3..11].copy_from_slice(&enc_blocks(&mut enc, &rest)); + assert_eq!(got, reference); + + for grouping in [1usize, 2, 4, 5, 8, 11] { + let mut dec = decryptor(); + let mut out = Vec::new(); + for chunk in reference.chunks(grouping) { + let mut buf = chunk.to_vec(); + dec.do_decrypt_blocks_inplace(&mut buf).unwrap(); + out.extend_from_slice(&buf); + } + assert_eq!(out, plaintext.to_vec(), "decrypting in groups of {grouping}"); + } +} + +/// The flat streaming method and the one-shots must agree with the block-shaped hook. +#[test] +fn flat_streaming_and_one_shots_agree_with_the_block_hook() { + let key = toy_key(); + let plaintext = [[0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; + let flat_plaintext: [u8; 3 * TOY_LEN] = plaintext.as_flattened().try_into().unwrap(); + + let block_ct = enc_blocks(&mut encryptor(), &plaintext); + assert_eq!(*block_ct.as_flattened(), enc_flat(&mut encryptor(), &flat_plaintext)); + + let mut buf = flat_plaintext; + let (_, init) = ToyEcb::::encrypt_inplace(&key, &mut buf).unwrap(); + assert_eq!(buf, *block_ct.as_flattened(), "one-shot must equal streaming"); + ToyEcb::::decrypt_inplace(&key, &init, &mut buf).unwrap(); + assert_eq!(buf, flat_plaintext); + + assert_eq!(dec_blocks(&mut decryptor(), &block_ct), plaintext); + let flat_ct: [u8; 3 * TOY_LEN] = block_ct.as_flattened().try_into().unwrap(); + assert_eq!(dec_flat(&mut decryptor(), &flat_ct), flat_plaintext); +} + +// ---- SP 800-38A Appendix D error propagation --------------------------------------------- + +/// Table D.2 for ECB: a bit error in `Cj` gives "RBE in the decryption of Cj" and nothing else -- +/// Appendix D: "For the ECB, OFB, and CTR modes, bit errors within a ciphertext block do not affect +/// the decryption of any other blocks." The "RBE" spread is the block cipher's diffusion, which +/// the byte-local toy cannot show and this does not claim. What it pins exactly instead: the +/// corrupted block is the *only* one affected, and within it the flipped bit went through the +/// inverse cipher -- it comes out in the same byte moved by the toy's `rotate_right(1)`, the +/// signature of `CIPH^-1_K` acting on it rather than of an in-place XOR. +#[test] +fn a_ciphertext_bit_error_affects_only_its_own_block() { + let plaintext = [[0x00u8; TOY_LEN], [0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; + let ct = enc_blocks(&mut encryptor(), &plaintext); + + for byte in 0..TOY_LEN { + for bit in 0..8 { + let flip = 1u8 << bit; + let mut corrupt = ct; + corrupt[1][byte] ^= flip; + let got = dec_blocks(&mut decryptor(), &corrupt); + assert_eq!(got[0], plaintext[0]); + let mut expected = plaintext[1]; + expected[byte] ^= flip.rotate_right(1); + assert_eq!( + got[1], expected, + "C2 byte {byte} bit {bit}: P2 carries the flip through the toy's inverse" + ); + assert_eq!(got[2], plaintext[2], "P3 is unaffected: nothing chains"); + assert_eq!(got[3], plaintext[3]); + } + } +} + +// ---- key handling ------------------------------------------------------------------------ + +#[test] +fn a_key_of_the_wrong_type_is_rejected() { + let bytes: [u8; TOY_LEN] = core::array::from_fn(|i| (i as u8) + 1); + let seed = KeyMaterial::::from_bytes_as_type(&bytes, KeyType::Seed).unwrap(); + assert!(ToyEcb::::do_encrypt_init(&seed).is_err()); + assert!(ToyEcb::::do_decrypt_init(&seed, &[]).is_err()); +} + +// ---- composition with the padding layer -------------------------------------------------- + +/// ECB is block-aligned by contract, so arbitrary-length data goes through `bouncycastle_cipher::padding` +/// like the other modes; its `INIT_DATA_LEN` of 0 flows through the adapters as an empty array. +#[test] +fn the_padding_layer_round_trips_every_length() { + type Enc = PaddedBlockCipherEncryptor, PKCS7, TOY_LEN, 0, TOY_LEN>; + type Dec = PaddedBlockCipherDecryptor, PKCS7, TOY_LEN, 0, TOY_LEN>; + + for len in 0..=(3 * TOY_LEN + 1) { + let plaintext: Vec = (0..len).map(|i| (i * 5 + 3) as u8).collect(); + let mut ciphertext = vec![0u8; Enc::encrypt_out_len(len)]; + let (init, written) = + Enc::encrypt_out(&toy_key(), &plaintext, &mut ciphertext).expect("padded encryption"); + assert_eq!(init, [0u8; 0]); + assert_eq!(written, ciphertext.len(), "len {len}"); + let mut recovered = vec![0u8; Dec::decrypt_out_len(written)]; + let n = Dec::decrypt_out(&toy_key(), &init, &ciphertext, &mut recovered) + .expect("padded decryption"); + assert_eq!(&recovered[..n], &plaintext[..], "len {len}: round trip through PKCS7"); + } +} + +// ---- memory ------------------------------------------------------------------------------ + +/// Pins the module docs' claim that an ECB value is exactly the permutation: +/// `size_of::>() == size_of::

()`. +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::size_of; + assert_eq!(size_of::>(), size_of::()); + assert_eq!(size_of::>(), size_of::>()); + // One block smaller than CBC, which stores a chaining value. + assert_eq!( + size_of::>() + TOY_LEN, + size_of::>() + ); +} diff --git a/crypto/cipher/tests/modes/gcm_tests.rs b/crypto/cipher/tests/modes/gcm_tests.rs new file mode 100644 index 00000000..751cb3ee --- /dev/null +++ b/crypto/cipher/tests/modes/gcm_tests.rs @@ -0,0 +1,332 @@ +//! Structural tests for GCM, driven by a toy permutation. +//! +//! 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 the `aes` crate's `gcm_bc-test-data.rs`, `gmac_bc-test-data.rs` and +//! `gcm_bc_java_tests.rs`. + +mod common; + +use bouncycastle_cipher::modes::Gcm; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use common::{ForwardOnlyToy, 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_detached_rng_out( + &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_encrypt_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_encrypt_final_detachedtag().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_decrypt_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_encrypt_out(&message[..data_split], &mut ct).unwrap(); + enc.do_encrypt_out(&message[data_split..], &mut ct[n..]).unwrap(); + let (_, _, tag) = enc.do_encrypt_final_detachedtag().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_detached_out(&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_detached_out(&key, &nonce, aad, &[], &tag, &mut []).unwrap(); + + // Wrong AAD must fail verification. + match ToyGcm::::decrypt_detached_out(&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.do_decrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_decrypt_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.do_decrypt_out_len(rest.len()); + let mut buf = vec![0u8; expect]; + dec.do_decrypt_out(rest, &mut buf).unwrap(); + let (_last, last_len) = dec.do_decrypt_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_detached_out( + &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_decrypt_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_decrypt_final_detachedtag(&tag) { + Err(SymmetricCipherError::AEADTagCheckFailed) => {} + other => panic!("expected AEADTagCheckFailed, got {other:?}"), + } +} + +/// SP 800-38D Sec 5.1: "GCM does not employ the inverse cipher function." GCTR (Sec 6.5) applies +/// `CIPH_K` to counter blocks in both directions and GHASH (Sec 6.4) is field arithmetic, so a +/// permutation that implements only the forward direction works. [`ForwardOnlyToy`] panics from +/// every inverse entry point; a detached one-shot round trip over it, at a length that is not a +/// whole number of blocks, must therefore agree with [`Toy`] and succeed. +#[test] +fn neither_direction_uses_the_inverse_cipher() { + fn round_trip

() -> ([u8; 48], [u8; 16]) + where + P: bouncycastle_core::hazmat::ElectronicCodeBook, + { + let key = toy_key(); + 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_detached_rng_out( + &key, + &mut FixedSeedRNG::<12>::new([0x4Du8; 12]), + aad, + message, + &mut ct, + ) + .unwrap(); + assert_ne!(&ct[..], &message[..]); + let mut pt = [0u8; 48]; + Gcm::::decrypt_detached_out( + &key, &nonce, aad, &ct, &tag, &mut pt, + ) + .unwrap(); + assert_eq!(&pt[..], &message[..]); + (ct, tag) + } + + // The forward-only toy must agree with the real one, or the round trip proves nothing. + assert_eq!(round_trip::(), round_trip::(), "the two toys must agree"); +} + +/// The whole [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] contract -- which runs the +/// symmetric-cipher suite first -- through the shared framework, over the toy 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::aead::TestFrameworkAEADCipher; + TestFrameworkAEADCipher::new() + .test_encryptor_decryptor::, ToyGcm>( + ); + TestFrameworkAEADCipher::new() + .test_encryptor_decryptor::, ToyGcm>( + ); +} + +/// 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 = ToyGcm; + type Dec = ToyGcm; + + let key = toy_key(); + let mut ct = [0u8; 32 + 16]; + let (nonce, n) = Enc::encrypt_with_aad_out(&key, b"aad", &[0x33u8; 32], &mut ct).unwrap(); + ct[0] ^= 1; + + let mut out = [0xEEu8; 32]; + assert!(matches!( + Dec::decrypt_with_aad_out(&key, &nonce, b"aad", &ct[..n], &mut out), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(out, [0u8; 32], "decrypt_with_aad_out 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_detached_out( + &key, + &nonce, + b"aad", + &ct[..32], + &tag, + &mut out + ), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); + assert_eq!(out, [0u8; 32], "decrypt_detached_out must zeroize on a failed tag check"); +} diff --git a/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs new file mode 100644 index 00000000..24eb5987 --- /dev/null +++ b/crypto/cipher/tests/modes/symmetric_cipher_api_tests.rs @@ -0,0 +1,272 @@ +//! The stream modes through the [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] API. +//! +//! The stream traits extend the symmetric-cipher traits with `FINAL_LEN = 0`: `Cfb` and `Cfb8` +//! implement both, the separate-output half over `bouncycastle_cipher::stream`'s helpers, and +//! `Ctr` gets both from `StreamCipher` over its keystream. That is what lets a caller +//! hold any of the five modes through one trait: a padded `Cbc` or `Ecb` with the padded block as +//! its final output, and a stream mode with nothing. +//! +//! What is worth testing here is the bridge, not the ciphers, which their own suites cover: +//! +//! * that the modes really do satisfy the shared conformance suite for those traits, the same one +//! the padding adapters run; +//! * that the separate-output API agrees byte for byte with the in-place one, since it is written +//! in terms of it; +//! * that it leaves the caller's input alone, which is the one thing the in-place API cannot offer +//! and therefore the reason to have both; +//! * and that the length predictions are exact, not upper bounds. + +mod common; + +use bouncycastle_cipher::modes::{Cfb, Cfb8, Ctr}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::traits::{ + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkSymmetricCipher; +use common::{TOY_LEN, Toy, toy_key}; + +type ToyCfb

= Cfb; +type ToyCfb8 = Cfb8; +type ToyCtr = Ctr; + +/// All three stream modes must satisfy the shared conformance suite for the symmetric-cipher +/// traits -- the same suite the padded adapters run, with `required_alignment` left at 1 because a +/// stream cipher accepts every length. +/// +/// It pins the whole contract: one-shot round trips at every length, the `std` one-shots against +/// the `_out` ones, streaming in eight chunkings with `update_out_len` exact on every call, +/// `do_encrypt_final_out` / `do_decrypt_final_out` against `do_encrypt_final` / `do_decrypt_final`, +/// a driven RNG reproducing its init data, corruption detection, short output buffers refused with +/// the required length, and the key-type and security-strength policy. +#[test] +fn the_stream_modes_conform_to_the_symmetric_cipher_suite() { + let framework = TestFrameworkSymmetricCipher::new(); + framework + .test_encryptor_decryptor::, ToyCfb>(); + framework + .test_encryptor_decryptor::, ToyCfb8>( + ); + framework.test_encryptor_decryptor::, ToyCtr>(); +} + +/// The separate-output API must produce exactly what the in-place API produces, for the same key +/// and init data. The separate-output API is written in terms of `do_encrypt_inplace`, so this is +/// the check that the bridge adds nothing and loses nothing. +#[test] +fn the_two_apis_agree_byte_for_byte() { + fn check( + name: &str, + key: &KeyMaterial, + ) where + E: StreamCipherEncryptor + + SymmetricCipherEncryptor, + D: StreamCipherDecryptor + + SymmetricCipherDecryptor, + { + for len in [0usize, 1, 15, 16, 17, 63, 64, 171] { + let plaintext: Vec = (0..len).map(|i| (i * 7 + 1) as u8).collect(); + + // The in-place API, which the mode implements directly. + let (mut enc, init) = E::do_encrypt_init(key).unwrap(); + let mut in_place = plaintext.clone(); + enc.do_encrypt_inplace(&mut in_place).unwrap(); + + // The separate-output API, under the same init data. + let mut dec_as_sym = + >::do_decrypt_init( + key, &init, + ) + .unwrap(); + let mut out = vec![0u8; plaintext.len()]; + let n = dec_as_sym.do_decrypt_out(&in_place, &mut out).unwrap(); + let (last, last_len) = dec_as_sym.do_decrypt_final().unwrap(); + assert_eq!(n, plaintext.len(), "{name}, len {len}: everything is released immediately"); + assert_eq!(last, [0u8; 0], "{name}: a stream cipher has no final output"); + assert_eq!(last_len, 0, "{name}: ...and none of it is data"); + assert_eq!(out, plaintext, "{name}, len {len}: the two APIs must agree"); + } + } + + check::, ToyCfb, TOY_LEN, TOY_LEN>("Cfb", &toy_key()); + check::, ToyCfb8, TOY_LEN, TOY_LEN>("Cfb8", &toy_key()); + check::, ToyCtr, TOY_LEN, 12>("Ctr", &toy_key()); +} + +/// The separate-output API must leave the caller's input untouched. That is the whole reason a +/// stream cipher wants it as well as the in-place one, so it is worth asserting rather than +/// assuming. +#[test] +fn the_input_buffer_is_not_modified() { + let key = toy_key(); + let plaintext: Vec = (0..100u8).collect(); + let original = plaintext.clone(); + + let (mut enc, _init) = + as SymmetricCipherEncryptor>::do_encrypt_init( + &key, + ) + .unwrap(); + let mut ciphertext = vec![0u8; plaintext.len()]; + enc.do_encrypt_out(&plaintext, &mut ciphertext).unwrap(); + + assert_eq!(plaintext, original, "the plaintext must be left alone"); + assert_ne!(ciphertext, original, "...and the ciphertext must actually be encrypted"); +} + +/// The length predictions are exact for a stream cipher, not upper bounds: what goes in comes out. +#[test] +fn the_length_predictions_are_exact() { + let key = toy_key(); + for len in [0usize, 1, 15, 16, 17, 1000] { + assert_eq!( + as SymmetricCipherEncryptor>::encrypt_out_len(len), + len, + "encrypt_out_len is the identity" + ); + assert_eq!( + as SymmetricCipherDecryptor>::decrypt_out_len(len), + len, + "decrypt_out_len is exact, not an upper bound" + ); + + let (enc, _) = + as SymmetricCipherEncryptor>::do_encrypt_init(&key) + .unwrap(); + assert_eq!(enc.do_encrypt_out_len(len), len, "update_out_len is the identity"); + } +} + +/// A short output buffer is refused with the length it needed, and nothing is consumed -- so the +/// same call with a big enough buffer then succeeds and gives the answer it would have given. +#[test] +fn a_short_output_buffer_is_refused_without_consuming_anything() { + use bouncycastle_core::errors::SymmetricCipherError; + + let key = toy_key(); + let plaintext: Vec = (0..32u8).collect(); + + let (mut enc, init) = + as SymmetricCipherEncryptor>::do_encrypt_init( + &key, + ) + .unwrap(); + + let mut too_small = vec![0u8; plaintext.len() - 1]; + match enc.do_encrypt_out(&plaintext, &mut too_small) { + Err(SymmetricCipherError::OutputBufferTooSmall(needed)) => { + assert_eq!(needed, plaintext.len(), "the error carries the required length"); + } + other => panic!("expected OutputBufferTooSmall, got {other:?}"), + } + + // Nothing was consumed, so the keystream has not advanced: the retry must give exactly what a + // fresh encryptor under the same init data would. + let mut big_enough = vec![0u8; plaintext.len()]; + enc.do_encrypt_out(&plaintext, &mut big_enough).unwrap(); + + let (mut fresh, _) = ToyCfb::::do_encrypt_init_rng( + &key, + &mut bouncycastle_core_test_framework::FixedSeedRNG::::new(init), + ) + .unwrap(); + let mut reference = plaintext.clone(); + fresh.do_encrypt_inplace(&mut reference).unwrap(); + assert_eq!(big_enough, reference, "the refused call must not have advanced the keystream"); +} + +/// The decrypt side refuses a short output buffer too, with the length it needed. +/// +/// The mirror of the encryptor test above. Worth having separately rather than assuming symmetry: +/// the two directions are separate impls with their own buffer check, and mutation testing showed the +/// decryptor's comparison was unexercised until this existed. +#[test] +fn a_short_output_buffer_is_refused_when_decrypting_too() { + use bouncycastle_core::errors::SymmetricCipherError; + + let key = toy_key(); + let plaintext: Vec = (0..32u8).collect(); + + // Encrypt normally, then try to decrypt into a buffer one byte too small. + let (mut enc, init) = ToyCfb::::do_encrypt_init(&key).unwrap(); + let mut ciphertext = plaintext.clone(); + enc.do_encrypt_inplace(&mut ciphertext).unwrap(); + + let mut dec = + as SymmetricCipherDecryptor>::do_decrypt_init( + &key, &init, + ) + .unwrap(); + + let mut too_small = vec![0u8; ciphertext.len() - 1]; + match dec.do_decrypt_out(&ciphertext, &mut too_small) { + Err(SymmetricCipherError::OutputBufferTooSmall(needed)) => { + assert_eq!(needed, ciphertext.len(), "the error carries the required length"); + } + other => panic!("expected OutputBufferTooSmall, got {other:?}"), + } + + // Nothing was consumed, so the retry recovers the plaintext exactly. + let mut big_enough = vec![0u8; ciphertext.len()]; + let n = dec.do_decrypt_out(&ciphertext, &mut big_enough).unwrap(); + assert_eq!(n, ciphertext.len()); + assert_eq!(big_enough, plaintext, "the refused call must not have advanced the keystream"); + + // An oversized buffer is fine, the data lands in the leading bytes and the rest is zeroed: the + // check is "too short", not "not exactly equal". + let mut oversized = vec![0xAAu8; ciphertext.len() + 8]; + let mut dec = + as SymmetricCipherDecryptor>::do_decrypt_init( + &key, &init, + ) + .unwrap(); + let n = dec.do_decrypt_out(&ciphertext, &mut oversized).expect("an oversized buffer is fine"); + assert_eq!(n, ciphertext.len()); + assert_eq!(&oversized[..n], &plaintext[..], "the data lands in the leading bytes"); + assert!(oversized[n..].iter().all(|&b| b == 0), "the rest is zeroed"); +} + +/// The allocating one-shots -- the `Vec`-returning `encrypt` / `decrypt`, which no other test here +/// reaches -- round-trip at a length that is not a whole number of blocks, for all three stream +/// modes: the shape a caller most often wants from this API. +#[test] +fn the_allocating_one_shots_round_trip() { + let key = toy_key(); + let message = b"a message of no particular length at all"; + + // CFB128 + let (iv, ct) = as SymmetricCipherEncryptor>::encrypt( + &key, message, + ) + .unwrap(); + assert_eq!(ct.len(), message.len(), "a stream cipher does not change the length"); + let back = as SymmetricCipherDecryptor>::decrypt( + &key, &iv, &ct, + ) + .unwrap(); + assert_eq!(back, message); + + // CFB8 + let (iv, ct) = as SymmetricCipherEncryptor>::encrypt( + &key, message, + ) + .unwrap(); + let back = as SymmetricCipherDecryptor>::decrypt( + &key, &iv, &ct, + ) + .unwrap(); + assert_eq!(back, message); + + // CTR + let (nonce, ct) = + as SymmetricCipherEncryptor>::encrypt(&key, message) + .unwrap(); + assert_eq!(nonce.len(), 12, "CTR's init data is its 12-byte nonce"); + let back = as SymmetricCipherDecryptor>::decrypt( + &key, &nonce, &ct, + ) + .unwrap(); + assert_eq!(back, message); +} diff --git a/crypto/cipher/tests/padding/nopadding_tests.rs b/crypto/cipher/tests/padding/nopadding_tests.rs new file mode 100644 index 00000000..2afe4c85 --- /dev/null +++ b/crypto/cipher/tests/padding/nopadding_tests.rs @@ -0,0 +1,55 @@ +//! Tests for `NoPadding`: a `BlockCipherPadding` scheme that adds nothing and refuses to. +//! +//! There is no rule to transcribe; the contract is that `pad` is an error whenever it is called +//! (being called means a partial block existed), `unpad` reports a whole block of data, and the +//! scheme declares that it does not pad aligned data, so the adapters emit no final block. + +use bouncycastle_cipher::padding::{NoPadding, PKCS7}; +use bouncycastle_core::errors::PaddingError; +use bouncycastle_core::traits::BlockCipherPadding; + +fn pad_always_refuses() { + for data_len in 0..K { + let mut block: [u8; K] = core::array::from_fn(|i| i as u8 ^ 0xA5); + let original = block; + assert_eq!( + >::pad(&mut block, data_len), + Err(PaddingError::PaddingNotPermitted), + "K={K} data_len={data_len}" + ); + assert_eq!(block, original, "K={K} data_len={data_len}: nothing may be written"); + } + // Beyond the block is the same error every scheme gives. + let mut block = [0u8; K]; + assert_eq!( + >::pad(&mut block, K), + Err(PaddingError::DataLengthTooLong(K - 1)) + ); +} + +#[test] +fn pad_refuses_every_data_length() { + pad_always_refuses::<1>(); + pad_always_refuses::<8>(); + pad_always_refuses::<16>(); + pad_always_refuses::<255>(); +} + +#[test] +fn unpad_reports_the_whole_block_as_data() { + for fill in [0x00u8, 0x01, 0x10, 0x7f, 0xff] { + assert_eq!(>::unpad(&[fill; 16]), Ok(16)); + assert_eq!(>::unpad(&[fill; 8]), Ok(8)); + } + // ...including blocks that would be well-formed PKCS7 padding: there is nothing to strip. + let mut pkcs7 = [0u8; 16]; + >::pad(&mut pkcs7, 5).unwrap(); + assert_eq!(>::unpad(&pkcs7), Ok(16)); +} + +/// The flag the adapters key off: PKCS7 always appends a block to aligned data, NoPadding never. +#[test] +fn always_pads_flags() { + assert!(>::ALWAYS_PADS); + assert!(!>::ALWAYS_PADS); +} diff --git a/crypto/cipher/tests/padding/padded_tests.rs b/crypto/cipher/tests/padding/padded_tests.rs new file mode 100644 index 00000000..287b682b --- /dev/null +++ b/crypto/cipher/tests/padding/padded_tests.rs @@ -0,0 +1,412 @@ +//! Tests for PaddedBlockCipherEncryptor / PaddedBlockCipherDecryptor. +//! +//! No real block cipher exists in the workspace yet, so these tests drive the adapters with a toy +//! CBC-style cipher whose "block permutation" is XOR with the key. It is cryptographically worthless +//! but exercises every code path of the adapters: IV generation, chaining state across calls, and +//! the one-block lag on decryption. + +use bouncycastle_cipher::padding::{ + NoPadding, PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor, +}; +use bouncycastle_core::errors::{KeyMaterialError, PaddingError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, RNG, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::block_cipher::TestFrameworkBlockCipher; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkSymmetricCipher; +use bouncycastle_rng::hash_drbg80090a::{HashDRBG80090A, HashDRBG80090AParams_SHA256}; + +const B: usize = 8; + +/// c_j = p_j ^ c_{j-1} ^ key ; p_j = c_j ^ c_{j-1} ^ key +struct ToyCbc { + key: [u8; B], + chain: [u8; B], +} + +impl ToyCbc { + fn check_key(key: &KeyMaterial) -> Result<[u8; B], SymmetricCipherError> { + if key.key_type() != KeyType::SymmetricCipherKey { + return Err(KeyMaterialError::InvalidKeyType("expected SymmetricCipherKey"))?; + } + if key.security_strength() < Self::MAX_SECURITY_STRENGTH { + return Err(KeyMaterialError::GenericError("key too weak"))?; + } + let mut k = [0u8; B]; + k.copy_from_slice(key.ref_to_bytes()); + Ok(k) + } +} + +impl Algorithm for ToyCbc { + const ALG_NAME: &'static str = "ToyCbc"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::None; +} + +impl BlockCipherEncryptor for ToyCbc { + fn do_encrypt_init(key: &KeyMaterial) -> Result<(Self, [u8; B]), SymmetricCipherError> { + let mut rng = HashDRBG80090A::::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; B]), SymmetricCipherError> { + let key = Self::check_key(key)?; + let mut iv = [0u8; B]; + rng.next_bytes_out(&mut iv)?; + Ok((Self { key, chain: iv }, iv)) + } + fn do_encrypt_blocks_inplace( + &mut self, + blocks: &mut [[u8; B]], + ) -> Result { + for block in blocks.iter_mut() { + for (b, (c, k)) in block.iter_mut().zip(self.chain.iter().zip(self.key.iter())) { + *b ^= c ^ k; + } + self.chain = *block; + } + Ok(blocks.len() * B) + } +} + +impl BlockCipherDecryptor for ToyCbc { + fn do_decrypt_init(key: &KeyMaterial, iv: &[u8; B]) -> Result { + Ok(Self { key: Self::check_key(key)?, chain: *iv }) + } + fn do_decrypt_blocks_inplace( + &mut self, + blocks: &mut [[u8; B]], + ) -> Result { + let len = blocks.len() * B; + for block in blocks.iter_mut() { + let ct = *block; + for (b, (c, k)) in block.iter_mut().zip(self.chain.iter().zip(self.key.iter())) { + *b ^= c ^ k; + } + self.chain = ct; + } + Ok(len) + } +} + +type Enc = PaddedBlockCipherEncryptor; +type Dec = PaddedBlockCipherDecryptor; +/// The same adapters over `NoPadding`: an alignment check rather than a padding scheme. +type EncNP = PaddedBlockCipherEncryptor; +type DecNP = PaddedBlockCipherDecryptor; + +fn key() -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(&[0x5a; B], KeyType::SymmetricCipherKey).unwrap() +} + +fn msg(len: usize) -> Vec { + (0..len).map(|i| (i * 7 + 3) as u8).collect() +} + +#[test] +fn toy_cipher_passes_core_test_framework() { + TestFrameworkBlockCipher::new().test::(); +} + +/// The padded adapters are the first implementors of `SymmetricCipherEncryptor` / +/// `SymmetricCipherDecryptor`, so this is also what exercises those traits' provided one-shots. +#[test] +fn padded_adapters_pass_the_symmetric_cipher_framework() { + TestFrameworkSymmetricCipher::new().test_encryptor_decryptor::(); +} + +#[test] +fn one_shot_roundtrip_all_lengths() { + let key = key(); + for len in 0..=3 * B + 1 { + let pt = msg(len); + let mut ct = vec![0u8; Enc::encrypt_out_len(len)]; + let (iv, n) = Enc::encrypt_out(&key, &pt, &mut ct).unwrap(); + assert_eq!(n, ct.len()); + assert_eq!(n, (len / B + 1) * B, "always one extra padding block"); + + let mut out = vec![0u8; Dec::decrypt_out_len(n)]; + let m = Dec::decrypt_out(&key, &iv, &ct[..n], &mut out).unwrap(); + assert_eq!(&out[..m], &pt[..]); + } +} + +#[test] +fn streaming_matches_one_shot_for_every_chunking() { + let key = key(); + let len = 5 * B + 3; + let pt = msg(len); + + for chunk in [1usize, 2, 3, 7, 8, 9, 15, 16, 17, len] { + // encrypt in chunks + let (mut enc, iv) = Enc::do_encrypt_init(&key).unwrap(); + let mut ct = Vec::new(); + for piece in pt.chunks(chunk) { + let expect = enc.do_encrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact"); + ct.extend_from_slice(&buf[..n]); + } + let (last, last_len) = enc.do_encrypt_final().unwrap(); + assert_eq!(last_len, B, "PKCS7 always emits a final block"); + ct.extend_from_slice(&last[..last_len]); + assert_eq!(ct.len(), Enc::encrypt_out_len(len)); + + // one-shot decrypt + let mut out = vec![0u8; Dec::decrypt_out_len(ct.len())]; + let m = Dec::decrypt_out(&key, &iv, &ct, &mut out).unwrap(); + assert_eq!(&out[..m], &pt[..], "chunk {chunk}"); + + // decrypt in the same chunks + let mut dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + let mut rec = Vec::new(); + for piece in ct.chunks(chunk) { + let expect = dec.do_decrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact (decrypt)"); + rec.extend_from_slice(&buf[..n]); + } + let (block, data_len) = dec.do_decrypt_final().unwrap(); + rec.extend_from_slice(&block[..data_len]); + assert_eq!(rec, pt, "chunk {chunk}"); + } +} + +#[test] +fn decryptor_lags_by_exactly_one_block() { + let key = key(); + let (iv, ct) = { + let mut ct = vec![0u8; Enc::encrypt_out_len(2 * B)]; + let (iv, _) = Enc::encrypt_out(&key, &msg(2 * B), &mut ct).unwrap(); + (iv, ct) + }; + assert_eq!(ct.len(), 3 * B); + let mut dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + let mut out = [0u8; 3 * B]; + // first block: nothing can be released yet + assert_eq!(dec.do_decrypt_out_len(B), 0); + assert_eq!(dec.do_decrypt_out(&ct[..B], &mut out).unwrap(), 0); + // second block: releases the first + assert_eq!(dec.do_decrypt_out_len(B), B); + assert_eq!(dec.do_decrypt_out(&ct[B..2 * B], &mut out).unwrap(), B); + // third block: releases the second + assert_eq!(dec.do_decrypt_out(&ct[2 * B..], &mut out[B..]).unwrap(), B); + let (last, n) = dec.do_decrypt_final().unwrap(); + assert_eq!(n, 0, "block-aligned plaintext => final block is all padding"); + assert_eq!(&out[..2 * B], &msg(2 * B)[..]); + let _ = last; +} + +#[test] +fn final_out_variants() { + let key = key(); + let (mut enc, iv) = Enc::do_encrypt_init(&key).unwrap(); + let mut ct = [0u8; 2 * B]; + let n = enc.do_encrypt_out(&msg(B + 2), &mut ct).unwrap(); + assert_eq!(n, B); + let mut last = [0u8; B]; + assert_eq!(enc.do_encrypt_final_out(&mut last).unwrap(), B); + ct[B..].copy_from_slice(&last); + + let mut dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + let mut out = [0u8; B]; + assert_eq!(dec.do_decrypt_out(&ct, &mut out).unwrap(), B); + let mut last_pt = [0u8; B]; + let data_len = dec.do_decrypt_final_out(&mut last_pt).unwrap(); + assert_eq!(data_len, 2); + let mut rec = out.to_vec(); + rec.extend_from_slice(&last_pt[..data_len]); + assert_eq!(rec, msg(B + 2)); +} + +#[test] +fn tampered_final_block_is_rejected() { + let key = key(); + for len in [0, 1, B - 1, B, B + 5] { + let mut ct = vec![0u8; Enc::encrypt_out_len(len)]; + let (iv, n) = Enc::encrypt_out(&key, &msg(len), &mut ct).unwrap(); + // flipping the low bit of the final byte corrupts the PKCS7 length byte + ct[n - 1] ^= 0x01; + let mut out = vec![0u8; n]; + match Dec::decrypt_out(&key, &iv, &ct, &mut out) { + Err(SymmetricCipherError::PaddingError(PaddingError::InvalidPadding)) => {} + other => panic!("len {len}: expected InvalidPadding, got {other:?}"), + } + } +} + +#[test] +fn malformed_ciphertext_lengths_are_rejected() { + let key = key(); + let iv = [0u8; B]; + let mut out = [0u8; 4 * B]; + + // empty + assert!(matches!( + Dec::decrypt_out(&key, &iv, &[], &mut out), + Err(SymmetricCipherError::DecryptionFailed) + )); + // not a multiple of the block length + assert!(matches!( + Dec::decrypt_out(&key, &iv, &[0u8; B + 1], &mut out), + Err(SymmetricCipherError::DecryptionFailed) + )); + // streaming: partial trailing block at final + let mut dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + dec.do_decrypt_out(&[0u8; B + 3], &mut out).unwrap(); + assert!(matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed))); + // streaming: nothing fed at all + let dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + assert!(matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed))); +} + +#[test] +fn output_buffer_too_small_reports_required_length() { + let key = key(); + let pt = msg(2 * B + 1); + + let mut small = [0u8; 2 * B]; + match Enc::encrypt_out(&key, &pt, &mut small) { + Err(SymmetricCipherError::OutputBufferTooSmall(need)) => assert_eq!(need, 3 * B), + other => panic!("{other:?}"), + } + + let (mut enc, iv) = Enc::do_encrypt_init(&key).unwrap(); + let mut tiny = [0u8; B - 1]; + match enc.do_encrypt_out(&pt, &mut tiny) { + Err(SymmetricCipherError::OutputBufferTooSmall(need)) => assert_eq!(need, 2 * B), + other => panic!("{other:?}"), + } + drop(enc); + + let ct = [0u8; 3 * B]; + let mut small = [0u8; 3 * B - 2]; + match Dec::decrypt_out(&key, &iv, &ct, &mut small) { + Err(SymmetricCipherError::OutputBufferTooSmall(need)) => { + assert_eq!(need, 3 * B - 1) + } + other => panic!("{other:?}"), + } +} + +#[test] +fn wrong_key_type_is_rejected_by_adapters() { + let mac_key = KeyMaterial::::from_bytes_as_type(&[1u8; B], KeyType::MACKey).unwrap(); + assert!(matches!( + Enc::do_encrypt_init(&mac_key), + Err(SymmetricCipherError::KeyMaterialError(_)) + )); + assert!(matches!( + Dec::do_decrypt_init(&mac_key, &[0u8; B]), + Err(SymmetricCipherError::KeyMaterialError(_)) + )); +} + +// ---- NoPadding through the adapters -------------------------------------------------------- + +/// With `NoPadding` the adapters enforce alignment: the framework is told that only multiples of +/// the block length are accepted, and it asserts that every other length is refused with a +/// `PaddingError`, at `encrypt_out` and at a streaming `do_encrypt_final`. +#[test] +fn no_padding_adapters_pass_the_symmetric_cipher_framework() { + let mut framework = TestFrameworkSymmetricCipher::new(); + framework.required_alignment = B; + framework.test_encryptor_decryptor::(); +} + +/// An aligned message passes through with its length unchanged -- no final block is added -- and the +/// ciphertext is exactly what the bare mode produces: NoPadding is a check, not a transformation. +#[test] +fn no_padding_adds_nothing_to_aligned_data() { + let key = key(); + for blocks in 0..=4usize { + let len = blocks * B; + let pt = msg(len); + assert_eq!(EncNP::encrypt_out_len(len), len); + assert_eq!(DecNP::decrypt_out_len(len), len); + + let mut ct = vec![0u8; len]; + let (iv, n) = EncNP::encrypt_out(&key, &pt, &mut ct).unwrap(); + assert_eq!(n, len, "{blocks} blocks: output length equals input length"); + + // Byte for byte the bare cipher's output under the same IV. + let mut bare = pt.clone(); + let (mut enc, _) = + ToyCbc::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(iv)).unwrap(); + let (blocks_mut, _) = bare.as_chunks_mut::(); + enc.do_encrypt_blocks_inplace(blocks_mut).unwrap(); + assert_eq!(ct, bare, "{blocks} blocks: the adapter must not alter the ciphertext"); + + let mut out = vec![0u8; len]; + let m = DecNP::decrypt_out(&key, &iv, &ct, &mut out).unwrap(); + assert_eq!(&out[..m], &pt[..], "{blocks} blocks: round trip"); + + // Streaming: do_encrypt_final reports zero output bytes. + let (mut enc, _) = EncNP::do_encrypt_init(&key).unwrap(); + let mut buf = vec![0u8; enc.do_encrypt_out_len(len)]; + assert_eq!(enc.do_encrypt_out(&pt, &mut buf).unwrap(), len); + let (_, last_len) = enc.do_encrypt_final().unwrap(); + assert_eq!(last_len, 0, "{blocks} blocks: no final block"); + } +} + +/// An unaligned message is refused with `PaddingNotPermitted`, from the one-shot and from a +/// streaming `do_encrypt_final`, and nothing is written for the final block. +#[test] +fn no_padding_refuses_unaligned_data() { + let key = key(); + for len in [1usize, B - 1, B + 1, 2 * B + 3, 3 * B - 1] { + let pt = msg(len); + let mut ct = vec![0u8; len + B]; + assert!( + matches!( + EncNP::encrypt_out(&key, &pt, &mut ct), + Err(SymmetricCipherError::PaddingError(PaddingError::PaddingNotPermitted)) + ), + "len {len}: one-shot must refuse an unaligned message" + ); + + let (mut enc, _) = EncNP::do_encrypt_init(&key).unwrap(); + let whole = len / B * B; + let mut buf = vec![0u8; whole]; + assert_eq!(enc.do_encrypt_out(&pt, &mut buf).unwrap(), whole, "whole blocks still stream"); + assert!( + matches!( + enc.do_encrypt_final(), + Err(SymmetricCipherError::PaddingError(PaddingError::PaddingNotPermitted)) + ), + "len {len}: do_encrypt_final must refuse the buffered partial block" + ); + } +} + +/// On the decrypt side, an empty ciphertext is the empty message (there is no padding block to +/// demand), and an unaligned ciphertext is still malformed. +#[test] +fn no_padding_decryptor_accepts_empty_and_rejects_unaligned() { + let key = key(); + let iv = [0x11u8; B]; + let mut out = [0u8; 0]; + assert_eq!(DecNP::decrypt_out(&key, &iv, &[], &mut out).unwrap(), 0); + let dec = DecNP::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec.do_decrypt_final().unwrap().1, 0); + + for len in [1usize, B - 1, B + 1, 2 * B + 5] { + let mut out = vec![0u8; len]; + assert!( + matches!( + DecNP::decrypt_out(&key, &iv, &msg(len), &mut out), + Err(SymmetricCipherError::DecryptionFailed) + ), + "len {len}: an unaligned ciphertext is malformed" + ); + } +} diff --git a/crypto/cipher/tests/padding/pkcs7_tests.rs b/crypto/cipher/tests/padding/pkcs7_tests.rs new file mode 100644 index 00000000..d90973d8 --- /dev/null +++ b/crypto/cipher/tests/padding/pkcs7_tests.rs @@ -0,0 +1,127 @@ +//! Tests for PKCS7 against the rule of RFC 5652 §6.3: +//! "the input shall be padded at the trailing end with k-(lth mod k) octets all having value +//! k-(lth mod k)". There are no official test vectors for this scheme; expected values below are +//! computed directly from that rule. + +use bouncycastle_cipher::padding::PKCS7; +use bouncycastle_core::errors::PaddingError; +use bouncycastle_core::traits::BlockCipherPadding; + +fn roundtrip_all_lengths() { + for data_len in 0..K { + let mut block = [0xA5u8; K]; + for (i, b) in block.iter_mut().enumerate().take(data_len) { + *b = i as u8; + } + let original = block; + + PKCS7::pad(&mut block, data_len).unwrap(); + + // data untouched + assert_eq!(&block[..data_len], &original[..data_len]); + // RFC 5652 §6.3: k - (lth mod k) octets, each of value k - (lth mod k) + let expected_pad = K - data_len; + assert_eq!(block[data_len..].len(), expected_pad); + assert!(block[data_len..].iter().all(|&b| b as usize == expected_pad)); + + assert_eq!(>::unpad(&block), Ok(data_len)); + } +} + +#[test] +fn roundtrip_16() { + roundtrip_all_lengths::<16>(); +} + +#[test] +fn roundtrip_8() { + roundtrip_all_lengths::<8>(); +} + +#[test] +fn roundtrip_boundary_block_lengths() { + roundtrip_all_lengths::<1>(); + roundtrip_all_lengths::<255>(); +} + +#[test] +fn rfc5652_worked_examples() { + // RFC 5652 §6.3 lists the padding strings: "01 -- if lth mod k = k-1", "02 02 -- if lth mod k = k-2", + // ..., "k k ... k k -- if lth mod k = 0". + const K: usize = 16; + let mut b = [0xFFu8; K]; + PKCS7::pad(&mut b, K - 1).unwrap(); + assert_eq!(b[K - 1], 0x01); + + let mut b = [0xFFu8; K]; + PKCS7::pad(&mut b, K - 2).unwrap(); + assert_eq!(&b[K - 2..], &[0x02, 0x02]); + + let mut b = [0xFFu8; K]; + PKCS7::pad(&mut b, 0).unwrap(); + assert_eq!(b, [K as u8; K]); +} + +#[test] +fn pad_rejects_full_block() { + let mut b = [0u8; 16]; + assert_eq!( + >::pad(&mut b, 16), + Err(PaddingError::DataLengthTooLong(15)) + ); + assert_eq!( + >::pad(&mut b, 17), + Err(PaddingError::DataLengthTooLong(15)) + ); + // block untouched on error + assert_eq!(b, [0u8; 16]); +} + +#[test] +fn unpad_rejects_malformed() { + const K: usize = 16; + + // last byte zero: no such padding string + let mut b = [0x00u8; K]; + assert_eq!(>::unpad(&b), Err(PaddingError::InvalidPadding)); + + // last byte greater than k + b[K - 1] = (K + 1) as u8; + assert_eq!(>::unpad(&b), Err(PaddingError::InvalidPadding)); + b[K - 1] = 0xFF; + assert_eq!(>::unpad(&b), Err(PaddingError::InvalidPadding)); + + // claims 4 bytes of padding but one of them is wrong, at every possible position + for bad in 0..4 { + let mut b = [0x11u8; K]; + b[K - 4..].copy_from_slice(&[0x04; 4]); + b[K - 4 + bad] ^= 0x01; + if bad == 3 { + // corrupting the length byte itself turns it into 0x05; the preceding bytes are 0x04, so + // still invalid + assert_eq!(b[K - 1], 0x05); + } + assert_eq!( + >::unpad(&b), + Err(PaddingError::InvalidPadding), + "bad position {bad}" + ); + } + + // a full padding block with a single wrong byte anywhere is invalid + for pos in 0..K { + let mut b = [K as u8; K]; + b[pos] ^= 0x80; + assert_eq!(>::unpad(&b), Err(PaddingError::InvalidPadding)); + } +} + +#[test] +fn unpad_ignores_data_bytes_that_happen_to_equal_pad_value() { + // data bytes equal to the pad value must not confuse the length recovery + const K: usize = 16; + let mut b = [0x03u8; K]; // 13 data bytes all 0x03, then 3 bytes of 0x03 padding + PKCS7::pad(&mut b, 13).unwrap(); + assert_eq!(b, [0x03u8; K]); + assert_eq!(>::unpad(&b), Ok(13)); +} diff --git a/crypto/cipher/tests/suspend_tests.rs b/crypto/cipher/tests/suspend_tests.rs new file mode 100644 index 00000000..c761adea --- /dev/null +++ b/crypto/cipher/tests/suspend_tests.rs @@ -0,0 +1,259 @@ +//! Suspend-and-resume round trips for every mode and adapter, over the test framework's toy +//! permutation. +//! +//! Each test does part of an operation, suspends a clone of the cipher, resumes it with the +//! re-supplied key, and then finishes both the original and the resumed cipher the same way. The +//! two must agree byte for byte, which is the whole contract: a resumed cipher is the suspended +//! one, continued. The shared framework suite runs once per type for the version-header rules. +//! The AES aliases get the same impls through these generic types, so this is where they are +//! pinned; `bouncycastle-aes` only checks that each alias reaches them. + +use bouncycastle_cipher::modes::hazmat::Ecb; +use bouncycastle_cipher::modes::{Cbc, Ccm, Cfb, Cfb8, Ctr, Gcm}; +use bouncycastle_cipher::padding::{PKCS7, PaddedBlockCipherDecryptor, PaddedBlockCipherEncryptor}; +use bouncycastle_cipher::{Decrypting, Encrypting}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, BlockCipherDecryptor, BlockCipherEncryptor, + StreamCipherDecryptor, StreamCipherEncryptor, SuspendableKeyed, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::ToyBlockCipher; +use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; + +type ToyEcb = Ecb; +type ToyCbc = Cbc; +type ToyCfb = Cfb; +type ToyCfb8 = Cfb8; +type ToyCtr = Ctr; +type ToyGcm = Gcm; +type ToyCcm = Ccm; +type ToyPaddedEnc = PaddedBlockCipherEncryptor, PKCS7, 16, 16, 16>; +type ToyPaddedDec = PaddedBlockCipherDecryptor, PKCS7, 16, 16, 16>; + +fn key() -> KeyMaterial<16> { + KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap() +} + +fn message(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(3)).collect() +} + +/// Runs the framework suite on `cipher`, then suspends a clone, resumes it, and finishes both +/// with `finish`. Whatever `finish` returns must be identical for the two. +fn round_trip(cipher: C, finish: impl Fn(C) -> Vec) -> Vec +where + C: SuspendableKeyed> + Clone, +{ + let key = key(); + TestFrameworkSuspendableKeyedState::new().test(&cipher, &key); + let resumed = C::from_suspended(cipher.clone().suspend(), &key).unwrap(); + let original_output = finish(cipher); + assert_eq!(original_output, finish(resumed), "the resumed cipher must continue identically"); + original_output +} + +#[test] +fn cbc_both_directions() { + let (mut enc, iv) = ToyCbc::::do_encrypt_init(&key()).unwrap(); + let mut first = [0x11u8; 16]; + enc.do_encrypt_inplace(&mut first).unwrap(); + let rest = round_trip::<{ ToyCbc::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let mut data = [0x22u8; 48]; + e.do_encrypt_inplace(&mut data).unwrap(); + data.to_vec() + }); + + let mut dec = ToyCbc::::do_decrypt_init(&key(), &iv).unwrap(); + dec.do_decrypt_inplace(&mut first).unwrap(); + assert_eq!(first, [0x11u8; 16]); + let plain = round_trip::<{ ToyCbc::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + let mut data: [u8; 48] = rest.as_slice().try_into().unwrap(); + d.do_decrypt_inplace(&mut data).unwrap(); + data.to_vec() + }); + assert_eq!(plain, vec![0x22u8; 48]); +} + +#[test] +fn ecb_both_directions() { + let (mut enc, _) = ToyEcb::::do_encrypt_init(&key()).unwrap(); + enc.do_encrypt_inplace(&mut [0x11u8; 16]).unwrap(); + round_trip::<{ ToyEcb::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let mut data = [0x22u8; 32]; + e.do_encrypt_inplace(&mut data).unwrap(); + data.to_vec() + }); + let dec = ToyEcb::::do_decrypt_init(&key(), &[]).unwrap(); + round_trip::<{ ToyEcb::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + let mut data = [0x33u8; 32]; + d.do_decrypt_inplace(&mut data).unwrap(); + data.to_vec() + }); +} + +#[test] +fn cfb_mid_segment_both_directions() { + let msg = message(40); + let (mut enc, iv) = ToyCfb::::do_encrypt_init(&key()).unwrap(); + let mut head = msg[..7].to_vec(); + enc.do_encrypt_inplace(&mut head).unwrap(); + let tail = round_trip::<{ ToyCfb::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let mut data = msg[7..].to_vec(); + e.do_encrypt_inplace(&mut data).unwrap(); + data + }); + + let mut dec = ToyCfb::::do_decrypt_init(&key(), &iv).unwrap(); + dec.do_decrypt_inplace(&mut head).unwrap(); + assert_eq!(head, msg[..7]); + let plain = round_trip::<{ ToyCfb::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + let mut data = tail.clone(); + d.do_decrypt_inplace(&mut data).unwrap(); + data + }); + assert_eq!(plain, msg[7..]); +} + +#[test] +fn cfb8_both_directions() { + let msg = message(20); + let (mut enc, iv) = ToyCfb8::::do_encrypt_init(&key()).unwrap(); + let mut head = msg[..5].to_vec(); + enc.do_encrypt_inplace(&mut head).unwrap(); + let tail = round_trip::<{ ToyCfb8::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let mut data = msg[5..].to_vec(); + e.do_encrypt_inplace(&mut data).unwrap(); + data + }); + let mut dec = ToyCfb8::::do_decrypt_init(&key(), &iv).unwrap(); + dec.do_decrypt_inplace(&mut head).unwrap(); + let plain = round_trip::<{ ToyCfb8::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + let mut data = tail.clone(); + d.do_decrypt_inplace(&mut data).unwrap(); + data + }); + assert_eq!(plain, msg[5..]); +} + +#[test] +fn ctr_mid_block_both_directions() { + let msg = message(50); + let (mut enc, nonce) = ToyCtr::::do_encrypt_init(&key()).unwrap(); + let mut head = msg[..7].to_vec(); + enc.do_encrypt_inplace(&mut head).unwrap(); + let tail = round_trip::<{ ToyCtr::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let mut data = msg[7..].to_vec(); + e.do_encrypt_inplace(&mut data).unwrap(); + data + }); + let mut dec = ToyCtr::::do_decrypt_init(&key(), &nonce).unwrap(); + dec.do_decrypt_inplace(&mut head).unwrap(); + let plain = round_trip::<{ ToyCtr::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + let mut data = tail.clone(); + d.do_decrypt_inplace(&mut data).unwrap(); + data + }); + assert_eq!(plain, msg[7..]); +} + +#[test] +fn gcm_both_directions_with_aad() { + let msg = message(45); + let aad = b"authenticated header"; + + let (mut enc, nonce) = ToyGcm::::do_encrypt_init(&key()).unwrap(); + enc.do_update_aad(aad).unwrap(); + let mut head = [0u8; 5]; + enc.do_encrypt_out(&msg[..5], &mut head).unwrap(); + // Ciphertext of the rest, then the tag. + let tail = round_trip::<{ ToyGcm::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let mut out = vec![0u8; 40]; + e.do_encrypt_out(&msg[5..], &mut out).unwrap(); + let (tag, tag_len) = e.do_encrypt_final().unwrap(); + out.extend_from_slice(&tag[..tag_len]); + out + }); + let mut ciphertext = head.to_vec(); + ciphertext.extend_from_slice(&tail); + + // The decryptor holds the last 16 bytes back, so after 10 bytes nothing has been released, + // but data has started and the AAD phase is closed: a state worth suspending. + let mut dec = ToyGcm::::do_decrypt_init(&key(), &nonce).unwrap(); + dec.do_update_aad(aad).unwrap(); + let mut nothing = [0u8; 0]; + assert_eq!(dec.do_decrypt_out(&ciphertext[..10], &mut nothing).unwrap(), 0); + let plain = round_trip::<{ ToyGcm::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + let mut out = vec![0u8; ciphertext.len()]; + let n = d.do_decrypt_out(&ciphertext[10..], &mut out).unwrap(); + let (_, last) = d.do_decrypt_final().expect("the tag must verify after a resume"); + out.truncate(n + last); + out + }); + assert_eq!(plain, msg); +} + +#[test] +fn ccm_both_directions() { + let msg = message(37); + let nonce = [0x24u8; 12]; + let aad = b"header"; + + let mut enc = ToyCcm::::new(&key(), &nonce, aad, msg.len()).unwrap(); + let mut head = msg[..9].to_vec(); + enc.do_encrypt(&mut head).unwrap(); + let tail = round_trip::<{ ToyCcm::::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let mut data = msg[9..].to_vec(); + e.do_encrypt(&mut data).unwrap(); + data.extend_from_slice(&e.do_encrypt_final().unwrap()); + data + }); + let (ct_tail, tag) = tail.split_at(msg.len() - 9); + let tag: [u8; 16] = tag.try_into().unwrap(); + + let mut dec = ToyCcm::::new(&key(), &nonce, aad, msg.len()).unwrap(); + dec.do_decrypt_update(&mut head).unwrap(); + let plain = round_trip::<{ ToyCcm::::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + let mut data = ct_tail.to_vec(); + d.do_decrypt_update(&mut data).unwrap(); + d.do_decrypt_final(&tag).expect("the tag must verify after a resume"); + data + }); + assert_eq!(plain, msg[9..]); +} + +#[test] +fn padded_cbc_both_directions() { + let msg = message(45); + let (mut enc, iv) = ToyPaddedEnc::do_encrypt_init(&key()).unwrap(); + let mut head = [0u8; 16]; + // 20 bytes in: one block out, four buffered. + assert_eq!(enc.do_encrypt_out(&msg[..20], &mut head).unwrap(), 16); + let tail = round_trip::<{ ToyPaddedEnc::SUSPENDED_STATE_LEN }, _>(enc, |mut e| { + let mut out = vec![0u8; 32]; + let n = e.do_encrypt_out(&msg[20..], &mut out).unwrap(); + out.truncate(n); + let (last, last_len) = e.do_encrypt_final().unwrap(); + out.extend_from_slice(&last[..last_len]); + out + }); + let mut ciphertext = head.to_vec(); + ciphertext.extend_from_slice(&tail); + assert_eq!(ciphertext.len(), 48); + + // 20 bytes in: one block released, one held back, four buffered. + let mut dec = ToyPaddedDec::do_decrypt_init(&key(), &iv).unwrap(); + let mut first = [0u8; 16]; + assert_eq!(dec.do_decrypt_out(&ciphertext[..36], &mut first).unwrap(), 16); + let plain = round_trip::<{ ToyPaddedDec::SUSPENDED_STATE_LEN }, _>(dec, |mut d| { + let mut out = vec![0u8; 32]; + let n = d.do_decrypt_out(&ciphertext[36..], &mut out).unwrap(); + out.truncate(n); + let (last, data_len) = d.do_decrypt_final().unwrap(); + out.extend_from_slice(&last[..data_len]); + out + }); + let mut recovered = first.to_vec(); + recovered.extend_from_slice(&plain); + assert_eq!(recovered, msg); +} diff --git a/crypto/core-test-framework/Cargo.toml b/crypto/core-test-framework/Cargo.toml index 69447b69..d64acfd3 100644 --- a/crypto/core-test-framework/Cargo.toml +++ b/crypto/core-test-framework/Cargo.toml @@ -5,5 +5,8 @@ edition.workspace = true [dependencies] bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true +bouncycastle-hex.workspace = true +serde_json = "1.0" # for parsing the bc-test-data and Wycheproof vector files [dev-dependencies] diff --git a/crypto/core-test-framework/src/aead.rs b/crypto/core-test-framework/src/aead.rs new file mode 100644 index 00000000..4d93d9ab --- /dev/null +++ b/crypto/core-test-framework/src/aead.rs @@ -0,0 +1,995 @@ +//! Shared conformance tests for [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] implementors. +//! +//! Two runners: +//! +//! * [`TestFrameworkAEADCipher`] checks the whole AEAD contract -- it runs the +//! [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] suite first, then the AAD and tag +//! behaviour on top. +//! * [`TestFrameworkAEADTaggedLayout`] concentrates on the byte-boundary edges of the inline +//! `ciphertext || tag` layout -- where the tag lands, what a decryptor holds back, what happens +//! when the input is shorter than the tag -- at every length across a few multiples of `TAG_LEN` +//! and under every chunking, which is where an implementor's own bookkeeping goes wrong. Every +//! encryption there is driven through a [`FixedSeedRNG`] so that the nonce is the same on every +//! path and streaming output can be compared byte for byte with one-shot output. +//! +//! [`SymmetricCipherEncryptor`]: bouncycastle_core::traits::SymmetricCipherEncryptor +//! [`SymmetricCipherDecryptor`]: bouncycastle_core::traits::SymmetricCipherDecryptor + +use crate::symmetric_ciphers::TestFrameworkSymmetricCipher; +use crate::{DUMMY_SEED, FixedSeedRNG}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{AEADCipherDecryptor, AEADCipherEncryptor}; + +/// Instance of the test framework. +pub struct TestFrameworkAEADCipher { + /// The one message length the pair's streaming methods accept, if they accept only one; see + /// [`TestFrameworkSymmetricCipher::fixed_message_len`], which this is passed on to. `None` + /// (the default) means any length. The streaming checks here then run at that length only, + /// and the one-shots at every length up to and including it. + /// + /// [`TestFrameworkSymmetricCipher::fixed_message_len`]: crate::symmetric_ciphers::TestFrameworkSymmetricCipher::fixed_message_len + pub fixed_message_len: Option, +} + +impl TestFrameworkAEADCipher { + /// + pub fn new() -> Self { + Self { fixed_message_len: None } + } + + /// 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; + /// * every AEAD method that writes into a caller's buffer accepts one larger than needed, + /// returns the number of bytes that call wrote, and zeroes every byte past that count; + /// * a key of the wrong [`KeyType`] is rejected, and the security-strength policy matches + /// [`Algorithm::MAX_SECURITY_STRENGTH`]. + /// + /// `bouncycastle-core`'s own `tests/aead_buffering_toy_tests.rs` separately pins that a cipher + /// which holds back more than the tag is handled correctly by the traits' default one-shots, + /// since `E`/`D` here are supplied by the caller and might not hold anything back. + /// + /// [`Algorithm::MAX_SECURITY_STRENGTH`]: bouncycastle_core::traits::Algorithm::MAX_SECURITY_STRENGTH + /// + /// [`TestFrameworkBlockCipher::test`]: crate::block_cipher::TestFrameworkBlockCipher::test + /// [`TestFrameworkSymmetricCipher::test_encryptor_decryptor`]: crate::symmetric_ciphers::TestFrameworkSymmetricCipher::test_encryptor_decryptor + /// [`SymmetricCipherEncryptor`]: bouncycastle_core::traits::SymmetricCipherEncryptor + /// [`SymmetricCipherDecryptor`]: bouncycastle_core::traits::SymmetricCipherDecryptor + pub fn test_encryptor_decryptor< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, + E: AEADCipherEncryptor, + D: AEADCipherDecryptor, + >( + &self, + ) { + 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.fixed_message_len = self.fixed_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 round trip, every length up to a few times the tag length (and up to the fixed + // length, if there is one, so that the streaming checks inside the loop reach it) + let max_len = (3 * TAG_LEN.max(1) + 5).max(self.fixed_message_len.unwrap_or(0)); + assert!(max_len <= DUMMY_SEED.len(), "the fixed message length must fit the seed buffer"); + for len in 0..=max_len { + let msg = &DUMMY_SEED[..len]; + // a fixed-length pair takes only that length, on every entry point: the one-shots + // are the trait's own, provided over the streaming methods that enforce it + if let Some(fixed) = self.fixed_message_len + && fixed != len + { + let mut ct = vec![0u8; E::encrypt_detached_out_len(len)]; + assert!( + E::encrypt_detached_out(&key, aad, msg, &mut ct).is_err(), + "fixed length: a {len}-byte detached one-shot must be refused" + ); + let mut inline = vec![0u8; E::encrypt_out_len(len)]; + assert!( + E::encrypt_with_aad_out(&key, aad, msg, &mut inline).is_err(), + "fixed length: a {len}-byte inline one-shot must be refused" + ); + // ...and so is a ciphertext of any length but the frame's + let fixed_msg = &DUMMY_SEED[..fixed]; + let mut sealed = vec![0u8; E::encrypt_out_len(fixed)]; + let (nonce, n) = + E::encrypt_with_aad_out(&key, aad, fixed_msg, &mut sealed).unwrap(); + let mut wrong = sealed[..n].to_vec(); + wrong.resize(len + TAG_LEN, 0); + let mut pt = vec![0u8; D::decrypt_out_len(wrong.len())]; + assert!( + D::decrypt_with_aad_out(&key, &nonce, aad, &wrong, &mut pt).is_err(), + "fixed length: a {}-byte ciphertext must be refused", + wrong.len() + ); + continue; + } + let mut ct = vec![0u8; E::encrypt_detached_out_len(len)]; + let (nonce, ct_len, tag) = E::encrypt_detached_out(&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_detached_out_len(ct.len())]; + let pt_len = D::decrypt_detached_out(&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_detached_out"); + + // the inline `ciphertext || tag` layout with AAD: `encrypt_with_aad_out` 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_detached_out_len(len)]; + let (pinned_nonce, detached_len, detached_tag) = E::encrypt_detached_rng_out( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut detached, + ) + .unwrap(); + detached.truncate(detached_len); + detached.extend_from_slice(&detached_tag); + + let mut inline = vec![0u8; E::encrypt_out_len(len)]; + let (inline_nonce, inline_len) = + E::encrypt_with_aad_out(&key, aad, msg, &mut inline).unwrap(); + assert_eq!( + inline_len, + E::encrypt_detached_out_len(len) + TAG_LEN, + "encrypt_with_aad_out must write the ciphertext plus the tag, len {len}" + ); + let mut pt4 = vec![0u8; D::decrypt_out_len(inline_len)]; + let pt4_len = + D::decrypt_with_aad_out(&key, &inline_nonce, aad, &inline[..inline_len], &mut pt4) + .unwrap(); + assert_eq!(&pt4[..pt4_len], msg, "tagged one-shot round trip, len {len}"); + + // ...and so must the RNG-driven and allocating inline-with-AAD one-shots. The roomy + // buffer is deliberate: see the `encrypt_detached_rng_out` probe below. + let mut inline_rng = vec![0u8; E::encrypt_out_len(len) + 3]; + let (rng_nonce, rng_len) = E::encrypt_with_aad_rng_out( + &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_with_aad_rng_out 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_with_aad_rng_out( + &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_with_aad_rng_out( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut short, + ) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, E::encrypt_out_len(len)) + } + other => panic!("encrypt_with_aad_rng_out 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.do_encrypt_out_len(len)]; + let written5 = enc5.do_encrypt_out(msg, &mut inline5).unwrap(); + inline5.truncate(written5); + let (last5, last5_len) = enc5.do_encrypt_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.do_decrypt_out_len(inline5.len())]; + let got5 = dec5.do_decrypt_out(&inline5, &mut pt5).unwrap(); + pt5.truncate(got5); + let (last, data_len) = dec5.do_decrypt_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.do_decrypt_out_len(short.len())]; + dec6.do_decrypt_out(short, &mut scratch).unwrap(); + assert!( + matches!(dec6.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed)), + "a stream shorter than the tag must be DecryptionFailed, len {len}" + ); + } + + // too-short output buffers on the one-shots are refused with the required length, + // before any work is done + let need = E::encrypt_detached_out_len(len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match E::encrypt_detached_out(&key, aad, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, need) + } + other => panic!("encrypt_detached_out into a short buffer: {other:?}"), + } + let mut short = vec![0u8; need - 1]; + match E::encrypt_detached_rng_out( + &key, + &mut FixedSeedRNG::::new([0xA5u8; NONCE_LEN]), + aad, + msg, + &mut short, + ) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, need) + } + other => panic!("encrypt_detached_rng_out 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_detached_out(&key, aad, msg, &mut roomy).unwrap(); + assert_eq!(n, need, "encrypt_detached_out into a roomy buffer"); + let mut roomy = vec![0u8; need + 3]; + let (_, n, _) = E::encrypt_detached_rng_out( + &key, + &mut FixedSeedRNG::::new([0xA5u8; NONCE_LEN]), + aad, + msg, + &mut roomy, + ) + .unwrap(); + assert_eq!( + n, need, + "encrypt_detached_rng_out must write exactly encrypt_detached_out_len bytes" + ); + } + let need = E::encrypt_out_len(len); + let mut short = vec![0u8; need - 1]; + match E::encrypt_with_aad_out(&key, aad, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("encrypt_with_aad_out into a short buffer: {other:?}"), + } + let need = D::decrypt_detached_out_len(ct.len()); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match D::decrypt_detached_out(&key, &nonce, aad, &ct, &tag, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, need) + } + other => panic!("decrypt_detached_out into a short buffer: {other:?}"), + } + } + let need = D::decrypt_out_len(inline_len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match D::decrypt_with_aad_out(&key, &inline_nonce, aad, &inline, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => { + assert_eq!(n, need) + } + other => panic!("decrypt_with_aad_out into a short buffer: {other:?}"), + } + } + } + + // 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[..self.fixed_message_len.unwrap_or(max_len.max(17))]; + let mut ct_ref = vec![0u8; E::encrypt_detached_out_len(msg.len())]; + let (nonce_ref, ct_ref_len, tag_ref) = E::encrypt_detached_rng_out( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut ct_ref, + ) + .unwrap(); + ct_ref.truncate(ct_ref_len); + + for chunk in [1usize, 2, 3, 7, TAG_LEN.max(1), TAG_LEN + 1, msg.len().max(1)] { + 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.do_encrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_encrypt_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_encrypt_final_detachedtag_out(&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.do_decrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_decrypt_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_decrypt_final_detachedtag_out(&tag, &mut final_buf).unwrap(); + pt.extend_from_slice(&final_buf[..final_len]); + assert_eq!(pt, msg, "chunk {chunk}: streaming round trip"); + } + + // 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.do_encrypt_out_len(msg.len())]; + let n = enc.do_encrypt_out(msg, &mut ct).unwrap(); + ct.truncate(n); + let (last, last_len, tag) = enc.do_encrypt_final_detachedtag().unwrap(); + ct.extend_from_slice(&last[..last_len]); + assert_eq!(ct, ct_ref, "do_encrypt_final_detachedtag must give the one-shot ciphertext"); + assert_eq!(tag, tag_ref, "do_encrypt_final_detachedtag 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.do_decrypt_out_len(ct.len())]; + let n = dec.do_decrypt_out(&ct, &mut pt).unwrap(); + pt.truncate(n); + let (last, data_len) = dec.do_decrypt_final_detachedtag(&tag).unwrap(); + pt.extend_from_slice(&last[..data_len]); + assert_eq!(pt, msg, "do_decrypt_final_detachedtag 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.do_decrypt_out_len(ct.len())]; + dec.do_decrypt_out(&ct, &mut pt).unwrap(); + assert!( + matches!( + dec.do_decrypt_final_detachedtag(&wrong_tag), + Err(SymmetricCipherError::AEADTagCheckFailed) + ), + "do_decrypt_final_detachedtag 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_detached_out_len(msg.len())]; + let (nonce_empty, len_empty, tag_empty) = E::encrypt_detached_rng_out( + &key, + &mut FixedSeedRNG::::new(pinned), + b"", + msg, + &mut with_empty, + ) + .unwrap(); + with_empty.truncate(len_empty); + let mut without = vec![0u8; E::encrypt_detached_out_len(msg.len())]; + let (nonce_none, len_none, tag_none) = E::encrypt_detached_rng_out( + &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_rng_out(&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 (unless the pair's fixed + // length rules an empty message out) + if self.fixed_message_len.is_none_or(|fixed| fixed == 0) { + let (nonce, _ct_len, tag) = E::encrypt_detached_out(&key, aad, &[], &mut []).unwrap(); + D::decrypt_detached_out(&key, &nonce, aad, &[], &tag, &mut []).unwrap(); + match D::decrypt_detached_out( + &key, + &nonce, + b"different associated data", + &[], + &tag, + &mut [], + ) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("an empty message must still authenticate its AAD, got {other:?}"), + }; + } + + // 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. (Not for a message + // with no data at all, where there is no data call to end it.) + if !msg.is_empty() { + let (mut enc, nonce) = E::do_encrypt_init(&key).unwrap(); + let mut ct = vec![0u8; enc.do_encrypt_out_len(msg.len())]; + enc.do_encrypt_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_encrypt_final_detachedtag_out(&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.do_decrypt_out_len(1)]; + let mut got = dec.do_decrypt_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.do_decrypt_out_len(ct.len() - 1)]; + got = dec.do_decrypt_out(&ct[1..], &mut rest).unwrap(); + pt.extend_from_slice(&rest[..got]); + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_decrypt_final_detachedtag_out(&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. A message long enough to have a byte 3 to flip, unless + // the pair's fixed length says otherwise. + let msg = &DUMMY_SEED[..self.fixed_message_len.unwrap_or(max_len.max(17))]; + let mut ct = vec![0u8; E::encrypt_detached_out_len(msg.len())]; + let (nonce, ct_len, tag) = E::encrypt_detached_out(&key, aad, msg, &mut ct).unwrap(); + ct.truncate(ct_len); + + if ct.len() > 3 { + let mut tampered = ct.clone(); + tampered[3] ^= 0xFF; + let mut buf = vec![0u8; D::decrypt_detached_out_len(tampered.len())]; + match D::decrypt_detached_out(&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_len(tampered_inline.len())]; + let result = if with_aad { + D::decrypt_with_aad_out(&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:?}"), + }; + + let mut wrong_tag = tag; + wrong_tag[0] ^= 0xFF; + let mut buf = vec![0u8; D::decrypt_detached_out_len(ct.len())]; + match D::decrypt_detached_out(&key, &nonce, aad, &ct, &wrong_tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified tag must fail the tag check, got {other:?}"), + }; + + let mut buf = vec![0u8; D::decrypt_detached_out_len(ct.len())]; + match D::decrypt_detached_out( + &key, + &nonce, + b"not the right associated data", + &ct, + &tag, + &mut buf, + ) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified AAD must fail the tag check, got {other:?}"), + }; + + if NONCE_LEN > 0 { + let mut wrong_nonce = nonce; + wrong_nonce[0] ^= 0xFF; + let mut buf = vec![0u8; D::decrypt_detached_out_len(ct.len())]; + match D::decrypt_detached_out(&key, &wrong_nonce, aad, &ct, &tag, &mut buf) { + Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } + other => panic!("a modified nonce must fail the tag check, got {other:?}"), + }; + + // 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); + } + + // Output-buffer contract for the AEAD `_out` methods, as the symmetric suite above checks it + // for the inherited ones: a buffer larger than needed is accepted, the returned count is + // what that call wrote, and every byte past it is zeroed, whatever the buffer held on the + // way in. Each buffer is pre-filled with a non-zero sentinel, so a byte left as the caller + // had it shows up. The detached finals' buffers are `[u8; FINAL_LEN]` by type and so + // cannot be oversized; for them only the zeroed tail is checked. + const SENTINEL: u8 = 0xA5; + const EXTRA: usize = 7; + let assert_tail_zeroed = |buf: &[u8], n: usize, what: &str| { + assert!( + n <= buf.len(), + "{what}: claims {n} bytes written to a {}-byte buffer", + buf.len() + ); + assert!( + buf[n..].iter().all(|&b| b == 0), + "{what}: the bytes past the {n} written must be zeroed" + ); + }; + let len = self.fixed_message_len.unwrap_or(max_len); + let msg = &DUMMY_SEED[..len]; + + // the detached one-shots + let need = E::encrypt_detached_out_len(len); + let mut ct = vec![SENTINEL; need + EXTRA]; + let (nonce, n, tag) = E::encrypt_detached_out(&key, aad, msg, &mut ct).unwrap(); + assert_eq!(n, need, "encrypt_detached_out into an oversized buffer"); + assert_tail_zeroed(&ct, n, "encrypt_detached_out"); + ct.truncate(n); + let mut buf = vec![SENTINEL; need + EXTRA]; + let (_, n, _) = E::encrypt_detached_rng_out( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut buf, + ) + .unwrap(); + assert_eq!(n, need, "encrypt_detached_rng_out into an oversized buffer"); + assert_tail_zeroed(&buf, n, "encrypt_detached_rng_out"); + let mut pt = vec![SENTINEL; D::decrypt_detached_out_len(ct.len()) + EXTRA]; + let n = D::decrypt_detached_out(&key, &nonce, aad, &ct, &tag, &mut pt).unwrap(); + assert_eq!(&pt[..n], msg, "decrypt_detached_out into an oversized buffer"); + assert_tail_zeroed(&pt, n, "decrypt_detached_out"); + + // the inline one-shots with associated data + let need = E::encrypt_out_len(len); + let mut sealed = vec![SENTINEL; need + EXTRA]; + let (nonce, n) = E::encrypt_with_aad_out(&key, aad, msg, &mut sealed).unwrap(); + assert_eq!(n, need, "encrypt_with_aad_out into an oversized buffer"); + assert_tail_zeroed(&sealed, n, "encrypt_with_aad_out"); + sealed.truncate(n); + let mut buf = vec![SENTINEL; need + EXTRA]; + let (_, n) = E::encrypt_with_aad_rng_out( + &key, + &mut FixedSeedRNG::::new(pinned), + aad, + msg, + &mut buf, + ) + .unwrap(); + assert_eq!(n, need, "encrypt_with_aad_rng_out into an oversized buffer"); + assert_tail_zeroed(&buf, n, "encrypt_with_aad_rng_out"); + let mut pt = vec![SENTINEL; D::decrypt_out_len(sealed.len()) + EXTRA]; + let n = D::decrypt_with_aad_out(&key, &nonce, aad, &sealed, &mut pt).unwrap(); + assert_eq!(&pt[..n], msg, "decrypt_with_aad_out into an oversized buffer"); + assert_tail_zeroed(&pt, n, "decrypt_with_aad_out"); + + // the detached finals: each reports only what it wrote itself, so with what the update + // released it adds up to exactly the message + let (mut enc, nonce) = E::do_encrypt_init(&key).unwrap(); + enc.do_update_aad(aad).unwrap(); + let mut ct = vec![0u8; enc.do_encrypt_out_len(len)]; + let written = enc.do_encrypt_out(msg, &mut ct).unwrap(); + ct.truncate(written); + let mut last = [SENTINEL; FINAL_LEN]; + let (last_len, tag) = enc.do_encrypt_final_detachedtag_out(&mut last).unwrap(); + assert_tail_zeroed(&last, last_len, "do_encrypt_final_detachedtag_out"); + ct.extend_from_slice(&last[..last_len]); + assert_eq!( + ct.len(), + E::encrypt_detached_out_len(len), + "do_encrypt_out and do_encrypt_final_detachedtag_out must report only their own bytes" + ); + let mut dec = D::do_decrypt_init(&key, &nonce).unwrap(); + dec.do_update_aad(aad).unwrap(); + let mut rec = vec![0u8; dec.do_decrypt_out_len(ct.len())]; + let released = dec.do_decrypt_out(&ct, &mut rec).unwrap(); + rec.truncate(released); + let mut last = [SENTINEL; FINAL_LEN]; + let data_len = dec.do_decrypt_final_detachedtag_out(&tag, &mut last).unwrap(); + assert_tail_zeroed(&last, data_len, "do_decrypt_final_detachedtag_out"); + rec.extend_from_slice(&last[..data_len]); + assert_eq!( + rec, msg, + "do_decrypt_out and do_decrypt_final_detachedtag_out must report only their own bytes" + ); + + // The key-type and security-strength checks on `do_encrypt_init` / `do_decrypt_init` are + // covered by the `TestFrameworkSymmetricCipher` suite run above. + } +} + +/// Instance of the test framework. +pub struct TestFrameworkAEADTaggedLayout { + // Put any config options here +} + +impl Default for TestFrameworkAEADTaggedLayout { + fn default() -> Self { + Self::new() + } +} + +/// The associated data every case is run under. +const AAD: &[u8] = b"aad"; + +impl TestFrameworkAEADTaggedLayout { + /// + pub fn new() -> Self { + Self {} + } + + /// Exercises the inline-layout contract for one encryptor/decryptor pair. + /// + /// Checks, in order: + /// * at every message length from 0 to `4 * TAG_LEN + 5`: the one-shot pair round-trips, the + /// detached one-shot is the same ciphertext with the tag split off, and for every chunking + /// the streaming pair agrees with the one-shot byte for byte -- with the decryptor, not the + /// caller, holding back the possible tag, in both the inline and the detached finalization; + /// * a tampered inline stream fails at finalization on both entry points and the one-shots + /// zeroize their buffer, and an input shorter than the tag is `DecryptionFailed` rather + /// than a panic on the short slice; + /// * every inline one-shot refuses an output buffer one byte short, naming the length it + /// needs, and accepts one of exactly that length. + pub fn test< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, + E: AEADCipherEncryptor, + D: AEADCipherDecryptor, + >( + &self, + ) { + let key = KeyMaterial::::from_bytes_as_type( + &DUMMY_SEED[..KEY_LEN], + KeyType::SymmetricCipherKey, + ) + .unwrap(); + Self::round_trip_at_every_length_and_chunking::( + &key, + ); + Self::tampering_and_short_input_are_rejected::( + &key, + ); + Self::undersized_buffers_are_rejected::(&key); + } + + /// The pinned RNG every encryption draws its nonce from, so that all paths use one nonce. + fn rng() -> FixedSeedRNG { + FixedSeedRNG::::new(core::array::from_fn(|i| 0xA5 ^ (i as u8))) + } + + /// Encrypts `msg` into the inline layout with the one-shot, under the pinned nonce, and + /// returns it with that nonce. + fn tagged_ct< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, + E: AEADCipherEncryptor, + >( + key: &KeyMaterial, + msg: &[u8], + ) -> (Vec, [u8; NONCE_LEN]) { + let mut ct = vec![0u8; E::encrypt_out_len(msg.len())]; + let (nonce, written) = + E::encrypt_with_aad_rng_out(key, &mut Self::rng::(), AAD, msg, &mut ct) + .unwrap(); + assert_eq!(written, msg.len() + TAG_LEN, "inline layout is ciphertext || tag"); + ct.truncate(written); + (ct, nonce) + } + + fn round_trip_at_every_length_and_chunking< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, + E: AEADCipherEncryptor, + D: AEADCipherDecryptor, + >( + key: &KeyMaterial, + ) { + for len in 0..=(4 * TAG_LEN + 5) { + let msg = &DUMMY_SEED[..len]; + let (ct, nonce) = + Self::tagged_ct::(key, msg); + + let mut pt = vec![0u8; D::decrypt_out_len(ct.len())]; + let n = D::decrypt_with_aad_out(key, &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; E::encrypt_detached_out_len(len)]; + let (d_nonce, d_len, d_tag) = E::encrypt_detached_rng_out( + key, + &mut Self::rng::(), + AAD, + msg, + &mut detached, + ) + .unwrap(); + assert_eq!(d_nonce, nonce, "len {len}: the pinned RNG must give the same nonce"); + 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) = + E::do_encrypt_init_rng(key, &mut Self::rng::()).unwrap(); + assert_eq!(stream_nonce, nonce, "len {len}: streaming init draws the same nonce"); + enc.do_update_aad(AAD).unwrap(); + let mut stream_ct = vec![0u8; len + FINAL_LEN]; + let mut written = 0; + for piece in msg.chunks(chunk) { + written += enc.do_encrypt_out(piece, &mut stream_ct[written..]).unwrap(); + } + let mut last = [0u8; FINAL_LEN]; + let last_len = enc.do_encrypt_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, so + // nothing past the plaintext is ever released, and the final call releases + // whatever plaintext it was still holding and nothing more. + let mut dec = D::do_decrypt_init(key, &stream_nonce).unwrap(); + dec.do_update_aad(AAD).unwrap(); + let mut out = vec![0u8; stream_ct.len() + FINAL_LEN]; + let mut written = 0; + for piece in stream_ct.chunks(chunk) { + written += dec.do_decrypt_out(piece, &mut out[written..]).unwrap(); + } + assert!(written <= len, "len {len}, chunk {chunk}: the tag must be held back"); + let (last, data_len) = dec.do_decrypt_final().unwrap(); + assert_eq!( + written + data_len, + len, + "len {len}, chunk {chunk}: the final call releases exactly the rest" + ); + 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 = D::do_decrypt_init(key, &stream_nonce).unwrap(); + dec.do_update_aad(AAD).unwrap(); + let mut out = vec![0u8; len + FINAL_LEN]; + let mut written = 0; + for piece in stream_ct[..len].chunks(chunk) { + written += dec.do_decrypt_out(piece, &mut out[written..]).unwrap(); + } + let mut last = [0u8; FINAL_LEN]; + let last_len = dec.do_decrypt_final_detachedtag_out(&d_tag, &mut last).unwrap(); + assert_eq!( + written + last_len, + len, + "len {len}, chunk {chunk}: detached final flushes the rest" + ); + out[written..written + last_len].copy_from_slice(&last[..last_len]); + out.truncate(written + last_len); + assert_eq!(out, msg, "len {len}, chunk {chunk}: detached streaming round trip"); + } + } + } + + fn tampering_and_short_input_are_rejected< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, + E: AEADCipherEncryptor, + D: AEADCipherDecryptor, + >( + key: &KeyMaterial, + ) { + let msg = &DUMMY_SEED[..10]; + let (ct, nonce) = Self::tagged_ct::(key, msg); + + let mut tampered = ct.clone(); + tampered[0] ^= 0xFF; + let mut pt = vec![0u8; tampered.len()]; + assert!(matches!( + D::decrypt_with_aad_out(key, &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 = D::do_decrypt_init(key, &nonce).unwrap(); + dec.do_update_aad(AAD).unwrap(); + dec.do_decrypt_out(&tampered, &mut pt).unwrap(); + assert!(matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::AEADTagCheckFailed))); + + // A wrong detached tag fails, and `decrypt_detached_out` 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!( + D::decrypt_detached_out(key, &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 check"); + + for short_len in 0..TAG_LEN { + let mut pt = vec![0u8; TAG_LEN]; + assert!( + matches!( + D::decrypt_with_aad_out(key, &nonce, AAD, &ct[..short_len], &mut pt), + Err(SymmetricCipherError::DecryptionFailed) + ), + "{short_len} bytes cannot carry a {TAG_LEN}-byte tag (one-shot)" + ); + let mut dec = D::do_decrypt_init(key, &nonce).unwrap(); + assert_eq!(dec.do_decrypt_out(&ct[..short_len], &mut pt).unwrap(), 0); + assert!( + matches!(dec.do_decrypt_final(), Err(SymmetricCipherError::DecryptionFailed)), + "{short_len} bytes cannot carry a {TAG_LEN}-byte tag (streaming)" + ); + } + } + + fn undersized_buffers_are_rejected< + const KEY_LEN: usize, + const NONCE_LEN: usize, + const TAG_LEN: usize, + const FINAL_LEN: usize, + E: AEADCipherEncryptor, + D: AEADCipherDecryptor, + >( + key: &KeyMaterial, + ) { + let msg = &DUMMY_SEED[..8]; + let (ct, nonce) = Self::tagged_ct::(key, msg); + + let needed = E::encrypt_out_len(msg.len()); + assert_eq!(needed, msg.len() + TAG_LEN); + let mut short = vec![0u8; needed - 1]; + match E::encrypt_with_aad_rng_out(key, &mut Self::rng::(), AAD, msg, &mut short) + { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, needed), + other => panic!("encrypt_with_aad_out into a short buffer: {other:?}"), + } + + let needed = D::decrypt_out_len(ct.len()); + assert_eq!(needed, msg.len()); + let mut short = vec![0u8; needed - 1]; + match D::decrypt_with_aad_out(key, &nonce, AAD, &ct, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, needed), + other => panic!("decrypt_with_aad_out 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 = D::decrypt_with_aad_out(key, &nonce, AAD, &ct, &mut exact).unwrap(); + assert_eq!(&exact[..n], msg, "a buffer of exactly `needed` bytes must be enough"); + } +} diff --git a/crypto/core-test-framework/src/block_cipher.rs b/crypto/core-test-framework/src/block_cipher.rs new file mode 100644 index 00000000..86748f8f --- /dev/null +++ b/crypto/core-test-framework/src/block_cipher.rs @@ -0,0 +1,185 @@ +//! Shared conformance tests for [`BlockCipherEncryptor`] / [`BlockCipherDecryptor`] implementors: +//! the whole-block refinement of the symmetric cipher traits. + +use crate::{DUMMY_SEED, FixedSeedRNG}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; + +/// Instance of the test framework. +pub struct TestFrameworkBlockCipher { + // Put any config options here +} + +impl TestFrameworkBlockCipher { + /// + pub fn new() -> Self { + Self {} + } + + /// + pub fn test< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, + E: BlockCipherEncryptor, + D: BlockCipherDecryptor, + >( + &self, + ) { + let key = KeyMaterial::::from_bytes_as_type( + &DUMMY_SEED[..KEY_LEN], + KeyType::SymmetricCipherKey, + ) + .unwrap(); + + // to test blocks, we'll chunk our dummy seed + let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); + let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); + + // one block at a time, through the flat streaming methods (LEN = BLOCK_LEN), in place + for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { + let mut buf = *msg_chunk; + encryptor.do_encrypt_inplace(&mut buf).unwrap(); + decryptor.do_decrypt_inplace(&mut buf).unwrap(); + assert_eq!(msg_chunk, &buf); + } + + // multi-block (two at a time) through the implementor hook `do_*_blocks`: blocks encrypted together + // must decrypt both together and one at a time, and blocks encrypted one at a time must + // decrypt together. + let (mut encryptor, iv) = E::do_encrypt_init(&key).unwrap(); + let mut decryptor = D::do_decrypt_init(&key, &iv).unwrap(); + + for msg_pair in DUMMY_SEED.as_chunks::().0.as_chunks::<2>().0.iter() { + // encrypt together, decrypt together + let mut buf = *msg_pair; + encryptor.do_encrypt_blocks_inplace(&mut buf).unwrap(); + decryptor.do_decrypt_blocks_inplace(&mut buf).unwrap(); + assert_eq!(msg_pair, &buf); + + // encrypt together, decrypt one at a time + let mut buf = *msg_pair; + encryptor.do_encrypt_blocks_inplace(&mut buf).unwrap(); + for (msg_chunk, block) in msg_pair.iter().zip(buf.iter_mut()) { + decryptor.do_decrypt_inplace(block).unwrap(); + assert_eq!(msg_chunk, block); + } + + // encrypt one at a time, decrypt together + let mut buf = *msg_pair; + for block in buf.iter_mut() { + encryptor.do_encrypt_inplace(block).unwrap(); + } + decryptor.do_decrypt_blocks_inplace(&mut buf).unwrap(); + assert_eq!(msg_pair, &buf); + } + + // one-shot API: a block-aligned byte array, in place. It must round-trip and agree with the + // streaming API for the same key and init data. Only LEN = BLOCK_LEN can be formed + // generically here (`2 * BLOCK_LEN` needs generic_const_exprs); multi-block one-shots are + // covered by the modes crate's tests with a concrete BLOCK_LEN. + let one_block: &[u8; BLOCK_LEN] = &DUMMY_SEED.as_chunks::().0[0]; + let mut buf = *one_block; + let (n, iv) = E::encrypt_inplace(&key, &mut buf).unwrap(); + assert_eq!(n, BLOCK_LEN, "encrypt must report the number of bytes written"); + let ct = buf; + let n = D::decrypt_inplace(&key, &iv, &mut buf).unwrap(); + assert_eq!(n, BLOCK_LEN, "decrypt must report the number of bytes written"); + assert_eq!(buf, *one_block); + // ...and it must agree with the streaming API under the same init data. + let mut streamed = D::do_decrypt_init(&key, &iv).unwrap(); + let mut buf = ct; + streamed.do_decrypt_inplace(&mut buf).unwrap(); + assert_eq!(buf, *one_block); + + // The RNG-taking constructor is only exercised for a cipher that has init data to + // generate. Its contract requires an implementation with `INIT_DATA_LEN == 0` (ECB) to + // panic instead, so driving it here would fail that implementor for conforming. + if INIT_DATA_LEN > 0 { + // the RNG-taking one-shot must give the streaming API's answer for the same RNG stream + let pinned = [0xA5u8; INIT_DATA_LEN]; + let mut expected = *one_block; + let (mut streamed, iv_streamed) = + E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)) + .unwrap(); + streamed.do_encrypt_inplace(&mut expected).unwrap(); + let mut buf = *one_block; + let (n, iv) = E::encrypt_rng_inplace( + &key, + &mut FixedSeedRNG::::new(pinned), + &mut buf, + ) + .unwrap(); + assert_eq!(n, BLOCK_LEN, "encrypt_rng must report the number of bytes written"); + assert_eq!(iv, iv_streamed); + assert_eq!(buf, expected); + } + + // test that the iv is random (ie not the same on two runs). A mode with no init data at all + // (ECB, INIT_DATA_LEN == 0) has nothing to compare: two empty arrays are always equal. + if INIT_DATA_LEN > 0 { + let (_encryptor, iv1) = E::do_encrypt_init(&key).unwrap(); + let (_encryptor, iv2) = E::do_encrypt_init(&key).unwrap(); + assert_ne!(iv1, iv2); + } + + // error case: KeyMaterial of wrong type + let mac_key = + KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) + .unwrap(); + match E::do_encrypt_init(&mac_key) { + 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, + ]; + 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 + // 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; + } + // 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(_) => { + if ss >= &E::MAX_SECURITY_STRENGTH { /* good */ + } else { + panic!("Should have been a strong enough key"); + } + } + Err(SymmetricCipherError::KeyMaterialError(_)) => { + if ss < &E::MAX_SECURITY_STRENGTH { /* good */ + } else { + panic!("Should not have accepted a key weaker than algorithm"); + } + } + _ => panic!("Unexpected error"), + }; + } + assert!(strengths_tested > 0, "strength sweep must not be vacuous"); + } +} diff --git a/crypto/core-test-framework/src/electronic_code_book.rs b/crypto/core-test-framework/src/electronic_code_book.rs new file mode 100644 index 00000000..d6ccfc49 --- /dev/null +++ b/crypto/core-test-framework/src/electronic_code_book.rs @@ -0,0 +1,200 @@ +//! Shared conformance tests for [`ElectronicCodeBook`] implementors. + +use crate::DUMMY_SEED; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; + +/// Instance of the test framework. +pub struct TestFrameworkElectronicCodeBook { + // Put any config options here +} + +impl Default for TestFrameworkElectronicCodeBook { + fn default() -> Self { + Self::new() + } +} + +impl TestFrameworkElectronicCodeBook { + /// + pub fn new() -> Self { + Self {} + } + + /// Exercises the trait contract for one implementor. + /// + /// Checks, in order: + /// * `decrypt_block` inverts `encrypt_block` on every block of [`DUMMY_SEED`]; + /// * the permutation actually permutes (a block is not left unchanged); + /// * distinct inputs give distinct outputs, i.e. it is injective on the blocks tested; + /// * `encrypt_2blocks` agrees with two `encrypt_block` calls **including their order**, and + /// likewise for `decrypt_2blocks` -- this is what pins each implementor's pair methods to + /// single-block semantics, and it is the reason the pair methods are worth having in the trait at all; + /// * the pair methods round-trip each other; + /// * `encrypt_4blocks` / `decrypt_4blocks` likewise agree with four single-block calls in + /// order, and round-trip each other; + /// * a key of the wrong [`KeyType`] is rejected; + /// * the security-strength policy matches [`Algorithm::MAX_SECURITY_STRENGTH`]. + /// + /// [`Algorithm::MAX_SECURITY_STRENGTH`]: bouncycastle_core::traits::Algorithm::MAX_SECURITY_STRENGTH + pub fn test< + const KEY_LEN: usize, + const BLOCK_LEN: usize, + P: ElectronicCodeBook, + >( + &self, + ) { + let key = KeyMaterial::::from_bytes_as_type( + &DUMMY_SEED[..KEY_LEN], + KeyType::SymmetricCipherKey, + ) + .unwrap(); + let perm = P::new(&key).unwrap(); + + let blocks = DUMMY_SEED.as_chunks::().0; + + // encrypt / decrypt are inverses, and the permutation is not the identity. + for block in blocks.iter() { + let mut buf = *block; + perm.encrypt_block(&mut buf); + assert_ne!(&buf, block, "encrypt_block must not be the identity"); + perm.decrypt_block(&mut buf); + assert_eq!(&buf, block, "decrypt_block must invert encrypt_block"); + + // ...and the other way round, since a mode may call either direction first. + let mut buf = *block; + perm.decrypt_block(&mut buf); + assert_ne!(&buf, block, "decrypt_block must not be the identity"); + perm.encrypt_block(&mut buf); + assert_eq!(&buf, block, "encrypt_block must invert decrypt_block"); + } + + // Distinct inputs must give distinct outputs. A permutation is injective, so this catches + // an implementation that collapses inputs (e.g. one that masks part of the block away). + for pair in blocks.as_chunks::<2>().0.iter() { + let [a, b] = pair; + assert_ne!(a, b, "DUMMY_SEED blocks should differ; test setup problem"); + let mut ea = *a; + let mut eb = *b; + perm.encrypt_block(&mut ea); + perm.encrypt_block(&mut eb); + assert_ne!(ea, eb, "distinct blocks must encrypt to distinct blocks"); + } + + // The pair methods must be indistinguishable from the single-block ones, in both slots. + // An implementation that swapped the two results, or processed only one of them, fails here. + for pair in blocks.as_chunks::<2>().0.iter() { + let [a, b] = pair; + + let mut singly = [*a, *b]; + perm.encrypt_block(&mut singly[0]); + perm.encrypt_block(&mut singly[1]); + let mut paired = [*a, *b]; + perm.encrypt_2blocks(&mut paired); + assert_eq!(paired, singly, "encrypt_2blocks must match two encrypt_block calls"); + + let mut singly = [*a, *b]; + perm.decrypt_block(&mut singly[0]); + perm.decrypt_block(&mut singly[1]); + let mut paired = [*a, *b]; + perm.decrypt_2blocks(&mut paired); + assert_eq!(paired, singly, "decrypt_2blocks must match two decrypt_block calls"); + + // Round-trip through the pair methods alone. + let mut buf = [*a, *b]; + perm.encrypt_2blocks(&mut buf); + perm.decrypt_2blocks(&mut buf); + assert_eq!(buf, [*a, *b], "decrypt_2blocks must invert encrypt_2blocks"); + } + + // The four-block methods must be indistinguishable from four single-block calls, in every + // slot, however the implementor builds them (two pair calls, one four-lane pass, or singly). + let fours = blocks.as_chunks::<4>().0; + assert!( + !fours.is_empty(), + "DUMMY_SEED should hold at least four blocks; test setup problem" + ); + for four in fours.iter() { + let mut singly = *four; + for block in singly.iter_mut() { + perm.encrypt_block(block); + } + let mut batched = *four; + perm.encrypt_4blocks(&mut batched); + assert_eq!(batched, singly, "encrypt_4blocks must match four encrypt_block calls"); + + let mut singly = *four; + for block in singly.iter_mut() { + perm.decrypt_block(block); + } + let mut batched = *four; + perm.decrypt_4blocks(&mut batched); + assert_eq!(batched, singly, "decrypt_4blocks must match four decrypt_block calls"); + + let mut buf = *four; + perm.encrypt_4blocks(&mut buf); + perm.decrypt_4blocks(&mut buf); + assert_eq!(buf, *four, "decrypt_4blocks must invert encrypt_4blocks"); + } + + // A pair of *identical* blocks must give a pair of identical outputs. This catches an + // implementation whose two lanes are not actually independent. + let block = blocks[0]; + let mut buf = [block, block]; + perm.encrypt_2blocks(&mut buf); + assert_eq!(buf[0], buf[1], "identical inputs must give identical outputs"); + let mut single = block; + perm.encrypt_block(&mut single); + assert_eq!(buf[0], single); + + // error case: KeyMaterial of the wrong type + let mac_key = + KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) + .unwrap(); + match P::new(&mac_key) { + Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } + _ => panic!("A key that is not a SymmetricCipherKey should have been rejected"), + }; + + // error case: security strengths too weak, and strong enough + 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, so skip the strengths a KEY_LEN-byte key cannot + // carry. Do NOT relax 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(); + + match P::new(&key) { + Ok(_) => assert!( + ss >= &P::MAX_SECURITY_STRENGTH, + "should have required a key at least as strong as the algorithm" + ), + Err(SymmetricCipherError::KeyMaterialError(_)) => assert!( + ss < &P::MAX_SECURITY_STRENGTH, + "should not have rejected a key strong enough for the algorithm" + ), + _ => panic!("Unexpected error"), + }; + } + } +} diff --git a/crypto/core-test-framework/src/fixed_seed_rng.rs b/crypto/core-test-framework/src/fixed_seed_rng.rs index 584ad6fb..3c8074b5 100644 --- a/crypto/core-test-framework/src/fixed_seed_rng.rs +++ b/crypto/core-test-framework/src/fixed_seed_rng.rs @@ -1,9 +1,10 @@ //! A deterministic fake [`RNG`] for reproducible tests. use bouncycastle_core::errors::{KeyMaterialError, RNGError}; -use bouncycastle_core::key_material; +use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{RNG, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::RNG; /// A test-only fake [`RNG`] that produces a fixed, fully deterministic byte stream. /// @@ -77,7 +78,7 @@ impl RNG for FixedSeedRNG { /// strength is enough for every ML-KEM / ML-DSA parameter set. fn fill_keymaterial_out(&mut self, out: &mut dyn KeyMaterialTrait) -> Result { let mut len = 0; - key_material::do_hazardous_operations(out, |out| { + do_hazardous_operations(out, |out| { len = self .next_bytes_out(out.ref_to_bytes_mut()?) .map_err(|_| KeyMaterialError::GenericError("RNG failed to acquire next bytes."))?; diff --git a/crypto/core-test-framework/src/hash.rs b/crypto/core-test-framework/src/hash.rs index 6c880ba9..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() @@ -99,7 +157,7 @@ impl TestFrameworkHash { /*** fn do_final_partial_bits_out(self, partial_byte: u8, num_bits: usize, output: &mut [u8]) -> Result; ***/ // A known-answer test for these needs a different expected output from the rest of this - // Helper: the digest of `input` finished with the low `num_bits` bits of `partial_byte`. + // Helper: the digest of `input` finished with the top `num_bits` bits of `partial_byte`. let partial_digest = |partial_byte: u8, num_bits: usize| -> Vec { let mut message_digest = H::default(); message_digest.do_update(input); @@ -119,17 +177,18 @@ impl TestFrameworkHash { ); } - // "The num_bits message bits are taken from the least significant bits of - // partial_byte": the unused high bits are not part of the message, and so must not - // change the 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 { - // no overflow: 1u8 << 7 == 0x80 - let mask = (1u8 << num_bits) - 1; + // 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_digest(partial_byte, num_bits), partial_digest(partial_byte & mask, num_bits), - "bits above num_bits = {num_bits} must be ignored / partial_byte: {partial_byte:#04X}" + "the low 8 - num_bits = {} bits must be ignored / partial_byte: {partial_byte:#04X}", + 8 - num_bits ); } } @@ -184,11 +243,14 @@ impl TestFrameworkHash { // Each (num_bits, partial_byte) pair is a distinct message, and so must produce a // distinct digest. This is what catches an implementation that silently drops the - // partial bits, or absorbs the wrong number of them. + // partial bits, or absorbs the wrong number of them. The num_bits message bits are + // enumerated in the top bits of the byte (the shift is done in u16 so that + // num_bits == 0, an 8-bit shift, cannot overflow). let mut partial_outputs: Vec> = Vec::new(); for num_bits in 0..=7 { - for partial_byte in 0..(1u16 << num_bits) { - partial_outputs.push(partial_digest(partial_byte as u8, num_bits)); + for message_bits in 0..(1u16 << num_bits) { + let partial_byte = (message_bits << (8 - num_bits)) as u8; + partial_outputs.push(partial_digest(partial_byte, num_bits)); } } let num_partial_outputs = partial_outputs.len(); @@ -201,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/kdf.rs b/crypto/core-test-framework/src/kdf.rs index 679ef598..8df29f45 100644 --- a/crypto/core-test-framework/src/kdf.rs +++ b/crypto/core-test-framework/src/kdf.rs @@ -3,7 +3,8 @@ use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; -use bouncycastle_core::traits::{KDF, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::KDF; /// Instance of the test framework. pub struct TestFrameworkKDF { diff --git a/crypto/core-test-framework/src/kem.rs b/crypto/core-test-framework/src/kem.rs index 67223c6b..f82e757f 100644 --- a/crypto/core-test-framework/src/kem.rs +++ b/crypto/core-test-framework/src/kem.rs @@ -2,8 +2,9 @@ use crate::FixedSeedRNG; use bouncycastle_core::errors::KEMError; +use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, RNG, SecurityStrength, + KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, RNG, }; /// Instance of the test framework. diff --git a/crypto/core-test-framework/src/key_stream.rs b/crypto/core-test-framework/src/key_stream.rs new file mode 100644 index 00000000..4d901910 --- /dev/null +++ b/crypto/core-test-framework/src/key_stream.rs @@ -0,0 +1,161 @@ +//! Shared conformance tests for [`KeyStream`] implementors. + +use crate::DUMMY_SEED; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::hazmat::KeyStream; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; + +/// Instance of the test framework. +pub struct TestFrameworkKeyStream { + // Put any config options here +} + +impl Default for TestFrameworkKeyStream { + fn default() -> Self { + Self::new() + } +} + +impl TestFrameworkKeyStream { + /// + pub fn new() -> Self { + Self {} + } + + /// Exercises the trait contract for one implementor. + /// + /// Checks, in order: + /// * `apply_blocks` XORs: applied to [`DUMMY_SEED`] it gives `DUMMY_SEED` XOR the keystream it + /// writes into zeros, and that keystream is not all zeros; + /// * the keystream is a function of the key and init data alone: the same pair gives the same + /// keystream, and different init data a different one; + /// * every chunking of the blocks into `apply_blocks` calls gives the one-call answer -- + /// including chunk sizes that are not multiples of any batch width the implementor uses; + /// * `remaining_blocks` goes down by exactly one per block applied (a keystream that reports + /// `u64::MAX`, no practical limit, may stay there); + /// * a key of the wrong [`KeyType`] is rejected; + /// * the security-strength policy matches [`Algorithm::MAX_SECURITY_STRENGTH`]. + /// + /// [`Algorithm::MAX_SECURITY_STRENGTH`]: bouncycastle_core::traits::Algorithm::MAX_SECURITY_STRENGTH + pub fn test< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, + KS: KeyStream, + >( + &self, + ) { + let key = KeyMaterial::::from_bytes_as_type( + &DUMMY_SEED[..KEY_LEN], + KeyType::SymmetricCipherKey, + ) + .unwrap(); + let init_data = [0xA5u8; INIT_DATA_LEN]; + let blocks = DUMMY_SEED.as_chunks::().0; + let n = blocks.len(); + + // The keystream itself: XORed into zeros. + let mut keystream = vec![[0u8; BLOCK_LEN]; n]; + KS::new(&key, &init_data).unwrap().apply_blocks(&mut keystream); + assert!( + keystream.iter().any(|b| b.iter().any(|&x| x != 0)), + "apply_blocks must produce keystream, not leave its input unchanged" + ); + + // XOR semantics: applied to data, the result is data XOR keystream. + let mut data = blocks.to_vec(); + KS::new(&key, &init_data).unwrap().apply_blocks(&mut data); + for ((d, k), p) in data.iter().zip(keystream.iter()).zip(blocks.iter()) { + let expected: [u8; BLOCK_LEN] = core::array::from_fn(|i| p[i] ^ k[i]); + assert_eq!(d, &expected, "apply_blocks must XOR the keystream into its input"); + } + + // Deterministic in (key, init data), and dependent on the init data. + let mut again = vec![[0u8; BLOCK_LEN]; n]; + KS::new(&key, &init_data).unwrap().apply_blocks(&mut again); + assert_eq!(again, keystream, "the same key and init data must give the same keystream"); + if INIT_DATA_LEN > 0 { + let mut other = vec![[0u8; BLOCK_LEN]; n]; + KS::new(&key, &[0x5Au8; INIT_DATA_LEN]).unwrap().apply_blocks(&mut other); + assert_ne!(other, keystream, "different init data must give a different keystream"); + } + + // Every chunking agrees with the single call, and `remaining_blocks` counts down by one per + // block. The chunk sizes straddle the batch widths an implementor is likely to use. + for chunk in [1usize, 2, 3, 4, 5, 7, 8, 9, n - 1, n] { + let mut ks = KS::new(&key, &init_data).unwrap(); + let mut chunked = vec![[0u8; BLOCK_LEN]; n]; + for piece in chunked.chunks_mut(chunk) { + let before = ks.remaining_blocks(); + ks.apply_blocks(piece); + let after = ks.remaining_blocks(); + if before != u64::MAX { + assert_eq!( + after, + before - piece.len() as u64, + "remaining_blocks must drop by exactly the blocks applied" + ); + } + } + assert_eq!(chunked, keystream, "chunk size {chunk} must give the one-call keystream"); + } + + // An empty call produces nothing and consumes nothing. + let mut ks = KS::new(&key, &init_data).unwrap(); + let before = ks.remaining_blocks(); + ks.apply_blocks(&mut []); + assert_eq!(ks.remaining_blocks(), before, "an empty call must not consume keystream"); + let mut first = [[0u8; BLOCK_LEN]]; + ks.apply_blocks(&mut first); + assert_eq!(first[0], keystream[0], "an empty call must not advance the keystream"); + + // error case: KeyMaterial of the wrong type + let mac_key = + KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) + .unwrap(); + match KS::new(&mac_key, &init_data) { + Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } + _ => panic!("A key that is not a SymmetricCipherKey should have been rejected"), + }; + + // error case: security strengths too weak, and strong enough + 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, so skip the strengths a KEY_LEN-byte key cannot + // carry. Do NOT relax 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(); + + match KS::new(&key, &init_data) { + Ok(_) => assert!( + ss >= &KS::MAX_SECURITY_STRENGTH, + "should have required a key at least as strong as the algorithm" + ), + Err(SymmetricCipherError::KeyMaterialError(_)) => assert!( + ss < &KS::MAX_SECURITY_STRENGTH, + "should not have rejected a key strong enough for the algorithm" + ), + _ => panic!("Unexpected error"), + }; + } + } +} diff --git a/crypto/core-test-framework/src/lib.rs b/crypto/core-test-framework/src/lib.rs index 2dced83d..64ab6ba5 100644 --- a/crypto/core-test-framework/src/lib.rs +++ b/crypto/core-test-framework/src/lib.rs @@ -14,17 +14,24 @@ // properly document everything. #![forbid(missing_docs)] +pub mod aead; +pub mod block_cipher; +pub mod electronic_code_book; pub mod hash; pub mod kdf; pub mod kem; +pub mod key_stream; pub mod mac; pub mod signature; pub mod suspendable_state; pub mod symmetric_ciphers; +pub mod test_data_loaders; pub mod xof; mod fixed_seed_rng; pub use fixed_seed_rng::FixedSeedRNG; +mod toy_block_cipher; +pub use toy_block_cipher::{TOY_BLOCK_LEN, ToyBlockCipher}; /// A dummy seed for use in tests which is \x00..\xFF repeated for 1024 bytes pub const DUMMY_SEED: &[u8; 1024] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"; diff --git a/crypto/core-test-framework/src/mac.rs b/crypto/core-test-framework/src/mac.rs index 8430507c..fcb04892 100644 --- a/crypto/core-test-framework/src/mac.rs +++ b/crypto/core-test-framework/src/mac.rs @@ -2,11 +2,10 @@ use crate::DUMMY_SEED; use bouncycastle_core::errors::{KeyMaterialError, MACError}; -use bouncycastle_core::key_material::{ - KeyMaterial512, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::MAC; -use bouncycastle_core::traits::SecurityStrength; /// Instance of the test framework. pub struct TestFrameworkMAC { diff --git a/crypto/core-test-framework/src/signature.rs b/crypto/core-test-framework/src/signature.rs index 28eece51..7c255efb 100644 --- a/crypto/core-test-framework/src/signature.rs +++ b/crypto/core-test-framework/src/signature.rs @@ -112,54 +112,54 @@ impl TestFrameworkSignature { VERIFIER::verify(&pk, DUMMY_SEED, None, &sig).unwrap(); // Test the streaming signing API - // fn sign_init(&mut self, sk: &SK) -> Result<(), SignatureError>; - // fn sign_update(&mut self, msg_chunk: &[u8]); - // fn sign_final(&mut self, msg_chunk: &[u8], ctx: &[u8]) -> Result, SignatureError>; - // fn sign_final_out(&mut self, msg_chunk: &[u8], ctx: &[u8], output: &mut [u8]) -> Result<(), SignatureError>; - - // First, test the streaming API with one call to .sign_update - let mut s = SIGNER::sign_init(&sk, Some(b"streaming API")).unwrap(); - s.sign_update(DUMMY_SEED); - let sig_val = s.sign_final().unwrap(); + // fn do_sign_init(&mut self, sk: &SK) -> Result<(), SignatureError>; + // fn do_sign_update(&mut self, msg_chunk: &[u8]); + // fn do_sign_final(&mut self, msg_chunk: &[u8], ctx: &[u8]) -> Result, SignatureError>; + // fn do_sign_final_out(&mut self, msg_chunk: &[u8], ctx: &[u8], output: &mut [u8]) -> Result<(), SignatureError>; + + // First, test the streaming API with one call to .do_sign_update + let mut s = SIGNER::do_sign_init(&sk, Some(b"streaming API")).unwrap(); + s.do_sign_update(DUMMY_SEED); + let sig_val = s.do_sign_final().unwrap(); VERIFIER::verify(&pk, DUMMY_SEED, Some(b"streaming API"), &sig_val).unwrap(); // Then with the message broken into chunks - let mut s = SIGNER::sign_init(&sk, Some(b"streaming API chunked")).unwrap(); + let mut s = SIGNER::do_sign_init(&sk, Some(b"streaming API chunked")).unwrap(); for msg_chunk in DUMMY_SEED.chunks(100) { - s.sign_update(msg_chunk); + s.do_sign_update(msg_chunk); } - let sig_val = s.sign_final().unwrap(); + let sig_val = s.do_sign_final().unwrap(); VERIFIER::verify(&pk, DUMMY_SEED, Some(b"streaming API chunked"), &sig_val).unwrap(); // Test the streaming verification API // one-shot let sig = SIGNER::sign(&sk, DUMMY_SEED, Some(b"streaming API")).unwrap(); - let mut v = VERIFIER::verify_init(&pk, Some(b"streaming API")).unwrap(); - v.verify_update(DUMMY_SEED); - v.verify_final(&sig).unwrap(); + let mut v = VERIFIER::do_verify_init(&pk, Some(b"streaming API")).unwrap(); + v.do_verify_update(DUMMY_SEED); + v.do_verify_final(&sig).unwrap(); // chunked let sig = SIGNER::sign(&sk, DUMMY_SEED, Some(b"streaming API")).unwrap(); - let mut v = VERIFIER::verify_init(&pk, Some(b"streaming API")).unwrap(); + let mut v = VERIFIER::do_verify_init(&pk, Some(b"streaming API")).unwrap(); for msg_chunk in DUMMY_SEED.chunks(100) { - v.verify_update(msg_chunk); + v.do_verify_update(msg_chunk); } - v.verify_final(&sig).unwrap(); + v.do_verify_final(&sig).unwrap(); // failure case for streaming verify let sig = SIGNER::sign(&sk, DUMMY_SEED, Some(b"streaming API")).unwrap(); - let mut v = VERIFIER::verify_init(&pk, Some(b"streaming API")).unwrap(); - v.verify_update(b"this is the wrong message"); - match v.verify_final(&sig) { + let mut v = VERIFIER::do_verify_init(&pk, Some(b"streaming API")).unwrap(); + v.do_verify_update(b"this is the wrong message"); + match v.do_verify_final(&sig) { Err(SignatureError::SignatureVerificationFailed) => (), _ => panic!("This should have thrown an error but it didn't."), } // test sign_out version of streaming API - let mut s = SIGNER::sign_init(&sk, Some(b"streaming API")).unwrap(); - s.sign_update(DUMMY_SEED); + let mut s = SIGNER::do_sign_init(&sk, Some(b"streaming API")).unwrap(); + s.do_sign_update(DUMMY_SEED); let mut sig_val = [0u8; SIG_LEN]; - let bytes_written = s.sign_final_out(&mut sig_val).unwrap(); + let bytes_written = s.do_sign_final_out(&mut sig_val).unwrap(); assert_eq!(bytes_written, SIG_LEN); VERIFIER::verify(&pk, DUMMY_SEED, Some(b"streaming API"), &sig_val).unwrap(); diff --git a/crypto/core-test-framework/src/suspendable_state.rs b/crypto/core-test-framework/src/suspendable_state.rs index 9d196403..d9fa5e1a 100644 --- a/crypto/core-test-framework/src/suspendable_state.rs +++ b/crypto/core-test-framework/src/suspendable_state.rs @@ -1,8 +1,8 @@ //! Generic behaviour tests for anything that implements [`Suspendable`] and [`SuspendableKeyed`]. use bouncycastle_core::errors::SuspendableError; -use bouncycastle_core::suspendable_state::{LIB_VERSION, SemVer}; use bouncycastle_core::traits::{Suspendable, SuspendableKeyed}; +use bouncycastle_utils::suspendable_state::{LIB_VERSION, SemVer}; /// Instance of the test framework. pub struct TestFrameworkSuspendableState { diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 57fc0ee1..f0459782 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -1,181 +1,444 @@ -//! Generic behaviour tests for the symmetric cipher traits. +//! Generic behaviour tests for the symmetric cipher traits: [`SymmetricCipherEncryptor`] / +//! [`SymmetricCipherDecryptor`] and their stream-cipher refinement. The block-cipher and AEAD +//! refinements have their own runners in [`crate::block_cipher`] and [`crate::aead`]. -use crate::DUMMY_SEED; +use crate::{DUMMY_SEED, FixedSeedRNG}; use bouncycastle_core::errors::SymmetricCipherError; -use bouncycastle_core::key_material::{ - KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - AEADCipher, BlockCipher, SecurityStrength, StreamCipher, SymmetricCipher, + StreamCipherDecryptor, StreamCipherEncryptor, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, }; /// Instance of the test framework. pub struct TestFrameworkSymmetricCipher { - // Put any config options here + /// For [`test_encryptor_decryptor`](Self::test_encryptor_decryptor): the plaintext length + /// granularity the pair accepts. 1 (the default) means every length round-trips. A larger value + /// -- the block length, for a `PaddedBlockCipherEncryptor` over `NoPadding` -- means only + /// multiples of it round-trip, and every other length must be *rejected* by `do_encrypt_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 one message length + /// the pair's streaming methods accept, if they accept only one. `None` (the default) means + /// any length. A cipher whose payload length is fixed by its type -- CCM, whose `B0` block + /// encodes the payload length, through its `DATA_LEN` parameter -- sets it here: the + /// streaming checks then run at exactly that length, the one-shots -- the trait's own, + /// provided over the streaming methods -- are checked to refuse every other length, and the + /// test also checks that one byte more is refused at the update and one byte fewer at the + /// final, on both sides. + pub fixed_message_len: Option, } impl TestFrameworkSymmetricCipher { /// pub fn new() -> Self { - Self {} + Self { required_alignment: 1, fixed_message_len: None } } - /// Test all the members of trait SymmetricCipher against the given input-output pair. - /// This gives good baseline test coverage, but is not exhaustive. - pub fn test< + /// Exercises the [`SymmetricCipherEncryptor`] / [`SymmetricCipherDecryptor`] contract for a + /// paired implementor. + /// + /// Checks, in order: + /// * the one-shot `encrypt_out` / `decrypt_out` round-trip for every plaintext length from + /// 0 to a few times `FINAL_LEN`, writing exactly `encrypt_out_len` bytes and at most + /// `decrypt_out_len`; + /// * the `std` one-shots agree with the `_out` ones; + /// * streaming in every chunking agrees with the one-shot, `update_out_len` is exact on every + /// call, and `do_encrypt_final_out` / `do_decrypt_final_out` agree with `do_encrypt_final` / + /// `do_decrypt_final`; + /// * a driven RNG reproduces its init data, and the same key and init data give the same + /// ciphertext through `do_encrypt_init_rng` and `encrypt_rng_out`; + /// * a corrupted ciphertext either fails to decrypt or decrypts to something else; + /// * an output buffer that is too short is refused, naming the required length, before any + /// work is done; + /// * every method that writes into a caller's buffer accepts one larger than needed, returns + /// the number of bytes that call wrote (not a running total over the stream), and zeroes + /// every byte of the buffer past that count; + /// * a key of the wrong [`KeyType`] is rejected, and the security-strength policy matches + /// [`Algorithm::MAX_SECURITY_STRENGTH`]. + /// + /// [`Algorithm::MAX_SECURITY_STRENGTH`]: bouncycastle_core::traits::Algorithm::MAX_SECURITY_STRENGTH + pub fn test_encryptor_decryptor< const KEY_LEN: usize, const INIT_DATA_LEN: usize, - C: SymmetricCipher, + const FINAL_LEN: usize, + E: SymmetricCipherEncryptor, + D: SymmetricCipherDecryptor, >( &self, ) { - let msg = b"The quick brown fox jumps over the lazy dog"; - let key = KeyMaterial::::from_bytes_as_type( &DUMMY_SEED[..KEY_LEN], KeyType::SymmetricCipherKey, ) .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); + if let Some(fixed) = self.fixed_message_len { + assert!(fixed <= DUMMY_SEED.len(), "fixed_message_len must fit the seed buffer"); + } - // 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]); - - // todo -- add tests for encrypt() / decrypt() wrapped in a #[cfg(std)] - - // 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); + // one-shot round trip, every (accepted) length; every other length must be refused + for len in 0..=max_len { + let msg = &DUMMY_SEED[..len]; + if !len.is_multiple_of(align) { + let mut ct = vec![0u8; E::encrypt_out_len(len) + FINAL_LEN]; + match E::encrypt_out(&key, msg, &mut ct) { + Err(SymmetricCipherError::PaddingError(_)) => {} + other => panic!("len {len} is not aligned and must be refused, got {other:?}"), + } + let (mut enc, _) = E::do_encrypt_init(&key).unwrap(); + let mut buf = vec![0u8; enc.do_encrypt_out_len(len)]; + enc.do_encrypt_out(msg, &mut buf).unwrap(); + assert!( + matches!(enc.do_encrypt_final(), Err(SymmetricCipherError::PaddingError(_))), + "len {len}: streaming do_encrypt_final must refuse an unaligned message" + ); + continue; } - 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) - .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() { - // Tag the key at an arbitrary strength for the purpose of this test. Inside a - // do_hazardous_operations() closure, set_security_strength() raises the strength - // (and bypasses the key-length guard) without complaining. - do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); + // a fixed-length pair's one-shots refuse every other length + if let Some(fixed) = self.fixed_message_len + && fixed != len + { + let mut ct = vec![0u8; E::encrypt_out_len(len)]; + assert!( + E::encrypt_out(&key, msg, &mut ct).is_err(), + "fixed length: a {len}-byte one-shot must be refused" + ); + continue; + } + let mut ct = vec![0u8; E::encrypt_out_len(len)]; + let (init_data, ct_len) = E::encrypt_out(&key, msg, &mut ct).unwrap(); + assert_eq!(ct_len, ct.len(), "encrypt_out must write exactly encrypt_out_len bytes"); + + let mut pt = vec![0u8; D::decrypt_out_len(ct_len)]; + let pt_len = D::decrypt_out(&key, &init_data, &ct[..ct_len], &mut pt).unwrap(); + assert!(pt_len <= pt.len(), "decrypt_out_len must bound the plaintext"); + assert_eq!(&pt[..pt_len], msg, "one-shot round trip, len {len}"); + + // the std one-shots agree with the _out ones for the same init data + let (init_data2, ct2) = E::encrypt(&key, msg).unwrap(); + assert_eq!(ct2.len(), ct_len, "encrypt must return exactly the bytes written"); + let pt2 = D::decrypt(&key, &init_data2, &ct2).unwrap(); + assert_eq!(pt2, msg, "std round trip, len {len}"); + let pt3 = D::decrypt(&key, &init_data, &ct[..ct_len]).unwrap(); + assert_eq!(pt3, msg, "decrypt must agree with decrypt_out"); + } - 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"); - } - } - Err(SymmetricCipherError::KeyMaterialError(_)) => { - if ss < &C::MAX_SECURITY_STRENGTH { /* good */ - } else { - panic!("Should not have accepted a key weaker than algorithm"); - } + // streaming in every chunking agrees with the one-shot + let len = self.fixed_message_len.unwrap_or(max_len); + let msg = &DUMMY_SEED[..len]; + let chunkings: [usize; 8] = + [1, 2, 3, 7, FINAL_LEN.max(1), FINAL_LEN + 1, 2 * FINAL_LEN + 3, len.max(1)]; + for chunk in chunkings { + // encrypt in chunks, checking update_out_len is exact each time + let (mut enc, init_data) = E::do_encrypt_init(&key).unwrap(); + let mut ct = Vec::new(); + for piece in msg.chunks(chunk) { + let expect = enc.do_encrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact (encrypt, chunk {chunk})"); + ct.extend_from_slice(&buf[..n]); + } + let mut last = [0u8; FINAL_LEN]; + let last_len = enc.do_encrypt_final_out(&mut last).unwrap(); + assert!( + last_len <= FINAL_LEN, + "do_encrypt_final_out must not claim more than FINAL_LEN bytes" + ); + ct.extend_from_slice(&last[..last_len]); + assert_eq!( + ct.len(), + E::encrypt_out_len(len), + "streaming total must match encrypt_out_len" + ); + + // one-shot decrypt of the streamed ciphertext + let mut pt = vec![0u8; D::decrypt_out_len(ct.len())]; + let m = D::decrypt_out(&key, &init_data, &ct, &mut pt).unwrap(); + assert_eq!( + &pt[..m], + msg, + "streamed ciphertext must decrypt in one shot (chunk {chunk})" + ); + + // decrypt in the same chunks, via do_decrypt_final and via do_decrypt_final_out + for use_out in [false, true] { + let mut dec = D::do_decrypt_init(&key, &init_data).unwrap(); + let mut rec = Vec::new(); + for piece in ct.chunks(chunk) { + let expect = dec.do_decrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact (decrypt, chunk {chunk})"); + rec.extend_from_slice(&buf[..n]); } - _ => panic!("Unexpected error"), - }; + let (block, data_len) = if use_out { + let mut block = [0u8; FINAL_LEN]; + let data_len = dec.do_decrypt_final_out(&mut block).unwrap(); + (block, data_len) + } else { + dec.do_decrypt_final().unwrap() + }; + rec.extend_from_slice(&block[..data_len]); + assert_eq!( + rec, msg, + "streamed round trip (chunk {chunk}, do_decrypt_final_out {use_out})" + ); + } } - } -} - -/// Instance of the test framework. -pub struct TestFrameworkBlockCipher { - // Put any config options here -} - -impl TestFrameworkBlockCipher { - /// - pub fn new() -> Self { - Self {} - } - - /// - pub fn test< - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - const BLOCK_LEN: usize, - C: BlockCipher, - >( - &self, - ) { - let key = KeyMaterial::::from_bytes_as_type( - &DUMMY_SEED[..KEY_LEN], - KeyType::SymmetricCipherKey, - ) - .unwrap(); - // to test blocks, we'll chunk our dummy seed - let (mut encryptor, iv) = C::do_encrypt_init(&key).unwrap(); - let mut decryptor = C::do_decrypt_init(&key, &iv).unwrap(); - - for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct = encryptor.do_encrypt_block(msg_chunk).unwrap(); - let pt = decryptor.do_decrypt_block(&ct).unwrap(); - assert_eq!(msg_chunk, &pt); + // a fixed message length is enforced on both sides: one byte more is refused at the + // update, consuming nothing, and one byte fewer is refused at the final. Which variant + // each refusal carries is the implementor's to pin; here only that it refuses. + if let Some(fixed) = self.fixed_message_len { + let (mut enc, init_data) = E::do_encrypt_init(&key).unwrap(); + let mut ct = vec![0u8; enc.do_encrypt_out_len(fixed)]; + let n = enc.do_encrypt_out(msg, &mut ct).unwrap(); + ct.truncate(n); + let mut more = vec![0u8; enc.do_encrypt_out_len(1) + 1]; + assert!( + enc.do_encrypt_out(&DUMMY_SEED[..1], &mut more).is_err(), + "fixed length: one byte more must be refused at the update" + ); + // ...and the refusal consumed nothing: the final still completes the message. + let (last, last_len) = enc.do_encrypt_final().unwrap(); + ct.extend_from_slice(&last[..last_len]); + let mut pt = vec![0u8; D::decrypt_out_len(ct.len())]; + let m = D::decrypt_out(&key, &init_data, &ct, &mut pt).unwrap(); + assert_eq!(&pt[..m], msg, "fixed length: a refused update must not disturb the state"); + + if fixed > 0 { + let (mut enc, _) = E::do_encrypt_init(&key).unwrap(); + let mut buf = vec![0u8; enc.do_encrypt_out_len(fixed - 1)]; + enc.do_encrypt_out(&msg[..fixed - 1], &mut buf).unwrap(); + assert!( + enc.do_encrypt_final().is_err(), + "fixed length: one byte fewer must be refused at the final (encrypt)" + ); + let mut dec = D::do_decrypt_init(&key, &init_data).unwrap(); + let mut buf = vec![0u8; dec.do_decrypt_out_len(fixed - 1)]; + dec.do_decrypt_out(&ct[..fixed - 1], &mut buf).unwrap(); + assert!( + dec.do_decrypt_final().is_err(), + "fixed length: one byte fewer must be refused at the final (decrypt)" + ); + } + let mut dec = D::do_decrypt_init(&key, &init_data).unwrap(); + let mut rec = vec![0u8; dec.do_decrypt_out_len(ct.len())]; + let n = dec.do_decrypt_out(&ct, &mut rec).unwrap(); + rec.truncate(n); + let mut more = vec![0u8; dec.do_decrypt_out_len(1) + 1]; + assert!( + dec.do_decrypt_out(&DUMMY_SEED[..1], &mut more).is_err(), + "fixed length: one byte more must be refused at the update (decrypt)" + ); + let (last, data_len) = dec.do_decrypt_final().unwrap(); + rec.extend_from_slice(&last[..data_len]); + assert_eq!( + rec, msg, + "fixed length: a refused update must not disturb the state (decrypt)" + ); } - // do it again using the _out versions - - let (mut encryptor, iv) = C::do_encrypt_init(&key).unwrap(); - let mut decryptor = C::do_decrypt_init(&key, &iv).unwrap(); - - let mut ct = [0u8; BLOCK_LEN]; - let mut pt = [0u8; BLOCK_LEN]; - for msg_chunk in DUMMY_SEED.as_chunks::().0.iter() { - let ct_bytes_written = encryptor.do_encrypt_block_out(msg_chunk, &mut ct).unwrap(); - assert_eq!(ct_bytes_written, BLOCK_LEN); + // The RNG-taking constructor is only exercised for a cipher that has init data to + // generate. Its contract requires an implementation with `INIT_DATA_LEN == 0` (ECB) to + // panic instead, so driving it here would fail that implementor for conforming. + if INIT_DATA_LEN > 0 { + // a driven RNG reproduces its init data, and determines the ciphertext + let seed: [u8; INIT_DATA_LEN] = core::array::from_fn(|i| DUMMY_SEED[100 + i]); + let (mut enc, init_data) = + E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(seed)) + .unwrap(); + assert_eq!(init_data, seed, "a fixed RNG must yield its stream as the init data"); + let mut streamed = vec![0u8; enc.do_encrypt_out_len(len)]; + let n = enc.do_encrypt_out(msg, &mut streamed).unwrap(); + streamed.truncate(n); + let (last, last_len) = enc.do_encrypt_final().unwrap(); + streamed.extend_from_slice(&last[..last_len]); + let mut one_shot = vec![0u8; E::encrypt_out_len(len)]; + let (init_data2, n2) = E::encrypt_rng_out( + &key, + &mut FixedSeedRNG::::new(seed), + msg, + &mut one_shot, + ) + .unwrap(); + assert_eq!(init_data2, seed); + assert_eq!( + &one_shot[..n2], + &streamed[..], + "same key and init data must give the same ciphertext" + ); + } - let pt_bytes_written = decryptor.do_decrypt_block_out(&ct, &mut pt).unwrap(); - assert_eq!(pt_bytes_written, BLOCK_LEN); + // corrupting the ciphertext does not give back the plaintext (or fails to decrypt) + let mut ct = vec![0u8; E::encrypt_out_len(len)]; + let (init_data, ct_len) = E::encrypt_out(&key, msg, &mut ct).unwrap(); + assert!(ct_len > 0, "the test message is non-empty, so its ciphertext must be"); + for flip in [0usize, ct_len / 2, ct_len - 1] { + let mut bad = ct[..ct_len].to_vec(); + bad[flip] ^= 0x80; + let mut pt = vec![0u8; D::decrypt_out_len(ct_len)]; + match D::decrypt_out(&key, &init_data, &bad, &mut pt) { + Ok(m) => { + assert_ne!(&pt[..m], msg, "corrupted byte {flip} decrypted to the plaintext") + } + Err(SymmetricCipherError::DecryptionFailed) + | Err(SymmetricCipherError::PaddingError(_)) + | Err(SymmetricCipherError::AEADTagCheckFailed) => { /* also fine */ } + Err(e) => panic!("unexpected error for corrupted byte {flip}: {e:?}"), + } + } - assert_eq!(msg_chunk, &pt); + // too-short output buffers are refused with the required length, before any work is done + let need = E::encrypt_out_len(len); + let mut short = vec![0u8; need - 1]; + match E::encrypt_out(&key, msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("encrypt_out into a short buffer: {other:?}"), + } + let need = D::decrypt_out_len(ct_len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match D::decrypt_out(&key, &init_data, &ct[..ct_len], &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + 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.do_encrypt_out_len(len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match enc.do_encrypt_out(msg, &mut short) { + Err(SymmetricCipherError::OutputBufferTooSmall(n)) => assert_eq!(n, need), + other => panic!("do_update_out into a short buffer: {other:?}"), + } } - // test that the iv is random (ie not the same on two runs) - let (_encryptor, iv1) = C::do_encrypt_init(&key).unwrap(); - let (_encryptor, iv2) = C::do_encrypt_init(&key).unwrap(); - assert_ne!(iv1, iv2); + // Output-buffer contract, for every method that writes into a caller's buffer: a buffer + // larger than needed is accepted; the returned count is the number of bytes *that call* + // wrote, not a running total over the stream; and every byte past it is zeroed, whatever + // the buffer held on the way in. Each buffer is pre-filled with a non-zero sentinel, so a + // byte left as the caller had it shows up. The `_final_out` buffers are `[u8; FINAL_LEN]` + // by type and so cannot be oversized; for them only the zeroed tail is checked. + const SENTINEL: u8 = 0xA5; + const EXTRA: usize = 7; + let assert_tail_zeroed = |buf: &[u8], n: usize, what: &str| { + assert!( + n <= buf.len(), + "{what}: claims {n} bytes written to a {}-byte buffer", + buf.len() + ); + assert!( + buf[n..].iter().all(|&b| b == 0), + "{what}: the bytes past the {n} written must be zeroed" + ); + }; - // error case: KeyMaterial of wrong type + // the one-shots + let need = E::encrypt_out_len(len); + let mut ct = vec![SENTINEL; need + EXTRA]; + let (init_data, ct_len) = E::encrypt_out(&key, msg, &mut ct).unwrap(); + assert_eq!(ct_len, need, "encrypt_out into an oversized buffer must write encrypt_out_len"); + assert_tail_zeroed(&ct, ct_len, "encrypt_out"); + ct.truncate(ct_len); + if INIT_DATA_LEN > 0 { + let seed: [u8; INIT_DATA_LEN] = core::array::from_fn(|i| DUMMY_SEED[100 + i]); + let mut buf = vec![SENTINEL; need + EXTRA]; + let (_, n) = E::encrypt_rng_out( + &key, + &mut FixedSeedRNG::::new(seed), + msg, + &mut buf, + ) + .unwrap(); + assert_eq!( + n, need, + "encrypt_rng_out into an oversized buffer must write encrypt_out_len" + ); + assert_tail_zeroed(&buf, n, "encrypt_rng_out"); + } + let mut pt = vec![SENTINEL; D::decrypt_out_len(ct_len) + EXTRA]; + let n = D::decrypt_out(&key, &init_data, &ct, &mut pt).unwrap(); + assert_eq!(&pt[..n], msg, "decrypt_out into an oversized buffer"); + assert_tail_zeroed(&pt, n, "decrypt_out"); + + // Streaming, in pieces of 1 byte, FINAL_LEN + 1 bytes and the rest: a cipher that holds + // data back then releases nothing on some calls after earlier calls released data, which is + // where a running total and a per-call count differ. Each call must report its own + // `do_*_out_len`, and the per-call counts must add up to exactly the whole stream's + // length, which a running total would overshoot. + let cuts = |total: usize| [0, 1.min(total), (FINAL_LEN + 2).min(total), total]; + let c = cuts(len); + let (mut enc, init_data) = E::do_encrypt_init(&key).unwrap(); + let mut streamed = Vec::new(); + for w in c.windows(2) { + let piece = &msg[w[0]..w[1]]; + let expect = enc.do_encrypt_out_len(piece.len()); + let mut buf = vec![SENTINEL; expect + EXTRA]; + let n = enc.do_encrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "do_encrypt_out must report the bytes this call wrote"); + assert_tail_zeroed(&buf, n, "do_encrypt_out"); + streamed.extend_from_slice(&buf[..n]); + } + let mut last = [SENTINEL; FINAL_LEN]; + let last_len = enc.do_encrypt_final_out(&mut last).unwrap(); + assert_tail_zeroed(&last, last_len, "do_encrypt_final_out"); + streamed.extend_from_slice(&last[..last_len]); + assert_eq!( + streamed.len(), + E::encrypt_out_len(len), + "the per-call counts must sum to encrypt_out_len, not overshoot it as running totals" + ); + + let c = cuts(streamed.len()); + let mut dec = D::do_decrypt_init(&key, &init_data).unwrap(); + let mut rec = Vec::new(); + for w in c.windows(2) { + let piece = &streamed[w[0]..w[1]]; + let expect = dec.do_decrypt_out_len(piece.len()); + let mut buf = vec![SENTINEL; expect + EXTRA]; + let n = dec.do_decrypt_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "do_decrypt_out must report the bytes this call wrote"); + assert_tail_zeroed(&buf, n, "do_decrypt_out"); + rec.extend_from_slice(&buf[..n]); + } + let mut last = [SENTINEL; FINAL_LEN]; + let data_len = dec.do_decrypt_final_out(&mut last).unwrap(); + assert_tail_zeroed(&last, data_len, "do_decrypt_final_out"); + rec.extend_from_slice(&last[..data_len]); + assert_eq!( + rec, msg, + "the per-call counts must sum to the plaintext, not overshoot it as running totals" + ); + + // error case: KeyMaterial of the wrong type let mac_key = KeyMaterial::::from_bytes_as_type(&DUMMY_SEED[..KEY_LEN], KeyType::MACKey) .unwrap(); - match C::do_encrypt_init(&mac_key) { + match E::do_encrypt_init(&mac_key) { Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } - _ => panic!("Unexpected error"), + _ => panic!("A key that is not a SymmetricCipherKey should have been rejected"), + }; + match D::do_decrypt_init(&mac_key, &init_data) { + Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } + _ => panic!("A key that is not a SymmetricCipherKey should have been rejected"), }; - // error case: security strengths too weak and too strong + // error case: security strengths too weak, and strong enough let mut key = KeyMaterial::::from_bytes_as_type( &DUMMY_SEED[..KEY_LEN], KeyType::SymmetricCipherKey, @@ -189,23 +452,27 @@ impl TestFrameworkBlockCipher { SecurityStrength::_256bit, ]; for ss in security_strengths.iter() { - // Tag the key at an arbitrary strength for the purpose of this test. Inside a - // do_hazardous_operations() closure, set_security_strength() raises the strength - // (and bypasses the key-length guard) without complaining. - do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); - - match C::do_encrypt_init(&key) { - Ok(_) => { - if ss >= &C::MAX_SECURITY_STRENGTH { /* good */ - } else { - panic!("Should have been a strong enough key"); - } - } + // Skip the strengths a KEY_LEN-byte key cannot carry; see `TestFrameworkElectronicCodeBook`. + if ss > &SecurityStrength::from_bytes(KEY_LEN) { + continue; + } + do_hazardous_operations(&mut key, |key| key.set_security_strength(*ss)).unwrap(); + + match E::do_encrypt_init(&key) { + Ok(_) => assert!( + ss >= &E::MAX_SECURITY_STRENGTH, + "should have required a key at least as strong as the algorithm" + ), + Err(SymmetricCipherError::KeyMaterialError(_)) => assert!( + ss < &E::MAX_SECURITY_STRENGTH, + "should not have rejected a key strong enough for the algorithm" + ), + _ => panic!("Unexpected error"), + }; + match D::do_decrypt_init(&key, &init_data) { + Ok(_) => assert!(ss >= &D::MAX_SECURITY_STRENGTH), Err(SymmetricCipherError::KeyMaterialError(_)) => { - if ss < &C::MAX_SECURITY_STRENGTH { /* good */ - } else { - panic!("Should not have accepted a key weaker than algorithm"); - } + assert!(ss < &D::MAX_SECURITY_STRENGTH) } _ => panic!("Unexpected error"), }; @@ -214,101 +481,148 @@ impl TestFrameworkBlockCipher { } /// Instance of the test framework. -pub struct TestFrameworkAEADCipher { +pub struct TestFrameworkStreamCipher { // Put any config options here } -impl TestFrameworkAEADCipher { +impl TestFrameworkStreamCipher { /// pub fn new() -> Self { Self {} } - /// Test all the members of trait AEADCipher against the given input-output pair. - /// This gives good baseline test coverage, but is not exhaustive. + /// Test the contract of a [`StreamCipherEncryptor`] / [`StreamCipherDecryptor`] pair: every + /// chunking of the streaming API agrees with the one-shot and round-trips through the other + /// direction, the RNG-taking constructors reproduce their init data, and the key-type and + /// security-strength policy is enforced. This gives good baseline test coverage, but is not + /// exhaustive; algorithm-specific test vectors belong in the implementing crate. pub fn test< const KEY_LEN: usize, - const NONCE_LEN: usize, - const TAG_LEN: usize, - C: AEADCipher, + const INIT_DATA_LEN: usize, + E: StreamCipherEncryptor, + D: StreamCipherDecryptor, >( &self, ) { - 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, ) .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]); + // one-shot, in place: must round-trip, and report every byte as written. + let mut buf = *DUMMY_SEED; + let (n, iv) = E::encrypt_inplace(&key, &mut buf).unwrap(); + assert_eq!(n, buf.len(), "encrypt must report the number of bytes written"); + let reference_ct = buf; + assert_ne!(&reference_ct[..], &DUMMY_SEED[..], "encryption must change the data"); + let n = D::decrypt_inplace(&key, &iv, &mut buf).unwrap(); + assert_eq!(n, buf.len(), "decrypt must report the number of bytes written"); + assert_eq!(&buf[..], &DUMMY_SEED[..]); + + // the streaming API under the same init data must give the one-shot's answer whatever + // the chunking, including chunks that are not a multiple of any internal keystream block + // and empty chunks; and encrypting in one chunking must decrypt in any other. + let chunkings: &[usize] = &[1, 3, 7, 16, 63, 64, 65, 250, DUMMY_SEED.len()]; + for &enc_chunk in chunkings { + let mut buf = *DUMMY_SEED; + let (mut encryptor, iv2) = E::do_encrypt_init(&key).unwrap(); + // stream through the encryptor, with an empty chunk thrown in at the start and end + encryptor.do_encrypt_inplace(&mut []).unwrap(); + for chunk in buf.chunks_mut(enc_chunk) { + encryptor.do_encrypt_inplace(chunk).unwrap(); + } + encryptor.do_encrypt_inplace(&mut []).unwrap(); + let ct = buf; + + for &dec_chunk in chunkings { + let mut buf = ct; + let mut decryptor = D::do_decrypt_init(&key, &iv2).unwrap(); + decryptor.do_decrypt_inplace(&mut []).unwrap(); + for chunk in buf.chunks_mut(dec_chunk) { + decryptor.do_decrypt_inplace(chunk).unwrap(); + } + decryptor.do_decrypt_inplace(&mut []).unwrap(); + assert_eq!( + &buf[..], + &DUMMY_SEED[..], + "enc chunk {enc_chunk}, dec chunk {dec_chunk}" + ); + } + + // and the one-shot decrypt agrees with every streaming encryption + let mut buf = ct; + D::decrypt_inplace(&key, &iv2, &mut buf).unwrap(); + assert_eq!(&buf[..], &DUMMY_SEED[..]); } - 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]); - - // todo -- add tests for aead_encrypt() / aead_decrypt() wrapped in a #[cfg(std)] - - // 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) { - Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - Err(SymmetricCipherError::DecryptionFailed) => { /* also acceptable */ } - _ => panic!("Modified ciphertext must fail the AEAD tag check"), - }; - // 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( - &key, - &nonce, - b"not the right associated data", - &ct[..ct_bytes_written], - &tag, - &mut pt, - ) { - Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } - _ => panic!("Expected TagCheckFailed error"), - }; - // 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"), - }; + // the streaming decryptor must agree with the one-shot encryptor under its init data + let mut buf = reference_ct; + let mut streamed = D::do_decrypt_init(&key, &iv).unwrap(); + for chunk in buf.chunks_mut(5) { + streamed.do_decrypt_inplace(chunk).unwrap(); + } + assert_eq!(&buf[..], &DUMMY_SEED[..]); + + // The RNG-taking constructor is only exercised for a cipher that has init data to + // generate. Its contract requires an implementation with `INIT_DATA_LEN == 0` (ECB) to + // panic instead, so driving it here would fail that implementor for conforming. + if INIT_DATA_LEN > 0 { + // the RNG-taking one-shot must give the streaming API's answer for the same RNG stream, + // and the same init data. + let pinned = [0xA5u8; INIT_DATA_LEN]; + let mut expected = *DUMMY_SEED; + let (mut streamed, iv_streamed) = + E::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(pinned)) + .unwrap(); + streamed.do_encrypt_inplace(&mut expected).unwrap(); + let mut buf = *DUMMY_SEED; + let (n, iv) = E::encrypt_rng_inplace( + &key, + &mut FixedSeedRNG::::new(pinned), + &mut buf, + ) + .unwrap(); + assert_eq!(n, buf.len(), "encrypt_rng must report the number of bytes written"); + assert_eq!(iv, iv_streamed); + assert_eq!(&buf[..], &expected[..]); + // ...and a driven RNG determines the ciphertext: the same RNG stream again gives the same + // init data and ciphertext, so the ciphertext is a function of (key, init data) alone. + let mut buf2 = *DUMMY_SEED; + let (_, iv_again) = E::encrypt_rng_inplace( + &key, + &mut FixedSeedRNG::::new(pinned), + &mut buf2, + ) + .unwrap(); + assert_eq!(iv, iv_again); + assert_eq!(&buf[..], &buf2[..]); + } - // 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); + // test that the init data is random (ie not the same on two runs). A cipher with no init + // data at all (INIT_DATA_LEN == 0) has nothing to compare: two empty arrays are always equal. + if INIT_DATA_LEN > 0 { + let (_encryptor, iv1) = E::do_encrypt_init(&key).unwrap(); + let (_encryptor, iv2) = E::do_encrypt_init(&key).unwrap(); + assert_ne!(iv1, iv2); + // and different init data under the same key gives different ciphertext + let mut a = *DUMMY_SEED; + let mut b = *DUMMY_SEED; + let (_, iv_a) = E::encrypt_inplace(&key, &mut a).unwrap(); + let (_, iv_b) = E::encrypt_inplace(&key, &mut b).unwrap(); + assert_ne!(iv_a, iv_b); + assert_ne!(&a[..], &b[..]); + } - // error case: KeyMaterial of wrong type + // error case: KeyMaterial of wrong type, for both directions 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) { + match E::do_encrypt_init(&mac_key) { + Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } + _ => panic!("Unexpected error"), + }; + match D::do_decrypt_init(&mac_key, &[0u8; INIT_DATA_LEN]) { Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } _ => panic!("Unexpected error"), }; @@ -327,54 +641,39 @@ impl TestFrameworkAEADCipher { SecurityStrength::_256bit, ]; for ss in security_strengths.iter() { - // Tag the key at an arbitrary strength for the purpose of this test. Inside a - // do_hazardous_operations() closure, set_security_strength() raises the strength - // (and bypasses the key-length guard) without complaining. + // `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(); - // The key-strength requirement must be enforced both by the AEAD one-shot and by the - // inherited SymmetricCipher one-shot (encrypt_out), so exercise both. - let check_strength = |result: Result<(), SymmetricCipherError>| match result { + let check = |r: Result<(), SymmetricCipherError>, max: &SecurityStrength| match r { Ok(_) => { - if ss >= &C::MAX_SECURITY_STRENGTH { /* good */ + if ss >= max { /* good */ } else { panic!("Should have been a strong enough key"); } } Err(SymmetricCipherError::KeyMaterialError(_)) => { - if ss < &C::MAX_SECURITY_STRENGTH { /* good */ + if ss < max { /* good */ } else { panic!("Should not have accepted a key weaker than algorithm"); } } _ => 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(|_| ())); + check(E::do_encrypt_init(&key).map(|_| ()), &E::MAX_SECURITY_STRENGTH); + check( + D::do_decrypt_init(&key, &[0u8; INIT_DATA_LEN]).map(|_| ()), + &D::MAX_SECURITY_STRENGTH, + ); } } } - -/// Instance of the test framework. -pub struct TestFrameworkStreamCipher { - // Put any config options here -} - -impl TestFrameworkStreamCipher { - /// - pub fn new() -> Self { - Self {} - } - - /// Test all the members of trait StreamCipher against the given input-output pair. - /// This gives good baseline test coverage, but is not exhaustive. - pub fn test< - const KEY_LEN: usize, - const INIT_DATA_LEN: usize, - C: StreamCipher, - >( - &self, - ) { - todo!() - } -} diff --git a/crypto/core-test-framework/src/test_data_loaders.rs b/crypto/core-test-framework/src/test_data_loaders.rs new file mode 100644 index 00000000..839cc70f --- /dev/null +++ b/crypto/core-test-framework/src/test_data_loaders.rs @@ -0,0 +1,91 @@ +//! Loaders for the external test-vector repositories. +//! +//! Vector suites read from two repositories that are cloned beside this one rather than vendored: +//! [bc-test-data](https://github.com/bcgit/bc-test-data) at `../bc-test-data`, and +//! [Wycheproof](https://github.com/C2SP/wycheproof) at `../wycheproof`. Both are optional. When one +//! is absent its loader prints a warning, once per test binary, and returns `None`; the caller +//! returns early, so `cargo test` passes for someone who has cloned only this repository. When a +//! repository is present, a file missing from it is a failure rather than a skip. +//! +//! The paths are resolved from this crate's manifest directory, which is at the same depth as every +//! crate under `crypto/`, so they do not depend on the directory `cargo test` runs in. Under +//! `cargo mutants`, which copies the tree into `/tmp`, they resolve to `/tmp/bc-test-data` and +//! `/tmp/wycheproof`. + +use bouncycastle_hex as hex; +use std::fs; +use std::path::Path; +use std::sync::Once; + +/// The parsed form of a vector file, re-exported so that suites need no `serde_json` dependency +/// of their own. +pub use serde_json::Value; + +const BC_TEST_DATA_ROOT: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../bc-test-data"); +const WYCHEPROOF_ROOT: &str = + concat!(env!("CARGO_MANIFEST_DIR"), "/../../../wycheproof/testvectors_v1"); + +static BC_TEST_DATA_CHECK: Once = Once::new(); +static WYCHEPROOF_CHECK: Once = Once::new(); + +/// Returns the contents of `bc-test-data//`, or `None` (after a one-time warning) +/// if bc-test-data is not cloned beside this repository. +/// +/// Panics if bc-test-data is present but the file cannot be read. +pub fn bc_test_data(dir: &str, filename: &str) -> Option { + read(BC_TEST_DATA_ROOT, &BC_TEST_DATA_CHECK, "bc-test-data", &format!("{dir}/{filename}")) +} + +/// Returns the contents of `wycheproof/testvectors_v1/`, or `None` (after a one-time +/// warning) if Wycheproof is not cloned beside this repository. +/// +/// Panics if Wycheproof is present but the file cannot be read. +pub fn wycheproof(filename: &str) -> Option { + read(WYCHEPROOF_ROOT, &WYCHEPROOF_CHECK, "wycheproof", filename) +} + +/// [`bc_test_data`], parsed as JSON. +/// +/// Panics if the file is not valid JSON. +pub fn bc_test_data_json(dir: &str, filename: &str) -> Option { + bc_test_data(dir, filename).map(|s| parse(&s, filename)) +} + +/// [`wycheproof`], parsed as JSON. +/// +/// Panics if the file is not valid JSON. +pub fn wycheproof_json(filename: &str) -> Option { + wycheproof(filename).map(|s| parse(&s, filename)) +} + +/// The hex-encoded `field` of one test case, decoded; a missing field or bad hex names the case. +pub fn hex_field(case: &Value, field: &str, tc_id: u64) -> Vec { + let s = case + .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}")) +} + +fn parse(contents: &str, filename: &str) -> Value { + serde_json::from_str(contents).unwrap_or_else(|e| panic!("{filename} is not valid JSON: {e}")) +} + +fn read(root: &str, check: &Once, repo: &str, path: &str) -> Option { + let found = Path::new(root).is_dir(); + check.call_once(|| { + if found { + println!("{repo} found at: {root}"); + } else { + println!("WARNING: {repo} not found at {root}; tests that need it will be skipped"); + } + }); + if !found { + return None; + } + let path = format!("{root}/{path}"); + Some( + fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("{repo} is present but {path} is unreadable: {e}")), + ) +} diff --git a/crypto/core-test-framework/src/toy_block_cipher.rs b/crypto/core-test-framework/src/toy_block_cipher.rs new file mode 100644 index 00000000..8bca0a35 --- /dev/null +++ b/crypto/core-test-framework/src/toy_block_cipher.rs @@ -0,0 +1,107 @@ +//! A deliberately insecure block "cipher" for exercising the code that is built on top of one. +//! +//! [`ToyBlockCipher`] implements [`ElectronicCodeBook`] with a 16-byte key and a 16-byte block, so +//! it slots in wherever AES-128 would, and it validates its key the way a real permutation does: +//! it wants a [`KeyType::SymmetricCipherKey`] of the right length and at least 128-bit strength. +//! That is what lets the conformance suites' key-policy checks run against it. Everything else +//! about it is chosen for testability, not security: +//! +//! * **Each byte is permuted on its own**, as `rotate_left(1)` then XOR with the corresponding key +//! byte. A one-bit change in a block therefore moves exactly one bit of the output, one place to +//! the left, in the same byte -- which makes a mode's error propagation exact arithmetic instead +//! of a statistical claim about diffusion. The flip side is that anything that *depends* on +//! diffusion (a "random bit errors" claim, a collision argument) cannot be shown with it. +//! * **Encryption and decryption are genuinely different functions.** The obvious toy, +//! `block ^= key`, is its own inverse and would let a mode that called the wrong direction +//! round-trip regardless. Here the inverse is XOR then `rotate_right(1)`, so a decryptor that +//! used the forward function, or vice versa, produces the wrong answer. +//! * The batch methods are plain loops over the single-block ones, so a mode driven through them +//! gets the same answer as one driven block by block. +//! +//! It exists so that a crate generic over a block cipher -- a mode of operation, say -- can have +//! runnable documentation examples and unit tests without depending on a real cipher crate, which +//! would be a dependency cycle when that cipher crate depends on the mode. **Never use it for +//! anything but tests and examples.** It is included in this crate's public API for the same +//! reason [`FixedSeedRNG`](crate::FixedSeedRNG) is: it is a test double, and this crate is only +//! ever a dev-dependency. + +use bouncycastle_core::errors::{KeyMaterialError, SymmetricCipherError}; +use bouncycastle_core::hazmat::ElectronicCodeBook; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::Algorithm; + +/// Key and block length of [`ToyBlockCipher`]: the same as AES-128, so the toy exercises the same +/// shapes a real cipher would. +pub const TOY_BLOCK_LEN: usize = 16; + +/// A per-byte, key-validating, insecure permutation with a 16-byte key and block. See the module +/// docs for what it is and is not good for. +#[derive(Clone)] +pub struct ToyBlockCipher { + key: [u8; TOY_BLOCK_LEN], +} + +impl Algorithm for ToyBlockCipher { + const ALG_NAME: &'static str = "ToyBlockCipher"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl ElectronicCodeBook for ToyBlockCipher { + /// Rejects the same keys a real permutation would, so that key-policy checks are meaningful. + fn new(key: &KeyMaterial) -> Result { + if key.key_type() != KeyType::SymmetricCipherKey { + return Err(KeyMaterialError::InvalidKeyType( + "ToyBlockCipher needs a SymmetricCipherKey", + ) + .into()); + } + if key.key_len() != TOY_BLOCK_LEN { + return Err(KeyMaterialError::InvalidLength.into()); + } + if key.security_strength() < SecurityStrength::_128bit { + return Err( + KeyMaterialError::SecurityStrength("ToyBlockCipher needs a 128-bit key").into() + ); + } + let mut bytes = [0u8; TOY_BLOCK_LEN]; + bytes.copy_from_slice(key.ref_to_bytes()); + Ok(Self { key: bytes }) + } + + fn encrypt_block(&self, block: &mut [u8; TOY_BLOCK_LEN]) { + for (b, k) in block.iter_mut().zip(self.key.iter()) { + *b = b.rotate_left(1) ^ *k; + } + } + + fn decrypt_block(&self, block: &mut [u8; TOY_BLOCK_LEN]) { + for (b, k) in block.iter_mut().zip(self.key.iter()) { + *b = (*b ^ *k).rotate_right(1); + } + } + + fn encrypt_2blocks(&self, blocks: &mut [[u8; TOY_BLOCK_LEN]; 2]) { + for block in blocks.iter_mut() { + self.encrypt_block(block); + } + } + + fn decrypt_2blocks(&self, blocks: &mut [[u8; TOY_BLOCK_LEN]; 2]) { + for block in blocks.iter_mut() { + self.decrypt_block(block); + } + } + + fn encrypt_4blocks(&self, blocks: &mut [[u8; TOY_BLOCK_LEN]; 4]) { + for block in blocks.iter_mut() { + self.encrypt_block(block); + } + } + + fn decrypt_4blocks(&self, blocks: &mut [[u8; TOY_BLOCK_LEN]; 4]) { + for block in blocks.iter_mut() { + self.decrypt_block(block); + } + } +} diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index 9ec5040b..2c941f37 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -1,251 +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_output_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_output_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_output_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_output_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_output_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 low `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 taken from the least significant bits of - // partial_byte". - // So the unused high bits are not part of the message and must not change the output. - for num_bits in 0..=7 { - // no overflow: 1u8 << 7 == 0x80 - let mask = (1u8 << num_bits) - 1; - 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), - "bits above num_bits = {num_bits} must be ignored / partial_byte: {partial_byte:#04X}" - ); - } - } + // 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> ***/ - // "The bits are returned in the least significant num_bits bits of the returned u8, with - // the remaining high bits zero." - // They are the 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]. - let split = expected_output.len() / 2; - for num_bits in 0..=7 { - // no overflow: 1u8 << 7 == 0x80 - let mask = (1u8 << num_bits) - 1; - - 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] & mask, - "the squeezed bits must be the low bits of the next output byte / num_bits: {num_bits}" - ); - assert_eq!( - partial_byte & !mask, - 0x00, - "the unused high 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-test-framework/tests/toy_block_cipher_tests.rs b/crypto/core-test-framework/tests/toy_block_cipher_tests.rs new file mode 100644 index 00000000..23f7de1c --- /dev/null +++ b/crypto/core-test-framework/tests/toy_block_cipher_tests.rs @@ -0,0 +1,14 @@ +//! Pins [`ToyBlockCipher`] to the [`ElectronicCodeBook`] contract through this crate's own +//! conformance suite, so that a crate using the toy as a stand-in can rely on it behaving like a +//! real implementor: both directions are inverses, the permutation is injective, the batch +//! methods agree with the single-block ones, and the key policy is enforced. +//! +//! [`ElectronicCodeBook`]: bouncycastle_core::hazmat::ElectronicCodeBook + +use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; +use bouncycastle_core_test_framework::{TOY_BLOCK_LEN, ToyBlockCipher}; + +#[test] +fn the_toy_block_cipher_conforms_to_the_trait() { + TestFrameworkElectronicCodeBook::new().test::(); +} diff --git a/crypto/core/src/errors.rs b/crypto/core/src/errors.rs index 7be5197e..11f8ecc1 100644 --- a/crypto/core/src/errors.rs +++ b/crypto/core/src/errors.rs @@ -8,7 +8,7 @@ //! an error if a caller matches exhaustively against the current set of variants. /// -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum HashError { /// @@ -24,7 +24,7 @@ pub enum HashError { } /// -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum KeyMaterialError { /// @@ -44,7 +44,7 @@ pub enum KeyMaterialError { } /// -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum KDFError { /// @@ -60,7 +60,7 @@ pub enum KDFError { } /// -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum KEMError { /// @@ -84,7 +84,7 @@ pub enum KEMError { } /// -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum MACError { /// @@ -100,7 +100,7 @@ pub enum MACError { } /// -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum RNGError { /// @@ -126,18 +126,13 @@ pub enum RNGError { KeyMaterialError(KeyMaterialError), } -/// -#[derive(Debug)] -#[non_exhaustive] -pub enum SuspendableError { - /// The serialized state was produced by a library version incompatible with this one. - IncompatibleVersion, - /// The serialized state is malformed or corrupt. - InvalidData, -} +/// Errors from [`Suspendable`](crate::traits::Suspendable) and +/// [`SuspendableKeyed`](crate::traits::SuspendableKeyed). Defined in `bouncycastle-utils` next to +/// the version-header helpers that raise it, and re-exported here with the other error types. +pub use bouncycastle_utils::suspendable_state::SuspendableError; /// -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum SignatureError { /// @@ -161,7 +156,7 @@ pub enum SignatureError { } /// -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub enum SymmetricCipherError { /// @@ -170,18 +165,48 @@ pub enum SymmetricCipherError { AEADTagCheckFailed, /// DecryptionFailed, - /// Indicates that the output buffer is not large enough to hold the requested output. - /// The usize represents the required buffer length. - IncorrectOutputBufferLength(&'static str, usize), + /// The caller's output buffer is too small for what this call would write. The usize is the + /// minimum length the buffer needs for the same call to succeed on a retry; the call consumed + /// no input and left the cipher's state untouched, so retrying with a buffer at least that + /// long produces exactly what the refused call would have. + OutputBufferTooSmall(usize), + /// The cipher has no keystream or counter space left under its current init data: this call + /// would need more than remains, and continuing would repeat keystream. The call consumed no + /// input and left the cipher's state untouched, so the bytes that still fit can be processed + /// in a shorter call; the rest needs a fresh encryption under new init data. + DataLimitExceeded, /// KeyMaterialError(KeyMaterialError), /// + PaddingError(PaddingError), + /// RNGError(RNGError), /// StateError(&'static str), } +/// Errors from a [`crate::traits::BlockCipherPadding`] scheme. +#[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum PaddingError { + /// `pad()` was asked to pad more data than fits in a block alongside at least one byte of padding. + /// The usize is the maximum permitted data length (`BLOCK_LEN - 1`). + DataLengthTooLong(usize), + /// `unpad()` found the block does not carry well-formed padding. Deliberately carries no detail + /// about *how* the padding was malformed. + InvalidPadding, + /// `pad()` was asked to add padding by a scheme that adds none (`NoPadding`): the data was not + /// a whole number of blocks, and the caller must align it. + PaddingNotPermitted, +} + /*** Promotion functions ***/ +impl From for SymmetricCipherError { + fn from(e: PaddingError) -> SymmetricCipherError { + Self::PaddingError(e) + } +} + impl From for SymmetricCipherError { fn from(e: KeyMaterialError) -> SymmetricCipherError { Self::KeyMaterialError(e) diff --git a/crypto/core/src/hazmat/electronic_code_book.rs b/crypto/core/src/hazmat/electronic_code_book.rs new file mode 100644 index 00000000..0bcea7b6 --- /dev/null +++ b/crypto/core/src/hazmat/electronic_code_book.rs @@ -0,0 +1,84 @@ +//! The [`ElectronicCodeBook`] trait: a keyed block permutation. + +use crate::errors::SymmetricCipherError; +use crate::key_material::KeyMaterial; +use crate::traits::Algorithm; + +// Imports needed for docs +#[allow(unused_imports)] +use crate::key_material::KeyType; +// end of imports needed for docs + +/// A keyed block permutation: the `CIPH_K` / `CIPH^-1_K` of NIST SP 800-38A Sec 5.1. +/// +/// # 🚨 Security Considerations 🚨 +/// A permutation applied to data block by block is ECB: equal plaintext blocks give equal +/// ciphertext blocks, so the structure of the plaintext survives. This is the primitive under +/// CBC, CTR, GCM and the rest of `bouncycastle_cipher::modes`, not a cipher for data; see the +/// [module docs](crate::hazmat) for the supported uses. +/// +/// Implementors are expected to hold the key schedule in a zeroize-on-drop wrapper +/// (`bouncycastle_utils::secret::Secret`), so it is scrubbed when the value is dropped. +/// +/// # Why the block methods are infallible +/// +/// Every length here is fixed by a type, and a constructed value is always ready to use, so there +/// is nothing a caller can get wrong once [`ElectronicCodeBook::new`] has returned. Only `new` can +/// fail, and only because of the key. +pub trait ElectronicCodeBook: + Algorithm + Sized +{ + /// Expands the key. + /// + /// # Errors + /// Rejects a key whose [`KeyType`] is not [`KeyType::SymmetricCipherKey`], and one whose + /// security strength is below [`Algorithm::MAX_SECURITY_STRENGTH`], both as a + /// [`SymmetricCipherError::KeyMaterialError`]. + fn new(key: &KeyMaterial) -> Result; + + /// The forward cipher function, in place. + fn encrypt_block(&self, block: &mut [u8; BLOCK_LEN]); + + /// The inverse cipher function, in place. + fn decrypt_block(&self, block: &mut [u8; BLOCK_LEN]); + + /// The forward cipher function on two *independent* blocks, in place. + /// + /// Required, with no default, so that every implementor decides for itself how to run a pair. + /// A bit-sliced engine whose natural unit is a pair (see `bouncycastle-aes`) runs both blocks + /// in one pass for barely more than the cost of one; an engine with no unit wider than a block + /// makes two [`ElectronicCodeBook::encrypt_block`] calls. A default of two single-block calls + /// would be right only for the second kind, and silently wrong -- twice the work, with nothing + /// failing -- for a wider engine that forgot to override it. + /// + /// Must be indistinguishable from two [`ElectronicCodeBook::encrypt_block`] calls, including + /// the order of the two results. `TestFrameworkElectronicCodeBook` pins that. + /// + /// Modes whose structure is parallel -- CBC decryption, CFB decryption, CTR -- should prefer + /// this. CBC and CFB *encryption* cannot use it: each input block depends on the previous + /// output. + fn encrypt_2blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]); + + /// The inverse cipher function on two *independent* blocks, in place. + /// See [`ElectronicCodeBook::encrypt_2blocks`]. + fn decrypt_2blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]); + + /// The forward cipher function on four *independent* blocks, in place. + /// + /// Required for the same reason as [`ElectronicCodeBook::encrypt_2blocks`]. An engine whose + /// natural unit is a pair runs the four as two pair calls; a bit-sliced engine whose S-box + /// circuit substitutes four blocks per pass runs them as one full pass rather than two + /// half-empty pair calls. Four is the unit because it is the widest any engine in this library + /// fills: AES fills a pair, and the `u16`- and `u32`-plane engines (SM4, Camellia, ARIA) fill + /// four. + /// Must be indistinguishable from four [`ElectronicCodeBook::encrypt_block`] calls, including + /// the order of the four results. `TestFrameworkElectronicCodeBook` pins that. + /// + /// Modes with parallel structure chunk their data into fours first, then pairs, then single + /// blocks; see CBC decryption in `bouncycastle_cipher::modes`. + fn encrypt_4blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 4]); + + /// The inverse cipher function on four *independent* blocks, in place. + /// See [`ElectronicCodeBook::encrypt_4blocks`]. + fn decrypt_4blocks(&self, blocks: &mut [[u8; BLOCK_LEN]; 4]); +} diff --git a/crypto/core/src/hazmat/hazardous_operations.rs b/crypto/core/src/hazmat/hazardous_operations.rs new file mode 100644 index 00000000..61fc35dd --- /dev/null +++ b/crypto/core/src/hazmat/hazardous_operations.rs @@ -0,0 +1,110 @@ +//! [`do_hazardous_operations`]: the scoped override for [`KeyMaterial`](crate::key_material)'s checks. + +use crate::errors::KeyMaterialError; +use crate::key_material::KeyMaterialTrait; + +/// Runs the provided closure within which hazardous operations are allowed. +/// All hazardous operations will return a [`KeyMaterialError::HazardousOperationNotPermitted`] +/// if used outside of this closure. +/// +/// Example usage: +/// +/// ```rust +/// use bouncycastle_core::hazmat::do_hazardous_operations; +/// use bouncycastle_core::key_material::{KeyType, KeyMaterial256, KeyMaterialTrait}; +/// use bouncycastle_core::security_strength::SecurityStrength; +/// +/// // Let's create an all-zero key +/// let mut key = KeyMaterial256::default(); +/// +/// // Let's set a key of all zeroes, which the library would normally force to be +/// // [KeyType::Zeroized], but we want to force it to [KeyType::Seed], which is considered a +/// // hazardous operation. +/// do_hazardous_operations(&mut key, |key| { +/// key.set_bytes_as_type(&[8u8; 32], KeyType::Seed) +/// // note that the closure is required to return Result<(), KeyMaterialError>, +/// // so we can chain [KeyMaterial::set_bytes_as_type], otherwise we would need +/// // to end with Ok(()). +/// }).unwrap(); +/// +/// assert_eq!(key.key_len(), 32); +/// assert_eq!(key.key_type(), KeyType::Seed); +/// ``` +/// +/// ```rust +/// use bouncycastle_core::hazmat::do_hazardous_operations; +/// use bouncycastle_core::key_material::{KeyType, KeyMaterial256, KeyMaterialTrait}; +/// use bouncycastle_core::security_strength::SecurityStrength; +/// +/// // Let's create an all-zero key +/// let mut key = KeyMaterial256::default(); +/// assert_eq!(key.key_type(), KeyType::Zeroized); +/// assert_eq!(key.security_strength(), SecurityStrength::None); +/// +/// // Now we want to tell the library that this all-zero key +/// // is to be used as a 32-byte [KeyType::Seed] at the 256-bit security strength, +/// // which the library will not allow you to do outside of the hazerdous operations closure. +/// do_hazardous_operations(&mut key, |key| { +/// key.set_key_len(32)?; +/// key.set_key_type(KeyType::Seed)?; +/// key.set_security_strength(SecurityStrength::_256bit)?; +/// Ok(()) +/// }).unwrap(); +/// +/// assert_eq!(key.key_type(), KeyType::Seed); +/// assert_eq!(key.security_strength(), SecurityStrength::_256bit); +/// ``` +/// +/// Another common usage of hazardous operations is to get a direct mutable reference to the +/// underlying KeyMaterial byte buffer; for example if you want to copy in key bytes from somewhere else. +/// +/// ```rust +/// use bouncycastle_core::hazmat::do_hazardous_operations; +/// use bouncycastle_core::key_material::{KeyType, KeyMaterial512, KeyMaterialTrait}; +/// use bouncycastle_core::security_strength::SecurityStrength; +/// +/// // In this example, we initialize a KeyMateriol512 (64 bytes) with only 32 bytes of input. +/// let mut key = KeyMaterial512::from_bytes_as_type( +/// &[1u8; 32], +/// KeyType::CryptographicRandom +/// ).unwrap(); +/// assert_eq!(key.key_len(), 32); +/// +/// // Now we want to expand the length to 64 bytes and copy in an additional 32 bytes of key data, +/// // using [KeyMaterial::mut_ref_to_bytes]. +/// let additional_bytes = [2u8; 32]; +/// do_hazardous_operations(&mut key, |key| { +/// key.set_key_len(64)?; +/// key.ref_to_bytes_mut()?[32..].copy_from_slice(&additional_bytes); +/// Ok(()) +/// }).unwrap(); +/// +/// assert_eq!(key.key_len(), 64); +/// // Reading the key bytes via [KeyMateriol::ref_to_bytes] is not a hazardous operation. +/// assert_eq!(key.ref_to_bytes()[..32], [1u8; 32]); +/// assert_eq!(key.ref_to_bytes()[32..], [2u8; 32]); +/// ``` +/// +// Dev note: This is a free function rather than a method on [KeyMaterialTrait] because it is +// generic over the closure type, which would make the trait non-dyn-compatible; the trait is used +// as `&dyn KeyMaterialTrait` elsewhere (e.g. [KeyMaterialTrait::concatenate], [KeyMaterialTrait::equals]). +// The toggle itself lives on the crate-private [KeyMaterialInternalTrait], so external crates cannot +// flip the guard by hand and must go through this scoped wrapper (hence `#[allow(private_bounds)]`). +#[allow(private_bounds)] +pub fn do_hazardous_operations(key: &mut KEY, f: F) -> Result<(), KeyMaterialError> +where + KEY: KeyMaterialTrait + ?Sized, + F: FnOnce(&mut KEY) -> Result<(), KeyMaterialError>, +{ + let allows = key.allows_hazardous_operations(); + + key.allow_hazardous_operations(); + let ret = f(key); + + // to allow nested closures, if this key instance allowed + // before entering, then leave it. + if !allows { + key.drop_hazardous_operations(); + } + ret +} diff --git a/crypto/core/src/hazmat/key_stream.rs b/crypto/core/src/hazmat/key_stream.rs new file mode 100644 index 00000000..67d13231 --- /dev/null +++ b/crypto/core/src/hazmat/key_stream.rs @@ -0,0 +1,65 @@ +//! The [`KeyStream`] trait: a keyed keystream generator. + +use crate::errors::SymmetricCipherError; +use crate::key_material::KeyMaterial; +use crate::traits::Algorithm; + +// Imports needed for docs +#[allow(unused_imports)] +use crate::hazmat::ElectronicCodeBook; +#[allow(unused_imports)] +use crate::key_material::KeyType; +#[allow(unused_imports)] +use crate::traits::{BlockCipherEncryptor, StreamCipherDecryptor, StreamCipherEncryptor}; +// end of imports needed for docs + +/// A keyed keystream generator: the raw primitive under a stream cipher, as +/// [`ElectronicCodeBook`] is the raw primitive under a block cipher mode. +/// +/// It is constructed from a key and init data and XORs successive keystream blocks into whatever +/// it is handed. It has no direction and no init-data policy: generating the nonce, buffering a +/// partly-used block between calls, and refusing a call that would run past the end of the +/// keystream all belong to `bouncycastle_cipher::stream::StreamCipher`, which turns any +/// `KeyStream` into a [`StreamCipherEncryptor`] / [`StreamCipherDecryptor`] pair. +/// +/// Only a keystream that is independent of the data fits: CTR does, CFB does not, since its next +/// keystream block is the encryption of the last ciphertext block. +/// +/// # 🚨 Security Considerations 🚨 +/// [`KeyStream::new`] takes the init data from the caller, so nothing stops a caller reusing a +/// nonce under a key -- which repeats the keystream and reveals the XOR of the two plaintexts -- +/// and nothing stops it running past [`KeyStream::remaining_blocks`]. `StreamCipher` generates +/// the init data and enforces the limit; use it. See the [module docs](crate::hazmat) for the +/// supported uses of the raw trait. +/// +/// Implementors hold the key in a zeroize-on-drop wrapper, as for [`ElectronicCodeBook`]. Any +/// keystream they produce into scratch space of their own is live key material until it has been +/// XORed in, and gets the same treatment. +pub trait KeyStream: + Algorithm + Sized +{ + /// Expands the key and positions the keystream at its first block for `init_data`. + /// + /// # Errors + /// Rejects a key whose [`KeyType`] is not [`KeyType::SymmetricCipherKey`], and one whose + /// security strength is below [`Algorithm::MAX_SECURITY_STRENGTH`], both as a + /// [`SymmetricCipherError::KeyMaterialError`]. + fn new( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ) -> Result; + + /// How many more keystream blocks this value can produce before its keystream would repeat. + /// A keystream with no practical limit returns `u64::MAX`. + fn remaining_blocks(&self) -> u64; + + /// XORs the next `blocks.len()` keystream blocks into `blocks`, in place, and advances past + /// them. A sequence of calls is equivalent to one call over the concatenation; how to batch + /// the blocks is the implementor's decision, as for + /// [`BlockCipherEncryptor::do_encrypt_blocks_inplace`]. + /// + /// Infallible because the caller has already checked `blocks.len()` against + /// [`Self::remaining_blocks`]. Asking for more is a programmer error, and the implementor may + /// panic or repeat keystream. + fn apply_blocks(&mut self, blocks: &mut [[u8; BLOCK_LEN]]); +} diff --git a/crypto/core/src/hazmat/mod.rs b/crypto/core/src/hazmat/mod.rs new file mode 100644 index 00000000..669a54bd --- /dev/null +++ b/crypto/core/src/hazmat/mod.rs @@ -0,0 +1,20 @@ +//! Raw primitives whose safe use is the caller's responsibility. +//! +//! An item lives under a `hazmat` module when it is a correct, tested primitive whose +//! *composition* is the caller's responsibility, or that otherwise carry non-trivial +//! Security Considerations which are the caller's responsibility. +//! +//! Part of the design intention is to allow static code analyzers to easily find and flag +//! such uses with a simple search such as +//! +//! ```text +//! grep -rnE --include='*.rs' 'use .*::hazmat::' +//! ``` + +mod electronic_code_book; +mod hazardous_operations; +mod key_stream; + +pub use electronic_code_book::ElectronicCodeBook; +pub use hazardous_operations::do_hazardous_operations; +pub use key_stream::KeyStream; diff --git a/crypto/core/src/key_material.rs b/crypto/core/src/key_material.rs index 1e2226b8..bba2c91a 100644 --- a/crypto/core/src/key_material.rs +++ b/crypto/core/src/key_material.rs @@ -21,7 +21,7 @@ //! Some conversions, such as converting a key of type RawLowEntropy into a SymmetricCipherKey, will fail unless //! run inside of a [`do_hazardous_operations`] closure, see below. //! -//! # 🚨 Security 🚨 +//! # 🚨 Security Considerations 🚨 //! //! Additional security features: //! * Zeroizes on destruction. @@ -51,7 +51,9 @@ //! See [`do_hazardous_operations`] for documentation and sample code. use crate::errors::{KeyMaterialError, SuspendableError}; -use crate::traits::{RNG, SecurityStrength}; +use crate::hazmat::do_hazardous_operations; +use crate::security_strength::SecurityStrength; +use crate::traits::RNG; use bouncycastle_utils::{ct, min, secret::Secret}; use core::cmp::{Ordering, PartialOrd}; @@ -81,7 +83,8 @@ pub trait KeyMaterialTrait: KeyMaterialInternalTrait { /// Note that even if a [`KeyMaterialError::ActingOnZeroizedKey`] is returned, the object is still populated and usable. /// For example, you could catch it like this: /// ``` - /// use bouncycastle_core::key_material::{KeyMaterial256, KeyType, KeyMaterialTrait, do_hazardous_operations}; + /// use bouncycastle_core::hazmat::do_hazardous_operations; + /// use bouncycastle_core::key_material::{KeyMaterial256, KeyType, KeyMaterialTrait}; /// use bouncycastle_core::key_material::KeyMaterial; /// use bouncycastle_core::errors::KeyMaterialError; /// @@ -677,13 +680,14 @@ impl fmt::Debug for KeyMaterial { /// Internal-use trait holding the low-level hazardous-operations guard toggle. /// -/// These methods are deliberately split out of [`KeyMaterialTrait`] into a private trait so that -/// they are not accessible from outside this module. +/// These methods are deliberately split out of [`KeyMaterialTrait`] into a crate-private trait so +/// that they are not accessible from outside this crate; [`do_hazardous_operations`] is the only +/// way to flip the guard. /// /// This is a supertrait of [`KeyMaterialTrait`], so anything that implements [`KeyMaterialTrait`] /// also implements this. [`KeyMaterialTrait`] therefore stays dyn-compatible (both methods here are /// object-safe), which matters because `Box` is used widely as a return type. -trait KeyMaterialInternalTrait { +pub(crate) trait KeyMaterialInternalTrait { /// Whether this instance is currently allowed to perform potentially hazardous operations. fn allows_hazardous_operations(&self) -> bool; /// Sets this instance to be able to perform potentially hazardous operations such as @@ -696,7 +700,7 @@ trait KeyMaterialInternalTrait { /// and to give static analysis tools an obvious marker that a given KeyMaterial variable warrants /// further inspection. /// - /// Prefer the scoped [`KeyMaterial::do_hazardous_operations`] wrapper, which calls this and + /// Prefer the scoped [`do_hazardous_operations`] wrapper, which calls this and /// [`KeyMaterialInternalTrait::drop_hazardous_operations`] for you so the guard can't be left set. fn allow_hazardous_operations(&mut self); @@ -715,106 +719,3 @@ impl KeyMaterialInternalTrait for KeyMaterial { self.allow_hazardous_operations = false; } } - -/// Runs the provided closure within which hazardous operations are allowed. -/// All hazardous operations will return a [`KeyMaterialError::HazardousOperationNotPermitted`] -/// if used outside of this closure. -/// -/// Example usage: -/// -/// ```rust -/// use bouncycastle_core::key_material::{KeyType, KeyMaterial256, KeyMaterialTrait, do_hazardous_operations}; -/// use bouncycastle_core::traits::SecurityStrength; -/// -/// // Let's create an all-zero key -/// let mut key = KeyMaterial256::default(); -/// -/// // Let's set a key of all zeroes, which the library would normally force to be -/// // [KeyType::Zeroized], but we want to force it to [KeyType::Seed], which is considered a -/// // hazardous operation. -/// do_hazardous_operations(&mut key, |key| { -/// key.set_bytes_as_type(&[8u8; 32], KeyType::Seed) -/// // note that the closure is required to return Result<(), KeyMaterialError>, -/// // so we can chain [KeyMaterial::set_bytes_as_type], otherwise we would need -/// // to end with Ok(()). -/// }).unwrap(); -/// -/// assert_eq!(key.key_len(), 32); -/// assert_eq!(key.key_type(), KeyType::Seed); -/// ``` -/// -/// ```rust -/// use bouncycastle_core::key_material::{KeyType, KeyMaterial256, KeyMaterialTrait, do_hazardous_operations}; -/// use bouncycastle_core::traits::SecurityStrength; -/// -/// // Let's create an all-zero key -/// let mut key = KeyMaterial256::default(); -/// assert_eq!(key.key_type(), KeyType::Zeroized); -/// assert_eq!(key.security_strength(), SecurityStrength::None); -/// -/// // Now we want to tell the library that this all-zero key -/// // is to be used as a 32-byte [KeyType::Seed] at the 256-bit security strength, -/// // which the library will not allow you to do outside of the hazerdous operations closure. -/// do_hazardous_operations(&mut key, |key| { -/// key.set_key_len(32)?; -/// key.set_key_type(KeyType::Seed)?; -/// key.set_security_strength(SecurityStrength::_256bit)?; -/// Ok(()) -/// }).unwrap(); -/// -/// assert_eq!(key.key_type(), KeyType::Seed); -/// assert_eq!(key.security_strength(), SecurityStrength::_256bit); -/// ``` -/// -/// Another common usage of hazardous operations is to get a direct mutable reference to the -/// underlying KeyMaterial byte buffer; for example if you want to copy in key bytes from somewhere else. -/// -/// ```rust -/// use bouncycastle_core::key_material::{KeyType, KeyMaterial512, KeyMaterialTrait, do_hazardous_operations}; -/// use bouncycastle_core::traits::SecurityStrength; -/// -/// // In this example, we initialize a KeyMateriol512 (64 bytes) with only 32 bytes of input. -/// let mut key = KeyMaterial512::from_bytes_as_type( -/// &[1u8; 32], -/// KeyType::CryptographicRandom -/// ).unwrap(); -/// assert_eq!(key.key_len(), 32); -/// -/// // Now we want to expand the length to 64 bytes and copy in an additional 32 bytes of key data, -/// // using [KeyMaterial::mut_ref_to_bytes]. -/// let additional_bytes = [2u8; 32]; -/// do_hazardous_operations(&mut key, |key| { -/// key.set_key_len(64)?; -/// key.ref_to_bytes_mut()?[32..].copy_from_slice(&additional_bytes); -/// Ok(()) -/// }).unwrap(); -/// -/// assert_eq!(key.key_len(), 64); -/// // Reading the key bytes via [KeyMateriol::ref_to_bytes] is not a hazardous operation. -/// assert_eq!(key.ref_to_bytes()[..32], [1u8; 32]); -/// assert_eq!(key.ref_to_bytes()[32..], [2u8; 32]); -/// ``` -/// -// Dev note: This is a free function rather than a method on [KeyMaterialTrait] because it is -// generic over the closure type, which would make the trait non-dyn-compatible; the trait is used -// as `&dyn KeyMaterialTrait` elsewhere (e.g. [KeyMaterialTrait::concatenate], [KeyMaterialTrait::equals]). -// The toggle itself lives on the module-private [KeyMaterialInternalTrait], so external crates cannot -// flip the guard by hand and must go through this scoped wrapper (hence `#[allow(private_bounds)]`). -#[allow(private_bounds)] -pub fn do_hazardous_operations(key: &mut KEY, f: F) -> Result<(), KeyMaterialError> -where - KEY: KeyMaterialTrait + ?Sized, - F: FnOnce(&mut KEY) -> Result<(), KeyMaterialError>, -{ - let allows = key.allows_hazardous_operations(); - - key.allow_hazardous_operations(); - let ret = f(key); - - // to allow nested closures, if this key instance allowed - // before entering, then leave it. - if !allows { - key.drop_hazardous_operations(); - } - ret -} diff --git a/crypto/core/src/lib.rs b/crypto/core/src/lib.rs index a75792dc..3ec84e9f 100644 --- a/crypto/core/src/lib.rs +++ b/crypto/core/src/lib.rs @@ -7,6 +7,7 @@ #![forbid(missing_docs)] pub mod errors; +pub mod hazmat; pub mod key_material; -pub mod suspendable_state; +pub mod security_strength; pub mod traits; diff --git a/crypto/core/src/security_strength.rs b/crypto/core/src/security_strength.rs new file mode 100644 index 00000000..c19743de --- /dev/null +++ b/crypto/core/src/security_strength.rs @@ -0,0 +1,84 @@ +//! Provides the [`SecurityStrength`] struct. + +use crate::errors::SuspendableError; + +/// A general indicator used across the library for marking the security level of a cryptographic primitive, +/// and for tracking the security level of the algorithms that interacted with a given piece of data. +/// For example, if a KDF at the 128-bit security strength is used to produce a 512-bit key, that key +/// will also be tagged as having a 128-bit security strength. +/// +/// Some functions across the library may reject or behave differently based on the security strength +/// of the inputs they are given. For example a `keygen_from_seed()` may reject a seed taged at a lower +/// security strength than the one required by the algorithm, or it may proceed, but lower its own +/// advertised security strength accordingly -- each cryptographic primitive may have additional detail. +// Dev note: The explicit `#[repr(u8)]` discriminants are the stable on-the-wire encoding used by +// `SerializableState` implementations (see the corresponding `TryFrom` impl below). +// If additional strength levels are added in the future, they can be placed into the enum in +// any order, but should use currently unassigned values (unless you're doing this on a MAJOR or MINOR +// release as a breaking change). +#[derive(Eq, PartialEq, PartialOrd, Clone, Copy, Debug)] +#[repr(u8)] +#[non_exhaustive] +pub enum SecurityStrength { + /// + None = 0, + /// + _112bit = 1, + /// + _128bit = 2, + /// + _192bit = 3, + /// + _256bit = 4, +} + +impl TryFrom for SecurityStrength { + type Error = SuspendableError; + + /// Inverse of `self as u8`; rejects unrecognized discriminants with [`SuspendableError::InvalidData`]. + fn try_from(value: u8) -> Result { + Ok(match value { + 0 => Self::None, + 1 => Self::_112bit, + 2 => Self::_128bit, + 3 => Self::_192bit, + 4 => Self::_256bit, + _ => return Err(SuspendableError::InvalidData), + }) + } +} + +impl SecurityStrength { + /// Rounds down to the closest supported security strength. + /// For example, 120-bits is rounded down to 112-bit. + pub const fn from_bits(bits: usize) -> Self { + if bits < 112 { + Self::None + } else if bits < 128 { + Self::_112bit + } else if bits < 192 { + Self::_128bit + } else if bits < 256 { + Self::_192bit + } else { + Self::_256bit + } + } + + /// Rounds down to the closest supported security strength. + /// For example, 15 bytes (120-bits) is rounded down to 112-bit. + pub const fn from_bytes(bytes: usize) -> Self { + Self::from_bits(bytes * 8) + } + + /// Outputs the security strength in bits for easier computation. + pub fn as_int(&self) -> u32 { + match self { + Self::None => 0, + Self::_112bit => 112, + Self::_128bit => 128, + Self::_192bit => 192, + Self::_256bit => 256, + } + } +} diff --git a/crypto/core/src/suspendable_state.rs b/crypto/core/src/suspendable_state.rs deleted file mode 100644 index 47a8e0f0..00000000 --- a/crypto/core/src/suspendable_state.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Helper functions for standardizing serialization and deserialization of stateful objects. - -// todo -- should this move to bouncycastle-utils? - -use crate::errors::SuspendableError; - -/// A semantic library version, ordered by `major`, then `minor`, then `patch`. -/// -/// The field declaration order matters: the derived [`Ord`]/[`PartialOrd`] compare fields -/// lexicographically in declaration order, which is exactly semantic-version precedence. -/// A semantic version can often also take a suffix, e.g. "alpha", "beta", "rc1", etc. -/// We're not going to model that here because it's not useful for versioning serialized states. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct SemVer { - /// - pub major: u8, - /// - pub minor: u8, - /// - pub patch: u8, - // A semantic version can often also take a suffix, e.g. "alpha", "beta", "rc1", etc. - // We're not going to model that here because it's not useful for versioning serialized states. -} - -impl From<[u8; 3]> for SemVer { - fn from(v: [u8; 3]) -> Self { - SemVer { major: v[0], minor: v[1], patch: v[2] } - } -} - -impl From for [u8; 3] { - fn from(v: SemVer) -> Self { - [v.major, v.minor, v.patch] - } -} - -/// Parse a decimal ASCII string (a Cargo version component) into a u8 at compile time. -const fn parse_version_component(s: &str) -> u8 { - let bytes = s.as_bytes(); - let mut result: u8 = 0; - let mut i = 0; - while i < bytes.len() { - let d = bytes[i]; - assert!(d >= b'0' && d <= b'9', "version component must be numeric"); - // A component > 255 overflows u8 and fails the build (SemVer fields are u8 by design). - result = result * 10 + (d - b'0'); - i += 1; - } - result -} - -/// The current library version -- ie the version of the *bouncycastle-core* crate -- at compile time (via Cargo's -/// `CARGO_PKG_VERSION_*` env vars). -/// -/// MAINTAINER NOTE: this single value is the *only* compatibility gate for every serialized state in -/// the workspace (see [`check_lib_ver`]), and the policy accepts any future *patch* on the same -/// major.minor stream. Therefore any change to the on-the-wire layout of *any* suspendable state -- -/// in this crate or in any primitive crate -- MUST bump this crate's **minor** version (never just -/// the patch), otherwise an older build will silently accept and misread a newer, incompatible state. -/// Also keep this crate's version reconciled with the workspace release version so the stamp is -/// meaningful. -pub const LIB_VERSION: SemVer = SemVer { - major: parse_version_component(env!("CARGO_PKG_VERSION_MAJOR")), - minor: parse_version_component(env!("CARGO_PKG_VERSION_MINOR")), - patch: parse_version_component(env!("CARGO_PKG_VERSION_PATCH")), -}; - -#[test] -/// Just to check it visually -fn print_lib_ver() { - println!("LIB_VERSION: {:?}, as bytes: {:?}", LIB_VERSION, <[u8; 3]>::from(LIB_VERSION)); -} - -#[test] -fn test_cmp_lib_ver() { - use core::cmp::Ordering; - - assert!([0, 0, 0] < [0, 0, 1]); - - let cmp = |a: [u8; 3], b: [u8; 3]| SemVer::from(a).cmp(&SemVer::from(b)); - assert_eq!(cmp([0, 2, 1], [1, 1, 1]), Ordering::Less); - assert_eq!(cmp([2, 1, 1], [1, 1, 1]), Ordering::Greater); - assert_eq!(cmp([1, 0, 2], [1, 1, 1]), Ordering::Less); - assert_eq!(cmp([1, 2, 0], [1, 1, 1]), Ordering::Greater); - assert_eq!(cmp([1, 1, 0], [1, 1, 1]), Ordering::Less); - assert_eq!(cmp([1, 1, 2], [1, 1, 1]), Ordering::Greater); - assert_eq!(cmp([1, 1, 1], [1, 1, 1]), Ordering::Equal); -} - -/// Puts the library version into the first three bytes of the state array. -/// -/// Hands back a slice to the same array, starting after the version tag. -pub fn add_lib_ver(state: &mut [u8; SERIALIZED_LEN]) -> &mut [u8] { - state[..3].copy_from_slice(&<[u8; 3]>::from(LIB_VERSION)); - &mut state[3..] -} - -/// A helper for deserializing an object's state -/// -/// The state_out array must have length at least SERIALIZED_LEN - 3. -/// -/// Returns the number of bytes written to state_out, or a [`SuspendableError::IncompatibleVersion`] if -/// the version of the serialized state is earlier than the specified `not_before` version, or -/// is a future MAJOR or MINOR version (but future PATCH versions are ok). -/// -/// Note that for testability, this will always reject if the serialized state contains a version tag -/// of `[0,0,0]`. -/// -/// Hands back a slice to the same array, starting after the version tag. -pub fn check_lib_ver( - state: &[u8; SERIALIZED_LEN], - not_before: Option<[u8; 3]>, -) -> Result<&[u8], SuspendableError> { - // the .unwrap is infallible after the guard check - if state.len() < 3 { - return Err(SuspendableError::InvalidData); - } - let ver_bytes: [u8; 3] = state[..3].try_into().unwrap(); - let ver = SemVer::from(ver_bytes); - - let not_before = SemVer::from(not_before.unwrap_or([0, 0, 0])); - - if ver < not_before { - return Err(SuspendableError::IncompatibleVersion); - }; - // Nothing is ever compatible with [0,0,0] - if ver == SemVer::from([0, 0, 0]) { - return Err(SuspendableError::IncompatibleVersion); - }; - - // Check if state was produced by a later MAJOR or MINOR version; - // a future version on the same patch stream is ok (if not, then we've broken the rules of semantic versioning); - let patch_stream = SemVer::from([LIB_VERSION.major, LIB_VERSION.minor, 255]); - if ver > patch_stream { - return Err(SuspendableError::IncompatibleVersion); - } - - Ok(&state[3..]) -} diff --git a/crypto/core/src/traits.rs b/crypto/core/src/traits.rs index 22652570..4d8ffbec 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -1,7 +1,10 @@ //! Provides simplified abstracted APIs over classes of cryptographic primitives, such as Hash, KDF, etc. +// Objects in this file should be sorted alphabetically, regardless of whether they are a trait, struct, or enum. + use crate::errors::*; use crate::key_material::KeyMaterialTrait; +use crate::security_strength::SecurityStrength; use core::fmt::{Debug, Display}; use core::marker::Sized; @@ -12,69 +15,478 @@ 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: - SymmetricCipher + Sized +/// What the allocating one-shot [`AEADCipherEncryptor::encrypt_detached`] hands back: +/// `(nonce, ciphertext, tag)` +#[cfg(feature = "std")] +pub type AEADEncryptedTuple = + ([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. A decryptor may therefore +/// hold back up to the last `TAG_LEN` bytes it has seen, since until the stream ends they may be +/// the tag; [`SymmetricCipherDecryptor::do_decrypt_out_len`] says exactly how many bytes each +/// call releases. With the tag detached those held-back bytes turn out to be ciphertext, and +/// [`do_decrypt_final_detachedtag_out`](Self::do_decrypt_final_detachedtag_out) decrypts them; with +/// it inline, [`SymmetricCipherDecryptor::do_decrypt_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_decrypt_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_decrypt_final_detachedtag_out`](Self::do_decrypt_final_detachedtag_out) or +/// [`SymmetricCipherDecryptor::do_decrypt_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_detached_out`](Self::decrypt_detached_out), +/// [`decrypt_with_aad_out`](Self::decrypt_with_aad_out) 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. - /// 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( + /// 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. + /// + /// # Errors + /// [`SymmetricCipherError::StateError`] if called with a non-empty `aad` after + /// [`SymmetricCipherDecryptor::do_decrypt_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 the number of plaintext bytes + /// written. The entire output buffer is zeroized before the plaintext is written, so any bytes + /// past that count will be 0. `Ok` is the only thing that makes those bytes -- or anything + /// already released by [`SymmetricCipherDecryptor::do_decrypt_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_decrypt_final_detachedtag_out( + self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; FINAL_LEN], + ) -> Result; + + /// As [`do_decrypt_final_detachedtag_out`](Self::do_decrypt_final_detachedtag_out), returning + /// the final buffer together with the number of leading bytes of it that are plaintext, the + /// shape of [`SymmetricCipherDecryptor::do_decrypt_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. + /// + /// # Errors + /// As [`do_decrypt_final_detachedtag_out`](Self::do_decrypt_final_detachedtag_out). + fn do_decrypt_final_detachedtag( + self, + tag: &[u8; TAG_LEN], + ) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError> { + let mut plaintext = [0u8; FINAL_LEN]; + let data_len = self.do_decrypt_final_detachedtag_out(tag, &mut plaintext)?; + Ok((plaintext, data_len)) + } + + /// An upper bound on the plaintext recovered from `ciphertext_len` bytes of ciphertext with + /// the tag detached, i.e. the buffer [`decrypt_detached_out`](Self::decrypt_detached_out) + /// 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_detached_out_len(ciphertext_len: usize) -> usize { + ciphertext_len + } + + /// One-shot with the tag detached: decrypts `ciphertext` into `plaintext`, which needs + /// [`decrypt_detached_out_len`](Self::decrypt_detached_out_len) bytes, under `nonce` + /// and `aad`, and checks `tag`. Returns the number of plaintext bytes written. The entire + /// output buffer is zeroized before the plaintext is written, so any bytes past that count + /// will be 0. + /// + /// 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::OutputBufferTooSmall`] if `plaintext` is too short, checked + /// before any work is done; otherwise whatever the streaming methods return, including + /// [`do_decrypt_final_detachedtag_out`](Self::do_decrypt_final_detachedtag_out)'s. + fn decrypt_detached_out( key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], 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 [`BlockCipher`] - /// or [`StreamCipher`] as appropriate and feed the nonce into the IV field. - fn aead_encrypt_out( + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + let needed = Self::decrypt_detached_out_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_decrypt_out(ciphertext, plaintext)?; + let mut final_buf = [0u8; FINAL_LEN]; + match dec.do_decrypt_final_detachedtag_out(tag, &mut final_buf) { + Ok(final_len) => { + // Everything held back comes out of `do_decrypt_final_detachedtag_out`, so `written + // + final_len` is the ciphertext length, which `decrypt_detached_out_len` 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) + } + } + } + + /// 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_len`](SymmetricCipherDecryptor::decrypt_out_len) bytes of + /// `plaintext`. Returns the number of plaintext bytes written. The entire output buffer is + /// zeroized before the plaintext is written, so any bytes past that count will be 0. As with + /// every AEAD one-shot, `plaintext` is zeroized when the tag does not verify. + /// + /// # Errors + /// [`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_with_aad_out( key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], aad: &[u8], - plaintext: &[u8], - ciphertext: &mut [u8], - ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError>; - /// All AEAD ciphers will also be either a [`BlockCipher`] or a [`StreamCipher`], 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>; + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + let needed = Self::decrypt_out_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_decrypt_out(ciphertext, plaintext)?; + match dec.do_decrypt_final() { + Ok((last, data_len)) => { + // `decrypt_out_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_detached_out`. + plaintext[..written].fill(0); + Err(e) + } + } + } + #[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 + /// [`decrypt_detached_out`](Self::decrypt_detached_out), 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>; - /// 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( + ) -> Result, SymmetricCipherError> { + let mut plaintext = vec![0u8; Self::decrypt_detached_out_len(ciphertext.len())]; + let written = Self::decrypt_detached_out(key, nonce, aad, ciphertext, tag, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } + + #[cfg(feature = "std")] + /// One-shot, allocating, over the inline `ciphertext || tag` layout with associated data: as + /// [`decrypt_with_aad_out`](Self::decrypt_with_aad_out), 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], - tag: &[u8; TAG_LEN], - plaintext: &mut [u8], - ) -> Result; - /// All AEAD ciphers will also be either a [`BlockCipher`] or a [`StreamCipher`], 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>; + ) -> Result, SymmetricCipherError> { + let mut plaintext = vec![0u8; Self::decrypt_out_len(ciphertext.len())]; + let written = Self::decrypt_with_aad_out(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 +/// +/// * **SymmetricCipher: `ciphertext || tag`**: The inherited [`SymmetricCipherEncryptor`] methods +/// allow a caller to use an AEAD cipher, with the added security of the authentication, without +/// concerning themselves with the details of the AEAD interface. +/// Specifically, there is no way to provide associated data, and +/// [`SymmetricCipherEncryptor::do_encrypt_final`] appends the tag to the ciphertext, so the +/// output is `ciphertext || tag`. +/// +/// * **AEADCipher: `(ciphertext, tag)`**: The methods ending in `_detached` hand the tag back separately, +/// for callers whose protocol carries it in a separate field. +/// +/// # Associated data +/// +/// An AEAD can additionally authenticate data it does not encrypt -- called additional authenticated data (AAD), +/// or sometimes associated data -- typically a header that has to travel in the clear but must still +/// be protected against tampering. Every AEAD construction absorbs that AAD *before* the plaintext. +/// This leads to a stateful API flow: +/// +/// * [`do_encrypt_init`](SymmetricCipherEncryptor::do_encrypt_init) constructs the instance. +/// * [`do_update_aad`](Self::do_update_aad) may be called any number of times, including zero if +/// there is no AAD. +/// * The first [`do_encrypt_out`](SymmetricCipherEncryptor::do_encrypt_out) switches to encrypting, +/// after which additional calls to `do_update_aad` will fail with a +/// [`SymmetricCipherError::StateError`]. +/// +/// (An empty `aad` slice is a no-op and is accepted at any point.) +/// +/// # 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 supply one, though such an API may exist on the underlying +/// primitive. +/// +/// # A cipher may buffer +/// +/// Some AEADs release each ciphertext byte as soon as they see the plaintext byte; others hold +/// part of the input back, until a block is complete or until they can tell whether trailing +/// bytes are the tag. So a call to [`do_encrypt_out`](SymmetricCipherEncryptor::do_encrypt_out) +/// may produce less output than input, or none, which can be told in one of two ways: +/// +/// * The `Ok(usize)` that `do_encrypt_out` returns is `0`. +/// * Prior to the call, call [`do_encrypt_out_len`](SymmetricCipherEncryptor::do_encrypt_out_len) +/// to see how much output will be produced for the given amount of input. Doing it this way has +/// the advantage of being able to correctly size the output buffer for a subsequent +/// [`do_encrypt_out`](SymmetricCipherEncryptor::do_encrypt_out) call. +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_encrypt_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_encrypt_out`] -- see the trait docs for why the AAD comes + /// first. An implementor whose AAD buffer has a fixed capacity 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_decrypt_final_detachedtag_out`]. + /// + /// `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. + /// The entire output buffer is zeroized before the ciphertext is written, so any bytes past + /// the returned count will be 0. + fn do_encrypt_final_detachedtag_out( + self, + ciphertext: &mut [u8; FINAL_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError>; + + /// As [`do_encrypt_final_detachedtag_out`](Self::do_encrypt_final_detachedtag_out), returning + /// the final buffer, the number of leading bytes of it that are ciphertext, and the tag -- the + /// shape of [`SymmetricCipherEncryptor::do_encrypt_final`] with the tag alongside. Provided + /// over the `_out` form, the other way round from the base trait's pair; see + /// [`AEADCipherDecryptor::do_decrypt_final_detachedtag`]. + fn do_encrypt_final_detachedtag( + self, + ) -> Result<([u8; FINAL_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + let mut ciphertext = [0u8; FINAL_LEN]; + let (out_len, tag) = self.do_encrypt_final_detachedtag_out(&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_detached_out`](Self::encrypt_detached_out) 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_detached_out_len(plaintext_len: usize) -> usize { + plaintext_len + } + + /// One-shot with the tag detached: encrypts `plaintext` into `ciphertext`, which needs + /// [`encrypt_detached_out_len`](Self::encrypt_detached_out_len) bytes, authenticating `aad` + /// along with it under a fresh nonce. Returns the generated nonce, the number of bytes + /// written, and the tag. The entire output buffer is zeroized before the ciphertext is + /// written, so any bytes past that count will be 0. + /// + /// Provided as `do_encrypt_init`, one `do_update_aad`, one `do_update_out` and + /// `do_encrypt_final_detachedtag_out`. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked + /// before any work is done; otherwise whatever the streaming methods return. + fn encrypt_detached_out( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext.fill(0); + let needed = Self::encrypt_detached_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_encrypt_out(plaintext, ciphertext)?; + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final_detachedtag_out(&mut final_buf)?; + // Implementors that hold plaintext back must override `encrypt_detached_out_len` 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)) + } + + #[cfg(feature = "std")] + /// One-shot, allocating, with the tag detached: as + /// [`encrypt_detached_out`](Self::encrypt_detached_out), returning the ciphertext as a + /// `Vec`. Only available with the `std` feature. + fn encrypt_detached( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ) -> Result, SymmetricCipherError> { + let mut ciphertext = vec![0u8; Self::encrypt_detached_out_len(plaintext.len())]; + let (nonce, written, tag) = + Self::encrypt_detached_out(key, aad, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((nonce, ciphertext, tag)) + } + + /// As [`encrypt_detached_out`](Self::encrypt_detached_out), but sources randomness from the + /// provided RNG. + fn encrypt_detached_rng_out( + key: &KeyMaterial, + rng: &mut dyn RNG, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext.fill(0); + let needed = Self::encrypt_detached_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_encrypt_out(plaintext, ciphertext)?; + let mut final_buf = [0u8; FINAL_LEN]; + let (final_len, tag) = enc.do_encrypt_final_detachedtag_out(&mut final_buf)?; + // As in `encrypt_detached_out`. + 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. The + /// entire output buffer is zeroized before the ciphertext is written, so any bytes past that + /// count will be 0. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked + /// before any work is done; otherwise whatever the streaming methods return. + fn encrypt_with_aad_out( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + ciphertext.fill(0); + 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_encrypt_out(plaintext, ciphertext)?; + let (last, last_len) = enc.do_encrypt_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)) + } + + #[cfg(feature = "std")] + /// One-shot, allocating, into the inline `ciphertext || tag` layout with associated data: as + /// [`encrypt_with_aad_out`](Self::encrypt_with_aad_out), 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, + aad: &[u8], + plaintext: &[u8], + ) -> Result<([u8; NONCE_LEN], Vec), SymmetricCipherError> { + let mut ciphertext = vec![0u8; Self::encrypt_out_len(plaintext.len())]; + let (nonce, written) = Self::encrypt_with_aad_out(key, aad, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((nonce, ciphertext)) + } + + /// As [`encrypt_with_aad_out`](Self::encrypt_with_aad_out), but sources randomness from the + /// provided RNG: [`SymmetricCipherEncryptor::encrypt_rng_out`] with an `aad`. + fn encrypt_with_aad_rng_out( + key: &KeyMaterial, + rng: &mut dyn RNG, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + ciphertext.fill(0); + 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_encrypt_out(plaintext, ciphertext)?; + let (last, last_len) = enc.do_encrypt_final()?; + // As in `encrypt_with_aad_out`. + ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); + Ok((nonce, written + last_len)) + } } /// Metadata about a cryptographic algorithm. @@ -95,73 +507,191 @@ pub trait AlgorithmOID { const OID_DER: &'static [u8]; } -/// The basic functions of a block cipher. -/// This trait allows for a block cipher to generate initialization data, such as an Initialization Vector (IV) or Counter (CTR) -/// which is not technically part of the ciphertext, but must be transmitted along with the ciphertext in order for the -/// recipient to perform successful decryption. The length of the initialization data is specified by the implementing struct -/// via the `INIT_DATA_LEN` constant. -/// In order for these one-shot APIs to be usable securely in all contexts, the init data will be generated -/// securely by the block cipher implementation and returned along with the ciphertext, and there is no API for the -/// user to provide the init data. If you require this functionality, see the documentation for the underlying implementation. -pub trait BlockCipher: - SymmetricCipher + Sized +/// The decryption half of a block cipher's streaming API; see [`BlockCipherEncryptor`], whose +/// notes on in-place operation, compile-time lengths and the `Result` all apply here too. +pub trait BlockCipherDecryptor< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +>: Algorithm + Sized { - /// Constructor that begins a flow of the streaming API for encrypting one block at a time. - /// Allows for the implementation to return init data such as an IV which is generated prior to encrypting the first block. - fn do_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; - /// Encrypts a single block of plaintext. - fn do_encrypt_block( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Encrypts a single block of plaintext and writes the ciphertext to the provided buffer. - fn do_encrypt_block_out( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ciphertext: &mut [u8; BLOCK_LEN], - ) -> Result; - /// Encrypts the final block of plaintext. - fn do_encrypt_final( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Encrypts the final block of plaintext and writes the ciphertext to the provided buffer. - fn do_encrypt_final_out( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ciphertext: &mut [u8; BLOCK_LEN], - ) -> Result; - /// Constructor that begins a flow of the streaming API for decryption one block at a time. + /// Begins a streaming decryption flow from the init data returned by [`BlockCipherEncryptor::do_encrypt_init`]. fn do_decrypt_init( key: &KeyMaterial, init_data: &[u8; INIT_DATA_LEN], ) -> Result; - /// Decrypts a single block of ciphertext. - fn do_decrypt_block( - &mut self, - ciphertext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Decrypts a single block of ciphertext and writes the plaintext to the provided buffer. - fn do_decrypt_block_out( + /// The implementor hook: decrypts consecutive whole blocks in place. See + /// [`BlockCipherEncryptor::do_encrypt_blocks_inplace`]; callers should normally use the flat + /// [`BlockCipherDecryptor::do_decrypt_inplace`] instead. Returns the number of bytes written, which is + /// always `blocks.len() * BLOCK_LEN` since a block cipher mode never changes the length of its + /// data, but the count is still returned for consistency with the rest of the library's + /// output-buffer APIs. + fn do_decrypt_blocks_inplace( &mut self, - ciphertext: &[u8; BLOCK_LEN], - plaintext: &mut [u8; BLOCK_LEN], + blocks: &mut [[u8; BLOCK_LEN]], ) -> Result; - /// Decrypts the final block of ciphertext. - /// This is the decryption counterpart to [`BlockCipher::do_encrypt_final`] and is where an - /// implementation validates and strips any padding (or otherwise finalizes the flow). - fn do_decrypt_final( + + /// Streaming: decrypts `LEN` bytes, a whole number of blocks, in place. `LEN % BLOCK_LEN == 0` + /// is checked at compile time, exactly as for [`BlockCipherEncryptor::do_encrypt_inplace`]. + /// Returns the number of bytes written; see [`Self::do_decrypt_blocks_inplace`]. + fn do_decrypt_inplace( &mut self, - ciphertext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Decrypts the final block of ciphertext and writes the plaintext to the provided buffer. - fn do_decrypt_final_out( + data: &mut [u8; LEN], + ) -> Result { + const { + assert!( + LEN.is_multiple_of(BLOCK_LEN), + "length must be a whole number of BLOCK_LEN-byte blocks" + ) + }; + // The remainder is provably empty (asserted above) and ignored. + let (blocks, _) = data.as_chunks_mut::(); + self.do_decrypt_blocks_inplace(blocks) + } + + /// One-shot: decrypts `LEN` bytes in place from the given init data. `LEN % BLOCK_LEN == 0` is + /// checked at compile time exactly as for [`BlockCipherEncryptor::encrypt_inplace`]. Returns the + /// number of bytes written; see [`Self::do_decrypt_blocks_inplace`]. + fn decrypt_inplace( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + data: &mut [u8; LEN], + ) -> Result { + Self::do_decrypt_init(key, init_data)?.do_decrypt_inplace(data) + } +} + +/// The encryption half of a block cipher's API. +/// +/// Strictly block-aligned: whole blocks in, whole +/// blocks out, no finalization step. Padding of non-block-aligned data is handled by a separate layer +/// (`PaddedBlockCipherEncryptor` / `PaddedBlockCipherDecryptor`) built on top of this trait. +/// +/// Encryption and decryption are separate traits so that a policy can permit decryption of existing +/// data while forbidding new encryptions. +/// +/// This trait allows for a block cipher to generate initialization data, such as an Initialization +/// Vector (IV) or Counter (CTR) which is not technically part of the ciphertext, but must be +/// transmitted along with the ciphertext in order for the recipient to perform successful decryption. +/// The length of the initialization data is specified by the implementing struct via the +/// `INIT_DATA_LEN` constant. +/// +/// In order for these APIs to be usable securely in all contexts, the init data will be generated +/// securely by the block cipher implementation and returned along with the ciphertext, and there is no API for the +/// user to provide the init data to the encryptor. +/// If you require this functionality, see the documentation for the underlying implementation. +/// +/// # Everything is in place +/// +/// Every data method here transforms its buffer in place: the plaintext goes in, the ciphertext +/// comes out in the same bytes. A block cipher mode never changes the length of its data, so a +/// separate output buffer would only ever be a copy, and a copy of plaintext is one more thing to +/// scrub. Callers that need to keep the plaintext copy it first. +/// +/// # Lengths are checked at compile time +/// +/// Every buffer is a `[u8; LEN]`, and `LEN % BLOCK_LEN == 0` is checked by an inline `const` +/// assertion when the method is instantiated: a misaligned length is a compile error at the call +/// site, not a runtime `Err`, which is why there is no length variant of [`SymmetricCipherError`] +/// here. Data whose length is only known at run time is fed in block by block, or through the +/// padding layer. +/// +/// # Why the data methods still return `Result` +/// +/// Nothing about the buffer can go wrong, and a constructed value is always ready to use, so a +/// mode like CBC never returns `Err` from them. The `Result` is for modes with a per-initialization +/// data limit -- a counter-based mode must refuse to encrypt past the point where its counter would +/// repeat -- which a streaming API cannot check any earlier than the call that would cross it. +pub trait BlockCipherEncryptor< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +>: Algorithm + Sized +{ + /// Begins a streaming encryption flow, returning the generated init data (e.g. IV). + /// Sources randomness from the library's default OS-backed RNG. + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + /// As [`BlockCipherEncryptor::do_encrypt_init`], but sources randomness from the provided RNG. + /// + /// # Panics + /// An implementation that generates no init data -- `INIT_DATA_LEN == 0`, as in ECB -- must + /// panic here rather than ignore `rng` and succeed. There is no randomness for it to consume, + /// so a caller reaching for this constructor has mistaken the cipher for a randomized one, and + /// quietly returning a deterministic encryptor would leave that mistake undetected. This is a + /// programmer error, not bad input, so it is a panic rather than a + /// [`SymmetricCipherError`]. Implementations with `INIT_DATA_LEN > 0` must draw their init + /// data from `rng` and must not panic. + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + /// The implementor hook: encrypts consecutive whole blocks in place. A sequence of calls is + /// equivalent to one call over the concatenation. + /// + /// This is the only method an implementor writes besides the two `_init` constructors; the + /// block shape is what guarantees it never sees a partial block. It takes a slice rather than + /// a `[[u8; BLOCK_LEN]; N]` array because every whole number of blocks is valid, so there is + /// no length invariant for a const parameter to carry, and because how to batch the blocks -- + /// singly, in pairs, in fours -- is the mode's decision, not the caller's: a mode whose + /// permutation processes several blocks at once (CBC decryption, CTR) chunks the slice itself. + /// Callers should normally use the flat [`BlockCipherEncryptor::do_encrypt_inplace`] instead. + /// Returns the number of bytes written, which is always `blocks.len() * BLOCK_LEN` since a + /// block cipher mode never changes the length of its data, but the count is still returned for + /// consistency with the rest of the library's output-buffer APIs. + fn do_encrypt_blocks_inplace( &mut self, - ciphertext: &[u8; BLOCK_LEN], - plaintext: &mut [u8; BLOCK_LEN], + blocks: &mut [[u8; BLOCK_LEN]], ) -> Result; + + /// Streaming: encrypts `LEN` bytes, a whole number of blocks, in place. A sequence of calls + /// is equivalent to one call over the concatenation. Returns the number of bytes written; see + /// [`Self::do_encrypt_blocks_inplace`]. + /// + /// `LEN % BLOCK_LEN == 0` is checked **at compile time**; see the trait docs. The whole buffer + /// then goes to [`BlockCipherEncryptor::do_encrypt_blocks_inplace`] in one call. + fn do_encrypt_inplace( + &mut self, + data: &mut [u8; LEN], + ) -> Result { + const { + assert!( + LEN.is_multiple_of(BLOCK_LEN), + "length must be a whole number of BLOCK_LEN-byte blocks" + ) + }; + // The remainder is provably empty (asserted above) and ignored. + let (blocks, _) = data.as_chunks_mut::(); + self.do_encrypt_blocks_inplace(blocks) + } + + /// One-shot: encrypts `LEN` bytes in place under a fresh init, and returns the number of + /// bytes written (see [`Self::do_encrypt_blocks_inplace`]) alongside the generated init data. + /// `LEN % BLOCK_LEN == 0` is checked **at compile time**; see the trait docs. + fn encrypt_inplace( + key: &KeyMaterial, + data: &mut [u8; LEN], + ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init(key)?; + let written = enc.do_encrypt_inplace(data)?; + Ok((written, init_data)) + } + /// As [`BlockCipherEncryptor::encrypt_inplace`], but sources randomness from the provided RNG. + /// + /// # Panics + /// Provided over [`do_encrypt_init_rng`](Self::do_encrypt_init_rng), so it panics in exactly + /// the cases that does: an implementation with `INIT_DATA_LEN == 0`, which has no randomness + /// to consume. See that method for why. + fn encrypt_rng_inplace( + key: &KeyMaterial, + rng: &mut dyn RNG, + data: &mut [u8; LEN], + ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; + let written = enc.do_encrypt_inplace(data)?; + Ok((written, init_data)) + } } /// A hash function is a cryptographic primitive that takes an input of any length and produces a fixed-size output. @@ -170,11 +700,45 @@ pub trait BlockCipher(..)`. +/// 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. @@ -210,9 +774,20 @@ pub trait Hash: Algorithm + Default { fn do_final_out(self, output: &mut [u8]) -> usize; /// The same as [`Hash::do_final`], but allows for supplying a partial byte as the last input. - /// The `num_bits` message bits are taken from the least significant bits of - /// `partial_byte`, in order (bit 0 of `partial_byte` is the first message bit). This is the - /// FIPS 202 Appendix B.1 convention and is used uniformly for every hash family in this library. + /// + /// 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 bits are placed "commencing with the leading bit ... in bits 8 to 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", X.690 s. 8.6.2.2) are ignored. + /// So for a BIT STRING whose initial octet is `unused` (1..=7), pass its final content octet with + /// `num_bits = 8 - unused`. The convention is the same for every hash family in this library; + /// implementations whose native bit order differs (SHA-3, which absorbs a byte LSB-first per + /// FIPS 202 Appendix B.1) convert internally. + /// + /// Note on test vectors: the NIST CAVP SHAVS (SHA-2) bit-oriented files pack trailing bits + /// left-justified and can be passed here directly; the SHA3VS files use the FIPS 202 B.1 packing + /// (first bit in the LSB) and must be bit-reversed (`u8::reverse_bits`) first. + /// /// 0 is a valid value and means the message ends on a byte boundary (equivalent to [`Hash::do_final`]). /// `num_bits` must be in `0..=7`; larger values return [`HashError::InvalidLength`]. fn do_final_partial_bits(self, partial_byte: u8, num_bits: usize) @@ -544,6 +1119,34 @@ pub trait MAC: Sized { fn max_security_strength(&self) -> SecurityStrength; } +/// A block padding scheme, used to extend arbitrary-length data to a whole number of blocks so that it +/// can be processed by a [`BlockCipherEncryptor`]. Implementations are pure functions of the block +/// contents: no key, no state. +/// +/// Only the final, partial block of a message is ever padded; the padding layer sitting between the +/// caller and the block cipher is responsible for routing whole blocks straight through. +pub trait BlockCipherPadding { + /// Whether the scheme appends a whole block of padding to data that is already a whole number + /// of blocks. `true` for a scheme like PKCS7, which must always add at least one byte so that + /// unpadding is unambiguous; a caller then finishes an aligned message with `pad(block, 0)`. + /// `false` for a scheme that never adds bytes (`NoPadding`): an aligned message is finished with + /// no final block, and `pad` is called only for a partial one -- where such a scheme errors. + const ALWAYS_PADS: bool; + /// Pads `block` in place: bytes `0..data_len` are data and are left untouched, bytes + /// `data_len..BLOCK_LEN` are overwritten with padding. `data_len` must be less than `BLOCK_LEN` + /// (a full block of data requires a whole additional block of padding, which the caller supplies + /// as `data_len = 0` -- only when [`ALWAYS_PADS`](Self::ALWAYS_PADS) is `true`). + /// + /// # Errors + /// [`PaddingError::DataLengthTooLong`] if `data_len >= BLOCK_LEN`; + /// [`PaddingError::PaddingNotPermitted`] from a scheme that adds no bytes and was asked to. + fn pad(block: &mut [u8; BLOCK_LEN], data_len: usize) -> Result<(), PaddingError>; + /// Returns the number of data bytes in a padded `block`, or [`PaddingError::InvalidPadding`]. + /// Implementations must run in constant time with respect to the block contents, so that a + /// decryptor built on them does not leak a padding oracle. + fn unpad(block: &[u8; BLOCK_LEN]) -> Result; +} + /// Pre-Hashed Signature Verifier is an extension to [`SignatureVerifier`] that adds functionality specific to signature /// primatives that can operate on a pre-hashed message instead of the full message. pub trait PHSignatureVerifier< @@ -654,87 +1257,6 @@ pub trait RNG { fn security_strength(&self) -> SecurityStrength; } -/// A general indicator used across the library for marking the security level of a cryptographic primitive, -/// and for tracking the security level of the algorithms that interacted with a given piece of data. -/// For example, if a KDF at the 128-bit security strength is used to produce a 512-bit key, that key -/// will also be tagged as having a 128-bit security strength. -/// -/// Some functions across the library may reject or behave differently based on the security strength -/// of the inputs they are given. For example a `keygen_from_seed()` may reject a seed taged at a lower -/// security strength than the one required by the algorithm, or it may proceed, but lower its own -/// advertised security strength accordingly -- each cryptographic primitive may have additional detail. -// Dev note: The explicit `#[repr(u8)]` discriminants are the stable on-the-wire encoding used by -// `SerializableState` implementations (see the corresponding `TryFrom` impl below). -// If additional strength levels are added in the future, they can be placed into the enum in -// any order, but should use currently unassigned values (unless you're doing this on a MAJOR or MINOR -// release as a breaking change). -#[derive(Eq, PartialEq, PartialOrd, Clone, Copy, Debug)] -#[repr(u8)] -#[non_exhaustive] -pub enum SecurityStrength { - /// - None = 0, - /// - _112bit = 1, - /// - _128bit = 2, - /// - _192bit = 3, - /// - _256bit = 4, -} - -impl TryFrom for SecurityStrength { - type Error = SuspendableError; - - /// Inverse of `self as u8`; rejects unrecognized discriminants with [`SuspendableError::InvalidData`]. - fn try_from(value: u8) -> Result { - Ok(match value { - 0 => Self::None, - 1 => Self::_112bit, - 2 => Self::_128bit, - 3 => Self::_192bit, - 4 => Self::_256bit, - _ => return Err(SuspendableError::InvalidData), - }) - } -} - -impl SecurityStrength { - /// Rounds down to the closest supported security strength. - /// For example, 120-bits is rounded down to 112-bit. - pub fn from_bits(bits: usize) -> Self { - if bits < 112 { - Self::None - } else if bits < 128 { - Self::_112bit - } else if bits < 192 { - Self::_128bit - } else if bits < 256 { - Self::_192bit - } else { - Self::_256bit - } - } - - /// Rounds down to the closest supported security strength. - /// For example, 15 bytes (120-bits) is rounded down to 112-bit. - pub fn from_bytes(bytes: usize) -> Self { - Self::from_bits(bytes * 8) - } - - /// Outputs the security strength in bits for easier computation. - pub fn as_int(&self) -> u32 { - match self { - Self::None => 0, - Self::_112bit => 112, - Self::_128bit => 128, - Self::_192bit => 192, - Self::_256bit => 256, - } - } -} - // todo: could the public and private key types impl Into> and From> // todo: that automatically call the encode and from_bytes() ? @@ -788,16 +1310,16 @@ pub trait SignatureVerifier< fn verify(pk: &PK, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8]) -> Result<(), SignatureError>; /// streaming verification API - fn verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result; + fn do_verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result; // todo: make this a AsRef<[u8]> ? /// Update the verifier with the next chunk of data. /// This can be called multiple times. - fn verify_update(&mut self, msg_chunk: &[u8]); + fn do_verify_update(&mut self, msg_chunk: &[u8]); /// On success, returns Ok(()) /// On failure, returns Err([`SignatureError::SignatureVerificationFailed`]); may also return other types of [`SignatureError`] as appropriate (such as for invalid-length inputs). - fn verify_final(self, sig: &[u8]) -> Result<(), SignatureError>; + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError>; } /// A digital signature algorithm is defined as a set of three operations: @@ -860,70 +1382,122 @@ pub trait Signer, const SK_LEN: usize, const SIG /* streaming signing API */ /// Initialize a signer for streaming mode with the provided private key. - fn sign_init(sk: &SK, ctx: Option<&[u8]>) -> Result; + fn do_sign_init(sk: &SK, ctx: Option<&[u8]>) -> Result; // todo: make this a AsRef<[u8]> ? /// Update the signer with the next chunk of data. /// This can be called multiple times. - fn sign_update(&mut self, msg_chunk: &[u8]); + fn do_sign_update(&mut self, msg_chunk: &[u8]); /// Complete the signing operation. Consumes self. - fn sign_final(self) -> Result<[u8; SIG_LEN], SignatureError>; + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError>; /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. /// The entire output buffer is zeroized before the signature is written. - fn sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result; + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result; } -/// The basic functions of a stream cipher, which differ from those of a block cipher only in that -/// a stream cipher is assumed to have no underlying block size tied to the implementation, and so the caller gets to specify -/// the block size for the streaming APIs. -pub trait StreamCipher: - SymmetricCipher + Sized +/// The decryption half of a stream cipher's streaming API; see [`StreamCipherEncryptor`], whose +/// notes on in-place operation, arbitrary lengths and the `Result` all apply here too. +pub trait StreamCipherDecryptor: + SymmetricCipherDecryptor { - /// Constructor that begins a flow of the streaming API for encrypting one block at a time. - /// Allows for the implementation to return init data such as an IV which is generated prior to encrypting the first block. - fn do_stream_encrypt_init( - key: &KeyMaterial, - ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; - /// Encrypts a single block of plaintext. - fn do_stream_encrypt_block( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Encrypts a single block of plaintext and writes the ciphertext to the provided buffer. - fn do_stream_encrypt_block_out( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ciphertext: &mut [u8; BLOCK_LEN], - ) -> Result; - /// Encrypts the final block of plaintext. - fn do_stream_encrypt_final( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Encrypts the final block of plaintext and writes the ciphertext to the provided buffer. - fn do_stream_encrypt_final_out( - &mut self, - plaintext: &[u8; BLOCK_LEN], - ciphertext: &mut [u8; BLOCK_LEN], - ) -> Result; - /// Constructor that begins a flow of the streaming API for decryption one block at a time. - fn do_stream_decrypt_init( + /// Streaming: decrypts `data`, of any length, in place. A sequence of calls is equivalent to + /// one call over the concatenation, whatever the chunking, exactly as for + /// [`StreamCipherEncryptor::do_encrypt_inplace`]. Returns the number of bytes written, which is always + /// `data.len()` since a stream cipher never buffers or changes the length of its data, but the + /// count is still returned for consistency with the rest of the library's output-buffer APIs. + fn do_decrypt_inplace(&mut self, data: &mut [u8]) -> Result; + + /// One-shot: decrypts `data` in place from the given init data. Returns the number of bytes + /// written; see [`Self::do_decrypt_inplace`]. + fn decrypt_inplace( key: &KeyMaterial, init_data: &[u8; INIT_DATA_LEN], - ) -> Result; - /// Decrypts a single block of ciphertext. - fn do_stream_decrypt_block( - &mut self, - ciphertext: &[u8; BLOCK_LEN], - ) -> Result<[u8; BLOCK_LEN], SymmetricCipherError>; - /// Decrypts a single block of ciphertext and writes the plaintext to the provided buffer. - fn do_stream_decrypt_block_out( - &mut self, - ciphertext: &[u8; BLOCK_LEN], - plaintext: &mut [u8; BLOCK_LEN], - ) -> Result; + data: &mut [u8], + ) -> Result { + Self::do_decrypt_init(key, init_data)?.do_decrypt_inplace(data) + } +} + +/// The encryption half of a stream cipher's streaming API: the in-place view of a +/// [`SymmetricCipherEncryptor`] with `FINAL_LEN = 0`. +/// A stream cipher applies its keystream byte by byte, so the data methods take a +/// `&mut [u8]` of any length, and there is no finalization step. +/// +/// Encryption and decryption are separate traits so that policy can permit decryption of an +/// existing data while forbidding new encryptions. +/// +/// # Init data (a nonce or IV) +/// +/// Init data (a nonce or IV) is generated securely by the implementation in the constructor and +/// returned for transmission alongside the ciphertext; there is no API for the user to supply it. +/// +/// # Everything is in place +/// +/// Since a stream cipher, by definition, hos no ciphertext expansion, every data method here +/// transforms its buffer in place: the plaintext goes in, the ciphertext +/// comes out in the same buffer. +/// Callers that need to keep the plaintext copy it first, or use the supertrait's `encrypt_out`. +/// +/// # Any length is valid +/// +/// The data is a `&mut [u8]` because every length is valid, including zero. +/// How the keystream is produced internally -- in 64-byte blocks, in words, a bit +/// at a time -- is the cipher's business and must not leak to the caller. +pub trait StreamCipherEncryptor: + SymmetricCipherEncryptor +{ + /// Streaming: encrypts `data`, of any length, in place; on top of the generic APIs offered by + /// [`SymmetricCipherEncryptor`], a stream cipher can offer `_inplace()` versions since a stream + /// cipher's ciphertext always has exactly the same length as its plaintext. + /// + /// A sequence of calls is equivalent to + /// one call over the concatenation, whatever the chunking. Returns the number of bytes + /// written, which is always `data.len()` since a stream cipher never buffers or changes the + /// length of its data, but the count is still returned for consistency with the rest of the + /// library's output-buffer APIs. + /// + /// # Errors + /// [`SymmetricCipherError::DataLimitExceeded`] if this call would run past the cipher's + /// per-initialization data limit. Nothing about the buffer can go wrong, and a constructed + /// value is always ready to use; the `Result` is there because a counter-driven keystream must + /// refuse to run past the point where its counter would wrap and the keystream repeat, and a + /// streaming API cannot check that any earlier than the call that would cross it. + fn do_encrypt_inplace(&mut self, data: &mut [u8]) -> Result; + + /// One-shot: encrypts `data` in place under a fresh init, and returns the number of bytes + /// written (see [`Self::do_encrypt_inplace`]) alongside the generated init data. + /// + /// # Errors + /// Whatever [`SymmetricCipherEncryptor::do_encrypt_init`] or [`Self::do_encrypt_inplace`] returns. + fn encrypt_inplace( + key: &KeyMaterial, + data: &mut [u8], + ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init(key)?; + let written = StreamCipherEncryptor::do_encrypt_inplace(&mut enc, data)?; + Ok((written, init_data)) + } + /// As [`StreamCipherEncryptor::encrypt_inplace`], but sources randomness from the provided + /// RNG. + /// + /// # Panics + /// Provided over [`SymmetricCipherEncryptor::do_encrypt_init_rng`], so it panics in exactly + /// the cases that does: an implementation with `INIT_DATA_LEN == 0`, which has no randomness + /// to consume. See that method for why. + /// + /// # Errors + /// Whatever [`SymmetricCipherEncryptor::do_encrypt_init_rng`] or [`Self::do_encrypt_inplace`] returns. + fn encrypt_rng_inplace( + key: &KeyMaterial, + rng: &mut dyn RNG, + data: &mut [u8], + ) -> Result<(usize, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; + let written = StreamCipherEncryptor::do_encrypt_inplace(&mut enc, data)?; + Ok((written, init_data)) + } } /// Allows a stateful object to suspend its operation by serializing its state into a byte array @@ -989,137 +1563,536 @@ pub trait SuspendableKeyed: Sized { ) -> Result; } -/// The basic one-shot encrypt and decrypt that all types of symmetric ciphers must implement. -/// These are meant to be simple, easy to use, secure, and fool-proof APIs, but they may result in -/// ciphertexts that are incompatible with other implementations as ciphers in more complex modes, such -/// as AEADs or stream ciphers may need to stick extra data either at the beginning or end of the ciphertext. -/// See the documentation of the underlying implementation for more details. -pub trait SymmetricCipher: Algorithm { +/// The decryption half of a symmetric cipher's arbitrary-length API. See +/// [`SymmetricCipherEncryptor`] for the shape of the API and the meaning of `FINAL_LEN`; this is +/// its mirror image, and the two are implemented by paired types. +/// +/// Decryption is not the exact mirror of encryption in one respect: the last `FINAL_LEN` bytes a +/// decryptor releases may be only partly data. A padding scheme's final block carries +/// `data_len < BLOCK_LEN` bytes of plaintext and the rest padding, and an authenticated cipher may +/// release nothing at all once it has checked the tag. So +/// [`do_decrypt_final`](Self::do_decrypt_final) returns the buffer *and* how much of it is data, +/// and the one-shot length helper is an upper bound rather than an exact count. +/// +/// The one-shot [`decrypt_out`](Self::decrypt_out) is provided over the streaming methods, as is +/// the allocating [`decrypt`](Self::decrypt) behind the `std` feature. An implementor writes only +/// [`do_decrypt_init`](Self::do_decrypt_init), [`update_out_len`](Self::do_decrypt_out_len), +/// [`do_update_out`](Self::do_decrypt_out), [`do_decrypt_final`](Self::do_decrypt_final) and +/// [`decrypt_out_len`](Self::decrypt_out_len). +pub trait SymmetricCipherDecryptor< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const FINAL_LEN: usize, +>: Algorithm + Sized +{ + /// Begins a streaming decryption from the init data returned by + /// [`SymmetricCipherEncryptor::do_encrypt_init`]. + /// + /// # Errors + /// Rejects a key whose [`KeyType`] is not [`KeyType::SymmetricCipherKey`], and one whose + /// security strength is below [`Algorithm::MAX_SECURITY_STRENGTH`], both as a + /// [`SymmetricCipherError::KeyMaterialError`]. + fn do_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ) -> Result; + + /// The exact number of bytes the next [`do_update_out`](Self::do_decrypt_out) will write if + /// given `input_len` more bytes of ciphertext, so a caller can size the `plaintext` buffer for + /// that call before making it. + /// + /// It is not simply `input_len`: a decryptor holds back the tail of what it has seen -- the + /// block that might carry the padding, the bytes that might be the tag -- so how much a call + /// releases depends on what is already buffered, which is why this takes `&self` rather than + /// being a function of the length alone. + /// + /// Calling it is optional. A caller that would rather not compute lengths can pass whatever + /// buffer it has: if that buffer is too small the call fails with + /// [`SymmetricCipherError::OutputBufferTooSmall`] carrying the same number, having consumed + /// nothing, so retrying with a buffer at least that long produces exactly what the refused + /// call would have. This is for the caller who wants to allocate once up front -- one buffer + /// of `update_out_len(CHUNK)` bytes for a loop feeding fixed-size chunks -- rather than + /// discover the size from a failure. For the whole message in one call, see + /// [`decrypt_out_len`](Self::decrypt_out_len). + fn do_decrypt_out_len(&self, input_len: usize) -> usize; + + /// Streaming: consumes `ciphertext`, writing every plaintext byte that can be released so far + /// into `plaintext` and buffering the rest. Returns the number of bytes written, which is + /// exactly [`update_out_len`](Self::do_decrypt_out_len) of `ciphertext.len()`. + /// + /// A decryptor may have to hold back the tail of what it has seen -- the last block, which + /// might carry the padding, or the bytes that might be the tag -- so a sequence of calls + /// releases data later than the corresponding encryptor produced it, but the concatenation of + /// everything released plus the data part of [`do_decrypt_final`](Self::do_decrypt_final) is + /// the plaintext. + /// + /// The entire output buffer is zeroized before the plaintext is written, so any bytes past + /// `written` will be 0. In particular a call whose whole input is held back returns 0 and + /// leaves the whole buffer zeroed. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `plaintext` is shorter than + /// [`update_out_len`](Self::do_decrypt_out_len), carrying the required length, and + /// [`SymmetricCipherError::DataLimitExceeded`] if `ciphertext` would take the total past the + /// amount the cipher may process under one key and init data -- a limit a streaming API can + /// check no earlier than the call that would cross it. Nothing is consumed in either case. An + /// implementor whose message length is fixed by its type may also return + /// [`SymmetricCipherError::StateError`] if the input 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_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result; + + /// Streaming, allocating: as [`do_decrypt_out`](Self::do_decrypt_out), returning the + /// plaintext released by this call as a `Vec` of exactly + /// [`do_decrypt_out_len`](Self::do_decrypt_out_len) bytes -- which may be empty, if the whole + /// of `ciphertext` was held back. Only available with the `std` feature. + /// + /// # Errors + /// As [`do_decrypt_out`](Self::do_decrypt_out), except that the buffer is always large enough. #[cfg(feature = "std")] - /// A one-shot API to encrypt some plaintext with the given key. - /// This function returns the ciphertext as a `Vec`, and therefore is only available when compiling with std. - /// Returns a tuple containing the initialization data and the ciphertext. - /// This is not available if building for no_std. - fn encrypt( + fn do_decrypt(&mut self, ciphertext: &[u8]) -> Result, SymmetricCipherError> { + let needed = self.do_decrypt_out_len(ciphertext.len()); + let mut plaintext = vec![0u8; needed]; + let written = self.do_decrypt_out(ciphertext, &mut plaintext)?; + debug_assert_eq!(written, needed); + Ok(plaintext) + } + + /// Finishes the decryption, consuming the decryptor: processes whatever was held back, checks + /// it -- padding, tag -- and returns the final buffer together with the number of leading + /// bytes of it that are plaintext. The remainder of the buffer is not data and must not be + /// used. + /// + /// # Errors + /// [`SymmetricCipherError::DecryptionFailed`] if the ciphertext was malformed (empty, not a + /// whole number of blocks, or not the length an implementor's type fixes); + /// [`SymmetricCipherError::PaddingError`] or + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the check fails. In every error case the + /// caller learns only that decryption failed, not where. + fn do_decrypt_final(self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError>; + + /// As [`do_decrypt_final`](Self::do_decrypt_final), writing the data part of the final buffer + /// into `plaintext`. Returns the number of bytes written. The entire output buffer is zeroized + /// before the plaintext is written, so any bytes past that count will be 0. + fn do_decrypt_final_out( + self, + plaintext: &mut [u8; FINAL_LEN], + ) -> Result { + plaintext.fill(0); + let (buffer, data_len) = self.do_decrypt_final()?; + plaintext[..data_len].copy_from_slice(&buffer[..data_len]); + Ok(data_len) + } + + /// An upper bound on the plaintext recovered from `ciphertext_len` bytes of ciphertext, i.e. + /// the buffer [`decrypt_out`](Self::decrypt_out) requires. Exact for ciphers with no padding; + /// for a padding scheme the exact length is only known after decryption. + fn decrypt_out_len(ciphertext_len: usize) -> usize; + + /// One-shot: decrypts `ciphertext` into `plaintext`, which needs + /// [`decrypt_out_len`](Self::decrypt_out_len) bytes. Returns the number of plaintext + /// bytes written. The entire output buffer is zeroized before the plaintext is written, so any + /// bytes past that count will be 0. + /// + /// Provided as `do_decrypt_init`, one `do_update_out` and `do_decrypt_final`. If + /// `do_decrypt_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 + /// before any work is done; otherwise whatever the streaming methods return. + fn decrypt_out( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + let needed = Self::decrypt_out_len(ciphertext.len()); + if plaintext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let mut dec = Self::do_decrypt_init(key, init_data)?; + let written = dec.do_decrypt_out(ciphertext, plaintext)?; + match dec.do_decrypt_final() { + Ok((last, data_len)) => { + // `decrypt_out_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_detached_out` for why a plain `fill` is enough. + plaintext[..written].fill(0); + Err(e) + } + } + } + + /// One-shot, allocating: as [`decrypt_out`](Self::decrypt_out), returning the plaintext as a + /// `Vec` of exactly the recovered length. Only available with the `std` feature. + #[cfg(feature = "std")] + fn decrypt( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ciphertext: &[u8], + ) -> Result, SymmetricCipherError> { + let mut plaintext = vec![0u8; Self::decrypt_out_len(ciphertext.len())]; + let written = Self::decrypt_out(key, init_data, ciphertext, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } +} + +/// The encryption half of a symmetric cipher's arbitrary-length API: streaming `do_update_out` / +/// `do_encrypt_final`, plus one-shots provided over them. +/// +/// This is the layer a caller with *data* uses, as opposed to the block-aligned +/// [`BlockCipherEncryptor`] a mode implements. Its shape is that of the padding adapters in +/// `bouncycastle_cipher::padding`, which are its first implementors: an authenticated cipher or a stream +/// cipher fits the same shape, with the tag or nothing in place of the final padded block. +/// +/// `FINAL_LEN` is the fixed length of what [`do_encrypt_final`](Self::do_encrypt_final) produces +/// after the last byte of plaintext has been consumed: one block for a padding scheme, the tag +/// length for an authenticated cipher, zero for a stream cipher. Everything else about the output +/// length is answered exactly, before the fact, by [`update_out_len`](Self::do_encrypt_out_len) and +/// [`encrypt_out_len`](Self::encrypt_out_len), so a caller can size buffers without guessing. +/// +/// Init data (an IV or nonce) is generated by the constructor and returned, never supplied, for +/// the same reason as in [`BlockCipherEncryptor`]. Everything is `no_std`-friendly except the +/// allocating [`encrypt`](Self::encrypt), which sits behind the `std` feature. +/// +/// The one-shots [`encrypt_out`](Self::encrypt_out) and [`encrypt_rng_out`](Self::encrypt_rng_out) +/// are provided over the streaming methods. An implementor writes only the two `_init` +/// constructors, [`update_out_len`](Self::do_encrypt_out_len), [`do_update_out`](Self::do_encrypt_out), +/// [`do_encrypt_final`](Self::do_encrypt_final) and [`encrypt_out_len`](Self::encrypt_out_len). +pub trait SymmetricCipherEncryptor< + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const FINAL_LEN: usize, +>: Algorithm + Sized +{ + /// Begins a streaming encryption, returning the encryptor and the generated init data (IV or + /// nonce), which the recipient needs for [`SymmetricCipherDecryptor::do_decrypt_init`]. Sources + /// randomness from the library's default OS-backed RNG. + /// + /// # Errors + /// Rejects a key whose [`KeyType`] is not [`KeyType::SymmetricCipherKey`], and one whose + /// security strength is below [`Algorithm::MAX_SECURITY_STRENGTH`], both as a + /// [`SymmetricCipherError::KeyMaterialError`]. + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + + /// As [`do_encrypt_init`](Self::do_encrypt_init), but sources randomness from the provided RNG. + /// + /// # Panics + /// An implementation that generates no init data -- `INIT_DATA_LEN == 0`, as in ECB -- must + /// panic here rather than ignore `rng` and succeed. There is no randomness for it to consume, + /// so a caller reaching for this constructor has mistaken the cipher for a randomized one, and + /// quietly returning a deterministic encryptor would leave that mistake undetected. This is a + /// programmer error, not bad input, so it is a panic rather than a + /// [`SymmetricCipherError`]. Implementations with `INIT_DATA_LEN > 0` must draw their init + /// data from `rng` and must not panic. + fn do_encrypt_init_rng( key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError>; + + /// The exact number of bytes the next [`do_update_out`](Self::do_encrypt_out) will write if + /// given `input_len` more bytes of plaintext, so a caller can size the `ciphertext` buffer for + /// that call before making it. + /// + /// It is not simply `input_len`: a cipher that works a block at a time buffers a partial block + /// until it is full, so how much a call emits depends on what is already buffered, which is + /// why this takes `&self` rather than being a function of the length alone. + /// + /// Calling it is optional. A caller that would rather not compute lengths can pass whatever + /// buffer it has: if that buffer is too small the call fails with + /// [`SymmetricCipherError::OutputBufferTooSmall`] carrying the same number, having consumed + /// nothing, so retrying with a buffer at least that long produces exactly what the refused + /// call would have. This is for the caller who wants to allocate once up front -- one buffer + /// of `update_out_len(CHUNK)` bytes for a loop feeding fixed-size chunks -- rather than + /// discover the size from a failure. For the whole message in one call, see + /// [`encrypt_out_len`](Self::encrypt_out_len). + fn do_encrypt_out_len(&self, input_len: usize) -> usize; + + /// Streaming: consumes `plaintext`, writing every ciphertext byte that can be produced so far + /// into `ciphertext` and buffering the rest. Returns the number of bytes written, which is + /// exactly [`update_out_len`](Self::do_encrypt_out_len) of `plaintext.len()`. A sequence of calls + /// is equivalent to one call over the concatenation. + /// + /// The entire output buffer is zeroized before the ciphertext is written, so any bytes past + /// `written` will be 0. In particular a call that has to buffer all of its input -- a piece + /// that does not complete a block, say -- returns 0 and leaves the whole buffer zeroed. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is shorter than + /// [`update_out_len`](Self::do_encrypt_out_len), carrying the required length, and + /// [`SymmetricCipherError::DataLimitExceeded`] if `plaintext` would take the total past the + /// amount the cipher may process under one key and init data -- a limit a streaming API can + /// check no earlier than the call that would cross it. Nothing is consumed in either case. An + /// implementor whose message length is fixed by its type may also return + /// [`SymmetricCipherError::StateError`] if the input 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_encrypt_out( + &mut self, plaintext: &[u8], - ) -> Result<([u8; INIT_DATA_LEN], Vec), SymmetricCipherError>; - /// A one-shot API to encrypt some plaintext with the given key. - /// This function takes a reference to the output buffer for the ciphertext, and is therefore available in no_std. - /// See the documentation for the underlying implementation for details on providing a ciphertext 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 a tuple containing the initialization data and the number of bytes written to the ciphertext buffer. + ciphertext: &mut [u8], + ) -> Result; + + /// Streaming, allocating: as [`do_encrypt_out`](Self::do_encrypt_out), returning the + /// ciphertext released by this call as a `Vec` of exactly + /// [`do_encrypt_out_len`](Self::do_encrypt_out_len) bytes -- which may be empty, if the whole + /// of `plaintext` was buffered. Only available with the `std` feature. + /// + /// # Errors + /// As [`do_encrypt_out`](Self::do_encrypt_out), except that the buffer is always large enough. + #[cfg(feature = "std")] + fn do_encrypt(&mut self, plaintext: &[u8]) -> Result, SymmetricCipherError> { + let needed = self.do_encrypt_out_len(plaintext.len()); + let mut ciphertext = vec![0u8; needed]; + let written = self.do_encrypt_out(plaintext, &mut ciphertext)?; + debug_assert_eq!(written, needed); + Ok(ciphertext) + } + + /// Finishes the encryption, consuming the encryptor: pads and encrypts whatever was buffered, + /// or computes the tag, and returns the final buffer together with the number of leading bytes + /// of it that are ciphertext -- the last bytes of the message. For most ciphers that is always + /// `FINAL_LEN` (the padded block, the tag); a padding scheme that adds nothing to aligned data + /// returns 0 for an aligned message. The remainder of the buffer is not output. + /// + /// # Errors + /// [`SymmetricCipherError::PaddingError`] if the buffered data cannot be finished -- with a + /// scheme that adds no padding, a message that is not a whole number of blocks -- and + /// [`SymmetricCipherError::StateError`] from an implementor whose message length is fixed by + /// its type (see [`AEADCipherEncryptor`]) that was given less than it. + fn do_encrypt_final(self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError>; + + /// As [`do_encrypt_final`](Self::do_encrypt_final), writing the output part of the final + /// buffer into `ciphertext`. Returns the number of bytes written. The entire output buffer is + /// zeroized before the ciphertext is written, so any bytes past that count will be 0. + fn do_encrypt_final_out( + self, + ciphertext: &mut [u8; FINAL_LEN], + ) -> Result { + ciphertext.fill(0); + let (buffer, out_len) = self.do_encrypt_final()?; + ciphertext[..out_len].copy_from_slice(&buffer[..out_len]); + Ok(out_len) + } + + /// The exact ciphertext length for a `plaintext_len`-byte plaintext that the cipher accepts, + /// i.e. the buffer [`encrypt_out`](Self::encrypt_out) requires and the number of bytes it + /// writes. (A length the cipher rejects -- unaligned data under a scheme that adds no padding -- + /// fails in [`do_encrypt_final`](Self::do_encrypt_final) instead.) + fn encrypt_out_len(plaintext_len: usize) -> usize; + + /// One-shot: encrypts `plaintext` into `ciphertext`, which needs + /// [`encrypt_out_len`](Self::encrypt_out_len) bytes. Returns the generated init data and the + /// number of bytes written. The entire output buffer is zeroized before the ciphertext is + /// written, so any bytes past that count will be 0. + /// + /// Provided as `do_encrypt_init`, one `do_update_out` and `do_encrypt_final`. + /// + /// # Errors + /// [`SymmetricCipherError::OutputBufferTooSmall`] if `ciphertext` is too short, checked + /// before any work is done; otherwise whatever the streaming methods return. fn encrypt_out( key: &KeyMaterial, plaintext: &[u8], ciphertext: &mut [u8], - ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError>; - #[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. - /// This is not available if building for no_std. - fn decrypt( + ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { + ciphertext.fill(0); + let needed = Self::encrypt_out_len(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let (mut enc, init_data) = Self::do_encrypt_init(key)?; + let written = enc.do_encrypt_out(plaintext, ciphertext)?; + let (last, last_len) = enc.do_encrypt_final()?; + // `encrypt_out_len` is exactly `written + last_len`, so this fits in `ciphertext[..needed]`. + // .copy_from_slice is a bit of a code smell for a function meant to work in-place, + // but `ciphertext` is not required to be block-aligned, so it may not be large enough + // to hand to `do_encrypt_final_out()`. + ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); + Ok((init_data, written + last_len)) + } + + /// As [`encrypt_out`](Self::encrypt_out), but sources randomness from the provided RNG. + /// + /// # Panics + /// Provided over [`do_encrypt_init_rng`](Self::do_encrypt_init_rng), so it panics in exactly + /// the cases that does: an implementation with `INIT_DATA_LEN == 0`, which has no randomness + /// to consume. See that method for why. + fn encrypt_rng_out( key: &KeyMaterial, - init_data: [u8; INIT_DATA_LEN], - ciphertext: &[u8], - ) -> 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 a tuple containing the initialization data and the number of bytes written to the plaintext buffer. - fn decrypt_out( + rng: &mut dyn RNG, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { + ciphertext.fill(0); + let needed = Self::encrypt_out_len(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::OutputBufferTooSmall(needed)); + } + let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; + let written = enc.do_encrypt_out(plaintext, ciphertext)?; + let (last, last_len) = enc.do_encrypt_final()?; + ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); + Ok((init_data, written + last_len)) + } + + #[cfg(feature = "std")] + /// One-shot, allocating: as [`encrypt_out`](Self::encrypt_out), returning the ciphertext as a + /// `Vec`. Only available with the `std` feature. + fn encrypt( key: &KeyMaterial, - init_data: [u8; INIT_DATA_LEN], - ciphertext: &[u8], - plaintext: &mut [u8], - ) -> Result; + plaintext: &[u8], + ) -> Result<([u8; INIT_DATA_LEN], Vec), SymmetricCipherError> { + let mut ciphertext = vec![0u8; Self::encrypt_out_len(plaintext.len())]; + let (init_data, written) = Self::encrypt_out(key, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((init_data, ciphertext)) + } } -/// 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 `num_bits` message bits are taken from the least significant bits of - /// `partial_byte`, in order (bit 0 of `partial_byte` is the first message bit). This is the - /// FIPS 202 Appendix B.1 convention and is used uniformly for every hash family in this library. - /// 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, +/// as a typestate object. This is the type [`XOF::into_squeezer`] hands back. +/// +/// 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_output_final`](Self::do_output_final) means something weaker than `do_final` does on +/// [`Hash`] and [`MAC`]. There it is load-bearing -- the only way to get output, and it must +/// consume the value because finalizing pads the state. An XOF 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. +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. /// - /// 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 in the least significant `num_bits` bits of the returned u8, with the - /// remaining high bits zero. This follows the FIPS 202 Appendix B.1 bit-string convention - /// (the first bit of a byte is its least significant bit) and matches the input convention of - /// [`XOF::absorb_last_partial_byte`]. - /// 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; + /// 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_output_final(mut self, num_bytes: usize) -> Vec + where + Self: Sized, + { + self.do_output(num_bytes) + } - /// 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( + /// As [`do_output_final`](Self::do_output_final), filling the caller's buffer, which is + /// zeroized first. Returns the number of bytes written. + /// + /// Defaulted as [`do_output_final`](Self::do_output_final) is. + fn do_output_final_out(mut self, output: &mut [u8]) -> usize + where + Self: Sized, + { + self.do_output_out(output) + } +} + +/// Extendable-Output Functions (XOFs): A hash function with a variable-length output. +/// This relationship is captured by the type bound `XOF: Hash`. The instantiation that wraps an XOF +/// in a [`Hash`], specifies a fi~xed output length -- [`Hash::output_len`], often related to the +/// internal security parameters of the XOF. Often, other instantiantions are possible and the +/// provided one(s) are only a default. +/// +/// # Absorb, then squeeze +/// +/// All XOFs operate in two phases: accepting input, and producing output. When speaking specifically +/// about sponge constructions, these are referred to as "absorbing" and "squeezing", respectively. +/// +/// The underlying primitives of some XOFs, such as sponge functions, are capable of arbitrarily +/// interleaving absorbs and squeezes, however this XOF trait enforces absorb, then squeeze via a +/// typestate transition via the hard boundary [`into_squeezer`](Self::into_squeezer) +/// which consumes the [`XOF`] and returns an [`XOFSqueezer`]. +/// +/// # 🚨 Security Considerations 🚨 +/// ## A XOF is not a hash, cryptographically +/// +/// The reason that an XOF itself is not (usually) considered to be a hash function is related outputs. +/// Two XOFs given the same input, one read for 32 bytes and one for 1 KiB will be identical on their +/// first 32 bytes. +/// In many contexts, this breaks the Preimage properties that hash functions guarantee since it +/// becomes trivial for an attacker to tell that these two different outputs came from the same input. +/// +/// These security properties can be restored at the application layer by diversifying the inputs. +/// For example, by appending the output length to the input message, the following two invocation +/// will now produce un-correlated outputs even on the same `message`: +/// +/// ```text +/// xof(message || 0x32, 32) +/// xof(message || 0x64, 64) +/// ``` +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; - /// 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 produces `result_len` bytes. + /// + /// A one-shot names its length and never comes back, so this is + /// [`XOFSqueezer::do_output_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_output_final(result_len) + } + + /// 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_output_final_out(output) + } } diff --git a/crypto/core/tests/aead_buffering_toy_tests.rs b/crypto/core/tests/aead_buffering_toy_tests.rs new file mode 100644 index 00000000..f4c4b1f1 --- /dev/null +++ b/crypto/core/tests/aead_buffering_toy_tests.rs @@ -0,0 +1,347 @@ +//! Testing the default implementations of the AEAD traits. +//! +//! Every one-shot on [`AEADCipherEncryptor`] / [`AEADCipherDecryptor`] and their supertraits is a +//! default method in `bouncycastle-core` that stitches a streaming call and a final call together, +//! so the `written + final_len` arithmetic in each of them is only observable when the final call +//! releases data. No real cipher in the workspace does that a few bytes at a time -- GCM and Ascon +//! hold back nothing but the tag, CCM's adapters hold back everything -- so this is this crate's +//! own test of those defaults, over a toy built to hold back up to three bytes. It lives here +//! rather than in `bouncycastle-core-test-framework` because it tests code in this crate, and +//! because using the framework from here would make the two crates dev-depend on each other. +//! +//! [`AEADCipherEncryptor`]: bouncycastle_core::traits::AEADCipherEncryptor +//! [`AEADCipherDecryptor`]: bouncycastle_core::traits::AEADCipherDecryptor + +/// 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 `TestFrameworkAEADCipher::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/cipher/tests/modes/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. +#[test] +fn a_buffering_pair_is_handled_by_every_default_method() { + 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, + }; + + 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 do_encrypt_out_len(&self, input_len: usize) -> usize { + self.0.update_out_len(input_len) + } + fn do_encrypt_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + ciphertext.fill(0); + Ok(self.0.update_out(plaintext, ciphertext)) + } + fn do_encrypt_final(self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError> { + let mut out = [0u8; FINAL_LEN]; + let (n, tag) = self.do_encrypt_final_detachedtag_out(&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_encrypt_final_detachedtag_out( + mut self, + ciphertext: &mut [u8; FINAL_LEN], + ) -> Result<(usize, [u8; TAG_LEN]), SymmetricCipherError> { + ciphertext.fill(0); + 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 do_decrypt_out_len(&self, input_len: usize) -> usize { + self.0.update_out_len(input_len) + } + fn do_decrypt_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + plaintext.fill(0); + Ok(self.0.update_out(ciphertext, plaintext)) + } + /// The last `TAG_LEN` held-back bytes are the tag, the rest ciphertext. + fn do_decrypt_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_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_decrypt_final_detachedtag_out( + mut self, + tag: &[u8; TAG_LEN], + plaintext: &mut [u8; FINAL_LEN], + ) -> Result { + plaintext.fill(0); + 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) + } + } + + // The bytes every key and message is cut from: `0x00, 0x01, ...`, long enough for the longest + // message below. + let seed: [u8; 64] = core::array::from_fn(|i| i as u8); + let key = + KeyMaterial::::from_bytes_as_type(&seed[..KEY_LEN], KeyType::SymmetricCipherKey) + .unwrap(); + + for len in 0..=(3 * FINAL_LEN + 5) { + let msg = &seed[..len]; + let mut ct = vec![0u8; len]; + let (nonce, ct_len, tag) = Enc::encrypt_detached_out(&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.do_encrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_encrypt_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_encrypt_final_detachedtag_out(&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" + ); + + // detached: the decryptor releases what it held back as a possible tag in + // `do_decrypt_final_detachedtag_out`, 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.do_decrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_decrypt_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]); + } + let mut final_buf = [0u8; FINAL_LEN]; + let final_len = dec.do_decrypt_final_detachedtag_out(&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.do_decrypt_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_decrypt_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]); + } + let (last, data_len) = dec.do_decrypt_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_encrypt_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.do_encrypt_out_len(len)]; + let written = enc.do_encrypt_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_encrypt_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_with_aad_out(&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_len(one_len)]; + let back_len = + Dec::decrypt_with_aad_out(&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_detached_rng_out( + &key, + &mut bouncycastle_rng::DefaultRNG::default(), + b"", + msg, + &mut ct_rng, + ) + .unwrap(); + assert_eq!(&ct_rng[..n_rng], &ct[..], "len {len}: encrypt_detached_rng_out"); + assert_eq!(tag_rng, tag, "len {len}: encrypt_detached_rng_out tag"); + let mut back = vec![0u8; len]; + let back_len = Dec::decrypt_detached_out(&key, &nonce, b"", &ct, &tag, &mut back).unwrap(); + assert_eq!(&back[..back_len], msg, "len {len}: decrypt_detached_out"); + 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_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_encrypt_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.do_encrypt_out_len(first.len())]; + let n = enc.do_encrypt_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/tests/key_material_tests.rs b/crypto/core/tests/key_material_tests.rs index efcc7759..bca2f318 100644 --- a/crypto/core/tests/key_material_tests.rs +++ b/crypto/core/tests/key_material_tests.rs @@ -1,11 +1,12 @@ #[cfg(test)] mod test_key_material { use bouncycastle_core::errors::KeyMaterialError; + use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterial0, KeyMaterial128, KeyMaterial256, KeyMaterial512, - KeyMaterialTrait, KeyType, do_hazardous_operations, + KeyMaterialTrait, KeyType, }; - use bouncycastle_core::traits::SecurityStrength; + use bouncycastle_core::security_strength::SecurityStrength; const DUMMY_KEY: &[u8; 64] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\ \x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F\ diff --git a/crypto/core/tests/trait_tests.rs b/crypto/core/tests/security_strength_tests.rs similarity index 97% rename from crypto/core/tests/trait_tests.rs rename to crypto/core/tests/security_strength_tests.rs index 05600891..948d973e 100644 --- a/crypto/core/tests/trait_tests.rs +++ b/crypto/core/tests/security_strength_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use bouncycastle_core::traits::SecurityStrength; + use bouncycastle_core::security_strength::SecurityStrength; #[test] fn test_security_strength() { diff --git a/crypto/factory/Cargo.toml b/crypto/factory/Cargo.toml index e35e31f4..c9765796 100644 --- a/crypto/factory/Cargo.toml +++ b/crypto/factory/Cargo.toml @@ -4,9 +4,11 @@ version.workspace = true edition.workspace = true [dependencies] +bouncycastle-ascon.workspace = true bouncycastle-core.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true +bouncycastle-sm3.workspace = true bouncycastle-rng.workspace = true [dev-dependencies] diff --git a/crypto/factory/src/hash_factory.rs b/crypto/factory/src/hash_factory.rs index edbfd17a..0598d79a 100644 --- a/crypto/factory/src/hash_factory.rs +++ b/crypto/factory/src/hash_factory.rs @@ -28,16 +28,24 @@ 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::traits::{Algorithm, Hash, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash}; use bouncycastle_sha2 as sha2; -use bouncycastle_sha2::{SHA224_NAME, SHA256_NAME, SHA384_NAME, SHA512_NAME}; +use bouncycastle_sha2::{ + SHA224_NAME, SHA256_NAME, SHA384_NAME, SHA512_224_NAME, SHA512_256_NAME, SHA512_NAME, +}; use bouncycastle_sha3 as sha3; use bouncycastle_sha3::{SHA3_224_NAME, SHA3_256_NAME, SHA3_384_NAME, SHA3_512_NAME}; +use bouncycastle_sm3 as sm3; +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), @@ -48,6 +56,10 @@ pub enum HashFactory { /// SHA512(sha2::SHA512), /// + SHA512_224(sha2::SHA512_224), + /// + SHA512_256(sha2::SHA512_256), + /// SHA3_224(sha3::SHA3_224), /// SHA3_256(sha3::SHA3_256), @@ -55,6 +67,10 @@ pub enum HashFactory { SHA3_384(sha3::SHA3_384), /// SHA3_512(sha3::SHA3_512), + /// + SM3(sm3::SM3), + /// + AsconHash256(ascon::ascon_hash256::AsconHash256), } impl Default for HashFactory { @@ -80,10 +96,14 @@ impl AlgorithmFactory for HashFactory { SHA256_NAME => Ok(Self::SHA256(sha2::SHA256::new())), SHA384_NAME => Ok(Self::SHA384(sha2::SHA384::new())), SHA512_NAME => Ok(Self::SHA512(sha2::SHA512::new())), + SHA512_224_NAME => Ok(Self::SHA512_224(sha2::SHA512_224::new())), + SHA512_256_NAME => Ok(Self::SHA512_256(sha2::SHA512_256::new())), SHA3_224_NAME => Ok(Self::SHA3_224(sha3::SHA3_224::new())), SHA3_256_NAME => Ok(Self::SHA3_256(sha3::SHA3_256::new())), 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 @@ -108,10 +128,14 @@ impl Hash for HashFactory { Self::SHA256(h) => h.block_bitlen(), Self::SHA384(h) => h.block_bitlen(), Self::SHA512(h) => h.block_bitlen(), + Self::SHA512_224(h) => h.block_bitlen(), + Self::SHA512_256(h) => h.block_bitlen(), Self::SHA3_224(h) => h.block_bitlen(), Self::SHA3_256(h) => h.block_bitlen(), 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(), } } @@ -121,10 +145,14 @@ impl Hash for HashFactory { Self::SHA256(h) => h.output_len(), Self::SHA384(h) => h.output_len(), Self::SHA512(h) => h.output_len(), + Self::SHA512_224(h) => h.output_len(), + Self::SHA512_256(h) => h.output_len(), Self::SHA3_224(h) => h.output_len(), Self::SHA3_256(h) => h.output_len(), 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(), } } @@ -134,10 +162,14 @@ impl Hash for HashFactory { Self::SHA256(h) => h.hash(data), Self::SHA384(h) => h.hash(data), Self::SHA512(h) => h.hash(data), + Self::SHA512_224(h) => h.hash(data), + Self::SHA512_256(h) => h.hash(data), Self::SHA3_224(h) => h.hash(data), Self::SHA3_256(h) => h.hash(data), 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), } } @@ -149,10 +181,14 @@ impl Hash for HashFactory { Self::SHA256(h) => h.hash_out(data, output), Self::SHA384(h) => h.hash_out(data, output), Self::SHA512(h) => h.hash_out(data, output), + Self::SHA512_224(h) => h.hash_out(data, output), + Self::SHA512_256(h) => h.hash_out(data, output), Self::SHA3_224(h) => h.hash_out(data, output), Self::SHA3_256(h) => h.hash_out(data, output), 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), } } @@ -162,10 +198,14 @@ impl Hash for HashFactory { Self::SHA256(h) => h.do_update(data), Self::SHA384(h) => h.do_update(data), Self::SHA512(h) => h.do_update(data), + Self::SHA512_224(h) => h.do_update(data), + Self::SHA512_256(h) => h.do_update(data), Self::SHA3_224(h) => h.do_update(data), Self::SHA3_256(h) => h.do_update(data), 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), } } @@ -175,10 +215,14 @@ impl Hash for HashFactory { Self::SHA256(h) => h.do_final(), Self::SHA384(h) => h.do_final(), Self::SHA512(h) => h.do_final(), + Self::SHA512_224(h) => h.do_final(), + Self::SHA512_256(h) => h.do_final(), Self::SHA3_224(h) => h.do_final(), Self::SHA3_256(h) => h.do_final(), 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(), } } @@ -190,10 +234,14 @@ impl Hash for HashFactory { Self::SHA256(h) => h.do_final_out(output), Self::SHA384(h) => h.do_final_out(output), Self::SHA512(h) => h.do_final_out(output), + Self::SHA512_224(h) => h.do_final_out(output), + Self::SHA512_256(h) => h.do_final_out(output), Self::SHA3_224(h) => h.do_final_out(output), Self::SHA3_256(h) => h.do_final_out(output), 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), } } @@ -207,10 +255,14 @@ impl Hash for HashFactory { Self::SHA256(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), Self::SHA384(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), Self::SHA512(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), + Self::SHA512_224(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), + Self::SHA512_256(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), Self::SHA3_224(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), Self::SHA3_256(h) => h.do_final_partial_bits(partial_byte, num_partial_bits), 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), } } @@ -225,6 +277,12 @@ impl Hash for HashFactory { Self::SHA256(h) => h.do_final_partial_bits_out(partial_byte, num_partial_bits, output), Self::SHA384(h) => h.do_final_partial_bits_out(partial_byte, num_partial_bits, output), Self::SHA512(h) => h.do_final_partial_bits_out(partial_byte, num_partial_bits, output), + Self::SHA512_224(h) => { + h.do_final_partial_bits_out(partial_byte, num_partial_bits, output) + } + Self::SHA512_256(h) => { + h.do_final_partial_bits_out(partial_byte, num_partial_bits, output) + } Self::SHA3_224(h) => { h.do_final_partial_bits_out(partial_byte, num_partial_bits, output) } @@ -237,6 +295,10 @@ impl Hash for HashFactory { Self::SHA3_512(h) => { 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) + } } } @@ -246,10 +308,14 @@ impl Hash for HashFactory { Self::SHA256(h) => h.max_security_strength(), Self::SHA384(h) => h.max_security_strength(), Self::SHA512(h) => h.max_security_strength(), + Self::SHA512_224(h) => h.max_security_strength(), + Self::SHA512_256(h) => h.max_security_strength(), Self::SHA3_224(h) => h.max_security_strength(), Self::SHA3_256(h) => h.max_security_strength(), 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/kdf_factory.rs b/crypto/factory/src/kdf_factory.rs index 7c46a570..adda54e2 100644 --- a/crypto/factory/src/kdf_factory.rs +++ b/crypto/factory/src/kdf_factory.rs @@ -50,8 +50,11 @@ use crate::{AlgorithmFactory, DEFAULT, DEFAULT_128_BIT, DEFAULT_256_BIT, FactoryError}; use bouncycastle_core::errors::KDFError; use bouncycastle_core::key_material::KeyMaterialTrait; -use bouncycastle_core::traits::{KDF, SecurityStrength}; -use bouncycastle_sha2::hkdf::{HKDF_SHA256, HKDF_SHA256_NAME, HKDF_SHA512, HKDF_SHA512_NAME}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::KDF; +use bouncycastle_sha2::hkdf::{ + HKDF_SHA256, HKDF_SHA256_NAME, HKDF_SHA384, HKDF_SHA384_NAME, HKDF_SHA512, HKDF_SHA512_NAME, +}; use bouncycastle_sha3 as sha3; use bouncycastle_sha3::{ SHA3_224_NAME, SHA3_256_NAME, SHA3_384_NAME, SHA3_512_NAME, SHAKE128_NAME, SHAKE256_NAME, @@ -65,6 +68,9 @@ pub enum KDFFactory { HKDF_SHA256(HKDF_SHA256), /// #[allow(non_camel_case_types)] + HKDF_SHA384(HKDF_SHA384), + /// + #[allow(non_camel_case_types)] HKDF_SHA512(HKDF_SHA512), /// SHA3_224(sha3::SHA3_224), @@ -101,6 +107,7 @@ impl AlgorithmFactory for KDFFactory { DEFAULT_128_BIT => Ok(KDFFactory::default_128_bit()), DEFAULT_256_BIT => Ok(KDFFactory::default_256_bit()), HKDF_SHA256_NAME => Ok(Self::HKDF_SHA256(HKDF_SHA256::new())), + HKDF_SHA384_NAME => Ok(Self::HKDF_SHA384(HKDF_SHA384::new())), HKDF_SHA512_NAME => Ok(Self::HKDF_SHA512(HKDF_SHA512::new())), SHA3_224_NAME => Ok(Self::SHA3_224(sha3::SHA3_224::new())), SHA3_256_NAME => Ok(Self::SHA3_256(sha3::SHA3_256::new())), @@ -124,6 +131,7 @@ impl KDF for KDFFactory { ) -> Result, KDFError> { match self { Self::HKDF_SHA256(h) => h.derive_key(key, additional_input), + Self::HKDF_SHA384(h) => h.derive_key(key, additional_input), Self::HKDF_SHA512(h) => h.derive_key(key, additional_input), Self::SHA3_224(h) => h.derive_key(key, additional_input), Self::SHA3_256(h) => h.derive_key(key, additional_input), @@ -142,6 +150,7 @@ impl KDF for KDFFactory { ) -> Result { match self { Self::HKDF_SHA256(h) => h.derive_key_out(key, additional_input, output_key), + Self::HKDF_SHA384(h) => h.derive_key_out(key, additional_input, output_key), Self::HKDF_SHA512(h) => h.derive_key_out(key, additional_input, output_key), Self::SHA3_224(h) => h.derive_key_out(key, additional_input, output_key), Self::SHA3_256(h) => h.derive_key_out(key, additional_input, output_key), @@ -159,6 +168,7 @@ impl KDF for KDFFactory { ) -> Result, KDFError> { match self { Self::HKDF_SHA256(h) => h.derive_key_from_multiple(keys, additional_input), + Self::HKDF_SHA384(h) => h.derive_key_from_multiple(keys, additional_input), Self::HKDF_SHA512(h) => h.derive_key_from_multiple(keys, additional_input), Self::SHA3_224(h) => h.derive_key_from_multiple(keys, additional_input), Self::SHA3_256(h) => h.derive_key_from_multiple(keys, additional_input), @@ -179,6 +189,9 @@ impl KDF for KDFFactory { Self::HKDF_SHA256(h) => { h.derive_key_from_multiple_out(keys, additional_input, output_key) } + Self::HKDF_SHA384(h) => { + h.derive_key_from_multiple_out(keys, additional_input, output_key) + } Self::HKDF_SHA512(h) => { h.derive_key_from_multiple_out(keys, additional_input, output_key) } @@ -194,6 +207,7 @@ impl KDF for KDFFactory { fn max_security_strength(&self) -> SecurityStrength { match self { Self::HKDF_SHA256(h) => h.max_security_strength(), + Self::HKDF_SHA384(h) => h.max_security_strength(), Self::HKDF_SHA512(h) => h.max_security_strength(), Self::SHA3_224(h) => h.max_security_strength(), Self::SHA3_256(h) => h.max_security_strength(), diff --git a/crypto/factory/src/mac_factory.rs b/crypto/factory/src/mac_factory.rs index 14e4d0b4..f040a830 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -73,15 +73,20 @@ use crate::{DEFAULT, DEFAULT_128_BIT, DEFAULT_256_BIT, FactoryError}; use bouncycastle_core::errors::MACError; use bouncycastle_core::key_material::KeyMaterialTrait; -use bouncycastle_core::traits::{MAC, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::MAC; use bouncycastle_sha2 as sha2; use bouncycastle_sha2::hmac::{ - HMAC_SHA224_NAME, HMAC_SHA256_NAME, HMAC_SHA384_NAME, HMAC_SHA512_NAME, + HMAC_SHA224_NAME, HMAC_SHA256_NAME, HMAC_SHA384_NAME, HMAC_SHA512_224_NAME, + HMAC_SHA512_256_NAME, HMAC_SHA512_NAME, }; 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::kmac::{KMAC128, KMAC128_NAME, KMAC256, KMAC256_NAME}; +use bouncycastle_sm3 as sm3; +use bouncycastle_sm3::hmac::HMAC_SM3_NAME; /*** Defaults ***/ /// @@ -98,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), /// @@ -107,6 +119,10 @@ pub enum MACFactory { /// HMAC_SHA512(sha2::hmac::HMAC_SHA512), /// + HMAC_SHA512_224(sha2::hmac::HMAC_SHA512_224), + /// + HMAC_SHA512_256(sha2::hmac::HMAC_SHA512_256), + /// HMAC_SHA3_224(sha3::hmac::HMAC_SHA3_224), /// HMAC_SHA3_256(sha3::hmac::HMAC_SHA3_256), @@ -114,6 +130,8 @@ pub enum MACFactory { HMAC_SHA3_384(sha3::hmac::HMAC_SHA3_384), /// HMAC_SHA3_512(sha3::hmac::HMAC_SHA3_512), + /// + HMAC_SM3(sm3::hmac::HMAC_SM3), } impl MACFactory { @@ -135,14 +153,23 @@ 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)?)), HMAC_SHA512_NAME => Ok(Self::HMAC_SHA512(sha2::hmac::HMAC_SHA512::new(key)?)), + HMAC_SHA512_224_NAME => { + Ok(Self::HMAC_SHA512_224(sha2::hmac::HMAC_SHA512_224::new(key)?)) + } + HMAC_SHA512_256_NAME => { + Ok(Self::HMAC_SHA512_256(sha2::hmac::HMAC_SHA512_256::new(key)?)) + } HMAC_SHA3_224_NAME => Ok(Self::HMAC_SHA3_224(sha3::hmac::HMAC_SHA3_224::new(key)?)), HMAC_SHA3_256_NAME => Ok(Self::HMAC_SHA3_256(sha3::hmac::HMAC_SHA3_256::new(key)?)), HMAC_SHA3_384_NAME => Ok(Self::HMAC_SHA3_384(sha3::hmac::HMAC_SHA3_384::new(key)?)), HMAC_SHA3_512_NAME => Ok(Self::HMAC_SHA3_512(sha3::hmac::HMAC_SHA3_512::new(key)?)), + HMAC_SM3_NAME => Ok(Self::HMAC_SM3(sm3::hmac::HMAC_SM3::new(key)?)), _ => Err(FactoryError::UnsupportedAlgorithm(format!( "The algorithm: \"{}\" is not a known MAC", alg_name @@ -164,27 +191,37 @@ 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(), Self::HMAC_SHA512(h) => h.output_len(), + Self::HMAC_SHA512_224(h) => h.output_len(), + Self::HMAC_SHA512_256(h) => h.output_len(), Self::HMAC_SHA3_224(h) => h.output_len(), Self::HMAC_SHA3_256(h) => h.output_len(), Self::HMAC_SHA3_384(h) => h.output_len(), Self::HMAC_SHA3_512(h) => h.output_len(), + Self::HMAC_SM3(h) => h.output_len(), } } 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), Self::HMAC_SHA512(h) => h.mac(data), + Self::HMAC_SHA512_224(h) => h.mac(data), + Self::HMAC_SHA512_256(h) => h.mac(data), Self::HMAC_SHA3_224(h) => h.mac(data), Self::HMAC_SHA3_256(h) => h.mac(data), Self::HMAC_SHA3_384(h) => h.mac(data), Self::HMAC_SHA3_512(h) => h.mac(data), + Self::HMAC_SM3(h) => h.mac(data), } } @@ -192,53 +229,73 @@ 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), Self::HMAC_SHA512(h) => h.mac_out(data, out), + Self::HMAC_SHA512_224(h) => h.mac_out(data, out), + Self::HMAC_SHA512_256(h) => h.mac_out(data, out), Self::HMAC_SHA3_224(h) => h.mac_out(data, out), Self::HMAC_SHA3_256(h) => h.mac_out(data, out), Self::HMAC_SHA3_384(h) => h.mac_out(data, out), Self::HMAC_SHA3_512(h) => h.mac_out(data, out), + Self::HMAC_SM3(h) => h.mac_out(data, out), } } 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), Self::HMAC_SHA512(h) => h.verify(data, mac), + Self::HMAC_SHA512_224(h) => h.verify(data, mac), + Self::HMAC_SHA512_256(h) => h.verify(data, mac), Self::HMAC_SHA3_224(h) => h.verify(data, mac), Self::HMAC_SHA3_256(h) => h.verify(data, mac), Self::HMAC_SHA3_384(h) => h.verify(data, mac), Self::HMAC_SHA3_512(h) => h.verify(data, mac), + Self::HMAC_SM3(h) => h.verify(data, mac), } } 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), Self::HMAC_SHA512(h) => h.do_update(data), + Self::HMAC_SHA512_224(h) => h.do_update(data), + Self::HMAC_SHA512_256(h) => h.do_update(data), Self::HMAC_SHA3_224(h) => h.do_update(data), Self::HMAC_SHA3_256(h) => h.do_update(data), Self::HMAC_SHA3_384(h) => h.do_update(data), Self::HMAC_SHA3_512(h) => h.do_update(data), + Self::HMAC_SM3(h) => h.do_update(data), } } 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(), Self::HMAC_SHA512(h) => h.do_final(), + Self::HMAC_SHA512_224(h) => h.do_final(), + Self::HMAC_SHA512_256(h) => h.do_final(), Self::HMAC_SHA3_224(h) => h.do_final(), Self::HMAC_SHA3_256(h) => h.do_final(), Self::HMAC_SHA3_384(h) => h.do_final(), Self::HMAC_SHA3_512(h) => h.do_final(), + Self::HMAC_SM3(h) => h.do_final(), } } @@ -246,40 +303,55 @@ 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), Self::HMAC_SHA512(h) => h.do_final_out(&mut out), + Self::HMAC_SHA512_224(h) => h.do_final_out(&mut out), + Self::HMAC_SHA512_256(h) => h.do_final_out(&mut out), Self::HMAC_SHA3_224(h) => h.do_final_out(&mut out), Self::HMAC_SHA3_256(h) => h.do_final_out(&mut out), Self::HMAC_SHA3_384(h) => h.do_final_out(&mut out), Self::HMAC_SHA3_512(h) => h.do_final_out(&mut out), + Self::HMAC_SM3(h) => h.do_final_out(&mut out), } } 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), Self::HMAC_SHA512(h) => h.do_verify_final(mac), + Self::HMAC_SHA512_224(h) => h.do_verify_final(mac), + Self::HMAC_SHA512_256(h) => h.do_verify_final(mac), Self::HMAC_SHA3_224(h) => h.do_verify_final(mac), Self::HMAC_SHA3_256(h) => h.do_verify_final(mac), Self::HMAC_SHA3_384(h) => h.do_verify_final(mac), Self::HMAC_SHA3_512(h) => h.do_verify_final(mac), + Self::HMAC_SM3(h) => h.do_verify_final(mac), } } 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(), Self::HMAC_SHA512(h) => h.max_security_strength(), + Self::HMAC_SHA512_224(h) => h.max_security_strength(), + Self::HMAC_SHA512_256(h) => h.max_security_strength(), Self::HMAC_SHA3_224(h) => h.max_security_strength(), Self::HMAC_SHA3_256(h) => h.max_security_strength(), Self::HMAC_SHA3_384(h) => h.max_security_strength(), Self::HMAC_SHA3_512(h) => h.max_security_strength(), + Self::HMAC_SM3(h) => h.max_security_strength(), } } } diff --git a/crypto/factory/src/rng_factory.rs b/crypto/factory/src/rng_factory.rs index 14329969..72d7c988 100644 --- a/crypto/factory/src/rng_factory.rs +++ b/crypto/factory/src/rng_factory.rs @@ -45,7 +45,8 @@ use crate::{AlgorithmFactory, FactoryError}; use crate::{DEFAULT, DEFAULT_128_BIT, DEFAULT_256_BIT}; use bouncycastle_core::errors::RNGError; use bouncycastle_core::key_material::KeyMaterialTrait; -use bouncycastle_core::traits::{RNG, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::RNG; use bouncycastle_rng as rng; use bouncycastle_rng::{HASH_DRBG_SHA256_NAME, HASH_DRBG_SHA512_NAME}; diff --git a/crypto/factory/src/xof_factory.rs b/crypto/factory/src/xof_factory.rs index c3d97473..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,12 +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::traits::{KDF, SecurityStrength, XOF}; +use bouncycastle_core::security_strength::SecurityStrength; +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; /// @@ -49,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 { @@ -75,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 @@ -82,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 31d216bc..5d70757f 100644 --- a/crypto/factory/tests/hash_factory_tests.rs +++ b/crypto/factory/tests/hash_factory_tests.rs @@ -54,6 +54,69 @@ mod hash_factory_tests { let sha2 = HashFactory::new(sha2::SHA512_NAME).unwrap(); assert_eq!(sha2.output_len(), 64); assert_eq!(sha2.hash(&DUMMY_SEED[..512]), b"\xed\xb9\xbe\xd7\x21\xaa\x6a\x5f\x6f\xbc\x66\x19\xd3\xa3\xc2\xbe\x3d\x04\x30\x43\xf0\x5a\x9a\xeb\xc7\xb1\x19\x7a\x2a\xa9\xc4\x9a\x57\xd5\xdd\xd4\x67\x4c\x17\x85\x78\x50\x88\xd9\xf1\xff\x42\xc7\x97\xa0\x2a\xdc\x9b\x81\x7a\x13\x9a\x50\x97\x0d\xa6\xc9\x95\x24"); + + // SHA512/224 -- "abc" vector from the NIST example file SHA512_224.pdf + let sha2 = HashFactory::new("SHA512/224").unwrap(); + assert_eq!(sha2.output_len(), 28); + assert_eq!(sha2.hash(b"abc"), b"\x46\x34\x27\x0f\x70\x7b\x6a\x54\xda\xae\x75\x30\x46\x08\x42\xe2\x0e\x37\xed\x26\x5c\xee\xe9\xa4\x3e\x89\x24\xaa"); + + let sha2 = HashFactory::new(sha2::SHA512_224_NAME).unwrap(); + assert_eq!(sha2.output_len(), 28); + assert_eq!(sha2.hash(b"abc"), b"\x46\x34\x27\x0f\x70\x7b\x6a\x54\xda\xae\x75\x30\x46\x08\x42\xe2\x0e\x37\xed\x26\x5c\xee\xe9\xa4\x3e\x89\x24\xaa"); + + // SHA512/256 -- "abc" vector from the NIST example file SHA512_256.pdf + let sha2 = HashFactory::new("SHA512/256").unwrap(); + assert_eq!(sha2.output_len(), 32); + assert_eq!(sha2.hash(b"abc"), b"\x53\x04\x8e\x26\x81\x94\x1e\xf9\x9b\x2e\x29\xb7\x6b\x4c\x7d\xab\xe4\xc2\xd0\xc6\x34\xfc\x6d\x46\xe0\xe2\xf1\x31\x07\xe7\xaf\x23"); + + let sha2 = HashFactory::new(sha2::SHA512_256_NAME).unwrap(); + assert_eq!(sha2.output_len(), 32); + assert_eq!(sha2.hash(b"abc"), b"\x53\x04\x8e\x26\x81\x94\x1e\xf9\x9b\x2e\x29\xb7\x6b\x4c\x7d\xab\xe4\xc2\xd0\xc6\x34\xfc\x6d\x46\xe0\xe2\xf1\x31\x07\xe7\xaf\x23"); + + // The remaining pass-throughs, on the same "abc" vectors: streaming, the _out variants + // and block_bitlen. + let expected_224 = HashFactory::new("SHA512/224").unwrap().hash(b"abc"); + let expected_256 = HashFactory::new("SHA512/256").unwrap().hash(b"abc"); + for (name, expected) in [("SHA512/224", &expected_224), ("SHA512/256", &expected_256)] { + let mut sha2 = HashFactory::new(name).unwrap(); + assert_eq!(sha2.block_bitlen(), 1024); + sha2.do_update(b"a"); + sha2.do_update(b"bc"); + assert_eq!(&sha2.do_final(), expected); + + let mut sha2 = HashFactory::new(name).unwrap(); + sha2.do_update(b"abc"); + let mut out = vec![0xffu8; expected.len()]; + assert_eq!(sha2.do_final_out(&mut out), expected.len()); + assert_eq!(&out, expected); + + let mut out = vec![0xffu8; expected.len()]; + assert_eq!( + HashFactory::new(name).unwrap().hash_out(b"abc", &mut out), + expected.len() + ); + assert_eq!(&out, expected); + } + } + + #[test] + fn sm3_hash_tests() { + use bouncycastle_sm3 as sm3; + // Expected values: GB/T 32905-2016 Appendix A ("abc") and openssl dgst -sm3 (DUMMY_SEED[..512]). + for name in ["SM3", sm3::SM3_NAME] { + let h = HashFactory::new(name).unwrap(); + assert_eq!(h.output_len(), 32); + assert_eq!(h.block_bitlen(), 512); + assert_eq!( + h.hash(&DUMMY_SEED[..512]), + b"\xb2\x1f\x83\x0d\xca\x06\xbe\x8b\x67\x8c\xf9\x87\xf2\x6b\x9a\x43\x6e\x1b\x42\x79\x63\xb4\x45\x03\x32\xf0\x12\x70\xbd\x2d\xf7\x5c" + ); + let h = HashFactory::new(name).unwrap(); + assert_eq!( + h.hash(b"abc"), + b"\x66\xc7\xf0\xf4\x62\xee\xed\xd9\xd1\xf2\xd4\x6b\xdc\x10\xe4\xe2\x41\x67\xc4\x87\x5c\xf2\xf7\xa2\x29\x7d\xa0\x2b\x8f\x4b\xa8\xe0" + ); + } } #[test] @@ -97,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/kdf_factory_tests.rs b/crypto/factory/tests/kdf_factory_tests.rs index d2a62f1e..a8a70317 100644 --- a/crypto/factory/tests/kdf_factory_tests.rs +++ b/crypto/factory/tests/kdf_factory_tests.rs @@ -8,6 +8,7 @@ mod kdf_factory_tests { use bouncycastle_factory as factory; use bouncycastle_factory::AlgorithmFactory; use bouncycastle_factory::kdf_factory::KDFFactory; + use bouncycastle_sha2::hkdf::HKDF_SHA384; use bouncycastle_utils::ct; #[test] @@ -71,6 +72,16 @@ mod kdf_factory_tests { let expected_key = KeyMaterial256::from_bytes(b"\x37\xad\x29\x10\x9f\x43\x26\x52\x87\x80\x4b\x67\x4e\x26\x53\xd0\xa5\x13\x71\x89\x07\xf9\x7f\xca\x97\xc9\x5b\xde\xd8\x10\x4b\xbf").unwrap(); assert!(ct::ct_eq_bytes(derived_key.ref_to_bytes(), &expected_key.ref_to_bytes())); + /* HKDF-SHA384 */ + // The factory must route to HKDF_SHA384 itself, which is checked against Wycheproof in + // `bouncycastle-hkdf`; so here its output is compared with the alias used directly. + let key_material = KeyMaterial512::from_bytes(&DUMMY_SEED[..48]).unwrap(); + let derived_key = + KDFFactory::new("HKDF-SHA384").unwrap().derive_key(&key_material, b"info").unwrap(); + let expected_key = HKDF_SHA384::new().derive_key(&key_material, b"info").unwrap(); + assert!(ct::ct_eq_bytes(derived_key.ref_to_bytes(), expected_key.ref_to_bytes())); + assert_eq!(derived_key.key_len(), 48, "HKDF-SHA384 derives a hash-length key"); + /* HKDF-SHA512 */ // Note: this value is not checked against any external reference implementation, // The value is hard-coded to ensure consistency. diff --git a/crypto/factory/tests/mac_factory_tests.rs b/crypto/factory/tests/mac_factory_tests.rs index 912a7587..a7121465 100644 --- a/crypto/factory/tests/mac_factory_tests.rs +++ b/crypto/factory/tests/mac_factory_tests.rs @@ -22,7 +22,149 @@ mod hash_factory_tests { &hex::decode("896fb1128abbdf196832107cd49df33f47b4b1169912ba4f53684b22").unwrap(), )); + // HMAC-SHA512/224 -- NIST ACVP HMAC-SHA2-512/224 2.0, tgId 1, tcId 106 (MAC truncated to 160 bits) + let key = KeyMaterial::<45>::from_bytes_as_type( + &hex::decode("a0b7276557f6880d151ea5e147fa2c29daf3104fda96ff8ee440f69e2c07a74b6eb38751fe54b08f9f4a84d1d7").unwrap(), + KeyType::MACKey, + ) + .unwrap(); + let msg = hex::decode("2579f5df03e0fccde2b515944d88dc81ca3b4a20517cdc54170559f0d2f889e2f543eacf8a84b34563d0139351ea9a77399d274c5c6c1b0f488063b7255f9df648667fe800151ef288a68d6c8c24d57abd7e4f70eed149752beae4a9763cebf03c").unwrap(); + let expected = hex::decode("6e927067f724d4fedc96b310c5115979e8dde8a4").unwrap(); + let hmac = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + assert_eq!(hmac.output_len(), 28); + assert_eq!(&hmac.mac(&msg)[..20], &expected[..]); + let hmac = + MACFactory::new(bouncycastle_sha2::hmac::HMAC_SHA512_224_NAME, &key).unwrap(); + assert_eq!(&hmac.mac(&msg)[..20], &expected[..]); + + // HMAC-SHA512/256 -- NIST ACVP HMAC-SHA2-512/256 2.0, tgId 1, tcId 147 (MAC truncated to 160 bits) + let key = KeyMaterial::<55>::from_bytes_as_type( + &hex::decode("4915691891f05dec5569ca75819daac897aaeeebb2fb04e7fc696d076feccef399f0eea660a7de4b7bb6ef7829a5f82feed70b35b40458").unwrap(), + KeyType::MACKey, + ) + .unwrap(); + let msg = hex::decode("").unwrap(); + let expected = hex::decode("7857d4737760e127f1533185c6ad183ac4e10bd9").unwrap(); + let hmac = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + assert_eq!(hmac.output_len(), 32); + assert_eq!(&hmac.mac(&msg)[..20], &expected[..]); + let hmac = + MACFactory::new(bouncycastle_sha2::hmac::HMAC_SHA512_256_NAME, &key).unwrap(); + assert_eq!(&hmac.mac(&msg)[..20], &expected[..]); + + // HMAC-SHA512/224 pass-throughs: streaming, mac_out, verify and do_verify_final. + let key = KeyMaterial::<45>::from_bytes_as_type( + &hex::decode("a0b7276557f6880d151ea5e147fa2c29daf3104fda96ff8ee440f69e2c07a74b6eb38751fe54b08f9f4a84d1d7").unwrap(), + KeyType::MACKey, + ) + .unwrap(); + let msg = hex::decode("2579f5df03e0fccde2b515944d88dc81ca3b4a20517cdc54170559f0d2f889e2f543eacf8a84b34563d0139351ea9a77399d274c5c6c1b0f488063b7255f9df648667fe800151ef288a68d6c8c24d57abd7e4f70eed149752beae4a9763cebf03c").unwrap(); + let full = MACFactory::new("HMAC-SHA512/224", &key).unwrap().mac(&msg); + assert_eq!(full.len(), 28); + assert_eq!( + &full[..20], + &hex::decode("6e927067f724d4fedc96b310c5115979e8dde8a4").unwrap()[..] + ); + + let mut hmac = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + for chunk in msg.chunks(7) { + hmac.do_update(chunk); + } + assert_eq!(hmac.do_final(), full); + + let mut out = vec![0xffu8; 28]; + assert_eq!( + MACFactory::new("HMAC-SHA512/224", &key).unwrap().mac_out(&msg, &mut out).unwrap(), + 28 + ); + assert_eq!(out, full); + + let mut out = vec![0xffu8; 28]; + let mut hmac = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + hmac.do_update(&msg); + assert_eq!(hmac.do_final_out(&mut out).unwrap(), 28); + assert_eq!(out, full); + + let mut wrong = full.clone(); + wrong[0] ^= 1; + assert!(MACFactory::new("HMAC-SHA512/224", &key).unwrap().verify(&msg, &full)); + assert!(!MACFactory::new("HMAC-SHA512/224", &key).unwrap().verify(&msg, &wrong)); + let mut hmac = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + hmac.do_update(&msg); + assert!(hmac.do_verify_final(&full)); + let mut hmac = MACFactory::new("HMAC-SHA512/224", &key).unwrap(); + hmac.do_update(&msg); + assert!(!hmac.do_verify_final(&wrong)); + + // HMAC-SHA512/256 pass-throughs: streaming, mac_out, verify and do_verify_final. + let key = KeyMaterial::<55>::from_bytes_as_type( + &hex::decode("4915691891f05dec5569ca75819daac897aaeeebb2fb04e7fc696d076feccef399f0eea660a7de4b7bb6ef7829a5f82feed70b35b40458").unwrap(), + KeyType::MACKey, + ) + .unwrap(); + let msg = hex::decode("").unwrap(); + let full = MACFactory::new("HMAC-SHA512/256", &key).unwrap().mac(&msg); + assert_eq!(full.len(), 32); + assert_eq!( + &full[..20], + &hex::decode("7857d4737760e127f1533185c6ad183ac4e10bd9").unwrap()[..] + ); + + let mut hmac = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + for chunk in msg.chunks(7) { + hmac.do_update(chunk); + } + assert_eq!(hmac.do_final(), full); + + let mut out = vec![0xffu8; 32]; + assert_eq!( + MACFactory::new("HMAC-SHA512/256", &key).unwrap().mac_out(&msg, &mut out).unwrap(), + 32 + ); + assert_eq!(out, full); + + let mut out = vec![0xffu8; 32]; + let mut hmac = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + hmac.do_update(&msg); + assert_eq!(hmac.do_final_out(&mut out).unwrap(), 32); + assert_eq!(out, full); + + let mut wrong = full.clone(); + wrong[0] ^= 1; + assert!(MACFactory::new("HMAC-SHA512/256", &key).unwrap().verify(&msg, &full)); + assert!(!MACFactory::new("HMAC-SHA512/256", &key).unwrap().verify(&msg, &wrong)); + let mut hmac = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + hmac.do_update(&msg); + assert!(hmac.do_verify_final(&full)); + let mut hmac = MACFactory::new("HMAC-SHA512/256", &key).unwrap(); + hmac.do_update(&msg); + assert!(!hmac.do_verify_final(&wrong)); + // TODO: at least one test for each type } + + #[test] + fn hmac_sm3_tests() { + // RFC4231 Test Case 1 key/message; expected value from `openssl dgst -sm3 -mac HMAC`, + // confirmed with bc-java's HMac(new SM3Digest()). + let key = KeyMaterial::<32>::from_bytes_as_type( + &hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(), + KeyType::MACKey, + ) + .unwrap(); + for name in ["HMAC-SM3", bouncycastle_sm3::hmac::HMAC_SM3_NAME] { + let hmac = MACFactory::new(name, &key).unwrap(); + assert_eq!(hmac.output_len(), 32); + assert!( + hmac.verify( + b"Hi There", + &hex::decode( + "51b00d1fb49832bfb01c3ce27848e59f871d9ba938dc563b338ca964755cce70" + ) + .unwrap(), + ) + ); + } + } } } diff --git a/crypto/factory/tests/rng_factory_tests.rs b/crypto/factory/tests/rng_factory_tests.rs index 6e5d253f..5bd2c728 100644 --- a/crypto/factory/tests/rng_factory_tests.rs +++ b/crypto/factory/tests/rng_factory_tests.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod tests { - use bouncycastle_core::traits::{RNG, SecurityStrength}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::RNG; use bouncycastle_factory as factory; use bouncycastle_factory::AlgorithmFactory; 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/hex/src/lib.rs b/crypto/hex/src/lib.rs index 923151d7..4dbaf2c8 100644 --- a/crypto/hex/src/lib.rs +++ b/crypto/hex/src/lib.rs @@ -119,7 +119,10 @@ pub fn decode_out>(input: T, out: &mut [u8]) -> Result { - if inref[i + 1] == b'x' { + // A backslash is only ever skippable as the `\x` prefix of an escaped byte. One + // that ends the input, or is followed by anything else, falls through to the + // digit lookup below and is reported as an invalid character at its own index. + if i + 1 < inref.len() && inref[i + 1] == b'x' { i += 2; continue; } diff --git a/crypto/hex/tests/hex_tests.rs b/crypto/hex/tests/hex_tests.rs index ae6b9985..7fbf00a6 100644 --- a/crypto/hex/tests/hex_tests.rs +++ b/crypto/hex/tests/hex_tests.rs @@ -98,6 +98,12 @@ fn decode_test() { Err(_) => {} } + // A backslash that is not the `\x` of an escaped byte is an invalid character, including one + // that ends the input: the decoder must not read past the end looking for the `x`. + assert!(matches!(hex::decode("ab\\"), Err(HexError::InvalidHexCharacter(2)))); + assert!(matches!(hex::decode("\\"), Err(HexError::InvalidHexCharacter(0)))); + assert!(matches!(hex::decode("ab\\\\x01"), Err(HexError::InvalidHexCharacter(2)))); + /* test other bytes-like input formats */ assert_eq!( hex::decode(b"\x30\x30\x30\x31\x30\x32\x30\x33").unwrap(), diff --git a/crypto/hkdf/Cargo.toml b/crypto/hkdf/Cargo.toml index 6df444bd..90d392ef 100644 --- a/crypto/hkdf/Cargo.toml +++ b/crypto/hkdf/Cargo.toml @@ -8,14 +8,9 @@ bouncycastle-core.workspace = true bouncycastle-hmac.workspace = true bouncycastle-utils.workspace = true -# The concrete HKDF instantiations (HKDF_SHA256, HKDF_SHA512) live in bouncycastle-sha2, which makes -# that crate depend on this one, so this crate must not depend on it. bouncycastle-sha2 is a -# dev-dependency so that the tests, benches and doc examples here can still exercise HKDF over the -# library's own hashes; Cargo permits cycles through dev-dependencies. -# todo -- we're about to change that and move them to their respective crates in the next phase. [dev-dependencies] bouncycastle-core-test-framework.workspace = true -criterion.workspace = true bouncycastle-rng.workspace = true bouncycastle-hex.workspace = true bouncycastle-sha2.workspace = true +criterion.workspace = true diff --git a/crypto/hkdf/src/lib.rs b/crypto/hkdf/src/lib.rs index 8d7dc8ac..0d74a157 100644 --- a/crypto/hkdf/src/lib.rs +++ b/crypto/hkdf/src/lib.rs @@ -6,9 +6,9 @@ //! point through which a hash declares the metadata from which the HKDF instance is built. //! The library provides the following concrete instantiations of HKDF: //! -//! | Hash family | Instantiations | -//! |-------------|---------------------------------------------------------------| -//! | SHA-2 | `bouncycastle_sha2::hkdf` -- `HKDF_SHA256`, `HKDF_SHA512` | +//! | Hash family | Instantiations | +//! |-------------|--------------------------------------------------------------------------| +//! | SHA-2 | `bouncycastle_sha2::hkdf` -- `HKDF_SHA256`, `HKDF_SHA384`, `HKDF_SHA512` | //! //! # Instantiating HKDF over a hash //! @@ -39,48 +39,48 @@ //! //! ## Worked example //! -//! As an example, the `bouncycastle-sha2` crate instantiates HKDF-SHA256 and HKDF-SHA512 this way. The library does not ship -//! HKDF-SHA384, so that makes a good illustration of adding one -- for a hash in this library or for -//! a hash of your own, the shape is identical: +//! As an example, the `bouncycastle-sha2` crate instantiates HKDF-SHA256, HKDF-SHA384 and +//! HKDF-SHA512 this way. The library does not ship HKDF-SHA224, so that makes a good illustration of +//! adding one -- for a hash in this library or for a hash of your own, the shape is identical: //! //! ``` //! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; //! use bouncycastle_core::traits::{KDF, SuspendableKeyed}; //! use bouncycastle_hkdf::HKDF; -//! use bouncycastle_sha2::{SHA384, SUSPENDED_SHA512_STATE_LEN}; +//! use bouncycastle_sha2::{SHA224, SUSPENDED_SHA256_STATE_LEN}; //! -//! // SHA-384 is a member of the SHA-512 family, so its suspended state is the SHA-512 one. -//! const SUSPENDED_HKDF_SHA384_STATE_LEN: usize = SUSPENDED_SHA512_STATE_LEN + 14; +//! // SHA-224 is a member of the SHA-256 family, so its suspended state is the SHA-256 one. +//! const SUSPENDED_HKDF_SHA224_STATE_LEN: usize = SUSPENDED_SHA256_STATE_LEN + 14; //! //! #[allow(non_camel_case_types)] -//! pub type HKDF_SHA384 = -//! HKDF; +//! pub type HKDF_SHA224 = +//! HKDF; //! -//! pub const HKDF_SHA384_NAME: &str = "HKDF-SHA384"; +//! pub const HKDF_SHA224_NAME: &str = "HKDF-SHA224"; //! //! // That is all it takes: the KDF trait and the extract/expand API are now available. //! let ikm = KeyMaterial256::from_bytes_as_type( //! b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f", //! KeyType::Seed).unwrap(); -//! let okm = HKDF_SHA384::new().derive_key(&ikm, b"extra input").unwrap(); +//! let okm = HKDF_SHA224::new().derive_key(&ikm, b"extra input").unwrap(); //! -//! // ...and so is suspend/resume, because SHA-384 implements Suspendable and the two const +//! // ...and so is suspend/resume, because SHA-224 implements Suspendable and the two const //! // parameters above agree. //! let salt = KeyMaterial256::from_bytes_as_type( //! b"\x0f\x0e\x0d\x0c\x0b\x0a\x09\x08\x07\x06\x05\x04\x03\x02\x01\x00", //! KeyType::MACKey).unwrap(); -//! let mut hkdf = HKDF_SHA384::new(); +//! let mut hkdf = HKDF_SHA224::new(); //! hkdf.do_extract_init(&salt).unwrap(); //! hkdf.do_extract_update_bytes(b"part 1").unwrap(); //! let suspended = hkdf.suspend(); -//! assert_eq!(suspended.len(), SUSPENDED_HKDF_SHA384_STATE_LEN); +//! assert_eq!(suspended.len(), SUSPENDED_HKDF_SHA224_STATE_LEN); //! -//! let mut resumed = HKDF_SHA384::from_suspended(suspended, &salt).unwrap(); +//! let mut resumed = HKDF_SHA224::from_suspended(suspended, &salt).unwrap(); //! resumed.do_extract_update_bytes(b"part 2").unwrap(); //! let _prk = resumed.do_extract_final().unwrap(); //! ``` //! -//! # Security Considerations +//! # 🚨 Security Considerations 🚨 //! //! These apply to every instantiation; `bouncycastle_sha2::hkdf` repeats the ones that matter most in //! day-to-day use. @@ -107,15 +107,14 @@ #![forbid(missing_docs)] use bouncycastle_core::errors::{KDFError, KeyMaterialError, MACError, SuspendableError}; -use bouncycastle_core::key_material; +use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterial0, KeyMaterial512, KeyMaterialTrait, KeyType, }; -use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; -use bouncycastle_core::traits::{ - Hash, HashAlgParams, KDF, MAC, SecurityStrength, Suspendable, SuspendableKeyed, -}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Hash, HashAlgParams, KDF, MAC, Suspendable, SuspendableKeyed}; use bouncycastle_hmac::HMAC; +use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_utils::{max, min}; use std::marker::PhantomData; // Imports needed only for docs @@ -396,7 +395,7 @@ impl::new(&prk_as_mac_key) @@ -416,26 +415,29 @@ impl::new(&prk_as_mac_key)?; hmac.do_update(&T[..t_len]); hmac.do_update(info); hmac.do_update(&[i]); - t_len = hmac.do_final_out(&mut T[..remaining])?; - debug_assert_eq!(t_len, remaining); // this will be true for every iteration after T(0) / T(1) + t_len = hmac.do_final_out(&mut T[..hash_len])?; + debug_assert_eq!(t_len, hash_len); - key_material::do_hazardous_operations(okm, |okm| { + do_hazardous_operations(okm, |okm| { let out = okm.ref_to_bytes_mut()?; - out[bytes_written..bytes_written + t_len].copy_from_slice(&T[..t_len]); + out[bytes_written..bytes_written + remaining].copy_from_slice(&T[..remaining]); Ok(()) })?; - bytes_written += t_len; + bytes_written += remaining; // Set the KeyType of the output // Since some computation has been performed, the result will not actually be zeroized, even if all input key material was zeroized. - key_material::do_hazardous_operations(okm, |okm| { + do_hazardous_operations(okm, |okm| { if prk.key_type() == KeyType::Zeroized { okm.set_key_type(KeyType::Unknown)?; } else { @@ -574,7 +576,7 @@ impl::new(); - key_material::do_hazardous_operations(&mut ikm_key, |ikm_key| { + do_hazardous_operations(&mut ikm_key, |ikm_key| { // just for testing, ignore the error about zeroized keys ikm_key.set_bytes_as_type(&hex::decode(ikm).unwrap(), KeyType::CryptographicRandom) }) .unwrap(); let mut salt_key = KeyMaterial::<100>::new(); - key_material::do_hazardous_operations(&mut salt_key, |salt_key| { + do_hazardous_operations(&mut salt_key, |salt_key| { // just for testing, ignore the error about zeroized keys salt_key.set_bytes_as_type(&hex::decode(salt).unwrap(), KeyType::MACKey) }) @@ -543,10 +544,8 @@ mod hkdf_tests { // Some of the RFC5896 test vectors have input keys that are too short to meet the entropy seeding rules. // So, just for testing, we'll bump this up to full entropy, regardless of what entropy HKDF::extract() // thinks it should be based on the inputs. - key_material::do_hazardous_operations(&mut prk_key, |prk_key| { - prk_key.set_key_type(KeyType::MACKey) - }) - .unwrap(); + do_hazardous_operations(&mut prk_key, |prk_key| prk_key.set_key_type(KeyType::MACKey)) + .unwrap(); let mut okm_key = KeyMaterial::<100>::new(); _ = HKDF_SHA256::expand_out(&prk_key, &info, L, &mut okm_key).unwrap(); @@ -678,7 +677,7 @@ mod hkdf_tests { // SP800-56Cr2 tcId 1 let mut salt = KeyMaterial::<128>::new(); // have to do it this way for it to accept a zeroized key - key_material::do_hazardous_operations(&mut salt, |salt| { + do_hazardous_operations(&mut salt, |salt| { salt.set_bytes_as_type(&hex::decode("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), KeyType::MACKey) }).unwrap(); @@ -827,4 +826,35 @@ mod hkdf_tests { Err(SuspendableError::InvalidData) )); } + + /// RFC 5869 Sec 2.3: "OKM = first L octets of T", for any L up to 255 * HashLen, so a shorter + /// output is a prefix of a longer one. Every L from 0 to 200 covers each value of L mod HashLen + /// for both hashes: the last block used to be produced as an HMAC truncated to the remaining + /// length, which HMAC refuses below 4 bytes, so every L with L mod HashLen in 1..=3 failed. + #[test] + fn every_output_length_is_a_prefix_of_a_longer_one() { + let salt = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::MACKey).unwrap(); + let ikm = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[32..64], KeyType::Seed).unwrap(); + const MAX_L: usize = 200; + + let mut full = KeyMaterial::::new(); + HKDF_SHA256::extract_and_expand_out(&salt, &ikm, b"info", MAX_L, &mut full).unwrap(); + for l in 0..=MAX_L { + let mut okm = KeyMaterial::::new(); + let n = HKDF_SHA256::extract_and_expand_out(&salt, &ikm, b"info", l, &mut okm) + .unwrap_or_else(|e| panic!("HKDF-SHA256, L = {l}: {e:?}")); + assert_eq!(n, l, "HKDF-SHA256, L = {l}: bytes written"); + assert_eq!(okm.ref_to_bytes(), &full.ref_to_bytes()[..l], "HKDF-SHA256, L = {l}"); + } + + let mut full = KeyMaterial::::new(); + HKDF_SHA512::extract_and_expand_out(&salt, &ikm, b"info", MAX_L, &mut full).unwrap(); + for l in 0..=MAX_L { + let mut okm = KeyMaterial::::new(); + let n = HKDF_SHA512::extract_and_expand_out(&salt, &ikm, b"info", l, &mut okm) + .unwrap_or_else(|e| panic!("HKDF-SHA512, L = {l}: {e:?}")); + assert_eq!(n, l, "HKDF-SHA512, L = {l}: bytes written"); + assert_eq!(okm.ref_to_bytes(), &full.ref_to_bytes()[..l], "HKDF-SHA512, L = {l}"); + } + } } diff --git a/crypto/hkdf/tests/hkdf_wycheproof.rs b/crypto/hkdf/tests/hkdf_wycheproof.rs new file mode 100644 index 00000000..85e161cb --- /dev/null +++ b/crypto/hkdf/tests/hkdf_wycheproof.rs @@ -0,0 +1,115 @@ +//! Known-answer tests against Project Wycheproof's +//! `testvectors_v1/hkdf_sha{256,384,512}_test.json`. (`hkdf_sha1_test.json` has no counterpart +//! here.) +//! +//! Requires the Wycheproof repository (https://github.com/C2SP/wycheproof) to be cloned alongside +//! this repository, i.e. at `../wycheproof` relative to the root of this git project. If it is +//! absent the tests print a warning and pass, matching the convention used by the other vector +//! suites. +//! +//! A `valid` case must produce exactly `okm`. The only `invalid` cases ask for one byte more than +//! RFC 5869 Sec 2.3's limit of 255 * HashLen, which must be refused with `KDFError::InvalidLength`. + +use bouncycastle_core::errors::KDFError; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{Hash, HashAlgParams}; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; +use bouncycastle_hkdf::HKDF; +use bouncycastle_sha2::hkdf::{ + SUSPENDED_HKDF_SHA256_STATE_LEN, SUSPENDED_HKDF_SHA384_STATE_LEN, + SUSPENDED_HKDF_SHA512_STATE_LEN, +}; +use bouncycastle_sha2::{ + SHA256, SHA384, SHA512, SUSPENDED_SHA256_STATE_LEN, SUSPENDED_SHA512_STATE_LEN, +}; + +/// The longest IKM or salt in any of the files is 80 bytes. +const MAX_INPUT_LEN: usize = 80; +/// 255 * 64, the most HKDF-SHA-512 can produce. +const MAX_OKM_LEN: usize = 255 * 64; + +/// Runs every case in one `hkdf_*_test.json` file through `HKDF`. The state lengths are +/// those of `bouncycastle_sha2::hkdf`'s aliases, which cannot be passed as a type here. +fn run< + H: Hash + HashAlgParams + Default, + const HASH_STATE_LEN: usize, + const HKDF_STATE_LEN: usize, +>( + filename: &str, + algorithm: &str, +) { + let Some(doc) = wycheproof_json(filename) else { return }; + + assert_eq!(doc.get("algorithm").and_then(Value::as_str), Some(algorithm), "{filename}"); + + let (mut valid_count, mut invalid_count) = (0usize, 0usize); + for group in doc.get("testGroups").and_then(Value::as_array).expect("testGroups") { + 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 ctx = format!("{filename} tcId {tc_id}"); + let ikm = KeyMaterial::::from_bytes_as_type( + &hex_field(test, "ikm", tc_id), + KeyType::Seed, + ) + .expect("ikm fits"); + // An empty salt is an absent one: a zero-length KeyMaterial. + let salt = KeyMaterial::::from_bytes_as_type( + &hex_field(test, "salt", tc_id), + KeyType::MACKey, + ) + .expect("salt fits"); + let info = hex_field(test, "info", tc_id); + let size = test.get("size").and_then(Value::as_u64).expect("size") as usize; + + let mut okm = KeyMaterial::::new(); + let result = HKDF::::extract_and_expand_out( + &salt, &ikm, &info, size, &mut okm, + ); + + match test.get("result").and_then(Value::as_str).expect("result") { + "valid" => { + let written = result.unwrap_or_else(|e| panic!("{ctx}: {e:?}")); + assert_eq!(written, size, "{ctx}: bytes written"); + assert_eq!( + okm.ref_to_bytes(), + &hex_field(test, "okm", tc_id)[..], + "{ctx}: okm" + ); + valid_count += 1; + } + "invalid" => { + assert!( + matches!(result, Err(KDFError::InvalidLength(_))), + "{ctx}: expected InvalidLength, got {result:?}" + ); + invalid_count += 1; + } + other => panic!("{ctx}: unexpected result {other}"), + } + } + } + + println!("Wycheproof {algorithm}: {valid_count} valid and {invalid_count} invalid cases run"); + assert!(valid_count > 0 && invalid_count > 0, "{filename}: expected both valid and invalid"); +} + +#[test] +fn wycheproof_hkdf_sha256() { + run::( + "hkdf_sha256_test.json", "HKDF-SHA-256", + ); +} + +#[test] +fn wycheproof_hkdf_sha384() { + run::( + "hkdf_sha384_test.json", "HKDF-SHA-384", + ); +} + +#[test] +fn wycheproof_hkdf_sha512() { + run::( + "hkdf_sha512_test.json", "HKDF-SHA-512", + ); +} diff --git a/crypto/hmac/Cargo.toml b/crypto/hmac/Cargo.toml index 44d1eed1..0f4d97ee 100644 --- a/crypto/hmac/Cargo.toml +++ b/crypto/hmac/Cargo.toml @@ -7,14 +7,11 @@ edition.workspace = true bouncycastle-core.workspace = true bouncycastle-utils.workspace = true -# bouncycastle-sha2, -sha3 and -rng are dev-dependencies so that the tests, benches and doc examples -# here can still exercise HMAC over the library's own hashes; Cargo permits cycles through -# dev-dependencies. -# todo -- we're about to change that and move them to their respective crates in the next phase. [dev-dependencies] bouncycastle-core-test-framework.workspace = true -criterion.workspace = true bouncycastle-hex.workspace = true bouncycastle-rng.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true +bouncycastle-sm3.workspace = true +criterion.workspace = true diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index 8de8bc23..11cf01e1 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -62,7 +62,7 @@ //! [`HMACParams`] is deliberately **not** sealed, so the same recipe works for a hash function //! defined in any other crate. Simply follow the recipe above! //! -//! # Security Considerations +//! # 🚨 Security Considerations 🚨 //! //! These apply to every instantiation; the hash crates' `hmac` modules repeat the ones that matter //! most in day-to-day use. @@ -92,9 +92,9 @@ use bouncycastle_core::errors::{KeyMaterialError, MACError, RNGError, SuspendableError}; use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, Hash, HashAlgParams, MAC, RNG, SecurityStrength, Suspendable, - SuspendableKeyed, + Algorithm, AlgorithmOID, Hash, HashAlgParams, MAC, RNG, Suspendable, SuspendableKeyed, }; use bouncycastle_utils::{ct, secret::Secret}; use core::fmt::{Debug, Display, Formatter}; diff --git a/crypto/hmac/tests/hmac_tests.rs b/crypto/hmac/tests/hmac_tests.rs index 0cfbe415..481b0e42 100644 --- a/crypto/hmac/tests/hmac_tests.rs +++ b/crypto/hmac/tests/hmac_tests.rs @@ -1,11 +1,12 @@ #[cfg(test)] mod hmac_tests { use bouncycastle_core::errors::{KeyMaterialError, MACError, RNGError}; - use bouncycastle_core::key_material; + use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; - use bouncycastle_core::traits::{Algorithm, Hash, MAC, SecurityStrength}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{Algorithm, Hash, MAC}; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_core_test_framework::mac::TestFrameworkMAC; use bouncycastle_hex as hex; @@ -15,12 +16,14 @@ mod hmac_tests { use bouncycastle_sha2::*; use bouncycastle_sha3::hmac::*; use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; + use bouncycastle_sm3::SM3; + use bouncycastle_sm3::hmac::*; #[test] fn simple_tests() { // Simple test with zero-length key let mut zero_length_key = KeyMaterial256::default(); - key_material::do_hazardous_operations(&mut zero_length_key, |zero_length_key| { + do_hazardous_operations(&mut zero_length_key, |zero_length_key| { zero_length_key.set_key_type(KeyType::MACKey) }) .unwrap(); @@ -77,6 +80,12 @@ mod hmac_tests { _ = HMAC::::new(&key).unwrap(); _ = HMAC_SHA512::new(&key).unwrap(); + _ = HMAC::::new(&key).unwrap(); + _ = HMAC_SHA512_224::new(&key).unwrap(); + + _ = HMAC::::new(&key).unwrap(); + _ = HMAC_SHA512_256::new(&key).unwrap(); + _ = HMAC::::new(&key).unwrap(); _ = HMAC_SHA3_224::new(&key).unwrap(); @@ -88,6 +97,9 @@ mod hmac_tests { _ = HMAC::::new(&key).unwrap(); _ = HMAC_SHA3_512::new(&key).unwrap(); + + _ = HMAC::::new(&key).unwrap(); + _ = HMAC_SM3::new(&key).unwrap(); } #[test] @@ -179,8 +191,7 @@ mod hmac_tests { HMAC_SHA256::new_allow_weak_key(&zero_key).unwrap(); // non-zero len key of all-zero bytes - key_material::do_hazardous_operations(&mut zero_key, |zero_key| zero_key.set_key_len(32)) - .unwrap(); + do_hazardous_operations(&mut zero_key, |zero_key| zero_key.set_key_len(32)).unwrap(); HMAC_SHA256::new_allow_weak_key(&zero_key).unwrap(); // Note: zero-len keys that are not Zeroized or MACKey are not allowed @@ -282,10 +293,111 @@ mod hmac_tests { assert_eq!(HMAC_SHA256::ALG_NAME, HMAC_SHA256_NAME); assert_eq!(HMAC_SHA384::ALG_NAME, HMAC_SHA384_NAME); assert_eq!(HMAC_SHA512::ALG_NAME, HMAC_SHA512_NAME); + assert_eq!(HMAC_SHA512_224::ALG_NAME, HMAC_SHA512_224_NAME); + assert_eq!(HMAC_SHA512_256::ALG_NAME, HMAC_SHA512_256_NAME); + assert_eq!(HMAC_SHA512_224_NAME, "HMAC-SHA512/224"); + assert_eq!(HMAC_SHA512_256_NAME, "HMAC-SHA512/256"); + assert_eq!(HMAC_SHA512_224::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); + assert_eq!(HMAC_SHA512_256::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); assert_eq!(HMAC_SHA3_224::ALG_NAME, HMAC_SHA3_224_NAME); assert_eq!(HMAC_SHA3_256::ALG_NAME, HMAC_SHA3_256_NAME); assert_eq!(HMAC_SHA3_384::ALG_NAME, HMAC_SHA3_384_NAME); assert_eq!(HMAC_SHA3_512::ALG_NAME, HMAC_SHA3_512_NAME); + assert_eq!(HMAC_SM3::ALG_NAME, HMAC_SM3_NAME); + } + + #[cfg(test)] + mod acvp_sha512t { + use super::*; + + /// NIST ACVP known-answer tests for HMAC-SHA2-512/224, from the ACVP-Server repository + /// (gen-val/json-files/HMAC-SHA2-512-224-2.0/internalProjection.json, vsId 0). + /// The published vectors only carry MACs truncated to at most 160 bits (ACVP "macLen"), so the + /// leading bytes of the full 224-bit MAC are compared. The second case uses a key longer than the + /// 1024-bit block, which exercises the RFC 2104 pre-hashing of the key. + #[test] + fn hmac_sha512_224() { + // tgId 1, tcId 106: 45-byte key, MAC truncated to 160 bits + let key = KeyMaterial::<45>::from_bytes_as_type( + &hex::decode("a0b7276557f6880d151ea5e147fa2c29daf3104fda96ff8ee440f69e2c07a74b6eb38751fe54b08f9f4a84d1d7").unwrap(), + KeyType::MACKey, + ) + .unwrap(); + let msg = hex::decode("2579f5df03e0fccde2b515944d88dc81ca3b4a20517cdc54170559f0d2f889e2f543eacf8a84b34563d0139351ea9a77399d274c5c6c1b0f488063b7255f9df648667fe800151ef288a68d6c8c24d57abd7e4f70eed149752beae4a9763cebf03c").unwrap(); + let expected = hex::decode("6e927067f724d4fedc96b310c5115979e8dde8a4").unwrap(); + let full = HMAC_SHA512_224::new(&key).unwrap().mac(&msg); + assert_eq!(full.len(), 28); + assert_eq!(&full[..20], &expected[..]); + // the same vector through the streaming API in uneven chunks + let mut mac = HMAC_SHA512_224::new(&key).unwrap(); + for chunk in msg.chunks(13) { + mac.do_update(chunk); + } + assert_eq!(mac.do_final(), full); + + // tgId 1, tcId 110: 247-byte key (longer than the block, so pre-hashed), MAC truncated to 160 bits + let key = KeyMaterial::<247>::from_bytes_as_type( + &hex::decode("0791758d5d91b0108e885039e997dc32c41a0f986b1820d1f8c4c3da0ae6d88da58d91e1732942bb401eddc59ba1a39ee6cca8824705619873e9b6a04cf02e6b4debdb8c35c3fe6d9c569ecdb193baaf6510ca39522679811ac7a57297df11deeb8e58555108aeb106faa8c0867c5f185b4e7f5ece1afaa5412d95e47505684517254911ac15fde56e99534ccbbaaeb0ab1a77ff252903359f046b4eed1d4b5a47747b352c0b33d24da587d24f9aaaac7b8301c05fb0ba925a761cdfe74b8af66ca3e776662a33addad6b0dfbc5dabbce3529a7813b7fd2feae25f5fb80da8fd844430fb578eff15fb15775cdfa575b9d6d5ed90490f3a").unwrap(), + KeyType::MACKey, + ) + .unwrap(); + let msg = hex::decode("dedb0cc1c2a9b960d3").unwrap(); + let expected = hex::decode("9cf6def15b5ead939e1fda675b52147a01a6ccb6").unwrap(); + let full = HMAC_SHA512_224::new(&key).unwrap().mac(&msg); + assert_eq!(full.len(), 28); + assert_eq!(&full[..20], &expected[..]); + // the same vector through the streaming API in uneven chunks + let mut mac = HMAC_SHA512_224::new(&key).unwrap(); + for chunk in msg.chunks(13) { + mac.do_update(chunk); + } + assert_eq!(mac.do_final(), full); + } + + /// NIST ACVP known-answer tests for HMAC-SHA2-512/256, from the ACVP-Server repository + /// (gen-val/json-files/HMAC-SHA2-512-256-2.0/internalProjection.json, vsId 0). + /// The published vectors only carry MACs truncated to at most 160 bits (ACVP "macLen"), so the + /// leading bytes of the full 256-bit MAC are compared. The second case uses a key longer than the + /// 1024-bit block, which exercises the RFC 2104 pre-hashing of the key. + #[test] + fn hmac_sha512_256() { + // tgId 1, tcId 147: 55-byte key, MAC truncated to 160 bits + let key = KeyMaterial::<55>::from_bytes_as_type( + &hex::decode("4915691891f05dec5569ca75819daac897aaeeebb2fb04e7fc696d076feccef399f0eea660a7de4b7bb6ef7829a5f82feed70b35b40458").unwrap(), + KeyType::MACKey, + ) + .unwrap(); + let msg = hex::decode("").unwrap(); + let expected = hex::decode("7857d4737760e127f1533185c6ad183ac4e10bd9").unwrap(); + let full = HMAC_SHA512_256::new(&key).unwrap().mac(&msg); + assert_eq!(full.len(), 32); + assert_eq!(&full[..20], &expected[..]); + // the same vector through the streaming API in uneven chunks + let mut mac = HMAC_SHA512_256::new(&key).unwrap(); + for chunk in msg.chunks(13) { + mac.do_update(chunk); + } + assert_eq!(mac.do_final(), full); + + // tgId 1, tcId 106: 245-byte key (longer than the block, so pre-hashed), MAC truncated to 160 bits + let key = KeyMaterial::<245>::from_bytes_as_type( + &hex::decode("98d135e3cc6dffc2524a8a6c186cd0584eede3a734148b453199f71154bb3b96a315a037597c72f5081a17b2ef9990c065c2aaa65226c939098f603e6307dd69fc7906a82c361af89336cefe4d95d491d85b193125380fa9becd6e7475052cd7196447c32b681b7ef3cfde62d087067703d5438fdff6ce443c321048b50ec771999f85540cd8671cebf828f37d4cdbce1523823d77c5769fb8549b938406771cc35caeac561b9b8613ba5556958799d8c5954e2c2a8ace484bdc6fa75e7ad7404ebe7b1724a164634fadc8450dc27b28fcfa0e5c46c5da3e73d34dba7fea33db00631811b096d2d4f194f204c9421b9996ef929156").unwrap(), + KeyType::MACKey, + ) + .unwrap(); + let msg = + hex::decode("9268f10c36fd3366012e841260e60227a968f6c8546dee6abc83b3").unwrap(); + let expected = hex::decode("3288232187dcf1ea421f5c12bdeb4fd9d0a0a25b").unwrap(); + let full = HMAC_SHA512_256::new(&key).unwrap().mac(&msg); + assert_eq!(full.len(), 32); + assert_eq!(&full[..20], &expected[..]); + // the same vector through the streaming API in uneven chunks + let mut mac = HMAC_SHA512_256::new(&key).unwrap(); + for chunk in msg.chunks(13) { + mac.do_update(chunk); + } + assert_eq!(mac.do_final(), full); + } } #[cfg(test)] @@ -297,7 +409,7 @@ mod hmac_tests { fn hmac_sha224() { let test_framework = TestFrameworkMAC::new(); let mut zero_length_key = KeyMaterial256::default(); - key_material::do_hazardous_operations(&mut zero_length_key, |zero_length_key| { + do_hazardous_operations(&mut zero_length_key, |zero_length_key| { zero_length_key.set_key_type(KeyType::MACKey) }) .unwrap(); @@ -375,7 +487,7 @@ mod hmac_tests { // test with zero-length key let test_framework = TestFrameworkMAC::new(); let mut zero_length_key = KeyMaterial256::default(); - key_material::do_hazardous_operations(&mut zero_length_key, |zero_length_key| { + do_hazardous_operations(&mut zero_length_key, |zero_length_key| { zero_length_key.set_key_type(KeyType::MACKey) }) .unwrap(); @@ -454,7 +566,7 @@ mod hmac_tests { // test with zero-length key let test_framework = TestFrameworkMAC::new(); let mut zero_length_key = KeyMaterial256::default(); - key_material::do_hazardous_operations(&mut zero_length_key, |zero_length_key| { + do_hazardous_operations(&mut zero_length_key, |zero_length_key| { zero_length_key.set_key_type(KeyType::MACKey) }) .unwrap(); @@ -531,7 +643,7 @@ mod hmac_tests { // test with zero-length key let test_framework = TestFrameworkMAC::new(); let mut zero_length_key = KeyMaterial256::default(); - key_material::do_hazardous_operations(&mut zero_length_key, |zero_length_key| { + do_hazardous_operations(&mut zero_length_key, |zero_length_key| { zero_length_key.set_key_type(KeyType::MACKey) }) .unwrap(); @@ -605,12 +717,68 @@ mod hmac_tests { } } + /// HMAC-SM3 known answers. There is no RFC 4231 equivalent for SM3, so these reuse the RFC 4231 + /// keys/messages (cases 1, 2 and 6) with expected values generated by + /// `openssl dgst -sm3 -mac HMAC` and independently confirmed with bc-java's + /// `HMac(new SM3Digest())`, plus a zero-length key. + #[test] + fn hmac_sm3_known_answers() { + use bouncycastle_core::key_material::KeyMaterial; + let test_framework = TestFrameworkMAC::new(); + + // RFC4231 Test Case 1 key/message + test_framework.test_mac::( + &KeyMaterial::<20>::from_bytes_as_type( + &hex::decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b").unwrap(), + KeyType::MACKey, + ) + .unwrap(), + b"Hi There", + &hex::decode("51b00d1fb49832bfb01c3ce27848e59f871d9ba938dc563b338ca964755cce70") + .unwrap(), + ); + // RFC4231 Test Case 2 key/message + test_framework.test_mac::( + &KeyMaterial::<4>::from_bytes_as_type(b"Jefe", KeyType::MACKey).unwrap(), + b"what do ya want for nothing?", + &hex::decode("2e87f1d16862e6d964b50a5200bf2b10b764faa9680a296a2405f24bec39f882") + .unwrap(), + ); + // RFC4231 Test Case 6 key/message: key larger than the 64-byte block, so it is hashed first + test_framework.test_mac::( + &KeyMaterial::<131>::from_bytes_as_type(&[0xaa; 131], KeyType::MACKey).unwrap(), + b"Test Using Larger Than Block-Size Key - Hash Key First", + &hex::decode("b4fd844e13342002f0b2e0690ea7741f1497d993a70494cea601e657bedf67a0") + .unwrap(), + ); + + // zero-length key (weak; needs new_allow_weak_key) + let mut zero_length_key = KeyMaterial256::default(); + do_hazardous_operations(&mut zero_length_key, |k| k.set_key_type(KeyType::MACKey)).unwrap(); + let mut mac = HMAC_SM3::new_allow_weak_key(&zero_length_key).unwrap(); + mac.do_update(b"abc"); + assert_eq!( + mac.do_final(), + hex::decode("36525058ca466791502435c910517f1a7e86613d5f35ac1f18a94def0eaac81f") + .unwrap() + ); + + assert_eq!( + HMAC_SM3::new( + &KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::MACKey).unwrap() + ) + .unwrap() + .output_len(), + 32 + ); + } + #[test] fn suspendable_keyed_state() { use bouncycastle_core::errors::SuspendableError; - use bouncycastle_core::suspendable_state::LIB_VERSION; use bouncycastle_core::traits::SuspendableKeyed; use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableKeyedState; + use bouncycastle_utils::suspendable_state::LIB_VERSION; let key = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::MACKey).unwrap(); let msg = b"Colorless green ideas sleep furiously"; @@ -660,7 +828,10 @@ mod hmac_tests { round_trip(HMAC_SHA256::new(&key).unwrap(), &key, msg); round_trip(HMAC_SHA512::new(&key).unwrap(), &key, msg); + round_trip(HMAC_SHA512_224::new(&key).unwrap(), &key, msg); + round_trip(HMAC_SHA512_256::new(&key).unwrap(), &key, msg); round_trip(HMAC_SHA3_256::new(&key).unwrap(), &key, msg); + round_trip(HMAC_SM3::new(&key).unwrap(), &key, msg); // test suspend / resume with a key larger than block size let long_key = @@ -716,10 +887,13 @@ mod hmac_tests { keygen_test!(keygen_hmac_sha256, HMAC_SHA256, 32); keygen_test!(keygen_hmac_sha384, HMAC_SHA384, 48); keygen_test!(keygen_hmac_sha512, HMAC_SHA512, 64); + keygen_test!(keygen_hmac_sha512_224, HMAC_SHA512_224, 28); + keygen_test!(keygen_hmac_sha512_256, HMAC_SHA512_256, 32); keygen_test!(keygen_hmac_sha3_224, HMAC_SHA3_224, 28); keygen_test!(keygen_hmac_sha3_256, HMAC_SHA3_256, 32); keygen_test!(keygen_hmac_sha3_384, HMAC_SHA3_384, 48); keygen_test!(keygen_hmac_sha3_512, HMAC_SHA3_512, 64); + keygen_test!(keygen_hmac_sm3, HMAC_SM3, 32); /// `keygen_from_rng` must refuse an RNG whose security strength is below the strength the HMAC /// claims, otherwise the returned key would be tagged stronger than the entropy behind it. diff --git a/crypto/hmac/tests/hmac_wycheproof.rs b/crypto/hmac/tests/hmac_wycheproof.rs new file mode 100644 index 00000000..05d4bcf3 --- /dev/null +++ b/crypto/hmac/tests/hmac_wycheproof.rs @@ -0,0 +1,133 @@ +//! Known-answer tests against Project Wycheproof's `testvectors_v1/hmac_*_test.json` for every +//! HMAC this library instantiates: SHA-224/256/384/512, SHA-512/224, SHA-512/256, +//! SHA3-224/256/384/512 and SM3. (`hmac_sha1_test.json` has no counterpart here.) +//! +//! Requires the Wycheproof repository (https://github.com/C2SP/wycheproof) to be cloned alongside +//! this repository, i.e. at `../wycheproof` relative to the root of this git project. If it is +//! absent the tests print a warning and pass, matching the convention used by the other vector +//! suites. +//! +//! Each file has a full-length-tag group and a truncated-tag group (half the output length). A +//! full-length tag is checked through `verify`. A truncated tag is checked through `mac_out` into a +//! buffer of the tag's length, and `verify` must reject it: it requires the full output length. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::MAC; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; +use bouncycastle_sha2::hmac::{ + HMAC_SHA224, HMAC_SHA256, HMAC_SHA384, HMAC_SHA512, HMAC_SHA512_224, HMAC_SHA512_256, +}; +use bouncycastle_sha3::hmac::{HMAC_SHA3_224, HMAC_SHA3_256, HMAC_SHA3_384, HMAC_SHA3_512}; +use bouncycastle_sm3::hmac::HMAC_SM3; + +/// The longest key in any of the files is 65 bytes. +const MAX_KEY_LEN: usize = 128; + +/// Runs every case in one `hmac_*_test.json` file through `M`. +fn run(filename: &str, algorithm: &str) { + let Some(doc) = wycheproof_json(filename) else { return }; + + assert_eq!(doc.get("algorithm").and_then(Value::as_str), Some(algorithm), "{filename}"); + + let (mut full, mut truncated, mut invalid) = (0usize, 0usize, 0usize); + for group in doc.get("testGroups").and_then(Value::as_array).expect("testGroups") { + let tag_len = group.get("tagSize").and_then(Value::as_u64).expect("tagSize") as usize / 8; + + 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 msg = hex_field(test, "msg", tc_id); + let tag = hex_field(test, "tag", tc_id); + let valid = match test.get("result").and_then(Value::as_str).expect("result") { + "valid" => true, + "invalid" => false, + other => panic!("{filename} tcId {tc_id}: unexpected result {other}"), + }; + // Some keys are shorter than the hash's security strength (128-bit keys for + // HMAC-SHA-512, say); the key-strength policy is tested in `hmac_tests.rs`. + let key = KeyMaterial::::from_bytes_as_type( + &hex_field(test, "key", tc_id), + KeyType::MACKey, + ) + .expect("a MAC key"); + let mac = || M::new_allow_weak_key(&key).expect("an HMAC instance"); + + let ctx = format!("{filename} tcId {tc_id}"); + if tag_len == mac().output_len() { + assert_eq!(mac().verify(&msg, &tag), valid, "{ctx}: verify"); + if valid { + assert_eq!(mac().mac(&msg), tag, "{ctx}: mac"); + } + full += 1; + } else { + let mut out = vec![0u8; tag_len]; + assert_eq!(mac().mac_out(&msg, &mut out).expect("mac_out"), tag_len, "{ctx}"); + assert_eq!(out == tag, valid, "{ctx}: truncated mac_out"); + assert!(!mac().verify(&msg, &tag), "{ctx}: verify takes only a full-length tag"); + truncated += 1; + } + invalid += usize::from(!valid); + } + } + + println!( + "Wycheproof {algorithm}: {} cases ({full} full-length, {truncated} truncated; \ + {invalid} invalid)", + full + truncated + ); + assert!(full > 0 && truncated > 0 && invalid > 0, "{filename}: expected every kind of case"); +} + +#[test] +fn wycheproof_hmac_sha224() { + run::("hmac_sha224_test.json", "HMACSHA224"); +} + +#[test] +fn wycheproof_hmac_sha256() { + run::("hmac_sha256_test.json", "HMACSHA256"); +} + +#[test] +fn wycheproof_hmac_sha384() { + run::("hmac_sha384_test.json", "HMACSHA384"); +} + +#[test] +fn wycheproof_hmac_sha512() { + run::("hmac_sha512_test.json", "HMACSHA512"); +} + +#[test] +fn wycheproof_hmac_sha512_224() { + run::("hmac_sha512_224_test.json", "HMACSHA512/224"); +} + +#[test] +fn wycheproof_hmac_sha512_256() { + run::("hmac_sha512_256_test.json", "HMACSHA512/256"); +} + +#[test] +fn wycheproof_hmac_sha3_224() { + run::("hmac_sha3_224_test.json", "HMACSHA3-224"); +} + +#[test] +fn wycheproof_hmac_sha3_256() { + run::("hmac_sha3_256_test.json", "HMACSHA3-256"); +} + +#[test] +fn wycheproof_hmac_sha3_384() { + run::("hmac_sha3_384_test.json", "HMACSHA3-384"); +} + +#[test] +fn wycheproof_hmac_sha3_512() { + run::("hmac_sha3_512_test.json", "HMACSHA3-512"); +} + +#[test] +fn wycheproof_hmac_sm3() { + run::("hmac_sm3_test.json", "HMACSM3"); +} diff --git a/crypto/mldsa-lowmemory/Cargo.toml b/crypto/mldsa-lowmemory/Cargo.toml index 60eeadb8..395a97cf 100644 --- a/crypto/mldsa-lowmemory/Cargo.toml +++ b/crypto/mldsa-lowmemory/Cargo.toml @@ -15,7 +15,6 @@ bouncycastle-core-test-framework.workspace = true bouncycastle-hex.workspace = true bouncycastle-rng.workspace = true criterion.workspace = true -serde_json = "1.0" [[bench]] name = "mldsa_benches" diff --git a/crypto/mldsa-lowmemory/benches/note_on_mem_usage_benches.md b/crypto/mldsa-lowmemory/benches/note_on_mem_usage_benches.md deleted file mode 100644 index d029e88e..00000000 --- a/crypto/mldsa-lowmemory/benches/note_on_mem_usage_benches.md +++ /dev/null @@ -1 +0,0 @@ -Note that a test framework is located in the `\/src/bench_mldsa_mem_usage.rs` so that it can be built as a standalone binary and have its memory usage measured with /usr/bin/time without also measuring any of the cargo bench framework. \ No newline at end of file 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 9b8599d7..bf871512 100644 --- a/crypto/mldsa-lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa-lowmemory/src/hash_mldsa.rs @@ -81,9 +81,10 @@ use crate::{ }; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SecurityStrength, - SignatureVerifier, Signer, XOF, + Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SignatureVerifier, Signer, + XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; @@ -342,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(𝑠𝑘, 𝑀', 𝑟𝑛𝑑) @@ -363,9 +364,9 @@ impl< Ok(bytes_written) } - /// To be used for deterministic signing in conjunction with the [`Signer::sign_init`], - /// [`Signer::sign_update`], and [`Signer::sign_final`] flow. - /// It can be set anywhere after [`Signer::sign_init`] and before [`Signer::sign_final`] + /// To be used for deterministic signing in conjunction with the [`Signer::do_sign_init`], + /// [`Signer::do_sign_update`], and [`Signer::do_sign_final`] flow. + /// It can be set anywhere after [`Signer::do_sign_init`] and before [`Signer::do_sign_final`] pub fn set_signer_rnd(&mut self, rnd: [u8; 32]) { self.signer_rnd = Some(rnd); } @@ -441,7 +442,7 @@ impl< Self::sign_ph_out(sk, &ph_m, ctx, output) } - fn sign_init(sk: &SK, ctx: Option<&[u8]>) -> Result { + fn do_sign_init(sk: &SK, ctx: Option<&[u8]>) -> Result { let (ctx, ctx_len) = Self::parse_ctx(ctx)?; Ok(Self { _phantom: PhantomData, @@ -455,17 +456,17 @@ impl< }) } - fn sign_update(&mut self, msg_chunk: &[u8]) { + fn do_sign_update(&mut self, msg_chunk: &[u8]) { self.hash.do_update(msg_chunk); } - fn sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; - self.sign_final_out(&mut out)?; + self.do_sign_final_out(&mut out)?; Ok(out) } - fn sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { let ph: [u8; PH_LEN] = self.hash.do_final().try_into().unwrap(); if self.sk.is_none() && self.seed.is_none() { @@ -526,7 +527,7 @@ impl< Self::verify_ph(pk, &ph_m, ctx, sig) } - fn verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result { + fn do_verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result { let (ctx, ctx_len) = Self::parse_ctx(ctx)?; Ok(Self { _phantom: Default::default(), @@ -540,11 +541,11 @@ impl< }) } - fn verify_update(&mut self, msg_chunk: &[u8]) { + fn do_verify_update(&mut self, msg_chunk: &[u8]) { self.hash.do_update(msg_chunk); } - fn verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { assert!( self.pk.is_some(), "Somehow you managed to construct a streaming verifier without a public key, impressive!" @@ -631,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/lib.rs b/crypto/mldsa-lowmemory/src/lib.rs index 51a4780b..999c6cd8 100644 --- a/crypto/mldsa-lowmemory/src/lib.rs +++ b/crypto/mldsa-lowmemory/src/lib.rs @@ -179,7 +179,7 @@ //! And that's the basic usage! There are lots more bells-and-whistles in the form of exposed algorithm //! parameters, streaming APIs and other goodies that can be found by poking around this documentation. //! -//! # 🚨 Security 🚨 +//! # 🚨 Security Considerations 🚨 //! //! This crate intends to expose only APIs that are secure to use. //! There are, however, a few exceptions that are worth mentioning. diff --git a/crypto/mldsa-lowmemory/src/low_memory_helpers.rs b/crypto/mldsa-lowmemory/src/low_memory_helpers.rs index 7fb15a42..fac94a4a 100644 --- a/crypto/mldsa-lowmemory/src/low_memory_helpers.rs +++ b/crypto/mldsa-lowmemory/src/low_memory_helpers.rs @@ -34,6 +34,8 @@ pub(crate) fn compute_w_row( acc.add_ntt(&tmp); } + // Bound-keeping step before NTT⁻¹, not in FIPS 204: see [`Polynomial::reduce32`]. + acc.reduce32(); acc.inv_ntt(); acc.conditional_add_q(); acc @@ -83,6 +85,10 @@ pub(crate) fn compute_wp_approx_row( } Az_acc.sub(&ct1); + // Bound-keeping step before NTT⁻¹, not in FIPS 204: see `Polynomial::reduce32`. Here it is + // security-critical: 𝐳 and 𝐭1 are attacker-controlled, and without it a crafted signature + // overflows the butterflies (, Wycheproof mldsa_87_verify tcId 240/241). + Az_acc.reduce32(); Az_acc.inv_ntt(); Az_acc.conditional_add_q(); diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index b1658579..066ef7ae 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -19,10 +19,10 @@ //! let msg_chunk1 = b"The quick brown fox "; //! let msg_chunk2 = b"jumped over the lazy dog"; //! -//! let mut signer = MLDSA65::sign_init(&sk, None).unwrap(); -//! signer.sign_update(msg_chunk1); -//! signer.sign_update(msg_chunk2); -//! let sig = signer.sign_final().unwrap(); +//! let mut signer = MLDSA65::do_sign_init(&sk, None).unwrap(); +//! signer.do_sign_update(msg_chunk1); +//! signer.do_sign_update(msg_chunk2); +//! let sig = signer.do_sign_final().unwrap(); //! // This is the signature value that can be saved to a file or whatever it is needed. //! //! // This is compatible with a verifies that takes the whole message as one chunk: @@ -34,11 +34,11 @@ //! } //! //! // But of course there's also a streaming API for the verifier! -//! let mut verifier = MLDSA65::verify_init(&pk, None).unwrap(); -//! verifier.verify_update(msg_chunk1); -//! verifier.verify_update(msg_chunk2); +//! let mut verifier = MLDSA65::do_verify_init(&pk, None).unwrap(); +//! verifier.do_verify_update(msg_chunk1); +//! verifier.do_verify_update(msg_chunk2); //! -//! match verifier.verify_final(&sig.as_slice()) { +//! match verifier.do_verify_final(&sig.as_slice()) { //! Ok(()) => println!("Signature is valid!"), //! Err(SignatureError::SignatureVerificationFailed) => println!("Signature is invalid!"), //! Err(e) => panic!("Something else went wrong: {:?}", e), @@ -61,11 +61,11 @@ //! let msg_chunk1 = b"The quick brown fox "; //! let msg_chunk2 = b"jumped over the lazy dog"; //! -//! let mut signer = MLDSA65::sign_init(&sk, Some(b"signing ctx value")).unwrap(); +//! let mut signer = MLDSA65::do_sign_init(&sk, Some(b"signing ctx value")).unwrap(); //! signer.set_signer_rnd([0u8; 32]); // an all-zero rnd is the "deterministic" mode of ML-DSA -//! signer.sign_update(msg_chunk1); -//! signer.sign_update(msg_chunk2); -//! let sig = signer.sign_final().unwrap(); +//! signer.do_sign_update(msg_chunk1); +//! signer.do_sign_update(msg_chunk2); +//! let sig = signer.do_sign_final().unwrap(); //! ``` //! //! # External Mu mode @@ -398,8 +398,9 @@ use crate::{ }; 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, SecurityStrength, 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}; @@ -413,6 +414,7 @@ use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait}; #[allow(unused_imports)] use bouncycastle_core::traits::{PHSignatureVerifier, PHSigner}; use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; + /*** Constants ***/ /// @@ -787,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 }; @@ -817,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) @@ -965,8 +968,8 @@ impl< } /// To be used for deterministic signing in conjunction with the - /// [`MLDSA44::sign_init`], [`MLDSA44::sign_update`], and [`MLDSA44::sign_final`] flow. - /// Can be set anywhere after [`MLDSA44::sign_init`] and before [`MLDSA44::sign_final`] + /// [`MLDSA44::do_sign_init`], [`MLDSA44::do_sign_update`], and [`MLDSA44::do_sign_final`] flow. + /// Can be set anywhere after [`MLDSA44::do_sign_init`] and before [`MLDSA44::do_sign_final`] fn set_signer_rnd(&mut self, rnd: [u8; 32]) { self.signer_rnd = Some(rnd); } @@ -1013,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 { @@ -1034,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 [[𝑐 ̃ = 𝑐′ ]] @@ -1244,8 +1247,8 @@ pub trait MLDSATrait< rnd: [u8; 32], output: &mut [u8; SIG_LEN], ) -> Result; - /// To be used for deterministic signing in conjunction with the [`MLDSA44::sign_init`], [`MLDSA44::sign_update`], and [`MLDSA44::sign_final`] flow. - /// Can be set anywhere after [`MLDSA44::sign_init`] and before [`MLDSA44::sign_final`] + /// To be used for deterministic signing in conjunction with the [`MLDSA44::do_sign_init`], [`MLDSA44::do_sign_update`], and [`MLDSA44::do_sign_final`] flow. + /// Can be set anywhere after [`MLDSA44::do_sign_init`] and before [`MLDSA44::do_sign_final`] fn set_signer_rnd(&mut self, rnd: [u8; 32]); /// An alternate way to start the streaming signing mode by providing a private key seed instead of an expanded private key fn sign_init_from_seed( @@ -1290,7 +1293,7 @@ impl< Ok(bytes_written) } - fn sign_init(sk: &SK, ctx: Option<&[u8]>) -> Result { + fn do_sign_init(sk: &SK, ctx: Option<&[u8]>) -> Result { Ok(Self { _phantom: PhantomData, mu_builder: MuBuilder::do_init(&sk.tr(), ctx)?, @@ -1301,17 +1304,17 @@ impl< }) } - fn sign_update(&mut self, msg_chunk: &[u8]) { + fn do_sign_update(&mut self, msg_chunk: &[u8]) { self.mu_builder.do_update(msg_chunk); } - fn sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; - self.sign_final_out(&mut out)?; + self.do_sign_final_out(&mut out)?; Ok(out) } - fn sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { let mu = self.mu_builder.do_final(); if self.sk.is_none() && self.seed.is_none() { @@ -1369,7 +1372,7 @@ impl< Self::verify_mu(pk, &mu, &sig.try_into().unwrap()) } - fn verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result { + fn do_verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result { Ok(Self { _phantom: Default::default(), mu_builder: MuBuilder::do_init(&pk.compute_tr(), ctx)?, @@ -1380,11 +1383,11 @@ impl< }) } - fn verify_update(&mut self, msg_chunk: &[u8]) { + fn do_verify_update(&mut self, msg_chunk: &[u8]) { self.mu_builder.do_update(msg_chunk); } - fn verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { let mu = self.mu_builder.do_final(); assert!( @@ -1446,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) @@ -1461,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 76477c63..34bedbcc 100644 --- a/crypto/mldsa-lowmemory/src/mldsa_keys.rs +++ b/crypto/mldsa-lowmemory/src/mldsa_keys.rs @@ -9,9 +9,10 @@ use crate::mldsa::{MLDSA65_FULL_SK_LEN, MLDSA65_PK_LEN, MLDSA65_SK_LEN}; use crate::mldsa::{MLDSA87_FULL_SK_LEN, MLDSA87_PK_LEN, MLDSA87_SK_LEN}; use crate::params::{MLDSA44Params, MLDSA65Params, MLDSA87Params, MLDSAParams}; use bouncycastle_core::errors::SignatureError; -use bouncycastle_core::key_material; +use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{SecurityStrength, SignaturePrivateKey, SignaturePublicKey, XOF}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Hash, SignaturePrivateKey, SignaturePublicKey, XOF, XOFSqueezer}; use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; use core::fmt; use core::fmt::{Debug, Display, Formatter}; @@ -95,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 } @@ -337,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) @@ -378,6 +380,8 @@ impl::from_bytes(bytes)?; - key_material::do_hazardous_operations(&mut keymat, |keymat| { + do_hazardous_operations(&mut keymat, |keymat| { keymat.set_key_type(KeyType::Seed)?; keymat.set_security_strength(SecurityStrength::_256bit) })?; diff --git a/crypto/mldsa-lowmemory/src/params.rs b/crypto/mldsa-lowmemory/src/params.rs index a07f1200..9952a01d 100644 --- a/crypto/mldsa-lowmemory/src/params.rs +++ b/crypto/mldsa-lowmemory/src/params.rs @@ -19,7 +19,8 @@ use crate::hash_mldsa::{ use crate::mldsa::{ ML_DSA_44_NAME, ML_DSA_65_NAME, ML_DSA_87_NAME, MLDSA_SEED_LEN, POLY_T1PACKED_LEN, q, }; -use bouncycastle_core::traits::{Algorithm, AlgorithmOID, Hash, HashAlgParams, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, AlgorithmOID, Hash, HashAlgParams}; use bouncycastle_sha2::{SHA256, SHA512}; use bouncycastle_utils::secret::ZeroizablePrimitive; diff --git a/crypto/mldsa-lowmemory/src/polynomial.rs b/crypto/mldsa-lowmemory/src/polynomial.rs index 6b38bbbe..6d37f6c5 100644 --- a/crypto/mldsa-lowmemory/src/polynomial.rs +++ b/crypto/mldsa-lowmemory/src/polynomial.rs @@ -8,7 +8,7 @@ use core::ops::{Index, IndexMut}; /// A polynomial over the ML-DSA ring. /// -/// # 🚨 Security 🚨 +/// # 🚨 Security Considerations 🚨 /// Polynomials themselves are not inherently secret since sometimes they are part of public keys /// and sometimes private keys. /// It is the responsibility of the caller to wrap sensitive instances in `Secret`. @@ -50,6 +50,14 @@ impl Polynomial { } } + /// Reduces every coefficient to |𝑐| < 𝑞 without changing its residue. See [`reduce32`] for when + /// this is required for security reasons; note that [`Self::reduce`] is a *Montgomery* reduction and is not a substitute. + pub(crate) fn reduce32(&mut self) { + for x in self.coeffs.iter_mut() { + *x = reduce32(*x); + } + } + /// Algorithm 44 AddNTT(𝑎, 𝑏)̂ /// Computes the sum a + 𝑏 of two elements 𝑎, 𝑏 ∈ 𝑇𝑞. /// Note: result could be up to 2q. @@ -228,6 +236,17 @@ impl Polynomial { /// Output: Polynomial 𝑤(𝑋) = ∑255 /// 𝑗=0 𝑤𝑗𝑋𝑗 ∈ 𝑅𝑞 pub(crate) fn inv_ntt(&mut self) { + // A core input condition on the InverseNTT is that every input + // coefficient must satisfy |𝑤| < 𝑞 for the sums to stay within an i32. + // The reason this is required is because Algorithm 42's butterflies (steps 13-14) + // run 8 levels without reducing, leading to an i32 overflow if the input was out-of-range. + // Callers must pass them through `reduce32` first. + // Note that only a crafted input, such as (Wycheproof mldsa_87_verify tcId 240/241) will trigger this. + debug_assert!( + self.coeffs.iter().all(|c| c.abs() < q), + "inv_ntt input coefficient not reduced below q; call reduce32() first" + ); + let mut m: usize = N; let mut len: usize = 1; @@ -292,6 +311,20 @@ pub(crate) fn conditional_add_q(a: i32) -> i32 { a + ((a >> 31) & q) } +/// Plain (non-Montgomery) reduction: for 𝑎 ≤ 2^31 − 2^22 − 1 returns 𝑟 ≡ 𝑎 (mod 𝑞) with |𝑟| ≤ 6283008 < 𝑞. +/// `reduce32` in the reference implementation (pq-crystals/dilithium, ref/reduce.c). +/// +/// This is not in FIPS 204 since it assumes all arithmetic is done mod q. +/// In this implementation, sums of Montgomery products are left unreduced, but `inv_ntt` (Algorithm 42) +/// needs |input| < 𝑞 to stay within an `i32`, so this is applied to those sums first. Omitting it before +/// the verifier's NTT⁻¹ is exploitable (, Wycheproof mldsa_87_verify tcId 240/241). +pub(crate) fn reduce32(a: i32) -> i32 { + // The reference implementation's stated input bound; above it `a + 2^22` overflows. + debug_assert!(a <= i32::MAX - (1 << 22)); + let t = (a + (1 << 22)) >> 23; + a - t * q +} + #[test] /// These are the results it's giving; I'm not sure if these are "correct" or not. fn test_conditional_add_q() { @@ -336,3 +369,40 @@ const ZETAS: [i32; 256] = [ -2235985, -420899, -2286327, 183443, -976891, 1612842, -3545687, -554416, 3919660, -48306, -1362209, 3937738, 1400424, -846154, 1976782, ]; + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_reduce32() { + // congruent to the input mod q, and within the reference implementation's stated output range + for &a in &[ + 0, + 1, + -1, + q - 1, + q, + q + 1, + -q, + -q - 1, + 8 * q, + -8 * q, + 6283008, + -6283008, + i32::MIN, + i32::MAX - (1 << 22), + ] { + let r = reduce32(a); + assert!((-6283008..=6283008).contains(&r), "reduce32({a}) = {r} out of range"); + assert_eq!( + (r as i64 - a as i64).rem_euclid(q as i64), + 0, + "reduce32({a}) = {r} not congruent" + ); + assert!(r.abs() < q); + } + // the largest sum inv_ntt may see: (l + 1) products each in (-q, q), for l = 7 + assert!(reduce32(8 * q - 8).abs() < q); + assert!(reduce32(-(8 * q - 8)).abs() < q); + } +} diff --git a/crypto/mldsa-lowmemory/tests/bc_test_data.rs b/crypto/mldsa-lowmemory/tests/bc_test_data.rs deleted file mode 100644 index b2f71bdb..00000000 --- a/crypto/mldsa-lowmemory/tests/bc_test_data.rs +++ /dev/null @@ -1,995 +0,0 @@ -// 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)] -#[allow(dead_code)] -#[cfg(test)] -mod bc_test_data { - #![allow(unused)] - - #[allow(unused)] - #[allow(dead_code)] - use crate::BustedMuBuilder; - use bouncycastle_core::errors::SignatureError; - use bouncycastle_core::key_material; - use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; - use bouncycastle_core::traits::{ - Hash, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, - }; - use bouncycastle_hex as hex; - use bouncycastle_mldsa_lowmemory::{ - HashMLDSA44_with_SHA512, HashMLDSA65_with_SHA512, HashMLDSA87_with_SHA512, MLDSA44, - MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65, - MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87, - MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyTrait, - MLDSATrait, - }; - use bouncycastle_sha2::SHA512; - use std::fs; - use std::path::Path; - use std::process::exit; - use std::sync::Once; - - const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/pqc/crypto/mldsa"; - const TEST_DATA_PATH: &str = "../../../bc-test-data/pqc/crypto/mldsa"; - - 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!("wycheproof found at: {:?}", TEST_DATA_PATH_RELATIVE), - 2 => println!("wycheproof found at: {:?}", TEST_DATA_PATH), - _ => println!("WARNING: wycheproof directory not found; tests will be skipped"), - }); - - if !found == 3 { - return Err(()); - } - - 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) - } - - #[test] - #[allow(non_snake_case)] - fn ML_DSA_keyGen() { - let contents = match get_test_data("ML-DSA-keyGen.txt") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = KeyGenTestCase::parse(contents); - - for test_case in test_cases { - test_case.run(); - } - } - - #[derive(Clone)] - struct KeyGenTestCase { - vs_id: u32, - algorithm: String, - mode: String, - revision: String, - is_sample: bool, - tg_id: u32, - test_type: String, - parameter_set: String, - tc_id: u32, - seed: String, - pk: String, - sk: String, - } - - impl KeyGenTestCase { - fn new() -> Self { - Self { - vs_id: 0, - algorithm: String::new(), - mode: String::new(), - revision: String::new(), - is_sample: false, - tg_id: 0, - test_type: String::new(), - parameter_set: String::new(), - tc_id: 0, - seed: String::new(), - pk: String::new(), - sk: String::new(), - } - } - - fn is_full(&self) -> bool { - !self.algorithm.is_empty() - } - - fn parse(data: String) -> Vec { - let mut test_cases = Vec::::new(); - let mut test_case = KeyGenTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "vsId" => test_case.vs_id = value.parse().unwrap(), - "algorithm" => test_case.algorithm = value.to_string(), - "mode" => test_case.mode = value.to_string(), - "revision" => test_case.revision = value.to_string(), - "isSample" => test_case.is_sample = value.parse().unwrap(), - "tgId" => test_case.tg_id = value.parse().unwrap(), - "testType" => test_case.test_type = value.to_string(), - "parameterSet" => test_case.parameter_set = value.to_string(), - "tcId" => test_case.tc_id = value.parse().unwrap(), - "seed" => test_case.seed = value.to_string(), - "pk" => test_case.pk = value.to_string(), - "sk" => test_case.sk = value.to_string(), - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self) { - assert_eq!(self.mode, "keyGen"); - - let mut seed = KeyMaterial256::from_bytes_as_type( - &hex::decode(&self.seed).unwrap(), - KeyType::Seed, - ) - .unwrap(); - // for the purposes of the test cases, accept an all-zero seed - key_material::do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed).unwrap(); - seed.set_security_strength(SecurityStrength::_256bit) - }); - - match self.parameter_set.as_str() { - "ML-DSA-44" => { - let (pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA44_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA44_SK_LEN] = - hex::decode(&self.seed).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - } - "ML-DSA-65" => { - let (pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA65_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA65_SK_LEN] = - hex::decode(&self.seed).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - } - "ML-DSA-87" => { - let (pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA87_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA87_SK_LEN] = - hex::decode(&self.seed).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - } - val => panic!("Invalid parameter set: {}", val), - } - } - } - - // this seems buggy and I'm not sure why. Possibly because the bc-test-data was written against Round 3 Dilithium and not ML-DSA. - // todo -- debug - // #[test] - #[allow(non_snake_case)] - fn ML_DSA_sigGen() { - let contents = match get_test_data("ML-DSA-sigGen.txt") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = SigGenTestCase::parse(contents); - - let num_tests = test_cases.len(); - for test_case in test_cases { - test_case.run(); - } - - println!("SUCCESS! ML-DSA-sigGen test cases passed: {}!", num_tests); - } - - #[derive(Clone)] - struct SigGenTestCase { - vs_id: u32, - algorithm: String, - mode: String, - revision: String, - is_sample: bool, - tg_id: u32, - test_type: String, - parameter_set: String, - deterministic: bool, - tc_id: u32, - sk: String, - message: String, - rnd: String, - signature: String, - } - - impl SigGenTestCase { - fn new() -> Self { - Self { - vs_id: 0, - algorithm: String::new(), - mode: String::new(), - revision: String::new(), - is_sample: false, - tg_id: 0, - test_type: String::new(), - parameter_set: String::new(), - deterministic: false, - tc_id: 0, - sk: String::new(), - message: String::new(), - rnd: String::new(), - signature: String::new(), - } - } - - fn is_full(&self) -> bool { - !self.algorithm.is_empty() - } - - fn parse(data: String) -> Vec { - let mut test_cases = Vec::::new(); - let mut test_case = SigGenTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "vsId" => test_case.vs_id = value.parse().unwrap(), - "algorithm" => test_case.algorithm = value.to_string(), - "mode" => test_case.mode = value.to_string(), - "revision" => test_case.revision = value.to_string(), - "isSample" => test_case.is_sample = value.parse().unwrap(), - "tgId" => test_case.tg_id = value.parse().unwrap(), - "testType" => test_case.test_type = value.to_string(), - "parameterSet" => test_case.parameter_set = value.to_string(), - "deterministic" => test_case.deterministic = value.parse().unwrap(), - "tcId" => test_case.tc_id = value.parse().unwrap(), - "sk" => test_case.sk = value.to_string(), - "message" => test_case.message = value.to_string(), - "rnd" => test_case.rnd = value.to_string(), - "signature" => test_case.signature = value.to_string(), - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self) { - assert_eq!(self.mode, "sigGen"); - - let rnd = if self.deterministic { - [0u8; 32] - } else { - hex::decode(&self.rnd).unwrap().as_slice().try_into().unwrap() - }; - - match self.parameter_set.as_str() { - "ML-DSA-44" => { - let sk = - MLDSA44PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); - - // note: a sign_mu_deterministic() is being exposed, but not sign_deterministic() - // so it is necessary to manually compute mu - // let mu = MLDSA44::compute_mu_from_tr( - // &hex::decode(&self.message).unwrap(), - // None, - // sk.tr(), - // ).unwrap(); - let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); - mb.do_update(&hex::decode(&self.message).unwrap()); - let mu = mb.do_final(); - - let sig = MLDSA44::sign_mu_deterministic(&sk, &mu, rnd).unwrap(); - assert_eq!( - &sig, - &*hex::decode(&self.signature).unwrap(), - "ML-DSA-sigGen params: {}, vsId: {}, tgId: {}, tcId: {}", - self.parameter_set, - self.vs_id, - self.tg_id, - self.tc_id - ); - } - "ML-DSA-65" => { - let sk = - MLDSA65PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); - - // note: a sign_mu_deterministic() is being exposed, but not sign_deterministic() - // so it is necessary to manually compute mu - // let mu = MLDSA65::compute_mu_from_tr( - // &hex::decode(&self.message).unwrap(), - // None, - // sk.tr(), - // ).unwrap(); - let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); - mb.do_update(&hex::decode(&self.message).unwrap()); - let mu = mb.do_final(); - - let sig = MLDSA65::sign_mu_deterministic(&sk, &mu, rnd).unwrap(); - assert_eq!(&sig, &*hex::decode(&self.signature).unwrap()); - } - "ML-DSA-87" => { - let sk = - MLDSA87PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); - - // note: a sign_mu_deterministic() is being exposed, but not sign_deterministic() - // so it is necessary to manually compute mu - // let mu = MLDSA87::compute_mu_from_tr( - // &hex::decode(&self.message).unwrap(), - // None, - // sk.tr(), - // ).unwrap(); - let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); - mb.do_update(&hex::decode(&self.message).unwrap()); - let mu = mb.do_final(); - - let sig = MLDSA87::sign_mu_deterministic(&sk, &mu, rnd).unwrap(); - assert_eq!(&sig, &*hex::decode(&self.signature).unwrap()); - } - val => panic!("Invalid parameter set: {}", val), - } - } - } - - // DISABLED: this is not an implementation bug. - // - // This procedure contains a bug that hasn't yet been found. - // Possibly because the bc-test-data was written against Round 3 Dilithium and not ML-DSA. - // - // The bc-test-data vectors predate the FIPS 204 M' construction. They were generated with - // mu = H(tr || M), omitting the 0x00 || |ctx| || ctx prefix. See BustedMuBuilder below, which - // reproduces that legacy construction for the sigGen tests. - // - // ML_DSA_sigGen works around this by computing the legacy mu itself and injecting it through - // the public external-mu signing API (sign_mu_deterministic). Verification has no equivalent: - // MLDSA::verify() always applies the M' prefix internally, and verify_mu_internal() is private, - // so these vectors can never verify through the public API. - // - // Re-enable once a public verify_mu() exists (symmetric with sign_mu), or once external-mode - // (ctx-aware) vectors are available. - // todo -- debug - // #[test] - #[allow(non_snake_case)] - fn ML_DSA_sigVer() { - let contents = match get_test_data("ML-DSA-sigVer.txt") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = SigVerTestCase::parse(contents); - - for test_case in test_cases { - test_case.run(); - } - } - - #[derive(Clone)] - struct SigVerTestCase { - vs_id: u32, - algorithm: String, - mode: String, - revision: String, - is_sample: bool, - tg_id: u32, - test_type: String, - parameter_set: String, - pk: String, - tc_id: u32, - message: String, - signature: String, - test_passed: bool, - } - - impl SigVerTestCase { - fn new() -> Self { - Self { - vs_id: 0, - algorithm: String::new(), - mode: String::new(), - revision: String::new(), - is_sample: false, - tg_id: 0, - test_type: String::new(), - parameter_set: String::new(), - tc_id: 0, - pk: String::new(), - message: String::new(), - signature: String::new(), - test_passed: false, - } - } - - fn is_full(&self) -> bool { - !self.algorithm.is_empty() - } - - fn parse(data: String) -> Vec { - let mut test_cases = Vec::::new(); - let mut test_case = SigVerTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "vsId" => test_case.vs_id = value.parse().unwrap(), - "algorithm" => test_case.algorithm = value.to_string(), - "mode" => test_case.mode = value.to_string(), - "revision" => test_case.revision = value.to_string(), - "isSample" => test_case.is_sample = value.parse().unwrap(), - "tgId" => test_case.tg_id = value.parse().unwrap(), - "testType" => test_case.test_type = value.to_string(), - "parameterSet" => test_case.parameter_set = value.to_string(), - "pk" => test_case.pk = value.to_string(), - "tcId" => test_case.tc_id = value.parse().unwrap(), - "message" => test_case.message = value.to_string(), - "signature" => test_case.signature = value.to_string(), - "testPassed" => test_case.test_passed = value.parse().unwrap(), - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self) { - assert_eq!(self.mode, "sigVer"); - - match self.parameter_set.as_str() { - "ML-DSA-44" => { - let pk = MLDSA44PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); - - match MLDSA44::verify( - &pk, - &hex::decode(&self.message).unwrap(), - None, - &hex::decode(&self.signature).unwrap(), - ) { - Ok(()) => { - if !self.test_passed { - panic!("Verification succeeded when it shouldn't have!") - } - } - Err(SignatureError::SignatureVerificationFailed) => { - if self.test_passed { - panic!( - "Verification failed when it shouldn't have! vsId: {}, tgId: {}, tcId: {}", - self.vs_id, self.tg_id, self.tc_id - ) - } - } - _ => panic!("An unexpected error occurred"), - } - } - "ML-DSA-65" => { - let pk = MLDSA65PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); - - match MLDSA65::verify( - &pk, - &hex::decode(&self.message).unwrap(), - None, - &hex::decode(&self.signature).unwrap(), - ) { - Ok(()) => { - if self.test_passed { /* good */ - } else { - panic!("Verification succeeded when it shouldn't have!") - } - } - Err(SignatureError::SignatureVerificationFailed) => { - if !self.test_passed { - } else { - panic!("Verification failed when it should have!") - } - } - _ => panic!("An unexpected error occurred"), - } - } - "ML-DSA-87" => { - let pk = MLDSA87PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); - - match MLDSA87::verify( - &pk, - &hex::decode(&self.message).unwrap(), - None, - &hex::decode(&self.signature).unwrap(), - ) { - Ok(()) => { - if self.test_passed { /* good */ - } else { - panic!("Verification succeeded when it shouldn't have!") - } - } - Err(SignatureError::SignatureVerificationFailed) => { - if !self.test_passed { - } else { - panic!("Verification failed when it should have!") - } - } - _ => panic!("An unexpected error occurred"), - } - } - val => panic!("Invalid parameter set: {}", val), - } - } - } - - // DISABLED: root cause not yet established. - // These .rsp vectors are modern FIPS 204 (they carry a `context` tag and include - // HashML-DSA/SHA-512 cases), and this test uses the real compute_mu_from_tr with ctx, - // - // #[test] - #[allow(non_snake_case)] - fn ML_DSA_rsp() { - // MLDsa44 - let contents = match get_test_data("mldsa44.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MLDsa44"); - } - - // MLDsa65 - let contents = match get_test_data("mldsa65.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MLDsa65"); - } - - // MLDsa87 - let contents = match get_test_data("mldsa87.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MLDsa87"); - } - - // MLDsa44 - let contents = match get_test_data("mldsa44sha512.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MLDsa44"); - } - - // MLDsa65 - let contents = match get_test_data("mldsa65sha512.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MlDsa65"); - } - - // MLDsa87 - let contents = match get_test_data("mldsa87sha512.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MlDsa87"); - } - } - - #[derive(Clone)] - struct MldsaRspTestCase { - count: u32, - seed: String, - mlen: u32, - msg: String, - pk: String, - sk: String, - smlen: u32, - sm: String, - message_hash: String, - message_prime: String, - context: String, - } - - impl MldsaRspTestCase { - fn new() -> Self { - Self { - count: 0, - seed: String::new(), - mlen: 0, - msg: String::new(), - pk: String::new(), - sk: String::new(), - smlen: 0, - sm: String::new(), - message_hash: String::new(), - message_prime: String::new(), - context: String::new(), - } - } - - fn is_full(&self) -> bool { - !self.seed.is_empty() - } - - fn parse(data: String) -> Vec> { - let mut test_cases = Vec::new(); - let mut test_case = MldsaRspTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "count" => test_case.count = value.parse().unwrap(), - "seed" => test_case.seed = value.to_string(), - "mlen" => test_case.mlen = value.parse().unwrap(), - "msg" => test_case.msg = value.to_string(), - "pk" => test_case.pk = value.to_string(), - "sk" => test_case.sk = value.to_string(), - "smlen" => test_case.smlen = value.parse().unwrap(), - "sm" => test_case.sm = value.to_string(), - "message_hash" => test_case.message_hash = value.to_string(), - "message_prime" => test_case.message_prime = value.to_string(), - "context" => { - test_case.context = value.to_string(); - if test_case.context == "zero_length" || test_case.context == "none" { - test_case.context = String::new(); - } - } - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self, parameter_set: &str) { - match parameter_set { - "MLDsa44" => { - let mut seed = KeyMaterial256::from_bytes_as_type( - &hex::decode(&self.seed).unwrap(), - KeyType::Seed, - ) - .unwrap(); - // for the purposes of the test cases, accept an all-zero seed - key_material::do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed).unwrap(); - seed.set_security_strength(SecurityStrength::_256bit) - }); - - let (pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA44_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA44_SK_LEN] = - hex::decode(&self.sk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - - if IS_HASH_MLDSA { - // we're only testing SHA512 - let ph: [u8; 64] = SHA512::new() - .hash(&hex::decode(&self.msg).unwrap()) - .as_slice() - .try_into() - .unwrap(); - assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); - - let sig = HashMLDSA44_with_SHA512::sign_ph_deterministic( - &sk, - Some(&*hex::decode(&self.context).unwrap()), - &ph, - [0u8; 32], - ) - .unwrap(); - assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); - - HashMLDSA44_with_SHA512::verify( - &pk, - &*hex::decode(&self.msg).unwrap(), - Some(&*hex::decode(&self.context).unwrap()), - &sig, - ) - .expect(&format!( - "paramSet: {}, is_hash: {}, count: {}", - parameter_set, IS_HASH_MLDSA, self.count - )); - } else { - // note: we're exposing a sign_mu_deterministic(), but not sign_deterministic() - // so need to manually compute mu - let mu = MLDSA65::compute_mu_from_tr( - &sk.tr(), - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - ) - .unwrap(); - - let sig = MLDSA44::sign_mu_deterministic(&sk, &mu, [0u8; 32]).unwrap(); - assert_eq!( - sig, - &*hex::decode(&self.sm).unwrap(), - "paramSet: {}, count: {}", - parameter_set, - self.count - ); - - MLDSA44::verify( - &pk, - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - &sig, - ) - .unwrap(); - } - } - "MlDsa65" | "MLDsa65" => { - let mut seed = KeyMaterial256::from_bytes_as_type( - &hex::decode(&self.seed).unwrap(), - KeyType::Seed, - ) - .unwrap(); - // for the purposes of the test cases, accept an all-zero seed - key_material::do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed).unwrap(); - seed.set_security_strength(SecurityStrength::_256bit) - }); - - let (pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA65_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA65_SK_LEN] = - hex::decode(&self.sk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - - if IS_HASH_MLDSA { - // we're only testing SHA512 - let ph: [u8; 64] = SHA512::new() - .hash(&hex::decode(&self.msg).unwrap()) - .as_slice() - .try_into() - .unwrap(); - assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); - - let sig = HashMLDSA65_with_SHA512::sign_ph_deterministic( - &sk, - Some(&*hex::decode(&self.context).unwrap()), - &ph, - [0u8; 32], - ) - .unwrap(); - assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); - - HashMLDSA65_with_SHA512::verify( - &pk, - &*hex::decode(&self.message_hash).unwrap(), - Some(&*hex::decode(&self.context).unwrap()), - &sig, - ) - .expect(&format!( - "paramSet: {}, isHash: {}, count: {}", - parameter_set, IS_HASH_MLDSA, self.count - )); - } else { - // note: we're exposing a sign_mu_deterministic(), but not sign_deterministic() - // so need to manually compute mu - let mu = MLDSA65::compute_mu_from_tr( - &sk.tr(), - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - ) - .unwrap(); - - let sig = MLDSA65::sign_mu_deterministic(&sk, &mu, [0u8; 32]).unwrap(); - assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); - - MLDSA65::verify( - &pk, - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - &sig, - ) - .unwrap(); - } - } - "MLDsa87" => { - let mut seed = KeyMaterial256::from_bytes_as_type( - &hex::decode(&self.seed).unwrap(), - KeyType::Seed, - ) - .unwrap(); - // for the purposes of the test cases, accept an all-zero seed - key_material::do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed).unwrap(); - seed.set_security_strength(SecurityStrength::_256bit) - }); - - let (pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA87_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA87_SK_LEN] = - hex::decode(&self.sk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - - if IS_HASH_MLDSA { - // we're only testing SHA512 - let ph: [u8; 64] = SHA512::new() - .hash(&hex::decode(&self.msg).unwrap()) - .as_slice() - .try_into() - .unwrap(); - assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); - - let sig = HashMLDSA87_with_SHA512::sign_ph_deterministic( - &sk, - Some(&*hex::decode(&self.context).unwrap()), - &ph, - [0u8; 32], - ) - .unwrap(); - assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); - - HashMLDSA87_with_SHA512::verify( - &pk, - &*hex::decode(&self.message_hash).unwrap(), - Some(&*hex::decode(&self.context).unwrap()), - &sig, - ) - .unwrap(); - } else { - // note: we're exposing a sign_mu_deterministic(), but not sign_deterministic() - // so need to manually compute mu - let mu = MLDSA65::compute_mu_from_tr( - &sk.tr(), - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - ) - .unwrap(); - - let sig = MLDSA87::sign_mu_deterministic(&sk, &mu, [0u8; 32]).unwrap(); - assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); - - MLDSA87::verify( - &pk, - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - &sig, - ) - .unwrap(); - } - } - val => panic!("Invalid parameter set: {}", val), - } - } - } -} - -/// This builds a "busted" mu where the ctx is absent (not 0-length, but actually not there) -/// just for the sake of compatibility with the bc-test-data tests -pub struct BustedMuBuilder { - h: SHAKE256, -} - -impl BustedMuBuilder { - /// Algorithm 7 - /// 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀′, 64) - pub fn compute_mu(msg: &[u8], tr: &[u8; 64]) -> Result<[u8; 64], SignatureError> { - let mut mu_builder = Self::do_init(&tr)?; - mu_builder.do_update(msg); - let mu = mu_builder.do_final(); - - Ok(mu) - } - - /// This function requires the public key hash `tr`, which can be computed from the public key using [`MLDSAPublicKey::compute_tr`]. - pub fn do_init(tr: &[u8; 64] /*ctx: Option<&[u8]>*/) -> Result { - // let ctx = match ctx { - // Some(ctx) => ctx, - // None => &[] - // }; - - // Algorithm 2 - // 1: if |𝑐𝑡𝑥| > 255 then - // if ctx.len() > 255 { - // return Err(SignatureError::LengthError("ctx value is longer than 255 bytes")); - // } - - // Algorithm 7 - // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) - let mut mb = Self { h: SHAKE256::new() }; - mb.h.absorb(tr).expect("absorb before squeeze is infallible"); - - // 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); - - // now ready to absorb M - Ok(mb) - } - - /// 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"); - } - - /// Finalize and return the mu value. - pub fn do_final(mut self) -> [u8; 64] { - // Completion of - // Algorithm 7 - // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) - let mut mu = [0u8; 64]; - self.h.squeeze_out(&mut mu); - - mu - } -} diff --git a/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs index 6920ca41..4105b87c 100644 --- a/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs @@ -6,6 +6,7 @@ mod hash_mldsa_tests { use super::*; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; + use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{Hash, PHSignatureVerifier, PHSigner}; use bouncycastle_core_test_framework::signature::TestFrameworkSignature; use bouncycastle_mldsa_lowmemory::{ @@ -125,25 +126,25 @@ mod hash_mldsa_tests { // test the streaming API from sk - let mut s = HashMLDSA44_with_SHA512::sign_init(&expected_sk, ctx).unwrap(); + let mut s = HashMLDSA44_with_SHA512::do_sign_init(&expected_sk, ctx).unwrap(); s.set_signer_rnd(rnd); - s.sign_update(msg); - let sig = s.sign_final().unwrap(); + s.do_sign_update(msg); + let sig = s.do_sign_final().unwrap(); assert_eq!(&sig, &expected_sig); // test the streaming API from seed let mut s = HashMLDSA44_with_SHA512::sign_init_from_seed(&seed, ctx).unwrap(); s.set_signer_rnd(rnd); - s.sign_update(msg); - let sig = s.sign_final().unwrap(); + s.do_sign_update(msg); + let sig = s.do_sign_final().unwrap(); assert_eq!(&sig, &expected_sig); // test the streaming verifier - let mut v = HashMLDSA44_with_SHA512::verify_init(&expected_pk, ctx).unwrap(); - v.verify_update(msg); - v.verify_final(&expected_sig).unwrap(); + let mut v = HashMLDSA44_with_SHA512::do_verify_init(&expected_pk, ctx).unwrap(); + v.do_verify_update(msg); + v.do_verify_final(&expected_sig).unwrap(); } #[test] @@ -155,11 +156,11 @@ mod hash_mldsa_tests { let (_pk, sk) = HashMLDSA44_with_SHA256::keygen().unwrap(); // ctx with len 255 works - HashMLDSA44_with_SHA256::sign_init(&sk, Some(&[1u8; 255])).unwrap(); + HashMLDSA44_with_SHA256::do_sign_init(&sk, Some(&[1u8; 255])).unwrap(); // ctx with len 256 is too long let too_long_ctx = [1u8; 256]; - match HashMLDSA44_with_SHA256::sign_init(&sk, Some(&too_long_ctx)) { + match HashMLDSA44_with_SHA256::do_sign_init(&sk, Some(&too_long_ctx)) { Err(SignatureError::LengthError(_)) => { /* good */ } _ => panic!("Expected error for ctx too long"), } @@ -237,7 +238,7 @@ mod hash_mldsa_tests { #[test] fn algorithm_names_strengths_and_oids() { - use bouncycastle_core::traits::{Algorithm, AlgorithmOID, SecurityStrength}; + use bouncycastle_core::traits::{Algorithm, AlgorithmOID}; // `Algorithm` is implemented once, generically over the pairing, so nothing else states // these per algorithm. diff --git a/crypto/mldsa-lowmemory/tests/mldsa_bc-test-data.rs b/crypto/mldsa-lowmemory/tests/mldsa_bc-test-data.rs new file mode 100644 index 00000000..584652db --- /dev/null +++ b/crypto/mldsa-lowmemory/tests/mldsa_bc-test-data.rs @@ -0,0 +1,932 @@ +//! Known-answer tests for ML-DSA-44/65/87 and HashML-DSA-44/65/87 against `ML-DSA-keyGen.txt`, +//! `ML-DSA-sigVer.txt` and `mldsa{44,65,87}{,sha512}.rsp`. `ML-DSA-sigGen.txt` is not run; see +//! `ML_DSA_sigGen` below. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data", under `pqc/crypto/mldsa/`. If it is not +//! present the tests print a warning and pass vacuously. + +#![allow(dead_code)] + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + Hash, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, XOF, XOFSqueezer, +}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_mldsa_lowmemory::mldsa::{ + MLDSA44_FULL_SK_LEN, MLDSA65_FULL_SK_LEN, MLDSA87_FULL_SK_LEN, +}; +use bouncycastle_mldsa_lowmemory::{ + HashMLDSA44_with_SHA512, HashMLDSA65_with_SHA512, HashMLDSA87_with_SHA512, MLDSA44, + MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65, MLDSA65_PK_LEN, + MLDSA65_SK_LEN, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87, MLDSA87_PK_LEN, MLDSA87_SK_LEN, + MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait, MLDSATrait, +}; +use bouncycastle_sha2::SHA512; +use bouncycastle_sha3::SHAKE256; + +const TEST_DATA_DIR: &str = "pqc/crypto/mldsa"; + +#[test] +#[allow(non_snake_case)] +fn ML_DSA_keyGen() { + let Some(contents) = bc_test_data(TEST_DATA_DIR, "ML-DSA-keyGen.txt") else { return }; + + let test_cases = KeyGenTestCase::parse(contents); + + for test_case in test_cases { + test_case.run(); + } +} + +#[derive(Clone)] +struct KeyGenTestCase { + vs_id: u32, + algorithm: String, + mode: String, + revision: String, + is_sample: bool, + tg_id: u32, + test_type: String, + parameter_set: String, + tc_id: u32, + seed: String, + pk: String, + sk: String, +} + +impl KeyGenTestCase { + fn new() -> Self { + Self { + vs_id: 0, + algorithm: String::new(), + mode: String::new(), + revision: String::new(), + is_sample: false, + tg_id: 0, + test_type: String::new(), + parameter_set: String::new(), + tc_id: 0, + seed: String::new(), + pk: String::new(), + sk: String::new(), + } + } + + fn is_full(&self) -> bool { + !self.algorithm.is_empty() + } + + fn parse(data: String) -> Vec { + let mut test_cases = Vec::::new(); + let mut test_case = KeyGenTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "vsId" => test_case.vs_id = value.parse().unwrap(), + "algorithm" => test_case.algorithm = value.to_string(), + "mode" => test_case.mode = value.to_string(), + "revision" => test_case.revision = value.to_string(), + "isSample" => test_case.is_sample = value.parse().unwrap(), + "tgId" => test_case.tg_id = value.parse().unwrap(), + "testType" => test_case.test_type = value.to_string(), + "parameterSet" => test_case.parameter_set = value.to_string(), + "tcId" => test_case.tc_id = value.parse().unwrap(), + "seed" => test_case.seed = value.to_string(), + "pk" => test_case.pk = value.to_string(), + "sk" => test_case.sk = value.to_string(), + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + fn run(&self) { + assert_eq!(self.mode, "keyGen"); + + let mut seed = + KeyMaterial256::from_bytes_as_type(&hex::decode(&self.seed).unwrap(), KeyType::Seed) + .unwrap(); + // for the purposes of the test cases, accept an all-zero seed + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + + match self.parameter_set.as_str() { + "ML-DSA-44" => { + let (pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA44_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA44_SK_LEN] = + hex::decode(&self.seed).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + } + "ML-DSA-65" => { + let (pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA65_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA65_SK_LEN] = + hex::decode(&self.seed).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + } + "ML-DSA-87" => { + let (pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA87_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA87_SK_LEN] = + hex::decode(&self.seed).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + } + val => panic!("Invalid parameter set: {}", val), + } + } +} + +// DISABLED: `ML-DSA-sigGen.txt` gives each signing key only as the expanded private key, and this +// crate's private keys can only be built from the 32-byte seed (`from_bytes` returns +// `DecodingError("Invalid seed length")`). The signatures themselves are checked by the full +// `mldsa` crate's copy of this test. +// #[test] +#[allow(non_snake_case)] +fn ML_DSA_sigGen() { + let Some(contents) = bc_test_data(TEST_DATA_DIR, "ML-DSA-sigGen.txt") else { return }; + let test_cases = SigGenTestCase::parse(contents); + + let num_tests = test_cases.len(); + for test_case in test_cases { + test_case.run(); + } + + println!("SUCCESS! ML-DSA-sigGen test cases passed: {}!", num_tests); +} + +#[derive(Clone)] +struct SigGenTestCase { + vs_id: u32, + algorithm: String, + mode: String, + revision: String, + is_sample: bool, + tg_id: u32, + test_type: String, + parameter_set: String, + deterministic: bool, + tc_id: u32, + sk: String, + message: String, + rnd: String, + signature: String, +} + +impl SigGenTestCase { + fn new() -> Self { + Self { + vs_id: 0, + algorithm: String::new(), + mode: String::new(), + revision: String::new(), + is_sample: false, + tg_id: 0, + test_type: String::new(), + parameter_set: String::new(), + deterministic: false, + tc_id: 0, + sk: String::new(), + message: String::new(), + rnd: String::new(), + signature: String::new(), + } + } + + fn is_full(&self) -> bool { + !self.algorithm.is_empty() + } + + fn parse(data: String) -> Vec { + let mut test_cases = Vec::::new(); + let mut test_case = SigGenTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "vsId" => test_case.vs_id = value.parse().unwrap(), + "algorithm" => test_case.algorithm = value.to_string(), + "mode" => test_case.mode = value.to_string(), + "revision" => test_case.revision = value.to_string(), + "isSample" => test_case.is_sample = value.parse().unwrap(), + "tgId" => test_case.tg_id = value.parse().unwrap(), + "testType" => test_case.test_type = value.to_string(), + "parameterSet" => test_case.parameter_set = value.to_string(), + "deterministic" => test_case.deterministic = value.parse().unwrap(), + "tcId" => test_case.tc_id = value.parse().unwrap(), + "sk" => test_case.sk = value.to_string(), + "message" => test_case.message = value.to_string(), + "rnd" => test_case.rnd = value.to_string(), + "signature" => test_case.signature = value.to_string(), + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + fn run(&self) { + assert_eq!(self.mode, "sigGen"); + + let rnd = if self.deterministic { + [0u8; 32] + } else { + hex::decode(&self.rnd).unwrap().as_slice().try_into().unwrap() + }; + + match self.parameter_set.as_str() { + "ML-DSA-44" => { + let sk = MLDSA44PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); + + // note: a sign_mu_deterministic() is being exposed, but not sign_deterministic() + // so it is necessary to manually compute mu + // let mu = MLDSA44::compute_mu_from_tr( + // &hex::decode(&self.message).unwrap(), + // None, + // sk.tr(), + // ).unwrap(); + let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); + mb.do_update(&hex::decode(&self.message).unwrap()); + let mu = mb.do_final(); + + let sig = MLDSA44::sign_mu_deterministic(&sk, &mu, rnd).unwrap(); + assert_eq!( + &sig, + &*hex::decode(&self.signature).unwrap(), + "ML-DSA-sigGen params: {}, vsId: {}, tgId: {}, tcId: {}", + self.parameter_set, + self.vs_id, + self.tg_id, + self.tc_id + ); + } + "ML-DSA-65" => { + let sk = MLDSA65PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); + + // note: a sign_mu_deterministic() is being exposed, but not sign_deterministic() + // so it is necessary to manually compute mu + // let mu = MLDSA65::compute_mu_from_tr( + // &hex::decode(&self.message).unwrap(), + // None, + // sk.tr(), + // ).unwrap(); + let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); + mb.do_update(&hex::decode(&self.message).unwrap()); + let mu = mb.do_final(); + + let sig = MLDSA65::sign_mu_deterministic(&sk, &mu, rnd).unwrap(); + assert_eq!(&sig, &*hex::decode(&self.signature).unwrap()); + } + "ML-DSA-87" => { + let sk = MLDSA87PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); + + // note: a sign_mu_deterministic() is being exposed, but not sign_deterministic() + // so it is necessary to manually compute mu + // let mu = MLDSA87::compute_mu_from_tr( + // &hex::decode(&self.message).unwrap(), + // None, + // sk.tr(), + // ).unwrap(); + let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); + mb.do_update(&hex::decode(&self.message).unwrap()); + let mu = mb.do_final(); + + let sig = MLDSA87::sign_mu_deterministic(&sk, &mu, rnd).unwrap(); + assert_eq!(&sig, &*hex::decode(&self.signature).unwrap()); + } + val => panic!("Invalid parameter set: {}", val), + } + } +} + +/// The sigVer vectors, like the sigGen ones, sign the message itself (ML-DSA's internal interface), +/// so each is checked against a mu built by `BustedMuBuilder` rather than through `verify`, which +/// would add the context prefix. +#[test] +#[allow(non_snake_case)] +fn ML_DSA_sigVer() { + let Some(contents) = bc_test_data(TEST_DATA_DIR, "ML-DSA-sigVer.txt") else { return }; + let test_cases = SigVerTestCase::parse(contents); + + for test_case in test_cases { + test_case.run(); + } +} + +#[derive(Clone)] +struct SigVerTestCase { + vs_id: u32, + algorithm: String, + mode: String, + revision: String, + is_sample: bool, + tg_id: u32, + test_type: String, + parameter_set: String, + pk: String, + tc_id: u32, + message: String, + signature: String, + test_passed: bool, +} + +impl SigVerTestCase { + fn new() -> Self { + Self { + vs_id: 0, + algorithm: String::new(), + mode: String::new(), + revision: String::new(), + is_sample: false, + tg_id: 0, + test_type: String::new(), + parameter_set: String::new(), + tc_id: 0, + pk: String::new(), + message: String::new(), + signature: String::new(), + test_passed: false, + } + } + + fn is_full(&self) -> bool { + !self.algorithm.is_empty() + } + + fn parse(data: String) -> Vec { + let mut test_cases = Vec::::new(); + let mut test_case = SigVerTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "vsId" => test_case.vs_id = value.parse().unwrap(), + "algorithm" => test_case.algorithm = value.to_string(), + "mode" => test_case.mode = value.to_string(), + "revision" => test_case.revision = value.to_string(), + "isSample" => test_case.is_sample = value.parse().unwrap(), + "tgId" => test_case.tg_id = value.parse().unwrap(), + "testType" => test_case.test_type = value.to_string(), + "parameterSet" => test_case.parameter_set = value.to_string(), + "pk" => test_case.pk = value.to_string(), + "tcId" => test_case.tc_id = value.parse().unwrap(), + "message" => test_case.message = value.to_string(), + "signature" => test_case.signature = value.to_string(), + "testPassed" => test_case.test_passed = value.parse().unwrap(), + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + fn run(&self) { + assert_eq!(self.mode, "sigVer"); + + match self.parameter_set.as_str() { + "ML-DSA-44" => { + let pk = MLDSA44PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); + + let mu = BustedMuBuilder::compute_mu( + &hex::decode(&self.message).unwrap(), + &pk.compute_tr(), + ) + .unwrap(); + let sig = + hex::decode(&self.signature).unwrap().try_into().expect("signature length"); + match MLDSA44::verify_mu(&pk, &mu, &sig) { + Ok(()) => { + if !self.test_passed { + panic!("Verification succeeded when it shouldn't have!") + } + } + Err(SignatureError::SignatureVerificationFailed) => { + if self.test_passed { + panic!( + "Verification failed when it shouldn't have! vsId: {}, tgId: {}, tcId: {}", + self.vs_id, self.tg_id, self.tc_id + ) + } + } + _ => panic!("An unexpected error occurred"), + } + } + "ML-DSA-65" => { + let pk = MLDSA65PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); + + let mu = BustedMuBuilder::compute_mu( + &hex::decode(&self.message).unwrap(), + &pk.compute_tr(), + ) + .unwrap(); + let sig = + hex::decode(&self.signature).unwrap().try_into().expect("signature length"); + match MLDSA65::verify_mu(&pk, &mu, &sig) { + Ok(()) => { + if self.test_passed { /* good */ + } else { + panic!("Verification succeeded when it shouldn't have!") + } + } + Err(SignatureError::SignatureVerificationFailed) => { + if !self.test_passed { + } else { + panic!("Verification failed when it should have!") + } + } + _ => panic!("An unexpected error occurred"), + } + } + "ML-DSA-87" => { + let pk = MLDSA87PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); + + let mu = BustedMuBuilder::compute_mu( + &hex::decode(&self.message).unwrap(), + &pk.compute_tr(), + ) + .unwrap(); + let sig = + hex::decode(&self.signature).unwrap().try_into().expect("signature length"); + match MLDSA87::verify_mu(&pk, &mu, &sig) { + Ok(()) => { + if self.test_passed { /* good */ + } else { + panic!("Verification succeeded when it shouldn't have!") + } + } + Err(SignatureError::SignatureVerificationFailed) => { + if !self.test_passed { + } else { + panic!("Verification failed when it should have!") + } + } + _ => panic!("An unexpected error occurred"), + } + } + val => panic!("Invalid parameter set: {}", val), + } + } +} + +/// The .rsp vectors give each case a `context`: hex bytes, `zero_length` (an empty context, which +/// still gets the usual prefix), or `none` (no context at all: the internal interface, as for the +/// ACVP files above). +#[test] +#[allow(non_snake_case)] +fn ML_DSA_rsp() { + // MLDsa44 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa44.rsp") else { return }; + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa44"); + } + + // MLDsa65 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa65.rsp") else { return }; + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa65"); + } + + // MLDsa87 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa87.rsp") else { return }; + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa87"); + } + + // MLDsa44 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa44sha512.rsp") else { return }; + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa44"); + } + + // MLDsa65 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa65sha512.rsp") else { return }; + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa65"); + } + + // MLDsa87 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa87sha512.rsp") else { return }; + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa87"); + } +} + +#[derive(Clone)] +struct MldsaRspTestCase { + count: u32, + seed: String, + mlen: u32, + msg: String, + pk: String, + sk: String, + smlen: u32, + sm: String, + message_hash: String, + message_prime: String, + context: String, + /// `context = none`: no context at all, so mu omits the context prefix entirely. + no_context: bool, +} + +impl MldsaRspTestCase { + fn new() -> Self { + Self { + count: 0, + seed: String::new(), + mlen: 0, + msg: String::new(), + pk: String::new(), + sk: String::new(), + smlen: 0, + sm: String::new(), + message_hash: String::new(), + message_prime: String::new(), + context: String::new(), + no_context: false, + } + } + + fn is_full(&self) -> bool { + !self.seed.is_empty() + } + + fn parse(data: String) -> Vec> { + let mut test_cases = Vec::new(); + let mut test_case = MldsaRspTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "count" => test_case.count = value.parse().unwrap(), + "seed" => test_case.seed = value.to_string(), + "mlen" => test_case.mlen = value.parse().unwrap(), + "msg" => test_case.msg = value.to_string(), + "pk" => test_case.pk = value.to_string(), + "sk" => test_case.sk = value.to_string(), + "smlen" => test_case.smlen = value.parse().unwrap(), + "sm" => test_case.sm = value.to_string(), + "message_hash" => test_case.message_hash = value.to_string(), + "message_prime" => test_case.message_prime = value.to_string(), + "context" => { + // Set on every record: `test_case` is reused from one record to the next. + test_case.no_context = value == "none"; + test_case.context = match value { + "none" | "zero_length" => String::new(), + hex => hex.to_string(), + }; + } + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + /// mu for a plain ML-DSA case: `BustedMuBuilder` for `context = none`, otherwise the usual + /// context-prefixed mu. + fn mu(&self, tr: &[u8; 64]) -> [u8; 64] { + let msg = hex::decode(&self.msg).unwrap(); + if self.no_context { + BustedMuBuilder::compute_mu(&msg, tr).unwrap() + } else { + MLDSA65::compute_mu_from_tr(tr, &msg, Some(&hex::decode(&self.context).unwrap())) + .unwrap() + } + } + + fn run(&self, parameter_set: &str) { + match parameter_set { + "MLDsa44" => { + let mut seed = KeyMaterial256::from_bytes_as_type( + &hex::decode(&self.seed).unwrap(), + KeyType::Seed, + ) + .unwrap(); + // for the purposes of the test cases, accept an all-zero seed + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + + let (pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA44_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA44_FULL_SK_LEN] = + hex::decode(&self.sk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode_full_sk(), sk_sized); + + if IS_HASH_MLDSA { + // we're only testing SHA512 + let ph: [u8; 64] = SHA512::new() + .hash(&hex::decode(&self.msg).unwrap()) + .as_slice() + .try_into() + .unwrap(); + assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); + + let sig = HashMLDSA44_with_SHA512::sign_ph_deterministic( + &sk, + Some(&*hex::decode(&self.context).unwrap()), + &ph, + [0u8; 32], + ) + .unwrap(); + assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); + + HashMLDSA44_with_SHA512::verify( + &pk, + &*hex::decode(&self.msg).unwrap(), + Some(&*hex::decode(&self.context).unwrap()), + &sig, + ) + .expect(&format!( + "paramSet: {}, is_hash: {}, count: {}", + parameter_set, IS_HASH_MLDSA, self.count + )); + } else { + // note: we're exposing a sign_mu_deterministic(), but not sign_deterministic() + // so need to manually compute mu + let mu = self.mu(&sk.tr()); + + let sig = MLDSA44::sign_mu_deterministic(&sk, &mu, [0u8; 32]).unwrap(); + assert_eq!( + sig, + &*hex::decode(&self.sm).unwrap(), + "paramSet: {}, count: {}", + parameter_set, + self.count + ); + + if self.no_context { + MLDSA44::verify_mu(&pk, &mu, &sig) + } else { + MLDSA44::verify( + &pk, + &hex::decode(&self.msg).unwrap(), + Some(&hex::decode(&self.context).unwrap()), + &sig, + ) + } + .unwrap(); + } + } + "MLDsa65" => { + let mut seed = KeyMaterial256::from_bytes_as_type( + &hex::decode(&self.seed).unwrap(), + KeyType::Seed, + ) + .unwrap(); + // for the purposes of the test cases, accept an all-zero seed + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + + let (pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA65_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA65_FULL_SK_LEN] = + hex::decode(&self.sk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode_full_sk(), sk_sized); + + if IS_HASH_MLDSA { + // we're only testing SHA512 + let ph: [u8; 64] = SHA512::new() + .hash(&hex::decode(&self.msg).unwrap()) + .as_slice() + .try_into() + .unwrap(); + assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); + + let sig = HashMLDSA65_with_SHA512::sign_ph_deterministic( + &sk, + Some(&*hex::decode(&self.context).unwrap()), + &ph, + [0u8; 32], + ) + .unwrap(); + assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); + + HashMLDSA65_with_SHA512::verify( + &pk, + &*hex::decode(&self.msg).unwrap(), + Some(&*hex::decode(&self.context).unwrap()), + &sig, + ) + .expect(&format!( + "paramSet: {}, isHash: {}, count: {}", + parameter_set, IS_HASH_MLDSA, self.count + )); + } else { + // note: we're exposing a sign_mu_deterministic(), but not sign_deterministic() + // so need to manually compute mu + let mu = self.mu(&sk.tr()); + + let sig = MLDSA65::sign_mu_deterministic(&sk, &mu, [0u8; 32]).unwrap(); + assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); + + if self.no_context { + MLDSA65::verify_mu(&pk, &mu, &sig) + } else { + MLDSA65::verify( + &pk, + &hex::decode(&self.msg).unwrap(), + Some(&hex::decode(&self.context).unwrap()), + &sig, + ) + } + .unwrap(); + } + } + "MLDsa87" => { + let mut seed = KeyMaterial256::from_bytes_as_type( + &hex::decode(&self.seed).unwrap(), + KeyType::Seed, + ) + .unwrap(); + // for the purposes of the test cases, accept an all-zero seed + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + + let (pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA87_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA87_FULL_SK_LEN] = + hex::decode(&self.sk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode_full_sk(), sk_sized); + + if IS_HASH_MLDSA { + // we're only testing SHA512 + let ph: [u8; 64] = SHA512::new() + .hash(&hex::decode(&self.msg).unwrap()) + .as_slice() + .try_into() + .unwrap(); + assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); + + let sig = HashMLDSA87_with_SHA512::sign_ph_deterministic( + &sk, + Some(&*hex::decode(&self.context).unwrap()), + &ph, + [0u8; 32], + ) + .unwrap(); + assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); + + HashMLDSA87_with_SHA512::verify( + &pk, + &*hex::decode(&self.msg).unwrap(), + Some(&*hex::decode(&self.context).unwrap()), + &sig, + ) + .unwrap(); + } else { + // note: we're exposing a sign_mu_deterministic(), but not sign_deterministic() + // so need to manually compute mu + let mu = self.mu(&sk.tr()); + + let sig = MLDSA87::sign_mu_deterministic(&sk, &mu, [0u8; 32]).unwrap(); + assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); + + if self.no_context { + MLDSA87::verify_mu(&pk, &mu, &sig) + } else { + MLDSA87::verify( + &pk, + &hex::decode(&self.msg).unwrap(), + Some(&hex::decode(&self.context).unwrap()), + &sig, + ) + } + .unwrap(); + } + } + val => panic!("Invalid parameter set: {}", val), + } + } +} + +/// This builds a "busted" mu where the ctx is absent (not 0-length, but actually not there) +/// just for the sake of compatibility with the bc-test-data tests +pub struct BustedMuBuilder { + h: SHAKE256, +} + +impl BustedMuBuilder { + /// Algorithm 7 + /// 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀′, 64) + pub fn compute_mu(msg: &[u8], tr: &[u8; 64]) -> Result<[u8; 64], SignatureError> { + let mut mu_builder = Self::do_init(&tr)?; + mu_builder.do_update(msg); + let mu = mu_builder.do_final(); + + Ok(mu) + } + + /// This function requires the public key hash `tr`, which can be computed from the public key using [`MLDSAPublicKey::compute_tr`]. + pub fn do_init(tr: &[u8; 64] /*ctx: Option<&[u8]>*/) -> Result { + // let ctx = match ctx { + // Some(ctx) => ctx, + // None => &[] + // }; + + // Algorithm 2 + // 1: if |𝑐𝑡𝑥| > 255 then + // if ctx.len() > 255 { + // return Err(SignatureError::LengthError("ctx value is longer than 255 bytes")); + // } + + // Algorithm 7 + // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) + let mut mb = Self { h: SHAKE256::new() }; + mb.h.do_update(tr); + + // Algorithm 2 + // 10: 𝑀′ ← BytesToBits(IntegerToBytes(0, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥) ∥ 𝑀 + // all done together + // 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) + } + + /// Stream a chunk of the message. + pub fn do_update(&mut self, msg_chunk: &[u8]) { + self.h.do_update(msg_chunk); + } + + /// Finalize and return the mu value. + pub fn do_final(self) -> [u8; 64] { + // Completion of + // Algorithm 7 + // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) + let mut mu = [0u8; 64]; + self.h.into_squeezer().do_output_out(&mut mu); + + mu + } +} diff --git a/crypto/mldsa-lowmemory/tests/mldsa_key_tests.rs b/crypto/mldsa-lowmemory/tests/mldsa_key_tests.rs index d97563d1..4b42d647 100644 --- a/crypto/mldsa-lowmemory/tests/mldsa_key_tests.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_key_tests.rs @@ -4,9 +4,11 @@ mod mldsa_key_tests { #![allow(unused_imports)] use bouncycastle_core::errors::SignatureError; + use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material; use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; - use bouncycastle_core::traits::{SecurityStrength, SignaturePrivateKey, SignaturePublicKey}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; use bouncycastle_core_test_framework::signature::TestFrameworkSignatureKeys; use bouncycastle_hex as hex; use bouncycastle_mldsa_lowmemory::mldsa::{MLDSA_SEED_LEN, MLDSA44_FULL_SK_LEN}; @@ -97,19 +99,19 @@ mod mldsa_key_tests { // It rejects a keyen with a seed too weak, and preserves the seed otherwise let mut seed128 = seed.clone(); - key_material::do_hazardous_operations(&mut seed128, |seed| { + do_hazardous_operations(&mut seed128, |seed| { seed.set_security_strength(SecurityStrength::_128bit) }) .unwrap(); let mut seed192 = seed.clone(); - key_material::do_hazardous_operations(&mut seed192, |seed| { + do_hazardous_operations(&mut seed192, |seed| { seed.set_security_strength(SecurityStrength::_192bit) }) .unwrap(); let mut seed256 = seed.clone(); - key_material::do_hazardous_operations(&mut seed256, |seed| { + do_hazardous_operations(&mut seed256, |seed| { seed.set_security_strength(SecurityStrength::_256bit) }) .unwrap(); diff --git a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs index 69832aa2..85979fd5 100644 --- a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs @@ -3,11 +3,11 @@ mod mldsa_tests { use crate::{MLDSA44_KAT1, MLDSA65_KAT1, MLDSA87_KAT1}; use bouncycastle_core::errors::{RNGError, SignatureError, SuspendableError}; - use bouncycastle_core::key_material; + use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; + use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - RNG, SecurityStrength, 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; @@ -277,10 +277,8 @@ mod mldsa_tests { assert_eq!(derived_pk.encode(), expected_pk_bytes.as_slice()); // success case KeyType: BytesFullEntropy - key_material::do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::CryptographicRandom) - }) - .unwrap(); + do_hazardous_operations(&mut seed, |seed| seed.set_key_type(KeyType::CryptographicRandom)) + .unwrap(); _ = MLDSA44::keygen_from_seed(&seed).unwrap(); // Failure case: key type != Seed || BytesFullEntropy @@ -343,21 +341,22 @@ mod mldsa_tests { .unwrap(); // test the streaming API on the same value - let mut s = MLDSA44::sign_init(&sk, Some(&hex::decode(MLDSA44_KAT1.ctx).unwrap())).unwrap(); + let mut s = + MLDSA44::do_sign_init(&sk, Some(&hex::decode(MLDSA44_KAT1.ctx).unwrap())).unwrap(); s.set_signer_rnd(rnd); - s.sign_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); - let sig = s.sign_final().unwrap(); + s.do_sign_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); + let sig = s.do_sign_final().unwrap(); let decoded_sig: &[u8; MLDSA44_SIG_LEN] = &hex::decode(MLDSA44_KAT1.signature).unwrap().try_into().unwrap(); assert_eq!(&sig, decoded_sig); // Then with the message broken into chunks - let mut s = MLDSA44::sign_init(&sk, Some(b"streaming API chunked")).unwrap(); + let mut s = MLDSA44::do_sign_init(&sk, Some(b"streaming API chunked")).unwrap(); s.set_signer_rnd(rnd); for msg_chunk in DUMMY_SEED.chunks(100) { - s.sign_update(msg_chunk); + s.do_sign_update(msg_chunk); } - let sig_val = s.sign_final().unwrap(); + let sig_val = s.do_sign_final().unwrap(); MLDSA44::verify(&sk.derive_pk(), DUMMY_SEED, Some(b"streaming API chunked"), &sig_val) .unwrap(); @@ -390,10 +389,11 @@ mod mldsa_tests { .unwrap(); // test the streaming API on the same value - let mut s = MLDSA65::sign_init(&sk, Some(&hex::decode(MLDSA65_KAT1.ctx).unwrap())).unwrap(); + let mut s = + MLDSA65::do_sign_init(&sk, Some(&hex::decode(MLDSA65_KAT1.ctx).unwrap())).unwrap(); s.set_signer_rnd(rnd); - s.sign_update(&hex::decode(MLDSA65_KAT1.message).unwrap()); - let sig = s.sign_final().unwrap(); + s.do_sign_update(&hex::decode(MLDSA65_KAT1.message).unwrap()); + let sig = s.do_sign_final().unwrap(); let decoded_sig: &[u8; MLDSA65_SIG_LEN] = &hex::decode(MLDSA65_KAT1.signature).unwrap().try_into().unwrap(); assert_eq!(&sig, decoded_sig); @@ -427,10 +427,11 @@ mod mldsa_tests { .unwrap(); // test the streaming API on the same value - let mut s = MLDSA87::sign_init(&sk, Some(&hex::decode(MLDSA87_KAT1.ctx).unwrap())).unwrap(); + let mut s = + MLDSA87::do_sign_init(&sk, Some(&hex::decode(MLDSA87_KAT1.ctx).unwrap())).unwrap(); s.set_signer_rnd(rnd); - s.sign_update(&hex::decode(MLDSA87_KAT1.message).unwrap()); - let sig = s.sign_final().unwrap(); + s.do_sign_update(&hex::decode(MLDSA87_KAT1.message).unwrap()); + let sig = s.do_sign_final().unwrap(); let decoded_sig: &[u8; MLDSA87_SIG_LEN] = &hex::decode(MLDSA87_KAT1.signature).unwrap().try_into().unwrap(); assert_eq!(&sig, decoded_sig); @@ -538,7 +539,7 @@ mod mldsa_tests { KeyType::Seed, ) .unwrap(); - key_material::do_hazardous_operations(&mut low_security_seed, |seed| { + do_hazardous_operations(&mut low_security_seed, |seed| { seed.set_security_strength(SecurityStrength::_192bit) }) .unwrap(); @@ -552,7 +553,7 @@ mod mldsa_tests { KeyType::Seed, ) .unwrap(); - key_material::do_hazardous_operations(&mut low_security_seed, |seed| { + do_hazardous_operations(&mut low_security_seed, |seed| { seed.set_security_strength(SecurityStrength::_128bit) }) .unwrap(); @@ -568,16 +569,16 @@ mod mldsa_tests { MLDSA44::sign_init_from_seed(&seed, Some(&hex::decode(MLDSA44_KAT1.ctx).unwrap())) .unwrap(); s.set_signer_rnd(rnd); - s.sign_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); - let sig = s.sign_final().unwrap(); + s.do_sign_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); + let sig = s.do_sign_final().unwrap(); assert_eq!(&sig, &expected_sig); // while we're at it, test the streaming verifier cause I'm not sure where else this is being tested. let mut v = - MLDSA44::verify_init(&pk, Some(&hex::decode(MLDSA44_KAT1.ctx).unwrap())).unwrap(); - v.verify_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); - v.verify_final(&expected_sig).unwrap(); + MLDSA44::do_verify_init(&pk, Some(&hex::decode(MLDSA44_KAT1.ctx).unwrap())).unwrap(); + v.do_verify_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); + v.do_verify_final(&expected_sig).unwrap(); } #[test] @@ -589,11 +590,11 @@ mod mldsa_tests { let (_pk, sk) = MLDSA44::keygen().unwrap(); // ctx with len 255 works - MLDSA44::sign_init(&sk, Some(&[1u8; 255])).unwrap(); + MLDSA44::do_sign_init(&sk, Some(&[1u8; 255])).unwrap(); // ctx with len 256 is too long let too_long_ctx = [1u8; 256]; - match MLDSA44::sign_init(&sk, Some(&too_long_ctx)) { + match MLDSA44::do_sign_init(&sk, Some(&too_long_ctx)) { Err(SignatureError::LengthError(_)) => { /* good */ } _ => panic!("Expected error for ctx too long"), } @@ -867,7 +868,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 +875,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-lowmemory/tests/wycheproof.rs b/crypto/mldsa-lowmemory/tests/mldsa_wycheproof.rs similarity index 78% rename from crypto/mldsa-lowmemory/tests/wycheproof.rs rename to crypto/mldsa-lowmemory/tests/mldsa_wycheproof.rs index ac6de879..ce1cddb2 100644 --- a/crypto/mldsa-lowmemory/tests/wycheproof.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_wycheproof.rs @@ -22,154 +22,93 @@ #![allow(dead_code)] use bouncycastle_core::errors::SignatureError; -use bouncycastle_core::key_material; +use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{SecurityStrength, SignaturePublicKey, SignatureVerifier}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{SignaturePublicKey, SignatureVerifier}; +use bouncycastle_core_test_framework::test_data_loaders::{Value, wycheproof_json}; use bouncycastle_hex as hex; use bouncycastle_mldsa_lowmemory::{ MLDSA44, MLDSA44PublicKey, MLDSA65, MLDSA65PublicKey, MLDSA87, MLDSA87PublicKey, MLDSAPublicKeyTrait, MLDSATrait, MuBuilder, }; -#[cfg(test)] -mod wycheproof { - use crate::{MLDSASignSeedTestCase, MLDSAVerifyTestCase, ParameterSet}; - use std::fs; - use std::path::Path; - use std::sync::Once; +#[test] +fn mldsa_44_sign_seed_test() { + let Some(json) = wycheproof_json("mldsa_44_sign_seed_test.json") else { return }; + let test_cases = MLDSASignSeedTestCase::parse(json, ParameterSet::Mldsa44); - const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; - const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; - - 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!("wycheproof found at: {:?}", TEST_DATA_PATH_RELATIVE), - 2 => println!("wycheproof found at: {:?}", TEST_DATA_PATH), - _ => println!("WARNING: wycheproof directory not found; tests will be skipped"), - }); - - if !found == 3 { - return Err(()); - } - - 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) + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa44(); } - #[test] - fn mldsa_44_sign_seed_test() { - let contents = match get_test_data("mldsa_44_sign_seed_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MLDSASignSeedTestCase::parse(contents, ParameterSet::Mldsa44); + println!("mldsa_44_sign_seed_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa44(); - } +#[test] +fn mldsa_44_verify_test() { + let Some(json) = wycheproof_json("mldsa_44_verify_test.json") else { return }; + let test_cases = MLDSAVerifyTestCase::parse(json, ParameterSet::Mldsa44); - println!("mldsa_44_sign_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa44(); } - #[test] - fn mldsa_44_verify_test() { - let contents = match get_test_data("mldsa_44_verify_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MLDSAVerifyTestCase::parse(contents, ParameterSet::Mldsa44); + println!("mldsa_44_verify_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa44(); - } +#[test] +fn mldsa_65_sign_seed_test() { + let Some(json) = wycheproof_json("mldsa_65_sign_seed_test.json") else { return }; + let test_cases = MLDSASignSeedTestCase::parse(json, ParameterSet::Mldsa65); - println!("mldsa_44_verify_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa65(); } - #[test] - fn mldsa_65_sign_seed_test() { - let contents = match get_test_data("mldsa_65_sign_seed_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MLDSASignSeedTestCase::parse(contents, ParameterSet::Mldsa65); + println!("mldsa_65_sign_seed_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa65(); - } +#[test] +fn mldsa_65_verify_test() { + let Some(json) = wycheproof_json("mldsa_65_verify_test.json") else { return }; + let test_cases = MLDSAVerifyTestCase::parse(json, ParameterSet::Mldsa65); - println!("mldsa_65_sign_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa65(); } - #[test] - fn mldsa_65_verify_test() { - let contents = match get_test_data("mldsa_65_verify_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MLDSAVerifyTestCase::parse(contents, ParameterSet::Mldsa65); + println!("mldsa_65_verify_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa65(); - } +#[test] +fn mldsa_87_sign_seed_test() { + let Some(json) = wycheproof_json("mldsa_87_sign_seed_test.json") else { return }; + let test_cases = MLDSASignSeedTestCase::parse(json, ParameterSet::Mldsa87); - println!("mldsa_65_verify_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa87(); } - #[test] - fn mldsa_87_sign_seed_test() { - let contents = match get_test_data("mldsa_87_sign_seed_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MLDSASignSeedTestCase::parse(contents, ParameterSet::Mldsa87); + println!("mldsa_87_sign_seed_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa87(); - } +#[test] +fn mldsa_87_verify_test() { + let Some(json) = wycheproof_json("mldsa_87_verify_test.json") else { return }; + let test_cases = MLDSAVerifyTestCase::parse(json, ParameterSet::Mldsa87); - println!("mldsa_87_sign_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa87(); } - #[test] - fn mldsa_87_verify_test() { - let contents = match get_test_data("mldsa_87_verify_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; - let test_cases = MLDSAVerifyTestCase::parse(contents, ParameterSet::Mldsa87); - - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa87(); - } - - println!("mldsa_87_verify_test: all {} test cases passed.", num_test_cases); - } + println!("mldsa_87_verify_test: all {} test cases passed.", num_test_cases); } /* Structs for holding test data */ @@ -216,10 +155,7 @@ impl MLDSASignSeedTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); @@ -273,7 +209,7 @@ impl MLDSASignSeedTestCase { } }; // allow an all-zero seed for testing - key_material::do_hazardous_operations(&mut seed, |seed| { + do_hazardous_operations(&mut seed, |seed| { seed.set_key_type(KeyType::Seed)?; match seed.set_security_strength(SecurityStrength::_256bit) { Ok(_) => Ok(()), @@ -374,7 +310,7 @@ impl MLDSASignSeedTestCase { } }; // allow an all-zero seed for testing - key_material::do_hazardous_operations(&mut seed, |seed| { + do_hazardous_operations(&mut seed, |seed| { seed.set_key_type(KeyType::Seed).unwrap(); match seed.set_security_strength(SecurityStrength::_256bit) { Ok(_) => Ok(()), @@ -475,7 +411,7 @@ impl MLDSASignSeedTestCase { } }; // allow an all-zero seed for testing - key_material::do_hazardous_operations(&mut seed, |seed| { + do_hazardous_operations(&mut seed, |seed| { seed.set_key_type(KeyType::Seed).unwrap(); match seed.set_security_strength(SecurityStrength::_256bit) { Ok(_) => Ok(()), @@ -585,10 +521,7 @@ impl MLDSAVerifyTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); diff --git a/crypto/mldsa/Cargo.toml b/crypto/mldsa/Cargo.toml index 6071b765..6b852769 100644 --- a/crypto/mldsa/Cargo.toml +++ b/crypto/mldsa/Cargo.toml @@ -15,7 +15,6 @@ bouncycastle-core-test-framework.workspace = true bouncycastle-hex.workspace = true bouncycastle-rng.workspace = true criterion.workspace = true -serde_json = "1.0" [[bench]] name = "mldsa_benches" diff --git a/crypto/mldsa/src/aux_functions.rs b/crypto/mldsa/src/aux_functions.rs index bf0c2f91..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 }; @@ -953,19 +956,18 @@ pub(crate) fn conditional_add_q(a: i32) -> i32 { a + ((a >> 31) & q) } -#[test] -/// These are the results it's giving; I'm not sure if these are "correct" or not. -fn test_conditional_add_q() { - assert_eq!(conditional_add_q(-q - 1), -1); - assert_eq!(conditional_add_q(-q), 0); - assert_eq!(conditional_add_q(-q - 2), -2); - assert_eq!(conditional_add_q(-q + 1), 1); - assert_eq!(conditional_add_q(-1), q - 1); - assert_eq!(conditional_add_q(0), 0); - assert_eq!(conditional_add_q(1), 1); - assert_eq!(conditional_add_q(q - 1), q - 1); - assert_eq!(conditional_add_q(q), q); - assert_eq!(conditional_add_q(q + 1), q + 1); +/// Plain (non-Montgomery) reduction: for 𝑎 ≤ 2^31 − 2^22 − 1 returns 𝑟 ≡ 𝑎 (mod 𝑞) with |𝑟| ≤ 6283008 < 𝑞. +/// `reduce32` in the reference implementation (pq-crystals/dilithium, ref/reduce.c). +/// +/// This is not in FIPS 204 since it assumes all arithmetic is done mod q. +/// In this implementation, sums of Montgomery products are left unreduced, but `inv_ntt` (Algorithm 42) +/// needs |input| < 𝑞 to stay within an `i32`, so this is applied to those sums first. Omitting it before +/// the verifier's NTT⁻¹ is exploitable (, Wycheproof mldsa_87_verify tcId 240/241). +pub(crate) fn reduce32(a: i32) -> i32 { + // The reference implementation's stated input bound; above it `a + 2^22` overflows. + debug_assert!(a <= i32::MAX - (1 << 22)); + let t = (a + (1 << 22)) >> 23; + a - t * q } /// Constants for NTT @@ -997,3 +999,56 @@ pub(crate) const ZETAS: [i32; 256] = [ -2235985, -420899, -2286327, 183443, -976891, 1612842, -3545687, -554416, 3919660, -48306, -1362209, 3937738, 1400424, -846154, 1976782, ]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + /// These are the results it's giving; I'm not sure if these are "correct" or not. + fn test_conditional_add_q() { + assert_eq!(conditional_add_q(-q - 1), -1); + assert_eq!(conditional_add_q(-q), 0); + assert_eq!(conditional_add_q(-q - 2), -2); + assert_eq!(conditional_add_q(-q + 1), 1); + assert_eq!(conditional_add_q(-1), q - 1); + assert_eq!(conditional_add_q(0), 0); + assert_eq!(conditional_add_q(1), 1); + assert_eq!(conditional_add_q(q - 1), q - 1); + assert_eq!(conditional_add_q(q), q); + assert_eq!(conditional_add_q(q + 1), q + 1); + } + + #[test] + fn test_reduce32() { + // congruent to the input mod q, and within the reference implementation's stated output range + for &a in &[ + 0, + 1, + -1, + q - 1, + q, + q + 1, + -q, + -q - 1, + 8 * q, + -8 * q, + 6283008, + -6283008, + i32::MIN, + i32::MAX - (1 << 22), + ] { + let r = reduce32(a); + assert!((-6283008..=6283008).contains(&r), "reduce32({a}) = {r} out of range"); + assert_eq!( + (r as i64 - a as i64).rem_euclid(q as i64), + 0, + "reduce32({a}) = {r} not congruent" + ); + assert!(r.abs() < q); + } + // the largest sum inv_ntt may see: (l + 1) products each in (-q, q), for l = 7 + assert!(reduce32(8 * q - 8).abs() < q); + assert!(reduce32(-(8 * q - 8)).abs() < q); + } +} diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index 35747605..f7478c3d 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -82,9 +82,10 @@ use crate::{ }; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SecurityStrength, - SignatureVerifier, Signer, XOF, + Algorithm, AlgorithmOID, Hash, PHSignatureVerifier, PHSigner, RNG, SignatureVerifier, Signer, + XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use core::marker::PhantomData; @@ -384,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 @@ -411,9 +412,9 @@ impl< Ok(bytes_written) } - /// To be used for deterministic signing in conjunction with the [`Signer::sign_init`], - /// [`Signer::sign_update`], and [`Signer::sign_final`] flow. - /// Can be set anywhere after [`Signer::sign_init`] and before [`Signer::sign_final`] + /// To be used for deterministic signing in conjunction with the [`Signer::do_sign_init`], + /// [`Signer::do_sign_update`], and [`Signer::do_sign_final`] flow. + /// Can be set anywhere after [`Signer::do_sign_init`] and before [`Signer::do_sign_final`] pub fn set_signer_rnd(&mut self, rnd: [u8; 32]) { self.signer_rnd = Some(rnd); } @@ -489,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 }; @@ -556,7 +557,7 @@ impl< Self::sign_ph_out(sk, &ph_m, ctx, output) } - fn sign_init(sk: &SK, ctx: Option<&[u8]>) -> Result { + fn do_sign_init(sk: &SK, ctx: Option<&[u8]>) -> Result { let (ctx, ctx_len) = Self::parse_ctx(ctx)?; Ok(Self { _phantom: PhantomData, @@ -570,23 +571,23 @@ impl< }) } - fn sign_update(&mut self, msg_chunk: &[u8]) { + fn do_sign_update(&mut self, msg_chunk: &[u8]) { self.hash.do_update(msg_chunk); } - fn sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; - self.sign_final_out(&mut out)?; + self.do_sign_final_out(&mut out)?; Ok(out) } - fn sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { let ph: [u8; PH_LEN] = self.hash.do_final().try_into().unwrap(); if self.sk.is_none() && self.seed.is_none() { return Err(SignatureError::GenericError( - "sign_final_out called on a streaming context with no private key or seed; \ - this is a verify-initialized context. Call verify_final instead", + "do_sign_final_out called on a streaming context with no private key or seed; \ + this is a verify-initialized context. Call do_verify_final instead", )); } @@ -648,7 +649,7 @@ impl< Self::verify_ph(pk, &ph_m, ctx, sig) } - fn verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result { + fn do_verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result { let (ctx, ctx_len) = Self::parse_ctx(ctx)?; Ok(Self { _phantom: Default::default(), @@ -662,11 +663,11 @@ impl< }) } - fn verify_update(&mut self, msg_chunk: &[u8]) { + fn do_verify_update(&mut self, msg_chunk: &[u8]) { self.hash.do_update(msg_chunk); } - fn verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { assert!( self.pk.is_some(), "Somehow you managed to construct a streaming verifier without a public key, impressive!" diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs index 15d0cb01..6bf98ec9 100644 --- a/crypto/mldsa/src/lib.rs +++ b/crypto/mldsa/src/lib.rs @@ -88,7 +88,7 @@ //! Values in parentheses are the usual sizes in our un-optimized implementation in the \[bouncycastle_mldsa] crate. //! //! -//! # 🚨 Security 🚨 +//! # 🚨 Security Considerations 🚨 //! //! This crate intends to expose only APIs that are secure to use. //! There are, however, a few exceptions that are worth mentioning. diff --git a/crypto/mldsa/src/matrix.rs b/crypto/mldsa/src/matrix.rs index e08bb62f..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}; @@ -120,6 +120,9 @@ pub trait VectorTrait: /// Montgomery-reduces every coefficient. fn reduce(&mut self); + /// Plainly reduces every coefficient to |𝑐| < 𝑞; see `Polynomial::reduce32`. + fn reduce32(&mut self); + /// Applies Algorithm 41 NTT(𝑤) to every coordinate. fn ntt(&mut self); @@ -238,6 +241,12 @@ impl VectorTrait for Vector { } } + fn reduce32(&mut self) { + for i in 0..LEN { + self[i].reduce32(); + } + } + fn ntt(&mut self) { for i in 0..LEN { self[i].ntt(); @@ -302,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 9e003579..be78cb93 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -19,10 +19,10 @@ //! let msg_chunk1 = b"The quick brown fox "; //! let msg_chunk2 = b"jumped over the lazy dog"; //! -//! let mut signer = MLDSA65::sign_init(&sk, None).unwrap(); -//! signer.sign_update(msg_chunk1); -//! signer.sign_update(msg_chunk2); -//! let sig = signer.sign_final().unwrap(); +//! let mut signer = MLDSA65::do_sign_init(&sk, None).unwrap(); +//! signer.do_sign_update(msg_chunk1); +//! signer.do_sign_update(msg_chunk2); +//! let sig = signer.do_sign_final().unwrap(); //! // This is the signature value that can be saved to a file or whatever is needed. //! //! // This is compatible with a verifier that takes the whole message as one chunk: @@ -35,11 +35,11 @@ //! //! // There is also a streaming API for the verifier. //! -//! let mut verifier = MLDSA65::verify_init(&pk, None).unwrap(); -//! verifier.verify_update(msg_chunk1); -//! verifier.verify_update(msg_chunk2); +//! let mut verifier = MLDSA65::do_verify_init(&pk, None).unwrap(); +//! verifier.do_verify_update(msg_chunk1); +//! verifier.do_verify_update(msg_chunk2); //! -//! match verifier.verify_final(&sig.as_slice()) { +//! match verifier.do_verify_final(&sig.as_slice()) { //! Ok(()) => println!("Signature is valid!"), //! Err(SignatureError::SignatureVerificationFailed) => println!("Signature is invalid!"), //! Err(e) => panic!("Something else went wrong: {:?}", e), @@ -62,11 +62,11 @@ //! let msg_chunk1 = b"The quick brown fox "; //! let msg_chunk2 = b"jumped over the lazy dog"; //! -//! let mut signer = MLDSA65::sign_init(&sk, Some(b"signing ctx value")).unwrap(); +//! let mut signer = MLDSA65::do_sign_init(&sk, Some(b"signing ctx value")).unwrap(); //! signer.set_signer_rnd([0u8; 32]); // an all-zero rnd is the "deterministic" mode of ML-DSA -//! signer.sign_update(msg_chunk1); -//! signer.sign_update(msg_chunk2); -//! let sig = signer.sign_final().unwrap(); +//! signer.do_sign_update(msg_chunk1); +//! signer.do_sign_update(msg_chunk2); +//! let sig = signer.do_sign_final().unwrap(); //! ``` //! //! # External Mu mode @@ -489,8 +489,9 @@ use crate::{ }; 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, SecurityStrength, 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}; @@ -690,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(𝜌′) @@ -721,6 +723,8 @@ impl< let (t1, mut t0) = { // scope for t let mut t = t_hat; + // Bound-keeping step before NTT⁻¹, not in FIPS 204: see [`Polynomial::reduce32`]. + t.reduce32(); t.inv_ntt(); t.add_vector_ntt(&s2); t.conditional_add_q(); @@ -784,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 }; @@ -827,6 +832,8 @@ impl< let mut y_hat = y.clone(); y_hat.ntt(); let mut w = A_hat.matrix_vector_ntt(&y_hat); + // Bound-keeping step before NTT⁻¹, not in FIPS 204: see [`Polynomial::reduce32`]. + w.reduce32(); w.inv_ntt(); w.conditional_add_q(); w @@ -841,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) @@ -1007,6 +1015,10 @@ impl< t1_shift_hat.scalar_vector_ntt(&c_hat) }; let mut wp_approx = Az.sub_vector(&ct1); + // Bound-keeping step before NTT⁻¹, not in FIPS 204: see `Polynomial::reduce32`. Here it + // is security-critical: 𝐳 and 𝐭1 are attacker-controlled, and without it a crafted + // signature overflows the butterflies (, Wycheproof mldsa_87_verify tcId 240/241). + wp_approx.reduce32(); wp_approx.inv_ntt(); wp_approx.conditional_add_q(); @@ -1019,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 }; @@ -1242,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) @@ -1261,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 }; @@ -1319,6 +1334,8 @@ impl< let mut y_hat = y.clone(); y_hat.ntt(); let mut w = A_hat.matrix_vector_ntt(&y_hat); + // Bound-keeping step before NTT⁻¹, not in FIPS 204: see [`Polynomial::reduce32`]. + w.reduce32(); w.inv_ntt(); w.conditional_add_q(); w @@ -1333,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) @@ -1402,6 +1420,8 @@ impl< // while s2_hat is in scope, derive t0 let mut t = t_hat; + // Bound-keeping step before NTT⁻¹, not in FIPS 204: see [`Polynomial::reduce32`]. + t.reduce32(); t.inv_ntt(); t.add_vector_ntt(&s2); t.conditional_add_q(); @@ -1751,8 +1771,8 @@ pub trait MLDSATrait< rnd: [u8; 32], output: &mut [u8; SIG_LEN], ) -> Result; - /// To be used for deterministic signing in conjunction with the [`MLDSA44::sign_init`], [`MLDSA44::sign_update`], and [`MLDSA44::sign_final`] flow. - /// Can be set anywhere after [`MLDSA44::sign_init`] and before [`MLDSA44::sign_final`]. + /// To be used for deterministic signing in conjunction with the [`MLDSA44::do_sign_init`], [`MLDSA44::do_sign_update`], and [`MLDSA44::do_sign_final`] flow. + /// Can be set anywhere after [`MLDSA44::do_sign_init`] and before [`MLDSA44::do_sign_final`]. fn set_signer_rnd(&mut self, rnd: [u8; 32]); /// Alternative initialization of the streaming signer where the user has their private key /// as a seed and they want to delay its expansion as late as possible for memory-usage reasons. @@ -1809,7 +1829,7 @@ impl< Ok(bytes_written) } - fn sign_init(sk: &SK, ctx: Option<&[u8]>) -> Result { + fn do_sign_init(sk: &SK, ctx: Option<&[u8]>) -> Result { Ok(Self { _phantom: PhantomData, mu_builder: MuBuilder::do_init(&sk.tr(), ctx)?, @@ -1820,23 +1840,23 @@ impl< }) } - fn sign_update(&mut self, msg_chunk: &[u8]) { + fn do_sign_update(&mut self, msg_chunk: &[u8]) { self.mu_builder.do_update(msg_chunk); } - fn sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { + fn do_sign_final(self) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; - self.sign_final_out(&mut out)?; + self.do_sign_final_out(&mut out)?; Ok(out) } - fn sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { + fn do_sign_final_out(self, output: &mut [u8; SIG_LEN]) -> Result { let mu = self.mu_builder.do_final(); if self.sk.is_none() && self.seed.is_none() { return Err(SignatureError::GenericError( - "sign_final_out called on a streaming context with no private key or seed; \ - this is a verify-initialized context. Call verify_final instead", + "do_sign_final_out called on a streaming context with no private key or seed; \ + this is a verify-initialized context. Call do_verify_final instead", )); } @@ -1886,7 +1906,7 @@ impl< Self::verify_mu(pk, Some(&pk.A_hat()), &mu, sig) } - fn verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result { + fn do_verify_init(pk: &PK, ctx: Option<&[u8]>) -> Result { Ok(Self { _phantom: Default::default(), mu_builder: MuBuilder::do_init(&pk.compute_tr(), ctx)?, @@ -1897,11 +1917,11 @@ impl< }) } - fn verify_update(&mut self, msg_chunk: &[u8]) { + fn do_verify_update(&mut self, msg_chunk: &[u8]) { self.mu_builder.do_update(msg_chunk); } - fn verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { + fn do_verify_final(self, sig: &[u8]) -> Result<(), SignatureError> { let mu = self.mu_builder.do_final(); let pk: &PK = self @@ -1961,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) @@ -1976,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 5d4dee7d..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 } @@ -574,6 +574,8 @@ impl let A_hat = expandA::

(&self.rho); let mut t_ntt = A_hat.matrix_vector_ntt(&self.s1_hat); + // Bound-keeping step before NTT⁻¹, not in FIPS 204: see [`Polynomial::reduce32`]. + t_ntt.reduce32(); t_ntt.inv_ntt(); t_ntt }; diff --git a/crypto/mldsa/src/params.rs b/crypto/mldsa/src/params.rs index 67c2ab76..1c0da964 100644 --- a/crypto/mldsa/src/params.rs +++ b/crypto/mldsa/src/params.rs @@ -15,7 +15,8 @@ use crate::hash_mldsa::{ }; use crate::matrix::{Matrix, MatrixTrait, Vector, VectorTrait}; use crate::mldsa::{ML_DSA_44_NAME, ML_DSA_65_NAME, ML_DSA_87_NAME, q}; -use bouncycastle_core::traits::{Algorithm, AlgorithmOID, Hash, HashAlgParams, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, AlgorithmOID, Hash, HashAlgParams}; use bouncycastle_sha2::{SHA256, SHA512}; use bouncycastle_utils::secret::ZeroizablePrimitive; diff --git a/crypto/mldsa/src/polynomial.rs b/crypto/mldsa/src/polynomial.rs index 73b33a0c..d67e4842 100644 --- a/crypto/mldsa/src/polynomial.rs +++ b/crypto/mldsa/src/polynomial.rs @@ -1,7 +1,7 @@ //! Represents a polynomial over the ML-DSA ring. use crate::aux_functions::{ - ZETAS, conditional_add_q, high_bits, low_bits, make_hint, montgomery_reduce, + ZETAS, conditional_add_q, high_bits, low_bits, make_hint, montgomery_reduce, reduce32, }; use crate::mldsa::{N, d, q}; use crate::params::{GAMMA2_Q_MINUS_1_OVER_32, GAMMA2_Q_MINUS_1_OVER_88, MLDSAParams}; @@ -10,7 +10,7 @@ use core::ops::{Index, IndexMut}; /// A polynomial over the ML-DSA ring. /// -/// # 🚨 Security 🚨 +/// # 🚨 Security Considerations 🚨 /// Polynomials themselves are not inherently secret since sometimes they are part of public keys /// and sometimes private keys. /// It is the responsibility of the caller to wrap sensitive instances in `Secret`. @@ -55,6 +55,14 @@ impl Polynomial { } } + /// Reduces every coefficient to |𝑐| < 𝑞 without changing its residue. See [`reduce32`] for when + /// this is required; note that [`Self::reduce`] is a *Montgomery* reduction and is not a substitute. + pub(crate) fn reduce32(&mut self) { + for x in self.coeffs.iter_mut() { + *x = reduce32(*x); + } + } + /// Algorithm 44 AddNTT(𝑎, 𝑏)̂ /// Computes the sum a + 𝑏 of two elements 𝑎, 𝑏 ∈ 𝑇𝑞. /// Note: result could be up to 2q. @@ -219,6 +227,17 @@ impl Polynomial { /// Input: 𝑤_hat = (𝑤_hat[0], … , 𝑤_hat[255]) ∈ 𝑇𝑞. /// Output: Polynomial 𝑤(𝑋) = Σ_{j=0}^{255} 𝑤𝑗𝑋𝑗 ∈ 𝑅𝑞 pub(crate) fn inv_ntt(&mut self) { + // A core input condition on the InverseNTT is that every input + // coefficient must satisfy |𝑤| < 𝑞 for the sums to stay within an i32. + // The reason this is required is because Algorithm 42's butterflies (steps 13-14) + // run 8 levels without reducing, leading to an i32 overflow if the input was out-of-range. + // Callers must pass them through `reduce32` first. + // Note that only a crafted input, such as (Wycheproof mldsa_87_verify tcId 240/241) will trigger this. + debug_assert!( + self.coeffs.iter().all(|c| c.abs() < q), + "inv_ntt input coefficient not reduced below q; call reduce32() first" + ); + let mut m: usize = N; let mut len: usize = 1; diff --git a/crypto/mldsa/tests/bc_test_data.rs b/crypto/mldsa/tests/bc_test_data.rs deleted file mode 100644 index e82df129..00000000 --- a/crypto/mldsa/tests/bc_test_data.rs +++ /dev/null @@ -1,997 +0,0 @@ -// 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. - -#![allow(dead_code)] - -use bouncycastle_core::errors::SignatureError; -use bouncycastle_core::traits::XOF; -use bouncycastle_sha3::SHAKE256; - -#[cfg(test)] -mod bc_test_data { - use crate::BustedMuBuilder; - use bouncycastle_core::errors::SignatureError; - use bouncycastle_core::key_material::{ - KeyMaterial256, KeyMaterialTrait, KeyType, do_hazardous_operations, - }; - use bouncycastle_core::traits::{ - Hash, SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, - }; - use bouncycastle_hex as hex; - use bouncycastle_mldsa::{ - HashMLDSA44_with_SHA512, HashMLDSA65_with_SHA512, HashMLDSA87_with_SHA512, MLDSA44, - MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65, - MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87, - MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyTrait, - MLDSATrait, - }; - use bouncycastle_sha2::SHA512; - use std::fs; - use std::path::Path; - use std::sync::Once; - - const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/pqc/crypto/mldsa"; - const TEST_DATA_PATH: &str = "../bc-test-data/pqc/crypto/mldsa"; - - 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!("wycheproof found at: {:?}", TEST_DATA_PATH_RELATIVE), - 2 => println!("wycheproof found at: {:?}", TEST_DATA_PATH), - _ => println!("WARNING: wycheproof directory not found; tests will be skipped"), - }); - - if !found == 3 { - return Err(()); - } - - 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) - } - - #[test] - #[allow(non_snake_case)] - fn ML_DSA_keyGen() { - let contents = match get_test_data("ML-DSA-keyGen.txt") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = KeyGenTestCase::parse(contents); - - for test_case in test_cases { - test_case.run(); - } - } - - #[derive(Clone)] - struct KeyGenTestCase { - vs_id: u32, - algorithm: String, - mode: String, - revision: String, - is_sample: bool, - tg_id: u32, - test_type: String, - parameter_set: String, - tc_id: u32, - seed: String, - pk: String, - sk: String, - } - - impl KeyGenTestCase { - fn new() -> Self { - Self { - vs_id: 0, - algorithm: String::new(), - mode: String::new(), - revision: String::new(), - is_sample: false, - tg_id: 0, - test_type: String::new(), - parameter_set: String::new(), - tc_id: 0, - seed: String::new(), - pk: String::new(), - sk: String::new(), - } - } - - fn is_full(&self) -> bool { - !self.algorithm.is_empty() - } - - fn parse(data: String) -> Vec { - let mut test_cases = Vec::::new(); - let mut test_case = KeyGenTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "vsId" => test_case.vs_id = value.parse().unwrap(), - "algorithm" => test_case.algorithm = value.to_string(), - "mode" => test_case.mode = value.to_string(), - "revision" => test_case.revision = value.to_string(), - "isSample" => test_case.is_sample = value.parse().unwrap(), - "tgId" => test_case.tg_id = value.parse().unwrap(), - "testType" => test_case.test_type = value.to_string(), - "parameterSet" => test_case.parameter_set = value.to_string(), - "tcId" => test_case.tc_id = value.parse().unwrap(), - "seed" => test_case.seed = value.to_string(), - "pk" => test_case.pk = value.to_string(), - "sk" => test_case.sk = value.to_string(), - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self) { - assert_eq!(self.mode, "keyGen"); - - let mut seed = KeyMaterial256::from_bytes_as_type( - &hex::decode(&self.seed).unwrap(), - KeyType::Seed, - ) - .unwrap(); - // for the purposes of the test cases, accept an all-zero seed - do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed)?; - seed.set_security_strength(SecurityStrength::_256bit) - }) - .unwrap(); - - match self.parameter_set.as_str() { - "ML-DSA-44" => { - let (pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA44_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA44_SK_LEN] = - hex::decode(&self.sk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - } - "ML-DSA-65" => { - let (pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA65_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA65_SK_LEN] = - hex::decode(&self.sk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - } - "ML-DSA-87" => { - let (pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA87_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA87_SK_LEN] = - hex::decode(&self.sk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - } - val => panic!("Invalid parameter set: {}", val), - } - } - } - - #[test] - #[allow(non_snake_case)] - fn ML_DSA_sigGen() { - let contents = match get_test_data("ML-DSA-sigGen.txt") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = SigGenTestCase::parse(contents); - - let num_tests = test_cases.len(); - for test_case in test_cases { - test_case.run(); - } - - println!("SUCCESS! ML-DSA-sigGen test cases passed: {}!", num_tests); - } - - #[derive(Clone)] - struct SigGenTestCase { - vs_id: u32, - algorithm: String, - mode: String, - revision: String, - is_sample: bool, - tg_id: u32, - test_type: String, - parameter_set: String, - deterministic: bool, - tc_id: u32, - sk: String, - message: String, - rnd: String, - signature: String, - } - - impl SigGenTestCase { - fn new() -> Self { - Self { - vs_id: 0, - algorithm: String::new(), - mode: String::new(), - revision: String::new(), - is_sample: false, - tg_id: 0, - test_type: String::new(), - parameter_set: String::new(), - deterministic: false, - tc_id: 0, - sk: String::new(), - message: String::new(), - rnd: String::new(), - signature: String::new(), - } - } - - fn is_full(&self) -> bool { - !self.algorithm.is_empty() - } - - fn parse(data: String) -> Vec { - let mut test_cases = Vec::::new(); - let mut test_case = SigGenTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "vsId" => test_case.vs_id = value.parse().unwrap(), - "algorithm" => test_case.algorithm = value.to_string(), - "mode" => test_case.mode = value.to_string(), - "revision" => test_case.revision = value.to_string(), - "isSample" => test_case.is_sample = value.parse().unwrap(), - "tgId" => test_case.tg_id = value.parse().unwrap(), - "testType" => test_case.test_type = value.to_string(), - "parameterSet" => test_case.parameter_set = value.to_string(), - "deterministic" => test_case.deterministic = value.parse().unwrap(), - "tcId" => test_case.tc_id = value.parse().unwrap(), - "sk" => test_case.sk = value.to_string(), - "message" => test_case.message = value.to_string(), - "rnd" => test_case.rnd = value.to_string(), - "signature" => test_case.signature = value.to_string(), - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self) { - assert_eq!(self.mode, "sigGen"); - - let rnd = if self.deterministic { - [0u8; 32] - } else { - hex::decode(&self.rnd).unwrap().as_slice().try_into().unwrap() - }; - - match self.parameter_set.as_str() { - "ML-DSA-44" => { - let sk = - MLDSA44PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); - - // Note: The code exposes a sign_mu_deterministic(), but not sign_deterministic() - // so mu needs to be computed manually - // let mu = MLDSA44::compute_mu_from_tr( - // &hex::decode(&self.message).unwrap(), - // None, - // sk.tr(), - // ).unwrap(); - let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); - mb.do_update(&hex::decode(&self.message).unwrap()); - let mu = mb.do_final(); - - let sig = MLDSA44::sign_mu_deterministic(&sk, None, &mu, rnd).unwrap(); - assert_eq!( - &sig, - &*hex::decode(&self.signature).unwrap(), - "ML-DSA-sigGen params: {}, vsId: {}, tgId: {}, tcId: {}", - self.parameter_set, - self.vs_id, - self.tg_id, - self.tc_id - ); - } - "ML-DSA-65" => { - let sk = - MLDSA65PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); - - // Note: The code exposes a sign_mu_deterministic(), but not sign_deterministic() - // so mu needs to be computed manually - // let mu = MLDSA65::compute_mu_from_tr( - // &hex::decode(&self.message).unwrap(), - // None, - // sk.tr(), - // ).unwrap(); - let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); - mb.do_update(&hex::decode(&self.message).unwrap()); - let mu = mb.do_final(); - - let sig = MLDSA65::sign_mu_deterministic(&sk, None, &mu, rnd).unwrap(); - assert_eq!(&sig, &*hex::decode(&self.signature).unwrap()); - } - "ML-DSA-87" => { - let sk = - MLDSA87PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); - - // Note: The code exposes a sign_mu_deterministic(), but not sign_deterministic() - // so mu needs to be computed manually - // let mu = MLDSA87::compute_mu_from_tr( - // &hex::decode(&self.message).unwrap(), - // None, - // sk.tr(), - // ).unwrap(); - let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); - mb.do_update(&hex::decode(&self.message).unwrap()); - let mu = mb.do_final(); - - let sig = MLDSA87::sign_mu_deterministic(&sk, None, &mu, rnd).unwrap(); - assert_eq!(&sig, &*hex::decode(&self.signature).unwrap()); - } - val => panic!("Invalid parameter set: {}", val), - } - } - } - - // DISABLED: this is not an implementation bug. - // Possibly because the bc-test-data was written against Round 3 Dilithium and not ML-DSA. - // todo -- debug - // #[test] - #[allow(unused)] - #[allow(non_snake_case)] - fn ML_DSA_sigVer() { - let contents = match get_test_data("ML-DSA-sigVer.txt") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = SigVerTestCase::parse(contents); - - for test_case in test_cases { - test_case.run(); - } - } - - #[derive(Clone)] - struct SigVerTestCase { - vs_id: u32, - algorithm: String, - mode: String, - revision: String, - is_sample: bool, - tg_id: u32, - test_type: String, - parameter_set: String, - pk: String, - tc_id: u32, - message: String, - signature: String, - test_passed: bool, - } - - impl SigVerTestCase { - fn new() -> Self { - Self { - vs_id: 0, - algorithm: String::new(), - mode: String::new(), - revision: String::new(), - is_sample: false, - tg_id: 0, - test_type: String::new(), - parameter_set: String::new(), - tc_id: 0, - pk: String::new(), - message: String::new(), - signature: String::new(), - test_passed: false, - } - } - - fn is_full(&self) -> bool { - !self.algorithm.is_empty() - } - - fn parse(data: String) -> Vec { - let mut test_cases = Vec::::new(); - let mut test_case = SigVerTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "vsId" => test_case.vs_id = value.parse().unwrap(), - "algorithm" => test_case.algorithm = value.to_string(), - "mode" => test_case.mode = value.to_string(), - "revision" => test_case.revision = value.to_string(), - "isSample" => test_case.is_sample = value.parse().unwrap(), - "tgId" => test_case.tg_id = value.parse().unwrap(), - "testType" => test_case.test_type = value.to_string(), - "parameterSet" => test_case.parameter_set = value.to_string(), - "pk" => test_case.pk = value.to_string(), - "tcId" => test_case.tc_id = value.parse().unwrap(), - "message" => test_case.message = value.to_string(), - "signature" => test_case.signature = value.to_string(), - "testPassed" => test_case.test_passed = value.parse().unwrap(), - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self) { - assert_eq!(self.mode, "sigVer"); - - match self.parameter_set.as_str() { - "ML-DSA-44" => { - let pk = MLDSA44PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); - - // No ctx because the bc-test-data tests were written against an earlier version of the spec - // that didn't have it. - match MLDSA44::verify( - &pk, - &hex::decode(&self.message).unwrap(), - None, - &hex::decode(&self.signature).unwrap(), - ) { - Ok(()) => { - if !self.test_passed { - panic!("Verification succeeded when it shouldn't have!") - } - } - Err(SignatureError::SignatureVerificationFailed) => { - if self.test_passed { - panic!( - "Verification failed when it shouldn't have! vsId: {}, tgId: {}, tcId: {}", - self.vs_id, self.tg_id, self.tc_id - ) - } - } - _ => panic!("An unexpected error occurred"), - } - } - "ML-DSA-65" => { - let pk = MLDSA65PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); - - match MLDSA65::verify( - &pk, - &hex::decode(&self.message).unwrap(), - None, - &hex::decode(&self.signature).unwrap(), - ) { - Ok(()) => { - if self.test_passed { /* good */ - } else { - panic!("Verification succeeded when it shouldn't have!") - } - } - Err(SignatureError::SignatureVerificationFailed) => { - if !self.test_passed { - } else { - panic!("Verification failed when it should have!") - } - } - _ => panic!("An unexpected error occurred"), - } - } - "ML-DSA-87" => { - let pk = MLDSA87PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); - - match MLDSA87::verify( - &pk, - &hex::decode(&self.message).unwrap(), - None, - &hex::decode(&self.signature).unwrap(), - ) { - Ok(()) => { - if self.test_passed { /* good */ - } else { - panic!("Verification succeeded when it shouldn't have!") - } - } - Err(SignatureError::SignatureVerificationFailed) => { - if !self.test_passed { - } else { - panic!("Verification failed when it should have!") - } - } - _ => panic!("An unexpected error occurred"), - } - } - val => panic!("Invalid parameter set: {}", val), - } - } - } - - // DISABLED: root cause not yet established. - // These .rsp vectors are modern FIPS 204 (they carry a `context` tag and include - // HashML-DSA/SHA-512 cases), and this test needs to use the real compute_mu_from_tr with ctx, - // todo -- debug - // #[test] - #[allow(unused)] - #[allow(non_snake_case)] - fn ML_DSA_rsp() { - // MLDsa44 - let contents = match get_test_data("mldsa44.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MLDsa44"); - } - - // MLDsa65 - let contents = match get_test_data("mldsa65.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MLDsa65"); - } - - // MLDsa87 - let contents = match get_test_data("mldsa87.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MLDsa87"); - } - - // MLDsa44sha512 - let contents = match get_test_data("mldsa44sha512.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MLDsa44"); - } - - // MLDsa65sha512 - let contents = match get_test_data("mldsa65sha512.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MlDsa65"); - } - - // MLDsa87sha512 - let contents = match get_test_data("mldsa87sha512.rsp") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = MldsaRspTestCase::::parse(contents); - for test_case in test_cases { - test_case.run("MlDsa87"); - } - } - - #[derive(Clone)] - struct MldsaRspTestCase { - count: u32, - seed: String, - mlen: u32, - msg: String, - pk: String, - sk: String, - smlen: u32, - sm: String, - message_hash: String, - message_prime: String, - context: String, - } - - impl MldsaRspTestCase { - fn new() -> Self { - Self { - count: 0, - seed: String::new(), - mlen: 0, - msg: String::new(), - pk: String::new(), - sk: String::new(), - smlen: 0, - sm: String::new(), - message_hash: String::new(), - message_prime: String::new(), - context: String::new(), - } - } - - fn is_full(&self) -> bool { - !self.seed.is_empty() - } - - fn parse(data: String) -> Vec> { - let mut test_cases = Vec::new(); - let mut test_case = MldsaRspTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "count" => test_case.count = value.parse().unwrap(), - "seed" => test_case.seed = value.to_string(), - "mlen" => test_case.mlen = value.parse().unwrap(), - "msg" => test_case.msg = value.to_string(), - "pk" => test_case.pk = value.to_string(), - "sk" => test_case.sk = value.to_string(), - "smlen" => test_case.smlen = value.parse().unwrap(), - "sm" => test_case.sm = value.to_string(), - "message_hash" => test_case.message_hash = value.to_string(), - "message_prime" => test_case.message_prime = value.to_string(), - "context" => { - test_case.context = value.to_string(); - if test_case.context == "zero_length" || test_case.context == "none" { - test_case.context = String::new(); - } - } - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self, parameter_set: &str) { - match parameter_set { - "MLDsa44" => { - let mut seed = KeyMaterial256::from_bytes_as_type( - &hex::decode(&self.seed).unwrap(), - KeyType::Seed, - ) - .unwrap(); - // For the purposes of the test cases, accept an all-zero seed - do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed)?; - seed.set_security_strength(SecurityStrength::_256bit) - }) - .unwrap(); - - let (pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA44_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA44_SK_LEN] = - hex::decode(&self.sk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - - if IS_HASH_MLDSA { - // It only tests SHA512 - let ph: [u8; 64] = SHA512::new() - .hash(&hex::decode(&self.msg).unwrap()) - .as_slice() - .try_into() - .unwrap(); - assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); - - let sig = HashMLDSA44_with_SHA512::sign_ph_deterministic( - &sk, - None, - Some(&*hex::decode(&self.context).unwrap()), - &ph, - [0u8; 32], - ) - .unwrap(); - assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); - - HashMLDSA44_with_SHA512::verify( - &pk, - &*hex::decode(&self.msg).unwrap(), - Some(&*hex::decode(&self.context).unwrap()), - &sig, - ) - .expect(&format!( - "paramSet: {}, is_hash: {}, count: {}", - parameter_set, IS_HASH_MLDSA, self.count - )); - } else { - // note: The code only exposes a sign_mu_deterministic(), but not sign_deterministic() - // so mu needs to be computed manually - let mu = MLDSA65::compute_mu_from_tr( - sk.tr(), - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - ) - .unwrap(); - - let sig = - MLDSA44::sign_mu_deterministic(&sk, None, &mu, [0u8; 32]).unwrap(); - assert_eq!( - sig, - &*hex::decode(&self.sm).unwrap(), - "paramSet: {}, count: {}", - parameter_set, - self.count - ); - - MLDSA44::verify( - &pk, - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - &sig, - ) - .unwrap(); - } - } - "MlDsa65" | "MLDsa65" => { - let mut seed = KeyMaterial256::from_bytes_as_type( - &hex::decode(&self.seed).unwrap(), - KeyType::Seed, - ) - .unwrap(); - // for the purposes of the test cases, accept an all-zero seed - do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed)?; - seed.set_security_strength(SecurityStrength::_256bit) - }) - .unwrap(); - - let (pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA65_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA65_SK_LEN] = - hex::decode(&self.sk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - - if IS_HASH_MLDSA { - // it only tests SHA512 - let ph: [u8; 64] = SHA512::new() - .hash(&hex::decode(&self.msg).unwrap()) - .as_slice() - .try_into() - .unwrap(); - assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); - - let sig = HashMLDSA65_with_SHA512::sign_ph_deterministic( - &sk, - None, - Some(&*hex::decode(&self.context).unwrap()), - &ph, - [0u8; 32], - ) - .unwrap(); - assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); - - HashMLDSA65_with_SHA512::verify( - &pk, - &*hex::decode(&self.message_hash).unwrap(), - Some(&*hex::decode(&self.context).unwrap()), - &sig, - ) - .expect(&format!( - "paramSet: {}, isHash: {}, count: {}", - parameter_set, IS_HASH_MLDSA, self.count - )); - } else { - // note: The code only exposes a sign_mu_deterministic(), but not sign_deterministic() - // so mu needs to be computed manually - let mu = MLDSA65::compute_mu_from_tr( - sk.tr(), - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - ) - .unwrap(); - - let sig = - MLDSA65::sign_mu_deterministic(&sk, None, &mu, [0u8; 32]).unwrap(); - assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); - - MLDSA65::verify( - &pk, - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - &sig, - ) - .unwrap(); - } - } - "MLDsa87" => { - let mut seed = KeyMaterial256::from_bytes_as_type( - &hex::decode(&self.seed).unwrap(), - KeyType::Seed, - ) - .unwrap(); - // for the purposes of the test cases, accept an all-zero seed - do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed)?; - seed.set_security_strength(SecurityStrength::_256bit) - }) - .unwrap(); - - let (pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLDSA87_PK_LEN] = - hex::decode(&self.pk).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLDSA87_SK_LEN] = - hex::decode(&self.sk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - - if IS_HASH_MLDSA { - // it only tests SHA512 - let ph: [u8; 64] = SHA512::new() - .hash(&hex::decode(&self.msg).unwrap()) - .as_slice() - .try_into() - .unwrap(); - assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); - - let sig = HashMLDSA87_with_SHA512::sign_ph_deterministic( - &sk, - None, - Some(&*hex::decode(&self.context).unwrap()), - &ph, - [0u8; 32], - ) - .unwrap(); - assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); - - HashMLDSA87_with_SHA512::verify( - &pk, - &*hex::decode(&self.message_hash).unwrap(), - Some(&*hex::decode(&self.context).unwrap()), - &sig, - ) - .unwrap(); - } else { - // Note: The code exposes a sign_mu_deterministic(), but not sign_deterministic() - // so mu needs to be computed manually - let mu = MLDSA65::compute_mu_from_tr( - sk.tr(), - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - ) - .unwrap(); - - let sig = - MLDSA87::sign_mu_deterministic(&sk, None, &mu, [0u8; 32]).unwrap(); - assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); - - MLDSA87::verify( - &pk, - &hex::decode(&self.msg).unwrap(), - Some(&hex::decode(&self.context).unwrap()), - &sig, - ) - .unwrap(); - } - } - val => panic!("Invalid parameter set: {}", val), - } - } - } -} - -/// This builds a "busted" mu where the ctx is absent (not 0-length, but actually not there) -/// just for the sake of compatibility with the bc-test-data tests -pub struct BustedMuBuilder { - h: SHAKE256, -} - -impl BustedMuBuilder { - /// Algorithm 7 - /// 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀′, 64) - pub fn compute_mu(msg: &[u8], tr: &[u8; 64]) -> Result<[u8; 64], SignatureError> { - let mut mu_builder = Self::do_init(&tr)?; - mu_builder.do_update(msg); - let mu = mu_builder.do_final(); - - Ok(mu) - } - - /// This function requires the public key hash `tr`, which can be computed from the public key using [`MLDSAPublicKey::compute_tr`]. - pub fn do_init(tr: &[u8; 64] /*ctx: Option<&[u8]>*/) -> Result { - // let ctx = match ctx { - // Some(ctx) => ctx, - // None => &[] - // }; - - // Algorithm 2 - // 1: if |𝑐𝑡𝑥| > 255 then - // if ctx.len() > 255 { - // return Err(SignatureError::LengthError("ctx value is longer than 255 bytes")); - // } - - // Algorithm 7 - // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) - let mut mb = Self { h: SHAKE256::new() }; - mb.h.absorb(tr).expect("absorb before squeeze is infallible"); - - // 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); - - // now ready to absorb M - Ok(mb) - } - - /// 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"); - } - - /// Finalize and return the mu value. - pub fn do_final(mut self) -> [u8; 64] { - // Completion of - // Algorithm 7 - // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) - let mut mu = [0u8; 64]; - self.h.squeeze_out(&mut mu); - - mu - } -} diff --git a/crypto/mldsa/tests/hash_mldsa_tests.rs b/crypto/mldsa/tests/hash_mldsa_tests.rs index 1ff7081e..006b9017 100644 --- a/crypto/mldsa/tests/hash_mldsa_tests.rs +++ b/crypto/mldsa/tests/hash_mldsa_tests.rs @@ -2,8 +2,9 @@ mod hash_mldsa_tests { use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; + use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Hash, PHSignatureVerifier, PHSigner, SecurityStrength, SignatureVerifier, Signer, + Hash, PHSignatureVerifier, PHSigner, SignatureVerifier, Signer, }; use bouncycastle_core_test_framework::signature::TestFrameworkSignature; use bouncycastle_hex as hex; @@ -47,11 +48,11 @@ mod hash_mldsa_tests { let (_pk, sk) = HashMLDSA44_with_SHA256::keygen().unwrap(); // ctx with len 255 works - HashMLDSA44_with_SHA256::sign_init(&sk, Some(&[1u8; 255])).unwrap(); + HashMLDSA44_with_SHA256::do_sign_init(&sk, Some(&[1u8; 255])).unwrap(); // ctx with len 256 is too long let too_long_ctx = [1u8; 256]; - match HashMLDSA44_with_SHA256::sign_init(&sk, Some(&too_long_ctx)) { + match HashMLDSA44_with_SHA256::do_sign_init(&sk, Some(&too_long_ctx)) { Err(SignatureError::LengthError(_)) => { /* good */ } _ => panic!("Expected error for ctx too long"), } @@ -254,23 +255,23 @@ mod hash_mldsa_tests { // END expected values // test the streaming API from sk - let mut s = HashMLDSA44_with_SHA512::sign_init(&expected_sk, ctx).unwrap(); + let mut s = HashMLDSA44_with_SHA512::do_sign_init(&expected_sk, ctx).unwrap(); s.set_signer_rnd(rnd); - s.sign_update(msg); - let sig = s.sign_final().unwrap(); + s.do_sign_update(msg); + let sig = s.do_sign_final().unwrap(); assert_eq!(&sig, &expected_sig); // test the streaming API from seed let mut s = HashMLDSA44_with_SHA512::sign_init_from_seed(&seed, ctx).unwrap(); s.set_signer_rnd(rnd); - s.sign_update(msg); - let sig = s.sign_final().unwrap(); + s.do_sign_update(msg); + let sig = s.do_sign_final().unwrap(); assert_eq!(&sig, &expected_sig); // test the streaming verifier - let mut v = HashMLDSA44_with_SHA512::verify_init(&expected_pk, ctx).unwrap(); - v.verify_update(msg); - v.verify_final(&expected_sig).unwrap(); + let mut v = HashMLDSA44_with_SHA512::do_verify_init(&expected_pk, ctx).unwrap(); + v.do_verify_update(msg); + v.do_verify_final(&expected_sig).unwrap(); } #[test] diff --git a/crypto/mldsa/tests/mldsa_bc-test-data.rs b/crypto/mldsa/tests/mldsa_bc-test-data.rs new file mode 100644 index 00000000..90ae1da7 --- /dev/null +++ b/crypto/mldsa/tests/mldsa_bc-test-data.rs @@ -0,0 +1,935 @@ +//! Known-answer tests for ML-DSA-44/65/87 and HashML-DSA-44/65/87 against `ML-DSA-keyGen.txt`, +//! `ML-DSA-sigGen.txt`, `ML-DSA-sigVer.txt` and `mldsa{44,65,87}{,sha512}.rsp`. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data", under `pqc/crypto/mldsa/`. If it is not +//! present the tests print a warning and pass vacuously. + +#![allow(dead_code)] + +use bouncycastle_core::errors::SignatureError; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{ + Hash, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, XOF, XOFSqueezer, +}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_mldsa::{ + HashMLDSA44_with_SHA512, HashMLDSA65_with_SHA512, HashMLDSA87_with_SHA512, MLDSA44, + MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65, MLDSA65_PK_LEN, + MLDSA65_SK_LEN, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87, MLDSA87_PK_LEN, MLDSA87_SK_LEN, + MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait, MLDSATrait, +}; +use bouncycastle_sha2::SHA512; +use bouncycastle_sha3::SHAKE256; + +const TEST_DATA_DIR: &str = "pqc/crypto/mldsa"; + +#[test] +#[allow(non_snake_case)] +fn ML_DSA_keyGen() { + let Some(contents) = bc_test_data(TEST_DATA_DIR, "ML-DSA-keyGen.txt") else { return }; + + let test_cases = KeyGenTestCase::parse(contents); + + for test_case in test_cases { + test_case.run(); + } +} + +#[derive(Clone)] +struct KeyGenTestCase { + vs_id: u32, + algorithm: String, + mode: String, + revision: String, + is_sample: bool, + tg_id: u32, + test_type: String, + parameter_set: String, + tc_id: u32, + seed: String, + pk: String, + sk: String, +} + +impl KeyGenTestCase { + fn new() -> Self { + Self { + vs_id: 0, + algorithm: String::new(), + mode: String::new(), + revision: String::new(), + is_sample: false, + tg_id: 0, + test_type: String::new(), + parameter_set: String::new(), + tc_id: 0, + seed: String::new(), + pk: String::new(), + sk: String::new(), + } + } + + fn is_full(&self) -> bool { + !self.algorithm.is_empty() + } + + fn parse(data: String) -> Vec { + let mut test_cases = Vec::::new(); + let mut test_case = KeyGenTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "vsId" => test_case.vs_id = value.parse().unwrap(), + "algorithm" => test_case.algorithm = value.to_string(), + "mode" => test_case.mode = value.to_string(), + "revision" => test_case.revision = value.to_string(), + "isSample" => test_case.is_sample = value.parse().unwrap(), + "tgId" => test_case.tg_id = value.parse().unwrap(), + "testType" => test_case.test_type = value.to_string(), + "parameterSet" => test_case.parameter_set = value.to_string(), + "tcId" => test_case.tc_id = value.parse().unwrap(), + "seed" => test_case.seed = value.to_string(), + "pk" => test_case.pk = value.to_string(), + "sk" => test_case.sk = value.to_string(), + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + fn run(&self) { + assert_eq!(self.mode, "keyGen"); + + let mut seed = + KeyMaterial256::from_bytes_as_type(&hex::decode(&self.seed).unwrap(), KeyType::Seed) + .unwrap(); + // for the purposes of the test cases, accept an all-zero seed + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + + match self.parameter_set.as_str() { + "ML-DSA-44" => { + let (pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA44_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA44_SK_LEN] = + hex::decode(&self.sk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + } + "ML-DSA-65" => { + let (pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA65_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA65_SK_LEN] = + hex::decode(&self.sk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + } + "ML-DSA-87" => { + let (pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA87_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA87_SK_LEN] = + hex::decode(&self.sk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + } + val => panic!("Invalid parameter set: {}", val), + } + } +} + +#[test] +#[allow(non_snake_case)] +fn ML_DSA_sigGen() { + let Some(contents) = bc_test_data(TEST_DATA_DIR, "ML-DSA-sigGen.txt") else { return }; + + let test_cases = SigGenTestCase::parse(contents); + + let num_tests = test_cases.len(); + for test_case in test_cases { + test_case.run(); + } + + println!("SUCCESS! ML-DSA-sigGen test cases passed: {}!", num_tests); +} + +#[derive(Clone)] +struct SigGenTestCase { + vs_id: u32, + algorithm: String, + mode: String, + revision: String, + is_sample: bool, + tg_id: u32, + test_type: String, + parameter_set: String, + deterministic: bool, + tc_id: u32, + sk: String, + message: String, + rnd: String, + signature: String, +} + +impl SigGenTestCase { + fn new() -> Self { + Self { + vs_id: 0, + algorithm: String::new(), + mode: String::new(), + revision: String::new(), + is_sample: false, + tg_id: 0, + test_type: String::new(), + parameter_set: String::new(), + deterministic: false, + tc_id: 0, + sk: String::new(), + message: String::new(), + rnd: String::new(), + signature: String::new(), + } + } + + fn is_full(&self) -> bool { + !self.algorithm.is_empty() + } + + fn parse(data: String) -> Vec { + let mut test_cases = Vec::::new(); + let mut test_case = SigGenTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "vsId" => test_case.vs_id = value.parse().unwrap(), + "algorithm" => test_case.algorithm = value.to_string(), + "mode" => test_case.mode = value.to_string(), + "revision" => test_case.revision = value.to_string(), + "isSample" => test_case.is_sample = value.parse().unwrap(), + "tgId" => test_case.tg_id = value.parse().unwrap(), + "testType" => test_case.test_type = value.to_string(), + "parameterSet" => test_case.parameter_set = value.to_string(), + "deterministic" => test_case.deterministic = value.parse().unwrap(), + "tcId" => test_case.tc_id = value.parse().unwrap(), + "sk" => test_case.sk = value.to_string(), + "message" => test_case.message = value.to_string(), + "rnd" => test_case.rnd = value.to_string(), + "signature" => test_case.signature = value.to_string(), + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + fn run(&self) { + assert_eq!(self.mode, "sigGen"); + + let rnd = if self.deterministic { + [0u8; 32] + } else { + hex::decode(&self.rnd).unwrap().as_slice().try_into().unwrap() + }; + + match self.parameter_set.as_str() { + "ML-DSA-44" => { + let sk = MLDSA44PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); + + // Note: The code exposes a sign_mu_deterministic(), but not sign_deterministic() + // so mu needs to be computed manually + // let mu = MLDSA44::compute_mu_from_tr( + // &hex::decode(&self.message).unwrap(), + // None, + // sk.tr(), + // ).unwrap(); + let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); + mb.do_update(&hex::decode(&self.message).unwrap()); + let mu = mb.do_final(); + + let sig = MLDSA44::sign_mu_deterministic(&sk, None, &mu, rnd).unwrap(); + assert_eq!( + &sig, + &*hex::decode(&self.signature).unwrap(), + "ML-DSA-sigGen params: {}, vsId: {}, tgId: {}, tcId: {}", + self.parameter_set, + self.vs_id, + self.tg_id, + self.tc_id + ); + } + "ML-DSA-65" => { + let sk = MLDSA65PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); + + // Note: The code exposes a sign_mu_deterministic(), but not sign_deterministic() + // so mu needs to be computed manually + // let mu = MLDSA65::compute_mu_from_tr( + // &hex::decode(&self.message).unwrap(), + // None, + // sk.tr(), + // ).unwrap(); + let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); + mb.do_update(&hex::decode(&self.message).unwrap()); + let mu = mb.do_final(); + + let sig = MLDSA65::sign_mu_deterministic(&sk, None, &mu, rnd).unwrap(); + assert_eq!(&sig, &*hex::decode(&self.signature).unwrap()); + } + "ML-DSA-87" => { + let sk = MLDSA87PrivateKey::from_bytes(&hex::decode(&self.sk).unwrap()).unwrap(); + + // Note: The code exposes a sign_mu_deterministic(), but not sign_deterministic() + // so mu needs to be computed manually + // let mu = MLDSA87::compute_mu_from_tr( + // &hex::decode(&self.message).unwrap(), + // None, + // sk.tr(), + // ).unwrap(); + let mut mb = BustedMuBuilder::do_init(&sk.tr()).unwrap(); + mb.do_update(&hex::decode(&self.message).unwrap()); + let mu = mb.do_final(); + + let sig = MLDSA87::sign_mu_deterministic(&sk, None, &mu, rnd).unwrap(); + assert_eq!(&sig, &*hex::decode(&self.signature).unwrap()); + } + val => panic!("Invalid parameter set: {}", val), + } + } +} + +/// The sigVer vectors, like the sigGen ones, sign the message itself (ML-DSA's internal interface), +/// so each is checked against a mu built by `BustedMuBuilder` rather than through `verify`, which +/// would add the context prefix. +#[test] +#[allow(non_snake_case)] +fn ML_DSA_sigVer() { + let Some(contents) = bc_test_data(TEST_DATA_DIR, "ML-DSA-sigVer.txt") else { return }; + + let test_cases = SigVerTestCase::parse(contents); + + for test_case in test_cases { + test_case.run(); + } +} + +#[derive(Clone)] +struct SigVerTestCase { + vs_id: u32, + algorithm: String, + mode: String, + revision: String, + is_sample: bool, + tg_id: u32, + test_type: String, + parameter_set: String, + pk: String, + tc_id: u32, + message: String, + signature: String, + test_passed: bool, +} + +impl SigVerTestCase { + fn new() -> Self { + Self { + vs_id: 0, + algorithm: String::new(), + mode: String::new(), + revision: String::new(), + is_sample: false, + tg_id: 0, + test_type: String::new(), + parameter_set: String::new(), + tc_id: 0, + pk: String::new(), + message: String::new(), + signature: String::new(), + test_passed: false, + } + } + + fn is_full(&self) -> bool { + !self.algorithm.is_empty() + } + + fn parse(data: String) -> Vec { + let mut test_cases = Vec::::new(); + let mut test_case = SigVerTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "vsId" => test_case.vs_id = value.parse().unwrap(), + "algorithm" => test_case.algorithm = value.to_string(), + "mode" => test_case.mode = value.to_string(), + "revision" => test_case.revision = value.to_string(), + "isSample" => test_case.is_sample = value.parse().unwrap(), + "tgId" => test_case.tg_id = value.parse().unwrap(), + "testType" => test_case.test_type = value.to_string(), + "parameterSet" => test_case.parameter_set = value.to_string(), + "pk" => test_case.pk = value.to_string(), + "tcId" => test_case.tc_id = value.parse().unwrap(), + "message" => test_case.message = value.to_string(), + "signature" => test_case.signature = value.to_string(), + "testPassed" => test_case.test_passed = value.parse().unwrap(), + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + fn run(&self) { + assert_eq!(self.mode, "sigVer"); + + match self.parameter_set.as_str() { + "ML-DSA-44" => { + let pk = MLDSA44PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); + + let mu = BustedMuBuilder::compute_mu( + &hex::decode(&self.message).unwrap(), + &pk.compute_tr(), + ) + .unwrap(); + let sig = + hex::decode(&self.signature).unwrap().try_into().expect("signature length"); + match MLDSA44::verify_mu(&pk, None, &mu, &sig) { + Ok(()) => { + if !self.test_passed { + panic!("Verification succeeded when it shouldn't have!") + } + } + Err(SignatureError::SignatureVerificationFailed) => { + if self.test_passed { + panic!( + "Verification failed when it shouldn't have! vsId: {}, tgId: {}, tcId: {}", + self.vs_id, self.tg_id, self.tc_id + ) + } + } + _ => panic!("An unexpected error occurred"), + } + } + "ML-DSA-65" => { + let pk = MLDSA65PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); + + let mu = BustedMuBuilder::compute_mu( + &hex::decode(&self.message).unwrap(), + &pk.compute_tr(), + ) + .unwrap(); + let sig = + hex::decode(&self.signature).unwrap().try_into().expect("signature length"); + match MLDSA65::verify_mu(&pk, None, &mu, &sig) { + Ok(()) => { + if self.test_passed { /* good */ + } else { + panic!("Verification succeeded when it shouldn't have!") + } + } + Err(SignatureError::SignatureVerificationFailed) => { + if !self.test_passed { + } else { + panic!("Verification failed when it should have!") + } + } + _ => panic!("An unexpected error occurred"), + } + } + "ML-DSA-87" => { + let pk = MLDSA87PublicKey::from_bytes(&hex::decode(&self.pk).unwrap()).unwrap(); + + let mu = BustedMuBuilder::compute_mu( + &hex::decode(&self.message).unwrap(), + &pk.compute_tr(), + ) + .unwrap(); + let sig = + hex::decode(&self.signature).unwrap().try_into().expect("signature length"); + match MLDSA87::verify_mu(&pk, None, &mu, &sig) { + Ok(()) => { + if self.test_passed { /* good */ + } else { + panic!("Verification succeeded when it shouldn't have!") + } + } + Err(SignatureError::SignatureVerificationFailed) => { + if !self.test_passed { + } else { + panic!("Verification failed when it should have!") + } + } + _ => panic!("An unexpected error occurred"), + } + } + val => panic!("Invalid parameter set: {}", val), + } + } +} + +/// The .rsp vectors give each case a `context`: hex bytes, `zero_length` (an empty context, which +/// still gets the usual prefix), or `none` (no context at all: the internal interface, as for the +/// ACVP files above). +#[test] +#[allow(non_snake_case)] +fn ML_DSA_rsp() { + // MLDsa44 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa44.rsp") else { return }; + + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa44"); + } + + // MLDsa65 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa65.rsp") else { return }; + + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa65"); + } + + // MLDsa87 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa87.rsp") else { return }; + + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa87"); + } + + // MLDsa44sha512 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa44sha512.rsp") else { return }; + + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa44"); + } + + // MLDsa65sha512 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa65sha512.rsp") else { return }; + + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa65"); + } + + // MLDsa87sha512 + let Some(contents) = bc_test_data(TEST_DATA_DIR, "mldsa87sha512.rsp") else { return }; + + let test_cases = MldsaRspTestCase::::parse(contents); + for test_case in test_cases { + test_case.run("MLDsa87"); + } +} + +#[derive(Clone)] +struct MldsaRspTestCase { + count: u32, + seed: String, + mlen: u32, + msg: String, + pk: String, + sk: String, + smlen: u32, + sm: String, + message_hash: String, + message_prime: String, + context: String, + /// `context = none`: no context at all, so mu omits the context prefix entirely. + no_context: bool, +} + +impl MldsaRspTestCase { + fn new() -> Self { + Self { + count: 0, + seed: String::new(), + mlen: 0, + msg: String::new(), + pk: String::new(), + sk: String::new(), + smlen: 0, + sm: String::new(), + message_hash: String::new(), + message_prime: String::new(), + context: String::new(), + no_context: false, + } + } + + fn is_full(&self) -> bool { + !self.seed.is_empty() + } + + fn parse(data: String) -> Vec> { + let mut test_cases = Vec::new(); + let mut test_case = MldsaRspTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "count" => test_case.count = value.parse().unwrap(), + "seed" => test_case.seed = value.to_string(), + "mlen" => test_case.mlen = value.parse().unwrap(), + "msg" => test_case.msg = value.to_string(), + "pk" => test_case.pk = value.to_string(), + "sk" => test_case.sk = value.to_string(), + "smlen" => test_case.smlen = value.parse().unwrap(), + "sm" => test_case.sm = value.to_string(), + "message_hash" => test_case.message_hash = value.to_string(), + "message_prime" => test_case.message_prime = value.to_string(), + "context" => { + // Set on every record: `test_case` is reused from one record to the next. + test_case.no_context = value == "none"; + test_case.context = match value { + "none" | "zero_length" => String::new(), + hex => hex.to_string(), + }; + } + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + /// mu for a plain ML-DSA case: `BustedMuBuilder` for `context = none`, otherwise the usual + /// context-prefixed mu. + fn mu(&self, tr: &[u8; 64]) -> [u8; 64] { + let msg = hex::decode(&self.msg).unwrap(); + if self.no_context { + BustedMuBuilder::compute_mu(&msg, tr).unwrap() + } else { + MLDSA65::compute_mu_from_tr(tr, &msg, Some(&hex::decode(&self.context).unwrap())) + .unwrap() + } + } + + fn run(&self, parameter_set: &str) { + match parameter_set { + "MLDsa44" => { + let mut seed = KeyMaterial256::from_bytes_as_type( + &hex::decode(&self.seed).unwrap(), + KeyType::Seed, + ) + .unwrap(); + // For the purposes of the test cases, accept an all-zero seed + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + + let (pk, sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA44_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA44_SK_LEN] = + hex::decode(&self.sk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + + if IS_HASH_MLDSA { + // It only tests SHA512 + let ph: [u8; 64] = SHA512::new() + .hash(&hex::decode(&self.msg).unwrap()) + .as_slice() + .try_into() + .unwrap(); + assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); + + let sig = HashMLDSA44_with_SHA512::sign_ph_deterministic( + &sk, + None, + Some(&*hex::decode(&self.context).unwrap()), + &ph, + [0u8; 32], + ) + .unwrap(); + assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); + + HashMLDSA44_with_SHA512::verify( + &pk, + &*hex::decode(&self.msg).unwrap(), + Some(&*hex::decode(&self.context).unwrap()), + &sig, + ) + .expect(&format!( + "paramSet: {}, is_hash: {}, count: {}", + parameter_set, IS_HASH_MLDSA, self.count + )); + } else { + // note: The code only exposes a sign_mu_deterministic(), but not sign_deterministic() + // so mu needs to be computed manually + let mu = self.mu(sk.tr()); + + let sig = MLDSA44::sign_mu_deterministic(&sk, None, &mu, [0u8; 32]).unwrap(); + assert_eq!( + sig, + &*hex::decode(&self.sm).unwrap(), + "paramSet: {}, count: {}", + parameter_set, + self.count + ); + + if self.no_context { + MLDSA44::verify_mu(&pk, None, &mu, &sig) + } else { + MLDSA44::verify( + &pk, + &hex::decode(&self.msg).unwrap(), + Some(&hex::decode(&self.context).unwrap()), + &sig, + ) + } + .unwrap(); + } + } + "MLDsa65" => { + let mut seed = KeyMaterial256::from_bytes_as_type( + &hex::decode(&self.seed).unwrap(), + KeyType::Seed, + ) + .unwrap(); + // for the purposes of the test cases, accept an all-zero seed + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + + let (pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA65_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA65_SK_LEN] = + hex::decode(&self.sk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + + if IS_HASH_MLDSA { + // it only tests SHA512 + let ph: [u8; 64] = SHA512::new() + .hash(&hex::decode(&self.msg).unwrap()) + .as_slice() + .try_into() + .unwrap(); + assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); + + let sig = HashMLDSA65_with_SHA512::sign_ph_deterministic( + &sk, + None, + Some(&*hex::decode(&self.context).unwrap()), + &ph, + [0u8; 32], + ) + .unwrap(); + assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); + + HashMLDSA65_with_SHA512::verify( + &pk, + &*hex::decode(&self.msg).unwrap(), + Some(&*hex::decode(&self.context).unwrap()), + &sig, + ) + .expect(&format!( + "paramSet: {}, isHash: {}, count: {}", + parameter_set, IS_HASH_MLDSA, self.count + )); + } else { + // note: The code only exposes a sign_mu_deterministic(), but not sign_deterministic() + // so mu needs to be computed manually + let mu = self.mu(sk.tr()); + + let sig = MLDSA65::sign_mu_deterministic(&sk, None, &mu, [0u8; 32]).unwrap(); + assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); + + if self.no_context { + MLDSA65::verify_mu(&pk, None, &mu, &sig) + } else { + MLDSA65::verify( + &pk, + &hex::decode(&self.msg).unwrap(), + Some(&hex::decode(&self.context).unwrap()), + &sig, + ) + } + .unwrap(); + } + } + "MLDsa87" => { + let mut seed = KeyMaterial256::from_bytes_as_type( + &hex::decode(&self.seed).unwrap(), + KeyType::Seed, + ) + .unwrap(); + // for the purposes of the test cases, accept an all-zero seed + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + + let (pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLDSA87_PK_LEN] = + hex::decode(&self.pk).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLDSA87_SK_LEN] = + hex::decode(&self.sk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + + if IS_HASH_MLDSA { + // it only tests SHA512 + let ph: [u8; 64] = SHA512::new() + .hash(&hex::decode(&self.msg).unwrap()) + .as_slice() + .try_into() + .unwrap(); + assert_eq!(ph, &*hex::decode(&self.message_hash).unwrap()); + + let sig = HashMLDSA87_with_SHA512::sign_ph_deterministic( + &sk, + None, + Some(&*hex::decode(&self.context).unwrap()), + &ph, + [0u8; 32], + ) + .unwrap(); + assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); + + HashMLDSA87_with_SHA512::verify( + &pk, + &*hex::decode(&self.msg).unwrap(), + Some(&*hex::decode(&self.context).unwrap()), + &sig, + ) + .unwrap(); + } else { + // Note: The code exposes a sign_mu_deterministic(), but not sign_deterministic() + // so mu needs to be computed manually + let mu = self.mu(sk.tr()); + + let sig = MLDSA87::sign_mu_deterministic(&sk, None, &mu, [0u8; 32]).unwrap(); + assert_eq!(sig, &*hex::decode(&self.sm).unwrap()); + + if self.no_context { + MLDSA87::verify_mu(&pk, None, &mu, &sig) + } else { + MLDSA87::verify( + &pk, + &hex::decode(&self.msg).unwrap(), + Some(&hex::decode(&self.context).unwrap()), + &sig, + ) + } + .unwrap(); + } + } + val => panic!("Invalid parameter set: {}", val), + } + } +} + +/// This builds a "busted" mu where the ctx is absent (not 0-length, but actually not there) +/// just for the sake of compatibility with the bc-test-data tests +pub struct BustedMuBuilder { + h: SHAKE256, +} + +impl BustedMuBuilder { + /// Algorithm 7 + /// 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀′, 64) + pub fn compute_mu(msg: &[u8], tr: &[u8; 64]) -> Result<[u8; 64], SignatureError> { + let mut mu_builder = Self::do_init(&tr)?; + mu_builder.do_update(msg); + let mu = mu_builder.do_final(); + + Ok(mu) + } + + /// This function requires the public key hash `tr`, which can be computed from the public key using [`MLDSAPublicKey::compute_tr`]. + pub fn do_init(tr: &[u8; 64] /*ctx: Option<&[u8]>*/) -> Result { + // let ctx = match ctx { + // Some(ctx) => ctx, + // None => &[] + // }; + + // Algorithm 2 + // 1: if |𝑐𝑡𝑥| > 255 then + // if ctx.len() > 255 { + // return Err(SignatureError::LengthError("ctx value is longer than 255 bytes")); + // } + + // Algorithm 7 + // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀', 64) + let mut mb = Self { h: SHAKE256::new() }; + mb.h.do_update(tr); + + // Algorithm 2 + // 10: 𝑀′ ← BytesToBits(IntegerToBytes(0, 1) ∥ IntegerToBytes(|𝑐𝑡𝑥|, 1) ∥ 𝑐𝑡𝑥) ∥ 𝑀 + // all done together + // 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) + } + + /// Stream a chunk of the message. + pub fn do_update(&mut self, msg_chunk: &[u8]) { + self.h.do_update(msg_chunk); + } + + /// Finalize and return the mu value. + pub fn do_final(self) -> [u8; 64] { + // Completion of + // Algorithm 7 + // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) + let mut mu = [0u8; 64]; + self.h.into_squeezer().do_output_out(&mut mu); + + mu + } +} diff --git a/crypto/mldsa/tests/mldsa_key_tests.rs b/crypto/mldsa/tests/mldsa_key_tests.rs index 0fc5fe1d..0e8044f0 100644 --- a/crypto/mldsa/tests/mldsa_key_tests.rs +++ b/crypto/mldsa/tests/mldsa_key_tests.rs @@ -1,10 +1,10 @@ #[cfg(test)] mod mldsa_key_tests { use bouncycastle_core::errors::SignatureError; - use bouncycastle_core::key_material::{ - KeyMaterial256, KeyMaterialTrait, KeyType, do_hazardous_operations, - }; - use bouncycastle_core::traits::{SecurityStrength, SignaturePrivateKey, SignaturePublicKey}; + use bouncycastle_core::hazmat::do_hazardous_operations; + use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey}; use bouncycastle_core_test_framework::signature::TestFrameworkSignatureKeys; use bouncycastle_hex as hex; use bouncycastle_mldsa::{ diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index aebd3a06..d9350bc9 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -3,12 +3,11 @@ mod mldsa_tests { use crate::{MLDSA44_KAT1, MLDSA65_KAT1, MLDSA87_KAT1}; use bouncycastle_core::errors::{RNGError, SignatureError, SuspendableError}; - use bouncycastle_core::key_material::{ - KeyMaterial256, KeyMaterialTrait, KeyType, do_hazardous_operations, - }; + use bouncycastle_core::hazmat::do_hazardous_operations; + use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; + use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - RNG, SecurityStrength, 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; @@ -377,21 +376,22 @@ mod mldsa_tests { .unwrap(); // test the streaming API on the same value - let mut s = MLDSA44::sign_init(&sk, Some(&hex::decode(MLDSA44_KAT1.ctx).unwrap())).unwrap(); + let mut s = + MLDSA44::do_sign_init(&sk, Some(&hex::decode(MLDSA44_KAT1.ctx).unwrap())).unwrap(); s.set_signer_rnd(rnd); - s.sign_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); - let sig = s.sign_final().unwrap(); + s.do_sign_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); + let sig = s.do_sign_final().unwrap(); let decoded_sig: [u8; MLDSA44_SIG_LEN] = hex::decode(MLDSA44_KAT1.signature).unwrap().try_into().unwrap(); assert_eq!(&sig, &decoded_sig); // Then with the message broken into chunks - let mut s = MLDSA44::sign_init(&sk, Some(b"streaming API chunked")).unwrap(); + let mut s = MLDSA44::do_sign_init(&sk, Some(b"streaming API chunked")).unwrap(); s.set_signer_rnd(rnd); for msg_chunk in DUMMY_SEED.chunks(100) { - s.sign_update(msg_chunk); + s.do_sign_update(msg_chunk); } - let sig_val = s.sign_final().unwrap(); + let sig_val = s.do_sign_final().unwrap(); MLDSA44::verify(&sk.derive_pk(), DUMMY_SEED, Some(b"streaming API chunked"), &sig_val) .unwrap(); @@ -424,10 +424,11 @@ mod mldsa_tests { .unwrap(); // test the streaming API on the same value - let mut s = MLDSA65::sign_init(&sk, Some(&hex::decode(MLDSA65_KAT1.ctx).unwrap())).unwrap(); + let mut s = + MLDSA65::do_sign_init(&sk, Some(&hex::decode(MLDSA65_KAT1.ctx).unwrap())).unwrap(); s.set_signer_rnd(rnd); - s.sign_update(&hex::decode(MLDSA65_KAT1.message).unwrap()); - let sig = s.sign_final().unwrap(); + s.do_sign_update(&hex::decode(MLDSA65_KAT1.message).unwrap()); + let sig = s.do_sign_final().unwrap(); let decoded_sig: [u8; MLDSA65_SIG_LEN] = hex::decode(MLDSA65_KAT1.signature).unwrap().try_into().unwrap(); assert_eq!(&sig, &decoded_sig); @@ -461,10 +462,11 @@ mod mldsa_tests { .unwrap(); // Test the streaming API on the same value - let mut s = MLDSA87::sign_init(&sk, Some(&hex::decode(MLDSA87_KAT1.ctx).unwrap())).unwrap(); + let mut s = + MLDSA87::do_sign_init(&sk, Some(&hex::decode(MLDSA87_KAT1.ctx).unwrap())).unwrap(); s.set_signer_rnd(rnd); - s.sign_update(&hex::decode(MLDSA87_KAT1.message).unwrap()); - let sig = s.sign_final().unwrap(); + s.do_sign_update(&hex::decode(MLDSA87_KAT1.message).unwrap()); + let sig = s.do_sign_final().unwrap(); let decoded_sig: [u8; MLDSA87_SIG_LEN] = hex::decode(MLDSA87_KAT1.signature).unwrap().try_into().unwrap(); assert_eq!(&sig, &decoded_sig); @@ -705,16 +707,16 @@ mod mldsa_tests { MLDSA44::sign_init_from_seed(&seed, Some(&hex::decode(MLDSA44_KAT1.ctx).unwrap())) .unwrap(); s.set_signer_rnd(rnd); - s.sign_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); - let sig = s.sign_final().unwrap(); + s.do_sign_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); + let sig = s.do_sign_final().unwrap(); assert_eq!(&sig, &expected_sig); // Test also the streaming verifier let mut v = - MLDSA44::verify_init(&pk, Some(&hex::decode(MLDSA44_KAT1.ctx).unwrap())).unwrap(); - v.verify_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); - v.verify_final(&expected_sig).unwrap(); + MLDSA44::do_verify_init(&pk, Some(&hex::decode(MLDSA44_KAT1.ctx).unwrap())).unwrap(); + v.do_verify_update(&hex::decode(MLDSA44_KAT1.message).unwrap()); + v.do_verify_final(&expected_sig).unwrap(); } #[test] @@ -726,11 +728,11 @@ mod mldsa_tests { let (_pk, sk) = MLDSA44::keygen().unwrap(); // ctx with len 255 works - MLDSA44::sign_init(&sk, Some(&[1u8; 255])).unwrap(); + MLDSA44::do_sign_init(&sk, Some(&[1u8; 255])).unwrap(); // ctx with len 256 is too long let too_long_ctx = [1u8; 256]; - match MLDSA44::sign_init(&sk, Some(&too_long_ctx)) { + match MLDSA44::do_sign_init(&sk, Some(&too_long_ctx)) { Err(SignatureError::LengthError(_)) => { /* good */ } _ => panic!("Expected error for ctx too long"), } @@ -1053,7 +1055,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 +1062,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/tests/wycheproof.rs b/crypto/mldsa/tests/mldsa_wycheproof.rs similarity index 81% rename from crypto/mldsa/tests/wycheproof.rs rename to crypto/mldsa/tests/mldsa_wycheproof.rs index 9e313271..8ab2695b 100644 --- a/crypto/mldsa/tests/wycheproof.rs +++ b/crypto/mldsa/tests/mldsa_wycheproof.rs @@ -18,208 +18,133 @@ #![allow(dead_code)] use bouncycastle_core::errors::SignatureError; -use bouncycastle_core::key_material::{ - KeyMaterial256, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; -use bouncycastle_core::traits::{ - SecurityStrength, SignaturePrivateKey, SignaturePublicKey, SignatureVerifier, -}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey, SignatureVerifier}; +use bouncycastle_core_test_framework::test_data_loaders::{Value, wycheproof_json}; use bouncycastle_hex as hex; use bouncycastle_mldsa::{ MLDSA44, MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87, MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPublicKeyTrait, MLDSATrait, MuBuilder, }; -#[cfg(test)] -mod wycheproof { - use crate::{ - MLDSASignNoSeedTestCase, MLDSASignSeedTestCase, MLDSAVerifyTestCase, ParameterSet, - }; - use std::fs; - use std::path::Path; - use std::sync::Once; - - const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; - const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; - - 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!("wycheproof found at: {:?}", TEST_DATA_PATH_RELATIVE), - 2 => println!("wycheproof found at: {:?}", TEST_DATA_PATH), - _ => println!("WARNING: wycheproof directory not found; tests will be skipped"), - }); - - if !found == 3 { - return Err(()); - } - - 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) - } - - #[test] - fn mldsa_44_sign_noseed_test() { - let contents = match get_test_data("mldsa_44_sign_noseed_test.json") { - Ok(contents) => contents, - Err(_) => return, - }; - let test_cases = MLDSASignNoSeedTestCase::parse(contents, ParameterSet::Mldsa44); +#[test] +fn mldsa_44_sign_noseed_test() { + let Some(json) = wycheproof_json("mldsa_44_sign_noseed_test.json") else { return }; + let test_cases = MLDSASignNoSeedTestCase::parse(json, ParameterSet::Mldsa44); - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa44(); - } - - println!("mldsa_44_sign_noseed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa44(); } - #[test] - fn mldsa_44_sign_seed_test() { - let contents = match get_test_data("mldsa_44_sign_seed_test.json") { - Ok(contents) => contents, - Err(_) => return, - }; - let test_cases = MLDSASignSeedTestCase::parse(contents, ParameterSet::Mldsa44); + println!("mldsa_44_sign_noseed_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa44(); - } +#[test] +fn mldsa_44_sign_seed_test() { + let Some(json) = wycheproof_json("mldsa_44_sign_seed_test.json") else { return }; + let test_cases = MLDSASignSeedTestCase::parse(json, ParameterSet::Mldsa44); - println!("mldsa_44_sign_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa44(); } - #[test] - fn mldsa_44_verify_test() { - let contents = match get_test_data("mldsa_44_verify_test.json") { - Ok(contents) => contents, - Err(_) => return, - }; - let test_cases = MLDSAVerifyTestCase::parse(contents, ParameterSet::Mldsa44); + println!("mldsa_44_sign_seed_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa44(); - } +#[test] +fn mldsa_44_verify_test() { + let Some(json) = wycheproof_json("mldsa_44_verify_test.json") else { return }; + let test_cases = MLDSAVerifyTestCase::parse(json, ParameterSet::Mldsa44); - println!("mldsa_44_verify_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa44(); } - #[test] - fn mldsa_65_sign_noseed_test() { - let contents = match get_test_data("mldsa_65_sign_noseed_test.json") { - Ok(contents) => contents, - Err(_) => return, - }; - let test_cases = MLDSASignNoSeedTestCase::parse(contents, ParameterSet::Mldsa65); + println!("mldsa_44_verify_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa65(); - } +#[test] +fn mldsa_65_sign_noseed_test() { + let Some(json) = wycheproof_json("mldsa_65_sign_noseed_test.json") else { return }; + let test_cases = MLDSASignNoSeedTestCase::parse(json, ParameterSet::Mldsa65); - println!("mldsa_65_sign_noseed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa65(); } - #[test] - fn mldsa_65_sign_seed_test() { - let contents = match get_test_data("mldsa_65_sign_seed_test.json") { - Ok(contents) => contents, - Err(_) => return, - }; - let test_cases = MLDSASignSeedTestCase::parse(contents, ParameterSet::Mldsa65); + println!("mldsa_65_sign_noseed_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa65(); - } +#[test] +fn mldsa_65_sign_seed_test() { + let Some(json) = wycheproof_json("mldsa_65_sign_seed_test.json") else { return }; + let test_cases = MLDSASignSeedTestCase::parse(json, ParameterSet::Mldsa65); - println!("mldsa_65_sign_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa65(); } - #[test] - fn mldsa_65_verify_test() { - let contents = match get_test_data("mldsa_65_verify_test.json") { - Ok(contents) => contents, - Err(_) => return, - }; - let test_cases = MLDSAVerifyTestCase::parse(contents, ParameterSet::Mldsa65); + println!("mldsa_65_sign_seed_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa65(); - } +#[test] +fn mldsa_65_verify_test() { + let Some(json) = wycheproof_json("mldsa_65_verify_test.json") else { return }; + let test_cases = MLDSAVerifyTestCase::parse(json, ParameterSet::Mldsa65); - println!("mldsa_65_verify_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa65(); } - #[test] - fn mldsa_87_sign_noseed_test() { - let contents = match get_test_data("mldsa_87_sign_noseed_test.json") { - Ok(contents) => contents, - Err(_) => return, - }; + println!("mldsa_65_verify_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLDSASignNoSeedTestCase::parse(contents, ParameterSet::Mldsa87); +#[test] +fn mldsa_87_sign_noseed_test() { + let Some(json) = wycheproof_json("mldsa_87_sign_noseed_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa87(); - } + let test_cases = MLDSASignNoSeedTestCase::parse(json, ParameterSet::Mldsa87); - println!("mldsa_87_sign_noseed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa87(); } - #[test] - fn mldsa_87_sign_seed_test() { - let contents = match get_test_data("mldsa_87_sign_seed_test.json") { - Ok(contents) => contents, - Err(_) => return, - }; - let test_cases = MLDSASignSeedTestCase::parse(contents, ParameterSet::Mldsa87); + println!("mldsa_87_sign_noseed_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa87(); - } +#[test] +fn mldsa_87_sign_seed_test() { + let Some(json) = wycheproof_json("mldsa_87_sign_seed_test.json") else { return }; + let test_cases = MLDSASignSeedTestCase::parse(json, ParameterSet::Mldsa87); - println!("mldsa_87_sign_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa87(); } - #[test] - fn mldsa_87_verify_test() { - let contents = match get_test_data("mldsa_87_verify_test.json") { - Ok(contents) => contents, - Err(_) => return, - }; - let test_cases = MLDSAVerifyTestCase::parse(contents, ParameterSet::Mldsa87); + println!("mldsa_87_sign_seed_test: all {} test cases passed.", num_test_cases); +} - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mldsa87(); - } +#[test] +fn mldsa_87_verify_test() { + let Some(json) = wycheproof_json("mldsa_87_verify_test.json") else { return }; + let test_cases = MLDSAVerifyTestCase::parse(json, ParameterSet::Mldsa87); - println!("mldsa_87_verify_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mldsa87(); } + + println!("mldsa_87_verify_test: all {} test cases passed.", num_test_cases); } /* Structs for holding test data */ @@ -266,10 +191,7 @@ impl MLDSASignNoSeedTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); @@ -564,10 +486,7 @@ impl MLDSASignSeedTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); @@ -927,10 +846,7 @@ impl MLDSAVerifyTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); diff --git a/crypto/mlkem-lowmemory/Cargo.toml b/crypto/mlkem-lowmemory/Cargo.toml index 6edb19ad..f52dff4f 100644 --- a/crypto/mlkem-lowmemory/Cargo.toml +++ b/crypto/mlkem-lowmemory/Cargo.toml @@ -14,7 +14,6 @@ bouncycastle-core-test-framework.workspace = true bouncycastle-hex.workspace = true bouncycastle-rng.workspace = true criterion.workspace = true -serde_json = "1.0" [[bench]] name = "mlkem_benches" diff --git a/crypto/mlkem-lowmemory/benches/mlkem_benches.rs b/crypto/mlkem-lowmemory/benches/mlkem_benches.rs index 8ea83baf..42252eda 100644 --- a/crypto/mlkem-lowmemory/benches/mlkem_benches.rs +++ b/crypto/mlkem-lowmemory/benches/mlkem_benches.rs @@ -1,6 +1,7 @@ use bouncycastle_core::key_material::{KeyMaterial512, KeyType}; use bouncycastle_core::traits::KEMDecapsulator; use bouncycastle_hex as hex; +use bouncycastle_mlkem_lowmemory::hazmat::EncapsWithRandomness; use bouncycastle_mlkem_lowmemory::{ MLKEM_RND_LEN, MLKEM512, MLKEM512_CT_LEN, MLKEM768, MLKEM768_CT_LEN, MLKEM1024, MLKEM1024_CT_LEN, MLKEMTrait, @@ -79,7 +80,7 @@ fn bench_mlkem_encaps(c: &mut Criterion) { group.bench_function("ML-KEM-512_lowmemory", |b| { b.iter(|| { for i in 0..NUM_ELEMS { - _ = black_box(MLKEM512::encaps_internal(&pk, nonces[i])); + _ = black_box(MLKEM512::encaps_with_randomness(&pk, nonces[i])); } }) }); @@ -92,7 +93,7 @@ fn bench_mlkem_encaps(c: &mut Criterion) { group.bench_function("ML-KEM-768_lowmemory", |b| { b.iter(|| { for i in 0..NUM_ELEMS { - _ = black_box(MLKEM768::encaps_internal(&pk, nonces[i])); + _ = black_box(MLKEM768::encaps_with_randomness(&pk, nonces[i])); } }) }); @@ -105,7 +106,7 @@ fn bench_mlkem_encaps(c: &mut Criterion) { group.bench_function("ML-KEM-1024_lowmemory", |b| { b.iter(|| { for i in 0..NUM_ELEMS { - _ = black_box(MLKEM1024::encaps_internal(&pk, nonces[i])); + _ = black_box(MLKEM1024::encaps_with_randomness(&pk, nonces[i])); } }) }); @@ -139,8 +140,8 @@ fn bench_mlkem_decaps(c: &mut Criterion) { let mut cts = [[0u8; MLKEM512_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM512::encaps_internal(&pk, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice(&MLKEM512::encaps_with_randomness(&pk, [i as u8; MLKEM_RND_LEN]).1); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); @@ -160,8 +161,8 @@ fn bench_mlkem_decaps(c: &mut Criterion) { let mut cts = [[0u8; MLKEM768_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM768::encaps_internal(&pk, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice(&MLKEM768::encaps_with_randomness(&pk, [i as u8; MLKEM_RND_LEN]).1); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); @@ -181,8 +182,8 @@ fn bench_mlkem_decaps(c: &mut Criterion) { let mut cts = [[0u8; MLKEM1024_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM1024::encaps_internal(&pk, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice(&MLKEM1024::encaps_with_randomness(&pk, [i as u8; MLKEM_RND_LEN]).1); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); 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/hazmat/encaps_with_randomness.rs b/crypto/mlkem-lowmemory/src/hazmat/encaps_with_randomness.rs new file mode 100644 index 00000000..95d90915 --- /dev/null +++ b/crypto/mlkem-lowmemory/src/hazmat/encaps_with_randomness.rs @@ -0,0 +1,60 @@ +//! [`EncapsWithRandomness`]: ML-KEM.Encaps_internal with the randomness supplied by the caller. + +use crate::mlkem::{MLKEM, MLKEM_RND_LEN, MLKEM_SS_LEN}; +use crate::mlkem_keys::{ + MLKEMPrivateKeyInternalTrait, MLKEMPrivateKeyTrait, MLKEMPublicKeyInternalTrait, + MLKEMPublicKeyTrait, +}; +use crate::params::MLKEMParams; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::key_material::KeyMaterial; +#[allow(unused_imports)] +use bouncycastle_core::traits::KEMEncapsulator; +// end of imports needed for docs + +/// FIPS 203 Algorithm 17, ML-KEM.Encaps_internal(ek, m), with `m` supplied by the caller. +/// +/// # 🚨 Security Considerations 🚨 +/// `m` is the encapsulation randomness, the message the underlying PKE encrypts. It must be 32 +/// bytes of fresh, uniformly random, secret data for every call: any deterministic KEM, like any +/// deterministic encryption, fails every indistinguishability notion (IND-CPA, IND-CCA2), and a +/// predictable `m` hands an attacker the shared secret. [`KEMEncapsulator::encaps`] draws `m` from +/// the DRBG and is the function to use; this exists for known-answer tests and for environments +/// that must supply their own randomness. +/// +/// The shared secret comes back as raw bytes rather than wrapped in a [`KeyMaterial`] with its +/// type and security strength set; handling it is up to the caller. +/// +/// A trait rather than an inherent method so that the operation is only reachable with this +/// module's path in scope; see [`bouncycastle_core::hazmat`]. +pub trait EncapsWithRandomness { + /// Encapsulates to `ek` using `m` as the randomness, returning the shared secret and the + /// ciphertext. + fn encaps_with_randomness( + ek: &PK, + m: [u8; MLKEM_RND_LEN], + ) -> ([u8; MLKEM_SS_LEN], [u8; CT_LEN]); +} + +impl< + P: MLKEMParams, + PK: MLKEMPublicKeyTrait + MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, + const PK_LEN: usize, + const SK_LEN: usize, + const FULL_SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +> EncapsWithRandomness + for MLKEM +{ + fn encaps_with_randomness( + ek: &PK, + m: [u8; MLKEM_RND_LEN], + ) -> ([u8; MLKEM_SS_LEN], [u8; CT_LEN]) { + Self::encaps_internal(ek, m) + } +} diff --git a/crypto/mlkem-lowmemory/src/hazmat/mod.rs b/crypto/mlkem-lowmemory/src/hazmat/mod.rs new file mode 100644 index 00000000..fbc3face --- /dev/null +++ b/crypto/mlkem-lowmemory/src/hazmat/mod.rs @@ -0,0 +1,10 @@ +//! Raw ML-KEM operations whose safe use is the caller's responsibility; see +//! [`bouncycastle_core::hazmat`] for what the path means and the supported uses. +//! +//! [`EncapsWithRandomness`] takes the encapsulation randomness from the caller; the +//! [`KEMEncapsulator`](bouncycastle_core::traits::KEMEncapsulator) methods draw it from the DRBG +//! and are the ones to use. + +mod encaps_with_randomness; + +pub use encaps_with_randomness::EncapsWithRandomness; diff --git a/crypto/mlkem-lowmemory/src/lib.rs b/crypto/mlkem-lowmemory/src/lib.rs index 7a15b31e..5e471b77 100644 --- a/crypto/mlkem-lowmemory/src/lib.rs +++ b/crypto/mlkem-lowmemory/src/lib.rs @@ -202,7 +202,7 @@ //! ``` //! And that's the basic usage! //! -//! # 🚨 Security 🚨 +//! # 🚨 Security Considerations 🚨 //! //! This crate intends to expose only APIs that are secure to use. //! There are, however, a few exceptions worth mentioning. @@ -210,7 +210,7 @@ //! If using a [`MLKEM::keygen_from_seed`], then it is your responsibility to ensure that the seed is //! cryptographically random and unpredictable at a security strength that matches the MLKEM parameter set. //! -//! Also, [`MLKEM::encaps_internal`] requires the encapsulation randomness to be provided, so the ciphertext +//! Also, [`hazmat::EncapsWithRandomness`] requires the encapsulation randomness to be provided, so the ciphertext //! will only be as strong as the randomness that you provide. //! //! A note about cryptographic side-channel attacks: considerable effort has been expended to attempt @@ -241,6 +241,7 @@ use bouncycastle_core::key_material::KeyMaterialTrait; mod aux_functions; +pub mod hazmat; mod low_memory_helpers; pub mod mlkem; mod mlkem_keys; diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index da61c593..810e7303 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -14,15 +14,15 @@ use crate::mlkem_keys::{MLKEMPublicKeyInternalTrait, MLKEMPublicKeyTrait}; use crate::params::{MLKEM512Params, MLKEM768Params, MLKEM1024Params, MLKEMParams}; use crate::polynomial::Polynomial; use bouncycastle_core::errors::{KEMError, RNGError}; -use bouncycastle_core::key_material::{ - KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, SecurityStrength, XOF, + Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; -use bouncycastle_utils::ct::{conditional_copy_bytes, ct_eq_bytes}; +use bouncycastle_utils::ct::{conditional_copy_bytes, ct_eq_bytes_mask}; use bouncycastle_utils::secret::Secret; use core::marker::PhantomData; /*** Constants ***/ @@ -285,23 +285,10 @@ impl< /// Output: shared secret key 𝐾 ∈ 𝔹32 . /// Output: ciphertext 𝑐 ∈ 𝔹32(𝑑𝑢𝑘+𝑑𝑣). /// - /// Unlike the more public function exposed by [`KEMEncapsulator::encaps`], this returns the shared secret as raw bytes - /// instead of wrapped in an appropriately-set [`KeyMaterialTrait`]. - /// Proper handling is up to the user's own judgement. - /// - /// Note: this is an internal function that allows the caller to specify the encapsulation - /// randomness (which is the message `m` to be encrypted by the underlying PKE scheme). - /// This function should not be used directly unless there is a good reason to do so. - /// [`KEMEncapsulator::encaps`] should be used in 99.9% of cases. - /// The reason this is exposed publicly is: - /// A) for unit testing that requires access to the deterministically reproducible function, and - /// B) for operational environments that wish to provide randomness from their own source instead - /// of the built-in RNG in bc-rust. - /// As a reminder, any deterministic KEM (or any encryption mechanism) fails to satisfy any security - /// notion involving indistinguishability (e.g. IND-CPA, IND-CCA2, etc.). - /// Failing to use this properly will result in catastrophic vulnerabilities. - /// Please don't do it. - pub fn encaps_internal(ek: &PK, m: [u8; 32]) -> ([u8; 32], [u8; CT_LEN]) { + /// Reachable from outside the crate only through + /// [`EncapsWithRandomness`](crate::hazmat::EncapsWithRandomness), which carries the security + /// notes on supplying `m`. + pub(crate) fn encaps_internal(ek: &PK, m: [u8; 32]) -> ([u8; 32], [u8; CT_LEN]) { // 1: (𝐾, 𝑟) ← G(𝑚‖H(ek)) // ▷ derive shared secret key 𝐾 and randomness 𝑟 let K: [u8; MLKEM_SS_LEN]; @@ -431,9 +418,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 @@ -447,7 +435,7 @@ impl< // 10: 𝐾′ ← 𝐾_bar // ▷ if ciphertexts do not match, “implicitly reject" let mut K_out = [0u8; MLKEM_SS_LEN]; - conditional_copy_bytes(&K_prime, &K_bar, &mut K_out, ct_eq_bytes(&c, &c_prime)); + conditional_copy_bytes(&K_prime, &K_bar, &mut K_out, ct_eq_bytes_mask(&c, &c_prime)); K_out } diff --git a/crypto/mlkem-lowmemory/src/mlkem_keys.rs b/crypto/mlkem-lowmemory/src/mlkem_keys.rs index c5f62e6e..da2e778f 100644 --- a/crypto/mlkem-lowmemory/src/mlkem_keys.rs +++ b/crypto/mlkem-lowmemory/src/mlkem_keys.rs @@ -9,10 +9,10 @@ use crate::mlkem::{MLKEM1024_FULL_SK_LEN, MLKEM1024_PK_LEN, MLKEM1024_SK_LEN}; use crate::params::{MLKEM512Params, MLKEM768Params, MLKEM1024Params, MLKEMParams}; use crate::polynomial::Polynomial; use bouncycastle_core::errors::KEMError; -use bouncycastle_core::key_material::{ - KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; -use bouncycastle_core::traits::{Hash, KEMPrivateKey, KEMPublicKey, SecurityStrength}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Hash, KEMPrivateKey, KEMPublicKey}; use bouncycastle_sha3::SHA3_256; use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; use core::fmt; diff --git a/crypto/mlkem-lowmemory/src/params.rs b/crypto/mlkem-lowmemory/src/params.rs index 447fb533..3f0d62c7 100644 --- a/crypto/mlkem-lowmemory/src/params.rs +++ b/crypto/mlkem-lowmemory/src/params.rs @@ -15,7 +15,7 @@ use crate::mlkem::{ ML_KEM_512_NAME, ML_KEM_768_NAME, ML_KEM_1024_NAME, MLKEM_SEED_LEN, MLKEM_SS_LEN, }; -use bouncycastle_core::traits::SecurityStrength; +use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_utils::secret::ZeroizablePrimitive; /// A fixed-size byte buffer whose length depends on the parameter set. diff --git a/crypto/mlkem-lowmemory/src/polynomial.rs b/crypto/mlkem-lowmemory/src/polynomial.rs index cc0480f1..59979a7d 100644 --- a/crypto/mlkem-lowmemory/src/polynomial.rs +++ b/crypto/mlkem-lowmemory/src/polynomial.rs @@ -9,7 +9,7 @@ use core::ops::{Index, IndexMut}; /// A polynomial over the ML-KEM ring. /// -/// # 🚨 Security 🚨 +/// # 🚨 Security Considerations 🚨 /// Polynomials themselves are not inherently secret since sometimes they are part of public keys /// and sometimes private keys. /// It is the responsibility of the caller to wrap sensitive instances in `Secret`. diff --git a/crypto/mlkem-lowmemory/tests/bc_test_data.rs b/crypto/mlkem-lowmemory/tests/bc_test_data.rs deleted file mode 100644 index beb259c0..00000000 --- a/crypto/mlkem-lowmemory/tests/bc_test_data.rs +++ /dev/null @@ -1,363 +0,0 @@ -// 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. - -// This whole file doesn't work because the bc-test-data repository only has full private keys and not seeds - -#[cfg(test)] -mod bc_test_data { - use bouncycastle_core::key_material::{ - KeyMaterial512, KeyMaterialTrait, KeyType, do_hazardous_operations, - }; - use bouncycastle_core::traits::{KEMPublicKey, SecurityStrength}; - use bouncycastle_hex as hex; - use bouncycastle_mlkem_lowmemory::mlkem::{ - MLKEM512_FULL_SK_LEN, MLKEM768_FULL_SK_LEN, MLKEM1024_FULL_SK_LEN, - }; - use bouncycastle_mlkem_lowmemory::{ - MLKEM512, MLKEM512_PK_LEN, MLKEM768, MLKEM768_PK_LEN, MLKEM1024, MLKEM1024_PK_LEN, - MLKEMPrivateKeyTrait, MLKEMTrait, - }; - use std::fs; - use std::path::Path; - use std::sync::Once; - - const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/pqc/crypto/mlkem"; - const TEST_DATA_PATH: &str = "../bc-test-data/pqc/crypto/mlkem"; - - 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!("wycheproof found at: {:?}", TEST_DATA_PATH_RELATIVE), - 2 => println!("wycheproof found at: {:?}", TEST_DATA_PATH), - _ => println!("WARNING: wycheproof directory not found; tests will be skipped"), - }); - - if !found == 3 { - return Err(()); - } - - 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) - } - - #[test] - #[allow(non_snake_case)] - fn ML_KEM_keyGen() { - let contents = match get_test_data("ML-KEM-keyGen.txt") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = KeyGenTestCase::parse(contents); - - for test_case in test_cases { - test_case.run(); - } - } - - #[derive(Clone)] - struct KeyGenTestCase { - vs_id: u32, - algorithm: String, - mode: String, - revision: String, - is_sample: bool, - tg_id: u32, - test_type: String, - parameter_set: String, - tc_id: u32, - z: String, - d: String, - ek: String, - dk: String, - } - - impl KeyGenTestCase { - fn new() -> Self { - Self { - vs_id: 0, - algorithm: String::new(), - mode: String::new(), - revision: String::new(), - is_sample: false, - tg_id: 0, - test_type: String::new(), - parameter_set: String::new(), - tc_id: 0, - z: String::new(), - d: String::new(), - ek: String::new(), - dk: String::new(), - } - } - - fn is_full(&self) -> bool { - !self.algorithm.is_empty() - } - - fn parse(data: String) -> Vec { - let mut test_cases = Vec::::new(); - let mut test_case = KeyGenTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "vsId" => test_case.vs_id = value.parse().unwrap(), - "algorithm" => test_case.algorithm = value.to_string(), - "mode" => test_case.mode = value.to_string(), - "revision" => test_case.revision = value.to_string(), - "isSample" => test_case.is_sample = value.parse().unwrap(), - "tgId" => test_case.tg_id = value.parse().unwrap(), - "testType" => test_case.test_type = value.to_string(), - "parameterSet" => test_case.parameter_set = value.to_string(), - "tcId" => test_case.tc_id = value.parse().unwrap(), - "z" => test_case.z = value.to_string(), - "d" => test_case.d = value.to_string(), - "ek" => test_case.ek = value.to_string(), - "dk" => test_case.dk = value.to_string(), - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self) { - assert_eq!(self.mode, "keyGen"); - - let mut seed_bytes = [0u8; 64]; - seed_bytes[..32].copy_from_slice(&*hex::decode(&self.d).unwrap()); - seed_bytes[32..].copy_from_slice(&*hex::decode(&self.z).unwrap()); - - let mut seed = KeyMaterial512::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); - - // for the purposes of the test cases, accept an all-zero seed - do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed)?; - seed.set_security_strength(SecurityStrength::_256bit) - }) - .unwrap(); - - match self.parameter_set.as_str() { - "ML-KEM-512" => { - let (pk, sk) = MLKEM512::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLKEM512_PK_LEN] = - hex::decode(&self.ek).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLKEM512_FULL_SK_LEN] = - hex::decode(&self.dk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode_full_sk(), sk_sized); - } - "ML-KEM-768" => { - let (pk, sk) = MLKEM768::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLKEM768_PK_LEN] = - hex::decode(&self.ek).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLKEM768_FULL_SK_LEN] = - hex::decode(&self.dk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode_full_sk(), sk_sized); - } - "ML-KEM-1024" => { - let (pk, sk) = MLKEM1024::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLKEM1024_PK_LEN] = - hex::decode(&self.ek).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLKEM1024_FULL_SK_LEN] = - hex::decode(&self.dk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode_full_sk(), sk_sized); - } - val => panic!("Invalid parameter set: {}", val), - } - } - } - - // Doesn't work here because the bc-test-data doesn't include seeds - // #[test] - // #[allow(non_snake_case)] - // fn ML_KEM_encapDecap() { - // let contents = fs::read_to_string(TEST_DATA_PATH.to_string() + "/ML-KEM-encapDecap.txt").unwrap(); - // let test_cases = EncapDecapTestCase::parse(contents); - // - // let num_tests = test_cases.len(); - // for test_case in test_cases { - // test_case.run(); - // } - // - // println!("SUCCESS! ML-DSA-sigGen test cases passed: {}!", num_tests); - // } - - // #[derive(Clone)] - // struct EncapDecapTestCase { - // vs_id: u32, - // algorithm: String, - // mode: String, - // revision: String, - // is_sample: bool, - // tg_id: u32, - // test_type: String, - // parameter_set: String, - // function: String, - // tc_id: u32, - // ek: String, - // dk: String, - // m: String, - // c: String, - // k: String, - // } - // - // impl EncapDecapTestCase { - // fn new() -> Self { - // Self { vs_id: 0, algorithm: String::new(), mode: String::new(), revision: String::new(), is_sample: false, tg_id: 0, test_type: String::new(), parameter_set: String::new(), function: String::new(), tc_id: 0, ek: String::new(), dk: String::new(), m: String::new(), c: String::new(), k: String::new() } - // } - // - // fn is_full(&self) -> bool { - // !self.algorithm.is_empty() - // } - // - // fn parse(data: String) -> Vec { - // let mut test_cases = Vec::::new(); - // let mut test_case = EncapDecapTestCase::new(); - // for line in data.lines() { - // let (tag, value) = match line.split_once(" = ") { - // Some(pair) => pair, - // None => { - // if test_case.is_full() { test_cases.push(test_case.clone()); } - // continue; - // } - // }; - // - // match tag { - // "vsId" => test_case.vs_id = value.parse().unwrap(), - // "algorithm" => test_case.algorithm = value.to_string(), - // "mode" => test_case.mode = value.to_string(), - // "revision" => test_case.revision = value.to_string(), - // "isSample" => test_case.is_sample = value.parse().unwrap(), - // "tgId" => test_case.tg_id = value.parse().unwrap(), - // "testType" => test_case.test_type = value.to_string(), - // "parameterSet" => test_case.parameter_set = value.to_string(), - // "function" => test_case.function = value.to_string(), - // "tcId" => test_case.tc_id = value.parse().unwrap(), - // "ek" => test_case.ek = value.to_string(), - // "dk" => test_case.dk = value.to_string(), - // "m" => test_case.m = value.to_string(), - // "c" => test_case.c = value.to_string(), - // "k" => test_case.k = value.to_string(), - // val => panic!("Invalid tag: {}", val), - // } - // } - // - // test_cases - // } - // - // fn run(&self) { - // assert_eq!(self.mode, "encapDecap"); - // - // let mut seed = [0u8; 64]; - // seed[..32].copy_from_slice(&*hex::decode(&self.).unwrap()); - // - // match self.parameter_set.as_str() { - // "ML-KEM-512" => { - // match self.function.as_str() { - // "encapsulation" => { - // let pk = MLKEM512PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()).unwrap(); - // let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); - // let (ss, ct) = MLKEM512::encaps_internal(&pk, m); - // - // let expected_ss = hex::decode(&self.k).unwrap(); - // let expected_ct = hex::decode(&self.c).unwrap(); - // - // assert_eq!(ss, expected_ss.as_slice()); - // assert_eq!(ct, expected_ct.as_slice()); - // }, - // "decapsulation" => { - // let sk = MLKEM512PrivateKey::from_bytes(&hex::decode(&self.).unwrap()).unwrap(); - // let ct = hex::decode(&self.c).unwrap(); - // let ss = MLKEM512::decaps(&sk, ct.as_slice()).unwrap(); - // - // let expected_ss = hex::decode(&self.k).unwrap(); - // assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); - // }, - // _ => panic!("Invalid function: {}", self.function), - // }; - // }, - // "ML-KEM-768" => { - // match self.function.as_str() { - // "encapsulation" => { - // let pk = MLKEM768PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()).unwrap(); - // let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); - // let (ss, ct) = MLKEM768::encaps_internal(&pk, m); - // - // let expected_ss = hex::decode(&self.k).unwrap(); - // let expected_ct = hex::decode(&self.c).unwrap(); - // - // assert_eq!(ss, expected_ss.as_slice()); - // assert_eq!(ct, expected_ct.as_slice()); - // }, - // "decapsulation" => { - // let sk = MLKEM768PrivateKey::from_bytes(&hex::decode(&self.dk).unwrap()).unwrap(); - // let ct = hex::decode(&self.c).unwrap(); - // let ss = MLKEM768::decaps(&sk, ct.as_slice()).unwrap(); - // - // let expected_ss = hex::decode(&self.k).unwrap(); - // assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); - // }, - // _ => panic!("Invalid function: {}", self.function), - // }; - // }, - // "ML-KEM-1024" => { - // match self.function.as_str() { - // "encapsulation" => { - // let pk = MLKEM1024PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()).unwrap(); - // let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); - // let (ss, ct) = MLKEM1024::encaps_internal(&pk, m); - // - // let expected_ss = hex::decode(&self.k).unwrap(); - // let expected_ct = hex::decode(&self.c).unwrap(); - // - // assert_eq!(ss, expected_ss.as_slice()); - // assert_eq!(ct, expected_ct.as_slice()); - // }, - // "decapsulation" => { - // let sk = MLKEM1024PrivateKey::from_bytes(&hex::decode(&self.dk).unwrap()).unwrap(); - // let ct = hex::decode(&self.c).unwrap(); - // let ss = MLKEM1024::decaps(&sk, ct.as_slice()).unwrap(); - // - // let expected_ss = hex::decode(&self.k).unwrap(); - // assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); - // }, - // _ => panic!("Invalid function: {}", self.function), - // }; - // }, - // val => panic!("Invalid parameter set: {}", val), - // } - // } - // } - // } -} diff --git a/crypto/mlkem-lowmemory/tests/mlkem_bc-test-data.rs b/crypto/mlkem-lowmemory/tests/mlkem_bc-test-data.rs new file mode 100644 index 00000000..ef8ed954 --- /dev/null +++ b/crypto/mlkem-lowmemory/tests/mlkem_bc-test-data.rs @@ -0,0 +1,324 @@ +//! Known-answer tests for ML-KEM-512/768/1024 against `ML-KEM-keyGen.txt`. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data", under `pqc/crypto/mlkem/`. If it is not +//! present the tests print a warning and pass vacuously. +//! +//! `ML-KEM-encapDecap.txt` is not run: its decapsulation cases give the expanded private key `dk`, +//! and this crate's private keys hold only the 64-byte seed. The test is kept, commented out, +//! below. + +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::KEMPublicKey; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_mlkem_lowmemory::mlkem::{ + MLKEM512_FULL_SK_LEN, MLKEM768_FULL_SK_LEN, MLKEM1024_FULL_SK_LEN, +}; +use bouncycastle_mlkem_lowmemory::{ + MLKEM512, MLKEM512_PK_LEN, MLKEM768, MLKEM768_PK_LEN, MLKEM1024, MLKEM1024_PK_LEN, + MLKEMPrivateKeyTrait, MLKEMTrait, +}; + +const TEST_DATA_DIR: &str = "pqc/crypto/mlkem"; + +#[test] +#[allow(non_snake_case)] +fn ML_KEM_keyGen() { + let Some(contents) = bc_test_data(TEST_DATA_DIR, "ML-KEM-keyGen.txt") else { return }; + + let test_cases = KeyGenTestCase::parse(contents); + + for test_case in test_cases { + test_case.run(); + } +} + +#[derive(Clone)] +struct KeyGenTestCase { + vs_id: u32, + algorithm: String, + mode: String, + revision: String, + is_sample: bool, + tg_id: u32, + test_type: String, + parameter_set: String, + tc_id: u32, + z: String, + d: String, + ek: String, + dk: String, +} + +impl KeyGenTestCase { + fn new() -> Self { + Self { + vs_id: 0, + algorithm: String::new(), + mode: String::new(), + revision: String::new(), + is_sample: false, + tg_id: 0, + test_type: String::new(), + parameter_set: String::new(), + tc_id: 0, + z: String::new(), + d: String::new(), + ek: String::new(), + dk: String::new(), + } + } + + fn is_full(&self) -> bool { + !self.algorithm.is_empty() + } + + fn parse(data: String) -> Vec { + let mut test_cases = Vec::::new(); + let mut test_case = KeyGenTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "vsId" => test_case.vs_id = value.parse().unwrap(), + "algorithm" => test_case.algorithm = value.to_string(), + "mode" => test_case.mode = value.to_string(), + "revision" => test_case.revision = value.to_string(), + "isSample" => test_case.is_sample = value.parse().unwrap(), + "tgId" => test_case.tg_id = value.parse().unwrap(), + "testType" => test_case.test_type = value.to_string(), + "parameterSet" => test_case.parameter_set = value.to_string(), + "tcId" => test_case.tc_id = value.parse().unwrap(), + "z" => test_case.z = value.to_string(), + "d" => test_case.d = value.to_string(), + "ek" => test_case.ek = value.to_string(), + "dk" => test_case.dk = value.to_string(), + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + fn run(&self) { + assert_eq!(self.mode, "keyGen"); + + let mut seed_bytes = [0u8; 64]; + seed_bytes[..32].copy_from_slice(&*hex::decode(&self.d).unwrap()); + seed_bytes[32..].copy_from_slice(&*hex::decode(&self.z).unwrap()); + + let mut seed = KeyMaterial512::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); + + // for the purposes of the test cases, accept an all-zero seed + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + + match self.parameter_set.as_str() { + "ML-KEM-512" => { + let (pk, sk) = MLKEM512::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLKEM512_PK_LEN] = + hex::decode(&self.ek).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLKEM512_FULL_SK_LEN] = + hex::decode(&self.dk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode_full_sk(), sk_sized); + } + "ML-KEM-768" => { + let (pk, sk) = MLKEM768::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLKEM768_PK_LEN] = + hex::decode(&self.ek).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLKEM768_FULL_SK_LEN] = + hex::decode(&self.dk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode_full_sk(), sk_sized); + } + "ML-KEM-1024" => { + let (pk, sk) = MLKEM1024::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLKEM1024_PK_LEN] = + hex::decode(&self.ek).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLKEM1024_FULL_SK_LEN] = + hex::decode(&self.dk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode_full_sk(), sk_sized); + } + val => panic!("Invalid parameter set: {}", val), + } + } +} + +// Doesn't work here because the bc-test-data doesn't include seeds +// #[test] +// #[allow(non_snake_case)] +// fn ML_KEM_encapDecap() { +// let Some(contents) = bc_test_data(TEST_DATA_DIR, "ML-KEM-encapDecap.txt") else { return }; +// let test_cases = EncapDecapTestCase::parse(contents); +// +// let num_tests = test_cases.len(); +// for test_case in test_cases { +// test_case.run(); +// } +// +// println!("SUCCESS! ML-KEM-encapDecap test cases passed: {}!", num_tests); +// } + +// #[derive(Clone)] +// struct EncapDecapTestCase { +// vs_id: u32, +// algorithm: String, +// mode: String, +// revision: String, +// is_sample: bool, +// tg_id: u32, +// test_type: String, +// parameter_set: String, +// function: String, +// tc_id: u32, +// ek: String, +// dk: String, +// m: String, +// c: String, +// k: String, +// } +// +// impl EncapDecapTestCase { +// fn new() -> Self { +// Self { vs_id: 0, algorithm: String::new(), mode: String::new(), revision: String::new(), is_sample: false, tg_id: 0, test_type: String::new(), parameter_set: String::new(), function: String::new(), tc_id: 0, ek: String::new(), dk: String::new(), m: String::new(), c: String::new(), k: String::new() } +// } +// +// fn is_full(&self) -> bool { +// !self.algorithm.is_empty() +// } +// +// fn parse(data: String) -> Vec { +// let mut test_cases = Vec::::new(); +// let mut test_case = EncapDecapTestCase::new(); +// for line in data.lines() { +// let (tag, value) = match line.split_once(" = ") { +// Some(pair) => pair, +// None => { +// if test_case.is_full() { test_cases.push(test_case.clone()); } +// continue; +// } +// }; +// +// match tag { +// "vsId" => test_case.vs_id = value.parse().unwrap(), +// "algorithm" => test_case.algorithm = value.to_string(), +// "mode" => test_case.mode = value.to_string(), +// "revision" => test_case.revision = value.to_string(), +// "isSample" => test_case.is_sample = value.parse().unwrap(), +// "tgId" => test_case.tg_id = value.parse().unwrap(), +// "testType" => test_case.test_type = value.to_string(), +// "parameterSet" => test_case.parameter_set = value.to_string(), +// "function" => test_case.function = value.to_string(), +// "tcId" => test_case.tc_id = value.parse().unwrap(), +// "ek" => test_case.ek = value.to_string(), +// "dk" => test_case.dk = value.to_string(), +// "m" => test_case.m = value.to_string(), +// "c" => test_case.c = value.to_string(), +// "k" => test_case.k = value.to_string(), +// val => panic!("Invalid tag: {}", val), +// } +// } +// +// test_cases +// } +// +// fn run(&self) { +// assert_eq!(self.mode, "encapDecap"); +// +// let mut seed = [0u8; 64]; +// seed[..32].copy_from_slice(&*hex::decode(&self.).unwrap()); +// +// match self.parameter_set.as_str() { +// "ML-KEM-512" => { +// match self.function.as_str() { +// "encapsulation" => { +// let pk = MLKEM512PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()).unwrap(); +// let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); +// let (ss, ct) = MLKEM512::encaps_with_randomness(&pk, m); +// +// let expected_ss = hex::decode(&self.k).unwrap(); +// let expected_ct = hex::decode(&self.c).unwrap(); +// +// assert_eq!(ss, expected_ss.as_slice()); +// assert_eq!(ct, expected_ct.as_slice()); +// }, +// "decapsulation" => { +// let sk = MLKEM512PrivateKey::from_bytes(&hex::decode(&self.).unwrap()).unwrap(); +// let ct = hex::decode(&self.c).unwrap(); +// let ss = MLKEM512::decaps(&sk, ct.as_slice()).unwrap(); +// +// let expected_ss = hex::decode(&self.k).unwrap(); +// assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); +// }, +// _ => panic!("Invalid function: {}", self.function), +// }; +// }, +// "ML-KEM-768" => { +// match self.function.as_str() { +// "encapsulation" => { +// let pk = MLKEM768PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()).unwrap(); +// let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); +// let (ss, ct) = MLKEM768::encaps_with_randomness(&pk, m); +// +// let expected_ss = hex::decode(&self.k).unwrap(); +// let expected_ct = hex::decode(&self.c).unwrap(); +// +// assert_eq!(ss, expected_ss.as_slice()); +// assert_eq!(ct, expected_ct.as_slice()); +// }, +// "decapsulation" => { +// let sk = MLKEM768PrivateKey::from_bytes(&hex::decode(&self.dk).unwrap()).unwrap(); +// let ct = hex::decode(&self.c).unwrap(); +// let ss = MLKEM768::decaps(&sk, ct.as_slice()).unwrap(); +// +// let expected_ss = hex::decode(&self.k).unwrap(); +// assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); +// }, +// _ => panic!("Invalid function: {}", self.function), +// }; +// }, +// "ML-KEM-1024" => { +// match self.function.as_str() { +// "encapsulation" => { +// let pk = MLKEM1024PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()).unwrap(); +// let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); +// let (ss, ct) = MLKEM1024::encaps_with_randomness(&pk, m); +// +// let expected_ss = hex::decode(&self.k).unwrap(); +// let expected_ct = hex::decode(&self.c).unwrap(); +// +// assert_eq!(ss, expected_ss.as_slice()); +// assert_eq!(ct, expected_ct.as_slice()); +// }, +// "decapsulation" => { +// let sk = MLKEM1024PrivateKey::from_bytes(&hex::decode(&self.dk).unwrap()).unwrap(); +// let ct = hex::decode(&self.c).unwrap(); +// let ss = MLKEM1024::decaps(&sk, ct.as_slice()).unwrap(); +// +// let expected_ss = hex::decode(&self.k).unwrap(); +// assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); +// }, +// _ => panic!("Invalid function: {}", self.function), +// }; +// }, +// val => panic!("Invalid parameter set: {}", val), +// } +// } +// } +// } diff --git a/crypto/mlkem-lowmemory/tests/mlkem_key_tests.rs b/crypto/mlkem-lowmemory/tests/mlkem_key_tests.rs index b370ec24..ab2dc1d0 100644 --- a/crypto/mlkem-lowmemory/tests/mlkem_key_tests.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_key_tests.rs @@ -1,7 +1,8 @@ #[cfg(test)] mod mlkem_key_tests { use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; - use bouncycastle_core::traits::{KEMPrivateKey, KEMPublicKey, SecurityStrength}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{KEMPrivateKey, KEMPublicKey}; use bouncycastle_hex as hex; use bouncycastle_mlkem_lowmemory::mlkem::MLKEM512_FULL_SK_LEN; use bouncycastle_mlkem_lowmemory::{MLKEM512, MLKEM768, MLKEM1024}; diff --git a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs index 74cd7c17..29177869 100644 --- a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs @@ -2,14 +2,15 @@ #[cfg(test)] mod mlkem_tests { use bouncycastle_core::errors::{KEMError, RNGError}; - use bouncycastle_core::key_material::{ - KeyMaterial512, KeyMaterialTrait, KeyType, do_hazardous_operations, - }; + use bouncycastle_core::hazmat::do_hazardous_operations; + use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; + use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, + Hash, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, XOF, XOFSqueezer, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; + use bouncycastle_mlkem_lowmemory::hazmat::EncapsWithRandomness; use bouncycastle_mlkem_lowmemory::mlkem::{ MLKEM512_FULL_SK_LEN, MLKEM768_FULL_SK_LEN, MLKEM1024_FULL_SK_LEN, }; @@ -258,7 +259,7 @@ mod mlkem_tests { let expected_ciphertext: [u8; MLKEM1024_CT_LEN] = hex::decode("8B9FE419250C5FB0463C8181FCF7CEC777136B738E015EBA31067AA4A8C378BBAC0121B88214F1AEB866E4F33C277099E09B4BF7E21CDDA30B5B32C18B0E9660C30601D85DAEC07AAF4B343EC5516FA501DD63088B999FB9A414C6CA593806C08CD4C775139BF0F0BF3676D773EDD56E616A13830D5F5FE35E515DBC84E43AAD0167D57E60A9DE30886ACD3F7F2006CAC26A7A07B4DADBEDFBED7F305764386AAD726D5B2BF14A376BAD8B4896688491733FB34E6EDEA10BFD5E448541CB6E69E3D87DF190AFA7FF62577775BAACEA444A6128A20200251D8FA759DC60FDA6A9730CFFE4997FE7EBCDD1644AE2D55290A4074CDD2CE53C18D22BC33671E68727A9B5A2FEAFB114A8045D96A56981E200A09661375987625ACC233EDE817AF1DEEAA21C7C4377423E73C5AF9BFF58A49DE6DAFD07A3E3BABD891F62BBA41D1856B8BC502CC86EE115A3598431E2B54AB0C5EACC3CE6A03090925C1FD5A251B00576763A963994A7A23EE12EBFC1B994F93C6144178F0BEF88245CE77CD32EF651826A6090AF561A5864DEC2A51D846F1F48F88B4B55F58C2373E0F67BDC95DC23A43E8546232A7B234E49F5226A3A63BDBCED7240FC81C2DB68AAEB2671A2FD231997BF8839C63A7F41F15E7242821D42E80BBC0F43FA9E353DE8B25ED8FFC242EB512C6A5260919AAE89A11176532BCCC762A520A37AEC4E7209AA81CEE0DD4ADD932C47EB8100BE98AA1DEEA9EA698115ADCED950A6C536D19AEB325CEA8C5245C0A2281533FB90809DC2BE90567EBE6AE229FE09B44DA2182585EA694D8A9AB33EBC24B44E09BD510F34B4140E1FB41162F9415F2D9106A0CEA00A26ED0920021F4E5BCFB3DABF5850DAB22B2E889D9611FBE06D0C899708EB5E5FAD2FBBE0D5C0BDE080F8E760EDFA037D55DA77F0F39591BF5B050C905FA538B7228E238A290DF340778DCBD6BE40A3B1DD455FB27ADBE176AEF6CC295BEA570BDC221BA14002E3B113B0EF237452FBC9F1AEC42E0D2B33F19832DB0A6171CAEB0B30EEAD3A54B704B761C7D4AFEA8F6AFC15156666A081C43AEB2E04FEECEF8AABA4049BD78B120B9ABA86A60342A0CF806411C473C26C4BE1540E3312388BCBC8523BA73F40EA28D5564274F3661D7ACAA0F1E8D0F28DCF6B501329963E6857FDB2AAE873A7D9D6C14821F6C0B6AA50AC449075CD6F2A256C5A05959DAB5A5912CC8E8F8B9F59941BFCCE6A28CBA74A20382B1FD3382D056547D5BC5EF4AAE62F96F038C595A4F901D6AE790F8978292AD1CC3A1E800B71A5BBE84533646655E3752FBD6B02B97B204E75D28A34C2F990FB8E8CD31CE6E683FA7E67DA03367E8D47DC626F060FBA2D0425004CAC2A61D982D2E3D85008624B45DB022CF51BA265B5E974712A9372EECAC0EA272B2FC56EBED0D32105521BA2C4A8FE0C678CE4E45902C7BA9D510BD47B2B5F931DD732F27DE9B42FD4AA39EAC765283A9965EE97C0D88E23EFA6F718242C67770B87BF8832858C1D13FC520870BD34F2B9C6FBFD1A528B744F814C93F4F4E87108316FE2AB06E02292DEA7FCF6FEFB17BF5AA7376A4A9BDB7C49BF709EB1E05D60EF14CD85A75239B97BCA9A6A3CC1B28F28979D612431BAAC1ACEE5EF62776B4D51B7EB0F63DF507760097223CA903E16E02DEB7FCABFBEC26DAEDC0ED4CC55726BDC31D1775112EF3C35D1DF928C6EB7830D8CA6570CB5CE348E3F26DDE864F20E5BE7B99E264EBC0E9D8DE9C6E4B7FE3CFBE673833CF7E8B3081529062CB6815C7C0766822B3B31E56BA1FC73FE3DED4B5D435BFCE2F2997C1D4B9CE293220DD461103BE084BF12076372668A69836769C1F6D8C32E2C7BC2E7D66714C814793A2970C90DD94DF14C89C60DD35B52A14778E137E750CE83AC3AAB667FCBDCBA38B7FA6D1C6BF7B99D957078176D9779A09F84B75FBC2A11769EF65532B09ACA4C9A3766B4A1FC717F94648FB8B8D9363E54F1C4201C075C18B1EAE098B83598089585ED9DC06B96E2D1C96DC738086EBBC26C3193B64139E1FC1DFB22A17893506EF7B35792B4EB00196693686EB5DEB3CEB436DD16D2D92A0FD31F468AF8662040F5257BFA0F14991C0D560999EEF775178D14955ADF091DD797AC1FDCEC7776055271C0F130562D0B0A6749B159DD0DB9AC69271AC719B83B683CE8B32342AC4AB257B0F8083C8CC86338AFA4D386C9848F413ED0").unwrap().try_into().unwrap(); // encaps - let (ss, ct) = MLKEM1024::encaps_internal(&pk, message); + let (ss, ct) = MLKEM1024::encaps_with_randomness(&pk, message); assert_eq!(ss, expected_shared_secret); assert_eq!(ct, expected_ciphertext); @@ -302,7 +303,7 @@ mod mlkem_tests { let expected_ciphertext: [u8; MLKEM1024_CT_LEN] = hex::decode("8B9FE419250C5FB0463C8181FCF7CEC777136B738E015EBA31067AA4A8C378BBAC0121B88214F1AEB866E4F33C277099E09B4BF7E21CDDA30B5B32C18B0E9660C30601D85DAEC07AAF4B343EC5516FA501DD63088B999FB9A414C6CA593806C08CD4C775139BF0F0BF3676D773EDD56E616A13830D5F5FE35E515DBC84E43AAD0167D57E60A9DE30886ACD3F7F2006CAC26A7A07B4DADBEDFBED7F305764386AAD726D5B2BF14A376BAD8B4896688491733FB34E6EDEA10BFD5E448541CB6E69E3D87DF190AFA7FF62577775BAACEA444A6128A20200251D8FA759DC60FDA6A9730CFFE4997FE7EBCDD1644AE2D55290A4074CDD2CE53C18D22BC33671E68727A9B5A2FEAFB114A8045D96A56981E200A09661375987625ACC233EDE817AF1DEEAA21C7C4377423E73C5AF9BFF58A49DE6DAFD07A3E3BABD891F62BBA41D1856B8BC502CC86EE115A3598431E2B54AB0C5EACC3CE6A03090925C1FD5A251B00576763A963994A7A23EE12EBFC1B994F93C6144178F0BEF88245CE77CD32EF651826A6090AF561A5864DEC2A51D846F1F48F88B4B55F58C2373E0F67BDC95DC23A43E8546232A7B234E49F5226A3A63BDBCED7240FC81C2DB68AAEB2671A2FD231997BF8839C63A7F41F15E7242821D42E80BBC0F43FA9E353DE8B25ED8FFC242EB512C6A5260919AAE89A11176532BCCC762A520A37AEC4E7209AA81CEE0DD4ADD932C47EB8100BE98AA1DEEA9EA698115ADCED950A6C536D19AEB325CEA8C5245C0A2281533FB90809DC2BE90567EBE6AE229FE09B44DA2182585EA694D8A9AB33EBC24B44E09BD510F34B4140E1FB41162F9415F2D9106A0CEA00A26ED0920021F4E5BCFB3DABF5850DAB22B2E889D9611FBE06D0C899708EB5E5FAD2FBBE0D5C0BDE080F8E760EDFA037D55DA77F0F39591BF5B050C905FA538B7228E238A290DF340778DCBD6BE40A3B1DD455FB27ADBE176AEF6CC295BEA570BDC221BA14002E3B113B0EF237452FBC9F1AEC42E0D2B33F19832DB0A6171CAEB0B30EEAD3A54B704B761C7D4AFEA8F6AFC15156666A081C43AEB2E04FEECEF8AABA4049BD78B120B9ABA86A60342A0CF806411C473C26C4BE1540E3312388BCBC8523BA73F40EA28D5564274F3661D7ACAA0F1E8D0F28DCF6B501329963E6857FDB2AAE873A7D9D6C14821F6C0B6AA50AC449075CD6F2A256C5A05959DAB5A5912CC8E8F8B9F59941BFCCE6A28CBA74A20382B1FD3382D056547D5BC5EF4AAE62F96F038C595A4F901D6AE790F8978292AD1CC3A1E800B71A5BBE84533646655E3752FBD6B02B97B204E75D28A34C2F990FB8E8CD31CE6E683FA7E67DA03367E8D47DC626F060FBA2D0425004CAC2A61D982D2E3D85008624B45DB022CF51BA265B5E974712A9372EECAC0EA272B2FC56EBED0D32105521BA2C4A8FE0C678CE4E45902C7BA9D510BD47B2B5F931DD732F27DE9B42FD4AA39EAC765283A9965EE97C0D88E23EFA6F718242C67770B87BF8832858C1D13FC520870BD34F2B9C6FBFD1A528B744F814C93F4F4E87108316FE2AB06E02292DEA7FCF6FEFB17BF5AA7376A4A9BDB7C49BF709EB1E05D60EF14CD85A75239B97BCA9A6A3CC1B28F28979D612431BAAC1ACEE5EF62776B4D51B7EB0F63DF507760097223CA903E16E02DEB7FCABFBEC26DAEDC0ED4CC55726BDC31D1775112EF3C35D1DF928C6EB7830D8CA6570CB5CE348E3F26DDE864F20E5BE7B99E264EBC0E9D8DE9C6E4B7FE3CFBE673833CF7E8B3081529062CB6815C7C0766822B3B31E56BA1FC73FE3DED4B5D435BFCE2F2997C1D4B9CE293220DD461103BE084BF12076372668A69836769C1F6D8C32E2C7BC2E7D66714C814793A2970C90DD94DF14C89C60DD35B52A14778E137E750CE83AC3AAB667FCBDCBA38B7FA6D1C6BF7B99D957078176D9779A09F84B75FBC2A11769EF65532B09ACA4C9A3766B4A1FC717F94648FB8B8D9363E54F1C4201C075C18B1EAE098B83598089585ED9DC06B96E2D1C96DC738086EBBC26C3193B64139E1FC1DFB22A17893506EF7B35792B4EB00196693686EB5DEB3CEB436DD16D2D92A0FD31F468AF8662040F5257BFA0F14991C0D560999EEF775178D14955ADF091DD797AC1FDCEC7776055271C0F130562D0B0A6749B159DD0DB9AC69271AC719B83B683CE8B32342AC4AB257B0F8083C8CC86338AFA4D386C9848F413ED0").unwrap().try_into().unwrap(); // encaps - let (ss, ct) = MLKEM1024::encaps_internal(&pk, message); + let (ss, ct) = MLKEM1024::encaps_with_randomness(&pk, message); assert_eq!(ss, expected_shared_secret); assert_eq!(ct, expected_ciphertext); @@ -434,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); } @@ -652,38 +652,38 @@ mod mlkem_tests { // ML-KEM-512 let (pk512, _sk) = MLKEM512::keygen().unwrap(); - let (ss_ref, ct_ref) = MLKEM512::encaps_internal(&pk512, m); + let (ss_ref, ct_ref) = MLKEM512::encaps_with_randomness(&pk512, m); let mut rng = FixedSeedRNG::new(seed_bytes); let (ss, ct) = MLKEM512::encaps_rng(&pk512, &mut rng).unwrap(); - assert_eq!(ct, ct_ref, "ML-KEM-512 ciphertext must match encaps_internal"); + assert_eq!(ct, ct_ref, "ML-KEM-512 ciphertext must match encaps_with_randomness"); assert_eq!( ss_ref, ss.ref_to_bytes(), - "ML-KEM-512 shared secret must match encaps_internal" + "ML-KEM-512 shared secret must match encaps_with_randomness" ); // ML-KEM-768 let (pk768, _sk) = MLKEM768::keygen().unwrap(); - let (ss_ref, ct_ref) = MLKEM768::encaps_internal(&pk768, m); + let (ss_ref, ct_ref) = MLKEM768::encaps_with_randomness(&pk768, m); let mut rng = FixedSeedRNG::new(seed_bytes); let (ss, ct) = MLKEM768::encaps_rng(&pk768, &mut rng).unwrap(); - assert_eq!(ct, ct_ref, "ML-KEM-768 ciphertext must match encaps_internal"); + assert_eq!(ct, ct_ref, "ML-KEM-768 ciphertext must match encaps_with_randomness"); assert_eq!( ss_ref, ss.ref_to_bytes(), - "ML-KEM-768 shared secret must match encaps_internal" + "ML-KEM-768 shared secret must match encaps_with_randomness" ); // ML-KEM-1024 let (pk1024, _sk) = MLKEM1024::keygen().unwrap(); - let (ss_ref, ct_ref) = MLKEM1024::encaps_internal(&pk1024, m); + let (ss_ref, ct_ref) = MLKEM1024::encaps_with_randomness(&pk1024, m); let mut rng = FixedSeedRNG::new(seed_bytes); let (ss, ct) = MLKEM1024::encaps_rng(&pk1024, &mut rng).unwrap(); - assert_eq!(ct, ct_ref, "ML-KEM-1024 ciphertext must match encaps_internal"); + assert_eq!(ct, ct_ref, "ML-KEM-1024 ciphertext must match encaps_with_randomness"); assert_eq!( ss_ref, ss.ref_to_bytes(), - "ML-KEM-1024 shared secret must match encaps_internal" + "ML-KEM-1024 shared secret must match encaps_with_randomness" ); // Ensure that it rejects an RNG at a lower security level @@ -725,7 +725,7 @@ mod mlkem_tests { #[test] fn algorithm_names_and_oids() { - use bouncycastle_core::traits::{Algorithm, AlgorithmOID, SecurityStrength}; + use bouncycastle_core::traits::{Algorithm, AlgorithmOID}; // `Algorithm` and `AlgorithmOID` are implemented once, generically over the parameter set, // so nothing else states these per algorithm. Pinned here so that a wrong wiring of the diff --git a/crypto/mlkem-lowmemory/tests/wycheproof.rs b/crypto/mlkem-lowmemory/tests/mlkem_wycheproof.rs similarity index 70% rename from crypto/mlkem-lowmemory/tests/wycheproof.rs rename to crypto/mlkem-lowmemory/tests/mlkem_wycheproof.rs index 4bd5ad11..18efc435 100644 --- a/crypto/mlkem-lowmemory/tests/wycheproof.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_wycheproof.rs @@ -24,212 +24,142 @@ #![allow(dead_code)] -use bouncycastle_core::key_material::{ - KeyMaterial512, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; -use bouncycastle_core::traits::{KEMDecapsulator, KEMPublicKey, SecurityStrength}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{KEMDecapsulator, KEMPublicKey}; +use bouncycastle_core_test_framework::test_data_loaders::{Value, wycheproof_json}; use bouncycastle_hex as hex; +use bouncycastle_mlkem_lowmemory::hazmat::EncapsWithRandomness; use bouncycastle_mlkem_lowmemory::{ MLKEM512, MLKEM512PublicKey, MLKEM768, MLKEM768PublicKey, MLKEM1024, MLKEM1024PublicKey, MLKEMPrivateKeyTrait, MLKEMTrait, }; -#[cfg(test)] -mod wycheproof { - use crate::{MLKEMEncapsTestCase, MLKEMKeygenSeedTestCase, MLKEMTestCase, ParameterSet}; - use std::fs; - use std::path::Path; - use std::sync::Once; - - const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; - const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; - - 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; - }; +#[test] +fn mlkem_512_encaps_test() { + let Some(json) = wycheproof_json("mlkem_512_encaps_test.json") else { return }; - // just print once - TEST_DATA_CHECK.call_once(|| match found { - 1 => println!("wycheproof found at: {:?}", TEST_DATA_PATH_RELATIVE), - 2 => println!("wycheproof found at: {:?}", TEST_DATA_PATH), - _ => println!("WARNING: wycheproof directory not found; tests will be skipped"), - }); - - if !found == 3 { - return Err(()); - } - - 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(()); - }; + let test_cases = MLKEMEncapsTestCase::parse(json, ParameterSet::Mlkem512); - Ok(contents) + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem512(); } - #[test] - fn mlkem_512_encaps_test() { - let contents = match get_test_data("mlkem_512_encaps_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_512_encaps_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMEncapsTestCase::parse(contents, ParameterSet::Mlkem512); +#[test] +fn mlkem_512_keygen_seed_test() { + let Some(json) = wycheproof_json("mlkem_512_keygen_seed_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem512(); - } + let test_cases = MLKEMKeygenSeedTestCase::parse(json, ParameterSet::Mlkem512); - println!("mlkem_512_encaps_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem512(); } - #[test] - fn mlkem_512_keygen_seed_test() { - let contents = match get_test_data("mlkem_512_keygen_seed_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_512_keygen_seed_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMKeygenSeedTestCase::parse(contents, ParameterSet::Mlkem512); +#[test] +fn mlkem_512_test() { + let Some(json) = wycheproof_json("mlkem_512_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem512(); - } + let test_cases = MLKEMTestCase::parse(json, ParameterSet::Mlkem512); - println!("mlkem_512_keygen_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem512(); } - #[test] - fn mlkem_512_test() { - let contents = match get_test_data("mlkem_512_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_512_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMTestCase::parse(contents, ParameterSet::Mlkem512); +#[test] +fn mlkem_768_encaps_test() { + let Some(json) = wycheproof_json("mlkem_768_encaps_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem512(); - } + let test_cases = MLKEMEncapsTestCase::parse(json, ParameterSet::Mlkem768); - println!("mlkem_512_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem768(); } - #[test] - fn mlkem_768_encaps_test() { - let contents = match get_test_data("mlkem_768_encaps_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_768_encaps_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMEncapsTestCase::parse(contents, ParameterSet::Mlkem768); +#[test] +fn mlkem_768_keygen_seed_test() { + let Some(json) = wycheproof_json("mlkem_768_keygen_seed_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem768(); - } + let test_cases = MLKEMKeygenSeedTestCase::parse(json, ParameterSet::Mlkem768); - println!("mlkem_768_encaps_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem768(); } - #[test] - fn mlkem_768_keygen_seed_test() { - let contents = match get_test_data("mlkem_768_keygen_seed_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_768_keygen_seed_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMKeygenSeedTestCase::parse(contents, ParameterSet::Mlkem768); +#[test] +fn mlkem_768_test() { + let Some(json) = wycheproof_json("mlkem_768_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem768(); - } + let test_cases = MLKEMTestCase::parse(json, ParameterSet::Mlkem768); - println!("mlkem_768_keygen_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem768(); } - #[test] - fn mlkem_768_test() { - let contents = match get_test_data("mlkem_768_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_768_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMTestCase::parse(contents, ParameterSet::Mlkem768); +#[test] +fn mlkem_1024_encaps_test() { + let Some(json) = wycheproof_json("mlkem_1024_encaps_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem768(); - } + let test_cases = MLKEMEncapsTestCase::parse(json, ParameterSet::Mlkem1024); - println!("mlkem_768_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem1024(); } - #[test] - fn mlkem_1024_encaps_test() { - let contents = match get_test_data("mlkem_1024_encaps_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_1024_encaps_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMEncapsTestCase::parse(contents, ParameterSet::Mlkem1024); +#[test] +fn mlkem_1024_keygen_seed_test() { + let Some(json) = wycheproof_json("mlkem_1024_keygen_seed_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem1024(); - } + let test_cases = MLKEMKeygenSeedTestCase::parse(json, ParameterSet::Mlkem1024); - println!("mlkem_1024_encaps_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem1024(); } - #[test] - fn mlkem_1024_keygen_seed_test() { - let contents = match get_test_data("mlkem_1024_keygen_seed_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_1024_keygen_seed_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMKeygenSeedTestCase::parse(contents, ParameterSet::Mlkem1024); +#[test] +fn mlkem_1024_test() { + let Some(json) = wycheproof_json("mlkem_1024_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem1024(); - } + let test_cases = MLKEMTestCase::parse(json, ParameterSet::Mlkem1024); - println!("mlkem_1024_keygen_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem1024(); } - #[test] - fn mlkem_1024_test() { - let contents = match get_test_data("mlkem_1024_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = MLKEMTestCase::parse(contents, ParameterSet::Mlkem1024); - - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem1024(); - } - - println!("mlkem_1024_test: all {} test cases passed.", num_test_cases); - } + println!("mlkem_1024_test: all {} test cases passed.", num_test_cases); } /* Structs for holding test data */ @@ -267,10 +197,7 @@ impl MLKEMEncapsTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); @@ -326,7 +253,7 @@ impl MLKEMEncapsTestCase { } }; - let (k, ct) = MLKEM512::encaps_internal(&ek, m); + let (k, ct) = MLKEM512::encaps_with_randomness(&ek, m); if self.result == "valid" { assert_eq!(k, hex::decode(&self.k).unwrap().as_slice()); @@ -355,8 +282,10 @@ impl MLKEMEncapsTestCase { /* Perform the deterministic encaps and compare results */ - let (k, ct) = - MLKEM768::encaps_internal(&ek, hex::decode(&self.m).unwrap().try_into().unwrap()); + let (k, ct) = MLKEM768::encaps_with_randomness( + &ek, + hex::decode(&self.m).unwrap().try_into().unwrap(), + ); if self.result == "valid" { assert_eq!(k, hex::decode(&self.k).unwrap().as_slice()); @@ -385,8 +314,10 @@ impl MLKEMEncapsTestCase { /* Perform the deterministic encaps and compare results */ - let (k, ct) = - MLKEM1024::encaps_internal(&ek, hex::decode(&self.m).unwrap().try_into().unwrap()); + let (k, ct) = MLKEM1024::encaps_with_randomness( + &ek, + hex::decode(&self.m).unwrap().try_into().unwrap(), + ); if self.result == "valid" { assert_eq!(k, hex::decode(&self.k).unwrap().as_slice()); @@ -421,10 +352,7 @@ impl MLKEMKeygenSeedTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); @@ -524,10 +452,7 @@ impl MLKEMTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); diff --git a/crypto/mlkem/Cargo.toml b/crypto/mlkem/Cargo.toml index 2e0d26e8..bea4c1e6 100644 --- a/crypto/mlkem/Cargo.toml +++ b/crypto/mlkem/Cargo.toml @@ -14,7 +14,6 @@ bouncycastle-core-test-framework.workspace = true bouncycastle-hex.workspace = true bouncycastle-rng.workspace = true criterion.workspace = true -serde_json = "1.0" [[bench]] name = "mlkem_benches" diff --git a/crypto/mlkem/benches/mlkem_benches.rs b/crypto/mlkem/benches/mlkem_benches.rs index 313ab652..df169529 100644 --- a/crypto/mlkem/benches/mlkem_benches.rs +++ b/crypto/mlkem/benches/mlkem_benches.rs @@ -1,6 +1,7 @@ use bouncycastle_core::key_material::{KeyMaterial512, KeyType}; use bouncycastle_core::traits::KEMDecapsulator; use bouncycastle_hex as hex; +use bouncycastle_mlkem::hazmat::EncapsWithRandomness; use bouncycastle_mlkem::{ MLKEM_RND_LEN, MLKEM512, MLKEM512_CT_LEN, MLKEM512PrivateKeyExpanded, MLKEM768, MLKEM768_CT_LEN, MLKEM768PrivateKeyExpanded, MLKEM1024, MLKEM1024_CT_LEN, @@ -122,7 +123,7 @@ fn bench_mlkem_encaps(c: &mut Criterion) { group.bench_function("ML-KEM-512", |b| { b.iter(|| { for i in 0..NUM_ELEMS { - _ = black_box(MLKEM512::encaps_internal(&pk, None, nonces[i])); + _ = black_box(MLKEM512::encaps_with_randomness(&pk, None, nonces[i])); } }) }); @@ -135,7 +136,7 @@ fn bench_mlkem_encaps(c: &mut Criterion) { group.bench_function("ML-KEM-768", |b| { b.iter(|| { for i in 0..NUM_ELEMS { - _ = black_box(MLKEM768::encaps_internal(&pk, None, nonces[i])); + _ = black_box(MLKEM768::encaps_with_randomness(&pk, None, nonces[i])); } }) }); @@ -148,7 +149,7 @@ fn bench_mlkem_encaps(c: &mut Criterion) { group.bench_function("ML-KEM-1024", |b| { b.iter(|| { for i in 0..NUM_ELEMS { - _ = black_box(MLKEM1024::encaps_internal(&pk, None, nonces[i])); + _ = black_box(MLKEM1024::encaps_with_randomness(&pk, None, nonces[i])); } }) }); @@ -189,7 +190,7 @@ fn bench_mlkem_encaps_for_expanded(c: &mut Criterion) { group.bench_function("ML-KEM-512", |b| { b.iter(|| { for i in 0..NUM_ELEMS { - _ = black_box(MLKEM512::encaps_internal(&pk, Some(&a_hat), nonces[i])); + _ = black_box(MLKEM512::encaps_with_randomness(&pk, Some(&a_hat), nonces[i])); } }) }); @@ -203,7 +204,7 @@ fn bench_mlkem_encaps_for_expanded(c: &mut Criterion) { group.bench_function("ML-KEM-768", |b| { b.iter(|| { for i in 0..NUM_ELEMS { - _ = black_box(MLKEM768::encaps_internal(&pk, Some(&a_hat), nonces[i])); + _ = black_box(MLKEM768::encaps_with_randomness(&pk, Some(&a_hat), nonces[i])); } }) }); @@ -217,7 +218,7 @@ fn bench_mlkem_encaps_for_expanded(c: &mut Criterion) { group.bench_function("ML-KEM-1024", |b| { b.iter(|| { for i in 0..NUM_ELEMS { - _ = black_box(MLKEM1024::encaps_internal(&pk, Some(&a_hat), nonces[i])); + _ = black_box(MLKEM1024::encaps_with_randomness(&pk, Some(&a_hat), nonces[i])); } }) }); @@ -251,8 +252,10 @@ fn bench_mlkem_decaps(c: &mut Criterion) { let mut cts = [[0u8; MLKEM512_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // Create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM512::encaps_internal(&pk, None, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice( + &MLKEM512::encaps_with_randomness(&pk, None, [i as u8; MLKEM_RND_LEN]).1, + ); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); @@ -272,8 +275,10 @@ fn bench_mlkem_decaps(c: &mut Criterion) { let mut cts = [[0u8; MLKEM768_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // Create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM768::encaps_internal(&pk, None, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice( + &MLKEM768::encaps_with_randomness(&pk, None, [i as u8; MLKEM_RND_LEN]).1, + ); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); @@ -293,8 +298,10 @@ fn bench_mlkem_decaps(c: &mut Criterion) { let mut cts = [[0u8; MLKEM1024_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // Create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM1024::encaps_internal(&pk, None, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice( + &MLKEM1024::encaps_with_randomness(&pk, None, [i as u8; MLKEM_RND_LEN]).1, + ); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); @@ -337,8 +344,10 @@ fn bench_mlkem_decaps_with_expanded_key(c: &mut Criterion) { let mut cts = [[0u8; MLKEM512_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // Create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM512::encaps_internal(&pk, None, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice( + &MLKEM512::encaps_with_randomness(&pk, None, [i as u8; MLKEM_RND_LEN]).1, + ); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); @@ -359,8 +368,10 @@ fn bench_mlkem_decaps_with_expanded_key(c: &mut Criterion) { let mut cts = [[0u8; MLKEM768_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // Create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM768::encaps_internal(&pk, None, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice( + &MLKEM768::encaps_with_randomness(&pk, None, [i as u8; MLKEM_RND_LEN]).1, + ); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); @@ -381,8 +392,10 @@ fn bench_mlkem_decaps_with_expanded_key(c: &mut Criterion) { let mut cts = [[0u8; MLKEM1024_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // Create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM1024::encaps_internal(&pk, None, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice( + &MLKEM1024::encaps_with_randomness(&pk, None, [i as u8; MLKEM_RND_LEN]).1, + ); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); @@ -424,8 +437,10 @@ fn bench_mlkem_decaps_from_seed(c: &mut Criterion) { let mut cts = [[0u8; MLKEM512_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // Create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM512::encaps_internal(&pk, None, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice( + &MLKEM512::encaps_with_randomness(&pk, None, [i as u8; MLKEM_RND_LEN]).1, + ); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); @@ -445,8 +460,10 @@ fn bench_mlkem_decaps_from_seed(c: &mut Criterion) { let mut cts = [[0u8; MLKEM768_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // Create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM768::encaps_internal(&pk, None, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice( + &MLKEM768::encaps_with_randomness(&pk, None, [i as u8; MLKEM_RND_LEN]).1, + ); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); @@ -466,8 +483,10 @@ fn bench_mlkem_decaps_from_seed(c: &mut Criterion) { let mut cts = [[0u8; MLKEM1024_CT_LEN]; NUM_ELEMS]; for i in 0..NUM_ELEMS { // Create each ct with a unique nonce - // encaps_internal() returns (ss, ct) ... we only want ct, hence the ".1" - cts[i].copy_from_slice(&MLKEM1024::encaps_internal(&pk, None, [i as u8; MLKEM_RND_LEN]).1); + // encaps_with_randomness() returns (ss, ct) ... we only want ct, hence the ".1" + cts[i].copy_from_slice( + &MLKEM1024::encaps_with_randomness(&pk, None, [i as u8; MLKEM_RND_LEN]).1, + ); } group.throughput(criterion::Throughput::Elements(NUM_ELEMS as u64)); 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/hazmat/encaps_with_randomness.rs b/crypto/mlkem/src/hazmat/encaps_with_randomness.rs new file mode 100644 index 00000000..7826ecc2 --- /dev/null +++ b/crypto/mlkem/src/hazmat/encaps_with_randomness.rs @@ -0,0 +1,70 @@ +//! [`EncapsWithRandomness`]: ML-KEM.Encaps_internal with the randomness supplied by the caller. + +use crate::mlkem::{MLKEM, MLKEM_RND_LEN, MLKEM_SS_LEN}; +use crate::mlkem_keys::{ + MLKEMPrivateKeyInternalTrait, MLKEMPrivateKeyTrait, MLKEMPublicKeyInternalTrait, + MLKEMPublicKeyTrait, +}; +use crate::params::MLKEMParams; + +// Imports needed for docs +#[allow(unused_imports)] +use crate::MLKEMPublicKeyExpanded; +#[allow(unused_imports)] +use bouncycastle_core::key_material::KeyMaterial; +#[allow(unused_imports)] +use bouncycastle_core::traits::KEMEncapsulator; +// end of imports needed for docs + +/// FIPS 203 Algorithm 17, ML-KEM.Encaps_internal(ek, m), with `m` supplied by the caller. +/// +/// # 🚨 Security Considerations 🚨 +/// `m` is the encapsulation randomness, the message the underlying PKE encrypts. It must be 32 +/// bytes of fresh, uniformly random, secret data for every call: any deterministic KEM, like any +/// deterministic encryption, fails every indistinguishability notion (IND-CPA, IND-CCA2), and a +/// predictable `m` hands an attacker the shared secret. [`KEMEncapsulator::encaps`] draws `m` from +/// the DRBG and is the function to use; this exists for known-answer tests and for environments +/// that must supply their own randomness. +/// +/// The shared secret comes back as raw bytes rather than wrapped in a [`KeyMaterial`] with its +/// type and security strength set; handling it is up to the caller. +/// +/// A trait rather than an inherent method so that the operation is only reachable with this +/// module's path in scope; see [`bouncycastle_core::hazmat`]. +pub trait EncapsWithRandomness { + /// The expanded public matrix `A_hat`; see [`MLKEMPublicKeyTrait::A_hat`]. + type MatrixA; + + /// Encapsulates to `ek` using `m` as the randomness, returning the shared secret and the + /// ciphertext. + /// + /// `A_hat` is the public matrix expanded from `ek`, as [`MLKEMPublicKeyExpanded`] holds it; + /// pass it when the same key is used for many encapsulations, or `None` to have it computed. + fn encaps_with_randomness( + ek: &PK, + A_hat: Option<&Self::MatrixA>, + m: [u8; MLKEM_RND_LEN], + ) -> ([u8; MLKEM_SS_LEN], [u8; CT_LEN]); +} + +impl< + P: MLKEMParams, + PK: MLKEMPublicKeyTrait + MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, + const PK_LEN: usize, + const SK_LEN: usize, + const CT_LEN: usize, + const SS_LEN: usize, +> EncapsWithRandomness for MLKEM +{ + type MatrixA = P::MatrixA; + + fn encaps_with_randomness( + ek: &PK, + A_hat: Option<&P::MatrixA>, + m: [u8; MLKEM_RND_LEN], + ) -> ([u8; MLKEM_SS_LEN], [u8; CT_LEN]) { + Self::encaps_internal(ek, A_hat, m) + } +} diff --git a/crypto/mlkem/src/hazmat/mod.rs b/crypto/mlkem/src/hazmat/mod.rs new file mode 100644 index 00000000..fbc3face --- /dev/null +++ b/crypto/mlkem/src/hazmat/mod.rs @@ -0,0 +1,10 @@ +//! Raw ML-KEM operations whose safe use is the caller's responsibility; see +//! [`bouncycastle_core::hazmat`] for what the path means and the supported uses. +//! +//! [`EncapsWithRandomness`] takes the encapsulation randomness from the caller; the +//! [`KEMEncapsulator`](bouncycastle_core::traits::KEMEncapsulator) methods draw it from the DRBG +//! and are the ones to use. + +mod encaps_with_randomness; + +pub use encaps_with_randomness::EncapsWithRandomness; diff --git a/crypto/mlkem/src/lib.rs b/crypto/mlkem/src/lib.rs index 5c48828b..e162b2fa 100644 --- a/crypto/mlkem/src/lib.rs +++ b/crypto/mlkem/src/lib.rs @@ -116,16 +116,15 @@ //! All values are in bytes. The "in memory" sizes are measured by rust's `std::mem::size_of`. //! Values in parentheses are the usual sizes in the un-optimized implementation in the \[bouncycastle_mldsa] crate. //! -//! # 🚨 Security 🚨 +//! # 🚨 Security Considerations 🚨 //! -//! All functionality exposed by this crate is considered secure to use. -//! In other words, this crate does not contain any "hazmat" except for the obvious points about -//! handling your private keys properly: if you post your private key to github, or you generate -//! production keys from a weak seed, that use is unsupported -//! It is worth mentioning, however, that if using a [`MLKEM::keygen_from_seed`], then it is your -//! responsibility to ensure that the seed is cryptographically random and unpredictable. -//! And also that [`MLKEM::encaps_internal`] requires you to provide the randomness, so the ciphertext -//! will only be as strong as the randomness that you provide. +//! Everything at the crate root is considered secure to use. The one +//! [hazmat](bouncycastle_core::hazmat) item is [`hazmat::EncapsWithRandomness`], which takes the +//! encapsulation randomness from the caller, so the ciphertext is only as strong as the randomness +//! provided. Beyond that, the obvious points about handling your private keys properly apply: if +//! you post your private key to github, or you generate production keys from a weak seed, that use +//! is unsupported. If using [`MLKEM::keygen_from_seed`], it is your responsibility to ensure that +//! the seed is cryptographically random and unpredictable. //! //! A note about cryptographic side-channel attacks: considerable effort has been expended to attempt //! to make this implementation constant-time, which generally means that the core mathematical algorithm @@ -154,6 +153,7 @@ use bouncycastle_core::key_material::KeyMaterialTrait; mod aux_functions; +pub mod hazmat; mod matrix; pub mod mlkem; mod mlkem_keys; diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index 6490a521..1a8566f6 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -93,19 +93,11 @@ //! //! ## Deterministic encapsulation //! -//! This section pertains to [`MLKEM::encaps_internal`] which allows to pass in the encapsulation randomness -//! and thus obtain a deterministic encapsulation. -//! -//! The only good reasons for doing this are: -//! A) testing, if reproducible results are needed; or -//! B) if the user wants to use their own source of randomness, such as a hardware RNG, instead of the library's -//! default RNG. -//! As a reminder, any deterministic KEM (or any encryption mechanism) fails to satisfy any security -//! notion involving indistinguishability (e.g. IND-CPA, IND-CCA2, etc.). -//! Any custom randomness construction will have serious consequences. -//! Failing to use this properly, as indicated, will result in catastrophic vulnerabilities. +//! [`EncapsWithRandomness`](crate::hazmat::EncapsWithRandomness) takes the encapsulation +//! randomness from the caller; its docs say when that is acceptable and why it is under `hazmat`. //! //! ```rust +//! use bouncycastle_mlkem::hazmat::EncapsWithRandomness; //! use bouncycastle_mlkem::{MLKEM768, MLKEMTrait}; //! use bouncycastle_core::traits::KEMDecapsulator; //! use bouncycastle_core::errors::KEMError; @@ -117,7 +109,7 @@ //! let m: [u8; 32] = [0; 32]; //! //! // Create the shared secret and ciphertext using the public key and the random message `m` -//! let (ss, ct) = MLKEM768::encaps_internal(&pk, None, m); +//! let (ss, ct) = MLKEM768::encaps_with_randomness(&pk, None, m); //! //! // Recover the shared secret using the private key//! //! let ss1 = match MLKEM768::decaps(&sk, &ct) { @@ -146,15 +138,15 @@ use crate::params::{MLKEM512Params, MLKEM768Params, MLKEM1024Params, MLKEMParams use crate::polynomial::Polynomial; use bouncycastle_core::errors::KEMError; use bouncycastle_core::errors::RNGError; -use bouncycastle_core::key_material::{ - KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, SecurityStrength, XOF, + Algorithm, AlgorithmOID, Hash, KEMDecapsulator, KEMEncapsulator, RNG, XOF, XOFSqueezer, }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHA3_256, SHA3_512, SHAKE256}; -use bouncycastle_utils::ct::{conditional_copy_bytes, ct_eq_bytes}; +use bouncycastle_utils::ct::{conditional_copy_bytes, ct_eq_bytes_mask}; use bouncycastle_utils::secret::Secret; use core::marker::PhantomData; /*** Constants ***/ @@ -507,23 +499,10 @@ impl< /// Alternatively, a [`MLKEMPublicKeyExpanded`] with [`MLKEM::encaps_for_expanded_key`] can be used. /// If `None` is specified, the function will compute A_hat internally and everything will work fine. /// - /// Unlike the more public function exposed by [`KEMEncapsulator::encaps`], this returns the shared secret as raw bytes - /// instead of wrapped in an appropriately-set [`KeyMaterialTrait`]. - /// Proper handling is up to the user's own judgement. - /// - /// Note: this is an internal function that allows the caller to specify the encapsulation - /// randomness (which is the message `m` to be encrypted by the underlying PKE scheme). - /// This function should not be used directly unless there is a good reason to do so. - /// [`KEMEncapsulator::encaps`] should be used in 99.9% of cases. - /// The reason this is exposed publicly is: - /// A) for unit testing that requires access to the deterministically reproducible function, and - /// B) for operational environments that wish to provide randomness from their own source instead - /// of the built-in RNG in bc-rust. - /// As a reminder, any deterministic KEM (or any encryption mechanism) fails to satisfy any security - /// notion involving indistinguishability (e.g. IND-CPA, IND-CCA2, etc.). - /// Failing to use this properly will result in catastrophic vulnerabilities. - /// Please don't do it. - pub fn encaps_internal( + /// Reachable from outside the crate only through + /// [`EncapsWithRandomness`](crate::hazmat::EncapsWithRandomness), which carries the security + /// notes on supplying `m`. + pub(crate) fn encaps_internal( ek: &PK, A_hat: Option<&P::MatrixA>, m: [u8; 32], @@ -635,10 +614,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 @@ -658,7 +638,7 @@ impl< // 10: 𝐾′ ← 𝐾_bar // ▷ if ciphertexts do not match, “implicitly reject" let mut K_out = [0u8; MLKEM_SS_LEN]; - conditional_copy_bytes(&K_prime, &K_bar, &mut K_out, ct_eq_bytes(&c, &c_prime)); + conditional_copy_bytes(&K_prime, &K_bar, &mut K_out, ct_eq_bytes_mask(&c, &c_prime)); K_out } diff --git a/crypto/mlkem/src/mlkem_keys.rs b/crypto/mlkem/src/mlkem_keys.rs index 2df7f861..06042c67 100644 --- a/crypto/mlkem/src/mlkem_keys.rs +++ b/crypto/mlkem/src/mlkem_keys.rs @@ -6,7 +6,7 @@ use crate::mlkem::{MLKEM768_PK_LEN, MLKEM768_SK_LEN}; use crate::mlkem::{MLKEM1024_PK_LEN, MLKEM1024_SK_LEN}; use crate::params::{MLKEM512Params, MLKEM768Params, MLKEM1024Params, MLKEMParams}; use bouncycastle_core::errors::KEMError; -use bouncycastle_core::key_material; +use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{Hash, KEMPrivateKey, KEMPublicKey}; use bouncycastle_sha3::SHA3_256; @@ -493,7 +493,7 @@ impl< tmp[32..].copy_from_slice(&*self.z); let mut seed = KeyMaterial::<64>::from_bytes_as_type(&*tmp, KeyType::Seed).unwrap(); - key_material::do_hazardous_operations(&mut seed, |seed| { + do_hazardous_operations(&mut seed, |seed| { seed.set_security_strength(P::MAX_SECURITY_STRENGTH) }) .unwrap(); diff --git a/crypto/mlkem/src/params.rs b/crypto/mlkem/src/params.rs index 2e0df283..33927738 100644 --- a/crypto/mlkem/src/params.rs +++ b/crypto/mlkem/src/params.rs @@ -10,7 +10,7 @@ use crate::matrix::{Matrix, MatrixTrait, Vector, VectorTrait}; use crate::mlkem::{ML_KEM_512_NAME, ML_KEM_768_NAME, ML_KEM_1024_NAME, MLKEM_SS_LEN}; -use bouncycastle_core::traits::SecurityStrength; +use bouncycastle_core::security_strength::SecurityStrength; /// A crate-private (aka "sealed") trait that prevents a new ML-KEM parameter set from being defined /// outside this crate. diff --git a/crypto/mlkem/src/polynomial.rs b/crypto/mlkem/src/polynomial.rs index 5fdd6672..8270f011 100644 --- a/crypto/mlkem/src/polynomial.rs +++ b/crypto/mlkem/src/polynomial.rs @@ -10,7 +10,7 @@ use crate::params::MLKEMParams; /// A polynomial over the ML-KEM ring. /// -/// # 🚨 Security 🚨 +/// # 🚨 Security Considerations 🚨 /// Polynomials themselves are not inherently secret since sometimes they are part of public keys /// and sometimes private keys. /// It is the responsibility of the caller to wrap sensitive instances in `Secret`. diff --git a/crypto/mlkem/tests/bc_test_data.rs b/crypto/mlkem/tests/bc_test_data.rs deleted file mode 100644 index bfb0a640..00000000 --- a/crypto/mlkem/tests/bc_test_data.rs +++ /dev/null @@ -1,387 +0,0 @@ -// 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. - -#[cfg(test)] -mod bc_test_data { - use bouncycastle_core::key_material; - use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; - use bouncycastle_core::traits::{ - KEMDecapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, - }; - use bouncycastle_hex as hex; - use bouncycastle_mlkem::{ - MLKEM512, MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM512PrivateKey, MLKEM512PublicKey, - MLKEM768, MLKEM768_PK_LEN, MLKEM768_SK_LEN, MLKEM768PrivateKey, MLKEM768PublicKey, - MLKEM1024, MLKEM1024_PK_LEN, MLKEM1024_SK_LEN, MLKEM1024PrivateKey, MLKEM1024PublicKey, - MLKEMTrait, - }; - use std::fs; - use std::path::Path; - use std::sync::Once; - - const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/pqc/crypto/mlkem"; - const TEST_DATA_PATH: &str = "../bc-test-data/pqc/crypto/mlkem"; - - 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!("wycheproof found at: {:?}", TEST_DATA_PATH_RELATIVE), - 2 => println!("wycheproof found at: {:?}", TEST_DATA_PATH), - _ => println!("WARNING: wycheproof directory not found; tests will be skipped"), - }); - - if !found == 3 { - return Err(()); - } - - 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) - } - #[test] - #[allow(non_snake_case)] - fn ML_KEM_keyGen() { - let contents = match get_test_data("ML-KEM-keyGen.txt") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = KeyGenTestCase::parse(contents); - - for test_case in test_cases { - test_case.run(); - } - } - - #[derive(Clone)] - struct KeyGenTestCase { - vs_id: u32, - algorithm: String, - mode: String, - revision: String, - is_sample: bool, - tg_id: u32, - test_type: String, - parameter_set: String, - tc_id: u32, - z: String, - d: String, - ek: String, - dk: String, - } - - impl KeyGenTestCase { - fn new() -> Self { - Self { - vs_id: 0, - algorithm: String::new(), - mode: String::new(), - revision: String::new(), - is_sample: false, - tg_id: 0, - test_type: String::new(), - parameter_set: String::new(), - tc_id: 0, - z: String::new(), - d: String::new(), - ek: String::new(), - dk: String::new(), - } - } - - fn is_full(&self) -> bool { - !self.algorithm.is_empty() - } - - fn parse(data: String) -> Vec { - let mut test_cases = Vec::::new(); - let mut test_case = KeyGenTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "vsId" => test_case.vs_id = value.parse().unwrap(), - "algorithm" => test_case.algorithm = value.to_string(), - "mode" => test_case.mode = value.to_string(), - "revision" => test_case.revision = value.to_string(), - "isSample" => test_case.is_sample = value.parse().unwrap(), - "tgId" => test_case.tg_id = value.parse().unwrap(), - "testType" => test_case.test_type = value.to_string(), - "parameterSet" => test_case.parameter_set = value.to_string(), - "tcId" => test_case.tc_id = value.parse().unwrap(), - "z" => test_case.z = value.to_string(), - "d" => test_case.d = value.to_string(), - "ek" => test_case.ek = value.to_string(), - "dk" => test_case.dk = value.to_string(), - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self) { - assert_eq!(self.mode, "keyGen"); - - let mut seed_bytes = [0u8; 64]; - seed_bytes[..32].copy_from_slice(&*hex::decode(&self.d).unwrap()); - seed_bytes[32..].copy_from_slice(&*hex::decode(&self.z).unwrap()); - - let mut seed = KeyMaterial512::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); - - // for the purposes of the test cases, accept an all-zero seed - key_material::do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::Seed)?; - seed.set_security_strength(SecurityStrength::_256bit) - }) - .unwrap(); - - match self.parameter_set.as_str() { - "ML-KEM-512" => { - let (pk, sk) = MLKEM512::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLKEM512_PK_LEN] = - hex::decode(&self.ek).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLKEM512_SK_LEN] = - hex::decode(&self.dk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - } - "ML-KEM-768" => { - let (pk, sk) = MLKEM768::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLKEM768_PK_LEN] = - hex::decode(&self.ek).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLKEM768_SK_LEN] = - hex::decode(&self.dk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - } - "ML-KEM-1024" => { - let (pk, sk) = MLKEM1024::keygen_from_seed(&seed).unwrap(); - let pk_sized: [u8; MLKEM1024_PK_LEN] = - hex::decode(&self.ek).unwrap().try_into().unwrap(); - assert_eq!(pk.encode(), pk_sized); - let sk_sized: [u8; MLKEM1024_SK_LEN] = - hex::decode(&self.dk).unwrap().try_into().unwrap(); - assert_eq!(sk.encode(), sk_sized); - } - val => panic!("Invalid parameter set: {}", val), - } - } - } - - #[test] - #[allow(non_snake_case)] - fn ML_KEM_encapDecap() { - let contents = match get_test_data("ML-KEM-encapDecap.txt") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = EncapDecapTestCase::parse(contents); - - let num_tests = test_cases.len(); - for test_case in test_cases { - test_case.run(); - } - - println!("SUCCESS! ML-DSA-sigGen test cases passed: {}!", num_tests); - } - - #[derive(Clone)] - struct EncapDecapTestCase { - vs_id: u32, - algorithm: String, - mode: String, - revision: String, - is_sample: bool, - tg_id: u32, - test_type: String, - parameter_set: String, - function: String, - tc_id: u32, - ek: String, - dk: String, - m: String, - c: String, - k: String, - } - - impl EncapDecapTestCase { - fn new() -> Self { - Self { - vs_id: 0, - algorithm: String::new(), - mode: String::new(), - revision: String::new(), - is_sample: false, - tg_id: 0, - test_type: String::new(), - parameter_set: String::new(), - function: String::new(), - tc_id: 0, - ek: String::new(), - dk: String::new(), - m: String::new(), - c: String::new(), - k: String::new(), - } - } - - fn is_full(&self) -> bool { - !self.algorithm.is_empty() - } - - fn parse(data: String) -> Vec { - let mut test_cases = Vec::::new(); - let mut test_case = EncapDecapTestCase::new(); - for line in data.lines() { - let (tag, value) = match line.split_once(" = ") { - Some(pair) => pair, - None => { - if test_case.is_full() { - test_cases.push(test_case.clone()); - } - continue; - } - }; - - match tag { - "vsId" => test_case.vs_id = value.parse().unwrap(), - "algorithm" => test_case.algorithm = value.to_string(), - "mode" => test_case.mode = value.to_string(), - "revision" => test_case.revision = value.to_string(), - "isSample" => test_case.is_sample = value.parse().unwrap(), - "tgId" => test_case.tg_id = value.parse().unwrap(), - "testType" => test_case.test_type = value.to_string(), - "parameterSet" => test_case.parameter_set = value.to_string(), - "function" => test_case.function = value.to_string(), - "tcId" => test_case.tc_id = value.parse().unwrap(), - "ek" => test_case.ek = value.to_string(), - "dk" => test_case.dk = value.to_string(), - "m" => test_case.m = value.to_string(), - "c" => test_case.c = value.to_string(), - "k" => test_case.k = value.to_string(), - val => panic!("Invalid tag: {}", val), - } - } - - test_cases - } - - fn run(&self) { - assert_eq!(self.mode, "encapDecap"); - - match self.parameter_set.as_str() { - "ML-KEM-512" => { - match self.function.as_str() { - "encapsulation" => { - let pk = MLKEM512PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()) - .unwrap(); - let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); - let (ss, ct) = MLKEM512::encaps_internal(&pk, None, m); - - let expected_ss = hex::decode(&self.k).unwrap(); - let expected_ct = hex::decode(&self.c).unwrap(); - - assert_eq!(ss, expected_ss.as_slice()); - assert_eq!(ct, expected_ct.as_slice()); - } - "decapsulation" => { - let sk = - MLKEM512PrivateKey::from_bytes(&hex::decode(&self.dk).unwrap()) - .unwrap(); - let ct = hex::decode(&self.c).unwrap(); - let ss = MLKEM512::decaps(&sk, ct.as_slice()).unwrap(); - - let expected_ss = hex::decode(&self.k).unwrap(); - assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); - } - _ => panic!("Invalid function: {}", self.function), - }; - } - "ML-KEM-768" => { - match self.function.as_str() { - "encapsulation" => { - let pk = MLKEM768PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()) - .unwrap(); - let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); - let (ss, ct) = MLKEM768::encaps_internal(&pk, None, m); - - let expected_ss = hex::decode(&self.k).unwrap(); - let expected_ct = hex::decode(&self.c).unwrap(); - - assert_eq!(ss, expected_ss.as_slice()); - assert_eq!(ct, expected_ct.as_slice()); - } - "decapsulation" => { - let sk = - MLKEM768PrivateKey::from_bytes(&hex::decode(&self.dk).unwrap()) - .unwrap(); - let ct = hex::decode(&self.c).unwrap(); - let ss = MLKEM768::decaps(&sk, ct.as_slice()).unwrap(); - - let expected_ss = hex::decode(&self.k).unwrap(); - assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); - } - _ => panic!("Invalid function: {}", self.function), - }; - } - "ML-KEM-1024" => { - match self.function.as_str() { - "encapsulation" => { - let pk = - MLKEM1024PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()) - .unwrap(); - let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); - let (ss, ct) = MLKEM1024::encaps_internal(&pk, None, m); - - let expected_ss = hex::decode(&self.k).unwrap(); - let expected_ct = hex::decode(&self.c).unwrap(); - - assert_eq!(ss, expected_ss.as_slice()); - assert_eq!(ct, expected_ct.as_slice()); - } - "decapsulation" => { - let sk = - MLKEM1024PrivateKey::from_bytes(&hex::decode(&self.dk).unwrap()) - .unwrap(); - let ct = hex::decode(&self.c).unwrap(); - let ss = MLKEM1024::decaps(&sk, ct.as_slice()).unwrap(); - - let expected_ss = hex::decode(&self.k).unwrap(); - assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); - } - _ => panic!("Invalid function: {}", self.function), - }; - } - val => panic!("Invalid parameter set: {}", val), - } - } - } -} diff --git a/crypto/mlkem/tests/mlkem_bc-test-data.rs b/crypto/mlkem/tests/mlkem_bc-test-data.rs new file mode 100644 index 00000000..53f4430a --- /dev/null +++ b/crypto/mlkem/tests/mlkem_bc-test-data.rs @@ -0,0 +1,340 @@ +//! Known-answer tests for ML-KEM-512/768/1024 against `ML-KEM-keyGen.txt` and +//! `ML-KEM-encapDecap.txt`. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data", under `pqc/crypto/mlkem/`. If it is not +//! present the tests print a warning and pass vacuously. + +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{KEMDecapsulator, KEMPrivateKey, KEMPublicKey}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_mlkem::hazmat::EncapsWithRandomness; +use bouncycastle_mlkem::{ + MLKEM512, MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM512PrivateKey, MLKEM512PublicKey, MLKEM768, + MLKEM768_PK_LEN, MLKEM768_SK_LEN, MLKEM768PrivateKey, MLKEM768PublicKey, MLKEM1024, + MLKEM1024_PK_LEN, MLKEM1024_SK_LEN, MLKEM1024PrivateKey, MLKEM1024PublicKey, MLKEMTrait, +}; + +const TEST_DATA_DIR: &str = "pqc/crypto/mlkem"; + +#[test] +#[allow(non_snake_case)] +fn ML_KEM_keyGen() { + let Some(contents) = bc_test_data(TEST_DATA_DIR, "ML-KEM-keyGen.txt") else { return }; + + let test_cases = KeyGenTestCase::parse(contents); + + for test_case in test_cases { + test_case.run(); + } +} + +#[derive(Clone)] +struct KeyGenTestCase { + vs_id: u32, + algorithm: String, + mode: String, + revision: String, + is_sample: bool, + tg_id: u32, + test_type: String, + parameter_set: String, + tc_id: u32, + z: String, + d: String, + ek: String, + dk: String, +} + +impl KeyGenTestCase { + fn new() -> Self { + Self { + vs_id: 0, + algorithm: String::new(), + mode: String::new(), + revision: String::new(), + is_sample: false, + tg_id: 0, + test_type: String::new(), + parameter_set: String::new(), + tc_id: 0, + z: String::new(), + d: String::new(), + ek: String::new(), + dk: String::new(), + } + } + + fn is_full(&self) -> bool { + !self.algorithm.is_empty() + } + + fn parse(data: String) -> Vec { + let mut test_cases = Vec::::new(); + let mut test_case = KeyGenTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "vsId" => test_case.vs_id = value.parse().unwrap(), + "algorithm" => test_case.algorithm = value.to_string(), + "mode" => test_case.mode = value.to_string(), + "revision" => test_case.revision = value.to_string(), + "isSample" => test_case.is_sample = value.parse().unwrap(), + "tgId" => test_case.tg_id = value.parse().unwrap(), + "testType" => test_case.test_type = value.to_string(), + "parameterSet" => test_case.parameter_set = value.to_string(), + "tcId" => test_case.tc_id = value.parse().unwrap(), + "z" => test_case.z = value.to_string(), + "d" => test_case.d = value.to_string(), + "ek" => test_case.ek = value.to_string(), + "dk" => test_case.dk = value.to_string(), + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + fn run(&self) { + assert_eq!(self.mode, "keyGen"); + + let mut seed_bytes = [0u8; 64]; + seed_bytes[..32].copy_from_slice(&*hex::decode(&self.d).unwrap()); + seed_bytes[32..].copy_from_slice(&*hex::decode(&self.z).unwrap()); + + let mut seed = KeyMaterial512::from_bytes_as_type(&seed_bytes, KeyType::Seed).unwrap(); + + // for the purposes of the test cases, accept an all-zero seed + do_hazardous_operations(&mut seed, |seed| { + seed.set_key_type(KeyType::Seed)?; + seed.set_security_strength(SecurityStrength::_256bit) + }) + .unwrap(); + + match self.parameter_set.as_str() { + "ML-KEM-512" => { + let (pk, sk) = MLKEM512::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLKEM512_PK_LEN] = + hex::decode(&self.ek).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLKEM512_SK_LEN] = + hex::decode(&self.dk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + } + "ML-KEM-768" => { + let (pk, sk) = MLKEM768::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLKEM768_PK_LEN] = + hex::decode(&self.ek).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLKEM768_SK_LEN] = + hex::decode(&self.dk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + } + "ML-KEM-1024" => { + let (pk, sk) = MLKEM1024::keygen_from_seed(&seed).unwrap(); + let pk_sized: [u8; MLKEM1024_PK_LEN] = + hex::decode(&self.ek).unwrap().try_into().unwrap(); + assert_eq!(pk.encode(), pk_sized); + let sk_sized: [u8; MLKEM1024_SK_LEN] = + hex::decode(&self.dk).unwrap().try_into().unwrap(); + assert_eq!(sk.encode(), sk_sized); + } + val => panic!("Invalid parameter set: {}", val), + } + } +} + +#[test] +#[allow(non_snake_case)] +fn ML_KEM_encapDecap() { + let Some(contents) = bc_test_data(TEST_DATA_DIR, "ML-KEM-encapDecap.txt") else { return }; + + let test_cases = EncapDecapTestCase::parse(contents); + + let num_tests = test_cases.len(); + for test_case in test_cases { + test_case.run(); + } + + println!("SUCCESS! ML-KEM-encapDecap test cases passed: {}!", num_tests); +} + +#[derive(Clone)] +struct EncapDecapTestCase { + vs_id: u32, + algorithm: String, + mode: String, + revision: String, + is_sample: bool, + tg_id: u32, + test_type: String, + parameter_set: String, + function: String, + tc_id: u32, + ek: String, + dk: String, + m: String, + c: String, + k: String, +} + +impl EncapDecapTestCase { + fn new() -> Self { + Self { + vs_id: 0, + algorithm: String::new(), + mode: String::new(), + revision: String::new(), + is_sample: false, + tg_id: 0, + test_type: String::new(), + parameter_set: String::new(), + function: String::new(), + tc_id: 0, + ek: String::new(), + dk: String::new(), + m: String::new(), + c: String::new(), + k: String::new(), + } + } + + fn is_full(&self) -> bool { + !self.algorithm.is_empty() + } + + fn parse(data: String) -> Vec { + let mut test_cases = Vec::::new(); + let mut test_case = EncapDecapTestCase::new(); + for line in data.lines() { + let (tag, value) = match line.split_once(" = ") { + Some(pair) => pair, + None => { + if test_case.is_full() { + test_cases.push(test_case.clone()); + } + continue; + } + }; + + match tag { + "vsId" => test_case.vs_id = value.parse().unwrap(), + "algorithm" => test_case.algorithm = value.to_string(), + "mode" => test_case.mode = value.to_string(), + "revision" => test_case.revision = value.to_string(), + "isSample" => test_case.is_sample = value.parse().unwrap(), + "tgId" => test_case.tg_id = value.parse().unwrap(), + "testType" => test_case.test_type = value.to_string(), + "parameterSet" => test_case.parameter_set = value.to_string(), + "function" => test_case.function = value.to_string(), + "tcId" => test_case.tc_id = value.parse().unwrap(), + "ek" => test_case.ek = value.to_string(), + "dk" => test_case.dk = value.to_string(), + "m" => test_case.m = value.to_string(), + "c" => test_case.c = value.to_string(), + "k" => test_case.k = value.to_string(), + val => panic!("Invalid tag: {}", val), + } + } + + test_cases + } + + fn run(&self) { + assert_eq!(self.mode, "encapDecap"); + + match self.parameter_set.as_str() { + "ML-KEM-512" => { + match self.function.as_str() { + "encapsulation" => { + let pk = + MLKEM512PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()).unwrap(); + let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); + let (ss, ct) = MLKEM512::encaps_with_randomness(&pk, None, m); + + let expected_ss = hex::decode(&self.k).unwrap(); + let expected_ct = hex::decode(&self.c).unwrap(); + + assert_eq!(ss, expected_ss.as_slice()); + assert_eq!(ct, expected_ct.as_slice()); + } + "decapsulation" => { + let sk = MLKEM512PrivateKey::from_bytes(&hex::decode(&self.dk).unwrap()) + .unwrap(); + let ct = hex::decode(&self.c).unwrap(); + let ss = MLKEM512::decaps(&sk, ct.as_slice()).unwrap(); + + let expected_ss = hex::decode(&self.k).unwrap(); + assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); + } + _ => panic!("Invalid function: {}", self.function), + }; + } + "ML-KEM-768" => { + match self.function.as_str() { + "encapsulation" => { + let pk = + MLKEM768PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()).unwrap(); + let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); + let (ss, ct) = MLKEM768::encaps_with_randomness(&pk, None, m); + + let expected_ss = hex::decode(&self.k).unwrap(); + let expected_ct = hex::decode(&self.c).unwrap(); + + assert_eq!(ss, expected_ss.as_slice()); + assert_eq!(ct, expected_ct.as_slice()); + } + "decapsulation" => { + let sk = MLKEM768PrivateKey::from_bytes(&hex::decode(&self.dk).unwrap()) + .unwrap(); + let ct = hex::decode(&self.c).unwrap(); + let ss = MLKEM768::decaps(&sk, ct.as_slice()).unwrap(); + + let expected_ss = hex::decode(&self.k).unwrap(); + assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); + } + _ => panic!("Invalid function: {}", self.function), + }; + } + "ML-KEM-1024" => { + match self.function.as_str() { + "encapsulation" => { + let pk = MLKEM1024PublicKey::from_bytes(&hex::decode(&self.ek).unwrap()) + .unwrap(); + let m: [u8; 32] = hex::decode(&self.m).unwrap().try_into().unwrap(); + let (ss, ct) = MLKEM1024::encaps_with_randomness(&pk, None, m); + + let expected_ss = hex::decode(&self.k).unwrap(); + let expected_ct = hex::decode(&self.c).unwrap(); + + assert_eq!(ss, expected_ss.as_slice()); + assert_eq!(ct, expected_ct.as_slice()); + } + "decapsulation" => { + let sk = MLKEM1024PrivateKey::from_bytes(&hex::decode(&self.dk).unwrap()) + .unwrap(); + let ct = hex::decode(&self.c).unwrap(); + let ss = MLKEM1024::decaps(&sk, ct.as_slice()).unwrap(); + + let expected_ss = hex::decode(&self.k).unwrap(); + assert_eq!(ss.ref_to_bytes(), expected_ss.as_slice()); + } + _ => panic!("Invalid function: {}", self.function), + }; + } + val => panic!("Invalid parameter set: {}", val), + } + } +} diff --git a/crypto/mlkem/tests/mlkem_key_tests.rs b/crypto/mlkem/tests/mlkem_key_tests.rs index 24930d20..65c604e2 100644 --- a/crypto/mlkem/tests/mlkem_key_tests.rs +++ b/crypto/mlkem/tests/mlkem_key_tests.rs @@ -2,7 +2,8 @@ mod mlkem_key_tests { use bouncycastle_core::errors::KEMError; use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; - use bouncycastle_core::traits::{KEMPrivateKey, KEMPublicKey, SecurityStrength}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{KEMPrivateKey, KEMPublicKey}; use bouncycastle_hex as hex; use bouncycastle_mlkem::{MLKEM512, MLKEM768, MLKEM1024}; use bouncycastle_mlkem::{ diff --git a/crypto/mlkem/tests/mlkem_tests.rs b/crypto/mlkem/tests/mlkem_tests.rs index 4faf8498..c497c715 100644 --- a/crypto/mlkem/tests/mlkem_tests.rs +++ b/crypto/mlkem/tests/mlkem_tests.rs @@ -2,13 +2,15 @@ #[cfg(test)] mod mlkem_tests { use bouncycastle_core::errors::{KEMError, RNGError}; - use bouncycastle_core::key_material; + use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; + use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_core::traits::{ - KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength, XOF, + Hash, KEMDecapsulator, KEMEncapsulator, KEMPrivateKey, KEMPublicKey, XOF, XOFSqueezer, }; use bouncycastle_core_test_framework::FixedSeedRNG; use bouncycastle_hex as hex; + use bouncycastle_mlkem::hazmat::EncapsWithRandomness; use bouncycastle_mlkem::{MLKEM_RND_LEN, MLKEM512, MLKEM768, MLKEM1024}; use bouncycastle_mlkem::{ MLKEM_SS_LEN, MLKEM512_CT_LEN, MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM768_CT_LEN, @@ -240,7 +242,7 @@ mod mlkem_tests { let expected_ciphertext: [u8; MLKEM1024_CT_LEN] = hex::decode("8B9FE419250C5FB0463C8181FCF7CEC777136B738E015EBA31067AA4A8C378BBAC0121B88214F1AEB866E4F33C277099E09B4BF7E21CDDA30B5B32C18B0E9660C30601D85DAEC07AAF4B343EC5516FA501DD63088B999FB9A414C6CA593806C08CD4C775139BF0F0BF3676D773EDD56E616A13830D5F5FE35E515DBC84E43AAD0167D57E60A9DE30886ACD3F7F2006CAC26A7A07B4DADBEDFBED7F305764386AAD726D5B2BF14A376BAD8B4896688491733FB34E6EDEA10BFD5E448541CB6E69E3D87DF190AFA7FF62577775BAACEA444A6128A20200251D8FA759DC60FDA6A9730CFFE4997FE7EBCDD1644AE2D55290A4074CDD2CE53C18D22BC33671E68727A9B5A2FEAFB114A8045D96A56981E200A09661375987625ACC233EDE817AF1DEEAA21C7C4377423E73C5AF9BFF58A49DE6DAFD07A3E3BABD891F62BBA41D1856B8BC502CC86EE115A3598431E2B54AB0C5EACC3CE6A03090925C1FD5A251B00576763A963994A7A23EE12EBFC1B994F93C6144178F0BEF88245CE77CD32EF651826A6090AF561A5864DEC2A51D846F1F48F88B4B55F58C2373E0F67BDC95DC23A43E8546232A7B234E49F5226A3A63BDBCED7240FC81C2DB68AAEB2671A2FD231997BF8839C63A7F41F15E7242821D42E80BBC0F43FA9E353DE8B25ED8FFC242EB512C6A5260919AAE89A11176532BCCC762A520A37AEC4E7209AA81CEE0DD4ADD932C47EB8100BE98AA1DEEA9EA698115ADCED950A6C536D19AEB325CEA8C5245C0A2281533FB90809DC2BE90567EBE6AE229FE09B44DA2182585EA694D8A9AB33EBC24B44E09BD510F34B4140E1FB41162F9415F2D9106A0CEA00A26ED0920021F4E5BCFB3DABF5850DAB22B2E889D9611FBE06D0C899708EB5E5FAD2FBBE0D5C0BDE080F8E760EDFA037D55DA77F0F39591BF5B050C905FA538B7228E238A290DF340778DCBD6BE40A3B1DD455FB27ADBE176AEF6CC295BEA570BDC221BA14002E3B113B0EF237452FBC9F1AEC42E0D2B33F19832DB0A6171CAEB0B30EEAD3A54B704B761C7D4AFEA8F6AFC15156666A081C43AEB2E04FEECEF8AABA4049BD78B120B9ABA86A60342A0CF806411C473C26C4BE1540E3312388BCBC8523BA73F40EA28D5564274F3661D7ACAA0F1E8D0F28DCF6B501329963E6857FDB2AAE873A7D9D6C14821F6C0B6AA50AC449075CD6F2A256C5A05959DAB5A5912CC8E8F8B9F59941BFCCE6A28CBA74A20382B1FD3382D056547D5BC5EF4AAE62F96F038C595A4F901D6AE790F8978292AD1CC3A1E800B71A5BBE84533646655E3752FBD6B02B97B204E75D28A34C2F990FB8E8CD31CE6E683FA7E67DA03367E8D47DC626F060FBA2D0425004CAC2A61D982D2E3D85008624B45DB022CF51BA265B5E974712A9372EECAC0EA272B2FC56EBED0D32105521BA2C4A8FE0C678CE4E45902C7BA9D510BD47B2B5F931DD732F27DE9B42FD4AA39EAC765283A9965EE97C0D88E23EFA6F718242C67770B87BF8832858C1D13FC520870BD34F2B9C6FBFD1A528B744F814C93F4F4E87108316FE2AB06E02292DEA7FCF6FEFB17BF5AA7376A4A9BDB7C49BF709EB1E05D60EF14CD85A75239B97BCA9A6A3CC1B28F28979D612431BAAC1ACEE5EF62776B4D51B7EB0F63DF507760097223CA903E16E02DEB7FCABFBEC26DAEDC0ED4CC55726BDC31D1775112EF3C35D1DF928C6EB7830D8CA6570CB5CE348E3F26DDE864F20E5BE7B99E264EBC0E9D8DE9C6E4B7FE3CFBE673833CF7E8B3081529062CB6815C7C0766822B3B31E56BA1FC73FE3DED4B5D435BFCE2F2997C1D4B9CE293220DD461103BE084BF12076372668A69836769C1F6D8C32E2C7BC2E7D66714C814793A2970C90DD94DF14C89C60DD35B52A14778E137E750CE83AC3AAB667FCBDCBA38B7FA6D1C6BF7B99D957078176D9779A09F84B75FBC2A11769EF65532B09ACA4C9A3766B4A1FC717F94648FB8B8D9363E54F1C4201C075C18B1EAE098B83598089585ED9DC06B96E2D1C96DC738086EBBC26C3193B64139E1FC1DFB22A17893506EF7B35792B4EB00196693686EB5DEB3CEB436DD16D2D92A0FD31F468AF8662040F5257BFA0F14991C0D560999EEF775178D14955ADF091DD797AC1FDCEC7776055271C0F130562D0B0A6749B159DD0DB9AC69271AC719B83B683CE8B32342AC4AB257B0F8083C8CC86338AFA4D386C9848F413ED0").unwrap().try_into().unwrap(); // encaps - let (ss, ct) = MLKEM1024::encaps_internal(&pk, None, message); + let (ss, ct) = MLKEM1024::encaps_with_randomness(&pk, None, message); assert_eq!(ss, expected_shared_secret); assert_eq!(ct, expected_ciphertext); @@ -284,7 +286,7 @@ mod mlkem_tests { let expected_ciphertext: [u8; MLKEM1024_CT_LEN] = hex::decode("8B9FE419250C5FB0463C8181FCF7CEC777136B738E015EBA31067AA4A8C378BBAC0121B88214F1AEB866E4F33C277099E09B4BF7E21CDDA30B5B32C18B0E9660C30601D85DAEC07AAF4B343EC5516FA501DD63088B999FB9A414C6CA593806C08CD4C775139BF0F0BF3676D773EDD56E616A13830D5F5FE35E515DBC84E43AAD0167D57E60A9DE30886ACD3F7F2006CAC26A7A07B4DADBEDFBED7F305764386AAD726D5B2BF14A376BAD8B4896688491733FB34E6EDEA10BFD5E448541CB6E69E3D87DF190AFA7FF62577775BAACEA444A6128A20200251D8FA759DC60FDA6A9730CFFE4997FE7EBCDD1644AE2D55290A4074CDD2CE53C18D22BC33671E68727A9B5A2FEAFB114A8045D96A56981E200A09661375987625ACC233EDE817AF1DEEAA21C7C4377423E73C5AF9BFF58A49DE6DAFD07A3E3BABD891F62BBA41D1856B8BC502CC86EE115A3598431E2B54AB0C5EACC3CE6A03090925C1FD5A251B00576763A963994A7A23EE12EBFC1B994F93C6144178F0BEF88245CE77CD32EF651826A6090AF561A5864DEC2A51D846F1F48F88B4B55F58C2373E0F67BDC95DC23A43E8546232A7B234E49F5226A3A63BDBCED7240FC81C2DB68AAEB2671A2FD231997BF8839C63A7F41F15E7242821D42E80BBC0F43FA9E353DE8B25ED8FFC242EB512C6A5260919AAE89A11176532BCCC762A520A37AEC4E7209AA81CEE0DD4ADD932C47EB8100BE98AA1DEEA9EA698115ADCED950A6C536D19AEB325CEA8C5245C0A2281533FB90809DC2BE90567EBE6AE229FE09B44DA2182585EA694D8A9AB33EBC24B44E09BD510F34B4140E1FB41162F9415F2D9106A0CEA00A26ED0920021F4E5BCFB3DABF5850DAB22B2E889D9611FBE06D0C899708EB5E5FAD2FBBE0D5C0BDE080F8E760EDFA037D55DA77F0F39591BF5B050C905FA538B7228E238A290DF340778DCBD6BE40A3B1DD455FB27ADBE176AEF6CC295BEA570BDC221BA14002E3B113B0EF237452FBC9F1AEC42E0D2B33F19832DB0A6171CAEB0B30EEAD3A54B704B761C7D4AFEA8F6AFC15156666A081C43AEB2E04FEECEF8AABA4049BD78B120B9ABA86A60342A0CF806411C473C26C4BE1540E3312388BCBC8523BA73F40EA28D5564274F3661D7ACAA0F1E8D0F28DCF6B501329963E6857FDB2AAE873A7D9D6C14821F6C0B6AA50AC449075CD6F2A256C5A05959DAB5A5912CC8E8F8B9F59941BFCCE6A28CBA74A20382B1FD3382D056547D5BC5EF4AAE62F96F038C595A4F901D6AE790F8978292AD1CC3A1E800B71A5BBE84533646655E3752FBD6B02B97B204E75D28A34C2F990FB8E8CD31CE6E683FA7E67DA03367E8D47DC626F060FBA2D0425004CAC2A61D982D2E3D85008624B45DB022CF51BA265B5E974712A9372EECAC0EA272B2FC56EBED0D32105521BA2C4A8FE0C678CE4E45902C7BA9D510BD47B2B5F931DD732F27DE9B42FD4AA39EAC765283A9965EE97C0D88E23EFA6F718242C67770B87BF8832858C1D13FC520870BD34F2B9C6FBFD1A528B744F814C93F4F4E87108316FE2AB06E02292DEA7FCF6FEFB17BF5AA7376A4A9BDB7C49BF709EB1E05D60EF14CD85A75239B97BCA9A6A3CC1B28F28979D612431BAAC1ACEE5EF62776B4D51B7EB0F63DF507760097223CA903E16E02DEB7FCABFBEC26DAEDC0ED4CC55726BDC31D1775112EF3C35D1DF928C6EB7830D8CA6570CB5CE348E3F26DDE864F20E5BE7B99E264EBC0E9D8DE9C6E4B7FE3CFBE673833CF7E8B3081529062CB6815C7C0766822B3B31E56BA1FC73FE3DED4B5D435BFCE2F2997C1D4B9CE293220DD461103BE084BF12076372668A69836769C1F6D8C32E2C7BC2E7D66714C814793A2970C90DD94DF14C89C60DD35B52A14778E137E750CE83AC3AAB667FCBDCBA38B7FA6D1C6BF7B99D957078176D9779A09F84B75FBC2A11769EF65532B09ACA4C9A3766B4A1FC717F94648FB8B8D9363E54F1C4201C075C18B1EAE098B83598089585ED9DC06B96E2D1C96DC738086EBBC26C3193B64139E1FC1DFB22A17893506EF7B35792B4EB00196693686EB5DEB3CEB436DD16D2D92A0FD31F468AF8662040F5257BFA0F14991C0D560999EEF775178D14955ADF091DD797AC1FDCEC7776055271C0F130562D0B0A6749B159DD0DB9AC69271AC719B83B683CE8B32342AC4AB257B0F8083C8CC86338AFA4D386C9848F413ED0").unwrap().try_into().unwrap(); // encaps - let (ss, ct) = MLKEM1024::encaps_internal(&pk, None, message); + let (ss, ct) = MLKEM1024::encaps_with_randomness(&pk, None, message); assert_eq!(ss, expected_shared_secret); assert_eq!(ct, expected_ciphertext); @@ -324,10 +326,8 @@ mod mlkem_tests { assert_eq!(derived_pk.encode(), expected_pk_bytes.as_slice()); // success case KeyType: BytesFullEntropy - key_material::do_hazardous_operations(&mut seed, |seed| { - seed.set_key_type(KeyType::CryptographicRandom) - }) - .unwrap(); + do_hazardous_operations(&mut seed, |seed| seed.set_key_type(KeyType::CryptographicRandom)) + .unwrap(); _ = MLKEM512::keygen_from_seed(&seed).unwrap(); @@ -469,12 +469,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); } @@ -740,41 +739,41 @@ mod mlkem_tests { // ML-KEM-512 let (pk512, _sk) = MLKEM512::keygen().unwrap(); - let (ss_ref, ct_ref) = MLKEM512::encaps_internal(&pk512, None, m); + let (ss_ref, ct_ref) = MLKEM512::encaps_with_randomness(&pk512, None, m); let pk_expanded = MLKEM512PublicKeyExpanded::from(&pk512); let mut rng = FixedSeedRNG::new(seed_bytes); let (ss, ct) = MLKEM512::encaps_for_expanded_key_rng(&pk_expanded, &mut rng).unwrap(); - assert_eq!(ct, ct_ref, "ML-KEM-512 ciphertext must match encaps_internal"); + assert_eq!(ct, ct_ref, "ML-KEM-512 ciphertext must match encaps_with_randomness"); assert_eq!( ss_ref, ss.ref_to_bytes(), - "ML-KEM-512 shared secret must match encaps_internal" + "ML-KEM-512 shared secret must match encaps_with_randomness" ); // ML-KEM-768 let (pk768, _sk) = MLKEM768::keygen().unwrap(); - let (ss_ref, ct_ref) = MLKEM768::encaps_internal(&pk768, None, m); + let (ss_ref, ct_ref) = MLKEM768::encaps_with_randomness(&pk768, None, m); let pk_expanded = MLKEM768PublicKeyExpanded::from(&pk768); let mut rng = FixedSeedRNG::new(seed_bytes); let (ss, ct) = MLKEM768::encaps_for_expanded_key_rng(&pk_expanded, &mut rng).unwrap(); - assert_eq!(ct, ct_ref, "ML-KEM-768 ciphertext must match encaps_internal"); + assert_eq!(ct, ct_ref, "ML-KEM-768 ciphertext must match encaps_with_randomness"); assert_eq!( ss_ref, ss.ref_to_bytes(), - "ML-KEM-768 shared secret must match encaps_internal" + "ML-KEM-768 shared secret must match encaps_with_randomness" ); // ML-KEM-1024 let (pk1024, _sk) = MLKEM1024::keygen().unwrap(); - let (ss_ref, ct_ref) = MLKEM1024::encaps_internal(&pk1024, None, m); + let (ss_ref, ct_ref) = MLKEM1024::encaps_with_randomness(&pk1024, None, m); let pk_expanded = MLKEM1024PublicKeyExpanded::from(&pk1024); let mut rng = FixedSeedRNG::new(seed_bytes); let (ss, ct) = MLKEM1024::encaps_for_expanded_key_rng(&pk_expanded, &mut rng).unwrap(); - assert_eq!(ct, ct_ref, "ML-KEM-1024 ciphertext must match encaps_internal"); + assert_eq!(ct, ct_ref, "ML-KEM-1024 ciphertext must match encaps_with_randomness"); assert_eq!( ss_ref, ss.ref_to_bytes(), - "ML-KEM-1024 shared secret must match encaps_internal" + "ML-KEM-1024 shared secret must match encaps_with_randomness" ); // Ensure that it rejects an RNG at a lower security level @@ -816,7 +815,7 @@ mod mlkem_tests { #[test] fn algorithm_names_and_oids() { - use bouncycastle_core::traits::{Algorithm, AlgorithmOID, SecurityStrength}; + use bouncycastle_core::traits::{Algorithm, AlgorithmOID}; // `Algorithm` and `AlgorithmOID` are implemented once, generically over the parameter set, // so nothing else states these per algorithm. Pinned here so that a wrong wiring of the diff --git a/crypto/mlkem/tests/wycheproof.rs b/crypto/mlkem/tests/mlkem_wycheproof.rs similarity index 69% rename from crypto/mlkem/tests/wycheproof.rs rename to crypto/mlkem/tests/mlkem_wycheproof.rs index cb502686..84cdbfc5 100644 --- a/crypto/mlkem/tests/wycheproof.rs +++ b/crypto/mlkem/tests/mlkem_wycheproof.rs @@ -20,265 +20,186 @@ #![allow(dead_code)] -use bouncycastle_core::key_material; +use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{KEMDecapsulator, KEMPrivateKey, KEMPublicKey, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{KEMDecapsulator, KEMPrivateKey, KEMPublicKey}; +use bouncycastle_core_test_framework::test_data_loaders::{Value, wycheproof_json}; use bouncycastle_hex as hex; +use bouncycastle_mlkem::hazmat::EncapsWithRandomness; use bouncycastle_mlkem::{ MLKEM512, MLKEM512PrivateKey, MLKEM512PublicKey, MLKEM768, MLKEM768PrivateKey, MLKEM768PublicKey, MLKEM1024, MLKEM1024PrivateKey, MLKEM1024PublicKey, MLKEMTrait, }; -#[cfg(test)] -mod wycheproof { - use crate::{ - MLKEMEncapsTestCase, MLKEMKeygenSeedTestCase, MLKEMSemiExpandedDecapsTestCase, - MLKEMTestCase, ParameterSet, - }; - use std::fs; - use std::path::Path; - use std::sync::Once; - - const TEST_DATA_PATH_RELATIVE: &str = "../../../wycheproof/testvectors_v1"; - const TEST_DATA_PATH: &str = "../wycheproof/testvectors_v1"; - - 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!("wycheproof found at: {:?}", TEST_DATA_PATH_RELATIVE), - 2 => println!("wycheproof found at: {:?}", TEST_DATA_PATH), - _ => println!("WARNING: wycheproof directory not found; tests will be skipped"), - }); - - if !found == 3 { - return Err(()); - } +#[test] +fn mlkem_512_encaps_test() { + let Some(json) = wycheproof_json("mlkem_512_encaps_test.json") else { return }; - 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(()); - }; + let test_cases = MLKEMEncapsTestCase::parse(json, ParameterSet::Mlkem512); - Ok(contents) + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem512(); } - #[test] - fn mlkem_512_encaps_test() { - let contents = match get_test_data("mlkem_512_encaps_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_512_encaps_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMEncapsTestCase::parse(contents, ParameterSet::Mlkem512); +#[test] +fn mlkem_512_keygen_seed_test() { + let Some(json) = wycheproof_json("mlkem_512_keygen_seed_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem512(); - } + let test_cases = MLKEMKeygenSeedTestCase::parse(json, ParameterSet::Mlkem512); - println!("mlkem_512_encaps_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem512(); } - #[test] - fn mlkem_512_keygen_seed_test() { - let contents = match get_test_data("mlkem_512_keygen_seed_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_512_keygen_seed_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMKeygenSeedTestCase::parse(contents, ParameterSet::Mlkem512); +#[test] +fn mlkem_512_semi_expanded_decaps_test() { + let Some(json) = wycheproof_json("mlkem_512_semi_expanded_decaps_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem512(); - } + let test_cases = MLKEMSemiExpandedDecapsTestCase::parse(json, ParameterSet::Mlkem512); - println!("mlkem_512_keygen_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem512(); } - #[test] - fn mlkem_512_semi_expanded_decaps_test() { - let contents = match get_test_data("mlkem_512_semi_expanded_decaps_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_512_semi_expanded_decaps_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMSemiExpandedDecapsTestCase::parse(contents, ParameterSet::Mlkem512); +#[test] +fn mlkem_512_test() { + let Some(json) = wycheproof_json("mlkem_512_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem512(); - } + let test_cases = MLKEMTestCase::parse(json, ParameterSet::Mlkem512); - println!("mlkem_512_semi_expanded_decaps_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem512(); } - #[test] - fn mlkem_512_test() { - let contents = match get_test_data("mlkem_512_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_512_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMTestCase::parse(contents, ParameterSet::Mlkem512); +#[test] +fn mlkem_768_encaps_test() { + let Some(json) = wycheproof_json("mlkem_768_encaps_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem512(); - } + let test_cases = MLKEMEncapsTestCase::parse(json, ParameterSet::Mlkem768); - println!("mlkem_512_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem768(); } - #[test] - fn mlkem_768_encaps_test() { - let contents = match get_test_data("mlkem_768_encaps_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_768_encaps_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMEncapsTestCase::parse(contents, ParameterSet::Mlkem768); +#[test] +fn mlkem_768_keygen_seed_test() { + let Some(json) = wycheproof_json("mlkem_768_keygen_seed_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem768(); - } + let test_cases = MLKEMKeygenSeedTestCase::parse(json, ParameterSet::Mlkem768); - println!("mlkem_768_encaps_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem768(); } - #[test] - fn mlkem_768_keygen_seed_test() { - let contents = match get_test_data("mlkem_768_keygen_seed_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_768_keygen_seed_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMKeygenSeedTestCase::parse(contents, ParameterSet::Mlkem768); +#[test] +fn mlkem_768_semi_expanded_decaps_test() { + let Some(json) = wycheproof_json("mlkem_768_semi_expanded_decaps_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem768(); - } + let test_cases = MLKEMSemiExpandedDecapsTestCase::parse(json, ParameterSet::Mlkem768); - println!("mlkem_768_keygen_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem768(); } - #[test] - fn mlkem_768_semi_expanded_decaps_test() { - let contents = match get_test_data("mlkem_768_semi_expanded_decaps_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_768_semi_expanded_decaps_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMSemiExpandedDecapsTestCase::parse(contents, ParameterSet::Mlkem768); +#[test] +fn mlkem_768_test() { + let Some(json) = wycheproof_json("mlkem_768_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem768(); - } + let test_cases = MLKEMTestCase::parse(json, ParameterSet::Mlkem768); - println!("mlkem_768_semi_expanded_decaps_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem768(); } - #[test] - fn mlkem_768_test() { - let contents = match get_test_data("mlkem_768_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_768_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMTestCase::parse(contents, ParameterSet::Mlkem768); +#[test] +fn mlkem_1024_encaps_test() { + let Some(json) = wycheproof_json("mlkem_1024_encaps_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem768(); - } + let test_cases = MLKEMEncapsTestCase::parse(json, ParameterSet::Mlkem1024); - println!("mlkem_768_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem1024(); } - #[test] - fn mlkem_1024_encaps_test() { - let contents = match get_test_data("mlkem_1024_encaps_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_1024_encaps_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMEncapsTestCase::parse(contents, ParameterSet::Mlkem1024); +#[test] +fn mlkem_1024_keygen_seed_test() { + let Some(json) = wycheproof_json("mlkem_1024_keygen_seed_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem1024(); - } + let test_cases = MLKEMKeygenSeedTestCase::parse(json, ParameterSet::Mlkem1024); - println!("mlkem_1024_encaps_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem1024(); } - #[test] - fn mlkem_1024_keygen_seed_test() { - let contents = match get_test_data("mlkem_1024_keygen_seed_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_1024_keygen_seed_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMKeygenSeedTestCase::parse(contents, ParameterSet::Mlkem1024); +#[test] +fn mlkem_1024_semi_expanded_decaps_test() { + let Some(json) = wycheproof_json("mlkem_1024_semi_expanded_decaps_test.json") else { + return; + }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem1024(); - } + let test_cases = MLKEMSemiExpandedDecapsTestCase::parse(json, ParameterSet::Mlkem1024); - println!("mlkem_1024_keygen_seed_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem1024(); } - #[test] - fn mlkem_1024_semi_expanded_decaps_test() { - let contents = match get_test_data("mlkem_1024_semi_expanded_decaps_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; + println!("mlkem_1024_semi_expanded_decaps_test: all {} test cases passed.", num_test_cases); +} - let test_cases = MLKEMSemiExpandedDecapsTestCase::parse(contents, ParameterSet::Mlkem1024); +#[test] +fn mlkem_1024_test() { + let Some(json) = wycheproof_json("mlkem_1024_test.json") else { return }; - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem1024(); - } + let test_cases = MLKEMTestCase::parse(json, ParameterSet::Mlkem1024); - println!("mlkem_1024_semi_expanded_decaps_test: all {} test cases passed.", num_test_cases); + let num_test_cases = test_cases.len(); + for test_case in test_cases { + test_case.run_mlkem1024(); } - #[test] - fn mlkem_1024_test() { - let contents = match get_test_data("mlkem_1024_test.json") { - Ok(contents) => contents, - Err(()) => return, - }; - - let test_cases = MLKEMTestCase::parse(contents, ParameterSet::Mlkem1024); - - let num_test_cases = test_cases.len(); - for test_case in test_cases { - test_case.run_mlkem1024(); - } - - println!("mlkem_1024_test: all {} test cases passed.", num_test_cases); - } + println!("mlkem_1024_test: all {} test cases passed.", num_test_cases); } /* Structs for holding test data */ @@ -316,10 +237,7 @@ impl MLKEMEncapsTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); @@ -361,8 +279,11 @@ impl MLKEMEncapsTestCase { /* Perform the deterministic encaps and compare results */ - let (k, ct) = - MLKEM512::encaps_internal(&ek, None, hex::decode(&self.m).unwrap().try_into().unwrap()); + let (k, ct) = MLKEM512::encaps_with_randomness( + &ek, + None, + hex::decode(&self.m).unwrap().try_into().unwrap(), + ); if self.result == "valid" { assert_eq!(k, hex::decode(&self.k).unwrap().as_slice()); @@ -391,8 +312,11 @@ impl MLKEMEncapsTestCase { /* Perform the deterministic encaps and compare results */ - let (k, ct) = - MLKEM768::encaps_internal(&ek, None, hex::decode(&self.m).unwrap().try_into().unwrap()); + let (k, ct) = MLKEM768::encaps_with_randomness( + &ek, + None, + hex::decode(&self.m).unwrap().try_into().unwrap(), + ); if self.result == "valid" { assert_eq!(k, hex::decode(&self.k).unwrap().as_slice()); @@ -421,7 +345,7 @@ impl MLKEMEncapsTestCase { /* Perform the deterministic encaps and compare results */ - let (k, ct) = MLKEM1024::encaps_internal( + let (k, ct) = MLKEM1024::encaps_with_randomness( &ek, None, hex::decode(&self.m).unwrap().try_into().unwrap(), @@ -460,10 +384,7 @@ impl MLKEMKeygenSeedTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); @@ -562,10 +483,7 @@ impl MLKEMSemiExpandedDecapsTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); @@ -673,10 +591,7 @@ impl MLKEMTestCase { } } - fn parse(data: String, parameter_set: ParameterSet) -> Vec { - let json: serde_json::Value = - serde_json::from_str(&data).expect("test data is not valid JSON"); - + fn parse(json: Value, parameter_set: ParameterSet) -> Vec { let mut test_cases = Vec::::new(); let groups = json["testGroups"].as_array().expect("testGroups is not an array"); @@ -718,7 +633,7 @@ impl MLKEMTestCase { } }; // allow an all-zero seed for testing - key_material::do_hazardous_operations(&mut seed, |seed| { + do_hazardous_operations(&mut seed, |seed| { seed.set_key_type(KeyType::Seed).unwrap(); match seed.set_security_strength(SecurityStrength::_256bit) { Ok(_) => Ok(()), @@ -784,7 +699,7 @@ impl MLKEMTestCase { } }; // allow an all-zero seed for testing - key_material::do_hazardous_operations(&mut seed, |seed| { + do_hazardous_operations(&mut seed, |seed| { seed.set_key_type(KeyType::Seed).unwrap(); match seed.set_security_strength(SecurityStrength::_256bit) { Ok(_) => Ok(()), @@ -850,7 +765,7 @@ impl MLKEMTestCase { } }; // allow an all-zero seed for testing - key_material::do_hazardous_operations(&mut seed, |seed| { + do_hazardous_operations(&mut seed, |seed| { seed.set_key_type(KeyType::Seed).unwrap(); match seed.set_security_strength(SecurityStrength::_256bit) { Ok(_) => Ok(()), diff --git a/crypto/rng/benches/hash_drbg_benches.rs b/crypto/rng/benches/hash_drbg_benches.rs index ccaac0e9..b687e09f 100644 --- a/crypto/rng/benches/hash_drbg_benches.rs +++ b/crypto/rng/benches/hash_drbg_benches.rs @@ -1,19 +1,21 @@ use bouncycastle_core::key_material::{KeyMaterial0, KeyMaterial256, KeyMaterial512, KeyType}; -use bouncycastle_core::traits::{RNG, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::RNG; use bouncycastle_core_test_framework::DUMMY_SEED; +use bouncycastle_rng::hazmat::NewUninitialized; use bouncycastle_rng::{HashDRBG_SHA256, HashDRBG_SHA512, Sp80090ADrbg}; use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use std::hint::black_box; fn bench_hash_drbg_sha256(c: &mut Criterion) { - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); rng.instantiate(false, seed, &KeyMaterial0::new(), &[], SecurityStrength::_128bit).unwrap(); do_bench(c, &mut rng, "rng::hash_drbg80090a::HashDRBG_SHA256"); } fn bench_hash_drbg_sha512(c: &mut Criterion) { - let mut rng = HashDRBG_SHA512::new_unititialized(); + let mut rng = HashDRBG_SHA512::new_uninitialized(); let seed = KeyMaterial512::from_bytes_as_type(&DUMMY_SEED[..64], KeyType::Seed).unwrap(); rng.instantiate(false, seed, &KeyMaterial0::new(), &[], SecurityStrength::_256bit).unwrap(); do_bench(c, &mut rng, "rng::hash_drbg80090a::HashDRBG_SHA512"); diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index be70cb8d..936b7f4f 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -4,16 +4,17 @@ #![allow(private_bounds)] use crate::Sp80090ADrbg; +use crate::hazmat::NewUninitialized; use bouncycastle_core::errors::{KeyMaterialError, RNGError}; -use bouncycastle_core::key_material::{ - KeyMaterial512, KeyMaterialTrait, KeyType, do_hazardous_operations, -}; -use bouncycastle_core::traits::{Hash, HashAlgParams, RNG, SecurityStrength}; +use bouncycastle_core::hazmat::do_hazardous_operations; +use bouncycastle_core::key_material::{KeyMaterial512, KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Hash, HashAlgParams, RNG}; use bouncycastle_sha2::{SHA256, SHA512}; use bouncycastle_utils::{min, secret::Secret}; -use std::fmt::{Display, Formatter}; +use core::fmt::{Display, Formatter}; enum SupportedHash { SHA256, @@ -90,7 +91,7 @@ struct AdministrativeInfo { /// Explicit implementation of Display that prevents auto-generated ones from accidentally leaking secrets. impl Display for WorkingState { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!(f, "HashDRBG80090A::WorkingState::<{}>", SEED_LEN) } } @@ -110,26 +111,6 @@ impl HashDRBG80090A { Self::new_from_os() } - /// Creates a new, uninstantiated instance. After creating it, you must call instantiate() to seed it. - /// - /// **WARNING: Dangerous! This constructor does not initialize the DRBG from any entropy source, - /// and relies on you to provide a strong seed.** - pub fn new_unititialized() -> Self { - Self { - _phantom: core::marker::PhantomData, - state: WorkingState:: { - v: Secret::<[u8; LARGEST_HASHER_OUTPUT_LEN]>::new(), - c: Secret::<[u8; LARGEST_HASHER_OUTPUT_LEN]>::new(), - reseed_counter: Secret::new(), - }, - admin_info: AdministrativeInfo { - strength: H::MAX_SECURITY_STRENGTH, - prediction_resistance: false, - instantiated: false, - }, - } - } - /// Creates a new instance using the local OS RNG as a source of seed entropy. pub fn new_from_os() -> Self { let mut seed = KeyMaterial512::new(); @@ -151,13 +132,33 @@ impl HashDRBG80090A { }) .unwrap(); - let mut rng = Self::new_unititialized(); + let mut rng = Self::new_uninitialized(); let ss = seed.security_strength().clone(); rng.instantiate(false, seed, &KeyMaterial512::new(), "new_from_os".as_bytes(), ss).unwrap(); rng } } +impl NewUninitialized for HashDRBG80090A { + /// The state is all zeros and `instantiated` is false, so every output method refuses until + /// [`Sp80090ADrbg::instantiate`] has run. + fn new_uninitialized() -> Self { + Self { + _phantom: core::marker::PhantomData, + state: WorkingState:: { + v: Secret::<[u8; LARGEST_HASHER_OUTPUT_LEN]>::new(), + c: Secret::<[u8; LARGEST_HASHER_OUTPUT_LEN]>::new(), + reseed_counter: Secret::new(), + }, + admin_info: AdministrativeInfo { + strength: H::MAX_SECURITY_STRENGTH, + prediction_resistance: false, + instantiated: false, + }, + } + } +} + impl Default for HashDRBG80090A { /// Creates a new instance using the local OS RNG as a source of seed entropy. /// Alias for [`HashDRBG80090A::new_from_os`]. diff --git a/crypto/rng/src/hazmat/mod.rs b/crypto/rng/src/hazmat/mod.rs new file mode 100644 index 00000000..02b2099e --- /dev/null +++ b/crypto/rng/src/hazmat/mod.rs @@ -0,0 +1,11 @@ +//! Raw DRBG operations whose safe use is the caller's responsibility; see +//! [`bouncycastle_core::hazmat`] for what the path means and the supported uses. +//! +//! [`NewUninitialized`] constructs a DRBG with no seed at all; [`HashDRBG80090A::new`] +//! seeds from the OS and is the constructor to use. +//! +//! [`HashDRBG80090A::new`]: crate::hash_drbg80090a::HashDRBG80090A::new + +mod new_uninitialized; + +pub use new_uninitialized::NewUninitialized; diff --git a/crypto/rng/src/hazmat/new_uninitialized.rs b/crypto/rng/src/hazmat/new_uninitialized.rs new file mode 100644 index 00000000..c948d59a --- /dev/null +++ b/crypto/rng/src/hazmat/new_uninitialized.rs @@ -0,0 +1,24 @@ +//! [`NewUninitialized`]: a DRBG constructed with no entropy, to be seeded by the caller. + +// Imports needed for docs +#[allow(unused_imports)] +use crate::Sp80090ADrbg; +#[allow(unused_imports)] +use crate::hash_drbg80090a::HashDRBG80090A; +// end of imports needed for docs + +/// Constructs a DRBG with no seed at all. +/// +/// # 🚨 Security Considerations 🚨 +/// The value is unusable until [`Sp80090ADrbg::instantiate`] has been called, and everything +/// built on its output is only as strong as the seed material that call is given. Nothing here +/// checks that material. [`HashDRBG80090A::new`] seeds from the OS and is the constructor to use; +/// this exists for the SP 800-90A known-answer tests and for environments that must supply their +/// own entropy. +/// +/// A trait rather than an inherent constructor so that it is only reachable with this module's +/// path in scope; see [`bouncycastle_core::hazmat`]. +pub trait NewUninitialized: Sized { + /// Creates an uninstantiated instance; call [`Sp80090ADrbg::instantiate`] before use. + fn new_uninitialized() -> Self; +} diff --git a/crypto/rng/src/lib.rs b/crypto/rng/src/lib.rs index 30403580..14994a30 100644 --- a/crypto/rng/src/lib.rs +++ b/crypto/rng/src/lib.rs @@ -16,7 +16,7 @@ //! **WARNING: most people should stop reading here and should not attempt to modify the internals of RNGs. //! This crate contains dragons and other horrible things. 🐉🐍🐜** //! -//! # 🚨🚨🚨Security Warning 🚨🚨🚨 +//! # 🚨 Security Considerations 🚨 //! //! Misuse of the objects in this crate can lead to output which may appear random, but //! is in fact completely deterministic (ie multiple runs of your application will give the same outputs) @@ -25,7 +25,8 @@ //! //! This crate contains the [`Sp80090ADrbg`] trait, which is intentionally defined here and not in [`bouncycastle_core::traits`] //! since misuse of [`Sp80090ADrbg::instantiate`] can completely undermine the security of your entire -//! cryptographic application. +//! cryptographic application. A DRBG with no seed at all comes only from +//! [`hazmat::NewUninitialized`]. #![forbid(unsafe_code)] #![forbid(missing_docs)] @@ -35,7 +36,7 @@ use crate::hash_drbg80090a::{ }; use bouncycastle_core::errors::RNGError; use bouncycastle_core::key_material::KeyMaterialTrait; -use bouncycastle_core::traits::SecurityStrength; +use bouncycastle_core::security_strength::SecurityStrength; // needed for docs #[allow(unused_imports)] @@ -43,6 +44,7 @@ use bouncycastle_core::key_material::KeyType; // end doc-only imports pub mod hash_drbg80090a; +pub mod hazmat; /*** String constants ***/ /// diff --git a/crypto/rng/tests/hash_drbg80090a_tests.rs b/crypto/rng/tests/hash_drbg80090a_tests.rs index 4a8967b7..f408fbb8 100644 --- a/crypto/rng/tests/hash_drbg80090a_tests.rs +++ b/crypto/rng/tests/hash_drbg80090a_tests.rs @@ -4,9 +4,11 @@ mod tests { use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterial0, KeyMaterial256, KeyMaterialTrait, KeyType, }; - use bouncycastle_core::traits::{RNG, SecurityStrength}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::RNG; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_rng::Sp80090ADrbg; + use bouncycastle_rng::hazmat::NewUninitialized; use bouncycastle_rng::{HashDRBG_SHA256, HashDRBG_SHA512}; #[test] @@ -46,7 +48,7 @@ mod tests { #[test] fn test_init() { - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let mut out = [0u8; 32]; match rng.generate_out(&[], &mut out) { Err(RNGError::Uninitialized) => { /* good */ } @@ -58,7 +60,7 @@ mod tests { assert_ne!(out, [0u8; 32]); // Success case: seed len equals required entropy - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let mut out = [0u8; 32]; let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..16], KeyType::Seed).unwrap(); rng.instantiate(false, seed, &KeyMaterial0::new(), &[], SecurityStrength::_128bit).unwrap(); @@ -66,7 +68,7 @@ mod tests { assert_ne!(out, [0u8; 32]); // Error case: seed != KeyType::Seed - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::SymmetricCipherKey) .unwrap(); @@ -76,7 +78,7 @@ mod tests { } // Error case: seed too short - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..8], KeyType::Seed).unwrap(); match rng.instantiate(false, seed, &KeyMaterial0::new(), &[], SecurityStrength::_128bit) { Err(RNGError::KeyMaterialError(_)) => { /* good */ } @@ -89,7 +91,7 @@ mod tests { // Error case: security strength requested at init is higher than the underlying // hash function's max security strength - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); match rng.instantiate(false, seed, &KeyMaterial0::new(), &[], SecurityStrength::_256bit) { Err(RNGError::KeyMaterialError(KeyMaterialError::SecurityStrength(_))) => { /* good */ } @@ -99,16 +101,16 @@ mod tests { // Success case: security strength requested at init is lower than the underlying // hash function's max security strength // ... 112 bit - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); rng.instantiate(false, seed, &KeyMaterial0::new(), &[], SecurityStrength::_128bit).unwrap(); // ... 128 bit - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); rng.instantiate(false, seed, &KeyMaterial0::new(), &[], SecurityStrength::_128bit).unwrap(); // Error case: double initialize - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); rng.instantiate(false, seed, &KeyMaterial0::new(), &[], SecurityStrength::_128bit).unwrap(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); @@ -131,7 +133,7 @@ mod tests { rng.reseed(&seed, &[0u8; 32]).unwrap(); // Error case: uninitialized - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let seed = KeyMaterial256::from_bytes_as_type(&DUMMY_SEED[..32], KeyType::Seed).unwrap(); match rng.reseed(&seed, &[0u8; 32]) { Err(RNGError::Uninitialized) => { /*good*/ } @@ -173,7 +175,7 @@ mod tests { let mut rng = HashDRBG_SHA256::new_from_os(); let out = rng.generate(&[], 0).unwrap(); assert_eq!(out.len(), 0); - assert_eq!(out, []); + assert_eq!(out, [0u8; 0]); // Success case: one-byte output let mut rng = HashDRBG_SHA256::new_from_os(); @@ -187,7 +189,7 @@ mod tests { assert_ne!(out, [0u8; 1024]); // Error case: uninitialized - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); match rng.generate(&[], 32) { Err(RNGError::Uninitialized) => { /*good*/ } _ => panic!("Expected Uninitialized error"), @@ -214,7 +216,7 @@ mod tests { let mut out = [0u8; 0]; let bytes_written = rng.generate_out(&[], &mut out).unwrap(); assert_eq!(bytes_written, 0); - assert_eq!(out, []); + assert_eq!(out, [0u8; 0]); // Success case: one-byte output let mut rng = HashDRBG_SHA256::new_from_os(); @@ -232,7 +234,7 @@ mod tests { assert_ne!(out, [0u8; 1024]); // Error case: uninitialized - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let mut out = [0u8; 32]; match rng.generate_out(&[], &mut out) { Err(RNGError::Uninitialized) => { /*good*/ } @@ -261,7 +263,7 @@ mod tests { let mut out = KeyMaterial0::new(); let bytes_written = rng.generate_keymaterial_out(&[], &mut out).unwrap(); assert_eq!(bytes_written, 0); - assert_eq!(out.ref_to_bytes(), []); + assert_eq!(out.ref_to_bytes(), [0u8; 0]); assert_eq!(out.security_strength(), SecurityStrength::None); // Success case: one-byte output @@ -282,7 +284,7 @@ mod tests { assert_eq!(out.security_strength(), SecurityStrength::_128bit); // // Error case: uninitialized - let mut rng = HashDRBG_SHA256::new_unititialized(); + let mut rng = HashDRBG_SHA256::new_uninitialized(); let mut out = KeyMaterial256::new(); match rng.generate_keymaterial_out(&[], &mut out) { Err(RNGError::Uninitialized) => { /*good*/ } diff --git a/crypto/sha2/Cargo.toml b/crypto/sha2/Cargo.toml index affcde1b..565ca225 100644 --- a/crypto/sha2/Cargo.toml +++ b/crypto/sha2/Cargo.toml @@ -13,6 +13,7 @@ bouncycastle-utils.workspace = true criterion.workspace = true bouncycastle-core-test-framework.workspace = true bouncycastle-rng.workspace = true +bouncycastle-hex.workspace = true [[bench]] name = "sha2_benches" diff --git a/crypto/sha2/benches/sha2_benches.rs b/crypto/sha2/benches/sha2_benches.rs index 0d12a00a..09771c58 100644 --- a/crypto/sha2/benches/sha2_benches.rs +++ b/crypto/sha2/benches/sha2_benches.rs @@ -5,17 +5,17 @@ use bouncycastle_core::traits::{Hash, RNG}; use bouncycastle_rng as rng; use bouncycastle_sha2::*; -fn bench_sha256(c: &mut Criterion) { +fn bench_hash(c: &mut Criterion, group_name: &str) { let mut data = [0_u8; 1024]; rng::DefaultRNG::default().next_bytes_out(&mut data).unwrap(); - let mut digest = vec![0; SHA256::new().output_len()]; + let mut digest = vec![0; H::default().output_len()]; - let mut group = c.benchmark_group("sha2::sha256"); + let mut group = c.benchmark_group(group_name); group.throughput(Throughput::Bytes(16 * 1024)); group.bench_function("16KiB", |b| { b.iter(|| { - let mut md = SHA256::new(); + let mut md = H::default(); for _ in 0..16 { md.do_update(black_box(&data)); } @@ -26,26 +26,21 @@ fn bench_sha256(c: &mut Criterion) { group.finish(); } +fn bench_sha256(c: &mut Criterion) { + bench_hash::(c, "sha2::sha256"); +} + fn bench_sha512(c: &mut Criterion) { - let mut data = [0_u8; 1024]; - rng::DefaultRNG::default().next_bytes_out(&mut data).unwrap(); + bench_hash::(c, "sha2::sha512"); +} - let mut digest = vec![0; SHA512::new().output_len()]; +fn bench_sha512_224(c: &mut Criterion) { + bench_hash::(c, "sha2::sha512_224"); +} - let mut group = c.benchmark_group("sha2::sha512"); - group.throughput(Throughput::Bytes(16 * 1024)); - group.bench_function("16KiB", |b| { - b.iter(|| { - let mut md = SHA512::new(); - for _ in 0..16 { - md.do_update(black_box(&data)); - } - _ = md.do_final_out(&mut digest); - black_box(&digest); - }) - }); - group.finish(); +fn bench_sha512_256(c: &mut Criterion) { + bench_hash::(c, "sha2::sha512_256"); } -criterion_group!(benches, bench_sha256, bench_sha512); +criterion_group!(benches, bench_sha256, bench_sha512, bench_sha512_224, bench_sha512_256); criterion_main!(benches); diff --git a/crypto/sha2/src/hkdf.rs b/crypto/sha2/src/hkdf.rs index 7ca19650..d2088677 100644 --- a/crypto/sha2/src/hkdf.rs +++ b/crypto/sha2/src/hkdf.rs @@ -1,9 +1,9 @@ //! HMAC-based Extract-and-Expand Key Derivation Function (HKDF) over the SHA-2 hashes, as per //! RFC 5869, as allowed by NIST SP 800-56Cr2. //! -//! Uses [`bouncycastle_hkdf`] to provide the HKDF-SHA2 instantiations: [`HKDF_SHA256`] and -//! [`HKDF_SHA512`]. Only those two are instantiated, matching what the KDF factory and the CLI -//! expose. +//! Uses [`bouncycastle_hkdf`] to provide the HKDF-SHA2 instantiations: [`HKDF_SHA256`], +//! [`HKDF_SHA384`] and [`HKDF_SHA512`]. The KDF factory and the CLI expose HKDF-SHA256 and +//! HKDF-SHA512. //! //! HKDF is implemented generically in [`bouncycastle_hkdf`]; this module pins its const parameters //! to the SHA-2 hashes and publishes the resulting type aliases, so that HKDF over a SHA-2 hash is @@ -187,12 +187,14 @@ //! | Object | Size (bytes) | //! |---------------------------------------------------|--------------| //! | `HKDF_SHA256` | 296 | +//! | `HKDF_SHA384` | 392 | //! | `HKDF_SHA512` | 392 | //! | Suspended `HKDF_SHA256` state | 122 | +//! | Suspended `HKDF_SHA384` state | 218 | //! | Suspended `HKDF_SHA512` state | 218 | //! //! The object is an `Option` of the inner extract-phase HMAC -- 272 bytes for SHA-256, 368 for -//! SHA-512 -- plus 24 bytes of bookkeeping (the entropy counter, the accumulated security strength +//! SHA-384 and SHA-512 -- plus 24 bytes of bookkeeping (the entropy counter, the accumulated security strength //! and the state-machine tag, with padding). Note that the inner HMAC is written as `HMAC`, which //! takes the *default* key buffer length: the largest block length across all supported hashes //! (144 bytes) rather than the 64 or 128 that SHA-256 and SHA-512 actually need. So the inner @@ -202,7 +204,7 @@ //! The suspended state is the inner HMAC's suspended state (which is the hash's) plus 14 bytes; the //! salt is deliberately excluded and must be re-supplied on resume. //! -//! # Security Considerations +//! # 🚨 Security Considerations 🚨 //! //! * Resuming a suspended HKDF with a different salt cannot be detected and silently produces a //! different PRK; see the suspend/resume section above. @@ -216,8 +218,11 @@ //! * RFC 5869 Section 3.1 recommends a random salt where one is available; SP 800-56Cr2 permits an //! all-zero salt. An all-zero salt is not a [`KeyType::MACKey`], so it needs //! `MAC::new_allow_weak_key` semantics -- which is exactly what the extract phase does internally. -use crate::hmac::{SUSPENDED_HMAC_SHA256_STATE_LEN, SUSPENDED_HMAC_SHA512_STATE_LEN}; -use crate::{SHA256, SHA512}; +use crate::hmac::{ + SUSPENDED_HMAC_SHA256_STATE_LEN, SUSPENDED_HMAC_SHA384_STATE_LEN, + SUSPENDED_HMAC_SHA512_STATE_LEN, +}; +use crate::{SHA256, SHA384, SHA512}; use crate::{SUSPENDED_SHA256_STATE_LEN, SUSPENDED_SHA512_STATE_LEN}; use bouncycastle_hkdf::HKDF; @@ -225,12 +230,16 @@ use bouncycastle_hkdf::HKDF; #[allow(unused_imports)] use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; #[allow(unused_imports)] -use bouncycastle_core::traits::{KDF, SecurityStrength, SuspendableKeyed, XOF}; +use bouncycastle_core::security_strength::SecurityStrength; +#[allow(unused_imports)] +use bouncycastle_core::traits::{KDF, SuspendableKeyed, XOF}; /*** String constants ***/ /// pub const HKDF_SHA256_NAME: &str = "HKDF-SHA256"; /// +pub const HKDF_SHA384_NAME: &str = "HKDF-SHA384"; +/// pub const HKDF_SHA512_NAME: &str = "HKDF-SHA512"; /*** Serialized-state length constants ***/ @@ -239,6 +248,8 @@ pub const HKDF_SHA512_NAME: &str = "HKDF-SHA512"; // see the `SuspendableKeyed` impl in `bouncycastle-hkdf` for the layout. /// Length in bytes of the serialized state of [`HKDF_SHA256`]. pub const SUSPENDED_HKDF_SHA256_STATE_LEN: usize = SUSPENDED_HMAC_SHA256_STATE_LEN + 14; +/// Length in bytes of the serialized state of [`HKDF_SHA384`]. +pub const SUSPENDED_HKDF_SHA384_STATE_LEN: usize = SUSPENDED_HMAC_SHA384_STATE_LEN + 14; /// Length in bytes of the serialized state of [`HKDF_SHA512`]. pub const SUSPENDED_HKDF_SHA512_STATE_LEN: usize = SUSPENDED_HMAC_SHA512_STATE_LEN + 14; @@ -246,6 +257,10 @@ pub const SUSPENDED_HKDF_SHA512_STATE_LEN: usize = SUSPENDED_HMAC_SHA512_STATE_L /// Public type for HKDF using SHA256. #[allow(non_camel_case_types)] pub type HKDF_SHA256 = HKDF; +/// Public type for HKDF using SHA384. SHA-384 is a member of the SHA-512 family, so it shares +/// SHA-512's suspended-state length. +#[allow(non_camel_case_types)] +pub type HKDF_SHA384 = HKDF; /// Public type for HKDF using SHA512. #[allow(non_camel_case_types)] pub type HKDF_SHA512 = HKDF; diff --git a/crypto/sha2/src/hmac.rs b/crypto/sha2/src/hmac.rs index a94bb19b..ab35cb8f 100644 --- a/crypto/sha2/src/hmac.rs +++ b/crypto/sha2/src/hmac.rs @@ -227,7 +227,7 @@ //! hash's suspended state -- the key is deliberately excluded -- so it matches the corresponding row //! for the bare hash. //! -//! # Security Considerations +//! # 🚨 Security Considerations 🚨 //! //! * The key must carry at least the security strength claimed by the HMAC, and [`MAC::new`] //! enforces that. [`MAC::new_allow_weak_key`] deliberately skips the check; use it only where a @@ -244,10 +244,11 @@ //! MAC; see the suspend/resume section above. //! * A key longer than the hash's block length is pre-hashed down to the output length (RFC 2104 //! Section 2), so very long keys add no strength beyond that point. -use crate::{SHA224, SHA256, SHA384, SHA512}; +use crate::{SHA224, SHA256, SHA384, SHA512, SHA512_224, SHA512_256}; use crate::{SUSPENDED_SHA256_STATE_LEN, SUSPENDED_SHA512_STATE_LEN}; use bouncycastle_core::key_material::KeyMaterial; -use bouncycastle_core::traits::{HashAlgParams, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::HashAlgParams; use bouncycastle_hmac::{HMAC, HMACParams}; /*** Imports needed for docs ***/ @@ -267,6 +268,10 @@ pub const HMAC_SHA256_NAME: &str = "HMAC-SHA256"; pub const HMAC_SHA384_NAME: &str = "HMAC-SHA384"; /// pub const HMAC_SHA512_NAME: &str = "HMAC-SHA512"; +/// +pub const HMAC_SHA512_224_NAME: &str = "HMAC-SHA512/224"; +/// +pub const HMAC_SHA512_256_NAME: &str = "HMAC-SHA512/256"; /*** Type aliases ***/ /// Public type for HMAC using SHA224. @@ -321,6 +326,32 @@ impl HMACParams for SHA512 { &[0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x0b]; } +/// Public type for HMAC using SHA512/224. +#[allow(non_camel_case_types)] +pub type HMAC_SHA512_224 = HMAC::BLOCK_LEN }>; +impl HMACParams for SHA512_224 { + type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; + const HMAC_ALG_NAME: &'static str = HMAC_SHA512_224_NAME; + const HMAC_MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_112bit; + /// Defined in RFC 8018 Appendix B.1.2: id-hmacWithSHA512-224 { digestAlgorithm 12 } + const HMAC_OID: &'static [u32] = &[1, 2, 840, 113549, 2, 12]; + const HMAC_OID_DER: &'static [u8] = + &[0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x0c]; +} + +/// Public type for HMAC using SHA512/256. +#[allow(non_camel_case_types)] +pub type HMAC_SHA512_256 = HMAC::BLOCK_LEN }>; +impl HMACParams for SHA512_256 { + type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; + const HMAC_ALG_NAME: &'static str = HMAC_SHA512_256_NAME; + const HMAC_MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + /// Defined in RFC 8018 Appendix B.1.2: id-hmacWithSHA512-256 { digestAlgorithm 13 } + const HMAC_OID: &'static [u32] = &[1, 2, 840, 113549, 2, 13]; + const HMAC_OID_DER: &'static [u8] = + &[0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x0d]; +} + /*** Serialized-state length constants ***/ // HMAC's suspended state is exactly the inner hasher's state -- the key is deliberately excluded and // must be re-supplied on resume -- so each of these is the underlying hash's own state length. @@ -332,3 +363,7 @@ pub const SUSPENDED_HMAC_SHA256_STATE_LEN: usize = SUSPENDED_SHA256_STATE_LEN; pub const SUSPENDED_HMAC_SHA384_STATE_LEN: usize = SUSPENDED_SHA512_STATE_LEN; /// Length in bytes of the serialized state of [`HMAC_SHA512`]. pub const SUSPENDED_HMAC_SHA512_STATE_LEN: usize = SUSPENDED_SHA512_STATE_LEN; +/// Length in bytes of the serialized state of [`HMAC_SHA512_224`]. +pub const SUSPENDED_HMAC_SHA512_224_STATE_LEN: usize = SUSPENDED_SHA512_STATE_LEN; +/// Length in bytes of the serialized state of [`HMAC_SHA512_256`]. +pub const SUSPENDED_HMAC_SHA512_256_STATE_LEN: usize = SUSPENDED_SHA512_STATE_LEN; diff --git a/crypto/sha2/src/lib.rs b/crypto/sha2/src/lib.rs index c53544d7..e54c733f 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -9,7 +9,7 @@ //! # Examples //! ## Hash //! Hash functionality is accessed via the [`bouncycastle_core::traits::Hash`] trait, -//! which is implemented by [`SHA224`], [`SHA256`], [`SHA384`] and [`SHA512`]. +//! which is implemented by all the SHA2 primitives. //! //! The simplest usage is via the static functions. //! ``` @@ -21,7 +21,7 @@ //! ``` //! //! More advanced usage will require creating a SHA2 object to hold state between successive calls, -//! for example if input is received in chunks and not all available at the same time: +//! for example, if input is received in chunks and not all available at the same time: //! //! ``` //! use bouncycastle_sha2 as sha2; @@ -40,6 +40,20 @@ //! let output: Vec = sha2.do_final(); //! ``` //! +//! ## Partial byte +//! It is also possible to provide input where the final byte contains fewer than 8 bits of data +//! (a bit-oriented message, FIPS 180-4 s. 5.1). The partial byte is taken as the most significant bits, +//! leading bit first, and the low "unused" bits are ignored. The following hashes 16 bytes plus the +//! 3 message bits `101`: +//! ``` +//! use bouncycastle_core::traits::Hash; +//! use bouncycastle_sha2 as sha2; +//! +//! let data: &[u8] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\xA0"; +//! let mut sha2 = sha2::SHA256::new(); +//! sha2.do_update(&data[..16]); +//! let output: Vec = sha2.do_final_partial_bits(data[16], 3).expect("num_partial_bits is in 0..=7"); +//! ``` //! ## HMAC //! See [hmac]. //! @@ -47,36 +61,10 @@ //! //! See [hkdf] //! -//! # Memory Usage -//! -//! No heap memory is used by the algorithms themselves; the `Vec`-returning convenience methods -//! allocate only the output buffer, and the `*_out` variants allocate nothing. -//! -//! | Object | Size (bytes) | -//! |----------------------------------------------------------|--------------| -//! | `SHA224`, `SHA256` | 112 | -//! | `SHA384`, `SHA512` | 208 | -//! | Suspended `SHA224`/`SHA256` state | 108 | -//! | Suspended `SHA384`/`SHA512` state | 204 | +//! # SHA512t //! -//! The object holds the 8-word chaining value plus one block of buffered input. The compression -//! function additionally uses a 64-word (SHA-256 family, 256 bytes) or 80-word (SHA-512 family, -//! 640 bytes) message schedule on the stack for the duration of a call. -//! -//! # Security Considerations -//! -//! * SHA-224/256/384/512 offer 112/128/192/256 bits of collision resistance respectively. -//! * SHA-2 is a Merkle–Damgård construction and is therefore subject to length-extension: -//! `H(k || m)` is not a secure MAC. Use HMAC ([`crate::hmac`]) for keyed hashing. -//! * SHA-224 and SHA-384 are truncations of SHA-256 and SHA-512 with distinct initial values, and -//! are not vulnerable to length extension in the same direct way, but should still not be used as -//! `H(k || m)` MACs. -//! * The chaining value and input buffer are held in [`bouncycastle_utils::secret::Secret`] and -//! zeroized on drop. Transient copies (working variables and message schedule) in registers/stack -//! locals during compression are not zeroized. -//! * The implementation contains no data-dependent branches or table lookups. -//! * Messages up to 2^64 bytes are supported (FIPS 180-4 permits 2^64 bits for SHA-224/256 and -//! 2^128 bits for SHA-384/512; the SHA-512 family limit here is 2^67 bits). +//! See [`SHA512t`] for documentation around defining a custom truncation length of SHA512 other +//! than the [`SHA512_224`] and [`SHA512_256`] defined in FIPS 180-4. //! //! # Suspending and resuming execution //! @@ -108,6 +96,41 @@ //! sha2_resumed.do_update(msg_part2); //! let h: Vec = sha2_resumed.do_final(); //! ``` +//! +//! # Memory Usage +//! +//! | Object | Size (bytes) | +//! |-------------------------------------------------|--------------| +//! | `SHA224`, `SHA256` | 112 | +//! | `SHA384`, `SHA512` (incl. `SHA512_t` instances | 208 | +//! | Suspended `SHA224`/`SHA256` state | 108 | +//! | Suspended `SHA384`/`SHA512`/`SHA512t` state | 204 | +//! +//! `T` does not affect either size: the truncation happens on the way out of `do_final`, so every +//! member of the SHA-512 family carries the same 512-bit chaining value and 1024-bit buffer. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! * SHA-224/256/384/512 offer 112/128/192/256 bits of collision resistance respectively; +//! SHA-512/224 and SHA-512/256 offer 112 and 128 bits (SP 800-107r1, Table 1 (§4.2)). More +//! generally SHA-512/t offers t/2 bits, which is what [`SHA512t`]'s `MAX_SECURITY_STRENGTH` +//! reports, rounded down to a modelled level. +//! * **A short SHA-512/t truncation is a weak hash.** [`SHA512t`] accepts any `T` FIPS 180-4 +//! s. 5.3.6 defines a hash for, and the smaller `T` is, the less collision resistance it +//! offers: SHA-512/8 has a one-byte digest, and every `T` below 224 reports +//! `SecurityStrength::None`. Pick `T` for the security level you need, not for the digest size +//! you would like. +//! * SHA-2 is a Merkle–Damgård construction and is therefore subject to length-extension: +//! `H(k || m)` is not a secure MAC. Use HMAC (`bouncycastle-hmac`) for keyed hashing. +//! * SHA-224, SHA-384, SHA-512/224 and SHA-512/256 are truncations of SHA-256 or SHA-512 with +//! distinct initial values, and are not vulnerable to length extension in the same direct way, but +//! should still not be used as `H(k || m)` MACs. +//! * The chaining value and input buffer are held in [`bouncycastle_utils::secret::Secret`] and +//! zeroized on drop. Transient copies (working variables and message schedule) in registers/stack +//! locals during compression are not zeroized. +//! * The implementation contains no data-dependent branches or table lookups. +//! * Messages up to 2^64 bytes are supported (FIPS 180-4 permits 2^64 bits for SHA-224/256 and +//! 2^128 bits for SHA-384/512 and SHA-512/t; the SHA-512 family limit here is 2^67 bits). #![forbid(unsafe_code)] #![forbid(missing_docs)] @@ -120,22 +143,30 @@ pub mod hkdf; pub mod hmac; pub use self::sha256::SHA256Internal; +use self::sha256::{SHA224_H0, SHA256_H0}; pub use self::sha512::SHA512Internal; -use bouncycastle_core::traits::{Algorithm, AlgorithmOID, HashAlgParams, SecurityStrength}; +use self::sha512::{SHA384_H0, SHA512_H0, sha512t_h0}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, AlgorithmOID, HashAlgParams}; /*** Imports needed for docs ***/ #[allow(unused_imports)] use bouncycastle_core::traits::{Hash, KDF, MAC, Suspendable}; +/*** end of doc-only imports ***/ /*** String constants ***/ -/// +/// Algorithm name string for SHA224. pub const SHA224_NAME: &str = "SHA224"; -/// +/// Algorithm name string for SHA256. pub const SHA256_NAME: &str = "SHA256"; -/// +/// Algorithm name string for SHA384. pub const SHA384_NAME: &str = "SHA384"; -/// +/// Algorithm name string for SHA512. pub const SHA512_NAME: &str = "SHA512"; +/// Algorithm name string for SHA512/224. +pub const SHA512_224_NAME: &str = "SHA512/224"; +/// Algorithm name string for SHA512/256. +pub const SHA512_256_NAME: &str = "SHA512/256"; /*** pub types ***/ /// Public type for SHA224. @@ -146,16 +177,117 @@ pub type SHA256 = SHA256Internal; pub type SHA384 = SHA512Internal; /// Public type for SHA512. pub type SHA512 = SHA512Internal; +/// Public type for the SHA-512/t family (FIPS 180-4 s. 5.3.6): SHA-512 with a t-specific initial +/// hash value, truncated to `T` bits. +/// +/// This documentation explains how to instantiate the [`SHA512t`] struct from outside the library +/// with a truncation length other than the 224 and 256 specified in FIPS 180-4. +/// +/// # Which `T` are accepted +/// +/// FIPS 180-4 s. 5.3.6 defines SHA-512/t for every "positive integer without a leading zero such +/// that t < 512, and t is not 384", and then names two members of that family, SHA-512/224 and +/// SHA-512/256, as approved hash algorithms. This type implements the family as the section +/// defines it, with one narrowing of this crate's own: `T` must be a multiple of 8, because +/// [`Hash`] produces whole bytes. So `T` may be any multiple of 8 from 8 to 504 other than 384; +/// t = 384 is carved out because SHA-384 (s. 5.3.4) is already "SHA-512 truncated to 384 bits" -- +/// same compression function, same 384-bit output -- but predates SHA-512/t and has its own fixed +/// initial hash value rather than one produced by the IV Generation Function below. Letting +/// `T = 384` through here would derive a second, different 384-bit hash under a name already +/// taken, so the standard reserves 384 for SHA-384 instead. Anything else is a compile error +/// naming the rule it broke: +/// +/// ```compile_fail +/// use bouncycastle_sha2::SHA512t; +/// // FIPS 180-4 s. 5.3.6: "t is not 384" -- use SHA384, which has its own IV. +/// let _ = SHA512t::<384>::new(); +/// ``` +/// +/// ```compile_fail +/// use bouncycastle_sha2::SHA512t; +/// // Not a whole number of bytes: this crate requires t to be a multiple of 8. +/// let _ = SHA512t::<100>::new(); +/// ``` +/// +/// ```compile_fail +/// use bouncycastle_sha2::SHA512t; +/// // FIPS 180-4 s. 5.3.6: "t < 512" -- use SHA512 for the untruncated hash. +/// let _ = SHA512t::<512>::new(); +/// ``` +/// +/// # The initial hash value +/// +/// What makes SHA-512/t a different hash from "SHA-512, keep the first t bits" is its initial +/// hash value H(0): the eight 64-bit words the compression function starts from. FIPS 180-4 +/// s. 5.3.6 requires this of the family -- "Each hash function requires a distinct initial hash +/// value" -- so where s. 5.3.5 fixes H(0) for SHA-512, s. 5.3.6's IV Generation Function derives +/// a fresh one for each t by hashing the ASCII string "SHA-512/t" with a modified SHA-512. Because +/// the starting state differs, a SHA-512/t digest is not a prefix of the SHA-512 digest of the +/// same message, and digests for different t are unrelated to one another rather than prefixes of +/// a common value. H(0) is not a parameter the caller supplies: [`SHA512tParams`] computes it at +/// compile time from `T`, so a new `T` costs nothing at runtime and needs no table, and for +/// t = 224 and t = 256 the result is pinned by `tests/sha512t_h0_tests.rs` against the words +/// FIPS 180-4 prints in s. 5.3.6.1 and s. 5.3.6.2. +/// +/// # Example +/// +/// ``` +/// use bouncycastle_core::traits::{Algorithm, Hash}; +/// use bouncycastle_sha2::{SHA512_256, SHA512t}; +/// +/// // SHA512t<256> *is* SHA512_256. +/// let digest = SHA512_256::new().hash(b"abc"); +/// assert_eq!(SHA512t::<256>::new().hash(b"abc"), digest); +/// +/// // A custom truncation: 96 bits, so a 12-byte digest. +/// let digest = SHA512t::<96>::new().hash(b"abc"); +/// assert_eq!(digest.len(), 12); +/// assert_eq!( as Algorithm>::ALG_NAME, "SHA512/96"); +/// ``` +/// +/// Only [`SHA512_224`] and [`SHA512_256`] have an assigned [`AlgorithmOID`] and a `HashFactory` +/// entry; every other `T` is reachable only by naming it in code, as above. +pub type SHA512t = SHA512Internal>; +/// Public type for SHA512/224 (FIPS 180-4 s. 6.6). +pub type SHA512_224 = SHA512t<224>; +/// Public type for SHA512/256 (FIPS 180-4 s. 6.7). +pub type SHA512_256 = SHA512t<256>; /*** Param traits ***/ -/// Private trait on purpose so that only the NIST-approved params can be used. -trait SHA2Params: HashAlgParams {} +/// The SHA-256 family (SHA-224, SHA-256) 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 (aka "sealed") on purpose: it cannot be implemented outside this crate, so the +/// only parameter sets that exist are the NIST-approved ones below. +/// +/// `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]; +} -/*** SHA224 ***/ -impl HashAlgParams for SHA224 { - const OUTPUT_LEN: usize = 28; - const BLOCK_LEN: usize = 64; +/// 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`], 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]; +} + +/// The public hash types expose the same parameters as their `*Params` marker, so the constants +/// are defined exactly once (on the params struct) and forwarded here. +impl HashAlgParams for SHA256Internal { + const OUTPUT_LEN: usize = PARAMS::OUTPUT_LEN; + const BLOCK_LEN: usize = PARAMS::BLOCK_LEN; +} +impl HashAlgParams for SHA512Internal { + const OUTPUT_LEN: usize = PARAMS::OUTPUT_LEN; + const BLOCK_LEN: usize = PARAMS::BLOCK_LEN; } + +/*** SHA224 ***/ /// The parameters for SHA224. #[derive(Clone)] pub struct SHA224Params; @@ -173,13 +305,12 @@ impl AlgorithmOID for SHA224 { const OID_DER: &'static [u8] = &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04]; } -impl SHA2Params for SHA224Params {} +impl SHA256InitValue for SHA224Params { + // FIPS 180-4 s. 6.3 exception 1: H(0) as specified in s. 5.3.2. + const H0: [u32; 8] = SHA224_H0; +} /*** SHA256 ***/ -impl HashAlgParams for SHA256 { - const OUTPUT_LEN: usize = 32; - const BLOCK_LEN: usize = 64; -} /// The parameters for SHA256. #[derive(Clone)] pub struct SHA256Params; @@ -197,13 +328,12 @@ impl HashAlgParams for SHA256Params { const OUTPUT_LEN: usize = 32; const BLOCK_LEN: usize = 64; } -impl SHA2Params for SHA256Params {} +impl SHA256InitValue for SHA256Params { + // FIPS 180-4 s. 6.2.1 step 1: H(0) as specified in s. 5.3.3. + const H0: [u32; 8] = SHA256_H0; +} /*** SHA384 ***/ -impl HashAlgParams for SHA384 { - const OUTPUT_LEN: usize = 48; - const BLOCK_LEN: usize = 128; -} /// The parameters for SHA384. #[derive(Clone)] pub struct SHA384Params; @@ -221,16 +351,15 @@ impl HashAlgParams for SHA384Params { const OUTPUT_LEN: usize = 48; const BLOCK_LEN: usize = 128; } -impl SHA2Params for SHA384Params {} +impl SHA512InitValue for SHA384Params { + // FIPS 180-4 s. 6.5 exception 1: H(0) as specified in s. 5.3.4. + const H0: [u64; 8] = SHA384_H0; +} /*** SHA512 ***/ /// The parameters for SHA512. #[derive(Clone)] pub struct SHA512Params; -impl HashAlgParams for SHA512 { - const OUTPUT_LEN: usize = 64; - const BLOCK_LEN: usize = 128; -} impl Algorithm for SHA512Params { const ALG_NAME: &'static str = SHA512_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; @@ -245,7 +374,81 @@ impl AlgorithmOID for SHA512 { const OID_DER: &'static [u8] = &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]; } -impl SHA2Params for SHA512Params {} +impl SHA512InitValue for SHA512Params { + // FIPS 180-4 s. 6.4.1 step 1: H(0) as specified in s. 5.3.5. + const H0: [u64; 8] = SHA512_H0; +} + +/*** SHA-512/t ***/ +/// The parameters for SHA-512/t (FIPS 180-4 s. 5.3.6), for a truncation of `T` bits. +/// +/// Implemented for every `T` the section defines a hash for, with two restrictions checked when +/// the parameter set is instantiated, so a bad `T` is a compile error rather than a runtime one: +/// +/// * FIPS 180-4 s. 5.3.6's own rule, "t is any positive integer without a leading zero such that +/// t < 512, and t is not 384"; +/// * this crate's additional requirement that `T` be a multiple of 8, since the digest has to be a +/// whole number of bytes. +/// +/// See [`SHA512t`] for the accepted range and for what the t-specific initial hash value is. +#[derive(Clone)] +pub struct SHA512tParams; + +impl SHA512tParams { + /// `"SHA512/t"` with `T` in decimal, NUL-padded; see [`Self::ALG_NAME_STR`]. + const ALG_NAME_BYTES: [u8; sha512::ALG_NAME_BUF_LEN] = sha512::alg_name_bytes(T); + + /// The algorithm name, e.g. `"SHA512/224"`. Built at compile time from `T` because a const + /// generic cannot be formatted into a `&'static str` directly. + const ALG_NAME_STR: &'static str = { + let bytes: &'static [u8; sha512::ALG_NAME_BUF_LEN] = &Self::ALG_NAME_BYTES; + let (name, _padding) = bytes.split_at(sha512::alg_name_len(T)); + match core::str::from_utf8(name) { + Ok(name) => name, + // unreachable: alg_name_bytes writes only ASCII. + Err(_) => panic!("SHA-512/t algorithm name is not UTF-8"), + } + }; +} + +impl Algorithm for SHA512tParams { + const ALG_NAME: &'static str = Self::ALG_NAME_STR; + /// SP 800-107 Rev 1 Table 1: a t-bit digest offers t/2 bits of collision resistance, rounded + /// down to a modelled level. This reproduces the values the two approved truncations carry: + /// 112-bit for SHA-512/224 and 128-bit for SHA-512/256. + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::from_bits(T / 2); +} +impl HashAlgParams for SHA512tParams { + /// FIPS 180-4 s. 6.6 / s. 6.7 exception 2: truncated to the left-most `T` bits. `T` is a + /// multiple of 8 (checked by `check_t` in `sha512.rs`), so this is exact. + const OUTPUT_LEN: usize = T / 8; + const BLOCK_LEN: usize = 128; // FIPS 180-4 Figure 1: block size 1024 bits +} +impl SHA512InitValue for SHA512tParams { + /// FIPS 180-4 s. 5.3.6: H(0) from the IV Generation Function. For t = 224 and t = 256 this is + /// the value listed in s. 5.3.6.1 / s. 5.3.6.2, pinned against those words by + /// `tests/sha512t_h0_tests.rs`. + const H0: [u64; 8] = sha512t_h0(T); +} + +// The two approved truncations get everything else from the generic impls above; only their +// object identifiers, which exist for no other t, are specific to them. + +/*** SHA512/224 ***/ +/// Assigned by NIST in the Computer Security Objects Register: id-sha512-224 { hashAlgs 5 } +impl AlgorithmOID for SHA512_224 { + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 2, 5]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x05]; +} + +/*** SHA512/256 ***/ +/// Assigned by NIST in the Computer Security Objects Register: id-sha512-256 { hashAlgs 6 } +impl AlgorithmOID for SHA512_256 { + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 2, 6]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x06]; +} pub use sha256::SUSPENDED_SHA256_STATE_LEN; pub use sha512::SUSPENDED_SHA512_STATE_LEN; diff --git a/crypto/sha2/src/sha256.rs b/crypto/sha2/src/sha256.rs index 34d09775..1e1a0773 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -1,10 +1,12 @@ -use crate::SHA2Params; +use crate::SHA256InitValue; use bouncycastle_core::errors::{HashError, SuspendableError}; -use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; -use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, Suspendable}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, Suspendable}; +use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_utils::{min, secret::Secret}; use core::slice; +/// FIPS 180-4 s. 4.2.2: the sixty-four 32-bit constants K0..K63 shared by SHA-224 and SHA-256. const SHA256_K: [u32; 64] = [ 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, @@ -16,126 +18,150 @@ const SHA256_K: [u32; 64] = [ 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2, ]; +/// FIPS 180-4 Table 1 and s. 6.2: SHA-224 and SHA-256 are defined for a message of l bits where +/// 0 <= l < 2^64, so the longest whole-byte message they cover is 2^61 - 1 bytes. +const MAX_MESSAGE_BYTES: u64 = (1 << 61) - 1; + +/// FIPS 180-4 s. 5.3.2: the initial hash value H(0) for SHA-224. +pub(crate) const SHA224_H0: [u32; 8] = [ + 0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939, 0xFFC00B31, 0x68581511, 0x64F98FA7, 0xBEFA4FA4, +]; + +/// FIPS 180-4 s. 5.3.3: the initial hash value H(0) for SHA-256. +pub(crate) const SHA256_H0: [u32; 8] = [ + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, +]; + +/// FIPS 180-4 s. 4.1.2 (4.2) Ch(x, y, z) = (x AND y) XOR (NOT x AND z) +/// Mutants note: the two masks are disjoint, so `^` and `|` give identical results here; a +/// surviving `^`/`|` swap in this function is an equivalent mutant, not a missing test. #[inline] -fn ch(x: u32, y: u32, z: u32) -> u32 { +const fn ch(x: u32, y: u32, z: u32) -> u32 { (x & y) ^ (!x & z) } +/// FIPS 180-4 s. 4.1.2 (4.3) Maj(x, y, z) = (x AND y) XOR (x AND z) XOR (y AND z). +/// Written in the equivalent form (x AND y) OR (z AND (x XOR y)), which saves an operation. +/// Mutants note: the two masks are disjoint, so `^` and `|` give identical results here; a +/// surviving `^`/`|` swap in this function is an equivalent mutant, not a missing test. #[inline] -fn maj(x: u32, y: u32, z: u32) -> u32 { +const fn maj(x: u32, y: u32, z: u32) -> u32 { (x & y) | (z & (x ^ y)) } +/// FIPS 180-4 s. 4.1.2 (4.4) Sigma0(x) = ROTR2(x) XOR ROTR13(x) XOR ROTR22(x) #[inline] -fn sum0(x: u32) -> u32 { +const fn sum0(x: u32) -> u32 { x.rotate_right(2) ^ x.rotate_right(13) ^ x.rotate_right(22) } +/// FIPS 180-4 s. 4.1.2 (4.5) Sigma1(x) = ROTR6(x) XOR ROTR11(x) XOR ROTR25(x) #[inline] -fn sum1(x: u32) -> u32 { +const fn sum1(x: u32) -> u32 { x.rotate_right(6) ^ x.rotate_right(11) ^ x.rotate_right(25) } +/// FIPS 180-4 s. 4.1.2 (4.6) sigma0(x) = ROTR7(x) XOR ROTR18(x) XOR SHR3(x) #[inline] -fn theta0(x: u32) -> u32 { +const fn theta0(x: u32) -> u32 { x.rotate_right(7) ^ x.rotate_right(18) ^ (x >> 3) } +/// FIPS 180-4 s. 4.1.2 (4.7) sigma1(x) = ROTR17(x) XOR ROTR19(x) XOR SHR10(x) #[inline] -fn theta1(x: u32) -> u32 { +const fn theta1(x: u32) -> u32 { x.rotate_right(17) ^ x.rotate_right(19) ^ (x >> 10) } +/// FIPS 180-4 s. 6.2.2, one iteration of the outer loop: absorbs a single 512-bit message block +/// into the hash value `s` (H(i-1) in, H(i) out). +/// +/// Written as a `const fn` (hence `while` rather than `for` loops) to match the SHA-512 side, so the +/// two compression functions can be read side by side against s. 6.2.2 and s. 6.4.2. +#[inline] +const fn compress_block(s: &mut [u32; 8], block: &[u8; 64]) { + // FIPS 180-4 s. 6.2.2 step 1: prepare the message schedule {W_t}. + let mut x = [0u32; 64]; + // FIPS 180-4 s. 6.2.2 step 1: W_t = M_t(i) for 0 <= t <= 15 (s. 5.2.1: sixteen big-endian 32-bit words). + let (words, _remainder) = block.as_chunks::<4>(); + let mut i = 0; + while i < 16 { + x[i] = u32::from_be_bytes(words[i]); + i += 1; + } + // FIPS 180-4 s. 6.2.2 step 1: W_t = sigma1(W_t-2) + W_t-7 + sigma0(W_t-15) + W_t-16 for 16 <= t <= 63. + while i < 64 { + x[i] = theta1(x[i - 2]) + .wrapping_add(x[i - 7]) + .wrapping_add(theta0(x[i - 15])) + .wrapping_add(x[i - 16]); + i += 1; + } + + // FIPS 180-4 s. 6.2.2 step 2: initialize the working variables a..h with H(i-1). + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = *s; + + // FIPS 180-4 s. 6.2.2 step 3: for t = 0 to 63, one round. The spec rotates the working variables + // (h = g, g = f, ...); here the rotation is done by renaming the variables passed to the macro + // instead, eight rounds at a time, which is equivalent and avoids the moves. The spec's T1 lands + // in the "$h" position, "$d" becomes d + T1, and T1 + T2 is then computed in place. + macro_rules! sha256_round { + ($a:ident,$b:ident,$c:ident,$d:ident,$e:ident,$f:ident,$g:ident,$h:ident,$t:ident) => { + // FIPS 180-4 s. 6.2.2 step 3: T1 = h + Sigma1(e) + Ch(e, f, g) + K_t + W_t + $h = $h + .wrapping_add(sum1($e)) + .wrapping_add(ch($e, $f, $g)) + .wrapping_add(SHA256_K[$t]) + .wrapping_add(x[$t]); + // FIPS 180-4 s. 6.2.2 step 3: e = d + T1 + $d = $d.wrapping_add($h); + // FIPS 180-4 s. 6.2.2 step 3: a = T1 + T2, where T2 = Sigma0(a) + Maj(a, b, c) + $h = $h.wrapping_add(sum0($a)).wrapping_add(maj($a, $b, $c)); + $t += 1; + }; + } + + let mut t: usize = 0; + while t < 64 { + sha256_round!(a, b, c, d, e, f, g, h, t); + sha256_round!(h, a, b, c, d, e, f, g, t); + sha256_round!(g, h, a, b, c, d, e, f, t); + sha256_round!(f, g, h, a, b, c, d, e, t); + sha256_round!(e, f, g, h, a, b, c, d, t); + sha256_round!(d, e, f, g, h, a, b, c, t); + sha256_round!(c, d, e, f, g, h, a, b, t); + sha256_round!(b, c, d, e, f, g, h, a, t); + } + + // FIPS 180-4 s. 6.2.2 step 4: H_j(i) = (working variable j) + H_j(i-1). + s[0] = s[0].wrapping_add(a); + s[1] = s[1].wrapping_add(b); + s[2] = s[2].wrapping_add(c); + s[3] = s[3].wrapping_add(d); + s[4] = s[4].wrapping_add(e); + s[5] = s[5].wrapping_add(f); + s[6] = s[6].wrapping_add(g); + s[7] = s[7].wrapping_add(h); +} + #[derive(Clone)] -pub(crate) struct Sha256State { +pub(crate) struct Sha256State { _params: core::marker::PhantomData, h: Secret<[u32; 8]>, } -impl Sha256State { +impl Sha256State { pub(crate) fn new() -> Self { let mut h = Secret::<[u32; 8]>::new(); - match PARAMS::OUTPUT_LEN * 8 { - 224 => { - h.copy_from_slice(&[ - 0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939, 0xFFC00B31, 0x68581511, - 0x64F98FA7, 0xBEFA4FA4, - ]); - Self { _params: core::marker::PhantomData, h } - } - 256 => { - h.copy_from_slice(&[ - 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, - 0x1F83D9AB, 0x5BE0CD19, - ]); - Self { _params: std::marker::PhantomData, h } - } - _ => panic!("Invalid SHA-2 bit size: {}", PARAMS::OUTPUT_LEN), - } + // FIPS 180-4 s. 6.2.1 step 1: set the initial hash value H(0) (s. 5.3.3, or s. 5.3.2 for SHA-224). + h.copy_from_slice(&PARAMS::H0); + Self { _params: core::marker::PhantomData, h } } fn compress(&mut self, blocks: &[[u8; 64]]) { - let mut x = [0u32; 64]; - - // infallible; just unwrapping the [u32; 8] and re-casting to itself. - let s = &mut *self.h; - let &mut [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = s; - + // FIPS 180-4 s. 6.2.2: each message block M(1), ..., M(N) is processed in order. for block in blocks { - let (chunks, _remainder) = block.as_chunks::<4>(); - for (i, w) in x[..16].iter_mut().zip(chunks) { - *i = u32::from_be_bytes(*w); - } - - for i in 16..64 { - x[i] = theta1(x[i - 2]) - .wrapping_add(x[i - 7]) - .wrapping_add(theta0(x[i - 15])) - .wrapping_add(x[i - 16]); - } - - macro_rules! sha256_round { - ($a:ident,$b:ident,$c:ident,$d:ident,$e:ident,$f:ident,$g:ident,$h:ident,$t:ident,$K:ident,$x:ident) => { - $h = $h - .wrapping_add(sum1($e)) - .wrapping_add(ch($e, $f, $g)) - .wrapping_add($K[$t]) - .wrapping_add($x[$t]); - $d = $d.wrapping_add($h); - $h = $h.wrapping_add(sum0($a)).wrapping_add(maj($a, $b, $c)); - $t += 1; - }; - } - - let mut t: usize = 0; - for _ in 0..8 { - sha256_round!(a, b, c, d, e, f, g, h, t, SHA256_K, x); - sha256_round!(h, a, b, c, d, e, f, g, t, SHA256_K, x); - sha256_round!(g, h, a, b, c, d, e, f, t, SHA256_K, x); - sha256_round!(f, g, h, a, b, c, d, e, t, SHA256_K, x); - sha256_round!(e, f, g, h, a, b, c, d, t, SHA256_K, x); - sha256_round!(d, e, f, g, h, a, b, c, t, SHA256_K, x); - sha256_round!(c, d, e, f, g, h, a, b, t, SHA256_K, x); - sha256_round!(b, c, d, e, f, g, h, a, t, SHA256_K, x); - } - - a = a.wrapping_add(s[0]); - b = b.wrapping_add(s[1]); - c = c.wrapping_add(s[2]); - d = d.wrapping_add(s[3]); - e = e.wrapping_add(s[4]); - f = f.wrapping_add(s[5]); - g = g.wrapping_add(s[6]); - h = h.wrapping_add(s[7]); - - s[0] = a; - s[1] = b; - s[2] = c; - s[3] = d; - s[4] = e; - s[5] = f; - s[6] = g; - s[7] = h; + compress_block(&mut self.h, block); } } } @@ -144,17 +170,15 @@ impl Sha256State { /// This uses a private bound so that you cannot instantiate it directly and have to use the /// provided and NIST-approved parameters. #[derive(Clone)] -pub struct SHA256Internal { +pub struct SHA256Internal { _params: core::marker::PhantomData, state: Sha256State, byte_count: u64, x_buf: Secret<[u8; 64]>, x_buf_off: usize, - // TODO: Investigate whether maximum message size (according to FIPS 180-4) should be added - // (2^64 for SHA256 and 2^128 for SHA512) } -impl SHA256Internal { +impl SHA256Internal { /// Creates a new SHA256 instance, ready for use. pub fn new() -> Self { Self { @@ -167,18 +191,86 @@ impl SHA256Internal { } } -impl Default for SHA256Internal { +impl SHA256Internal { + /// Pads and compresses the final block(s) as per FIPS 180-4 s. 5.1.1, then writes the digest. + /// + /// The `num_partial_bits` (0..=7, validated by the caller) trailing message bits are the most + /// significant bits of `partial_byte`, leading bit first: the ASN.1 BIT STRING order of + /// X.690 s. 8.6.2.1, which is also how FIPS 180-4 s. 3.1 numbers the bits of a message byte. So + /// they are used in place, the low `8 - num_partial_bits` bits are ignored, and the mandatory + /// "1" padding bit follows the message bits immediately in the same byte. + /// + /// Returns the number of bytes written (`min(output.len(), OUTPUT_LEN)`); a shorter output buffer + /// truncates the digest, a longer one is zero-filled past the digest. + fn do_final_internal( + mut self, + partial_byte: u8, + num_partial_bits: usize, + output: &mut [u8], + ) -> usize { + debug_assert!(num_partial_bits <= 7); + output.fill(0); + + let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); + + // FIPS 180-4 s. 5.1.1: append the bit "1" to the end of the message. The message bits are the + // top num_partial_bits bits of partial_byte, so the final message byte is [those bits] [1] [0...]; + // with no partial bits this is the familiar 0x80. The mask is built in u16 so that the 8-bit + // shift for num_partial_bits == 0 cannot overflow (0xFF00 >> 0 truncates to 0x00). + let mask = (0xFF00u16 >> num_partial_bits) as u8; + // Mutants note: the masked message bits and the padding bit occupy disjoint bit positions, so + // `|` and `^` give identical results here; a surviving `|`/`^` swap is an equivalent mutant. + let pad_byte = (partial_byte & mask) | (0x80u8 >> num_partial_bits); + + self.x_buf[self.x_buf_off] = pad_byte; + self.x_buf_off += 1; + + // FIPS 180-4 s. 5.1.1: if fewer than 64 bits remain for l, the k zero bits run into a second block. + if self.x_buf_off > 56 { + self.x_buf[self.x_buf_off..].fill(0x00); + self.state.compress(slice::from_ref(&self.x_buf)); + self.x_buf_off = 0; + } + + // FIPS 180-4 s. 5.1.1: k zero bits so that l + 1 + k = 448 mod 512, then the 64-bit big-endian + // message length l in bits. + self.x_buf[self.x_buf_off..56].fill(0x00); + // byte_count is a byte counter, so l = (byte_count << 3) | num_partial_bits (the low three bits + // of byte_count << 3 are zero). + // Mutants note: the low three bits of byte_count << 3 are zero, so `|` and `^` give identical + // results here; a surviving `|`/`^` swap is an equivalent mutant. + let bit_len: u64 = (self.byte_count << 3) | (num_partial_bits as u64); + self.x_buf[56..64].copy_from_slice(&bit_len.to_be_bytes()); + self.state.compress(slice::from_ref(&self.x_buf)); + + // FIPS 180-4 s. 6.2.2: the digest is H_0(N) || ... || H_7(N) (big-endian words), truncated to the + // left-most OUTPUT_LEN bytes (s. 6.3 exception 2 for SHA-224), and further to the caller's + // buffer if that is shorter. + let h = &self.state.h; + for i in 0..(n / 4) { + output[i * 4..i * 4 + 4].copy_from_slice(&h[i].to_be_bytes()); + } + if !n.is_multiple_of(4) { + output[((n / 4) * 4)..((n / 4) * 4) + (n % 4)] + .copy_from_slice(&h[n / 4].to_be_bytes()[0..(n % 4)]); + } + + n + } +} + +impl Default for SHA256Internal { fn default() -> Self { Self::new() } } -impl Algorithm for SHA256Internal { +impl Algorithm for SHA256Internal { const ALG_NAME: &'static str = PARAMS::ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; } -impl Hash for SHA256Internal { +impl Hash for SHA256Internal { /// As per FIPS 180-4 Figure 1 fn block_bitlen(&self) -> usize { 512 @@ -204,8 +296,13 @@ impl Hash for SHA256Internal { fn do_update(&mut self, block: &[u8]) { let len = block.len(); - // TODO: Check there is enough space left in 'byte_count' to allow this operation, - // TODO: although overflowing a u64 is unlikely to happen in practice, and rust will throw an error anyway. + // FIPS 180-4 s. 5.1.1: do_final_internal encodes l in a 64-bit field as `byte_count << 3`, + // and a left shift discards rather than panics, so past MAX_MESSAGE_BYTES the digest would + // silently be that of a message 2^64 bits shorter. do_update returns (), hence debug-only. + debug_assert!( + self.byte_count.checked_add(len as u64).is_some_and(|total| total <= MAX_MESSAGE_BYTES), + "message exceeds the FIPS 180-4 limit of {MAX_MESSAGE_BYTES} bytes for SHA-224/SHA-256" + ); self.byte_count += len as u64; let available = 64 - self.x_buf_off; @@ -225,6 +322,7 @@ impl Hash for SHA256Internal { self.state.compress(slice::from_ref(&self.x_buf)); } + // FIPS 180-4 s. 5.2.1: the message is parsed into 512-bit blocks; a partial trailing block waits in x_buf. let (chunks, remainder) = block.as_chunks::<64>(); self.state.compress(chunks); @@ -240,63 +338,35 @@ impl Hash for SHA256Internal { output } - fn do_final_out(mut self, output: &mut [u8]) -> usize { - output.fill(0); - - let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); - - let bit_len: u64 = self.byte_count << 3; - - self.x_buf[self.x_buf_off] = 0x80; - self.x_buf_off += 1; - - if self.x_buf_off > 56 { - self.x_buf[self.x_buf_off..].fill(0x00); - self.state.compress(slice::from_ref(&self.x_buf)); - self.x_buf_off = 0; - } - - self.x_buf[self.x_buf_off..56].fill(0x00); - self.x_buf[56..64].copy_from_slice(&bit_len.to_be_bytes()); - self.state.compress(slice::from_ref(&self.x_buf)); - - let h = &self.state.h; - - // let n = output.len(); - for i in 0..(n / 4) { - output[i * 4..i * 4 + 4].copy_from_slice(&h[i].to_be_bytes()); - } - if !n.is_multiple_of(4) { - output[((n / 4) * 4)..((n / 4) * 4) + (n % 4)] - .copy_from_slice(&h[n / 4].to_be_bytes()[0..(n % 4)]); - } - - n + fn do_final_out(self, output: &mut [u8]) -> usize { + // A whole-byte message is the zero-partial-bits case of the general padding. + self.do_final_internal(0, 0, output) } - /// TODO: This is defined in FIPS 180-4 s. 5.1.2 - /// TODO: - /// TODO: It can be implemented if required - #[allow(unused)] fn do_final_partial_bits( self, partial_byte: u8, num_partial_bits: usize, ) -> Result, HashError> { - unimplemented!() + let mut output = vec![0u8; PARAMS::OUTPUT_LEN]; + self.do_final_partial_bits_out(partial_byte, num_partial_bits, &mut output)?; + Ok(output) } - /// TODO: This is defined in FIPS 180-4 s. 5.1.2 - /// TODO: - /// TODO: It can be implemented if required - #[allow(unused)] + /// FIPS 180-4 s. 5.1: bit-oriented messages. The `num_partial_bits` most significant bits of + /// `partial_byte` (ASN.1 BIT STRING order, leading bit first) are appended to the message before + /// padding; the low bits are ignored. `num_partial_bits == 0` behaves exactly like + /// [`Hash::do_final_out`]. fn do_final_partial_bits_out( self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8], ) -> Result { - unimplemented!() + if num_partial_bits > 7 { + return Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")); + } + Ok(self.do_final_internal(partial_byte, num_partial_bits, output)) } fn max_security_strength(&self) -> SecurityStrength { @@ -307,7 +377,7 @@ impl Hash for SHA256Internal { /// Length in bytes of the serialized state of SHA224 and SHA256. pub const SUSPENDED_SHA256_STATE_LEN: usize = 108; -impl Suspendable for SHA256Internal { +impl Suspendable for SHA256Internal { fn suspend(self) -> [u8; SUSPENDED_SHA256_STATE_LEN] { debug_assert_eq!(SUSPENDED_SHA256_STATE_LEN, 108); diff --git a/crypto/sha2/src/sha512.rs b/crypto/sha2/src/sha512.rs index c31e3065..cfbaf4cc 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -1,10 +1,13 @@ -use crate::SHA2Params; +use crate::SHA512InitValue; use bouncycastle_core::errors::{HashError, SuspendableError}; -use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; -use bouncycastle_core::traits::{Algorithm, Hash, SecurityStrength, Suspendable}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, Suspendable}; +use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_utils::{min, secret::Secret}; use core::slice; +/// FIPS 180-4 s. 4.2.3: the eighty 64-bit constants K0..K79 shared by SHA-384, SHA-512, +/// SHA-512/224 and SHA-512/256. const SHA512_K: [u64; 80] = [ 0x428A2F98D728AE22, 0x7137449123EF65CD, 0xB5C0FBCFEC4D3B2F, 0xE9B5DBA58189DBBC, 0x3956C25BF348B538, 0x59F111F1B605D019, 0x923F82A4AF194F9B, 0xAB1C5ED5DA6D8118, @@ -28,149 +31,310 @@ const SHA512_K: [u64; 80] = [ 0x4CC5D4BECB3E42B6, 0x597F299CFC657E2A, 0x5FCB6FAB3AD6FAEC, 0x6C44198C4A475817, ]; +/// FIPS 180-4 s. 5.3.4: the initial hash value H(0) for SHA-384. +pub(crate) const SHA384_H0: [u64; 8] = [ + 0xCBBB9D5DC1059ED8, 0x629A292A367CD507, 0x9159015A3070DD17, 0x152FECD8F70E5939, + 0x67332667FFC00B31, 0x8EB44A8768581511, 0xDB0C2E0D64F98FA7, 0x47B5481DBEFA4FA4, +]; + +/// FIPS 180-4 s. 5.3.5: the initial hash value H(0) for SHA-512. +pub(crate) const SHA512_H0: [u64; 8] = [ + 0x6A09E667F3BCC908, 0xBB67AE8584CAA73B, 0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1, + 0x510E527FADE682D1, 0x9B05688C2B3E6C1F, 0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179, +]; + +/// Rejects, at compile time, every `t` for which SHA-512/t is not defined or not representable +/// here. See [`sha512t_h0`] for where each rule comes from; the multiple-of-8 rule is this crate's, +/// the rest are FIPS 180-4 s. 5.3.6's. +pub(crate) const fn check_t(t: usize) { + // FIPS 180-4 s. 5.3.6: "t is any positive integer ... such that t < 512". + assert!(t > 0, "FIPS 180-4 s. 5.3.6: t must be a positive integer"); + assert!(t < 512, "FIPS 180-4 s. 5.3.6: t must be less than 512"); + // FIPS 180-4 s. 5.3.6: "and t is not 384". SHA-384 is its own algorithm (s. 5.3.4 / s. 6.5) + // with an IV that is not the one this function would generate. + assert!(t != 384, "FIPS 180-4 s. 5.3.6: t must not be 384 -- use SHA384 instead"); + // This crate's restriction, not the standard's: the digest must be a whole number of bytes. + assert!(t.is_multiple_of(8), "SHA-512/t here requires t to be a multiple of 8"); +} + +/// The number of decimal digits in `t`, i.e. the length of the "t" part of the ASCII string +/// "SHA-512/t" that FIPS 180-4 s. 5.3.6 hashes. `t < 512`, so one, two or three. +pub(crate) const fn t_digits(t: usize) -> usize { + if t >= 100 { + 3 + } else if t >= 10 { + 2 + } else { + 1 + } +} + +/// This crate's algorithm name for SHA-512/t, `"SHA512/t"` with `t` in decimal -- `"SHA512/224"`, +/// `"SHA512/256"`, `"SHA512/8"` -- returned NUL-padded to the longest form, with +/// [`alg_name_len`] giving the significant prefix. Two pieces because +/// [`Algorithm::ALG_NAME`](bouncycastle_core::traits::Algorithm::ALG_NAME) is a `&'static str` and +/// a const generic cannot size the buffer to the digit count. +/// +/// Note this is *not* the s. 5.3.6 spelling: the string the IV Generation Function hashes is +/// "SHA-512/t", with the hyphen, and is built separately in [`sha512t_h0`]. This one follows the +/// crate's existing names, [`SHA512_224_NAME`](crate::SHA512_224_NAME) and +/// [`SHA512_256_NAME`](crate::SHA512_256_NAME), which it has to keep reproducing exactly. +pub(crate) const fn alg_name_bytes(t: usize) -> [u8; ALG_NAME_BUF_LEN] { + let mut buf = [b'S', b'H', b'A', b'5', b'1', b'2', b'/', 0, 0, 0]; + let mut i = 7; + if t >= 100 { + buf[i] = b'0' + (t / 100) as u8; + i += 1; + } + if t >= 10 { + buf[i] = b'0' + ((t / 10) % 10) as u8; + i += 1; + } + buf[i] = b'0' + (t % 10) as u8; + buf +} + +/// Size of the [`alg_name_bytes`] buffer: `"SHA512/"` plus the most digits `t` can have. +pub(crate) const ALG_NAME_BUF_LEN: usize = 7 + 3; + +/// The significant length of [`alg_name_bytes`]'s output for `t`. +pub(crate) const fn alg_name_len(t: usize) -> usize { + 7 + t_digits(t) +} + +/// FIPS 180-4 s. 5.3.6 "SHA-512/t IV Generation Function": computes the initial hash value H(0) +/// for SHA-512/t. +/// +/// Quoting the procedure: +/// +/// > Denote H(0)' to be the initial hash value of SHA-512 as specified in Section 5.3.5 above. +/// > +/// > Denote H(0)'' to be the initial hash value computed below. +/// > +/// > H(0) is the IV for SHA-512/t. +/// > +/// > For i = 0 to 7 { Hi(0)'' = Hi(0)' xor a5a5a5a5a5a5a5a5(in hex). } +/// > +/// > H(0) = SHA-512 ("SHA-512/t") using H(0)'' as the IV, where t is the specific truncation value. +/// +/// where, per the same section, "t is any positive integer without a leading zero such that t < 512, +/// and t is not 384", and "SHA-512/t" is the ASCII string with t written in decimal (so for t = 256 +/// the message is the 11 bytes `53 48 41 2D 35 31 32 2F 32 35 36`). +/// +/// Deliberate deviation from s. 5.3.6: `t` must additionally be a multiple of 8. The section +/// allows "any positive integer" below 512, including values that are not a whole number of bytes, +/// but [`Hash`](bouncycastle_core::traits::Hash) is byte-oriented -- `OUTPUT_LEN` is a byte count +/// and `do_final_out` writes whole bytes -- so a t of, say, 100 bits has no representable digest +/// here. BC Java's `SHA512tDigest` imposes the same restriction ("bitLength needs to be a multiple +/// of 8"), so the two libraries accept exactly the same set of truncations. +/// +/// This is a `const fn` so that the IV is computed at compile time, which is also what makes the +/// rules above compile errors rather than panics: an unusable `t` fails the build at the point the +/// parameter set is instantiated. +pub(crate) const fn sha512t_h0(t: usize) -> [u64; 8] { + check_t(t); + + // FIPS 180-4 s. 5.3.6: H(0)'' = H(0)', the SHA-512 initial hash value (s. 5.3.5), with each word XOR a5a5a5a5a5a5a5a5. + let mut h = SHA512_H0; + let mut i = 0; + while i < 8 { + h[i] ^= 0xA5A5A5A5A5A5A5A5; + i += 1; + } + + // FIPS 180-4 s. 5.3.6: the message is the ASCII string "SHA-512/t" (at most 11 bytes, so one + // block). It is built directly in its padded form (s. 5.1.2) inside a single 1024-bit block + // (s. 5.2.2). + let mut block = [0u8; 128]; + let prefix = b"SHA-512/"; + let mut len = 0; + while len < prefix.len() { + block[len] = prefix[len]; + len += 1; + } + // FIPS 180-4 s. 5.3.6: t written in decimal "without a leading zero" ("t is 256, but not + // 0256"). t < 512, so one, two or three digits, and the leading digit is emitted only when it + // is significant -- writing a fixed three digits would produce the "0256" spelling the section + // forbids, and hence the wrong IV, for every t below 100. + if t >= 100 { + block[len] = b'0' + (t / 100) as u8; + len += 1; + } + if t >= 10 { + block[len] = b'0' + ((t / 10) % 10) as u8; + len += 1; + } + block[len] = b'0' + (t % 10) as u8; + len += 1; + + // FIPS 180-4 s. 5.1.2: append the bit "1", then k zero bits (the rest of the block is already zero). + block[len] = 0x80; + // FIPS 180-4 s. 5.1.2: the final 128 bits are the message length l in bits; l < 2^64 so bytes 112..120 stay 0. + let bit_len = (len as u64) * 8; + let bit_len_bytes = bit_len.to_be_bytes(); + let mut i = 0; + while i < 8 { + block[120 + i] = bit_len_bytes[i]; + i += 1; + } + + // FIPS 180-4 s. 5.3.6: H(0) = SHA-512("SHA-512/t") using H(0)'' as the IV, i.e. one pass of s. 6.4.2. + compress_block(&mut h, &block); + h +} + +/// FIPS 180-4 s. 4.1.3 (4.8) Ch(x, y, z) = (x AND y) XOR (NOT x AND z) +/// Mutants note: the two masks are disjoint, so `^` and `|` give identical results here; a +/// surviving `^`/`|` swap in this function is an equivalent mutant, not a missing test. #[inline] -fn ch(x: u64, y: u64, z: u64) -> u64 { +const fn ch(x: u64, y: u64, z: u64) -> u64 { (x & y) ^ (!x & z) } +/// FIPS 180-4 s. 4.1.3 (4.9) Maj(x, y, z) = (x AND y) XOR (x AND z) XOR (y AND z). +/// Written in the equivalent form (x AND y) OR (z AND (x XOR y)), which saves an operation. +/// Mutants note: the two masks are disjoint, so `^` and `|` give identical results here; a +/// surviving `^`/`|` swap in this function is an equivalent mutant, not a missing test. #[inline] -fn maj(x: u64, y: u64, z: u64) -> u64 { +const fn maj(x: u64, y: u64, z: u64) -> u64 { (x & y) | (z & (x ^ y)) } +/// FIPS 180-4 s. 4.1.3 (4.10) Sigma0(x) = ROTR28(x) XOR ROTR34(x) XOR ROTR39(x) #[inline] -fn sum0(x: u64) -> u64 { +const fn sum0(x: u64) -> u64 { x.rotate_right(28) ^ x.rotate_right(34) ^ x.rotate_right(39) } +/// FIPS 180-4 s. 4.1.3 (4.11) Sigma1(x) = ROTR14(x) XOR ROTR18(x) XOR ROTR41(x) #[inline] -fn sum1(x: u64) -> u64 { +const fn sum1(x: u64) -> u64 { x.rotate_right(14) ^ x.rotate_right(18) ^ x.rotate_right(41) } +/// FIPS 180-4 s. 4.1.3 (4.12) sigma0(x) = ROTR1(x) XOR ROTR8(x) XOR SHR7(x) #[inline] -fn theta0(x: u64) -> u64 { +const fn theta0(x: u64) -> u64 { x.rotate_right(1) ^ x.rotate_right(8) ^ (x >> 7) } +/// FIPS 180-4 s. 4.1.3 (4.13) sigma1(x) = ROTR19(x) XOR ROTR61(x) XOR SHR6(x) #[inline] -fn theta1(x: u64) -> u64 { +const fn theta1(x: u64) -> u64 { x.rotate_right(19) ^ x.rotate_right(61) ^ (x >> 6) } -// todo -- cleanup -// #[derive(Clone, Copy)] +/// FIPS 180-4 s. 6.4.2, one iteration of the outer loop: absorbs a single 1024-bit message block +/// into the hash value `s` (H(i-1) in, H(i) out). +/// +/// This is a `const fn` (hence `while` rather than `for` loops) so that [`sha512t_h0`] can run it +/// at compile time. At runtime it is ordinary code, and is the hot path of every SHA-512 variant. +#[inline] +const fn compress_block(s: &mut [u64; 8], block: &[u8; 128]) { + // FIPS 180-4 s. 6.4.2 step 1: prepare the message schedule {W_t}. + let mut x = [0u64; 80]; + // FIPS 180-4 s. 6.4.2 step 1: W_t = M_t(i) for 0 <= t <= 15 (s. 5.2.2: sixteen big-endian 64-bit words). + let (words, _remainder) = block.as_chunks::<8>(); + let mut i = 0; + while i < 16 { + x[i] = u64::from_be_bytes(words[i]); + i += 1; + } + // FIPS 180-4 s. 6.4.2 step 1: W_t = sigma1(W_t-2) + W_t-7 + sigma0(W_t-15) + W_t-16 for 16 <= t <= 79. + while i < 80 { + x[i] = theta1(x[i - 2]) + .wrapping_add(x[i - 7]) + .wrapping_add(theta0(x[i - 15])) + .wrapping_add(x[i - 16]); + i += 1; + } + + // FIPS 180-4 s. 6.4.2 step 2: initialize the working variables a..h with H(i-1). + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = *s; + + // FIPS 180-4 s. 6.4.2 step 3: for t = 0 to 79, one round. The spec rotates the working variables + // (h = g, g = f, ...); here the rotation is done by renaming the variables passed to the macro + // instead, eight rounds at a time, which is equivalent and avoids the moves. The spec's T1 lands + // in the "$h" position, "$d" becomes d + T1, and T1 + T2 is then computed in place. + macro_rules! sha512_round { + ($a:ident,$b:ident,$c:ident,$d:ident,$e:ident,$f:ident,$g:ident,$h:ident,$t:ident) => { + // FIPS 180-4 s. 6.4.2 step 3: T1 = h + Sigma1(e) + Ch(e, f, g) + K_t + W_t + $h = $h + .wrapping_add(sum1($e)) + .wrapping_add(ch($e, $f, $g)) + .wrapping_add(SHA512_K[$t]) + .wrapping_add(x[$t]); + // FIPS 180-4 s. 6.4.2 step 3: e = d + T1 + $d = $d.wrapping_add($h); + // FIPS 180-4 s. 6.4.2 step 3: a = T1 + T2, where T2 = Sigma0(a) + Maj(a, b, c) + $h = $h.wrapping_add(sum0($a)).wrapping_add(maj($a, $b, $c)); + $t += 1; + }; + } + + let mut t: usize = 0; + while t < 80 { + sha512_round!(a, b, c, d, e, f, g, h, t); + sha512_round!(h, a, b, c, d, e, f, g, t); + sha512_round!(g, h, a, b, c, d, e, f, t); + sha512_round!(f, g, h, a, b, c, d, e, t); + sha512_round!(e, f, g, h, a, b, c, d, t); + sha512_round!(d, e, f, g, h, a, b, c, t); + sha512_round!(c, d, e, f, g, h, a, b, t); + sha512_round!(b, c, d, e, f, g, h, a, t); + } + + // FIPS 180-4 s. 6.4.2 step 4: H_j(i) = (working variable j) + H_j(i-1). + s[0] = s[0].wrapping_add(a); + s[1] = s[1].wrapping_add(b); + s[2] = s[2].wrapping_add(c); + s[3] = s[3].wrapping_add(d); + s[4] = s[4].wrapping_add(e); + s[5] = s[5].wrapping_add(f); + s[6] = s[6].wrapping_add(g); + s[7] = s[7].wrapping_add(h); +} + #[derive(Clone)] -pub(crate) struct Sha512State { - _params: std::marker::PhantomData, +pub(crate) struct Sha512State { + _params: core::marker::PhantomData, h: Secret<[u64; 8]>, } -impl Sha512State { +impl Sha512State { pub(crate) fn new() -> Self { let mut h = Secret::<[u64; 8]>::new(); - match PARAMS::OUTPUT_LEN * 8 { - 384 => { - h.copy_from_slice(&[ - 0xCBBB9D5DC1059ED8, 0x629A292A367CD507, 0x9159015A3070DD17, 0x152FECD8F70E5939, - 0x67332667FFC00B31, 0x8EB44A8768581511, 0xDB0C2E0D64F98FA7, 0x47B5481DBEFA4FA4, - ]); - Self { _params: std::marker::PhantomData, h } - } - 512 => { - h.copy_from_slice(&[ - 0x6A09E667F3BCC908, 0xBB67AE8584CAA73B, 0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1, - 0x510E527FADE682D1, 0x9B05688C2B3E6C1F, 0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179, - ]); - Self { _params: std::marker::PhantomData, h } - } - _ => panic!("Invalid SHA-2 bit size"), - } + // FIPS 180-4 s. 6.4.1 step 1: set the initial hash value H(0) (s. 5.3.4 / 5.3.5 / 5.3.6 per variant). + h.copy_from_slice(&PARAMS::H0); + Self { _params: core::marker::PhantomData, h } } fn compress(&mut self, blocks: &[[u8; 128]]) { - let mut x = [0u64; 80]; - - let s = &mut *self.h; - let &mut [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = s; - + // FIPS 180-4 s. 6.4.2: each message block M(1), ..., M(N) is processed in order. for block in blocks { - let (chunks, _remainder) = block.as_chunks::<8>(); - for (i, w) in x[..16].iter_mut().zip(chunks) { - *i = u64::from_be_bytes(*w); - } - - for i in 16..80 { - x[i] = theta1(x[i - 2]) - .wrapping_add(x[i - 7]) - .wrapping_add(theta0(x[i - 15])) - .wrapping_add(x[i - 16]); - } - - macro_rules! sha512_round { - ($a:ident,$b:ident,$c:ident,$d:ident,$e:ident,$f:ident,$g:ident,$h:ident,$t:ident,$K:ident,$x:ident) => { - $h = $h - .wrapping_add(sum1($e)) - .wrapping_add(ch($e, $f, $g)) - .wrapping_add($K[$t]) - .wrapping_add($x[$t]); - $d = $d.wrapping_add($h); - $h = $h.wrapping_add(sum0($a)).wrapping_add(maj($a, $b, $c)); - $t += 1; - }; - } - - let mut t: usize = 0; - for _ in 0..10 { - sha512_round!(a, b, c, d, e, f, g, h, t, SHA512_K, x); - sha512_round!(h, a, b, c, d, e, f, g, t, SHA512_K, x); - sha512_round!(g, h, a, b, c, d, e, f, t, SHA512_K, x); - sha512_round!(f, g, h, a, b, c, d, e, t, SHA512_K, x); - sha512_round!(e, f, g, h, a, b, c, d, t, SHA512_K, x); - sha512_round!(d, e, f, g, h, a, b, c, t, SHA512_K, x); - sha512_round!(c, d, e, f, g, h, a, b, t, SHA512_K, x); - sha512_round!(b, c, d, e, f, g, h, a, t, SHA512_K, x); - } - - a = a.wrapping_add(s[0]); - b = b.wrapping_add(s[1]); - c = c.wrapping_add(s[2]); - d = d.wrapping_add(s[3]); - e = e.wrapping_add(s[4]); - f = f.wrapping_add(s[5]); - g = g.wrapping_add(s[6]); - h = h.wrapping_add(s[7]); - - s[0] = a; - s[1] = b; - s[2] = c; - s[3] = d; - s[4] = e; - s[5] = f; - s[6] = g; - s[7] = h; + compress_block(&mut self.h, block); } } } /// Internal struct for SHA512. /// This uses a private bound so that you cannot instantiate it directly and have to use the -/// provided and NIST-approved parameters. +/// parameter sets this crate provides. #[derive(Clone)] -pub struct SHA512Internal { - _params: std::marker::PhantomData, +pub struct SHA512Internal { + _params: core::marker::PhantomData, state: Sha512State, - // NOTE The code currently only supports 2^67 bits, not the full 2^128 + // NOTE: FIPS 180-4 allows messages up to 2^128 bits; this counter supports 2^67 bits (2^64 bytes). byte_count: u64, x_buf: Secret<[u8; 128]>, x_buf_off: usize, } -impl SHA512Internal { +impl SHA512Internal { /// Creates a new SHA512 instance, ready for use. pub fn new() -> Self { Self { - _params: std::marker::PhantomData, + _params: core::marker::PhantomData, state: Sha512State::::new(), byte_count: 0, x_buf: Secret::new(), @@ -179,18 +343,88 @@ impl SHA512Internal { } } -impl Default for SHA512Internal { +impl SHA512Internal { + /// Pads and compresses the final block(s) as per FIPS 180-4 s. 5.1.2, then writes the digest. + /// + /// The `num_partial_bits` (0..=7, validated by the caller) trailing message bits are the most + /// significant bits of `partial_byte`, leading bit first: the ASN.1 BIT STRING order of + /// X.690 s. 8.6.2.1, which is also how FIPS 180-4 s. 3.1 numbers the bits of a message byte. So + /// they are used in place, the low `8 - num_partial_bits` bits are ignored, and the mandatory + /// "1" padding bit follows the message bits immediately in the same byte. + /// + /// Returns the number of bytes written (`min(output.len(), OUTPUT_LEN)`); a shorter output buffer + /// truncates the digest, a longer one is zero-filled past the digest. + fn do_final_internal( + mut self, + partial_byte: u8, + num_partial_bits: usize, + output: &mut [u8], + ) -> usize { + debug_assert!(num_partial_bits <= 7); + output.fill(0); + + let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); + + // FIPS 180-4 s. 5.1.2: append the bit "1" to the end of the message. The message bits are the + // top num_partial_bits bits of partial_byte, so the final message byte is [those bits] [1] [0...]; + // with no partial bits this is the familiar 0x80. The mask is built in u16 so that the 8-bit + // shift for num_partial_bits == 0 cannot overflow (0xFF00 >> 0 truncates to 0x00). + let mask = (0xFF00u16 >> num_partial_bits) as u8; + // Mutants note: the masked message bits and the padding bit occupy disjoint bit positions, so + // `|` and `^` give identical results here; a surviving `|`/`^` swap is an equivalent mutant. + let pad_byte = (partial_byte & mask) | (0x80u8 >> num_partial_bits); + + self.x_buf[self.x_buf_off] = pad_byte; + self.x_buf_off += 1; + + // FIPS 180-4 s. 5.1.2: if fewer than 128 bits remain for l, the k zero bits run into a second block. + if self.x_buf_off > 112 { + self.x_buf[self.x_buf_off..].fill(0x00); + self.state.compress(slice::from_ref(&self.x_buf)); + self.x_buf_off = 0; + } + + // FIPS 180-4 s. 5.1.2: k zero bits so that l + 1 + k = 896 mod 1024, then the 128-bit big-endian + // message length l in bits. + self.x_buf[self.x_buf_off..112].fill(0x00); + // byte_count is a byte counter, so the high 64 bits of l are byte_count >> 61 and the low 64 + // bits are (byte_count << 3) | num_partial_bits (the low three bits of byte_count << 3 are zero). + let bit_len_hi: u64 = self.byte_count >> 61; + // Mutants note: the low three bits of byte_count << 3 are zero, so `|` and `^` give identical + // results here; a surviving `|`/`^` swap is an equivalent mutant. + let bit_len_lo: u64 = (self.byte_count << 3) | (num_partial_bits as u64); + self.x_buf[112..120].copy_from_slice(&bit_len_hi.to_be_bytes()); + self.x_buf[120..128].copy_from_slice(&bit_len_lo.to_be_bytes()); + self.state.compress(slice::from_ref(&self.x_buf)); + + // FIPS 180-4 s. 6.4.2: the digest is H_0(N) || ... || H_7(N) (big-endian words), truncated to the + // left-most OUTPUT_LEN bytes (s. 6.5 / 6.6 / 6.7 exception 2 for SHA-384, SHA-512/224 and SHA-512/256), and further to the caller's + // buffer if that is shorter. + let h = &self.state.h; + for i in 0..(n / 8) { + output[i * 8..i * 8 + 8].copy_from_slice(&h[i].to_be_bytes()); + } + if !n.is_multiple_of(8) { + output[((n / 8) * 8)..((n / 8) * 8) + (n % 8)] + .copy_from_slice(&h[n / 8].to_be_bytes()[0..(n % 8)]); + } + + n + } +} + +impl Default for SHA512Internal { fn default() -> Self { Self::new() } } -impl Algorithm for SHA512Internal { +impl Algorithm for SHA512Internal { const ALG_NAME: &'static str = PARAMS::ALG_NAME; const MAX_SECURITY_STRENGTH: SecurityStrength = PARAMS::MAX_SECURITY_STRENGTH; } -impl Hash for SHA512Internal { +impl Hash for SHA512Internal { /// As per FIPS 180-4 Figure 1 fn block_bitlen(&self) -> usize { 1024 @@ -216,8 +450,9 @@ impl Hash for SHA512Internal { fn do_update(&mut self, block: &[u8]) { let len = block.len(); - // TODO: Check there is enough space left in 'byte_count' to allow this operation, - // TODO: although overflowing a u64 is unlikely to happen in practice, and rust will throw an error anyway. + // FIPS 180-4 s. 5.1.2: do_final_internal writes the whole 128-bit field, carrying the top + // three bits of byte_count in bit_len_hi, so unlike SHA-256 nothing is lost to the shift. + // The limit is byte_count itself at 2^64 bytes, far inside the l < 2^128 bits of Table 1. self.byte_count += len as u64; let available = 128 - self.x_buf_off; @@ -236,6 +471,7 @@ impl Hash for SHA512Internal { //self.x_buf_off = 0; } + // FIPS 180-4 s. 5.2.2: the message is parsed into 1024-bit blocks; a partial trailing block waits in x_buf. let (chunks, remainder) = block.as_chunks::<128>(); self.state.compress(chunks); @@ -251,64 +487,35 @@ impl Hash for SHA512Internal { output } - fn do_final_out(mut self, output: &mut [u8]) -> usize { - output.fill(0); - - let n = *min(&output.len(), &PARAMS::OUTPUT_LEN); - - let bit_len_hi: u64 = self.byte_count >> 61; - let bit_len_lo: u64 = self.byte_count << 3; - - self.x_buf[self.x_buf_off] = 0x80; - self.x_buf_off += 1; - - if self.x_buf_off > 112 { - self.x_buf[self.x_buf_off..].fill(0x00); - self.state.compress(slice::from_ref(&self.x_buf)); - self.x_buf_off = 0; - } - - self.x_buf[self.x_buf_off..112].fill(0x00); - self.x_buf[112..120].copy_from_slice(&bit_len_hi.to_be_bytes()); - self.x_buf[120..128].copy_from_slice(&bit_len_lo.to_be_bytes()); - self.state.compress(slice::from_ref(&self.x_buf)); - - let h = &self.state.h; - - for i in 0..(n / 8) { - output[i * 8..i * 8 + 8].copy_from_slice(&h[i].to_be_bytes()); - } - if !n.is_multiple_of(8) { - output[((n / 8) * 8)..((n / 8) * 8) + (n % 8)] - .copy_from_slice(&h[n / 8].to_be_bytes()[0..(n % 8)]); - } - - n + fn do_final_out(self, output: &mut [u8]) -> usize { + // A whole-byte message is the zero-partial-bits case of the general padding. + self.do_final_internal(0, 0, output) } - /// TODO: This is defined in FIPS 180-4 s. 5.1.2 - /// TODO: - /// TODO: It can be implemented if required - #[allow(unused)] fn do_final_partial_bits( self, partial_byte: u8, num_partial_bits: usize, ) -> Result, HashError> { - unimplemented!() + let mut output = vec![0u8; PARAMS::OUTPUT_LEN]; + self.do_final_partial_bits_out(partial_byte, num_partial_bits, &mut output)?; + Ok(output) } - /// TODO: This is defined in FIPS 180-4 s. 5.1.2 - /// TODO: - /// TODO: It can be implemented if required - #[allow(unused)] + /// FIPS 180-4 s. 5.1: bit-oriented messages. The `num_partial_bits` most significant bits of + /// `partial_byte` (ASN.1 BIT STRING order, leading bit first) are appended to the message before + /// padding; the low bits are ignored. `num_partial_bits == 0` behaves exactly like + /// [`Hash::do_final_out`]. fn do_final_partial_bits_out( self, partial_byte: u8, num_partial_bits: usize, output: &mut [u8], ) -> Result { - unimplemented!() + if num_partial_bits > 7 { + return Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")); + } + Ok(self.do_final_internal(partial_byte, num_partial_bits, output)) } fn max_security_strength(&self) -> SecurityStrength { @@ -316,10 +523,10 @@ impl Hash for SHA512Internal { } } -/// Length in bytes of the serialized state of SHA384 and SHA512. +/// Length in bytes of the serialized state of SHA384, SHA512, SHA512/224 and SHA512/256. pub const SUSPENDED_SHA512_STATE_LEN: usize = 204; -impl Suspendable for SHA512Internal { +impl Suspendable for SHA512Internal { fn suspend(self) -> [u8; SUSPENDED_SHA512_STATE_LEN] { debug_assert_eq!(SUSPENDED_SHA512_STATE_LEN, 204); diff --git a/crypto/sha2/tests/sha2_bc-test-data.rs b/crypto/sha2/tests/sha2_bc-test-data.rs new file mode 100644 index 00000000..810669d7 --- /dev/null +++ b/crypto/sha2/tests/sha2_bc-test-data.rs @@ -0,0 +1,193 @@ +//! NIST CAVP SHAVS test vectors for SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224 and SHA-512/256. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data" (same convention as the mldsa/mlkem/sha3 crates), +//! under `crypto/sha2/{bit-oriented,byte-oriented}/`. If it is not present, the tests print a warning +//! and pass vacuously. +//! +//! Three SHAVS test types are exercised (SHAVS s. 6): +//! +//! * ShortMsg / LongMsg — `Len` (bits), `Msg`, `MD`. In the bit-oriented files `Len` is not a +//! multiple of 8 for most cases; the trailing bits are packed MSB-first in the final `Msg` byte +//! (SHAVS s. 6.2, "the message is left-justified"), which is exactly the ASN.1 BIT STRING order +//! that [`Hash::do_final_partial_bits`] takes, so the last byte is passed through unchanged. +//! * Monte — SHAVS s. 6.4 pseudo-random message test: `MD0 = MD1 = MD2 = Seed`, +//! `MDi = SHA(MDi-3 || MDi-2 || MDi-1)` for i in 3..=1002, `MD = MD1002`, then reseed with `MD` +//! for the next COUNT. 100 counts per file. (This differs from the SHA-3 Monte test, which hashes +//! only the previous digest.) + +use bouncycastle_core::traits::Hash; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_sha2::{SHA224, SHA256, SHA384, SHA512, SHA512_224, SHA512_256}; + +/// Splits a `Key = value` line from a `.rsp` file. +fn kv(line: &str) -> Option<(&str, &str)> { + let (k, v) = line.split_once('=')?; + Some((k.trim(), v.trim())) +} + +struct MsgCase { + len_bits: usize, + msg: Vec, + md: Vec, +} + +/// Parses a ShortMsg/LongMsg `.rsp` file into `(Len, Msg, MD)` triples. +fn parse_msg_file(content: &str) -> Vec { + let mut cases = vec![]; + let (mut len_bits, mut msg) = (None, None); + for line in content.lines() { + let Some((k, v)) = kv(line) else { continue }; + match k { + "Len" => len_bits = Some(v.parse::().expect("bad Len")), + "Msg" => msg = Some(hex::decode(v).expect("bad Msg hex")), + "MD" => cases.push(MsgCase { + len_bits: len_bits.take().expect("MD without Len"), + msg: msg.take().expect("MD without Msg"), + md: hex::decode(v).expect("bad MD hex"), + }), + _ => {} + } + } + cases +} + +/// Hashes the first `len_bits` bits of `msg` (CAVP MSB-first packing, as the API takes it) with `H`. +fn hash_bits(msg: &[u8], len_bits: usize) -> Vec { + let whole_bytes = len_bits / 8; + let partial_bits = len_bits % 8; + if partial_bits == 0 { + // Note: CAVP writes `Msg = 00` for Len = 0, so always slice rather than using msg directly. + H::default().hash(&msg[..whole_bytes]) + } else { + let mut h = H::default(); + h.do_update(&msg[..whole_bytes]); + // CAVP left-justifies the trailing bits in the last byte, which is the order the API takes. + h.do_final_partial_bits(msg[whole_bytes], partial_bits).expect("partial_bits is in 1..=7") + } +} + +fn run_msg_file(orientation: &str, filename: &str) { + let Some(content) = bc_test_data(&format!("crypto/sha2/{orientation}"), filename) else { + return; + }; + let cases = parse_msg_file(&content); + assert!(!cases.is_empty(), "{orientation}/{filename}: no test cases parsed"); + let mut partial_cases = 0; + for c in &cases { + if c.len_bits % 8 != 0 { + partial_cases += 1; + } + assert_eq!( + hash_bits::(&c.msg, c.len_bits), + c.md, + "{orientation}/{filename}: Len = {}", + c.len_bits + ); + // Whole-byte messages are also fed through the streaming API in uneven chunks. + if c.len_bits % 8 == 0 { + let mut h = H::default(); + for chunk in c.msg[..c.len_bits / 8].chunks(37) { + h.do_update(chunk); + } + assert_eq!( + h.do_final(), + c.md, + "{orientation}/{filename}: Len = {} (streamed)", + c.len_bits + ); + } + } + if orientation == "bit-oriented" { + assert!(partial_cases > 0, "{orientation}/{filename}: expected bit-length cases"); + } + println!("{orientation}/{filename}: {} cases ({partial_cases} bit-length)", cases.len()); +} + +struct MonteFile { + seed: Vec, + mds: Vec>, +} + +/// Parses a Monte `.rsp` file into the seed and the per-COUNT expected digests. +fn parse_monte_file(content: &str) -> MonteFile { + let mut seed = None; + let mut mds = vec![]; + for line in content.lines() { + let Some((k, v)) = kv(line) else { continue }; + match k { + "Seed" => seed = Some(hex::decode(v).expect("bad Seed hex")), + "MD" => mds.push(hex::decode(v).expect("bad MD hex")), + _ => {} + } + } + MonteFile { seed: seed.expect("Monte file without Seed"), mds } +} + +/// SHAVS s. 6.4 Monte Carlo test. +fn run_monte_file(orientation: &str, filename: &str) { + let Some(content) = bc_test_data(&format!("crypto/sha2/{orientation}"), filename) else { + return; + }; + let MonteFile { mut seed, mds } = parse_monte_file(&content); + assert_eq!(mds.len(), 100, "{orientation}/{filename}: expected 100 COUNTs"); + for (count, expected) in mds.iter().enumerate() { + // MD0 = MD1 = MD2 = Seed + let mut md = [seed.clone(), seed.clone(), seed.clone()]; + // for i = 3 to 1002: Mi = MDi-3 || MDi-2 || MDi-1; MDi = SHA(Mi) + for _ in 3..=1002 { + let mut m = Vec::with_capacity(3 * seed.len()); + m.extend_from_slice(&md[0]); + m.extend_from_slice(&md[1]); + m.extend_from_slice(&md[2]); + let next = H::default().hash(&m); + md.rotate_left(1); + md[2] = next; + } + // MDj = MD1002; Seed = MDj + assert_eq!(&md[2], expected, "{orientation}/{filename}: COUNT = {count}"); + seed = md[2].clone(); + } + println!("{orientation}/{filename}: {} counts", mds.len()); +} + +macro_rules! cavp_tests { + ($mod:ident, $hash:ty, $prefix:literal) => { + mod $mod { + use super::*; + + #[test] + fn bit_oriented_short_msg() { + run_msg_file::<$hash>("bit-oriented", concat!($prefix, "ShortMsg.rsp")); + } + #[test] + fn bit_oriented_long_msg() { + run_msg_file::<$hash>("bit-oriented", concat!($prefix, "LongMsg.rsp")); + } + #[test] + fn bit_oriented_monte() { + run_monte_file::<$hash>("bit-oriented", concat!($prefix, "Monte.rsp")); + } + #[test] + fn byte_oriented_short_msg() { + run_msg_file::<$hash>("byte-oriented", concat!($prefix, "ShortMsg.rsp")); + } + #[test] + fn byte_oriented_long_msg() { + run_msg_file::<$hash>("byte-oriented", concat!($prefix, "LongMsg.rsp")); + } + #[test] + fn byte_oriented_monte() { + run_monte_file::<$hash>("byte-oriented", concat!($prefix, "Monte.rsp")); + } + } + }; +} + +cavp_tests!(sha224, SHA224, "SHA224"); +cavp_tests!(sha256, SHA256, "SHA256"); +cavp_tests!(sha384, SHA384, "SHA384"); +cavp_tests!(sha512, SHA512, "SHA512"); +cavp_tests!(sha512_224, SHA512_224, "SHA512_224"); +cavp_tests!(sha512_256, SHA512_256, "SHA512_256"); diff --git a/crypto/sha2/tests/sha2_tests.rs b/crypto/sha2/tests/sha2_tests.rs index 42c6ba0f..7f5741b5 100644 --- a/crypto/sha2/tests/sha2_tests.rs +++ b/crypto/sha2/tests/sha2_tests.rs @@ -1,7 +1,8 @@ #[cfg(test)] mod sha2_tests { - use bouncycastle_core::errors::SuspendableError; - use bouncycastle_core::traits::{Algorithm, Hash, HashAlgParams, SecurityStrength}; + use bouncycastle_core::errors::{HashError, SuspendableError}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{Algorithm, Hash, HashAlgParams}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_sha2::*; @@ -12,8 +13,7 @@ mod sha2_tests { #[test] fn sha224() { - let mut test_framework = TestFrameworkHash::new(); - test_framework.enable_partial_byte_tests = false; + let test_framework = TestFrameworkHash::new(); test_framework.test_hash::(b"", b"\xd1\x4a\x02\x8c\x2a\x3a\x2b\xc9\x47\x61\x02\xbb\x28\x82\x34\xc4\x15\xa2\xb0\x1f\x82\x8e\xa6\x2a\xc5\xb3\xe4\x2f"); test_framework.test_hash::(b"a", b"\xab\xd3\x75\x34\xc7\xd9\xa2\xef\xb9\x46\x5d\xe9\x31\xcd\x70\x55\xff\xdb\x88\x79\x56\x3a\xe9\x80\x78\xd6\xd6\xd5"); test_framework.test_hash::(b"abc", b"\x23\x09\x7d\x22\x34\x05\xd8\x22\x86\x42\xa4\x77\xbd\xa2\x55\xb3\x2a\xad\xbc\xe4\xbd\xa0\xb3\xf7\xe3\x6c\x9d\xa7"); @@ -24,8 +24,7 @@ mod sha2_tests { #[test] fn sha256() { - let mut test_framework = TestFrameworkHash::new(); - test_framework.enable_partial_byte_tests = false; + let test_framework = TestFrameworkHash::new(); test_framework.test_hash::(b"", b"\xe3\xb0\xc4\x42\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99\x6f\xb9\x24\x27\xae\x41\xe4\x64\x9b\x93\x4c\xa4\x95\x99\x1b\x78\x52\xb8\x55"); test_framework.test_hash::(b"a", b"\xca\x97\x81\x12\xca\x1b\xbd\xca\xfa\xc2\x31\xb3\x9a\x23\xdc\x4d\xa7\x86\xef\xf8\x14\x7c\x4e\x72\xb9\x80\x77\x85\xaf\xee\x48\xbb"); test_framework.test_hash::(b"abc", b"\xba\x78\x16\xbf\x8f\x01\xcf\xea\x41\x41\x40\xde\x5d\xae\x22\x23\xb0\x03\x61\xa3\x96\x17\x7a\x9c\xb4\x10\xff\x61\xf2\x00\x15\xad"); @@ -35,8 +34,7 @@ mod sha2_tests { #[test] fn sha384() { - let mut test_framework = TestFrameworkHash::new(); - test_framework.enable_partial_byte_tests = false; + let test_framework = TestFrameworkHash::new(); test_framework.test_hash::(b"", b"\x38\xb0\x60\xa7\x51\xac\x96\x38\x4c\xd9\x32\x7e\xb1\xb1\xe3\x6a\x21\xfd\xb7\x11\x14\xbe\x07\x43\x4c\x0c\xc7\xbf\x63\xf6\xe1\xda\x27\x4e\xde\xbf\xe7\x6f\x65\xfb\xd5\x1a\xd2\xf1\x48\x98\xb9\x5b"); test_framework.test_hash::(b"a", b"\x54\xa5\x9b\x9f\x22\xb0\xb8\x08\x80\xd8\x42\x7e\x54\x8b\x7c\x23\xab\xd8\x73\x48\x6e\x1f\x03\x5d\xce\x9c\xd6\x97\xe8\x51\x75\x03\x3c\xaa\x88\xe6\xd5\x7b\xc3\x5e\xfa\xe0\xb5\xaf\xd3\x14\x5f\x31"); test_framework.test_hash::(b"abc", b"\xcb\x00\x75\x3f\x45\xa3\x5e\x8b\xb5\xa0\x3d\x69\x9a\xc6\x50\x07\x27\x2c\x32\xab\x0e\xde\xd1\x63\x1a\x8b\x60\x5a\x43\xff\x5b\xed\x80\x86\x07\x2b\xa1\xe7\xcc\x23\x58\xba\xec\xa1\x34\xc8\x25\xa7"); @@ -46,14 +44,167 @@ mod sha2_tests { #[test] fn sha512() { - let mut test_framework = TestFrameworkHash::new(); - test_framework.enable_partial_byte_tests = false; + let test_framework = TestFrameworkHash::new(); test_framework.test_hash::(b"", b"\xcf\x83\xe1\x35\x7e\xef\xb8\xbd\xf1\x54\x28\x50\xd6\x6d\x80\x07\xd6\x20\xe4\x05\x0b\x57\x15\xdc\x83\xf4\xa9\x21\xd3\x6c\xe9\xce\x47\xd0\xd1\x3c\x5d\x85\xf2\xb0\xff\x83\x18\xd2\x87\x7e\xec\x2f\x63\xb9\x31\xbd\x47\x41\x7a\x81\xa5\x38\x32\x7a\xf9\x27\xda\x3e"); test_framework.test_hash::(b"a", b"\x1f\x40\xfc\x92\xda\x24\x16\x94\x75\x09\x79\xee\x6c\xf5\x82\xf2\xd5\xd7\xd2\x8e\x18\x33\x5d\xe0\x5a\xbc\x54\xd0\x56\x0e\x0f\x53\x02\x86\x0c\x65\x2b\xf0\x8d\x56\x02\x52\xaa\x5e\x74\x21\x05\x46\xf3\x69\xfb\xbb\xce\x8c\x12\xcf\xc7\x95\x7b\x26\x52\xfe\x9a\x75"); test_framework.test_hash::(b"abc", b"\xdd\xaf\x35\xa1\x93\x61\x7a\xba\xcc\x41\x73\x49\xae\x20\x41\x31\x12\xe6\xfa\x4e\x89\xa9\x7e\xa2\x0a\x9e\xee\xe6\x4b\x55\xd3\x9a\x21\x92\x99\x2a\x27\x4f\xc1\xa8\x36\xba\x3c\x23\xa3\xfe\xeb\xbd\x45\x4d\x44\x23\x64\x3c\xe8\x0e\x2a\x9a\xc9\x4f\xa5\x4c\xa4\x9f"); test_framework.test_hash::(b"abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", b"\x8e\x95\x9b\x75\xda\xe3\x13\xda\x8c\xf4\xf7\x28\x14\xfc\x14\x3f\x8f\x77\x79\xc6\xeb\x9f\x7f\xa1\x72\x99\xae\xad\xb6\x88\x90\x18\x50\x1d\x28\x9e\x49\x00\xf7\xe4\x33\x1b\x99\xde\xc4\xb5\x43\x3a\xc7\xd3\x29\xee\xb6\xdd\x26\x54\x5e\x96\xe5\x5b\x87\x4b\xe9\x09"); test_framework.test_hash::(&DUMMY_SEED[..512], b"\xed\xb9\xbe\xd7\x21\xaa\x6a\x5f\x6f\xbc\x66\x19\xd3\xa3\xc2\xbe\x3d\x04\x30\x43\xf0\x5a\x9a\xeb\xc7\xb1\x19\x7a\x2a\xa9\xc4\x9a\x57\xd5\xdd\xd4\x67\x4c\x17\x85\x78\x50\x88\xd9\xf1\xff\x42\xc7\x97\xa0\x2a\xdc\x9b\x81\x7a\x13\x9a\x50\x97\x0d\xa6\xc9\x95\x24"); } + + /// Vectors: "" and the one-byte message from NIST CAVP SHA512_224ShortMsg.rsp (Len = 0 and + /// Len = 8); "abc" and the two-block message from the NIST example file SHA512_224.pdf. + #[test] + fn sha512_224() { + let test_framework = TestFrameworkHash::new(); + test_framework.test_hash::(b"", b"\x6e\xd0\xdd\x02\x80\x6f\xa8\x9e\x25\xde\x06\x0c\x19\xd3\xac\x86\xca\xbb\x87\xd6\xa0\xdd\xd0\x5c\x33\x3b\x84\xf4"); + test_framework.test_hash::(b"\xcf", b"\x41\x99\x23\x9e\x87\xd4\x7b\x6f\xed\xa0\x16\x80\x2b\xf3\x67\xfb\x6e\x8b\x56\x55\xef\xf6\x22\x5c\xb2\x66\x8f\x4a"); + test_framework.test_hash::(b"abc", b"\x46\x34\x27\x0f\x70\x7b\x6a\x54\xda\xae\x75\x30\x46\x08\x42\xe2\x0e\x37\xed\x26\x5c\xee\xe9\xa4\x3e\x89\x24\xaa"); + test_framework.test_hash::(b"abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", b"\x23\xfe\xc5\xbb\x94\xd6\x0b\x23\x30\x81\x92\x64\x0b\x0c\x45\x33\x35\xd6\x64\x73\x4f\xe4\x0e\x72\x68\x67\x4a\xf9"); + } + + /// Vectors: "" and the one-byte message from NIST CAVP SHA512_256ShortMsg.rsp (Len = 0 and + /// Len = 8); "abc" and the two-block message from the NIST example file SHA512_256.pdf. + #[test] + fn sha512_256() { + let test_framework = TestFrameworkHash::new(); + test_framework.test_hash::(b"", b"\xc6\x72\xb8\xd1\xef\x56\xed\x28\xab\x87\xc3\x62\x2c\x51\x14\x06\x9b\xdd\x3a\xd7\xb8\xf9\x73\x74\x98\xd0\xc0\x1e\xce\xf0\x96\x7a"); + test_framework.test_hash::(b"\xfa", b"\xc4\xef\x36\x92\x3c\x64\xe5\x1e\x87\x57\x20\xe5\x50\x29\x8a\x5a\xb8\xa3\xf2\xf8\x75\xb1\xe1\xa4\xc9\xb9\x5b\xab\xf7\x34\x4f\xef"); + test_framework.test_hash::(b"abc", b"\x53\x04\x8e\x26\x81\x94\x1e\xf9\x9b\x2e\x29\xb7\x6b\x4c\x7d\xab\xe4\xc2\xd0\xc6\x34\xfc\x6d\x46\xe0\xe2\xf1\x31\x07\xe7\xaf\x23"); + test_framework.test_hash::(b"abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu", b"\x39\x28\xe1\x84\xfb\x86\x90\xf8\x40\xda\x39\x88\x12\x1d\x31\xbe\x65\xcb\x9d\x3e\xf8\x3e\xe6\x14\x6f\xea\xc8\x61\xe1\x9b\x56\x3a"); + } + } + + /// FIPS 180-4 s. 5.1: bit-oriented messages. Zero partial bits must equal the byte-oriented + /// digest; more than 7 partial bits is rejected; only the top bits of the partial byte matter; + /// and the pad byte spilling into a second block must not break. Known answers are in + /// `partial_bits_known_answers`. + #[test] + fn partial_bits() { + fn check() { + // 0 partial bits == do_final + let mut a = H::default(); + a.do_update(b"abc"); + assert_eq!(a.do_final_partial_bits(0xFF, 0).unwrap(), H::default().hash(b"abc")); + + // out of range -> InvalidLength, never a panic + for bad in [8usize, 9, 16, 64, usize::MAX] { + let mut h = H::default(); + h.do_update(b"abc"); + assert!(matches!( + h.do_final_partial_bits(0xFF, bad), + Err(HashError::InvalidLength(_)) + )); + } + + // only the top num_partial_bits bits of partial_byte may influence the result + for n in 1..=7usize { + let mask = (0xFF00u16 >> n) as u8; + let x = H::default().do_final_partial_bits(0xA5, n).unwrap(); + let y = H::default().do_final_partial_bits(0xA5 & mask, n).unwrap(); + let z = H::default().do_final_partial_bits(0xA5 ^ 0x80, n).unwrap(); + assert_eq!(x, y, "n={n}"); + assert_ne!(x, z, "n={n}: the leading bit must change the digest"); + // and a bit-message is distinct from byte-messages of nearby length + assert_ne!(x, H::default().hash(&[]), "n={n}"); + assert_ne!(x, H::default().hash(&[0xA5 & mask]), "n={n}"); + } + + // the partial-bit path must also work when the pad byte spills into a second block + for len in [55usize, 56, 63, 64, 111, 112, 119, 127, 128] { + let msg = vec![0x5Au8; len]; + let mut h = H::default(); + h.do_update(&msg); + let mut out = vec![0u8; 64]; + let written = h.do_final_partial_bits_out(0xC0, 2, &mut out).unwrap(); + assert!(written > 0); + } + } + check::(); + check::(); + check::(); + check::(); + check::(); + check::(); + } + + /// Bit-oriented known answers (FIPS 180-4 s. 5.1). Expected values were produced by an + /// independent pure-Python implementation of FIPS 180-4 with bit-length padding, itself checked + /// against `hashlib` for byte-aligned inputs. `(prefix_len, fill, partial_byte, bits, digest)`, + /// where the `bits` message bits are the top bits of `partial_byte` (ASN.1 BIT STRING order). + #[test] + fn partial_bits_known_answers() { + fn hex(s: &str) -> Vec { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() + } + fn check(cases: &[(usize, u8, u8, usize, &str)]) { + for &(prefix_len, fill, partial_byte, bits, expected) in cases { + let mut h = H::default(); + h.do_update(&vec![fill; prefix_len]); + assert_eq!( + h.do_final_partial_bits(partial_byte, bits).unwrap(), + hex(expected), + "{prefix_len}/{bits}" + ); + } + } + check::(&[ + (0, 0, 0x80, 1, "b9debf7d52f36e6468a54817c1fa071166c3a63d384850e1575b42f702dc5aa1"), + (0, 0, 0xA8, 5, "9a6eb6cad1c1017a060c4cc9d1be5c9404397e4d05c8e6c91f6347db8591c1a9"), + (55, 0x5a, 0xC0, 2, "f9f22d1e48f4d6fe0f84db4a04bef65d4be116e4f182845b8a827c897b05723a"), + ( + 111, + 0x5a, + 0xA0, + 3, + "bf63c89e04968fba3fc26ccf8908e0b2d05221834a17f912b48d9816d821be6d", + ), + ]); + let mut h = SHA256::new(); + h.do_update(b"abc"); + assert_eq!( + h.do_final_partial_bits(0xfe, 7).unwrap(), + hex("9f5893e1b85faf8d646489927b5bc22b7394e2a14bbd47da00bbce3a1b27a5ba") + ); + + check::(&[ + ( + 0, + 0, + 0x80, + 1, + "5f72ee8494a425ba13fc8c48ac0a05cbaae7e932e471e948cb524333745aa432c1851c0c43682b0e67d64626f8f45cf165f6b538a94c63be98224e969e75d7ed", + ), + ( + 0, + 0, + 0xA8, + 5, + "dcaab1be5ce172f510ebe2da22f6488bd2f706c8124d6bb16de5cfb3432f0dd6e7262dd35206d500180b70563c419e142c354b6ac155ca8a3f0f0fdb88d567e9", + ), + ( + 55, + 0x5a, + 0xC0, + 2, + "4fe3a857ce5d8abc5dcc7ea0d3f97ff7bb0db06001e1f37c2c2c9d48bd4c609af169b0f5d200d1b9033af31819095a4679b62d87b15673a85ac75c8ecbc2bd57", + ), + ( + 111, + 0x5a, + 0xA0, + 3, + "f0af9c9852d733b024e097ae6aa9e7959c84c05a666b04f3c0df368e2ea93bcccf9136aefa54b0c4db432217742dec7d77365b3f5a6b63fe46c9fc259b8f0101", + ), + ]); + let mut h = SHA512::new(); + h.do_update(b"abc"); + assert_eq!( + h.do_final_partial_bits(0xfe, 7).unwrap(), + hex( + "ec168db3beb4379ddd4dd854461ac533f047f69ebf4770dec59442994a8320a4f240eeb0d808f8b7dc8d23d0428af5f095cc2ded70c516aef86ca68e99f8ffe6" + ) + ); } #[test] @@ -62,16 +213,26 @@ mod sha2_tests { assert_eq!(SHA256::OUTPUT_LEN, 32); assert_eq!(SHA384::OUTPUT_LEN, 48); assert_eq!(SHA512::OUTPUT_LEN, 64); + assert_eq!(SHA512_224::OUTPUT_LEN, 28); + assert_eq!(SHA512_256::OUTPUT_LEN, 32); + assert_eq!(SHA512t::<224>::OUTPUT_LEN, 28); + assert_eq!(SHA512t::<256>::OUTPUT_LEN, 32); assert_eq!(SHA224::BLOCK_LEN, 64); assert_eq!(SHA256::BLOCK_LEN, 64); assert_eq!(SHA384::BLOCK_LEN, 128); assert_eq!(SHA512::BLOCK_LEN, 128); + assert_eq!(SHA512_224::BLOCK_LEN, 128); + assert_eq!(SHA512_256::BLOCK_LEN, 128); assert_eq!(SHA224::new().block_bitlen(), 512); assert_eq!(SHA256::new().block_bitlen(), 512); assert_eq!(SHA384::new().block_bitlen(), 1024); assert_eq!(SHA512::new().block_bitlen(), 1024); + assert_eq!(SHA512_224::new().block_bitlen(), 1024); + assert_eq!(SHA512_256::new().block_bitlen(), 1024); + assert_eq!(SHA512_224::new().output_len(), 28); + assert_eq!(SHA512_256::new().output_len(), 32); } #[test] @@ -80,6 +241,10 @@ mod sha2_tests { assert_eq!(SHA256::ALG_NAME, SHA256_NAME); assert_eq!(SHA384::ALG_NAME, SHA384_NAME); assert_eq!(SHA512::ALG_NAME, SHA512_NAME); + assert_eq!(SHA512_224::ALG_NAME, SHA512_224_NAME); + assert_eq!(SHA512_256::ALG_NAME, SHA512_256_NAME); + assert_eq!(SHA512_224_NAME, "SHA512/224"); + assert_eq!(SHA512_256_NAME, "SHA512/256"); } #[test] @@ -88,6 +253,22 @@ mod sha2_tests { assert_eq!(SHA256::default().max_security_strength(), SecurityStrength::_128bit); assert_eq!(SHA384::default().max_security_strength(), SecurityStrength::_192bit); assert_eq!(SHA512::default().max_security_strength(), SecurityStrength::_256bit); + assert_eq!(SHA512_224::default().max_security_strength(), SecurityStrength::_112bit); + assert_eq!(SHA512_256::default().max_security_strength(), SecurityStrength::_128bit); + assert_eq!(SHA512_224::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); + assert_eq!(SHA512_256::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + } + + /// NIST CSOR: id-sha512-224 { hashAlgs 5 }, id-sha512-256 { hashAlgs 6 }. + #[test] + fn test_oids() { + use bouncycastle_core::traits::AlgorithmOID; + assert_eq!(SHA512_224::OID, &[2, 16, 840, 1, 101, 3, 4, 2, 5]); + assert_eq!(SHA512_256::OID, &[2, 16, 840, 1, 101, 3, 4, 2, 6]); + assert_eq!(SHA512_224::OID_DER.last(), Some(&5)); + assert_eq!(SHA512_256::OID_DER.last(), Some(&6)); + assert_eq!(&SHA512_224::OID_DER[..10], &SHA512::OID_DER[..10]); + assert_eq!(&SHA512_256::OID_DER[..10], &SHA512::OID_DER[..10]); } #[test] @@ -118,7 +299,7 @@ mod sha2_tests { assert_eq!(output, output2); // also, give it a busted x_buf_off, just to satisfy mutants that that's been tested - let mut busted_state = serialized_state.clone(); + let mut busted_state = serialized_state; busted_state[3 + 104] = 65; match SHA256::from_suspended(busted_state) { Err(SuspendableError::InvalidData) => { /* good */ } @@ -146,11 +327,53 @@ mod sha2_tests { assert_eq!(output, output2); // also, give it a busted x_buf_off, just to satisfy mutants that that's been tested - let mut busted_state = serialized_state.clone(); + let mut busted_state = serialized_state; busted_state[3 + 200] = 129; match SHA512::from_suspended(busted_state) { Err(SuspendableError::InvalidData) => { /* good */ } _ => panic!("Expected an error"), } + + // SHA512/224: same state layout as SHA512, but the truncated output must survive the + // round trip too. + let mut sha512_224 = SHA512_224::new(); + sha512_224.do_update(str.as_bytes()); + TestFrameworkSuspendableState::new().test(&sha512_224); + let serialized_state = sha512_224.clone().suspend(); + let output = sha512_224.do_final(); + let output2 = SHA512_224::from_suspended(serialized_state).unwrap().do_final(); + assert_eq!(output, output2); + assert_eq!(output.len(), 28); + } + + /// FIPS 180-4 s. 5.1.2 has SHA-384/512/512-t append the message length l as a *128-bit* field, + /// where s. 5.1.1 gives SHA-224/256 only 64 bits. Since byte_count is a u64 of bytes, l needs + /// `byte_count << 3` for the low word and `byte_count >> 61` for the high one, and it is the + /// high word that separates the two families: drop it and SHA-512 would silently agree with + /// itself across byte counts 2^61 apart, exactly as SHA-256 is obliged to. + /// + /// A message that long cannot be hashed in a test, but suspend()/from_suspended() round-trips + /// byte_count through a byte field, so the state can simply be written by hand. + #[test] + fn sha512_length_field_carries_the_high_word() { + use bouncycastle_core::traits::Suspendable; + + // Suspended layout (see SHA512Internal::suspend): 3 bytes of library version, h[0..8] as + // eight little-endian u64s, then byte_count as a little-endian u64. + const BYTE_COUNT_OFFSET: usize = 3 + 64; + fn with_byte_count(byte_count: u64) -> SHA512 { + let mut state: [u8; SUSPENDED_SHA512_STATE_LEN] = SHA512::new().suspend(); + state[BYTE_COUNT_OFFSET..BYTE_COUNT_OFFSET + 8] + .copy_from_slice(&byte_count.to_le_bytes()); + SHA512::from_suspended(state).unwrap() + } + + // 2^61 bytes is 2^64 bits: the low word of l is identical for these two, so they can only + // differ if the high word is written. + assert_ne!(with_byte_count(0).do_final(), with_byte_count(1 << 61).do_final()); + assert_ne!(with_byte_count(1).do_final(), with_byte_count((1 << 61) + 1).do_final()); + + // and byte_count 0 is still the empty-message digest, i.e. the high word is zero there + assert_eq!(with_byte_count(0).do_final(), SHA512::new().do_final()); } } diff --git a/crypto/sha2/tests/sha512t_h0_tests.rs b/crypto/sha2/tests/sha512t_h0_tests.rs new file mode 100644 index 00000000..1f5b59e9 --- /dev/null +++ b/crypto/sha2/tests/sha512t_h0_tests.rs @@ -0,0 +1,63 @@ +//! FIPS 180-4 s. 5.3.6 known-answer tests for the SHA-512/t IV Generation Function. +//! +//! The initial hash value H(0) for SHA-512/224 and SHA-512/256 is not stored as a literal in this +//! crate: it is produced by the IV Generation Function (FIPS 180-4 s. 5.3.6), evaluated at compile +//! time. These tests pin what that function produces against the words the standard lists in +//! s. 5.3.6.1 and s. 5.3.6.2. +//! +//! H(0) is read back through the public suspend API rather than from a crate-private constant. A +//! freshly-constructed hash has processed no message, so the chaining value in its serialized state +//! is still H(0). The layout is a 3-byte library version tag (written by +//! `bouncycastle_utils::suspendable_state::add_lib_ver`) followed by the eight 64-bit chaining +//! words, little-endian. +//! +//! Note that a wrong H(0) is also caught end-to-end by the CAVP vectors in `sha2_bc-test-data.rs`, +//! since every SHA-512/224 and SHA-512/256 digest would then differ. These tests localize such a +//! failure to the IV Generation Function itself. + +use bouncycastle_core::traits::Suspendable; +use bouncycastle_sha2::{SHA512_224, SHA512_256, SUSPENDED_SHA512_STATE_LEN}; + +/// Bytes occupied by the library version tag at the front of a suspended state. +const LIB_VER_TAG_LEN: usize = 3; + +/// FIPS 180-4 s. 5.3.6.1: the eight 64-bit words H(0) shall consist of for SHA-512/224, "obtained +/// by executing the SHA-512/t IV Generation Function with t = 224". +const SHA512_224_H0: [u64; 8] = [ + 0x8C3D37C819544DA2, 0x73E1996689DCD4D6, 0x1DFAB7AE32FF9C82, 0x679DD514582F9FCF, + 0x0F6D2B697BD44DA8, 0x77E36F7304C48942, 0x3F9D85A86A1D36C8, 0x1112E6AD91D692A1, +]; + +/// FIPS 180-4 s. 5.3.6.2: the eight 64-bit words H(0) shall consist of for SHA-512/256, "obtained +/// by executing the SHA-512/t IV Generation Function with t = 256". +const SHA512_256_H0: [u64; 8] = [ + 0x22312194FC2BF72C, 0x9F555FA3C84C64C2, 0x2393B86B6F53B151, 0x963877195940EABD, + 0x96283EE2A88EFFE3, 0xBE5E1E2553863992, 0x2B0199FC2C85B8AA, 0x0EB72DDC81C52CA2, +]; + +/// Recovers the eight chaining words of a freshly-constructed SHA-512-family hash, which has had no +/// message applied and so still holds H(0). +/// Uses the [`Suspendable`] API to read the internal state. +fn h0_of>() -> [u64; 8] { + let state = H::default().suspend(); + + let mut h0 = [0u64; 8]; + for (i, word) in h0.iter_mut().enumerate() { + let offset = LIB_VER_TAG_LEN + (i * 8); + // infallible: the slice is 8 bytes, and offset + 8 <= 3 + 64 < SUSPENDED_SHA512_STATE_LEN. + *word = u64::from_le_bytes(state[offset..offset + 8].try_into().unwrap()); + } + h0 +} + +/// FIPS 180-4 s. 6.6 exception 1 / s. 5.3.6.1: SHA-512/224 uses the H(0) listed in s. 5.3.6.1. +#[test] +fn sha512_224_h0_matches_the_listed_words() { + assert_eq!(h0_of::(), SHA512_224_H0); +} + +/// FIPS 180-4 s. 6.7 exception 1 / s. 5.3.6.2: SHA-512/256 uses the H(0) listed in s. 5.3.6.2. +#[test] +fn sha512_256_h0_matches_the_listed_words() { + assert_eq!(h0_of::(), SHA512_256_H0); +} diff --git a/crypto/sha2/tests/sha512t_tests.rs b/crypto/sha2/tests/sha512t_tests.rs new file mode 100644 index 00000000..c366e6ba --- /dev/null +++ b/crypto/sha2/tests/sha512t_tests.rs @@ -0,0 +1,276 @@ +//! SHA-512/t (FIPS 180-4 s. 5.3.6) across the whole range of `t`, not just the two approved +//! truncations. +//! +//! `SHA512t` is instantiable for every `t` the standard defines a hash for -- any positive +//! multiple of 8 below 512 other than 384 -- so the IV Generation Function's decimal formatting of +//! `t` now has three reachable branches ("SHA-512/8", "SHA-512/96", "SHA-512/224") where it +//! previously only ever saw three-digit values. These tests cover all three. +//! +//! # Where the expected values come from +//! +//! FIPS 180-4 publishes H(0) for t = 224 and t = 256 only (s. 5.3.6.1 / s. 5.3.6.2, pinned by +//! `sha512t_h0_tests.rs`) and no digests at all for any other truncation. The known-answer +//! values below were therefore generated with BC Java's `org.bouncycastle.crypto.digests +//! .SHA512tDigest`, an independent implementation of the same section, over the FIPS 180-4 +//! Appendix C sample messages. The two approved truncations are in the table as well, so a change +//! that broke the cross-check would have to break it consistently with the NIST-published values +//! for t = 224 and t = 256 to go unnoticed. +//! +//! A wrong H(0) for a given t changes every digest for that t, so these digests pin the IV +//! Generation Function -- including which decimal branch it took -- as well as the truncation. + +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, HashAlgParams}; +use bouncycastle_sha2::{SHA512_224, SHA512_224_NAME, SHA512_256, SHA512_256_NAME, SHA512t}; + +// The generic name and output length must keep reproducing exactly what the two approved +// truncations had when they were spelled out by hand. These hold at compile time, so a regression +// fails the build of this test crate rather than a test in it; `alg_name_spells_t_in_decimal` and +// `output_len_is_t_over_eight` below are the runtime half that `cargo mutants` can see fail. +const _: () = assert!(matches!(::ALG_NAME.as_bytes(), b"SHA512/224")); +const _: () = assert!(matches!(::ALG_NAME.as_bytes(), b"SHA512/256")); +const _: () = assert!(matches!(SHA512_224_NAME.as_bytes(), b"SHA512/224")); +const _: () = assert!(matches!(SHA512_256_NAME.as_bytes(), b"SHA512/256")); +const _: () = assert!(::OUTPUT_LEN == 28); +const _: () = assert!(::OUTPUT_LEN == 32); + +/// FIPS 180-4 Appendix C.1 / C.2 sample message. +const ABC: &[u8] = b"abc"; +/// FIPS 180-4 Appendix C.3 sample message (two-block). +const TWO_BLOCK: &[u8] = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; + +fn from_hex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have an even length"); + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() +} + +/// Drives one `SHA512t` through the whole [`Hash`] surface and checks every route agrees with +/// `expected_hex`. `construct` builds a fresh instance for each route. +fn check( + construct: impl Fn() -> H, + input: &[u8], + expected_hex: &str, +) { + let expected = from_hex(expected_hex); + assert_eq!( + expected.len(), + H::OUTPUT_LEN, + "{}: the expected value is {} bytes but OUTPUT_LEN is {}", + H::ALG_NAME, + expected.len(), + H::OUTPUT_LEN + ); + + /*** fn hash(self, data: &[u8]) -> Vec ***/ + assert_eq!(construct().hash(input), expected, "{}: hash()", H::ALG_NAME); + + /*** fn hash_out(self, data: &[u8], output: &mut [u8]) -> usize ***/ + let mut out = vec![0u8; H::OUTPUT_LEN]; + assert_eq!(construct().hash_out(input, &mut out), H::OUTPUT_LEN, "{}: hash_out()", H::ALG_NAME); + assert_eq!(out, expected, "{}: hash_out()", H::ALG_NAME); + + /*** streaming in one do_update, then do_final() ***/ + let mut h = construct(); + h.do_update(input); + assert_eq!(h.do_final(), expected, "{}: do_update + do_final", H::ALG_NAME); + + /*** streaming in one do_update, then do_final_out() ***/ + let mut h = construct(); + h.do_update(input); + let mut out = vec![0u8; H::OUTPUT_LEN]; + assert_eq!(h.do_final_out(&mut out), H::OUTPUT_LEN, "{}: do_final_out", H::ALG_NAME); + assert_eq!(out, expected, "{}: do_final_out", H::ALG_NAME); + + /*** chunked absorb must equal one-shot, at chunk sizes either side of the 128-byte block ***/ + for chunk_len in [1usize, 7, 64, 127, 128, 129] { + let mut h = construct(); + for chunk in input.chunks(chunk_len) { + h.do_update(chunk); + } + assert_eq!( + h.do_final(), + expected, + "{}: absorbing in {chunk_len}-byte chunks must equal the one-shot", + H::ALG_NAME + ); + } +} + +/// BC Java `SHA512tDigest` cross-check, for the truncations FIPS 180-4 publishes no values for. +/// +/// The `t` values span all three decimal branches of the IV Generation Function's "SHA-512/t" +/// string: one digit (8), two digits (16, 24, 88, 96) and three (104, 264, 504). +macro_rules! sha512t_kat { + ($name:ident, $t:literal, $empty:literal, $abc:literal, $two_block:literal) => { + #[test] + fn $name() { + check(SHA512t::<$t>::new, b"", $empty); + check(SHA512t::<$t>::new, ABC, $abc); + check(SHA512t::<$t>::new, TWO_BLOCK, $two_block); + } + }; +} + +sha512t_kat!(sha512_t8, 8, "79", "c5", "8d"); +sha512t_kat!(sha512_t16, 16, "b44e", "1768", "e8d7"); +sha512t_kat!(sha512_t24, 24, "2f8a89", "1e17ce", "765639"); +sha512t_kat!( + sha512_t88, 88, "f0a49fbe063fd7fba2bf3b", "8194668ea596265aef4ef5", "c040324022ed56c0badf79" +); +sha512t_kat!( + sha512_t96, + 96, + "44ab9c7c3eb2da370d2c0ed7", + "67246fd8d90dca7009449ad5", + "c75100023425182c76253d0a" +); +sha512t_kat!( + sha512_t104, + 104, + "47f922a2d2508feb288af79a30", + "456045a75a5d7e0ea4af09dfce", + "64fc045733525b8c29376fc6be" +); +sha512t_kat!( + sha512_t264, + 264, + "78180c9a54d1c1f5bd3b941cfec4ee2cded5663ed7bf535ecd964518515174db49", + "888cfb35a25f524f8d17a1bb97134a9a6850b0ff269f1eb26ae038c22cd47f4c58", + "873b4bd852e7e441c406e49b1caa88f76bfc4b95d373f783350398db4b4a3e5909" +); +sha512t_kat!( + sha512_t504, + 504, + "6c46fed4cb277417c5f2d88b19a88a9a010e9e81a24d4a38d818c84a1aa3b88dd115f9550869eb097001fe0e8315b1d6f04124215f095e0be7ca94f99cdc6a", + "8c43e4bf1cad93067af1ad632ba38bba0b5673bf0129f01a469224c2d981b8ecaa301facf8e392f97efc5997885a1c90cefba70d81892f40267df4fd6fef9a", + "9f4bd94b6620e1ec80a9d4cfa315ee73f6228ee7f8fc8f58f232cc117c58633936b2df04f2cef341aae4f92f68c53223c1a631f5b6eb597c6933e0fc5f3f1a" +); + +/// The two approved truncations must keep producing exactly what they did before `SHA512t` became +/// generic. These are the published SHA-512/224 and SHA-512/256 values, and they agree with the +/// same BC Java run that produced the table above. +#[test] +fn approved_truncations_are_unchanged() { + check(SHA512_224::new, b"", "6ed0dd02806fa89e25de060c19d3ac86cabb87d6a0ddd05c333b84f4"); + check(SHA512_224::new, ABC, "4634270f707b6a54daae7530460842e20e37ed265ceee9a43e8924aa"); + check(SHA512_224::new, TWO_BLOCK, "e5302d6d54bb242275d1e7622d68df6eb02dedd13f564c13dbda2174"); + + check(SHA512_256::new, b"", "c672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967a"); + check(SHA512_256::new, ABC, "53048e2681941ef99b2e29b76b4c7dabe4c2d0c634fc6d46e0e2f13107e7af23"); + check( + SHA512_256::new, + TWO_BLOCK, + "bde8e1f9f19bb9fd3406c90ec6bc47bd36d8ada9f11880dbc8a22a7078b6a461", + ); +} + +/// `SHA512t<224>` / `SHA512t<256>` and the named aliases are the same type, so the alias cannot +/// drift away from the generic parameter set. +#[test] +fn the_named_aliases_are_the_generic_type() { + fn same_type(_: &T, _: &T) {} + same_type(&SHA512_224::new(), &SHA512t::<224>::new()); + same_type(&SHA512_256::new(), &SHA512t::<256>::new()); +} + +/// A message spanning many blocks, to catch a `t` whose IV is right but whose multi-block path is +/// not. FIPS 180-4 Appendix C uses one million 'a' for exactly this. +#[test] +fn one_million_a() { + let million = vec![b'a'; 1_000_000]; + check(SHA512t::<8>::new, &million, "32"); + check(SHA512t::<96>::new, &million, "0e1f626963a870088bab77da"); + check(SHA512_224::new, &million, "37ab331d76f0d36de422bd0edeb22a28accd487b7a8453ae965dd287"); + check( + SHA512_256::new, + &million, + "9a59a052930187a97038cae692f30708aa6491923ef5194394dc68d56c74fb21", + ); + check( + SHA512t::<504>::new, + &million, + "f94e0eb099411d073274d87a908531ce7faa8591b28f56d86694e056ab0477f03af082453f5f44ec75c67ac58843fedd44429b0aa3322277b32b04e8a0586c", + ); +} + +/// The algorithm name is built from `T` at compile time; check every digit count, and that the two +/// approved truncations still spell themselves the way the crate's name constants do. +#[test] +fn alg_name_spells_t_in_decimal() { + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/8"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/16"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/96"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/104"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/224"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/256"); + assert_eq!( as Algorithm>::ALG_NAME, "SHA512/504"); + + assert_eq!( as Algorithm>::ALG_NAME, bouncycastle_sha2::SHA512_224_NAME); + assert_eq!( as Algorithm>::ALG_NAME, bouncycastle_sha2::SHA512_256_NAME); + + // No leading zero and no trailing NUL from the fixed-size buffer the name is built in. + for name in [ + as Algorithm>::ALG_NAME, + as Algorithm>::ALG_NAME, + as Algorithm>::ALG_NAME, + ] { + let digits = name.strip_prefix("SHA512/").expect("name starts with SHA512/"); + assert!(digits.bytes().all(|b| b.is_ascii_digit()), "{name}: digits only"); + assert!(!digits.starts_with('0'), "{name}: FIPS 180-4 s. 5.3.6 forbids a leading zero"); + } +} + +/// `OUTPUT_LEN` is `T / 8`, exactly, for every accepted `T`. +#[test] +fn output_len_is_t_over_eight() { + assert_eq!( as HashAlgParams>::OUTPUT_LEN, 1); + assert_eq!( as HashAlgParams>::OUTPUT_LEN, 12); + assert_eq!( as HashAlgParams>::OUTPUT_LEN, 28); + assert_eq!( as HashAlgParams>::OUTPUT_LEN, 32); + assert_eq!( as HashAlgParams>::OUTPUT_LEN, 63); + + // BLOCK_LEN does not vary with t: FIPS 180-4 Figure 1, block size 1024 bits. + assert_eq!( as HashAlgParams>::BLOCK_LEN, 128); + assert_eq!( as HashAlgParams>::BLOCK_LEN, 128); + assert_eq!(SHA512t::<8>::new().block_bitlen(), 1024); +} + +/// Collision resistance is t/2 bits, rounded down to a modelled level, and the two approved +/// truncations keep the strengths they were given by hand. +#[test] +fn security_strength_is_half_of_t() { + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::None); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::None); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_112bit); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!( as Algorithm>::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + + // and the instance method agrees with the associated const + assert_eq!( + SHA512_224::new().max_security_strength(), + as Algorithm>::MAX_SECURITY_STRENGTH + ); + assert_eq!( + SHA512t::<504>::new().max_security_strength(), + as Algorithm>::MAX_SECURITY_STRENGTH + ); +} + +/// A shorter output buffer truncates and a longer one is zero-filled past the digest, for a +/// generic `t` as much as for the approved ones. +#[test] +fn output_buffer_shorter_and_longer_than_the_digest() { + let full = from_hex("44ab9c7c3eb2da370d2c0ed7"); // SHA512/96("") + + let mut short = [0u8; 5]; + assert_eq!(SHA512t::<96>::new().hash_out(b"", &mut short), 5); + assert_eq!(short, full[..5]); + + let mut long = [0xAAu8; 20]; + assert_eq!(SHA512t::<96>::new().hash_out(b"", &mut long), 12); + assert_eq!(&long[..12], &full[..]); + assert_eq!(&long[12..], &[0u8; 8], "past the digest the buffer is zero-filled"); +} diff --git a/crypto/sha3/Cargo.toml b/crypto/sha3/Cargo.toml index 60f5170b..60a024d8 100644 --- a/crypto/sha3/Cargo.toml +++ b/crypto/sha3/Cargo.toml @@ -10,9 +10,9 @@ bouncycastle-utils.workspace = true [dev-dependencies] bouncycastle-core-test-framework.workspace = true -criterion.workspace = true bouncycastle-hex.workspace = true bouncycastle-rng.workspace = true +criterion.workspace = true [[bench]] name = "sha3_benches" 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..fafa1f8a --- /dev/null +++ b/crypto/sha3/src/cshake.rs @@ -0,0 +1,605 @@ +//! cSHAKE, the customizable SHAKE of NIST SP 800-185 Sec 3. + +use crate::keccak::SHA3_FAMILY_STATE_LEN; +use crate::shake::{SHAKEInternal, SHAKESqueezer}; +use crate::{SHAKE128Params, SHAKE256Params, SHAKEParams}; +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, resume_component, suspend_component, +}; + +// imports needed for docs +#[allow(unused_imports)] +use crate::SHAKE128; +// end of doc-only imports + +/// 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"; + +/// Length in bytes of the suspended state of cSHAKE. +pub const SUSPENDED_CSHAKE_STATE_LEN: usize = LIB_VERSION_LEN + CSHAKE_COMPONENT_LEN; +/// Length in bytes of the suspended state of a [`CSHAKESqueezer`]. +pub const SUSPENDED_LENGTH_BOUND_SQUEEZER_STATE_LEN: usize = SUSPENDED_CSHAKE_STATE_LEN; +/// The cSHAKE state without its version header: the SHA3-family state, then one byte saying +/// whether `N` or `S` was non-empty. The functions built on cSHAKE write this first, under their +/// own tag, and their own fields after it. +pub(crate) const CSHAKE_COMPONENT_LEN: usize = SHA3_FAMILY_STATE_LEN + 1; + +/// 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); + +/// 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; + +/// Internal struct for cSHAKE. +/// +/// 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 CSHAKEInternal { + /// Writes the state under `tag` into `out`, which is exactly [`CSHAKE_COMPONENT_LEN`] bytes. + pub(crate) fn write_tagged(&self, tag: u8, out: &mut [u8]) { + let (family, rest) = out.split_at_mut(SHA3_FAMILY_STATE_LEN); + self.shake.write_family_state(tag, family); + let mut w = CursorMut::new(rest); + w.u8(self.customized as u8); + debug_assert!(w.is_done()); + } + + /// The reverse of [`Self::write_tagged`]. A sponge that has begun squeezing is refused: a + /// cSHAKE a caller can hold is still absorbing, and the squeezing half is a [`SHAKESqueezer`] + /// or a [`CSHAKESqueezer`], which resume their own states. + pub(crate) fn read_tagged(state: &[u8], tag: u8) -> Result { + let (family, rest) = state.split_at(SHA3_FAMILY_STATE_LEN); + let shake = SHAKEInternal::read_family_state(family, tag)?; + if shake.is_squeezing() { + return Err(SuspendableError::InvalidData); + } + let mut r = Cursor::new(rest); + let customized = match r.u8() { + 0 => false, + 1 => true, + _ => return Err(SuspendableError::InvalidData), + }; + debug_assert!(r.is_done()); + Ok(Self { shake, customized }) + } + + /// [`Self::read_tagged`] for the functions built on cSHAKE, whose `N` is never empty: a state + /// claiming otherwise is not one they wrote. + pub(crate) fn read_tagged_customized(state: &[u8], tag: u8) -> Result { + let cshake = Self::read_tagged(state, tag)?; + if !cshake.customized { + return Err(SuspendableError::InvalidData); + } + Ok(cshake) + } +} + +impl SuspendableComponent for CSHAKEInternal { + const STATE_LEN: usize = CSHAKE_COMPONENT_LEN; + type Key = (); + + fn write_state(&self, out: &mut [u8]) { + self.write_tagged(PARAMS::CSHAKE_STATE_TAG, out) + } + + fn read_state(state: &[u8], _key: &()) -> Result { + Self::read_tagged(state, PARAMS::CSHAKE_STATE_TAG) + } +} + +/// The absorbing phase. Once output begins the sponge is a [`SHAKESqueezer`] -- the domain suffix +/// is in, and nothing cSHAKE-specific remains -- so it suspends and resumes as one. +impl Suspendable for CSHAKEInternal { + fn suspend(self) -> [u8; SUSPENDED_CSHAKE_STATE_LEN] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; SUSPENDED_CSHAKE_STATE_LEN]) -> Result { + resume_component(&state, &()) + } +} + +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_output_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_output_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_output_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) + } + } +} + +/*** cshake helpers ***/ +/// 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 +} + +/// 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_output_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. +#[derive(Clone)] +pub struct CSHAKESqueezer { + phase: Phase, +} + +/// Which side of the first read this squeezer is on. +#[derive(Clone)] +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: [`CSHAKESqueezer::read`] leaves this here only while the value moves + /// from one of the phases above to the other. + Binding, +} + +impl CSHAKESqueezer { + /// 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_output_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_output_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"), + } + } +} + +/// Both phases suspend. The sponge's own phase flag records which, so the state is the cSHAKE +/// layout under one tag, and a resumed `Unbound` squeezer still has its first read to make. +impl SuspendableComponent for CSHAKESqueezer { + const STATE_LEN: usize = CSHAKE_COMPONENT_LEN; + type Key = (); + + fn write_state(&self, out: &mut [u8]) { + let tag = PARAMS::LENGTH_BOUND_SQUEEZER_STATE_TAG; + match &self.phase { + Phase::Unbound(cshake) => cshake.write_tagged(tag, out), + Phase::Squeezing(squeezer) => { + let (family, rest) = out.split_at_mut(SHA3_FAMILY_STATE_LEN); + squeezer.write_family_state(tag, family); + // Every function that reaches this squeezer has a non-empty N. + let mut w = CursorMut::new(rest); + w.u8(1); + debug_assert!(w.is_done()); + } + Phase::Binding => unreachable!("Binding is never live outside `read`"), + } + } + + fn read_state(state: &[u8], _key: &()) -> Result { + let (family, rest) = state.split_at(SHA3_FAMILY_STATE_LEN); + let shake = SHAKEInternal::::read_family_state( + family, + PARAMS::LENGTH_BOUND_SQUEEZER_STATE_TAG, + )?; + let mut r = Cursor::new(rest); + if r.u8() != 1 { + return Err(SuspendableError::InvalidData); + } + debug_assert!(r.is_done()); + let phase = if shake.is_squeezing() { + Phase::Squeezing(SHAKESqueezer::from_squeezing(shake)) + } else { + Phase::Unbound(CSHAKEInternal { shake, customized: true }) + }; + Ok(Self { phase }) + } +} + +impl Suspendable + for CSHAKESqueezer +{ + fn suspend(self) -> [u8; SUSPENDED_LENGTH_BOUND_SQUEEZER_STATE_LEN] { + suspend_component(&self) + } + + fn from_suspended( + state: [u8; SUSPENDED_LENGTH_BOUND_SQUEEZER_STATE_LEN], + ) -> Result { + resume_component(&state, &()) + } +} + +impl XOFSqueezer for CSHAKESqueezer { + 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_output_final(self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.do_output_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_output_final_out(mut self, output: &mut [u8]) -> usize { + self.read((output.len() as u64) * 8, output) + } +} + +#[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/src/hmac.rs b/crypto/sha3/src/hmac.rs index 7a7240a4..2e67b8b2 100644 --- a/crypto/sha3/src/hmac.rs +++ b/crypto/sha3/src/hmac.rs @@ -179,7 +179,10 @@ //! //! Note that since HMAC is a keyed algorithm and we do not want to serialize the private key into //! the state, the trait structure forces you to re-provide the same key when you resume the -//! operation. Securely storing this key in the interim is the responsibility of the caller. Note +//! operation. Securely storing this key in the interim is the responsibility of the caller. The +//! state is not key-free, though: it is the inner sponge after absorbing `K ⊕ ipad`, and Keccak-f +//! is a permutation, so anyone holding the state and the message absorbed so far can invert it +//! back to `K ⊕ ipad` and hence the key. Store the suspended state as securely as the key. Note //! also that if you resume the HMAC with the wrong key, [`SuspendableKeyed::from_suspended`] has no //! way to detect this, so the end result will be a broken MAC value computed with different keys in //! the inner and outer pad. So make sure you resume with the same key! @@ -232,7 +235,7 @@ //! as the output size grows. The suspended state is exactly the inner hash's suspended state -- the //! key is deliberately excluded -- so all four share the sponge's single value. //! -//! # Security Considerations +//! # 🚨 Security Considerations 🚨 //! //! * The key must carry at least the security strength claimed by the HMAC, and [`MAC::new`] //! enforces that. [`MAC::new_allow_weak_key`] deliberately skips the check; use it only where a @@ -244,8 +247,9 @@ //! IG A.8 / NIST SP 800-107-r1 Section 5.3.3. That is a floor, not a recommendation -- RFC 2104 //! Section 5 recommends that the output length "be not less than half the length of the hash //! output ... and not less than 80 bits". -//! * Resuming a suspended HMAC with the wrong key cannot be detected and silently produces a wrong -//! MAC; see the suspend/resume section above. +//! * A suspended HMAC state can be inverted to the key and must be stored as securely as the key; +//! and resuming with the wrong key cannot be detected and silently produces a wrong MAC. See the +//! suspend/resume section above. //! * SHA-3 is a sponge and is not vulnerable to the length-extension attack that motivates HMAC for //! Merkle-Damgard hashes, so a plain `SHA3(k || m)` is not broken the way `SHA256(k || m)` is. //! HMAC-SHA3 remains the right choice for interoperability and for FIPS 198-1 conformance, and @@ -254,7 +258,8 @@ use crate::SUSPENDED_SHA3_STATE_LEN; use crate::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; use bouncycastle_core::key_material::KeyMaterial; -use bouncycastle_core::traits::{HashAlgParams, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::HashAlgParams; use bouncycastle_hmac::{HMAC, HMACParams}; /*** Imports needed for docs ***/ diff --git a/crypto/sha3/src/keccak.rs b/crypto/sha3/src/keccak.rs index 6188f826..d2a1bf11 100644 --- a/crypto/sha3/src/keccak.rs +++ b/crypto/sha3/src/keccak.rs @@ -1,6 +1,6 @@ use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::key_material::KeyType; -use bouncycastle_core::traits::SecurityStrength; +use bouncycastle_core::security_strength::SecurityStrength; use bouncycastle_utils::secret::Secret; const KECCAK_ROUND_CONSTANTS: [u64; 24] = [ @@ -250,7 +250,8 @@ impl KeccakInternal { } } - /// Absorbs the final `bits` (0..=7, in the least significant bits of `data`) of the message and + /// Absorbs the final `bits` (0..=7, in the least significant bits of `data`, FIPS 202 B.1 order; + /// the public API's MSB-first partial byte is reversed by the callers before reaching here) of the message and /// switches the sponge to the squeezing phase. `bits == 0` means "no further bits": the sponge is /// padded and switched to squeezing without absorbing anything. Callers that have already applied a /// domain-separation suffix rely on this — if the switch did not happen here, a later squeeze would diff --git a/crypto/sha3/src/kmac.rs b/crypto/sha3/src/kmac.rs new file mode 100644 index 00000000..a3ea3928 --- /dev/null +++ b/crypto/sha3/src/kmac.rs @@ -0,0 +1,483 @@ +//! KMAC, the Keccak Message Authentication Code of NIST SP 800-185 Sec 4. +//! +//! # KMAC +//! KMAC is a [`MAC`]. [`MAC::new`] takes a key tagged [`KeyType::MACKey`] and produces the nominal +//! output length; [`KMACInternal::new_with_params`] chooses `S` and the output length, which is +//! bound into the function rather than a truncation of it (see [`KMAC128`]): +//! ``` +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::traits::MAC; +//! use bouncycastle_sha3::kmac::KMAC128; +//! +//! let key = KeyMaterial256::from_bytes_as_type(&[0x42u8; 32], KeyType::MACKey).unwrap(); +//! +//! let tag: Vec = KMAC128::new(&key).unwrap().mac(b"Hello, world!"); +//! assert!(KMAC128::new(&key).unwrap().verify(b"Hello, world!", &tag)); +//! +//! // 16-byte tags, under a customization string. +//! let kmac = KMAC128::new_with_params(&key, b"My Tagged Application", 16, false).unwrap(); +//! let short_tag: Vec = kmac.mac(b"Hello, world!"); +//! assert_eq!(short_tag.len(), 16); +//! ``` +//! +//! # KMACXOF +//! KMACXOF, is an arbitrary-output-length form of KMAC and it implements the [`XOF`] trait. +//! It is a separate function from its fixed-length counterpart since its *final* read ([`XOF::xof`], +//! [`XOFSqueezer::do_output_final`]) binds its output length so that outputs of different lengths, +//! even over the same input, are completely unrelated (ie they don't have the problem that one is +//! a prefix of the other). +//! +//! See [`KMACXOF128`] for detail. +//! +//! Example of `KMACXOF128`: +//! ``` +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::traits::{Hash, MAC, XOF, XOFSqueezer}; +//! use bouncycastle_sha3::kmac::{KMAC128, KMACXOF128}; +//! +//! let key = KeyMaterial256::from_bytes_as_type(&[0x42u8; 32], KeyType::MACKey).unwrap(); +//! +//! let mut kmac = KMACXOF128::new(&key, b"", false).unwrap(); +//! kmac.do_update(b"Hello, world!"); +//! let mut squeezer = kmac.into_squeezer(); +//! let first: Vec = squeezer.do_output(16); +//! let more: Vec = squeezer.do_output(1024); +//! +//! // A final read of 32 bytes is KMAC128 at its nominal length, not a prefix of the stream above. +//! let bound: Vec = KMACXOF128::new(&key, b"", false).unwrap().xof(b"Hello, world!", 32); +//! assert_eq!(bound, KMAC128::new(&key).unwrap().mac(b"Hello, world!")); +//! assert_ne!(bound[..16], first[..]); +//! ``` + +use crate::cshake::{CSHAKE_COMPONENT_LEN, CSHAKEInternal, CSHAKESqueezer, right_encode}; +use crate::{SHAKE128Params, SHAKE256Params, SHAKEParams}; +use bouncycastle_core::errors::{HashError, KeyMaterialError, MACError, SuspendableError}; +use bouncycastle_core::key_material::{KeyMaterialTrait, KeyType}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, MAC, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_utils::ct; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, + suspend_component, +}; + +/// 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"; + +/// 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; + +/// Length in bytes of the suspended state of KMAC. +pub const SUSPENDED_KMAC_STATE_LEN: usize = LIB_VERSION_LEN + CSHAKE_COMPONENT_LEN + 8; +/// Length in bytes of the suspended state of KMACXOF. +pub const SUSPENDED_KMACXOF_STATE_LEN: usize = LIB_VERSION_LEN + CSHAKE_COMPONENT_LEN; + +/// 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 [`KMAC128`] or [`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. +#[derive(Clone)] +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 SuspendableComponent for KMACInternal { + const STATE_LEN: usize = CSHAKE_COMPONENT_LEN + 8; + type Key = (); + + fn write_state(&self, out: &mut [u8]) { + let (cshake, rest) = out.split_at_mut(CSHAKE_COMPONENT_LEN); + self.cshake.write_tagged(PARAMS::KMAC_STATE_TAG, cshake); + let mut w = CursorMut::new(rest); + w.u64(self.output_len as u64); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], _key: &()) -> Result { + let (cshake, rest) = state.split_at(CSHAKE_COMPONENT_LEN); + let cshake = CSHAKEInternal::read_tagged_customized(cshake, PARAMS::KMAC_STATE_TAG)?; + let mut r = Cursor::new(rest); + let output_len = bounded_usize(r.u64(), usize::MAX)?; + debug_assert!(r.is_done()); + // The strength is fixed by the parameter set (see `new_with_params`), not stored. + Ok(Self { + cshake, + output_len, + strength: SecurityStrength::from_bits(PARAMS::SIZE as usize), + }) + } +} + +// `Suspendable` rather than `SuspendableKeyed`, keyed though KMAC is. `SuspendableKeyed` lets a +// state omit the key because the key is wanted again at resume -- HMAC needs it for the outer +// `K xor opad` step. KMAC's key goes into the sponge in `new_with_params` and is never touched +// again, so a re-supplied key could neither rebuild anything nor be checked. +/// The suspended state is not key-free: Keccak-f is a permutation, so anyone holding the state +/// and the data absorbed so far can invert it back to the key. Store it as securely as the key. +impl Suspendable for KMACInternal { + fn suspend(self) -> [u8; SUSPENDED_KMAC_STATE_LEN] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; SUSPENDED_KMAC_STATE_LEN]) -> Result { + resume_component(&state, &()) + } +} + +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 [`KMACXOF128`] or [`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_output_final`] and [`XOF::xof`] absorb +/// `right_encode(8n)` and so produce `KMAC(K, X, 8n, S)` exactly (see [`CSHAKESqueezer`]), 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 SuspendableComponent for KMACXOFInternal { + const STATE_LEN: usize = CSHAKE_COMPONENT_LEN; + type Key = (); + + fn write_state(&self, out: &mut [u8]) { + self.cshake.write_tagged(PARAMS::KMACXOF_STATE_TAG, out) + } + + fn read_state(state: &[u8], _key: &()) -> Result { + let cshake = CSHAKEInternal::read_tagged_customized(state, PARAMS::KMACXOF_STATE_TAG)?; + Ok(Self { cshake, strength: SecurityStrength::from_bits(PARAMS::SIZE as usize) }) + } +} + +/// The absorbing phase; the squeezing half is a [`CSHAKESqueezer`], which suspends on its +/// own. The state inverts to the key exactly as [`KMACInternal`]'s does: store it as the key. +impl Suspendable for KMACXOFInternal { + fn suspend(self) -> [u8; SUSPENDED_KMACXOF_STATE_LEN] { + suspend_component(&self) + } + + fn from_suspended(state: [u8; SUSPENDED_KMACXOF_STATE_LEN]) -> Result { + resume_component(&state, &()) + } +} + +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_output_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 = CSHAKESqueezer; + + /// 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 [`CSHAKESqueezer`] decides it. + fn into_squeezer(self) -> Self::Squeezer { + CSHAKESqueezer::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/lib.rs b/crypto/sha3/src/lib.rs index 22c1dc0b..6d96a034 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -1,4 +1,4 @@ -//! Implements SHA3 as per NIST FIPS 202. +//! Implements SHA3 as per NIST FIPS 202, and the SHA-3 derived functions of NIST SP 800-185. //! //! This crate provides the following primitives: //! @@ -6,8 +6,11 @@ //! * SHAKE [`XOF`] functions. //! * SHA3-based [`KDF`] functions. //! * HMAC_SHA3_* [`MAC`] functions. +//! * The SP 800-185 functions: cSHAKE ([`XOF`]), KMAC ([`MAC`]), TupleHash and ParallelHash +//! ([`Hash`]), and their arbitrary-output-length forms KMACXOF, TupleHashXOF and +//! ParallelHashXOF ([`XOF`]). //! -//! # Examples +//! # Usage Examples //! ## Hash //! Hash functionality is accessed via the [`Hash`] trait, //! which is implemented by [`SHA3_224`], [`SHA3_256`], [`SHA3_384`] and [`SHA3_512`]. @@ -41,8 +44,11 @@ //! let output: Vec = sha3.do_final(); //! ``` //! -//! It is also possible to provide input where the final byte contains less than 8 bits of data (ie is a partial byte); -//! for example, the following code uses only 3 bits of the final byte: +//! It is also possible to provide input where the final byte contains less than 8 bits of data (ie is a partial byte). +//! The partial byte is taken as it arrives in the final octet of an ASN.1 BIT STRING: the message bits are +//! its most significant bits, leading bit first, and the low "unused" bits are ignored (the reversal into +//! the FIPS 202 Appendix B.1 bit order that Keccak absorbs is done internally). For example, the following +//! code uses only the top 3 bits of the final byte: //! ``` //! use bouncycastle_core::traits::Hash; //! use bouncycastle_sha3 as sha3; @@ -57,7 +63,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: //!``` @@ -65,31 +71,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`], 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. @@ -118,13 +132,34 @@ //! ## HMAC //! See [hmac]. //! +//! ## KMAC, TupleHash and ParallelHash +//! The SP 800-185 defines "SHA-3 Derived Functions" KMAC, ParallelHash, and TupleHash, which are +//! functions built on top of SHAKE with further domain-separating inputs bound into the computation. +//! Each takes a customization string `S`, which may be empty; instances with +//! different `S` are unrelated functions (SP 800-185 Sec 8.2.2). +//! +//! The core building block is "customizable SHAKE" or "cSHAKE", which is implemented in this crate +//! but not intended for direct use since NIST SP 800-185 §3.4 says: +//! +//! > The cSHAKE function includes an input string that may be used to provide a function name (N). +//! This is intended for use by NIST in defining SHA-3-derived functions, and should only be set to +//! values defined by NIST +//! +//! See: +//! +//! * [`kmac`] +//! * [`parallelhash`] +//! * [`tuplehash`] +//! //! # Suspending and resuming execution //! //! When hashing a large message, it can be advantageous to be able to suspend the operation //! to a cache and resume it later; for example if waiting for the message to stream over a slow network //! connection. //! -//! For this reason, all SHA3 algorithms impl [`Suspendable`]. +//! For this reason, every SHA3, SHAKE and SP 800-185 type impls [`Suspendable`], squeezers included, +//! so a long output stream can be paused as well as a long input. HMAC is keyed and impls +//! `SuspendableKeyed` instead; see [hmac]. //! //!```rust //! use bouncycastle_sha3 as sha3; @@ -151,20 +186,35 @@ //! //! # Memory Usage //! -//! All SHA3 and SHAKE variants share the same Keccak-f\[1600\] sponge and so have identical memory -//! footprints. No heap memory is used by the algorithms themselves; the `Vec`-returning -//! convenience methods allocate only the output buffer, and the `*_out` variants allocate nothing. -//! -//! | Object | Size (bytes) | -//! |-----------------------------------------|--------------| -//! | `SHA3_224` .. `SHA3_512`, `SHAKE128/256` | 440 | -//! | Suspended state ([`Suspendable`]) | 415 | +//! Everything here shares the same Keccak-f\[1600\] sponge, so sizes differ only by the bookkeeping +//! each function adds; ParallelHash carries a second sponge for the block being filled. No heap +//! memory is used by the algorithms themselves; the `Vec`-returning convenience methods +//! allocate only the output buffer, and the `*_out` variants allocate nothing. +//! +//! | Object | Size (bytes) | +//! |-----------------------------------------------------------------|--------------| +//! | `SHA3_224` .. `SHA3_512`, `SHAKE128/256`, `SHAKESqueezer` | 440 | +//! | `CSHAKE128/256`, `TUPLEHASHXOF128/256`, `LengthBoundSqueezer` | 448 | +//! | `KMACXOF128/256`, `TUPLEHASH128/256` | 456 | +//! | `KMAC128/256` | 464 | +//! | `PARALLELHASHXOF128/256` | 912 | +//! | `PARALLELHASH128/256` | 920 | +//! +//! Suspended states, as `SUSPENDED_*_STATE_LEN`: +//! +//! | State | Size (bytes) | +//! |-----------------------------------------------------------------|--------------| +//! | SHA3, SHAKE and `SHAKESqueezer` | 415 | +//! | cSHAKE, KMACXOF, TupleHashXOF, `LengthBoundSqueezer` | 416 | +//! | KMAC, TupleHash | 424 | +//! | ParallelHashXOF | 852 | +//! | ParallelHash | 860 | //! //! Sizes are `core::mem::size_of` values reported by `mem_usage_benches/bench_sha3_mem_usage.rs` //! (`cargo run --release -p mem_usage_benches --bin bench_sha3_mem_usage`), which also has valgrind //! massif entry points for measuring peak stack usage of the hash, XOF and suspend/resume paths. //! -//! # Security Considerations +//! # 🚨 Security Considerations 🚨 //! //! * SHA3-224/256/384/512 offer 112/128/192/256 bits of collision resistance respectively; SHAKE128 //! and SHAKE256 offer 128 and 256 bits of security for output lengths at least twice that size @@ -173,13 +223,25 @@ //! length must be bound to the digest, include it in the message (FIPS 202 Appendix A.2). //! * The sponge state and queue are held in [`bouncycastle_utils::secret::Secret`] and zeroized on //! drop. +//! * KMAC's security rests on its key and output lengths (SP 800-185 Sec 8.4): the key check is +//! [`kmac::KMACInternal::new_with_params`]'s, with `allow_weak_key` as the bypass, and an output +//! shorter than 8 bytes is the caller's to justify (Sec 8.4.2: never below 4, and below 8 +//! only after a risk analysis). +//! * cSHAKE has SHAKE's prefix property; the fixed-length KMAC, TupleHash and ParallelHash do +//! not, because the output length is bound in, but their XOF forms read as a stream do +//! (Sec 8.2.2). +//! * A customization string is not a key: for any `N` and `S`, cSHAKE has exactly SHAKE's +//! security (Sec 8.2.1). It separates instances; it does not strengthen them. +//! * A suspended KMAC or KMACXOF state inverts to the key, as a suspended HMAC-SHA3 state does +//! (see [hmac]): store it as securely as the key. #![forbid(unsafe_code)] #![forbid(missing_docs)] #![allow(private_bounds)] use crate::keccak::KeccakSize; -use bouncycastle_core::traits::{Algorithm, AlgorithmOID, HashAlgParams, SecurityStrength}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, AlgorithmOID, HashAlgParams}; // imports needed for docs #[allow(unused_imports)] @@ -187,15 +249,19 @@ use bouncycastle_core::errors::HashError; #[allow(unused_imports)] use bouncycastle_core::key_material::{KeyMaterial, KeyType}; #[allow(unused_imports)] -use bouncycastle_core::traits::{Hash, KDF, MAC, Suspendable, XOF}; +use bouncycastle_core::traits::{Hash, KDF, MAC, Suspendable, XOF, XOFSqueezer}; // end of doc-only imports +pub mod hmac; +pub mod kmac; +pub mod parallelhash; +pub mod tuplehash; + +mod cshake; mod keccak; mod sha3; mod shake; -pub mod hmac; - /*** String constants ***/ /// Algorithm name string for SHA3-224, as used by the factories and CLI. pub const SHA3_224_NAME: &str = "SHA3-224"; @@ -211,10 +277,16 @@ pub const SHAKE128_NAME: &str = "SHAKE128"; pub const SHAKE256_NAME: &str = "SHAKE256"; /*** pub types ***/ +pub use keccak::SUSPENDED_SHA3_STATE_LEN; + pub use sha3::SHA3Internal; -pub use shake::SHAKEInternal; -pub use keccak::SUSPENDED_SHA3_STATE_LEN; +pub use shake::{SHAKEInternal, SHAKESqueezer}; + +pub use cshake::{ + CSHAKE128, CSHAKE256, CSHAKEInternal, CSHAKESqueezer, SUSPENDED_CSHAKE_STATE_LEN, + SUSPENDED_LENGTH_BOUND_SQUEEZER_STATE_LEN, +}; /// Public type for SHA3_224. pub type SHA3_224 = SHA3Internal; @@ -231,8 +303,8 @@ pub type SHAKE256 = SHAKEInternal; /*** Param traits ***/ -/// Private trait on purpose so that only the NIST-approved params can be used. -trait SHA3Params: HashAlgParams { +/// Private (sealed) trait on purpose so that only the NIST-approved params can be used. +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 @@ -240,8 +312,6 @@ trait SHA3Params: HashAlgParams { const STATE_TAG: u8; } -// TODO: it would probably be more elegant to macro these. - /// The public hash types expose the same parameters as their `*Params` marker, so the constants /// are defined exactly once (on the params struct) and forwarded here. impl HashAlgParams for SHA3Internal { @@ -335,10 +405,40 @@ impl AlgorithmOID for SHA3_512 { &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0a]; } -trait SHAKEParams: Algorithm { +/// Private (sealed) trait on purpose so that only the NIST-approved params can be used. +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 first of eight state tags for the SP 800-185 functions built on this parameter set, + /// which follow it in the order below. The same rule as [`SHA3Params::STATE_TAG`]: distinct + /// from every other tag in the crate, and never reused. + const SP800_185_STATE_TAG_BASE: u8; + const CSHAKE_STATE_TAG: u8 = Self::SP800_185_STATE_TAG_BASE; + const KMAC_STATE_TAG: u8 = Self::SP800_185_STATE_TAG_BASE + 1; + const KMACXOF_STATE_TAG: u8 = Self::SP800_185_STATE_TAG_BASE + 2; + const TUPLEHASH_STATE_TAG: u8 = Self::SP800_185_STATE_TAG_BASE + 3; + const TUPLEHASHXOF_STATE_TAG: u8 = Self::SP800_185_STATE_TAG_BASE + 4; + const PARALLELHASH_STATE_TAG: u8 = Self::SP800_185_STATE_TAG_BASE + 5; + const PARALLELHASHXOF_STATE_TAG: u8 = Self::SP800_185_STATE_TAG_BASE + 6; + const LENGTH_BOUND_SQUEEZER_STATE_TAG: u8 = Self::SP800_185_STATE_TAG_BASE + 7; } /// The parameters for SHAKE128. #[derive(Clone)] @@ -350,6 +450,14 @@ impl Algorithm for SHAKE128Params { impl SHAKEParams for SHAKE128Params { const SIZE: KeccakSize = KeccakSize::_128; const STATE_TAG: u8 = 5; + const CSHAKE_ALG_NAME: &'static str = cshake::CSHAKE128_NAME; + const KMAC_ALG_NAME: &'static str = kmac::KMAC128_NAME; + const KMACXOF_ALG_NAME: &'static str = kmac::KMACXOF128_NAME; + const TUPLEHASH_ALG_NAME: &'static str = tuplehash::TUPLEHASH128_NAME; + const TUPLEHASHXOF_ALG_NAME: &'static str = tuplehash::TUPLEHASHXOF128_NAME; + const PARALLELHASH_ALG_NAME: &'static str = parallelhash::PARALLELHASH128_NAME; + const PARALLELHASHXOF_ALG_NAME: &'static str = parallelhash::PARALLELHASHXOF128_NAME; + const SP800_185_STATE_TAG_BASE: u8 = 7; // 7..=14 } /// Assigned by NIST in the Computer Security Objects Register: id-shake128 { hashAlgs 11 } impl AlgorithmOID for SHAKE128 { @@ -367,6 +475,14 @@ impl Algorithm for SHAKE256Params { impl SHAKEParams for SHAKE256Params { const SIZE: KeccakSize = KeccakSize::_256; const STATE_TAG: u8 = 6; + const CSHAKE_ALG_NAME: &'static str = cshake::CSHAKE256_NAME; + const KMAC_ALG_NAME: &'static str = kmac::KMAC256_NAME; + const KMACXOF_ALG_NAME: &'static str = kmac::KMACXOF256_NAME; + const TUPLEHASH_ALG_NAME: &'static str = tuplehash::TUPLEHASH256_NAME; + const TUPLEHASHXOF_ALG_NAME: &'static str = tuplehash::TUPLEHASHXOF256_NAME; + const PARALLELHASH_ALG_NAME: &'static str = parallelhash::PARALLELHASH256_NAME; + const PARALLELHASHXOF_ALG_NAME: &'static str = parallelhash::PARALLELHASHXOF256_NAME; + const SP800_185_STATE_TAG_BASE: u8 = 15; // 15..=22 } /// 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..b975c97c --- /dev/null +++ b/crypto/sha3/src/parallelhash.rs @@ -0,0 +1,511 @@ +//! ParallelHash, the parallelisable hash of NIST SP 800-185 Sec 6. +//! +//! The purpose of ParallelHash10 is to support the efficient hashing of very long strings, by taking +//! advantage of the parallelism available in modern processors. ParallelHash supports the 128- and +//! 256-bit security strengths, and also provides variable-length output. Changing any input +//! parameter to ParallelHash, even the requested output length, will result in unrelated output. Like +//! the other functions defined in this document, ParallelHash also supports user-selected +//! customization strings. +//! +//! ParallelHash divides the input bit string X into a sequence of contiguous, non-overlapping +//! blocks, each of length B bytes, and then computes the hash value for each block separately. +//! Finally, these hash values are combined and passed to cSHAKE along with the function name +//! (N) of "ParallelHash", the optional customization string S, and some encoded integer values, +//! to generate the final hash value of the function. +//! +//! # ParallelHash +//! +//!``` +//! use bouncycastle_core::traits::Hash; +//! use bouncycastle_sha3::parallelhash::ParallelHash128; +//! +//! let output: Vec = ParallelHash128::new(8192, b"", 32).hash(b"Hello, world!"); +//! ``` +//! +//! # ParallelHashXOF +//! ParallelHashXOF, is an arbitrary-output-length form of ParallelHash and it implements the [`XOF`] trait. +//! It is a separate function from its fixed-length counterpart since its *final* read ([`XOF::xof`], +//! [`XOFSqueezer::do_output_final`]) binds its output length so that outputs of different lengths, +//! even over the same input, are completely unrelated (ie they don't have the problem that one is +//! a prefix of the other). +//! +//! See [`ParallelHash128`] for detail. +//! +//! Example of `ParallelHash128`: +//! ``` +//! use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; +//! use bouncycastle_sha3::parallelhash::{ParallelHash128, ParallelHashXOF128}; +//! +//! let mut parallelhash = ParallelHashXOF128::new(8192, b""); +//! parallelhash.do_update(b"Hello, world!"); +//! let mut squeezer = parallelhash.into_squeezer(); +//! let first: Vec = squeezer.do_output(16); +//! let more: Vec = squeezer.do_output(1024); +//! +//! let bound: Vec = ParallelHashXOF128::new(8192, b"").xof(b"Hello, world!", 32); +//! assert_eq!(bound, ParallelHash128::new(8192, b"", 32).hash(b"Hello, world!")); +//! assert_ne!(bound[..16], first[..]); +//! ``` + +use crate::cshake::{ + CSHAKE_COMPONENT_LEN, CSHAKEInternal, CSHAKESqueezer, absorb_left_encode_into, right_encode, +}; +use crate::keccak::SHA3_FAMILY_STATE_LEN; +use crate::shake::SHAKEInternal; +use crate::{SHAKE128Params, SHAKE256Params, SHAKEParams}; +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, + suspend_component, +}; + +/// 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"; + +/// Length in bytes of the suspended state of ParallelHash. +pub const SUSPENDED_PARALLELHASH_STATE_LEN: usize = LIB_VERSION_LEN + PARALLEL_STATE_LEN + 8; +/// Length in bytes of the suspended state of ParallelHashXOF. +pub const SUSPENDED_PARALLELHASHXOF_STATE_LEN: usize = LIB_VERSION_LEN + PARALLEL_STATE_LEN; +/// The [`ParallelState`] layout: the outer cSHAKE, the inner SHAKE's family state, then +/// `block_size`, `block_fill` and `blocks` as `u64`s. +const PARALLEL_STATE_LEN: usize = CSHAKE_COMPONENT_LEN + SHA3_FAMILY_STATE_LEN + 24; + +/// The function-name string every ParallelHash binds, per SP 800-185 Sec 6.3. +const PARALLELHASH_FUNCTION_NAME: &[u8] = b"ParallelHash"; + +/// 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; + +/// The shared machinery of [`ParallelHashInternal`] and [`ParallelHashXOFInternal`]: the outer +/// cSHAKE, the block being filled, and the count of blocks hashed so far. +#[derive(Clone)] +struct ParallelState { + cshake: CSHAKEInternal, + block_size: usize, + /// The block being filled, as the SHAKE over its bytes so far: each block's contribution is + /// `SHAKE(block, 2c)`, so the sponge can take the bytes as they arrive. Holding the sponge + /// rather than the bytes keeps this a fixed size whatever `B` is, which a suspended state + /// needs. + inner: SHAKEInternal, + /// Bytes of the current block absorbed into `inner` so far, always less than `block_size`. + block_fill: usize, + 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, inner: SHAKEInternal::new(), block_fill: 0, blocks: 0 } + } + + /// Step 3 for the block in `inner`: finish its digest and absorb it 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_digest(&mut self) { + let mut digest = [0u8; 64]; + let digest = &mut digest[..Self::INNER_LEN]; + core::mem::replace(&mut self.inner, SHAKEInternal::new()).xof_out(&[], digest); + self.cshake.do_update(digest); + self.blocks += 1; + self.block_fill = 0; + } + + fn write_state(&self, tag: u8, out: &mut [u8]) { + let (cshake, rest) = out.split_at_mut(CSHAKE_COMPONENT_LEN); + self.cshake.write_tagged(tag, cshake); + let (inner, rest) = rest.split_at_mut(SHA3_FAMILY_STATE_LEN); + // The inner sponge is plain SHAKE and carries SHAKE's own tag; it is only ever read back + // from inside this state, under the outer tag. + self.inner.write_family_state(PARAMS::STATE_TAG, inner); + let mut w = CursorMut::new(rest); + w.u64(self.block_size as u64); + w.u64(self.block_fill as u64); + w.u64(self.blocks); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], tag: u8) -> Result { + let (cshake, rest) = state.split_at(CSHAKE_COMPONENT_LEN); + let cshake = CSHAKEInternal::read_tagged_customized(cshake, tag)?; + let (inner, rest) = rest.split_at(SHA3_FAMILY_STATE_LEN); + let inner = SHAKEInternal::read_family_state(inner, PARAMS::STATE_TAG)?; + if inner.is_squeezing() { + return Err(SuspendableError::InvalidData); + } + let mut r = Cursor::new(rest); + let block_size = bounded_usize(r.u64(), usize::MAX)?; + // Sec 6.2: 0 < B. The fill is strictly inside the block, since a full block is absorbed + // the moment it completes. + if block_size == 0 { + return Err(SuspendableError::InvalidData); + } + let block_fill = bounded_usize(r.u64(), block_size - 1)?; + let blocks = r.u64(); + debug_assert!(r.is_done()); + Ok(Self { cshake, block_size, inner, block_fill, blocks }) + } + + fn do_update(&mut self, mut data: &[u8]) { + while !data.is_empty() { + let take = (self.block_size - self.block_fill).min(data.len()); + let (now, rest) = data.split_at(take); + self.inner.do_update(now); + self.block_fill += take; + data = rest; + if self.block_fill == self.block_size { + self.absorb_block_digest(); + } + } + } + + /// 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 ([`CSHAKESqueezer`]). + fn finish_blocks(mut self) -> CSHAKEInternal { + if self.block_fill > 0 { + self.absorb_block_digest(); + } + // 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 [`ParallelHash128`] or [`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 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 SuspendableComponent for ParallelHashInternal { + const STATE_LEN: usize = PARALLEL_STATE_LEN + 8; + type Key = (); + + fn write_state(&self, out: &mut [u8]) { + let (state, rest) = out.split_at_mut(PARALLEL_STATE_LEN); + self.state.write_state(PARAMS::PARALLELHASH_STATE_TAG, state); + let mut w = CursorMut::new(rest); + w.u64(self.output_len as u64); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], _key: &()) -> Result { + let (parallel, rest) = state.split_at(PARALLEL_STATE_LEN); + let state = ParallelState::read_state(parallel, PARAMS::PARALLELHASH_STATE_TAG)?; + let mut r = Cursor::new(rest); + let output_len = bounded_usize(r.u64(), usize::MAX)?; + debug_assert!(r.is_done()); + Ok(Self { state, output_len }) + } +} + +/// Suspends mid-block as well as between blocks: the block being filled travels as its sponge. +impl Suspendable + for ParallelHashInternal +{ + fn suspend(self) -> [u8; SUSPENDED_PARALLELHASH_STATE_LEN] { + suspend_component(&self) + } + + fn from_suspended( + state: [u8; SUSPENDED_PARALLELHASH_STATE_LEN], + ) -> Result { + resume_component(&state, &()) + } +} + +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 [`ParallelHashXOF128`] or +/// [`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 SuspendableComponent for ParallelHashXOFInternal { + const STATE_LEN: usize = PARALLEL_STATE_LEN; + type Key = (); + + fn write_state(&self, out: &mut [u8]) { + self.state.write_state(PARAMS::PARALLELHASHXOF_STATE_TAG, out) + } + + fn read_state(state: &[u8], _key: &()) -> Result { + Ok(Self { state: ParallelState::read_state(state, PARAMS::PARALLELHASHXOF_STATE_TAG)? }) + } +} + +/// The absorbing phase, mid-block or not; the squeezing half is a [`CSHAKESqueezer`]. +impl Suspendable + for ParallelHashXOFInternal +{ + fn suspend(self) -> [u8; SUSPENDED_PARALLELHASHXOF_STATE_LEN] { + suspend_component(&self) + } + + fn from_suspended( + state: [u8; SUSPENDED_PARALLELHASHXOF_STATE_LEN], + ) -> Result { + resume_component(&state, &()) + } +} + +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_output_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 = CSHAKESqueezer; + + // 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 { + CSHAKESqueezer::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/sha3.rs b/crypto/sha3/src/sha3.rs index 4a5bad02..407d83e6 100644 --- a/crypto/sha3/src/sha3.rs +++ b/crypto/sha3/src/sha3.rs @@ -4,10 +4,11 @@ use crate::keccak::{ serialize_sha3_family_state, }; use bouncycastle_core::errors::{HashError, KDFError, SuspendableError}; -use bouncycastle_core::key_material; +use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; -use bouncycastle_core::traits::{Algorithm, Hash, KDF, SecurityStrength, Suspendable}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, KDF, Suspendable}; +use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_utils::{max, min}; /// Internal struct for SHA3. @@ -47,8 +48,9 @@ impl SHA3Internal { /// Appends the SHA3 domain-separation suffix and pads as per FIPS 202 s. 6.1, then squeezes the digest. /// /// Private, infallible body shared by [`Hash::do_final_out`] and [`Hash::do_final_partial_bits_out`]. - /// `num_partial_bits` (0..=7, validated by the caller) trailing message bits are taken from the - /// least significant bits of `partial_byte` (FIPS 202 Appendix B.1 bit ordering). FIPS 202 s. 6.1 + /// The `num_partial_bits` (0..=7, validated by the caller) trailing message bits are the most + /// significant bits of `partial_byte`, leading bit first (ASN.1 BIT STRING order); they are reversed + /// below into the FIPS 202 Appendix B.1 bit ordering that Keccak absorbs. FIPS 202 s. 6.1 /// defines SHA3-d(M) = KECCAK[c](M || 01, d), so the two suffix bits are appended directly above /// the message bits; pad10*1 is then applied by the sponge when it switches to squeezing. /// @@ -65,8 +67,12 @@ impl SHA3Internal { // Mutants note: This is just bit-setting into empty space. // It works the same regardless of whether it's OR or XOR. - let mut final_input: u16 = - ((partial_byte as u16) & ((1 << num_partial_bits) - 1)) | (0x02 << num_partial_bits); + // 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 | (0x02 << num_partial_bits); let mut final_bits = num_partial_bits + 2; // If message bits + suffix fill a whole byte, absorb it as a normal byte first. @@ -133,7 +139,7 @@ impl SHA3Internal { let mut key_type = self.kdf_key_type; let output_security_strength = self.kdf_security_strength; let mut bytes_written: usize = 0; - key_material::do_hazardous_operations(output_key, |output_key| { + do_hazardous_operations(output_key, |output_key| { bytes_written = self.do_final_out(output_key.ref_to_bytes_mut()?); output_key.set_key_len(bytes_written)?; Ok(()) @@ -147,7 +153,7 @@ impl SHA3Internal { if key_type == KeyType::Zeroized { key_type = KeyType::Unknown; } - key_material::do_hazardous_operations(&mut *output_key, |output_key| { + do_hazardous_operations(&mut *output_key, |output_key| { output_key.set_key_type(key_type)?; output_key.set_security_strength(*min( &output_security_strength, diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index 4d1a87a1..9b332a99 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -4,10 +4,11 @@ use crate::keccak::{ deserialize_sha3_family_state, serialize_sha3_family_state, }; use bouncycastle_core::errors::{HashError, KDFError, SuspendableError}; -use bouncycastle_core::key_material; +use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; -use bouncycastle_core::traits::{Algorithm, KDF, SecurityStrength, Suspendable, XOF}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, KDF, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; use bouncycastle_utils::{max, min}; /// Internal struct for SHAKE. @@ -53,32 +54,85 @@ impl SHAKEInternal { } } - /// Swallows errors and simply returns an empty Vec if the hashes fails for whatever reason. + /// Writes the SHA3-family state under `tag` into `out`, which is exactly + /// `SHA3_FAMILY_STATE_LEN` bytes: the sponge and the KDF metadata, with no version header. + /// For the functions built on SHAKE, which stamp their own tag and add fields of their own. + pub(crate) fn write_family_state(&self, tag: u8, out: &mut [u8]) { + let out: &mut [u8; SHA3_FAMILY_STATE_LEN] = + out.try_into().expect("a family state is exactly SHA3_FAMILY_STATE_LEN bytes"); + serialize_sha3_family_state( + out, + tag, + &self.keccak, + self.kdf_key_type, + self.kdf_security_strength, + self.kdf_entropy, + ); + } + + /// The reverse of [`Self::write_family_state`]. The sponge comes back in whichever phase it + /// was suspended in; [`Self::is_squeezing`] says which, and the caller decides what that + /// means for it. + pub(crate) fn read_family_state(state: &[u8], tag: u8) -> Result { + let input: &[u8; SHA3_FAMILY_STATE_LEN] = + state.try_into().map_err(|_| SuspendableError::InvalidData)?; + let rate = 1600 - ((PARAMS::SIZE as usize) << 1); + let (keccak, kdf_key_type, kdf_security_strength, kdf_entropy) = + deserialize_sha3_family_state(input, tag, rate)?; + Ok(Self { + _phantomdata: core::marker::PhantomData, + keccak, + kdf_key_type, + kdf_security_strength, + kdf_entropy, + }) + } + + /// Whether the sponge has begun producing output. + pub(crate) fn is_squeezing(&self) -> bool { + self.keccak.squeezing + } + 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) + } - // 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; + /// 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 } + } + + /// 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()); @@ -94,9 +148,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( @@ -132,12 +185,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( + do_hazardous_operations(output_key, |output_key| { + 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) @@ -147,7 +199,7 @@ impl SHAKEInternal { if self.kdf_key_type == KeyType::Zeroized { self.kdf_key_type = KeyType::Unknown; } - key_material::do_hazardous_operations(output_key, |output_key| { + do_hazardous_operations(output_key, |output_key| { output_key.set_key_type(self.kdf_key_type)?; output_key.set_security_strength(*min( &self.kdf_security_strength, @@ -191,6 +243,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, @@ -274,113 +334,264 @@ 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 SHAKESqueezer { + /// Wraps a sponge that is already squeezing, as read back from a suspended state. + pub(crate) fn from_squeezing(shake: SHAKEInternal) -> Self { + debug_assert!(shake.is_squeezing()); + Self { shake } } - fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize { - // hash_internal_out zeroizes `output` before writing. - self.hash_internal_out(data, output) + /// [`SHAKEInternal::write_family_state`] for the squeezing half. + pub(crate) fn write_family_state(&self, tag: u8, out: &mut [u8]) { + self.shake.write_family_state(tag, out) } +} - /// 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 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 do_output_out(&mut self, output: &mut [u8]) -> usize { + self.shake.squeeze_internal_out(output) + } +} + +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. - let mut final_input: u16 = - ((partial_byte as u16) & ((1 << num_partial_bits) - 1)) | (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_output_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_output_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_output_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 xof(self, data: &[u8], result_len: usize) -> Vec { + self.hash_internal(data, result_len) } - 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_out(self, data: &[u8], output: &mut [u8]) -> usize { + // hash_internal_out zeroizes `output` before writing. + self.hash_internal_out(data, output) } +} - /// Result is the number of bits squezed into `output`. - fn squeeze_partial_byte_final_out( +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; + } - *output = buf[0] & ((1u8 << num_bits) - 1); - 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..1c22fa89 --- /dev/null +++ b/crypto/sha3/src/tuplehash.rs @@ -0,0 +1,422 @@ +//! TupleHash, the tuple-hashing function of NIST SP 800-185 Sec 5. +//! +//! # TupleHash +//! TupleHash is a [`Hash`] over a sequence of strings rather than one string: each +//! [`Hash::do_update`] call is one tuple element, so the chunking is part of the input. +//! +//! The advantage of TupleHash over straight SHAKE is that `TupleHash( ("ab", "cd") )` and `TupleHash( ("a", "bcd") )` +//! yield unrelated outputs. +//! +//! `TupleHash` has two interfaces: `.hash_tuple()` which takes an array-of-arrays, or successive calls to `.do_update()`. +//!``` +//! use bouncycastle_core::traits::Hash; +//! use bouncycastle_sha3::tuplehash::TupleHash128; +//! +//! // .hash_tuple() takes tuples as an array of arrays +//! let tuple: [&[u8]; 2] = [b"user id", b"session"]; +//! let output: Vec = TupleHash128::new(b"", 32).hash_tuple(&tuple); +//! +//! // The same computation, one element per .do_update() +//! let mut th = TupleHash128::new(b"", 32); +//! th.do_update(b"user id"); +//! th.do_update(b"session"); +//! assert_eq!(th.do_final(), output); +//! ``` +//! +//! # TupleHashXOF +//! TupleHashXOF, is an arbitrary-output-length form of TupleHash and it implements the [`XOF`] trait. +//! It is a separate function from its fixed-length counterpart since its *final* read ([`XOF::xof`], +//! [`XOFSqueezer::do_output_final`]) binds its output length so that outputs of different lengths, +//! even over the same input, are completely unrelated (ie they don't have the problem that one is +//! a prefix of the other). +//! +//! See [`TupleHashXOF128`] for detail. +//! +//! Example of `KMACXOF128`: +//!``` +//! use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; +//! use bouncycastle_sha3::tuplehash::{TupleHash128, TupleHashXOF128}; +//! +//! let mut tuplehash = TupleHashXOF128::new(b""); +//! tuplehash.do_update(b"Hello, world!"); +//! let mut squeezer = tuplehash.into_squeezer(); +//! let first: Vec = squeezer.do_output(16); +//! let more: Vec = squeezer.do_output(1024); +//! +//! let bound: Vec = TupleHashXOF128::new(b"").xof(b"Hello, world!", 32); +//! assert_eq!(bound, TupleHash128::new(b"", 32).hash(b"Hello, world!")); +//! assert_ne!(bound[..16], first[..]); +//! ``` + +use crate::cshake::{ + CSHAKE_COMPONENT_LEN, CSHAKEInternal, CSHAKESqueezer, absorb_encoded_string_into, right_encode, +}; +use crate::{SHAKE128Params, SHAKE256Params, SHAKEParams}; +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, Hash, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_utils::suspendable_state::{ + Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, bounded_usize, resume_component, + suspend_component, +}; + +/// 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"; + +/// Length in bytes of the suspended state of TupleHash. +pub const SUSPENDED_TUPLEHASH_STATE_LEN: usize = LIB_VERSION_LEN + CSHAKE_COMPONENT_LEN + 8; +/// Length in bytes of the suspended state of TupleHashXOF. +pub const SUSPENDED_TUPLEHASHXOF_STATE_LEN: usize = LIB_VERSION_LEN + CSHAKE_COMPONENT_LEN; + +/// The function-name string every TupleHash binds, per SP 800-185 Sec 5.3. +const TUPLEHASH_FUNCTION_NAME: &[u8] = b"TupleHash"; + +/// 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; + +/// Internal struct for TupleHash. Use [`TupleHash128`] or [`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 SuspendableComponent for TupleHashInternal { + const STATE_LEN: usize = CSHAKE_COMPONENT_LEN + 8; + type Key = (); + + fn write_state(&self, out: &mut [u8]) { + let (cshake, rest) = out.split_at_mut(CSHAKE_COMPONENT_LEN); + self.cshake.write_tagged(PARAMS::TUPLEHASH_STATE_TAG, cshake); + let mut w = CursorMut::new(rest); + w.u64(self.output_len as u64); + debug_assert!(w.is_done()); + } + + fn read_state(state: &[u8], _key: &()) -> Result { + let (cshake, rest) = state.split_at(CSHAKE_COMPONENT_LEN); + let cshake = CSHAKEInternal::read_tagged_customized(cshake, PARAMS::TUPLEHASH_STATE_TAG)?; + let mut r = Cursor::new(rest); + let output_len = bounded_usize(r.u64(), usize::MAX)?; + debug_assert!(r.is_done()); + Ok(Self { cshake, output_len }) + } +} + +/// Elements are absorbed whole, so a suspended TupleHash is always between elements. +impl Suspendable for TupleHashInternal { + fn suspend(self) -> [u8; SUSPENDED_TUPLEHASH_STATE_LEN] { + suspend_component(&self) + } + + fn from_suspended( + state: [u8; SUSPENDED_TUPLEHASH_STATE_LEN], + ) -> Result { + resume_component(&state, &()) + } +} + +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 [`TupleHashXOF128`] or [`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_output_final`] and [`XOF::xof`] produce the fixed-length +/// TupleHash of Sec 5.3 (see [`CSHAKESqueezer`]), 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]]) -> CSHAKESqueezer { + for element in tuple { + self.do_update(element); + } + self.into_squeezer() + } +} + +impl SuspendableComponent for TupleHashXOFInternal { + const STATE_LEN: usize = CSHAKE_COMPONENT_LEN; + type Key = (); + + fn write_state(&self, out: &mut [u8]) { + self.cshake.write_tagged(PARAMS::TUPLEHASHXOF_STATE_TAG, out) + } + + fn read_state(state: &[u8], _key: &()) -> Result { + let cshake = CSHAKEInternal::read_tagged_customized(state, PARAMS::TUPLEHASHXOF_STATE_TAG)?; + Ok(Self { cshake }) + } +} + +// The absorbing phase, always between elements; the squeezing half is a LengthBoundSqueezer. +impl Suspendable + for TupleHashXOFInternal +{ + fn suspend(self) -> [u8; SUSPENDED_TUPLEHASHXOF_STATE_LEN] { + suspend_component(&self) + } + + fn from_suspended( + state: [u8; SUSPENDED_TUPLEHASHXOF_STATE_LEN], + ) -> Result { + resume_component(&state, &()) + } +} + +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_output_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 = CSHAKESqueezer; + + // 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 { + CSHAKESqueezer::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/tests/cshake_bc-test-data.rs b/crypto/sha3/tests/cshake_bc-test-data.rs new file mode 100644 index 00000000..5ac5af99 --- /dev/null +++ b/crypto/sha3/tests/cshake_bc-test-data.rs @@ -0,0 +1,105 @@ +//! NIST SP 800-185 sample values for cSHAKE128/256. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data" (same convention as the sha2/sha3 crates), under +//! `crypto/sp800-185/`. If it is not present the tests print a warning and pass vacuously. + +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; +use bouncycastle_hex as hex; +use bouncycastle_sha3::{CSHAKE128, CSHAKE256}; + +const TEST_DATA_DIR: &str = "crypto/sp800-185"; + +/// One `COUNT` block of a `.rsp` file. +struct Vector { + strength: usize, + n: String, + s: String, + output_len: usize, + msg: Vec, + output: Vec, +} + +/// Parses an SP 800-185 cSHAKE `.rsp` file into its `COUNT` blocks. +fn parse_rsp_file(content: &str) -> Vec { + 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); + out +} + +fn read_vectors(filename: &str) -> Option> { + Some(parse_rsp_file(&bc_test_data(TEST_DATA_DIR, filename)?)) +} + +/// 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()); +} + +/// 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/cshake_tests.rs b/crypto/sha3/tests/cshake_tests.rs new file mode 100644 index 00000000..d420d4fe --- /dev/null +++ b/crypto/sha3/tests/cshake_tests.rs @@ -0,0 +1,127 @@ +//! cSHAKE behaviour tests. The SP 800-185 sample values are in `cshake_bc-test-data.rs`. + +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; +use bouncycastle_sha3::{CSHAKE128, CSHAKE256, SHAKE128, SHAKE256}; + +/// 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"); +} diff --git a/crypto/sha3/tests/kmac_bc-test-data.rs b/crypto/sha3/tests/kmac_bc-test-data.rs new file mode 100644 index 00000000..efeb55ed --- /dev/null +++ b/crypto/sha3/tests/kmac_bc-test-data.rs @@ -0,0 +1,352 @@ +//! NIST SP 800-185 sample values for KMAC128/256 and KMACXOF128/256. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data" (same convention as the sha2/sha3 crates), under +//! `crypto/sp800-185/`. If it is not present the tests print a warning and pass vacuously. + +use bouncycastle_core::errors::MACError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{Hash, MAC, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_core_test_framework::xof::TestFrameworkXOF; +use bouncycastle_hex as hex; +use bouncycastle_sha3::kmac::{KMAC128, KMAC256, KMACXOF128, KMACXOF256}; + +const TEST_DATA_DIR: &str = "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, +} + +/// Parses an SP 800-185 KMAC `.rsp` file into its `COUNT` blocks. +fn parse_rsp_file(content: &str) -> Vec { + 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); + out +} + +fn read_vectors(filename: &str) -> Option> { + Some(parse_rsp_file(&bc_test_data(TEST_DATA_DIR, filename)?)) +} + +/// 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" + ); + } +} + +/// `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_output_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_output_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_output_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_output_final(n - split); + assert_eq!([head, tail].concat(), xof_expected, "{ctx}: do_final after a read stays the XOF"); +} + +/// 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}"), + } + } +} diff --git a/crypto/sha3/tests/kmac_tests.rs b/crypto/sha3/tests/kmac_tests.rs new file mode 100644 index 00000000..71c6f818 --- /dev/null +++ b/crypto/sha3/tests/kmac_tests.rs @@ -0,0 +1,227 @@ +//! KMAC and KMACXOF behaviour tests. The SP 800-185 sample values are in `kmac_bc-test-data.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_sha3::kmac::{KMAC128, KMAC256, KMACXOF128, KMACXOF256}; + +/// A 32-byte MAC key carries a 256-bit strength, so it 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") +} + +/// 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" + ); +} + +/// 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"); +} + +/// `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/kmac_wycheproof.rs b/crypto/sha3/tests/kmac_wycheproof.rs new file mode 100644 index 00000000..5e01ef60 --- /dev/null +++ b/crypto/sha3/tests/kmac_wycheproof.rs @@ -0,0 +1,79 @@ +//! Known-answer tests against Project Wycheproof's +//! `testvectors_v1/kmac{128,256}_no_customization_test.json`. +//! +//! Requires the Wycheproof repository (https://github.com/C2SP/wycheproof) to be cloned alongside +//! this repository, i.e. at `../wycheproof` relative to the root of this git project. If it is +//! absent the tests print a warning and pass, matching the convention used by the other vector +//! suites. +//! +//! Every case uses an empty customization string and asks for the group's `tagSize` as the output +//! length. KMAC binds that length into its input (SP 800-185 Sec 4.3), so each tag is a full-length +//! KMAC of that size and goes through `verify`, as well as through `mac` for the valid ones. + +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::MAC; +use bouncycastle_core_test_framework::test_data_loaders::{Value, hex_field, wycheproof_json}; +use bouncycastle_sha3::kmac::{KMAC128, KMAC256}; + +/// The longest key in either file is 129 bytes. +const MAX_KEY_LEN: usize = 160; + +/// Runs every case in one KMAC file; `new_kmac(key, output_len)` builds the KMAC under test. +fn run( + filename: &str, + algorithm: &str, + new_kmac: impl Fn(&KeyMaterial, usize) -> M, +) { + let Some(doc) = wycheproof_json(filename) else { return }; + + assert_eq!(doc.get("algorithm").and_then(Value::as_str), Some(algorithm), "{filename}"); + + let (mut valid_count, mut invalid_count) = (0usize, 0usize); + for group in doc.get("testGroups").and_then(Value::as_array).expect("testGroups") { + let tag_len = group.get("tagSize").and_then(Value::as_u64).expect("tagSize") as usize / 8; + + 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 ctx = format!("{filename} tcId {tc_id}"); + let key = KeyMaterial::::from_bytes_as_type( + &hex_field(test, "key", tc_id), + KeyType::MACKey, + ) + .expect("a MAC key"); + let msg = hex_field(test, "msg", tc_id); + let tag = hex_field(test, "tag", tc_id); + + match test.get("result").and_then(Value::as_str).expect("result") { + "valid" => { + assert_eq!(new_kmac(&key, tag_len).mac(&msg), tag, "{ctx}: mac"); + assert!(new_kmac(&key, tag_len).verify(&msg, &tag), "{ctx}: verify"); + valid_count += 1; + } + "invalid" => { + assert!(!new_kmac(&key, tag_len).verify(&msg, &tag), "{ctx}: verify"); + invalid_count += 1; + } + other => panic!("{ctx}: unexpected result {other}"), + } + } + } + + println!("Wycheproof {algorithm}: {valid_count} valid and {invalid_count} invalid cases run"); + assert!(valid_count > 0 && invalid_count > 0, "{filename}: expected both valid and invalid"); +} + +// Every key in these files is at least 256 bits, so neither needs the weak-key escape hatch. + +#[test] +fn wycheproof_kmac128() { + run("kmac128_no_customization_test.json", "KMAC128", |key, output_len| { + KMAC128::new_with_params(key, b"", output_len, false).expect("a KMAC128 instance") + }); +} + +#[test] +fn wycheproof_kmac256() { + run("kmac256_no_customization_test.json", "KMAC256", |key, output_len| { + KMAC256::new_with_params(key, b"", output_len, false).expect("a KMAC256 instance") + }); +} diff --git a/crypto/sha3/tests/parallelhash_bc-test-data.rs b/crypto/sha3/tests/parallelhash_bc-test-data.rs new file mode 100644 index 00000000..5dca6f16 --- /dev/null +++ b/crypto/sha3/tests/parallelhash_bc-test-data.rs @@ -0,0 +1,369 @@ +//! NIST SP 800-185 sample values for ParallelHash128/256 and ParallelHashXOF128/256. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data" (same convention as the sha2/sha3 crates), under +//! `crypto/sp800-185/`. If it is not present the tests print a warning and pass vacuously. + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_sha3::parallelhash::{ + ParallelHash128, ParallelHash256, ParallelHashXOF128, ParallelHashXOF256, +}; + +const TEST_DATA_DIR: &str = "crypto/sp800-185"; + +/// One `COUNT` block of a `.rsp` file. +struct Vector { + strength: usize, + block_size: usize, + s: String, + output_len: usize, + msg: Vec, + output: Vec, +} + +/// Parses an SP 800-185 ParallelHash `.rsp` file into its `COUNT` blocks. +fn parse_rsp_file(content: &str) -> Vec { + 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); + out +} + +fn read_vectors(filename: &str) -> Option> { + Some(parse_rsp_file(&bc_test_data(TEST_DATA_DIR, filename)?)) +} + +/// 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_output_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_output_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_output_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_output_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"); + } +} + +/// 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}"), + } + } +} diff --git a/crypto/sha3/tests/parallelhash_tests.rs b/crypto/sha3/tests/parallelhash_tests.rs new file mode 100644 index 00000000..d461024f --- /dev/null +++ b/crypto/sha3/tests/parallelhash_tests.rs @@ -0,0 +1,141 @@ +//! ParallelHash and ParallelHashXOF behaviour tests. The SP 800-185 sample values are in +//! `parallelhash_bc-test-data.rs`. + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::hash::TestFrameworkHash; +use bouncycastle_sha3::parallelhash::{ + ParallelHash128, ParallelHash256, ParallelHashXOF128, ParallelHashXOF256, +}; + +/// 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 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/sha3_bc-test-data.rs b/crypto/sha3/tests/sha3_bc-test-data.rs new file mode 100644 index 00000000..1bd6ae98 --- /dev/null +++ b/crypto/sha3/tests/sha3_bc-test-data.rs @@ -0,0 +1,312 @@ +//! NIST SHA3VS and FIPS 202 example vectors for SHA3-224/256/384/512. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data" (same convention as the other `*_bc-test-data.rs` +//! suites). If it is not present the tests print a warning and pass vacuously. +//! +//! Two vector sets are used: +//! +//! * NIST CAVP SHA3VS, under `crypto/sha3/{bit-oriented,byte-oriented}/`. +//! * The NIST FIPS 202 example values, `crypto/SHA3TestVectors.txt`. +//! +//! The SHA3VS files pack bit strings per FIPS 202 Appendix B.1 (Algorithms 10/11, h2b/b2h): the +//! excess bits of a `Len`-bit message occupy the *least significant* bits of the final `Msg` byte, +//! first bit in the LSB. The API takes partial bytes in ASN.1 BIT STRING order (X.690 s. 8.6.2.1: +//! first bit in the MSB, unused low bits), so the harness bit-reverses the final message byte +//! before absorbing it (`u8::reverse_bits`). +//! +//! SHA3VS test types exercised (SHA3VS s. 6): +//! +//! * ShortMsg / LongMsg — `Len` (bits), `Msg`, `MD`. +//! * Monte (s. 6.2.2) — `MD0 = Seed`; for i in 1..=1000: `MDi = SHA3(MDi-1)`; report `MD1000` +//! per COUNT and reseed with it. + +use bouncycastle_core::traits::Hash; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; + +// --------------------------------------------------------------------------------------------- +// NIST CAVP SHA3VS (`crypto/sha3/{bit-oriented,byte-oriented}/`) +// --------------------------------------------------------------------------------------------- + +/// Splits a `Key = value` or `[Key = value]` line from a `.rsp` file. +fn kv(line: &str) -> Option<(&str, &str)> { + let line = line.trim().trim_start_matches('[').trim_end_matches(']'); + let (k, v) = line.split_once('=')?; + Some((k.trim(), v.trim())) +} + +fn parse_hex(v: &str) -> Vec { + hex::decode(v).expect("bad hex") +} + +fn parse_num(v: &str) -> usize { + v.parse().expect("bad number") +} + +struct MsgCase { + len_bits: usize, + msg: Vec, + md: Vec, +} + +/// Parses a SHA3 ShortMsg/LongMsg or SHAKE ShortMsg/LongMsg file into `(Len, Msg, MD|Output)`. +fn parse_msg_file(content: &str) -> Vec { + let mut cases = vec![]; + let (mut len_bits, mut msg) = (None, None); + for line in content.lines() { + let Some((k, v)) = kv(line) else { continue }; + match k { + "Len" => len_bits = Some(parse_num(v)), + "Msg" => msg = Some(parse_hex(v)), + "MD" | "Output" => cases.push(MsgCase { + len_bits: len_bits.take().expect("digest without Len"), + msg: msg.take().expect("digest without Msg"), + md: parse_hex(v), + }), + _ => {} + } + } + cases +} + +/// Hashes the first `len_bits` bits of `msg` (FIPS 202 B.1 packing: excess bits in the LSBs, so the +/// final byte is bit-reversed into the API's MSB-first order). +fn sha3_bits(msg: &[u8], len_bits: usize) -> Vec { + let whole_bytes = len_bits / 8; + let partial_bits = len_bits % 8; + if partial_bits == 0 { + // CAVP writes `Msg = 00` for Len = 0, so always slice rather than using msg directly. + H::default().hash(&msg[..whole_bytes]) + } else { + let mut h = H::default(); + h.do_update(&msg[..whole_bytes]); + h.do_final_partial_bits(msg[whole_bytes].reverse_bits(), partial_bits) + .expect("partial_bits is in 1..=7") + } +} + +fn run_sha3_msg_file(orientation: &str, filename: &str) { + let Some(content) = bc_test_data(&format!("crypto/sha3/{orientation}"), filename) else { + return; + }; + let cases = parse_msg_file(&content); + assert!(!cases.is_empty(), "{orientation}/{filename}: no test cases parsed"); + let mut partial_cases = 0; + for c in &cases { + partial_cases += usize::from(c.len_bits % 8 != 0); + assert_eq!( + sha3_bits::(&c.msg, c.len_bits), + c.md, + "{orientation}/{filename}: Len = {}", + c.len_bits + ); + } + if orientation == "bit-oriented" { + assert!(partial_cases > 0, "{orientation}/{filename}: expected bit-length cases"); + } + println!("{orientation}/{filename}: {} cases ({partial_cases} bit-length)", cases.len()); +} + +/// SHA3VS s. 6.2.2 Monte Carlo test for the fixed-length SHA3 functions. +fn run_sha3_monte_file(orientation: &str, filename: &str) { + let Some(content) = bc_test_data(&format!("crypto/sha3/{orientation}"), filename) else { + return; + }; + let mut seed = None; + let mut mds = vec![]; + for line in content.lines() { + let Some((k, v)) = kv(line) else { continue }; + match k { + "Seed" => seed = Some(parse_hex(v)), + "MD" => mds.push(parse_hex(v)), + _ => {} + } + } + let mut md = seed.expect("Monte file without Seed"); + assert_eq!(mds.len(), 100, "{orientation}/{filename}: expected 100 COUNTs"); + for (count, expected) in mds.iter().enumerate() { + // MD0 = Seed; for i = 1 to 1000: MDi = SHA3(MDi-1); MDj = MD1000; Seed = MDj + for _ in 1..=1000 { + md = H::default().hash(&md); + } + assert_eq!(&md, expected, "{orientation}/{filename}: COUNT = {count}"); + } + println!("{orientation}/{filename}: {} counts", mds.len()); +} + +macro_rules! sha3_cavp_tests { + ($mod:ident, $hash:ty, $prefix:literal) => { + mod $mod { + use super::*; + + #[test] + fn bit_oriented_short_msg() { + run_sha3_msg_file::<$hash>("bit-oriented", concat!($prefix, "ShortMsg.rsp")); + } + #[test] + fn bit_oriented_long_msg() { + run_sha3_msg_file::<$hash>("bit-oriented", concat!($prefix, "LongMsg.rsp")); + } + #[test] + fn bit_oriented_monte() { + run_sha3_monte_file::<$hash>("bit-oriented", concat!($prefix, "Monte.rsp")); + } + #[test] + fn byte_oriented_short_msg() { + run_sha3_msg_file::<$hash>("byte-oriented", concat!($prefix, "ShortMsg.rsp")); + } + #[test] + fn byte_oriented_long_msg() { + run_sha3_msg_file::<$hash>("byte-oriented", concat!($prefix, "LongMsg.rsp")); + } + #[test] + fn byte_oriented_monte() { + run_sha3_monte_file::<$hash>("byte-oriented", concat!($prefix, "Monte.rsp")); + } + } + }; +} + +sha3_cavp_tests!(sha3_224, SHA3_224, "SHA3_224"); +sha3_cavp_tests!(sha3_256, SHA3_256, "SHA3_256"); +sha3_cavp_tests!(sha3_384, SHA3_384, "SHA3_384"); +sha3_cavp_tests!(sha3_512, SHA3_512, "SHA3_512"); + +// --------------------------------------------------------------------------------------------- +// NIST FIPS 202 example values (`crypto/SHA3TestVectors.txt`) +// --------------------------------------------------------------------------------------------- + +const SAMPLE_OF: &str = " sample of "; +const MSG_HEADER: &str = "Msg as bit string"; +const HASH_HEADER: &str = "Hash val is"; + +struct TestCase { + algorithm: usize, + bits: usize, + msg: Vec, + hash: Vec, +} + +/// Parses a NIST FIPS 202 example-vector file. +fn parse_test_vectors(content: &str) -> Vec { + let mut test_vectors: Vec = vec![]; + let string_content: Vec = content.lines().map(String::from).collect(); + + let mut i = 0; + while i < string_content.len() { + if string_content[i].contains(SAMPLE_OF) { + let header = string_content[i].split(SAMPLE_OF).collect::>(); + + let algorithm = + header[0].split("-").collect::>()[1].parse::().unwrap(); + let bits = header[1].split("-").collect::>()[0].parse::().unwrap(); + + i += 2; + if !string_content[i].contains(MSG_HEADER) { + panic!("Missing header {}", MSG_HEADER); + } + + i += 1; + let mut block: Vec = vec![]; + while string_content[i].len() != 0 { + if string_content[i].trim().eq("#(empty message)") { + i += 1; + break; + } + let line = string_content[i].replace(" ", ""); + block.append(&mut Vec::from(line)); + i += 1; + } + if block.len() != bits { + panic!("Test vector length mismatch: block len = {}, bits = {}", block.len(), bits) + } + let msg = decode_binary(&mut block); + + i += 1; + if !string_content[i].contains(HASH_HEADER) { + panic!("Missing header {}", HASH_HEADER); + } + + i += 1; + let mut block: Vec = vec![]; + while string_content[i].len() != 0 { + let line = string_content[i].replace(" ", ""); + block.append(&mut Vec::from(line)); + i += 1; + } + let hash = hex::decode(&*String::from_utf8(block).unwrap()).unwrap(); + + let v = TestCase { algorithm, bits, msg, hash }; + test_vectors.push(v); + } + i += 1; + } + + test_vectors +} + +fn decode_binary(block: &mut Vec) -> Vec { + let bits = block.len(); + let full_bytes = bits / 8; + let total_bytes = (bits + 7) / 8; + let mut result = vec![0u8; total_bytes]; + + // Whole bytes are packed per FIPS 202 Appendix B.1 (Algorithm 11, b2h: message bit 8i + j has + // weight 2^j in byte i, i.e. the first bit is the LSB), which is how SHA-3 reads a byte-oriented + // message. + for i in 0..full_bytes { + let index = i * 8; + block[index..(index + 8)].reverse(); + result[i] = parse_binary(&block[index..(index + 8)]); + } + + // The trailing partial byte is packed the way the API takes it: the remaining message bits + // in order from the most significant bit down (ASN.1 BIT STRING order, X.690 s. 8.6.2.1), + // with the unused low bits zero. + if total_bytes > full_bytes { + let partial_bits = bits - full_bytes * 8; + result[full_bytes] = parse_binary(&block[(full_bytes * 8)..]) << (8 - partial_bits); + } + + result +} + +fn parse_binary(block: &[u8]) -> u8 { + let str = std::str::from_utf8(block).unwrap(); + isize::from_str_radix(str, 2).unwrap() as u8 +} + +#[test] +fn run_kats() { + let Some(content) = bc_test_data("crypto", "SHA3TestVectors.txt") else { return }; + run_test_vectors(parse_test_vectors(&content)); +} + +fn run_test_vectors(test_vectors: Vec) { + for tc in test_vectors { + match tc.algorithm { + 224 => run_test_case(tc, SHA3_224::new()), + 256 => run_test_case(tc, SHA3_256::new()), + 384 => run_test_case(tc, SHA3_384::new()), + 512 => run_test_case(tc, SHA3_512::new()), + _ => panic!("Unsupported algorithm {}", tc.algorithm), + } + } +} + +fn run_test_case(tc: TestCase, mut sha3: impl Hash) { + let partial_bits = tc.bits % 8; + let output: Vec; + + if partial_bits == 0 { + sha3.do_update(tc.msg.as_slice()); + output = sha3.do_final(); + } else { + sha3.do_update(&tc.msg[..(tc.msg.len() - 1)]); + output = sha3.do_final_partial_bits(tc.msg[tc.msg.len() - 1], partial_bits).unwrap(); + } + + assert_eq!(tc.hash, output); +} diff --git a/crypto/sha3/tests/sha3_tests.rs b/crypto/sha3/tests/sha3_tests.rs index 0a3c686f..e58a7eb6 100644 --- a/crypto/sha3/tests/sha3_tests.rs +++ b/crypto/sha3/tests/sha3_tests.rs @@ -1,12 +1,12 @@ #[cfg(test)] mod sha3_tests { - use super::sha3_test_helpers::*; use bouncycastle_core::errors::HashError; - use bouncycastle_core::key_material; + use bouncycastle_core::hazmat::do_hazardous_operations; use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; - use bouncycastle_core::traits::{Hash, HashAlgParams, KDF, SecurityStrength}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{Hash, HashAlgParams, KDF}; use bouncycastle_core_test_framework::DUMMY_SEED; use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_core_test_framework::kdf::TestFrameworkKDF; @@ -383,7 +383,7 @@ mod sha3_tests { let mut output_seed = SHA3_256::new() .derive_key(&input_seed, b"some addtional input to the KDF") .expect("Error happened"); - key_material::do_hazardous_operations(&mut *output_seed, |output_seed| { + do_hazardous_operations(&mut *output_seed, |output_seed| { output_seed.set_key_type(KeyType::MACKey) }) .unwrap(); @@ -414,11 +414,6 @@ mod sha3_tests { assert_eq!(KDF::max_security_strength(&SHA3_512::default()), SecurityStrength::_256bit); } - #[test] - fn run_kats() { - run_test_vectors(read_test_vectors("SHA3TestVectors.txt")); - } - #[test] fn serializable_state() { use bouncycastle_core::errors::SuspendableError; @@ -475,168 +470,4 @@ mod sha3_tests { _ => panic!("Expected an error when loading a SHA3-256 state into SHAKE256"), } } - - fn run_test_vectors(test_vectors: Vec) { - for tc in test_vectors { - match tc.algorithm { - 224 => run_test_case(tc, SHA3_224::new()), - 256 => run_test_case(tc, SHA3_256::new()), - 384 => run_test_case(tc, SHA3_384::new()), - 512 => run_test_case(tc, SHA3_512::new()), - _ => panic!("Unsupported algorithm {}", tc.algorithm), - } - } - } - - fn run_test_case(tc: TestCase, mut sha3: impl Hash) { - let partial_bits = tc.bits % 8; - let output: Vec; - - if partial_bits == 0 { - sha3.do_update(tc.msg.as_slice()); - output = sha3.do_final(); - } else { - sha3.do_update(&tc.msg[..(tc.msg.len() - 1)]); - output = sha3.do_final_partial_bits(tc.msg[tc.msg.len() - 1], partial_bits).unwrap(); - } - - assert_eq!(tc.hash, output); - } -} - -/** Constant helpers **/ - -pub(crate) mod sha3_test_helpers { - use bouncycastle_hex as hex; - use std::fs; - use std::path::Path; - use std::sync::Once; - - // Test vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), - // which must be cloned alongside this repo at "../bc-test-data" (same convention as the mldsa - // and mlkem crates). If it is not present the vector tests print a warning and pass vacuously. - const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto"; - const TEST_DATA_PATH: &str = "../bc-test-data/crypto"; - - static TEST_DATA_CHECK: Once = Once::new(); - - /// Returns the contents of `filename` from bc-test-data, or `None` (after a one-time warning) - /// if the repo is not checked out. - fn get_test_data(filename: &str) -> Option { - let dir = - [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH].into_iter().find(|d| Path::new(d).exists()); - TEST_DATA_CHECK.call_once(|| match dir { - Some(d) => println!("bc-test-data found at: {d:?}"), - None => { - println!("WARNING: bc-test-data directory not found; vector tests will be skipped") - } - }); - let dir = dir?; - Some( - fs::read_to_string(format!("{dir}/{filename}")) - .expect("failed to read test vector file"), - ) - } - - const SAMPLE_OF: &str = " sample of "; - const MSG_HEADER: &str = "Msg as bit string"; - const HASH_HEADER: &str = "Hash val is"; - - pub(crate) struct TestCase { - pub(crate) algorithm: usize, - pub(crate) bits: usize, - pub(crate) msg: Vec, - pub(crate) hash: Vec, - } - - /// Parses the named NIST FIPS 202 example-vector file from bc-test-data. Returns an empty list - /// (skipping the test) if bc-test-data is not available. - pub(crate) fn read_test_vectors(filename: &str) -> Vec { - let mut test_vectors: Vec = vec![]; - let Some(content) = get_test_data(filename) else { - return test_vectors; - }; - let string_content: Vec = content.lines().map(String::from).collect(); - - let mut i = 0; - while i < string_content.len() { - if string_content[i].contains(SAMPLE_OF) { - let header = string_content[i].split(SAMPLE_OF).collect::>(); - - let algorithm = - header[0].split("-").collect::>()[1].parse::().unwrap(); - let bits = header[1].split("-").collect::>()[0].parse::().unwrap(); - - i += 2; - if !string_content[i].contains(MSG_HEADER) { - panic!("Missing header {}", MSG_HEADER); - } - - i += 1; - let mut block: Vec = vec![]; - while string_content[i].len() != 0 { - if string_content[i].trim().eq("#(empty message)") { - i += 1; - break; - } - let line = string_content[i].replace(" ", ""); - block.append(&mut Vec::from(line)); - i += 1; - } - if block.len() != bits { - panic!( - "Test vector length mismatch: block len = {}, bits = {}", - block.len(), - bits - ) - } - let msg = decode_binary(&mut block); - - i += 1; - if !string_content[i].contains(HASH_HEADER) { - panic!("Missing header {}", HASH_HEADER); - } - - i += 1; - let mut block: Vec = vec![]; - while string_content[i].len() != 0 { - let line = string_content[i].replace(" ", ""); - block.append(&mut Vec::from(line)); - i += 1; - } - let hash = hex::decode(&*String::from_utf8(block).unwrap()).unwrap(); - - let v = TestCase { algorithm, bits, msg, hash }; - test_vectors.push(v); - } - i += 1; - } - - test_vectors - } - - fn decode_binary(block: &mut Vec) -> Vec { - let bits = block.len(); - let full_bytes = bits / 8; - let total_bytes = (bits + 7) / 8; - let mut result = vec![0u8; total_bytes]; - - for i in 0..full_bytes { - let index = i * 8; - block[index..(index + 8)].reverse(); - result[i] = parse_binary(&block[index..(index + 8)]); - } - - if total_bytes > full_bytes { - block[(full_bytes * 8)..].reverse(); - result[full_bytes] = parse_binary(&block[(full_bytes * 8)..]); - } - - result - } - - fn parse_binary(block: &[u8]) -> u8 { - let str = std::str::from_utf8(block).unwrap(); - isize::from_str_radix(str, 2).unwrap() as u8 - } } diff --git a/crypto/sha3/tests/cavp_tests.rs b/crypto/sha3/tests/shake_bc-test-data.rs similarity index 55% rename from crypto/sha3/tests/cavp_tests.rs rename to crypto/sha3/tests/shake_bc-test-data.rs index 54069c88..4aeb076c 100644 --- a/crypto/sha3/tests/cavp_tests.rs +++ b/crypto/sha3/tests/shake_bc-test-data.rs @@ -1,55 +1,39 @@ -//! NIST CAVP SHA3VS test vectors for SHA3-224/256/384/512 and SHAKE128/256. +//! NIST SHA3VS and FIPS 202 example vectors for SHAKE128/256. //! //! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be -//! cloned alongside this repo at "../bc-test-data" (same convention as the mldsa/mlkem/sha2 crates), -//! under `crypto/sha3/{bit-oriented,byte-oriented}/`. If it is not present the tests print a warning -//! and pass vacuously. +//! cloned alongside this repo at "../bc-test-data" (same convention as the other `*_bc-test-data.rs` +//! suites). If it is not present the tests print a warning and pass vacuously. //! -//! Bit ordering: unlike the SHA-2 CAVP files, SHA-3 CAVP follows FIPS 202 Appendix B.1 — the excess -//! bits of a `Len`-bit message occupy the *least significant* bits of the final `Msg` byte, and the -//! excess bits of an `Outputlen`-bit SHAKE output occupy the least significant bits of the final -//! `Output` byte (verified over every partial case in the files: all high bits are zero). This is -//! exactly the convention of [`Hash::do_final_partial_bits`] / [`XOF::absorb_last_partial_byte`] / -//! [`XOF::squeeze_partial_byte_final`], so no shifting is needed. +//! Two vector sets are used: //! -//! Test types exercised (SHA3VS s. 6): +//! * NIST CAVP SHA3VS, under `crypto/sha3/{bit-oriented,byte-oriented}/`. +//! * The NIST FIPS 202 example values, `crypto/SHAKETestVectors.txt`. //! -//! * SHA3 ShortMsg / LongMsg — `Len` (bits), `Msg`, `MD`. -//! * SHA3 Monte (s. 6.2.2) — `MD0 = Seed`; for i in 1..=1000: `MDi = SHA3(MDi-1)`; report `MD1000` -//! per COUNT and reseed with it. -//! * SHAKE ShortMsg / LongMsg — `Len` (bits), `Msg`, `Output` at the fixed `[Outputlen]` of the file. -//! * SHAKE VariableOut — `Outputlen` (bits, not necessarily a multiple of 8), `Msg`, `Output`. -//! * SHAKE Monte (s. 6.2.3) — `Outputlen = maxoutlen`; for i in 1..=1000: `Msg = leftmost 128 bits +//! The SHA3VS files pack bit strings per FIPS 202 Appendix B.1 (Algorithms 10/11, h2b/b2h): the +//! excess bits of a `Len`-bit message occupy the *least significant* bits of the final `Msg` byte, +//! first bit in the LSB, and likewise the excess bits of an `Outputlen`-bit SHAKE output occupy the +//! least significant bits of the final `Output` byte. The API takes and returns partial bytes in +//! ASN.1 BIT STRING order (X.690 s. 8.6.2.1: first bit in the MSB, unused low bits), so the harness +//! bit-reverses the final message byte before absorbing it and the final output byte after squeezing +//! it (`u8::reverse_bits`). +//! +//! SHA3VS test types exercised (SHA3VS s. 6): +//! +//! * ShortMsg / LongMsg — `Len` (bits), `Msg`, `Output` at the fixed `[Outputlen]` of the file. +//! * VariableOut — `Outputlen` (bits, not necessarily a multiple of 8), `Msg`, `Output`. +//! * Monte (s. 6.2.3) — `Outputlen = maxoutlen`; for i in 1..=1000: `Msg = leftmost 128 bits //! of the previous Output (zero-padded)`, `Output = SHAKE(Msg, Outputlen)`, then //! `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::{XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; use bouncycastle_hex as hex; -use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; -use std::fs; -use std::path::Path; -use std::sync::Once; - -const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto/sha3"; -const TEST_DATA_PATH: &str = "../bc-test-data/crypto/sha3"; - -static TEST_DATA_CHECK: Once = Once::new(); - -/// Returns the contents of `/` from bc-test-data, or `None` (after a one-time -/// warning) if the repo is not checked out. -fn get_test_data(orientation: &str, filename: &str) -> Option { - let dir = [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH].into_iter().find(|d| Path::new(d).exists()); - TEST_DATA_CHECK.call_once(|| match dir { - Some(d) => println!("bc-test-data found at: {d:?}"), - None => println!("WARNING: bc-test-data directory not found; CAVP tests will be skipped"), - }); - let dir = dir?; - Some( - fs::read_to_string(format!("{dir}/{orientation}/{filename}")) - .expect("failed to read CAVP test vector file"), - ) -} +use bouncycastle_sha3::{SHAKE128, SHAKE256}; + +// --------------------------------------------------------------------------------------------- +// NIST CAVP SHA3VS (`crypto/sha3/{bit-oriented,byte-oriented}/`) +// --------------------------------------------------------------------------------------------- /// Splits a `Key = value` or `[Key = value]` line from a `.rsp` file. fn kv(line: &str) -> Option<(&str, &str)> { @@ -66,10 +50,6 @@ fn parse_num(v: &str) -> usize { v.parse().expect("bad number") } -// --------------------------------------------------------------------------------------------- -// SHA3 (fixed-length) tests -// --------------------------------------------------------------------------------------------- - struct MsgCase { len_bits: usize, msg: Vec, @@ -96,88 +76,34 @@ fn parse_msg_file(content: &str) -> Vec { cases } -/// Hashes the first `len_bits` bits of `msg` (FIPS 202 B.1 packing: excess bits in the LSBs). -fn sha3_bits(msg: &[u8], len_bits: usize) -> Vec { - let whole_bytes = len_bits / 8; - let partial_bits = len_bits % 8; - if partial_bits == 0 { - // CAVP writes `Msg = 00` for Len = 0, so always slice rather than using msg directly. - H::default().hash(&msg[..whole_bytes]) - } else { - let mut h = H::default(); - h.do_update(&msg[..whole_bytes]); - h.do_final_partial_bits(msg[whole_bytes], partial_bits).expect("partial_bits is in 1..=7") - } -} - -fn run_sha3_msg_file(orientation: &str, filename: &str) { - let Some(content) = get_test_data(orientation, filename) else { return }; - let cases = parse_msg_file(&content); - assert!(!cases.is_empty(), "{orientation}/{filename}: no test cases parsed"); - let mut partial_cases = 0; - for c in &cases { - partial_cases += usize::from(c.len_bits % 8 != 0); - assert_eq!( - sha3_bits::(&c.msg, c.len_bits), - c.md, - "{orientation}/{filename}: Len = {}", - c.len_bits - ); - } - if orientation == "bit-oriented" { - assert!(partial_cases > 0, "{orientation}/{filename}: expected bit-length cases"); - } - println!("{orientation}/{filename}: {} cases ({partial_cases} bit-length)", cases.len()); -} - -/// SHA3VS s. 6.2.2 Monte Carlo test for the fixed-length SHA3 functions. -fn run_sha3_monte_file(orientation: &str, filename: &str) { - let Some(content) = get_test_data(orientation, filename) else { return }; - let mut seed = None; - let mut mds = vec![]; - for line in content.lines() { - let Some((k, v)) = kv(line) else { continue }; - match k { - "Seed" => seed = Some(parse_hex(v)), - "MD" => mds.push(parse_hex(v)), - _ => {} - } - } - let mut md = seed.expect("Monte file without Seed"); - assert_eq!(mds.len(), 100, "{orientation}/{filename}: expected 100 COUNTs"); - for (count, expected) in mds.iter().enumerate() { - // MD0 = Seed; for i = 1 to 1000: MDi = SHA3(MDi-1); MDj = MD1000; Seed = MDj - for _ in 1..=1000 { - md = H::default().hash(&md); - } - assert_eq!(&md, expected, "{orientation}/{filename}: COUNT = {count}"); - } - println!("{orientation}/{filename}: {} counts", mds.len()); -} - -// --------------------------------------------------------------------------------------------- -// SHAKE tests -// --------------------------------------------------------------------------------------------- - /// SHAKE of the first `len_bits` bits of `msg`, producing `out_bits` bits of output (FIPS 202 B.1 -/// packing on both sides: excess bits in the LSBs of the final byte). +/// packing on both sides: excess bits in the LSBs of the final byte, so the final input byte is +/// bit-reversed into the API's MSB-first order and the final output byte is bit-reversed back). 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], 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")); + // 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 } fn run_shake_msg_file(orientation: &str, filename: &str) { - let Some(content) = get_test_data(orientation, filename) else { return }; + let Some(content) = bc_test_data(&format!("crypto/sha3/{orientation}"), filename) else { + return; + }; let out_bits = content .lines() .filter_map(kv) @@ -210,7 +136,9 @@ struct VarOutCase { } fn run_shake_variable_out_file(orientation: &str, filename: &str) { - let Some(content) = get_test_data(orientation, filename) else { return }; + let Some(content) = bc_test_data(&format!("crypto/sha3/{orientation}"), filename) else { + return; + }; let mut cases = vec![]; let (mut out_bits, mut msg) = (None, None); for line in content.lines() { @@ -249,7 +177,9 @@ fn run_shake_variable_out_file(orientation: &str, filename: &s /// SHA3VS s. 6.2.3 Monte Carlo test for SHAKE. fn run_shake_monte_file(orientation: &str, filename: &str) { - let Some(content) = get_test_data(orientation, filename) else { return }; + let Some(content) = bc_test_data(&format!("crypto/sha3/{orientation}"), filename) else { + return; + }; let (mut min_bits, mut max_bits, mut msg) = (None, None, None); let mut expected: Vec<(usize, Vec)> = vec![]; let mut out_len = None; @@ -282,7 +212,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; @@ -299,43 +229,6 @@ fn run_shake_monte_file(orientation: &str, filename: &str) { println!("{orientation}/{filename}: {} counts", expected.len()); } -// --------------------------------------------------------------------------------------------- -// Test matrix -// --------------------------------------------------------------------------------------------- - -macro_rules! sha3_cavp_tests { - ($mod:ident, $hash:ty, $prefix:literal) => { - mod $mod { - use super::*; - - #[test] - fn bit_oriented_short_msg() { - run_sha3_msg_file::<$hash>("bit-oriented", concat!($prefix, "ShortMsg.rsp")); - } - #[test] - fn bit_oriented_long_msg() { - run_sha3_msg_file::<$hash>("bit-oriented", concat!($prefix, "LongMsg.rsp")); - } - #[test] - fn bit_oriented_monte() { - run_sha3_monte_file::<$hash>("bit-oriented", concat!($prefix, "Monte.rsp")); - } - #[test] - fn byte_oriented_short_msg() { - run_sha3_msg_file::<$hash>("byte-oriented", concat!($prefix, "ShortMsg.rsp")); - } - #[test] - fn byte_oriented_long_msg() { - run_sha3_msg_file::<$hash>("byte-oriented", concat!($prefix, "LongMsg.rsp")); - } - #[test] - fn byte_oriented_monte() { - run_sha3_monte_file::<$hash>("byte-oriented", concat!($prefix, "Monte.rsp")); - } - } - }; -} - macro_rules! shake_cavp_tests { ($mod:ident, $xof:ty, $prefix:literal) => { mod $mod { @@ -383,9 +276,148 @@ macro_rules! shake_cavp_tests { }; } -sha3_cavp_tests!(sha3_224, SHA3_224, "SHA3_224"); -sha3_cavp_tests!(sha3_256, SHA3_256, "SHA3_256"); -sha3_cavp_tests!(sha3_384, SHA3_384, "SHA3_384"); -sha3_cavp_tests!(sha3_512, SHA3_512, "SHA3_512"); shake_cavp_tests!(shake128, SHAKE128, "SHAKE128"); shake_cavp_tests!(shake256, SHAKE256, "SHAKE256"); + +// --------------------------------------------------------------------------------------------- +// NIST FIPS 202 example values (`crypto/SHAKETestVectors.txt`) +// --------------------------------------------------------------------------------------------- + +const SAMPLE_OF: &str = " sample of "; +const MSG_HEADER: &str = "Msg as bit string"; +const OUTPUT_HEADER: &str = "Output val is"; + +struct TestCase { + algorithm: usize, + bits: usize, + msg: Vec, + output: Vec, +} + +/// Parses a NIST FIPS 202 example-vector file. +fn parse_test_vectors(content: &str) -> Vec { + let mut test_vectors: Vec = vec![]; + let string_content: Vec = content.lines().map(String::from).collect(); + + let mut i = 0; + while i < string_content.len() { + if string_content[i].contains(SAMPLE_OF) { + let header = string_content[i].split(SAMPLE_OF).collect::>(); + + let algorithm = + header[0].split("-").collect::>()[1].parse::().unwrap(); + let bits = header[1].split("-").collect::>()[0].parse::().unwrap(); + + i += 2; + if !string_content[i].contains(MSG_HEADER) { + panic!("Missing header {}", MSG_HEADER); + } + + i += 1; + let mut block: Vec = vec![]; + while string_content[i].len() != 0 { + if string_content[i].trim().eq("#(empty message)") { + i += 1; + break; + } + let line = string_content[i].replace(" ", ""); + block.append(&mut Vec::from(line)); + i += 1; + } + if block.len() != bits { + panic!("Test vector length mismatch: block len = {}, bits = {}", block.len(), bits) + } + let msg = decode_binary(&mut block); + + i += 1; + if !string_content[i].contains(OUTPUT_HEADER) { + panic!("Missing header {}", OUTPUT_HEADER); + } + + i += 1; + let mut block: Vec = vec![]; + while string_content[i].len() != 0 { + let line = string_content[i].replace(" ", ""); + block.append(&mut Vec::from(line)); + i += 1; + } + let output = hex::decode(&*String::from_utf8(block).unwrap()).unwrap(); + + let v = TestCase { algorithm, bits, msg, output }; + test_vectors.push(v); + } + i += 1; + } + + test_vectors +} + +fn decode_binary(block: &mut Vec) -> Vec { + let bits = block.len(); + let full_bytes = bits / 8; + let total_bytes = (bits + 7) / 8; + let mut result = vec![0u8; total_bytes]; + + // Whole bytes are packed per FIPS 202 Appendix B.1 (Algorithm 11, b2h: message bit 8i + j has + // weight 2^j in byte i, i.e. the first bit is the LSB), which is how SHA-3 reads a byte-oriented + // message. + for i in 0..full_bytes { + let index = i * 8; + block[index..(index + 8)].reverse(); + result[i] = parse_binary(&block[index..(index + 8)]); + } + + // The trailing partial byte is packed the way the API takes it: the remaining message bits + // in order from the most significant bit down (ASN.1 BIT STRING order, X.690 s. 8.6.2.1), + // with the unused low bits zero. + if total_bytes > full_bytes { + let partial_bits = bits - full_bytes * 8; + result[full_bytes] = parse_binary(&block[(full_bytes * 8)..]) << (8 - partial_bits); + } + + result +} + +fn parse_binary(block: &[u8]) -> u8 { + let str = std::str::from_utf8(block).unwrap(); + isize::from_str_radix(str, 2).unwrap() as u8 +} + +#[test] +fn run_kats() { + let Some(content) = bc_test_data("crypto", "SHAKETestVectors.txt") else { return }; + run_test_vectors(parse_test_vectors(&content)); +} + +fn run_test_vectors(test_vectors: Vec) { + for tc in test_vectors { + //println!("SHA3-{} {}-bits", &tc.algorithm, &tc.bits); + //println!("msg {}", hex::encode_upper(&tc.msg)); + //println!("hashes {}", hex::encode_upper(&tc.hashes)); + + match tc.algorithm { + 128 => run_test_case(tc, SHAKE128::new()), + 256 => run_test_case(tc, SHAKE256::new()), + _ => panic!("Unsupported algorithm {}", tc.algorithm), + } + } +} + +fn run_test_case(tc: TestCase, mut shake: impl XOF) { + let partial_bits = tc.bits % 8; + let output: Vec; + + if partial_bits == 0 { + shake.do_update(tc.msg.as_slice()); + let mut shake = shake.into_squeezer(); + output = shake.do_output(tc.output.len()); + } else { + 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/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index e10e5c85..b004407c 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -2,186 +2,124 @@ extern crate core; #[cfg(test)] mod shake_tests { - use super::shake_test_helpers::*; use bouncycastle_core::errors::HashError; use bouncycastle_core::key_material::{ KeyMaterial, KeyMaterial256, KeyMaterial512, KeyMaterialTrait, KeyType, }; - use bouncycastle_core::traits::{KDF, SecurityStrength, XOF}; + use bouncycastle_core::security_strength::SecurityStrength; + 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 the low `i` bits of it are the low `i` set bits. - assert_eq!(out, ((1u16 << i) - 1) 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, 0x01); - } - - /// 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 - /// low `num_bits` bits of the next output byte (FIPS 202 B.1 bit ordering), zero-extended. - #[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 & ((1u8 << n) - 1), "skip={skip} n={n}"); - assert_eq!(got >> n, 0, "high 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. + /// 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(0x08, 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(0x7F, 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); - - assert_eq!(first.as_slice(), &clean[..16]); - assert_eq!(second.as_slice(), &clean[16..]); + 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); } + /// 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 test_update_bytes() { - for tc in read_test_vectors("SHAKETestVectors.txt") { - //println!("SHAKE-{} {}-bits", &tc.algorithm, &tc.bits); - //println!("msg {}", hex::encode_upper(&tc.msg)); - //println!("hashes {}", hex::encode_upper(&tc.output)); - - match tc.algorithm { - 128 => run_test_case(tc, SHAKE128::new()), - 256 => run_test_case(tc, SHAKE256::new()), - _ => panic!("Unsupported algorithm {}", tc.algorithm), - } - } + 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] @@ -335,21 +273,16 @@ 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); - } - - #[test] - fn run_kats() { - run_test_vectors(read_test_vectors("SHAKETestVectors.txt")); + assert_eq!(Hash::max_security_strength(&SHAKE256::default()), SecurityStrength::_256bit); } #[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] @@ -361,36 +294,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"), } @@ -403,7 +358,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 */ } @@ -411,182 +366,11 @@ 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 */ } _ => panic!("Expected an error when loading a SHAKE256 state into SHA3-256"), } } - - fn run_test_vectors(test_vectors: Vec) { - for tc in test_vectors { - //println!("SHA3-{} {}-bits", &tc.algorithm, &tc.bits); - //println!("msg {}", hex::encode_upper(&tc.msg)); - //println!("hashes {}", hex::encode_upper(&tc.hashes)); - - match tc.algorithm { - 128 => run_test_case(tc, SHAKE128::new()), - 256 => run_test_case(tc, SHAKE256::new()), - _ => panic!("Unsupported algorithm {}", tc.algorithm), - } - } - } - - fn run_test_case(tc: TestCase, mut shake: impl XOF) { - let partial_bits = tc.bits % 8; - 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()); - } 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()); - } - - assert_eq!(tc.output, output); - } -} - -/** Constant helpers **/ - -pub(crate) mod shake_test_helpers { - use bouncycastle_hex as hex; - use std::fs; - use std::path::Path; - use std::sync::Once; - - // Test vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), - // which must be cloned alongside this repo at "../bc-test-data" (same convention as the mldsa - // and mlkem crates). If it is not present the vector tests print a warning and pass vacuously. - const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto"; - const TEST_DATA_PATH: &str = "../bc-test-data/crypto"; - - static TEST_DATA_CHECK: Once = Once::new(); - - /// Returns the contents of `filename` from bc-test-data, or `None` (after a one-time warning) - /// if the repo is not checked out. - fn get_test_data(filename: &str) -> Option { - let dir = - [TEST_DATA_PATH_RELATIVE, TEST_DATA_PATH].into_iter().find(|d| Path::new(d).exists()); - TEST_DATA_CHECK.call_once(|| match dir { - Some(d) => println!("bc-test-data found at: {d:?}"), - None => { - println!("WARNING: bc-test-data directory not found; vector tests will be skipped") - } - }); - let dir = dir?; - Some( - fs::read_to_string(format!("{dir}/{filename}")) - .expect("failed to read test vector file"), - ) - } - - const SAMPLE_OF: &str = " sample of "; - const MSG_HEADER: &str = "Msg as bit string"; - const OUTPUT_HEADER: &str = "Output val is"; - - pub(crate) struct TestCase { - pub(crate) algorithm: usize, - pub(crate) bits: usize, - pub(crate) msg: Vec, - pub(crate) output: Vec, - } - - /// Parses the named NIST FIPS 202 example-vector file from bc-test-data. Returns an empty list - /// (skipping the test) if bc-test-data is not available. - pub(crate) fn read_test_vectors(filename: &str) -> Vec { - let mut test_vectors: Vec = vec![]; - let Some(content) = get_test_data(filename) else { - return test_vectors; - }; - let string_content: Vec = content.lines().map(String::from).collect(); - - let mut i = 0; - while i < string_content.len() { - if string_content[i].contains(SAMPLE_OF) { - let header = string_content[i].split(SAMPLE_OF).collect::>(); - - let algorithm = - header[0].split("-").collect::>()[1].parse::().unwrap(); - let bits = header[1].split("-").collect::>()[0].parse::().unwrap(); - - i += 2; - if !string_content[i].contains(MSG_HEADER) { - panic!("Missing header {}", MSG_HEADER); - } - - i += 1; - let mut block: Vec = vec![]; - while string_content[i].len() != 0 { - if string_content[i].trim().eq("#(empty message)") { - i += 1; - break; - } - let line = string_content[i].replace(" ", ""); - block.append(&mut Vec::from(line)); - i += 1; - } - if block.len() != bits { - panic!( - "Test vector length mismatch: block len = {}, bits = {}", - block.len(), - bits - ) - } - let msg = decode_binary(&mut block); - - i += 1; - if !string_content[i].contains(OUTPUT_HEADER) { - panic!("Missing header {}", OUTPUT_HEADER); - } - - i += 1; - let mut block: Vec = vec![]; - while string_content[i].len() != 0 { - let line = string_content[i].replace(" ", ""); - block.append(&mut Vec::from(line)); - i += 1; - } - let output = hex::decode(&*String::from_utf8(block).unwrap()).unwrap(); - - let v = TestCase { algorithm, bits, msg, output }; - test_vectors.push(v); - } - i += 1; - } - - test_vectors - } - - fn decode_binary(block: &mut Vec) -> Vec { - let bits = block.len(); - let full_bytes = bits / 8; - let total_bytes = (bits + 7) / 8; - let mut result = vec![0u8; total_bytes]; - - for i in 0..full_bytes { - let index = i * 8; - block[index..(index + 8)].reverse(); - result[i] = parse_binary(&block[index..(index + 8)]); - } - - if total_bytes > full_bytes { - block[(full_bytes * 8)..].reverse(); - result[full_bytes] = parse_binary(&block[(full_bytes * 8)..]); - } - - result - } - - fn parse_binary(block: &[u8]) -> u8 { - let str = std::str::from_utf8(block).unwrap(); - isize::from_str_radix(str, 2).unwrap() as u8 - } } diff --git a/crypto/sha3/tests/sp800_185_suspend_tests.rs b/crypto/sha3/tests/sp800_185_suspend_tests.rs new file mode 100644 index 00000000..a017904f --- /dev/null +++ b/crypto/sha3/tests/sp800_185_suspend_tests.rs @@ -0,0 +1,276 @@ +//! Suspend/resume for the SP 800-185 functions: round trips in each phase, the rejections that +//! keep one function's state out of another, and the field checks of each layout. +//! +//! Offsets used below, from the layouts in the crate: 3 version bytes, then the variant tag at +//! 3, the sponge's `squeezing` flag at 404 (tag + 400 bytes of Keccak state), the cSHAKE +//! `customized` byte at 415, and whatever the function adds from 416. + +use bouncycastle_core::errors::SuspendableError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{Hash, MAC, Suspendable, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; +use bouncycastle_sha3::kmac::*; +use bouncycastle_sha3::parallelhash::*; +use bouncycastle_sha3::tuplehash::*; +use bouncycastle_sha3::*; + +const PART1: &[u8] = b"Colorless green ideas"; +const PART2: &[u8] = b" sleep furiously"; + +const SQUEEZING_FLAG: usize = 3 + 1 + 400; +const CUSTOMIZED: usize = 3 + 412; + +fn key() -> KeyMaterial<32> { + KeyMaterial::<32>::from_bytes_as_type(&[0x42u8; 32], KeyType::MACKey).expect("a MAC key") +} + +/// Feeds `PART1`, suspends, resumes, feeds `PART2`: the digest must match the uninterrupted one. +/// For TupleHash the two parts are two elements, on both sides. +fn hash_round_trip + Clone>(make: impl Fn() -> H) { + let mut h = make(); + h.do_update(PART1); + TestFrameworkSuspendableState::new().test(&h); + let state = h.clone().suspend(); + h.do_update(PART2); + let expected = h.do_final(); + + let mut resumed = H::from_suspended(state).expect("a state resumes as its own type"); + resumed.do_update(PART2); + assert_eq!(resumed.do_final(), expected); +} + +/// Suspends the squeezer before its first read and again after a streamed read. The first state +/// still has the fixed-length choice open, so a final read after resume is the fixed-length +/// function and a streamed read is the XOF; the second continues the stream. +fn squeezer_round_trip(make: impl Fn() -> X) +where + X: XOF + Clone, + X::Squeezer: Suspendable + Clone, +{ + let mut x = make(); + x.do_update(PART1); + let fixed = x.clone().into_squeezer().do_output_final(32); + let mut stream = x.into_squeezer(); + TestFrameworkSuspendableState::new().test(&stream); + let unbound_state = stream.clone().suspend(); + let first = stream.do_output(16); + + let resumed = X::Squeezer::from_suspended(unbound_state).expect("an unbound squeezer resumes"); + assert_eq!(resumed.do_output_final(32), fixed, "a final read after resume binds its length"); + let mut resumed = X::Squeezer::from_suspended(unbound_state).unwrap(); + assert_eq!(resumed.do_output(16), first, "a streamed read after resume is the XOF"); + + TestFrameworkSuspendableState::new().test(&stream); + let squeezing_state = stream.clone().suspend(); + let more = stream.do_output(100); + let mut resumed = X::Squeezer::from_suspended(squeezing_state).expect("a squeezing one too"); + assert_eq!(resumed.do_output(100), more, "the resumed stream continues where it stopped"); +} + +#[test] +fn cshake_round_trips() { + hash_round_trip(|| CSHAKE128::new(b"", b"Email Signature")); + hash_round_trip(|| CSHAKE256::new(b"", b"Email Signature")); + // Uncustomized cSHAKE is SHAKE, and must come back that way. + hash_round_trip(|| CSHAKE128::new(b"", b"")); + squeezer_round_trip(|| CSHAKE128::new(b"", b"Email Signature")); + squeezer_round_trip(|| CSHAKE256::new(b"", b"")); +} + +#[test] +fn kmac_round_trips() { + fn mac_round_trip + Clone>(make: impl Fn() -> M) { + let mut m = make(); + m.do_update(PART1); + TestFrameworkSuspendableState::new().test(&m); + let state = m.clone().suspend(); + m.do_update(PART2); + let expected = m.do_final(); + + let mut resumed = M::from_suspended(state).expect("a KMAC state resumes"); + assert_eq!(resumed.output_len(), expected.len(), "the output length is part of the state"); + resumed.do_update(PART2); + assert!(resumed.do_verify_final(&expected)); + } + mac_round_trip(|| KMAC128::new(&key()).unwrap()); + mac_round_trip(|| KMAC256::new(&key()).unwrap()); + mac_round_trip(|| { + KMAC128::new_with_params(&key(), b"My Tagged Application", 16, false).unwrap() + }); +} + +#[test] +fn kmacxof_round_trips() { + hash_round_trip(|| KMACXOF128::new(&key(), b"", false).unwrap()); + hash_round_trip(|| KMACXOF256::new(&key(), b"S", false).unwrap()); + squeezer_round_trip(|| KMACXOF128::new(&key(), b"", false).unwrap()); + squeezer_round_trip(|| KMACXOF256::new(&key(), b"S", false).unwrap()); +} + +#[test] +fn tuplehash_round_trips() { + hash_round_trip(|| TupleHash128::new(b"", 32)); + hash_round_trip(|| TupleHash256::new(b"My Tuple App", 48)); + hash_round_trip(|| TupleHashXOF128::new(b"")); + hash_round_trip(|| TupleHashXOF256::new(b"My Tuple App")); + squeezer_round_trip(|| TupleHashXOF128::new(b"")); + squeezer_round_trip(|| TupleHashXOF256::new(b"My Tuple App")); +} + +#[test] +fn parallelhash_round_trips() { + // PART1 is 21 bytes: a block size of 8 suspends five bytes into a block, 7 suspends exactly + // on a block boundary, and 64 suspends before the first block completes. + for block_size in [8usize, 7, 64] { + hash_round_trip(|| ParallelHash128::new(block_size, b"", 32)); + hash_round_trip(|| ParallelHash256::new(block_size, b"Parallel Data", 64)); + hash_round_trip(|| ParallelHashXOF128::new(block_size, b"")); + hash_round_trip(|| ParallelHashXOF256::new(block_size, b"Parallel Data")); + squeezer_round_trip(|| ParallelHashXOF128::new(block_size, b"")); + squeezer_round_trip(|| ParallelHashXOF256::new(block_size, b"Parallel Data")); + } +} + +/// A state is accepted only by the type that wrote it, even where the layouts are identical. +#[test] +fn each_type_rejects_the_others() { + fn rejected>(state: [u8; N], what: &str) { + assert!( + matches!(T::from_suspended(state), Err(SuspendableError::InvalidData)), + "{what} must be rejected" + ); + } + let mut x = CSHAKE128::new(b"", b"S"); + x.do_update(PART1); + rejected::<_, CSHAKE256>(x.clone().suspend(), "a cSHAKE128 state in cSHAKE256"); + rejected::<_, KMACXOF128>(x.clone().suspend(), "a cSHAKE128 state in KMACXOF128"); + rejected::<_, TupleHashXOF128>(x.clone().suspend(), "a cSHAKE128 state in TupleHashXOF128"); + rejected::<_, CSHAKESqueezer>(x.suspend(), "a cSHAKE128 state in a squeezer"); + + let mut k = KMACXOF128::new(&key(), b"", false).unwrap(); + k.do_update(PART1); + rejected::<_, CSHAKE128>(k.clone().suspend(), "a KMACXOF128 state in cSHAKE128"); + rejected::<_, TupleHashXOF128>(k.clone().suspend(), "a KMACXOF128 state in TupleHashXOF128"); + rejected::<_, KMACXOF256>(k.clone().suspend(), "a KMACXOF128 state in KMACXOF256"); + let squeezer = k.into_squeezer(); + rejected::<_, KMACXOF128>(squeezer.clone().suspend(), "an unbound squeezer in KMACXOF128"); + rejected::<_, CSHAKE128>(squeezer.suspend(), "an unbound squeezer in cSHAKE128"); + + let mut k = KMAC128::new(&key()).unwrap(); + k.do_update(PART1); + rejected::<_, TupleHash128>(k.clone().suspend(), "a KMAC128 state in TupleHash128"); + rejected::<_, KMAC256>(k.suspend(), "a KMAC128 state in KMAC256"); + let mut t = TupleHash128::new(b"", 32); + t.do_update(PART1); + rejected::<_, KMAC128>(t.suspend(), "a TupleHash128 state in KMAC128"); + + let mut p = ParallelHash128::new(8, b"", 32); + p.do_update(PART1); + rejected::<_, ParallelHash256>(p.suspend(), "a ParallelHash128 state in ParallelHash256"); + let mut p = ParallelHashXOF128::new(8, b""); + p.do_update(PART1); + rejected::<_, ParallelHashXOF256>(p.suspend(), "a ParallelHashXOF128 state in 256"); +} + +#[test] +fn corrupt_fields_are_rejected() { + fn rejected>(state: [u8; N], what: &str) { + assert!( + matches!(T::from_suspended(state), Err(SuspendableError::InvalidData)), + "{what} must be rejected" + ); + } + + let mut c = CSHAKE128::new(b"", b"S"); + c.do_update(PART1); + let good = c.suspend(); + let mut bad = good; + bad[CUSTOMIZED] = 2; + rejected::<_, CSHAKE128>(bad, "a customized byte that is neither 0 nor 1"); + let mut bad = good; + bad[SQUEEZING_FLAG] = 1; + rejected::<_, CSHAKE128>(bad, "a squeezing sponge in an absorbing cSHAKE"); + + let mut k = KMAC128::new(&key()).unwrap(); + k.do_update(PART1); + let mut bad = k.suspend(); + bad[CUSTOMIZED] = 0; + rejected::<_, KMAC128>(bad, "a KMAC claiming an empty function name"); + + let mut k = KMACXOF128::new(&key(), b"", false).unwrap(); + k.do_update(PART1); + let mut bad = k.into_squeezer().suspend(); + bad[CUSTOMIZED] = 0; + rejected::<_, CSHAKESqueezer>(bad, "a squeezer claiming no function name"); + + // ParallelHash: outer cSHAKE 3..416, inner SHAKE 416..828, then block_size, block_fill, blocks. + const INNER_SQUEEZING_FLAG: usize = 416 + 1 + 400; + const BLOCK_SIZE: usize = 416 + 412; + const BLOCK_FILL: usize = BLOCK_SIZE + 8; + let mut p = ParallelHash128::new(8, b"", 32); + p.do_update(PART1); + let good = p.suspend(); + assert_eq!(&good[BLOCK_SIZE..BLOCK_SIZE + 8], &8u64.to_le_bytes(), "layout check"); + assert_eq!(&good[BLOCK_FILL..BLOCK_FILL + 8], &5u64.to_le_bytes(), "21 bytes = 2 blocks + 5"); + let mut bad = good; + bad[BLOCK_SIZE..BLOCK_SIZE + 8].copy_from_slice(&0u64.to_le_bytes()); + rejected::<_, ParallelHash128>(bad, "a block size of zero"); + let mut bad = good; + bad[BLOCK_FILL..BLOCK_FILL + 8].copy_from_slice(&8u64.to_le_bytes()); + rejected::<_, ParallelHash128>(bad, "a fill equal to the block size"); + let mut bad = good; + bad[INNER_SQUEEZING_FLAG] = 1; + rejected::<_, ParallelHash128>(bad, "an inner sponge that is squeezing"); + let mut bad = good; + bad[CUSTOMIZED] = 0; + rejected::<_, ParallelHash128>(bad, "a ParallelHash claiming an empty function name"); +} + +/// Every type in the crate writes a different variant tag, so no state can be misread as +/// another's: the six FIPS 202 types and the eight SP 800-185 types at each width. +#[test] +fn state_tags_are_distinct() { + fn tag>(t: T) -> u8 { + t.suspend()[3] + } + let mut tags = vec![ + tag(SHA3_224::new()), + tag(SHA3_256::new()), + tag(SHA3_384::new()), + tag(SHA3_512::new()), + tag(SHAKE128::new()), + tag(SHAKE256::new()), + tag(CSHAKE128::new(b"", b"S")), + tag(KMAC128::new(&key()).unwrap()), + tag(KMACXOF128::new(&key(), b"", false).unwrap()), + tag(TupleHash128::new(b"", 32)), + tag(TupleHashXOF128::new(b"")), + tag(ParallelHash128::new(8, b"", 32)), + tag(ParallelHashXOF128::new(8, b"")), + tag(TupleHashXOF128::new(b"").into_squeezer()), + tag(CSHAKE256::new(b"", b"S")), + tag(KMAC256::new(&key()).unwrap()), + tag(KMACXOF256::new(&key(), b"", false).unwrap()), + tag(TupleHash256::new(b"", 32)), + tag(TupleHashXOF256::new(b"")), + tag(ParallelHash256::new(8, b"", 32)), + tag(ParallelHashXOF256::new(8, b"")), + tag(TupleHashXOF256::new(b"").into_squeezer()), + ]; + let n = tags.len(); + tags.sort_unstable(); + tags.dedup(); + assert_eq!(tags.len(), n, "two types share a state tag"); +} + +#[test] +fn pin_state_lengths() { + assert_eq!(SUSPENDED_CSHAKE_STATE_LEN, 416, "3 (version) + 412 (family) + 1 (customized)"); + assert_eq!(SUSPENDED_LENGTH_BOUND_SQUEEZER_STATE_LEN, 416); + assert_eq!(SUSPENDED_KMACXOF_STATE_LEN, 416); + assert_eq!(SUSPENDED_TUPLEHASHXOF_STATE_LEN, 416); + assert_eq!(SUSPENDED_KMAC_STATE_LEN, 424, "416 + 8 (output length)"); + assert_eq!(SUSPENDED_TUPLEHASH_STATE_LEN, 424); + assert_eq!(SUSPENDED_PARALLELHASHXOF_STATE_LEN, 852, "416 + 412 (inner) + 3 * 8"); + assert_eq!(SUSPENDED_PARALLELHASH_STATE_LEN, 860, "852 + 8 (output length)"); +} diff --git a/crypto/sha3/tests/tuplehash_bc-test-data.rs b/crypto/sha3/tests/tuplehash_bc-test-data.rs new file mode 100644 index 00000000..ff7218a6 --- /dev/null +++ b/crypto/sha3/tests/tuplehash_bc-test-data.rs @@ -0,0 +1,401 @@ +//! NIST SP 800-185 sample values for TupleHash128/256 and TupleHashXOF128/256. +//! +//! Vectors are read from the bc-test-data repo (https://github.com/bcgit/bc-test-data), which must be +//! cloned alongside this repo at "../bc-test-data" (same convention as the sha2/sha3 crates), under +//! `crypto/sp800-185/`. If it is not present the tests print a warning and pass vacuously. + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Hash, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::test_data_loaders::bc_test_data; +use bouncycastle_hex as hex; +use bouncycastle_sha3::tuplehash::{TupleHash128, TupleHash256, TupleHashXOF128, TupleHashXOF256}; + +const TEST_DATA_DIR: &str = "crypto/sp800-185"; + +/// One `COUNT` block of a `.rsp` file. +struct Vector { + strength: usize, + s: String, + output_len: usize, + tuple: Vec>, + output: Vec, +} + +/// Parses an SP 800-185 TupleHash `.rsp` file into its `COUNT` blocks. +fn parse_rsp_file(content: &str) -> Vec { + 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); + out +} + +fn read_vectors(filename: &str) -> Option> { + Some(parse_rsp_file(&bc_test_data(TEST_DATA_DIR, filename)?)) +} + +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_output_final(n), + 256 => TupleHashXOF256::new(s).output_for(&t).do_output_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_output_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_output_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_output_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_output_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"); + } +} + +/// 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}"), + } + } +} diff --git a/crypto/sha3/tests/tuplehash_tests.rs b/crypto/sha3/tests/tuplehash_tests.rs new file mode 100644 index 00000000..b497e8f3 --- /dev/null +++ b/crypto/sha3/tests/tuplehash_tests.rs @@ -0,0 +1,116 @@ +//! TupleHash and TupleHashXOF behaviour tests. The SP 800-185 sample values are in +//! `tuplehash_bc-test-data.rs`. + +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::{Algorithm, Hash, XOF, XOFSqueezer}; +use bouncycastle_core_test_framework::hash::TestFrameworkHash; +use bouncycastle_sha3::tuplehash::{TupleHash128, TupleHash256, TupleHashXOF128, TupleHashXOF256}; + +/// 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 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/crypto/sm3/Cargo.toml b/crypto/sm3/Cargo.toml new file mode 100644 index 00000000..924fdf7a --- /dev/null +++ b/crypto/sm3/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bouncycastle-sm3" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-hmac.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +criterion.workspace = true +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +bouncycastle-rng.workspace = true + +[[bench]] +name = "sm3_benches" +harness = false + +[[bench]] +name = "hmac_sm3_benches" +harness = false diff --git a/crypto/sm3/benches/hmac_sm3_benches.rs b/crypto/sm3/benches/hmac_sm3_benches.rs new file mode 100644 index 00000000..97cabfbb --- /dev/null +++ b/crypto/sm3/benches/hmac_sm3_benches.rs @@ -0,0 +1,32 @@ +use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +use bouncycastle_core::traits::{MAC, RNG}; +use bouncycastle_rng as rng; +use bouncycastle_sm3::hmac::HMAC_SM3; +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +fn bench_hmac_sm3(c: &mut Criterion) { + let mut data_block = [0_u8; 1024]; + rng::DefaultRNG::default().next_bytes_out(&mut data_block).unwrap(); + + let mut big_data: Vec = vec![]; + for _ in 0..16 { + big_data.extend_from_slice(&data_block); + } + + let hmac_key = KeyMaterial256::from_bytes_as_type(&data_block[..32], KeyType::MACKey).unwrap(); + let mut out = [0u8; 32]; + + let mut group = c.benchmark_group("hmac::HMAC_SM3::mac_out() -- 16x1024 one-shot"); + group.throughput(Throughput::Bytes(big_data.len() as u64)); + group.bench_function(format!("{} bytes -- ::hashes()", big_data.len() as u64), |b| { + b.iter(|| { + HMAC_SM3::new(&hmac_key).unwrap().mac_out(black_box(&big_data), &mut out).unwrap(); + black_box(&out); + }) + }); + group.finish(); +} + +criterion_group!(benches, bench_hmac_sm3); +criterion_main!(benches); diff --git a/crypto/sm3/benches/sm3_benches.rs b/crypto/sm3/benches/sm3_benches.rs new file mode 100644 index 00000000..25f407a1 --- /dev/null +++ b/crypto/sm3/benches/sm3_benches.rs @@ -0,0 +1,30 @@ +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use std::hint::black_box; + +use bouncycastle_core::traits::{Hash, RNG}; +use bouncycastle_rng as rng; +use bouncycastle_sm3::SM3; + +fn bench_sm3(c: &mut Criterion) { + let mut data = [0_u8; 1024]; + rng::DefaultRNG::default().next_bytes_out(&mut data).unwrap(); + + let mut digest = vec![0; SM3::new().output_len()]; + + let mut group = c.benchmark_group("sm3"); + group.throughput(Throughput::Bytes(16 * 1024)); + group.bench_function("16KiB", |b| { + b.iter(|| { + let mut md = SM3::new(); + for _ in 0..16 { + md.do_update(black_box(&data)); + } + _ = md.do_final_out(&mut digest); + black_box(&digest); + }) + }); + group.finish(); +} + +criterion_group!(benches, bench_sm3); +criterion_main!(benches); diff --git a/crypto/sm3/src/hmac.rs b/crypto/sm3/src/hmac.rs new file mode 100644 index 00000000..29fcabab --- /dev/null +++ b/crypto/sm3/src/hmac.rs @@ -0,0 +1,81 @@ +//! HMAC over SM3, as specified in RFC 2104, taking into account NIST Implementation Guidance in +//! FIPS 140-2 IG A.8 and NIST SP 800-107-r1. +//! +//! Uses [`bouncycastle_hmac`] to provide the HMAC-SM3 instantiation: [`HMAC_SM3`]. +//! +//! HMAC itself is implemented generically in [`bouncycastle_hmac`]; this module supplies the +//! SM3-specific parameters via [`HMACParams`] and publishes the resulting type alias, so that HMAC +//! over SM3 is found in this crate, and [`bouncycastle_hmac`] serves as a utility crate rather than +//! as part of the library's public API. See [`bouncycastle_hmac`] for the full description of the +//! three-phase [`MAC`] lifecycle, key typing and suspend/resume. +//! +//! The key buffer length is the underlying hash's block length: per RFC 2104, a key no longer than +//! the block is used verbatim, and only longer keys are pre-hashed down to the output length, so the +//! buffer must be able to hold a full block. It is taken from [`HashAlgParams::BLOCK_LEN`] -- the +//! 512-bit block GB/T 32905-2016 s. 5.2 pads to -- rather than restated as a literal so the two +//! cannot drift apart. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; +//! use bouncycastle_core::traits::MAC; +//! use bouncycastle_sm3::hmac::HMAC_SM3; +//! +//! let key = KeyMaterial256::from_bytes_as_type(&[0x0b; 32], KeyType::MACKey).unwrap(); +//! let tag = HMAC_SM3::new(&key).unwrap().mac(b"Hi There"); +//! assert_eq!(tag.len(), 32); +//! ``` +//! +//! # 🚨 Security Considerations 🚨 +//! +//! * Verify with [`MAC::verify`] or [`MAC::do_verify_final`] rather than computing the MAC yourself +//! and comparing: those use a constant-time comparison, while `==` on the byte slices leaks how +//! many leading bytes matched. +//! * Truncating the MAC output below [`MIN_FIPS_DIGEST_LEN`] (4 bytes) is rejected, per FIPS 140-2 +//! IG A.8 / NIST SP 800-107-r1 Section 5.3.3. That is a floor, not a recommendation -- RFC 2104 +//! Section 5 recommends that the output length "be not less than half the length of the hash +//! output ... and not less than 80 bits". +//! * Resuming a suspended HMAC with the wrong key cannot be detected and silently produces a wrong +//! MAC. +//! * SM3 is a Merkle-Damgard construction and so is subject to length extension; `SM3(k || m)` is +//! not a secure MAC and HMAC-SM3 is the right construction for keyed hashing over SM3. + +use crate::{SM3, SUSPENDED_SM3_STATE_LEN}; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::HashAlgParams; +use bouncycastle_hmac::{HMAC, HMACParams}; + +/*** Imports needed for docs ***/ +#[allow(unused_imports)] +use bouncycastle_core::key_material::KeyType; +#[allow(unused_imports)] +use bouncycastle_core::traits::MAC; +#[allow(unused_imports)] +use bouncycastle_hmac::MIN_FIPS_DIGEST_LEN; +/*** end of doc-only imports ***/ + +/*** String constants ***/ +/// Algorithm name string for HMAC-SM3, as used by the factories and CLI. +pub const HMAC_SM3_NAME: &str = "HMAC-SM3"; + +/*** Type aliases ***/ +/// Public type for HMAC using SM3. +#[allow(non_camel_case_types)] +pub type HMAC_SM3 = HMAC::BLOCK_LEN }>; +impl HMACParams for SM3 { + type MACKey = KeyMaterial<{ ::OUTPUT_LEN }>; + const HMAC_ALG_NAME: &'static str = HMAC_SM3_NAME; + const HMAC_MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + /// Assigned by the Chinese OSCCA (GM/T 0006): hmac-sm3 { sm3 2 } = 1.2.156.10197.1.401.2 + const HMAC_OID: &'static [u32] = &[1, 2, 156, 10197, 1, 401, 2]; + const HMAC_OID_DER: &'static [u8] = + &[0x06, 0x09, 0x2A, 0x81, 0x1C, 0xCF, 0x55, 0x01, 0x83, 0x11, 0x02]; +} + +/*** Serialized-state length constants ***/ +// HMAC's suspended state is exactly the inner hasher's state -- the key is deliberately excluded and +// must be re-supplied on resume -- so this is SM3's own state length. +/// Length in bytes of the serialized state of [`HMAC_SM3`]. +pub const SUSPENDED_HMAC_SM3_STATE_LEN: usize = SUSPENDED_SM3_STATE_LEN; diff --git a/crypto/sm3/src/lib.rs b/crypto/sm3/src/lib.rs new file mode 100644 index 00000000..79a7d948 --- /dev/null +++ b/crypto/sm3/src/lib.rs @@ -0,0 +1,140 @@ +//! Implements the SM3 cryptographic hash function as per GB/T 32905-2016 (also ISO/IEC 10118-3:2018 +//! and IETF draft-shen-sm3-hash-01). +//! +//! SM3 is a 256-bit Merkle–Damgård hash with a 512-bit block, structurally similar to SHA-256 but +//! with different message expansion, round compression functions and constants. +//! +//! # Examples +//! ## Hash +//! Hash functionality is accessed via the [`Hash`] trait, which is implemented by [`SM3`]. +//! +//! The simplest usage is via the one-shot functions. +//! ``` +//! use bouncycastle_core::traits::Hash; +//! use bouncycastle_sm3::SM3; +//! +//! let data: &[u8] = b"abc"; +//! let output: Vec = SM3::new().hash(data); +//! ``` +//! +//! It also has a streaming API that can accept input in chunks of any size. +//! +//! ``` +//! use bouncycastle_core::traits::Hash; +//! use bouncycastle_sm3::SM3; +//! +//! let data: &[u8] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F +//! \x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; +//! let mut sm3 = SM3::new(); +//! +//! for chunk in data.chunks(16) { +//! sm3.do_update(chunk); +//! } +//! +//! let output: Vec = sm3.do_final(); +//! ``` +//! +//! It is also possible to provide input where the final byte contains fewer than 8 bits of data +//! (a bit-oriented message, GB/T 32905-2016 s. 5.2). The partial byte is taken as it arrives in the +//! final octet of an ASN.1 BIT STRING: the message bits are its most significant bits, leading bit +//! first, and the low "unused" bits are ignored. The following hashes 16 bytes plus the 3 bits `101`: +//! ``` +//! use bouncycastle_core::traits::Hash; +//! use bouncycastle_sm3::SM3; +//! +//! let data: &[u8] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\xA0"; +//! let mut sm3 = SM3::new(); +//! sm3.do_update(&data[..16]); +//! let output: Vec = sm3.do_final_partial_bits(data[16], 3).expect("num_partial_bits is in 0..=7"); +//! ``` +//! +//! ## HMAC +//! See [hmac]. +//! +//! # Memory Usage +//! +//! No heap memory is used by the algorithm itself; the `Vec`-returning convenience methods +//! allocate only the output buffer, and the `*_out` variants allocate nothing. +//! +//! | Object | Size (bytes) | +//! |----------------------------|--------------| +//! | `SM3` | 112 | +//! | Suspended state | 108 | +//! +//! The object holds the 8-word chaining value plus one 64-byte block of buffered input. The +//! compression function additionally uses a 68-word message schedule (272 bytes) on the stack for +//! the duration of a call. +//! +//! # 🚨 Security Considerations 🚨 +//! +//! * SM3 offers 128 bits of collision resistance and 256 bits of preimage resistance. +//! * SM3 is a Merkle–Damgård construction and is therefore subject to length-extension: +//! `H(k || m)` is not a secure MAC. Use HMAC ([`sm3::hmac`](crate::hmac) for keyed hashing. +//! * The chaining value and input buffer are held in [`bouncycastle_utils::secret::Secret`] and +//! zeroized on drop. Transient copies (working variables and message schedule) in registers/stack +//! locals during compression are not zeroized. +//! * The implementation contains no data-dependent branches or table lookups. +//! * Messages up to 2^64 bytes are supported (the specification allows 2^64 bits). +//! +//! # Suspending and resuming execution +//! +//! When hashing a large message, it can be advantageous to be able to suspend the operation +//! to a cache and resume it later; for example if waiting for the message to stream over a slow network +//! connection. For this reason, [`SM3`] impls [`Suspendable`]. +//! +//! ```rust +//! use bouncycastle_sm3::SM3; +//! use bouncycastle_core::traits::{Hash, Suspendable}; +//! +//! let msg_part1 = b"The quick brown fox"; +//! let msg_part2 = b" jumped over the lazy dog"; +//! +//! let mut sm3 = SM3::new(); +//! sm3.do_update(msg_part1); +//! +//! // suspend the in-progress hash while "waiting" for the second part of the message. +//! let serialized_state = sm3.suspend(); +//! +//! // ... later, possibly on another host: resume from the serialized state. +//! let mut sm3_resumed = SM3::from_suspended(serialized_state).unwrap(); +//! sm3_resumed.do_update(msg_part2); +//! let h: Vec = sm3_resumed.do_final(); +//! ``` + +// todo #![no_std] +// waiting for the no_std refactor that removes the `-> Vec` from core::traits::Hash + +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod sm3; + +pub mod hmac; + +pub use self::sm3::{SM3, SUSPENDED_SM3_STATE_LEN}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Algorithm, AlgorithmOID, HashAlgParams}; + +/*** Imports needed for docs ***/ +#[allow(unused_imports)] +use bouncycastle_core::traits::{Hash, Suspendable}; + +/// Algorithm name string for SM3, as used by the factories and CLI. +pub const SM3_NAME: &str = "SM3"; + +impl Algorithm for SM3 { + const ALG_NAME: &'static str = SM3_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +/// GB/T 32905-2016: 256-bit digest, 512-bit block. +impl HashAlgParams for SM3 { + const OUTPUT_LEN: usize = 32; + const BLOCK_LEN: usize = 64; +} + +/// Assigned by the Chinese OSCCA: sm3 { 1 2 156 10197 1 401 } +impl AlgorithmOID for SM3 { + const OID: &'static [u32] = &[1, 2, 156, 10197, 1, 401]; + const OID_DER: &'static [u8] = &[0x06, 0x08, 0x2A, 0x81, 0x1C, 0xCF, 0x55, 0x01, 0x83, 0x11]; +} diff --git a/crypto/sm3/src/sm3.rs b/crypto/sm3/src/sm3.rs new file mode 100644 index 00000000..7290f968 --- /dev/null +++ b/crypto/sm3/src/sm3.rs @@ -0,0 +1,379 @@ +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::security_strength::SecurityStrength; +use bouncycastle_core::traits::{Hash, Suspendable}; +use bouncycastle_utils::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_utils::{min, secret::Secret}; +use core::slice; + +/// GB/T 32905-2016 s. 4.1: initial value IV. +const SM3_IV: [u32; 8] = [ + 0x7380166F, 0x4914B2B9, 0x172442D7, 0xDA8A0600, 0xA96F30BC, 0x163138AA, 0xE38DEE4D, 0xB0FB0E4E, +]; + +/// GB/T 32905-2016 s. 5.1: SM3 takes "a message m of length l (where l < 2^64) in bits", so the +/// longest whole-byte message it covers is 2^61 - 1 bytes. +const MAX_MESSAGE_BYTES: u64 = (1 << 61) - 1; + +/// GB/T 32905-2016 s. 4.2: constants T_j = 79CC4519 for 0 <= j <= 15, 7A879D8A for 16 <= j <= 63. +/// The round function uses (T_j <<< (j mod 32)), which is precomputed here at compile time. +/// Mutants note: `u32::rotate_left` reduces its argument modulo 32 itself, so replacing `j % 32` +/// with `j + 32` is an equivalent mutant; and `+=` -> `*=` on the loop counter is an infinite loop +/// in `const` evaluation, reported as a build timeout. +const SM3_T: [u32; 64] = { + let mut t = [0u32; 64]; + let mut j = 0; + while j < 64 { + let base: u32 = if j < 16 { 0x79CC4519 } else { 0x7A879D8A }; + t[j] = base.rotate_left((j % 32) as u32); + j += 1; + } + t +}; + +/// GB/T 32905-2016 s. 4.3: boolean functions FF_j and GG_j for 0 <= j <= 15. +#[inline] +fn ff0(x: u32, y: u32, z: u32) -> u32 { + x ^ y ^ z +} + +/// GB/T 32905-2016 s. 4.3: FF_j for 16 <= j <= 63 (majority). +/// Mutants note: majority can be written with `|` or `^` between the three terms (FIPS 180-4 writes +/// Maj with XOR), so a surviving `|`/`^` swap in this function is an equivalent mutant. +#[inline] +fn ff1(x: u32, y: u32, z: u32) -> u32 { + (x & y) | (x & z) | (y & z) +} + +/// GB/T 32905-2016 s. 4.3: GG_j for 16 <= j <= 63 (choice). +/// Mutants note: the two masks are disjoint, so `|` and `^` give identical results here; a +/// surviving `|`/`^` swap in this function is an equivalent mutant. +#[inline] +fn gg1(x: u32, y: u32, z: u32) -> u32 { + (x & y) | (!x & z) +} + +/// GB/T 32905-2016 s. 4.4: permutation P0(X) = X ^ (X <<< 9) ^ (X <<< 17). +#[inline] +fn p0(x: u32) -> u32 { + x ^ x.rotate_left(9) ^ x.rotate_left(17) +} + +/// GB/T 32905-2016 s. 4.4: permutation P1(X) = X ^ (X <<< 15) ^ (X <<< 23). +#[inline] +fn p1(x: u32) -> u32 { + x ^ x.rotate_left(15) ^ x.rotate_left(23) +} + +/// The SM3 cryptographic hash function (GB/T 32905-2016). +/// +/// See the [crate-level documentation](crate) for usage. +#[derive(Clone)] +pub struct SM3 { + /// Chaining value V^(i), 8 big-endian words. + v: Secret<[u32; 8]>, + /// Total number of message bytes absorbed so far. Supports messages up to 2^64 bytes. + byte_count: u64, + /// Buffered input that has not yet formed a whole block. + x_buf: Secret<[u8; 64]>, + /// Number of valid bytes in `x_buf` (always < 64). + x_buf_off: usize, +} + +impl SM3 { + /// Creates a new SM3 instance, ready for use. + pub fn new() -> Self { + let mut v = Secret::<[u32; 8]>::new(); + v.copy_from_slice(&SM3_IV); + Self { v, byte_count: 0, x_buf: Secret::new(), x_buf_off: 0 } + } + + /// GB/T 32905-2016 s. 5.3: compression function V^(i+1) = CF(V^(i), B^(i)) for each block. + /// + /// Takes the chaining value rather than `&mut self` so callers can pass `self.x_buf` as the + /// block without a conflicting borrow. + fn compress(v: &mut [u32; 8], blocks: &[[u8; 64]]) { + // s. 5.3.2 message expansion: W_0..W_67. W'_j = W_j ^ W_{j+4} is computed on the fly. + let mut w = [0u32; 68]; + + for block in blocks { + let (chunks, _remainder) = block.as_chunks::<4>(); + for (wj, bytes) in w[..16].iter_mut().zip(chunks) { + *wj = u32::from_be_bytes(*bytes); + } + for j in 16..68 { + // W_j = P1(W_{j-16} ^ W_{j-9} ^ (W_{j-3} <<< 15)) ^ (W_{j-13} <<< 7) ^ W_{j-6} + w[j] = p1(w[j - 16] ^ w[j - 9] ^ w[j - 3].rotate_left(15)) + ^ w[j - 13].rotate_left(7) + ^ w[j - 6]; + } + + // s. 5.3.3 compression: ABCDEFGH <- V^(i) + let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = *v; + + // One round of s. 5.3.3. `$ff` / `$gg` select the boolean functions for the round range. + macro_rules! sm3_round { + ($j:expr, $ff:ident, $gg:ident) => { + // SS1 = ((A <<< 12) + E + (T_j <<< (j mod 32))) <<< 7 + let a12 = a.rotate_left(12); + let ss1 = a12.wrapping_add(e).wrapping_add(SM3_T[$j]).rotate_left(7); + // SS2 = SS1 ^ (A <<< 12) + let ss2 = ss1 ^ a12; + // TT1 = FF_j(A,B,C) + D + SS2 + W'_j where W'_j = W_j ^ W_{j+4} + let tt1 = $ff(a, b, c) + .wrapping_add(d) + .wrapping_add(ss2) + .wrapping_add(w[$j] ^ w[$j + 4]); + // TT2 = GG_j(E,F,G) + H + SS1 + W_j + let tt2 = $gg(e, f, g).wrapping_add(h).wrapping_add(ss1).wrapping_add(w[$j]); + // D = C; C = B <<< 9; B = A; A = TT1; H = G; G = F <<< 19; F = E; E = P0(TT2) + d = c; + c = b.rotate_left(9); + b = a; + a = tt1; + h = g; + g = f.rotate_left(19); + f = e; + e = p0(tt2); + }; + } + + // Rounds 0..=15 use FF_0 = GG_0 = XOR (ff0 serves both). + for j in 0..16 { + sm3_round!(j, ff0, ff0); + } + // Rounds 16..=63 use the majority / choice functions. + for j in 16..64 { + sm3_round!(j, ff1, gg1); + } + + // V^(i+1) = ABCDEFGH ^ V^(i) + v[0] ^= a; + v[1] ^= b; + v[2] ^= c; + v[3] ^= d; + v[4] ^= e; + v[5] ^= f; + v[6] ^= g; + v[7] ^= h; + } + } + + /// Pads and compresses the final block(s) as per GB/T 32905-2016 s. 5.2, then writes the digest. + /// + /// The `num_partial_bits` (0..=7, validated by the caller) trailing message bits are the most + /// significant bits of `partial_byte`, leading bit first: the ASN.1 BIT STRING order of + /// X.690 s. 8.6.2.1, which is also how GB/T 32905-2016 (like FIPS 180-4) numbers the bits of a + /// message byte. So they are used in place, the low `8 - num_partial_bits` bits are ignored, and + /// the mandatory "1" padding bit follows the message bits immediately in the same byte. + /// + /// Returns the number of bytes written (`min(output.len(), 32)`); a shorter output buffer + /// truncates the digest, a longer one is zero-filled past the digest. + fn do_final_internal( + mut self, + partial_byte: u8, + num_partial_bits: usize, + output: &mut [u8], + ) -> usize { + debug_assert!(num_partial_bits <= 7); + output.fill(0); + + let n = *min(&output.len(), &32); + + // s. 5.2: final message byte = [the top num_partial_bits bits of partial_byte] [1] [0...]. With + // no partial bits this is 0x80. The mask is built in u16 so that the 8-bit shift for + // num_partial_bits == 0 cannot overflow (0xFF00 >> 0 truncates to 0x00). + let mask = (0xFF00u16 >> num_partial_bits) as u8; + // Mutants note: the masked message bits and the padding bit occupy disjoint bit positions, so + // `|` and `^` give identical results here; a surviving `|`/`^` swap is an equivalent mutant. + let pad_byte = (partial_byte & mask) | (0x80u8 >> num_partial_bits); + + self.x_buf[self.x_buf_off] = pad_byte; + self.x_buf_off += 1; + + // ... then k zero bits so that l + 1 + k = 448 mod 512. If the 64-bit length field no longer + // fits in this block, zero-fill and compress, then start a fresh block. + if self.x_buf_off > 56 { + self.x_buf[self.x_buf_off..].fill(0x00); + Self::compress(&mut self.v, slice::from_ref(&self.x_buf)); + self.x_buf_off = 0; + } + self.x_buf[self.x_buf_off..56].fill(0x00); + + // ... then the 64-bit big-endian message length l in bits. byte_count is a byte counter, so + // l = (byte_count << 3) | num_partial_bits (the low three bits of byte_count << 3 are zero). + // Mutants note: the low three bits of byte_count << 3 are zero, so `|` and `^` give identical + // results here; a surviving `|`/`^` swap is an equivalent mutant. + let bit_len: u64 = (self.byte_count << 3) | (num_partial_bits as u64); + self.x_buf[56..64].copy_from_slice(&bit_len.to_be_bytes()); + Self::compress(&mut self.v, slice::from_ref(&self.x_buf)); + + // s. 5.4: the digest is V^(n) as 8 big-endian words. + let v = &self.v; + for i in 0..(n / 4) { + output[i * 4..i * 4 + 4].copy_from_slice(&v[i].to_be_bytes()); + } + if !n.is_multiple_of(4) { + output[((n / 4) * 4)..((n / 4) * 4) + (n % 4)] + .copy_from_slice(&v[n / 4].to_be_bytes()[0..(n % 4)]); + } + + n + } +} + +impl Default for SM3 { + fn default() -> Self { + Self::new() + } +} + +impl Hash for SM3 { + /// GB/T 32905-2016 s. 5.2: 512-bit blocks. + fn block_bitlen(&self) -> usize { + 512 + } + + fn output_len(&self) -> usize { + 32 + } + + fn hash(self, data: &[u8]) -> Vec { + let mut output = vec![0u8; 32]; + self.hash_out(data, &mut output); + output + } + + 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, block: &[u8]) { + let len = block.len(); + + // GB/T 32905-2016 s. 5.2: do_final_internal encodes l in a 64-bit field as + // `byte_count << 3`, and a left shift discards rather than panics, so past + // MAX_MESSAGE_BYTES the digest would silently be that of a message 2^64 bits shorter. + debug_assert!( + self.byte_count.checked_add(len as u64).is_some_and(|total| total <= MAX_MESSAGE_BYTES), + "message exceeds the SM3 limit of {MAX_MESSAGE_BYTES} bytes" + ); + self.byte_count += len as u64; + + let available = 64 - self.x_buf_off; + if len < available { + self.x_buf[self.x_buf_off..self.x_buf_off + len].copy_from_slice(block); + self.x_buf_off += len; + return; + } + + let mut block = block; + if self.x_buf_off != 0 { + self.x_buf[self.x_buf_off..].copy_from_slice(&block[..available]); + block = &block[available..]; + Self::compress(&mut self.v, slice::from_ref(&self.x_buf)); + } + + let (chunks, remainder) = block.as_chunks::<64>(); + Self::compress(&mut self.v, chunks); + + let remaining = remainder.len(); + self.x_buf[..remaining].copy_from_slice(remainder); + self.x_buf_off = remaining; + } + + fn do_final(self) -> Vec { + let mut output = vec![0u8; 32]; + self.do_final_out(&mut output); + output + } + + fn do_final_out(self, output: &mut [u8]) -> usize { + // A whole-byte message is the zero-partial-bits case of the general padding. + self.do_final_internal(0, 0, output) + } + + fn do_final_partial_bits( + self, + partial_byte: u8, + num_partial_bits: usize, + ) -> Result, HashError> { + let mut output = vec![0u8; 32]; + self.do_final_partial_bits_out(partial_byte, num_partial_bits, &mut output)?; + Ok(output) + } + + /// GB/T 32905-2016 s. 5.2: bit-oriented messages. The `num_partial_bits` most significant bits of + /// `partial_byte` (ASN.1 BIT STRING order, leading bit first) are appended to the message before + /// padding; the low bits are ignored. `num_partial_bits == 0` behaves exactly like + /// [`Hash::do_final_out`]. + fn do_final_partial_bits_out( + self, + partial_byte: u8, + num_partial_bits: usize, + output: &mut [u8], + ) -> Result { + if num_partial_bits > 7 { + return Err(HashError::InvalidLength("num_partial_bits must be in the range [0,7]")); + } + Ok(self.do_final_internal(partial_byte, num_partial_bits, output)) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} + +/// Length in bytes of the serialized state of SM3. +/// +/// Layout (after the 3-byte library version header; all integers little-endian): +/// [0 .. 32) v [u32; 8] +/// [32 .. 40) byte_count u64 +/// [40 .. 104) x_buf [u8; 64] +/// [104 .. 105) x_buf_off u8 (always < 64) +pub const SUSPENDED_SM3_STATE_LEN: usize = 3 + 105; + +impl Suspendable for SM3 { + fn suspend(self) -> [u8; SUSPENDED_SM3_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_SM3_STATE_LEN]; + + // infallible: add_lib_ver returns a slice of exactly SUSPENDED_SM3_STATE_LEN - 3 = 105 bytes. + let out: &mut [u8; 105] = add_lib_ver(&mut out_to_return).try_into().unwrap(); + + for i in 0..8 { + out[i * 4..(i * 4) + 4].copy_from_slice(&self.v[i].to_le_bytes()); + } + out[32..40].copy_from_slice(&self.byte_count.to_le_bytes()); + out[40..104].copy_from_slice(&*self.x_buf); + debug_assert!(self.x_buf_off < 64); + out[104] = self.x_buf_off as u8; + + out_to_return + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_SM3_STATE_LEN], + ) -> Result { + // check the version tag. At the moment, we have no not_before version to specify. + // infallible: check_lib_ver returns a slice of exactly SUSPENDED_SM3_STATE_LEN - 3 = 105 bytes. + let input: &[u8; 105] = check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + let mut v = Secret::<[u32; 8]>::new(); + for i in 0..8 { + // infallible: a 4-byte slice into a [u8; 4] + v[i] = u32::from_le_bytes(input[i * 4..(i * 4) + 4].try_into().unwrap()); + } + // infallible: an 8-byte slice into a [u8; 8] + let byte_count = u64::from_le_bytes(input[32..40].try_into().unwrap()); + + let mut x_buf = Secret::<[u8; 64]>::new(); + x_buf.copy_from_slice(&input[40..104]); + + let x_buf_off = input[104] as usize; + if x_buf_off >= 64 { + return Err(SuspendableError::InvalidData); + } + + Ok(SM3 { v, byte_count, x_buf, x_buf_off }) + } +} diff --git a/crypto/sm3/tests/sm3_tests.rs b/crypto/sm3/tests/sm3_tests.rs new file mode 100644 index 00000000..7d5fb697 --- /dev/null +++ b/crypto/sm3/tests/sm3_tests.rs @@ -0,0 +1,239 @@ +#[cfg(test)] +mod sm3_tests { + use bouncycastle_core::errors::{HashError, SuspendableError}; + use bouncycastle_core::security_strength::SecurityStrength; + use bouncycastle_core::traits::{Algorithm, AlgorithmOID, Hash, HashAlgParams}; + use bouncycastle_core_test_framework::DUMMY_SEED; + use bouncycastle_core_test_framework::hash::TestFrameworkHash; + use bouncycastle_hex as hex; + use bouncycastle_sm3::*; + + fn h(s: &str) -> Vec { + hex::decode(s).unwrap() + } + + /// Runs the shared Hash-trait conformance suite against known answers. + /// The first two are the standard vectors from GB/T 32905-2016 Appendix A; the rest are the + /// bc-java SM3DigestTest vectors and digests of DUMMY_SEED generated with openssl and confirmed + /// with bc-java's `SM3Digest`. + #[test] + fn core_test_framework_hash() { + let test_framework = TestFrameworkHash::new(); + + test_framework.test_hash::( + b"abc", + &h("66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0"), + ); + test_framework.test_hash::( + b"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", + &h("debe9ff92275b8a138604889c18e5a4d6fdb70e5387e5765293dcba39c0c5732"), + ); + test_framework.test_hash::( + b"", + &h("1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b"), + ); + test_framework.test_hash::( + b"a", + &h("623476ac18f65a2909e43c7fec61b49c7e764a91a18ccb82f1917a29c86c5e88"), + ); + test_framework.test_hash::( + b"abcdefghijklmnopqrstuvwxyz", + &h("b80fe97a4da24afc277564f66a359ef440462ad28dcc6d63adb24d5c20a61595"), + ); + test_framework.test_hash::( + &DUMMY_SEED[..512], + &h("b21f830dca06be8b678cf987f26b9a436e1b427963b4450332f01270bd2df75c"), + ); + test_framework.test_hash::( + DUMMY_SEED, + &h("1f00bad6a72e851e0f6e94fd317f97b74d5fbc4c090aefb91e7554e3f9c8c7fb"), + ); + } + + /// bc-java SM3DigestTest "Additional vectors for GMSSL": the SM2 Z_A value from GM/T 0003.5 (also + /// checked against openssl `dgst -sm3`). + #[test] + fn bc_java_vectors() { + let msg = h(concat!( + "0090", + "414C494345313233405941484F4F2E434F4D", + "787968B4FA32C3FD2417842E73BBFEFF2F3C848B6831D7E0EC65228B3937E498", + "63E4C6D3B23B0C849CF84241484BFE48F61D59A5B16BA06E6E12D1DA27C5249A", + "421DEBD61B62EAB6746434EBC3CC315E32220B3BADD50BDC4C4E6C147FEDD43D", + "0680512BCBB42C07D47349D2153B70C4E5D7FDFCBFA36EA1A85841B9E46E09A2", + "0AE4C7798AA0F119471BEE11825BE46202BB79E2A5844495E97C04FF4DF2548A", + "7C0240F88F1CD4E16352A73C17B7F16F07353E53A176D684A9FE0C6BB798E857", + )); + assert_eq!( + SM3::new().hash(&msg), + h("f4a38489e32b45b6f876e3ac2168ca392362dc8f23459c1d1146fc3dbfb7bc9a") + ); + } + + /// Padding boundaries (GB/T 32905-2016 s. 5.2): message lengths around the 56- and 64-byte + /// points where the length field does / does not fit in the current block. Expected values + /// generated with openssl `dgst -sm3` over prefixes of DUMMY_SEED and confirmed with bc-java's + /// `SM3Digest`. + #[test] + fn padding_boundaries() { + for (len, expected) in [ + (55, "a79cf9dcee3404abf7f769698201647fd9d3ff61d629d0f58bb4b5579a427db8"), + (56, "62f7363b15f4de76dd925c493b9d6d00d4ba0ef2a1f334c1d0f13b293aeb40d1"), + (63, "6165e4cbb15cde01c6226e0015a47f710f8f8e1f2c296700033bb34d9212109c"), + (64, "93566f236d157aae078d1ddb5cebdbba1520b5142e22a8915564345ba2ae1d63"), + (65, "c886e6814be748285a10b28ae62ddacd85db830cd2cf3a2bfa2f729c15f63618"), + (119, "8f3ea392a89a7119982d6634660db1a95f35d68267a2235e3255998a857f4fbf"), + (128, "a9e7985473ca09df1510d83b572f72375430756c4a661b00724afeb8b75dd0a5"), + ] { + assert_eq!(SM3::new().hash(&DUMMY_SEED[..len]), h(expected), "len={len}"); + + // and the same via byte-at-a-time streaming, which exercises every x_buf_off value + let mut sm3 = SM3::new(); + for b in &DUMMY_SEED[..len] { + sm3.do_update(core::slice::from_ref(b)); + } + assert_eq!(sm3.do_final(), h(expected), "streaming len={len}"); + } + } + + #[test] + fn test_constants() { + assert_eq!(SM3::OUTPUT_LEN, 32); + assert_eq!(SM3::BLOCK_LEN, 64); + assert_eq!(SM3::new().block_bitlen(), 512); + assert_eq!(SM3::new().output_len(), 32); + } + + #[test] + fn test_algorithm() { + assert_eq!(SM3::ALG_NAME, SM3_NAME); + assert_eq!(SM3_NAME, "SM3"); + assert_eq!(SM3::OID, &[1, 2, 156, 10197, 1, 401]); + assert_eq!(SM3::OID_DER, &[0x06, 0x08, 0x2A, 0x81, 0x1C, 0xCF, 0x55, 0x01, 0x83, 0x11]); + } + + #[test] + fn test_security_strength() { + assert_eq!(SM3::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(SM3::default().max_security_strength(), SecurityStrength::_128bit); + } + + /// GB/T 32905-2016 s. 5.2: bit-oriented messages. Zero partial bits must equal the byte-oriented + /// digest; more than 7 partial bits is rejected; only the top bits of the partial byte matter; + /// and the pad byte spilling into a second block must not break. + #[test] + fn partial_bits() { + let mut a = SM3::new(); + a.do_update(b"abc"); + assert_eq!(a.do_final_partial_bits(0xFF, 0).unwrap(), SM3::new().hash(b"abc")); + + for bad in [8usize, 9, 16, 64, usize::MAX] { + let mut sm3 = SM3::new(); + sm3.do_update(b"abc"); + assert!( + matches!(sm3.do_final_partial_bits(0xFF, bad), Err(HashError::InvalidLength(_))), + "n={bad}" + ); + let mut out = [0u8; 32]; + assert!(matches!( + SM3::new().do_final_partial_bits_out(0xFF, bad, &mut out), + Err(HashError::InvalidLength(_)) + )); + } + + for n in 1..=7usize { + let mask = (0xFF00u16 >> n) as u8; + let x = SM3::new().do_final_partial_bits(0xA5, n).unwrap(); + let y = SM3::new().do_final_partial_bits(0xA5 & mask, n).unwrap(); + let z = SM3::new().do_final_partial_bits(0xA5 ^ 0x80, n).unwrap(); + assert_eq!(x, y, "n={n}"); + assert_ne!(x, z, "n={n}: the leading bit must change the digest"); + assert_ne!(x, SM3::new().hash(&[]), "n={n}"); + assert_ne!(x, SM3::new().hash(&[0xA5 & mask]), "n={n}"); + } + + for len in [55usize, 56, 63, 64, 119, 128] { + let mut sm3 = SM3::new(); + sm3.do_update(&vec![0x5Au8; len]); + let mut out = [0u8; 32]; + assert_eq!(sm3.do_final_partial_bits_out(0xC0, 2, &mut out).unwrap(), 32, "len={len}"); + } + } + + /// Bit-oriented known answers. Neither openssl nor bc-java expose a bit-length SM3 API, so the + /// expected values come from an independent pure-Python implementation of GB/T 32905-2016 with + /// bit-length padding, itself checked against `openssl dgst -sm3` on byte-aligned inputs. + /// `(prefix, partial_byte, bits, digest)`, where the `bits` message bits are the top bits of + /// `partial_byte` (ASN.1 BIT STRING order). + #[test] + fn partial_bits_known_answers() { + let cases: [(&[u8], u8, usize, &str); 6] = [ + (b"", 0x80, 1, "985ffe9568be96328729b1c16631e9328d356432413d7556a646b9eefe479b9e"), + (b"", 0xA8, 5, "469dd7b688a7b98d6362a8e2488a148cb4231bc196b796eee9652cb9044f3dcd"), + (b"abc", 0xfe, 7, "5ad9f5745671e4a49f6704fdadff8cc2ff8a9683d1c7c0810a5dd7db367e9d74"), + ( + &[0x5a; 55], + 0xC0, + 2, + "65985be43230ee70a939d38e34a88198e0d63bb307081459d8d75541d54a382e", + ), + ( + &[0x5a; 111], + 0xA0, + 3, + "8dfb4b90e5f899286782c9b192b67c5ebfbbab5a10d827d2518509307b7877c3", + ), + ( + &DUMMY_SEED[..64], + 0xF0, + 4, + "30e64a364406c1ac354ad17845b4df681de5bad9a1b41e996921a6f5effbf85b", + ), + ]; + for (prefix, partial_byte, bits, expected) in cases { + let mut sm3 = SM3::new(); + sm3.do_update(prefix); + assert_eq!( + sm3.do_final_partial_bits(partial_byte, bits).unwrap(), + h(expected), + "{}/{bits}", + prefix.len() + ); + } + } + + #[test] + fn suspendable_state() { + use bouncycastle_core::traits::Suspendable; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; + + let str = "Colorless green ideas sleep furiously"; + + let mut sm3 = SM3::new(); + sm3.do_update(str.as_bytes()); + + // do the default tests + let test_framework = TestFrameworkSuspendableState::new(); + test_framework.test(&sm3); + + // now let's serialize the in-progress state + let serialized_state = sm3.clone().suspend(); + assert_eq!(serialized_state.len(), SUSPENDED_SM3_STATE_LEN); + + // finish the hash + let output = sm3.do_final(); + + // then load from state and finish the hash and make sure we get the same thing + let sm3_from_state = SM3::from_suspended(serialized_state).unwrap(); + let output2 = sm3_from_state.do_final(); + assert_eq!(output, output2); + + // also, give it a busted x_buf_off, just to satisfy mutants that that's been tested + let mut busted_state = serialized_state; + busted_state[3 + 104] = 65; + match SM3::from_suspended(busted_state) { + Err(SuspendableError::InvalidData) => { /* good */ } + _ => panic!("Expected an error"), + } + } +} diff --git a/crypto/utils/Cargo.toml b/crypto/utils/Cargo.toml index a23ce1a1..13aa117a 100644 --- a/crypto/utils/Cargo.toml +++ b/crypto/utils/Cargo.toml @@ -2,6 +2,7 @@ name = "bouncycastle-utils" version.workspace = true edition.workspace = true +rust-version.workspace = true [dependencies] diff --git a/crypto/utils/src/ct.rs b/crypto/utils/src/ct.rs index 6238bf28..ab8ee9b9 100644 --- a/crypto/utils/src/ct.rs +++ b/crypto/utils/src/ct.rs @@ -31,7 +31,59 @@ pub struct Condition(T) where MaskType: SupportedMaskType; -impl Condition where MaskType: SupportedMaskType {} +// --------------------------------------------------------------------------------------------- +// Optimisation barrier +// +// Every constant-time construction in this file is masked arithmetic on a value the optimiser +// could otherwise prove to be one of a small number of constants (a `Condition` mask is all-ones +// or all-zeros; the accumulator of a comparison loop is zero until the first difference). Given +// that knowledge the compiler is free to lower an expression like `(t & m) | (f & !m)` to a branch +// or a conditional move on the secret, or to leave a comparison loop early. To stop that, the value +// is routed through a volatile store and load. +// +// This is still a best-effort, not a guarantee. +// The language guarantees less than is relied on here. The documentation (as of rust 1.99.0) of +// `core::ptr::read_volatile` / `write_volatile` says the accesses "are guaranteed to not be +// elided or reordered" relative to other externally observable events, and that a volatile read +// "will actually access memory and not e.g. be lowered to reusing data from a previous read". It +// says nothing about what the optimiser may still assume about the value stored. That LLVM +// carries no facts across the store/load pair is observed behaviour, verified by inspecting the +// release-build assembly of these functions on x86_64, i686, thumbv7em, riscv32imac, wasm32, +// msp430 and avr; another backend gets no such promise. `core::hint::black_box`, used +// previously, is weaker still: its documentation calls it "best-effort" and says it "does not +// offer any guarantees for cryptographic or security purposes". +// --------------------------------------------------------------------------------------------- + +/// Returns `value` unchanged, via a volatile store to a stack slot and a volatile load back. The +/// section comment above says what that does and does not guarantee. +#[inline(always)] +fn value_barrier(value: T) -> T { + let mut slot = value; + // SAFETY: + // * `&mut slot` must be a reference to an initialised, aligned `T` local on this stack frame, so + // it is valid for reads and writes for the duration of both calls, which is the only + // precondition of `write_volatile` and `read_volatile`. + // * The reference is exclusive; nothing else can observe `slot` during the two accesses. + // * `T: Copy`, so the bitwise copy `read_volatile` makes has no drop glue to run twice and + // the value written is a valid `T`, so the value read back is initialised. + unsafe { + core::ptr::write_volatile(&mut slot, value); + core::ptr::read_volatile(&slot) + } +} + +impl Condition +where + MaskType: SupportedMaskType, +{ + /// The mask after the optimisation barrier (section comment above), applied at the point of + /// use by every consumer that does masked arithmetic. Not `const`: volatile accesses are not + /// allowed in const context. + #[inline(always)] + fn barrier(self) -> Self { + Self(value_barrier(self.0)) + } +} // Each signed width is written out by hand rather than macro-generated: `cargo mutants` // cannot see into macro bodies, and these mask identities are the ones most worth @@ -118,17 +170,12 @@ impl Condition { } /// TRUE iff `value` occurs in `list`. The list contents and length are public. pub fn is_in_list(value: i64, list: &[i64]) -> Self { - // Research question: is this actually constant-time? - // A clever compiler might turn this into a short-circuiting loop. - // A quick google search shows that rust doesn't have the ability to annotate specific code blocks - // as no-optimize; the only option is to insert direct assembly. - + // Barrier inside the loop, for the reason given at "Byte-slice comparison helpers". let mut c = Self::FALSE; - for i in 0..list.len() { - let diff = value ^ list[i]; - c |= Self::is_zero(diff); + for x in list { + c |= Self::is_equal(value, *x); + c = c.barrier(); } - c } @@ -158,18 +205,21 @@ impl Condition { /// /// Therefore, if the [`Self::TRUE`] constant value of the [`Condition`] implementation is changed to `-1`, /// the test also runs normally. - pub const fn negate(self, value: i64) -> i64 { - (value ^ self.0).wrapping_sub(self.0) + pub fn negate(self, value: i64) -> i64 { + let mask = self.barrier().0; + (value ^ mask).wrapping_sub(mask) } /// Conditional selection: return `true_value` if the condition is true, otherwise /// return `false_value`. - pub const fn select(self, true_value: i64, false_value: i64) -> i64 { - (true_value & self.0) | (false_value & !self.0) + pub fn select(self, true_value: i64, false_value: i64) -> i64 { + let mask = self.barrier().0; + (true_value & mask) | (false_value & !mask) } - /// Conditional swap: returns (lhs, rhs) if the condition is true, otherwise - /// returns (rhs, lhs). - pub const fn swap(self, lhs: i64, rhs: i64) -> (i64, i64) { - (self.select(rhs, lhs), self.select(lhs, rhs)) + /// Conditional swap: returns (rhs, lhs) if the condition is true, otherwise (lhs, rhs). + pub fn swap(self, lhs: i64, rhs: i64) -> (i64, i64) { + // One barrier serves both outputs: `t` is `lhs ^ rhs` under TRUE and zero under FALSE. + let t = (lhs ^ rhs) & self.barrier().0; + (lhs ^ t, rhs ^ t) } /// Convert the mask to a runtime boolean. Only use this at genuine public /// decision points: branching on the result leaks the condition's value. @@ -259,17 +309,12 @@ impl Condition { } /// TRUE iff `value` occurs in `list`. The list contents and length are public. pub fn is_in_list(value: i32, list: &[i32]) -> Self { - // Research question: is this actually constant-time? - // A clever compiler might turn this into a short-circuiting loop. - // A quick google search shows that rust doesn't have the ability to annotate specific code blocks - // as no-optimize; the only option is to insert direct assembly. - + // Barrier inside the loop, for the reason given at "Byte-slice comparison helpers". let mut c = Self::FALSE; - for i in 0..list.len() { - let diff = value ^ list[i]; - c |= Self::is_zero(diff); + for x in list { + c |= Self::is_equal(value, *x); + c = c.barrier(); } - c } @@ -299,18 +344,21 @@ impl Condition { /// /// Therefore, if the [`Self::TRUE`] constant value of the [`Condition`] implementation is changed to `-1`, /// the test also runs normally. - pub const fn negate(self, value: i32) -> i32 { - (value ^ self.0).wrapping_sub(self.0) + pub fn negate(self, value: i32) -> i32 { + let mask = self.barrier().0; + (value ^ mask).wrapping_sub(mask) } /// Conditional selection: return `true_value` if the condition is true, otherwise /// return `false_value`. - pub const fn select(self, true_value: i32, false_value: i32) -> i32 { - (true_value & self.0) | (false_value & !self.0) + pub fn select(self, true_value: i32, false_value: i32) -> i32 { + let mask = self.barrier().0; + (true_value & mask) | (false_value & !mask) } - /// Conditional swap: returns (lhs, rhs) if the condition is true, otherwise - /// returns (rhs, lhs). - pub const fn swap(self, lhs: i32, rhs: i32) -> (i32, i32) { - (self.select(rhs, lhs), self.select(lhs, rhs)) + /// Conditional swap: returns (rhs, lhs) if the condition is true, otherwise (lhs, rhs). + pub fn swap(self, lhs: i32, rhs: i32) -> (i32, i32) { + // One barrier serves both outputs: `t` is `lhs ^ rhs` under TRUE and zero under FALSE. + let t = (lhs ^ rhs) & self.barrier().0; + (lhs ^ t, rhs ^ t) } /// Convert the mask to a runtime boolean. Only use this at genuine public /// decision points: branching on the result leaks the condition's value. @@ -388,18 +436,20 @@ impl Condition { } /// Conditional selection: return `true_value` if the condition is true, otherwise /// return `false_value`. - pub const fn select(self, true_value: u64, false_value: u64) -> u64 { - (true_value & self.0) | (false_value & !self.0) + pub fn select(self, true_value: u64, false_value: u64) -> u64 { + let mask = self.barrier().0; + (true_value & mask) | (false_value & !mask) } /// Conditionally move the source value to the destination if the condition is /// true, otherwise nothing is moved. pub fn mov(self, src: u64, dst: &mut u64) { *dst = self.select(src, *dst); } - /// Conditional swap: returns (lhs, rhs) if the condition is true, otherwise - /// returns (rhs, lhs). - pub const fn swap(self, lhs: u64, rhs: u64) -> (u64, u64) { - (self.select(rhs, lhs), self.select(lhs, rhs)) + /// Conditional swap: returns (rhs, lhs) if the condition is true, otherwise (lhs, rhs). + pub fn swap(self, lhs: u64, rhs: u64) -> (u64, u64) { + // One barrier serves both outputs: `t` is `lhs ^ rhs` under TRUE and zero under FALSE. + let t = (lhs ^ rhs) & self.barrier().0; + (lhs ^ t, rhs ^ t) } /// Convert the mask to a runtime boolean. Only use this at genuine public /// decision points: branching on the result leaks the condition's value. @@ -466,18 +516,20 @@ impl Condition { } /// Conditional selection: return `true_value` if the condition is true, otherwise /// return `false_value`. - pub const fn select(self, true_value: u32, false_value: u32) -> u32 { - (true_value & self.0) | (false_value & !self.0) + pub fn select(self, true_value: u32, false_value: u32) -> u32 { + let mask = self.barrier().0; + (true_value & mask) | (false_value & !mask) } /// Conditionally move the source value to the destination if the condition is /// true, otherwise nothing is moved. pub fn mov(self, src: u32, dst: &mut u32) { *dst = self.select(src, *dst); } - /// Conditional swap: returns (lhs, rhs) if the condition is true, otherwise - /// returns (rhs, lhs). - pub const fn swap(self, lhs: u32, rhs: u32) -> (u32, u32) { - (self.select(rhs, lhs), self.select(lhs, rhs)) + /// Conditional swap: returns (rhs, lhs) if the condition is true, otherwise (lhs, rhs). + pub fn swap(self, lhs: u32, rhs: u32) -> (u32, u32) { + // One barrier serves both outputs: `t` is `lhs ^ rhs` under TRUE and zero under FALSE. + let t = (lhs ^ rhs) & self.barrier().0; + (lhs ^ t, rhs ^ t) } /// Convert the mask to a runtime boolean. Only use this at genuine public /// decision points: branching on the result leaks the condition's value. @@ -560,52 +612,105 @@ where } } -/// Rust doesn't guarantee that anything can truly be constant-time under all compilation targets -/// and optimization levels. The following presents the standard constant-time shape. -pub fn ct_eq_bytes(a: &[u8], b: &[u8]) -> bool { +// --------------------------------------------------------------------------------------------- +// Byte-slice comparison helpers +// +// The accumulator is routed through `value_barrier` on every iteration. That forecloses both of +// the early exits that would otherwise be legal: +// +// * leaving the loop once the accumulator is non-zero, because the final `== 0` is already +// decided (only legal if the compiler can see that the zero test is the sole consumer), and +// * leaving the loop once the accumulator is all-ones, because further ORs cannot change it +// (legal regardless of the consumer, which is why the barrier must be *inside* the loop). +// --------------------------------------------------------------------------------------------- + +/// The slices are compared one machine word at a time, with a byte-wise tail. The word is a +/// `usize`, so it is 2, 4 or 8 bytes according to the target. +type AccWord = usize; +const ACC_BYTES: usize = size_of::(); + +/// TRUE iff the accumulator of a comparison loop is zero, as a mask rather than a `bool`, so +/// that nothing between the barriered accumulator and the consumer of the mask invites a branch. +fn acc_is_zero(acc: AccWord) -> Condition { + // The `is_not_zero` identity (the top bit of `x | -x` is set iff `x != 0`) taken apart with a + // barrier after each step. Written in one piece the compiler recognises it as `x != 0`, which + // avr lowers as a compare and branch, and spreading the top bit across a 32-bit word instead + // (as `Condition::is_zero` does) becomes a branch on msp430. Opaque at each step, it stays + // or/neg, shift, subtract on every target inspected. + let top = value_barrier(acc | acc.wrapping_neg()); + let not_zero = value_barrier(top >> (AccWord::BITS - 1)); + // `not_zero` is 0 or 1; subtracting 1 gives all-ones for a zero accumulator, zero otherwise. + Condition((not_zero as u32).wrapping_sub(1)) +} + +/// Constant-time equality of two byte slices, as a mask: [`Condition::TRUE`] iff `a == b`. +/// +/// The runtime depends on the *lengths* of the inputs, which are treated as public, but not on +/// their contents or on the position of any difference. Slices of different lengths compare +/// unequal immediately. +/// +/// Use this form where the result selects data, as in [`conditional_copy_bytes`], so that the +/// comparison and the selection are joined by a mask rather than a `bool`. Rust does not +/// guarantee constant-time execution on every target and optimisation level; the "Optimisation +/// barrier" section comment in this file says what is done about that. +pub fn ct_eq_bytes_mask(a: &[u8], b: &[u8]) -> Condition { if a.len() != b.len() { - return false; + return Condition::::FALSE; } - let mut result = 0u8; - for i in 0..a.len() { - result |= core::hint::black_box(a[i] ^ b[i]); + // Both slices now have the same length, so the two chunkings line up exactly and the + // `zip`s below never drop an element. + let (words_a, tail_a) = a.as_chunks::(); + let (words_b, tail_b) = b.as_chunks::(); + + let mut acc: AccWord = 0; + for (x, y) in words_a.iter().zip(words_b) { + acc = value_barrier(acc | (AccWord::from_ne_bytes(*x) ^ AccWord::from_ne_bytes(*y))); } - result == 0 + for (x, y) in tail_a.iter().zip(tail_b) { + acc = value_barrier(acc | AccWord::from(x ^ y)); + } + acc_is_zero(acc) +} + +/// Constant-time equality of two byte slices: [`ct_eq_bytes_mask`] as a `bool`, for callers that +/// go on to branch on the result at a public decision point. +pub fn ct_eq_bytes(a: &[u8], b: &[u8]) -> bool { + ct_eq_bytes_mask(a, b).to_bool() } -/// Rust doesn't guarantee that anything can truly be constant-time under all compilation targets -/// and optimization levels. The following presents the standard constant-time shape. +/// Constant-time check that every byte of `a` is zero. +/// +/// The runtime depends on the length of `a`, which is treated as public, but not on its contents +/// or on the position of the first non-zero byte. Same construction as [`ct_eq_bytes_mask`] with +/// the XOR against the second operand omitted. pub fn ct_eq_zero_bytes(a: &[u8]) -> bool { - let mut result = 0u8; - for i in 0..a.len() { - result |= core::hint::black_box(a[i]); + let (words, tail) = a.as_chunks::(); + + let mut acc: AccWord = 0; + for x in words { + acc = value_barrier(acc | AccWord::from_ne_bytes(*x)); + } + for x in tail { + acc = value_barrier(acc | AccWord::from(*x)); } - result == 0 + acc_is_zero(acc).to_bool() } -/// Copies either the contents of `a` or `b` into `out` according to `take_a` -/// and it does it in a constant-time manner without branching. +/// Copies `a` into `out` if `take_a` is TRUE, otherwise `b`, without branching on `take_a`. +/// +/// Take `take_a` from [`ct_eq_bytes_mask`] or a [`Condition`] constructor. Converting a secret +/// `bool` with [`Condition::from_bool`] puts a value the compiler may branch on between the +/// comparison and the copy. pub fn conditional_copy_bytes( a: &[u8; LEN], b: &[u8; LEN], out: &mut [u8; LEN], - take_a: bool, + take_a: Condition, ) { - // we want the behaviour of - // if take_a { 0xFF } else { 0x00 } - // but without using any branches that could leak timing signals - let mask: u8 = (take_a as u8) - | (take_a as u8) << 1 - | (take_a as u8) << 2 - | (take_a as u8) << 3 - | (take_a as u8) << 4 - | (take_a as u8) << 5 - | (take_a as u8) << 6 - | (take_a as u8) << 7; - - debug_assert_eq!(mask, if take_a { 0xFF } else { 0x00 }); - - for i in 0..LEN { - out[i] = core::hint::black_box(a[i] & mask) | core::hint::black_box(b[i] & !mask); + // One barrier for the whole copy; after it the byte mask is opaque, so the masked + // arithmetic per byte cannot be turned back into a branch. + let mask = take_a.barrier().0 as u8; + for ((o, x), y) in out.iter_mut().zip(a).zip(b) { + *o = (x & mask) | (y & !mask); } } diff --git a/crypto/utils/src/lib.rs b/crypto/utils/src/lib.rs index 39c1cadf..dadfef13 100644 --- a/crypto/utils/src/lib.rs +++ b/crypto/utils/src/lib.rs @@ -18,6 +18,7 @@ pub mod ct; pub mod secret; +pub mod suspendable_state; /// Basic max function. If they are equal, it returns the first one. pub fn max<'a, T: PartialOrd>(x: &'a T, y: &'a T) -> &'a T { diff --git a/crypto/utils/src/secret.rs b/crypto/utils/src/secret.rs index c1203f0f..e86cec84 100644 --- a/crypto/utils/src/secret.rs +++ b/crypto/utils/src/secret.rs @@ -221,7 +221,7 @@ impl ZeroizablePrimitive for [T; N] { /// print!("{}\n", size_of::>()); // also 32 /// ``` /// -/// # 🚨 Security 🚨 +/// # 🚨 Security Considerations 🚨 /// /// What this does NOT guarantee: /// diff --git a/crypto/utils/src/suspendable_state.rs b/crypto/utils/src/suspendable_state.rs new file mode 100644 index 00000000..d22ce2e9 --- /dev/null +++ b/crypto/utils/src/suspendable_state.rs @@ -0,0 +1,341 @@ +//! Suspending a stateful object to a byte array and resuming it later: the version header every +//! suspended state starts with, the error type, and the component trait that composite states +//! are built from. +//! +//! The traits themselves -- `Suspendable` and `SuspendableKeyed` -- live in `bouncycastle-core`, +//! since they are part of the trait vocabulary every primitive implements. What is here is the +//! machinery their implementations share. +//! +//! # The version header +//! +//! Every suspended state begins with the three-byte library version that wrote it +//! ([`add_lib_ver`]), and every deserializer checks it ([`check_lib_ver`]): a state from a +//! future major or minor version, or from the sentinel `0.0.0`, is refused, and anything else on +//! the same major.minor stream is accepted. See [`LIB_VERSION`] for the maintenance rule that +//! makes this gate sound. +//! +//! # Composing suspended states +//! +//! The traits carry the state length as a const generic parameter, `SuspendableKeyed`, so a +//! generic adapter over an inner type -- a block cipher mode over a permutation, a stream cipher +//! over a keystream -- cannot write its own `N` as "the inner length plus my own" on stable Rust +//! (`generic_const_exprs`). [`SuspendableComponent`] is the workaround: it names the length as +//! an associated const and reads and writes state through slices, so composition is ordinary +//! code. Each public type's `SuspendableKeyed` impl is then a shell over +//! [`suspend_component`] and [`resume_component`], which add the version header and check at +//! compile time that `N` is the component's length plus [`LIB_VERSION_LEN`]. A wrong `N` is a +//! compile error at the call site. +//! +//! ``` +//! use bouncycastle_utils::suspendable_state::{ +//! Cursor, CursorMut, LIB_VERSION_LEN, SuspendableComponent, SuspendableError, +//! bounded_usize, resume_component, suspend_component, +//! }; +//! +//! /// A toy: a counter that must never exceed 100, and a key it is checked against on resume. +//! struct Counter { count: usize } +//! +//! impl SuspendableComponent for Counter { +//! const STATE_LEN: usize = 8; +//! type Key = u8; +//! fn write_state(&self, out: &mut [u8]) { +//! CursorMut::new(out).u64(self.count as u64); +//! } +//! fn read_state(state: &[u8], key: &u8) -> Result { +//! if *key != 7 { return Err(SuspendableError::InvalidData); } +//! Ok(Counter { count: bounded_usize(Cursor::new(state).u64(), 100)? }) +//! } +//! } +//! +//! const STATE_LEN: usize = LIB_VERSION_LEN + Counter::STATE_LEN; +//! let state: [u8; STATE_LEN] = suspend_component(&Counter { count: 42 }); +//! let resumed: Counter = resume_component(&state, &7).unwrap(); +//! assert_eq!(resumed.count, 42); +//! assert!(resume_component::(&state, &8).is_err(), "wrong key"); +//! ``` +//! +//! [`Cursor`] and [`CursorMut`] are for writing the layouts as a sequence of fields rather than +//! offset arithmetic; [`bounded_usize`] reads a count back and refuses one past its bound. + +/// Errors from suspending and resuming an object's state. +#[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum SuspendableError { + /// The serialized state was produced by a library version incompatible with this one. + IncompatibleVersion, + /// The serialized state is malformed or corrupt. + InvalidData, +} + +/// A semantic library version, ordered by `major`, then `minor`, then `patch`. +/// +/// The field declaration order matters: the derived [`Ord`]/[`PartialOrd`] compare fields +/// lexicographically in declaration order, which is exactly semantic-version precedence. +/// A semantic version can often also take a suffix, e.g. "alpha", "beta", "rc1", etc. +/// We're not going to model that here because it's not useful for versioning serialized states. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct SemVer { + /// Incremented for incompatible changes. + pub major: u8, + /// Incremented for compatible additions, and for any change to a suspended-state layout. + pub minor: u8, + /// Incremented for fixes that change no layout. + pub patch: u8, +} + +impl From<[u8; 3]> for SemVer { + fn from(v: [u8; 3]) -> Self { + SemVer { major: v[0], minor: v[1], patch: v[2] } + } +} + +impl From for [u8; 3] { + fn from(v: SemVer) -> Self { + [v.major, v.minor, v.patch] + } +} + +/// Parse a decimal ASCII string (a Cargo version component) into a u8 at compile time. +const fn parse_version_component(s: &str) -> u8 { + let bytes = s.as_bytes(); + let mut result: u8 = 0; + let mut i = 0; + while i < bytes.len() { + let d = bytes[i]; + assert!(d >= b'0' && d <= b'9', "version component must be numeric"); + // A component > 255 overflows u8 and fails the build (SemVer fields are u8 by design). + result = result * 10 + (d - b'0'); + i += 1; + } + result +} + +/// The current library version at compile time, via Cargo's `CARGO_PKG_VERSION_*` env vars. Every +/// crate in the workspace takes `version.workspace = true`, so this is the workspace version +/// whichever crate is building. +/// +/// MAINTAINER NOTE: this single value is the *only* compatibility gate for every serialized state in +/// the workspace (see [`check_lib_ver`]), and the policy accepts any future *patch* on the same +/// major.minor stream. Therefore any change to the on-the-wire layout of *any* suspendable state -- +/// in any primitive crate -- MUST bump the workspace's **minor** version (never just the patch), +/// otherwise an older build will silently accept and misread a newer, incompatible state. +pub const LIB_VERSION: SemVer = SemVer { + major: parse_version_component(env!("CARGO_PKG_VERSION_MAJOR")), + minor: parse_version_component(env!("CARGO_PKG_VERSION_MINOR")), + patch: parse_version_component(env!("CARGO_PKG_VERSION_PATCH")), +}; + +/// Bytes of library-version header [`add_lib_ver`] puts in front of every suspended state. +pub const LIB_VERSION_LEN: usize = 3; + +/// Puts the library version into the first three bytes of the state array. +/// +/// Hands back a slice to the same array, starting after the version tag. +pub fn add_lib_ver(state: &mut [u8; SERIALIZED_LEN]) -> &mut [u8] { + state[..LIB_VERSION_LEN].copy_from_slice(&<[u8; 3]>::from(LIB_VERSION)); + &mut state[LIB_VERSION_LEN..] +} + +/// A helper for deserializing an object's state +/// +/// The state_out array must have length at least SERIALIZED_LEN - 3. +/// +/// Returns the number of bytes written to state_out, or a [`SuspendableError::IncompatibleVersion`] if +/// the version of the serialized state is earlier than the specified `not_before` version, or +/// is a future MAJOR or MINOR version (but future PATCH versions are ok). +/// +/// Note that for testability, this will always reject if the serialized state contains a version tag +/// of `[0,0,0]`. +/// +/// Hands back a slice to the same array, starting after the version tag. +pub fn check_lib_ver( + state: &[u8; SERIALIZED_LEN], + not_before: Option<[u8; 3]>, +) -> Result<&[u8], SuspendableError> { + // the .unwrap is infallible after the guard check + if state.len() < LIB_VERSION_LEN { + return Err(SuspendableError::InvalidData); + } + let ver_bytes: [u8; 3] = state[..LIB_VERSION_LEN].try_into().unwrap(); + let ver = SemVer::from(ver_bytes); + + let not_before = SemVer::from(not_before.unwrap_or([0, 0, 0])); + + if ver < not_before { + return Err(SuspendableError::IncompatibleVersion); + }; + // Nothing is ever compatible with [0,0,0] + if ver == SemVer::from([0, 0, 0]) { + return Err(SuspendableError::IncompatibleVersion); + }; + + // Check if state was produced by a later MAJOR or MINOR version; + // a future version on the same patch stream is ok (if not, then we've broken the rules of semantic versioning); + let patch_stream = SemVer::from([LIB_VERSION.major, LIB_VERSION.minor, 255]); + if ver > patch_stream { + return Err(SuspendableError::IncompatibleVersion); + } + + Ok(&state[LIB_VERSION_LEN..]) +} + +/// A piece of state that can be written to, and rebuilt from, a byte slice of a length it names, +/// given a key. See the module docs for why this exists alongside the `SuspendableKeyed` trait. +/// +/// `write_state` and `read_state` are given exactly [`STATE_LEN`](Self::STATE_LEN) bytes. The +/// version header is not part of it: a composite writes one header for the whole state, through +/// [`suspend_component`] and [`resume_component`]. +pub trait SuspendableComponent: Sized { + /// The number of bytes `write_state` fills and `read_state` reads. + const STATE_LEN: usize; + /// The key that must be re-supplied to resume. It is never written into the state. + type Key: ?Sized; + /// Writes the state into `out`, which is exactly `STATE_LEN` bytes. + fn write_state(&self, out: &mut [u8]); + /// Rebuilds the component from `state`, exactly `STATE_LEN` bytes, and the key. + /// + /// # Errors + /// [`SuspendableError::InvalidData`] if `state` is not one this component could have + /// written, or `key` is not a key the component accepts. + fn read_state(state: &[u8], key: &Self::Key) -> Result; +} + +/// The `suspend` of a component: the version header, then its state. +/// +/// `N` must be `LIB_VERSION_LEN + C::STATE_LEN`, checked at compile time. +pub fn suspend_component(component: &C) -> [u8; N] { + const { + assert!( + N == LIB_VERSION_LEN + C::STATE_LEN, + "N must be the type's SUSPENDED_STATE_LEN: the version header plus its state" + ) + }; + let mut out = [0u8; N]; + // `add_lib_ver` hands back exactly `N - LIB_VERSION_LEN == C::STATE_LEN` bytes. + component.write_state(add_lib_ver(&mut out)); + out +} + +/// The `from_suspended` of a component: checks the version header, then reads the state. `N` as +/// for [`suspend_component`]. +/// +/// # Errors +/// [`SuspendableError::IncompatibleVersion`] from the header check, otherwise whatever +/// [`SuspendableComponent::read_state`] returns. +pub fn resume_component( + state: &[u8; N], + key: &C::Key, +) -> Result { + const { + assert!( + N == LIB_VERSION_LEN + C::STATE_LEN, + "N must be the type's SUSPENDED_STATE_LEN: the version header plus its state" + ) + }; + // `check_lib_ver` hands back exactly `N - LIB_VERSION_LEN == C::STATE_LEN` bytes. + C::read_state(check_lib_ver(state, None)?, key) +} + +/// A cursor over a state buffer, so a layout reads as a sequence of fields rather than offset +/// arithmetic. Every length is fixed by the type, so these never fail on a state of the right +/// length; a wrong length is caught by the compile-time check in [`suspend_component`]. +pub struct Cursor<'a> { + buf: &'a [u8], + pos: usize, +} + +impl<'a> Cursor<'a> { + /// Starts at the beginning of `buf`. + pub fn new(buf: &'a [u8]) -> Self { + Self { buf, pos: 0 } + } + + /// The next `len` bytes. + pub fn bytes(&mut self, len: usize) -> &'a [u8] { + let out = &self.buf[self.pos..self.pos + len]; + self.pos += len; + out + } + + /// The next `N` bytes, as an array. + pub fn array(&mut self) -> [u8; N] { + let mut out = [0u8; N]; + out.copy_from_slice(self.bytes(N)); + out + } + + /// The next eight bytes as a little-endian `u64`. + pub fn u64(&mut self) -> u64 { + u64::from_le_bytes(self.array()) + } + + /// The next byte. + pub fn u8(&mut self) -> u8 { + self.bytes(1)[0] + } + + /// `true` once every byte has been read; a layout asserts this at the end of a read. + pub fn is_done(&self) -> bool { + self.pos == self.buf.len() + } +} + +/// The writing counterpart of [`Cursor`]. +pub struct CursorMut<'a> { + buf: &'a mut [u8], + pos: usize, +} + +impl<'a> CursorMut<'a> { + /// Starts at the beginning of `buf`. + pub fn new(buf: &'a mut [u8]) -> Self { + Self { buf, pos: 0 } + } + + /// Writes `bytes` next. + pub fn bytes(&mut self, bytes: &[u8]) { + self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes); + self.pos += bytes.len(); + } + + /// Writes `v` next, as eight little-endian bytes. + pub fn u64(&mut self, v: u64) { + self.bytes(&v.to_le_bytes()); + } + + /// Writes one byte next. + pub fn u8(&mut self, v: u8) { + self.bytes(&[v]); + } + + /// `true` once every byte has been written; a layout asserts this at the end of a write. + pub fn is_done(&self) -> bool { + self.pos == self.buf.len() + } +} + +/// A `usize` field read back from its `u64` encoding, refused if it is above `max`. +pub fn bounded_usize(v: u64, max: usize) -> Result { + if v > max as u64 { Err(SuspendableError::InvalidData) } else { Ok(v as usize) } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cmp_lib_ver() { + use core::cmp::Ordering; + + assert!([0, 0, 0] < [0, 0, 1]); + + let cmp = |a: [u8; 3], b: [u8; 3]| SemVer::from(a).cmp(&SemVer::from(b)); + assert_eq!(cmp([0, 2, 1], [1, 1, 1]), Ordering::Less); + assert_eq!(cmp([2, 1, 1], [1, 1, 1]), Ordering::Greater); + assert_eq!(cmp([1, 0, 2], [1, 1, 1]), Ordering::Less); + assert_eq!(cmp([1, 2, 0], [1, 1, 1]), Ordering::Greater); + assert_eq!(cmp([1, 1, 0], [1, 1, 1]), Ordering::Less); + assert_eq!(cmp([1, 1, 2], [1, 1, 1]), Ordering::Greater); + assert_eq!(cmp([1, 1, 1], [1, 1, 1]), Ordering::Equal); + } +} diff --git a/crypto/utils/tests/ct_tests.rs b/crypto/utils/tests/ct_tests.rs index 7bb8ef0f..81a76d33 100644 --- a/crypto/utils/tests/ct_tests.rs +++ b/crypto/utils/tests/ct_tests.rs @@ -228,6 +228,11 @@ mod unsigned_u64_tests { assert_eq!((lhs, rhs), (2, 1)); let (lhs, rhs) = Condition::::from_bool_const::().swap(1, 2); assert_eq!((lhs, rhs), (1, 2)); + // overlapping bit patterns: `1` and `2` cannot tell XOR from OR in the swap arithmetic + let (lhs, rhs) = Condition::::TRUE.swap(0x0F, 0x3C); + assert_eq!((lhs, rhs), (0x3C, 0x0F)); + let (lhs, rhs) = Condition::::FALSE.swap(0x0F, 0x3C); + assert_eq!((lhs, rhs), (0x0F, 0x3C)); } #[test] @@ -372,6 +377,11 @@ mod unsigned_u32_tests { assert_eq!((lhs, rhs), (2, 1)); let (lhs, rhs) = Condition::::from_bool_const::().swap(1, 2); assert_eq!((lhs, rhs), (1, 2)); + // overlapping bit patterns: `1` and `2` cannot tell XOR from OR in the swap arithmetic + let (lhs, rhs) = Condition::::TRUE.swap(0x0F, 0x3C); + assert_eq!((lhs, rhs), (0x3C, 0x0F)); + let (lhs, rhs) = Condition::::FALSE.swap(0x0F, 0x3C); + assert_eq!((lhs, rhs), (0x0F, 0x3C)); } #[test] @@ -531,6 +541,7 @@ mod signed_i64_tests { assert_canonical(Condition::::is_in_list(4, &[1, 2, 3]), false); assert_canonical(Condition::::is_in_list(-3, &[1, 2, 3, 4, -5, -1]), false); assert_canonical(Condition::::is_in_list(3, &[1, 2, 3, 3, 3, 3]), true); + assert_canonical(Condition::::is_in_list(1, &[]), false); } #[test] @@ -571,6 +582,11 @@ mod signed_i64_tests { assert_eq!((lhs, rhs), (2, 1)); let (lhs, rhs) = Condition::::from_bool_const::().swap(1, 2); assert_eq!((lhs, rhs), (1, 2)); + // overlapping bit patterns: `1` and `2` cannot tell XOR from OR in the swap arithmetic + let (lhs, rhs) = Condition::::TRUE.swap(0x0F, 0x3C); + assert_eq!((lhs, rhs), (0x3C, 0x0F)); + let (lhs, rhs) = Condition::::FALSE.swap(0x0F, 0x3C); + assert_eq!((lhs, rhs), (0x0F, 0x3C)); } #[test] @@ -726,6 +742,7 @@ mod signed_i32_tests { assert_canonical(Condition::::is_in_list(4, &[1, 2, 3]), false); assert_canonical(Condition::::is_in_list(-3, &[1, 2, 3, 4, -5, -1]), false); assert_canonical(Condition::::is_in_list(3, &[1, 2, 3, 3, 3, 3]), true); + assert_canonical(Condition::::is_in_list(1, &[]), false); } #[test] @@ -766,6 +783,11 @@ mod signed_i32_tests { assert_eq!((lhs, rhs), (2, 1)); let (lhs, rhs) = Condition::::from_bool_const::().swap(1, 2); assert_eq!((lhs, rhs), (1, 2)); + // overlapping bit patterns: `1` and `2` cannot tell XOR from OR in the swap arithmetic + let (lhs, rhs) = Condition::::TRUE.swap(0x0F, 0x3C); + assert_eq!((lhs, rhs), (0x3C, 0x0F)); + let (lhs, rhs) = Condition::::FALSE.swap(0x0F, 0x3C); + assert_eq!((lhs, rhs), (0x0F, 0x3C)); } #[test] @@ -805,6 +827,34 @@ mod signed_comparison_sweep { #[cfg(test)] mod ct_bytes_tests { + use bouncycastle_utils::ct::Condition; + + /// `ct_eq_bytes_mask` must be exactly TRUE or FALSE (a `select` on it reproduces the chosen + /// pattern bit for bit) and agree with `ct_eq_bytes`. + #[test] + fn test_ct_eq_bytes_mask() { + use bouncycastle_utils::ct::{ct_eq_bytes, ct_eq_bytes_mask}; + + const PATTERN: u32 = 0x5555_5555; + let a: [u8; 20] = core::array::from_fn(|i| i as u8); + let mut b = a; + for (other, expected) in [(&a[..], true), (&b[..19], false)] { + let m = ct_eq_bytes_mask(&a, other); + assert_eq!(m.select(PATTERN, !PATTERN), if expected { PATTERN } else { !PATTERN }); + assert_eq!(m.to_bool(), expected); + assert_eq!(ct_eq_bytes(&a, other), expected); + } + // a difference in the word path and one in the byte tail (for 2, 4 and 8-byte words) + for pos in [5, 19] { + b[pos] ^= 0x01; + let m = ct_eq_bytes_mask(&a, &b); + assert_eq!(m.select(PATTERN, !PATTERN), !PATTERN, "pos {pos}"); + assert!(!ct_eq_bytes(&a, &b), "pos {pos}"); + b[pos] ^= 0x01; + } + assert_eq!(ct_eq_bytes_mask(&[], &[]).select(PATTERN, !PATTERN), PATTERN); + } + #[test] fn test_ct_eq_bytes() { use bouncycastle_utils::ct::ct_eq_bytes; @@ -831,6 +881,53 @@ mod ct_bytes_tests { assert!(!ct_eq_bytes(&a, &b)); } + /// The implementation processes the input one machine word at a time (2, 4 or 8 bytes + /// depending on the target) and then the remaining tail byte-wise. Exercise every length up + /// to several words so that, whatever the word size, a difference in any position, word or + /// tail, is detected and equal inputs of every shape compare equal. + #[test] + fn test_ct_eq_bytes_word_boundaries() { + use bouncycastle_utils::ct::ct_eq_bytes; + + for len in 0..=40usize { + let a: [u8; 40] = core::array::from_fn(|i| (i as u8).wrapping_mul(37) ^ 0x5C); + let a = &a[..len]; + let mut b = [0u8; 40]; + b[..len].copy_from_slice(a); + assert!(ct_eq_bytes(a, &b[..len]), "len {len}"); + + // flip a single bit at each position in turn + for pos in 0..len { + for bit in [0x01u8, 0x80] { + b[pos] ^= bit; + assert!(!ct_eq_bytes(a, &b[..len]), "len {len} pos {pos} bit {bit:#x}"); + b[pos] ^= bit; + } + } + } + } + + /// The accumulator must OR the differences together, not XOR them: two positions carrying + /// the same difference must not cancel out, whether they fall in the same word, in + /// different words, or in the byte tail. 46 bytes leaves a tail of at least two bytes for + /// 4- and 8-byte words, so the pairs at the end land in the tail on those targets. The pairs + /// four bytes apart within one word sit in the two halves that the final narrowing to 32 + /// bits folds together, which must OR as well. + #[test] + fn test_ct_eq_bytes_repeated_difference() { + use bouncycastle_utils::ct::ct_eq_bytes; + + let a = [0x42u8; 46]; + for (p, q) in + [(0, 1), (0, 4), (0, 8), (3, 19), (7, 39), (10, 14), (33, 39), (41, 45), (44, 45)] + { + let mut b = a; + b[p] ^= 0x10; + b[q] ^= 0x10; + assert!(!ct_eq_bytes(&a, &b), "positions {p} {q}"); + } + } + #[test] fn test_ct_eq_zero_bytes() { use bouncycastle_utils::ct::ct_eq_zero_bytes; @@ -853,6 +950,40 @@ mod ct_bytes_tests { assert!(!ct_eq_zero_bytes(&buf)); } + /// Same boundary sweep as for `ct_eq_bytes`: a non-zero byte at any position of any length + /// around the machine-word boundaries must be detected. + #[test] + fn test_ct_eq_zero_bytes_word_boundaries() { + use bouncycastle_utils::ct::ct_eq_zero_bytes; + + for len in 0..=40usize { + let mut buf = [0u8; 40]; + assert!(ct_eq_zero_bytes(&buf[..len]), "len {len}"); + for pos in 0..len { + for val in [0x01u8, 0x80] { + buf[pos] = val; + assert!(!ct_eq_zero_bytes(&buf[..len]), "len {len} pos {pos} val {val:#x}"); + buf[pos] = 0; + } + } + } + } + + /// As for `ct_eq_bytes`: two identical non-zero bytes must not cancel each other out. + #[test] + fn test_ct_eq_zero_bytes_repeated_nonzero() { + use bouncycastle_utils::ct::ct_eq_zero_bytes; + + for (p, q) in + [(0, 1), (0, 4), (0, 8), (3, 19), (7, 39), (10, 14), (33, 39), (41, 45), (44, 45)] + { + let mut buf = [0u8; 46]; + buf[p] = 0x10; + buf[q] = 0x10; + assert!(!ct_eq_zero_bytes(&buf), "positions {p} {q}"); + } + } + #[test] fn test_conditional_copy_bytes() { use bouncycastle_utils::ct::conditional_copy_bytes; @@ -861,12 +992,37 @@ mod ct_bytes_tests { let b = [0x10, 0x11, 0x12, 0x13]; let mut out = [0u8; 4]; - conditional_copy_bytes(&a, &b, &mut out, true); + conditional_copy_bytes(&a, &b, &mut out, Condition::::TRUE); assert_eq!(out, [0x01, 0x02, 0x03, 0x04]); - conditional_copy_bytes(&a, &b, &mut out, false); + conditional_copy_bytes(&a, &b, &mut out, Condition::::FALSE); assert_eq!(out, [0x10, 0x11, 0x12, 0x13]); + // every byte position must follow the flag independently. `a` and `b` are unrelated + // (not complements of each other, so a wrong mask formulation cannot produce the right + // answer for one flag value by accident) and between them cover 0x00 and 0xFF bytes. + let a: [u8; 32] = core::array::from_fn(|i| (i as u8).wrapping_mul(0x11)); + let b: [u8; 32] = core::array::from_fn(|i| (i as u8).wrapping_mul(0x37).wrapping_add(0xC9)); + assert!(a.iter().any(|&x| x == 0x00) && a.iter().any(|&x| x == 0xFF)); + let mut out = [0xEEu8; 32]; + conditional_copy_bytes(&a, &b, &mut out, Condition::::TRUE); + assert_eq!(out, a); + conditional_copy_bytes(&a, &b, &mut out, Condition::::FALSE); + assert_eq!(out, b); + + // a == b: the mask is invisible, but `out` must still be overwritten either way + let mut out = [0xEEu8; 32]; + conditional_copy_bytes(&a, &a, &mut out, Condition::::TRUE); + assert_eq!(out, a); + let mut out = [0xEEu8; 32]; + conditional_copy_bytes(&a, &a, &mut out, Condition::::FALSE); + assert_eq!(out, a); + + // the empty array must be a no-op + let mut empty = [0u8; 0]; + conditional_copy_bytes(&[], &[], &mut empty, Condition::::TRUE); + conditional_copy_bytes(&[], &[], &mut empty, Condition::::FALSE); + // test wrong-sized array // in fact: this won't even compile, so there's nothing to test // let c = [0x20, 0x21, 0x22]; diff --git a/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index a3623aac..f6b2cf7f 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -9,12 +9,20 @@ bouncycastle.workspace = true [[bin]] name = "bench_mldsa_mem_usage" -path = "bench_mldsa_mem_usage.rs" +path = "src/bench_mldsa_mem_usage.rs" [[bin]] name = "bench_mlkem_mem_usage" -path = "bench_mlkem_mem_usage.rs" +path = "src/bench_mlkem_mem_usage.rs" [[bin]] name = "bench_sha3_mem_usage" -path = "bench_sha3_mem_usage.rs" +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_aes_mem_usage.rs b/mem_usage_benches/src/bench_aes_mem_usage.rs new file mode 100644 index 00000000..40cd2b2e --- /dev/null +++ b/mem_usage_benches/src/bench_aes_mem_usage.rs @@ -0,0 +1,354 @@ +//! 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_aes_mem_usage > /dev/null +//! +//! ms_print massif.out.* +//! ``` +//! +//! or, shoved all into one line: +//! +//! ```text +//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_aes_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. +//! +//! # What to expect, and why massif cannot see it +//! +//! Unlike ML-KEM and ML-DSA, AES has no interesting stack profile: there is no polynomial +//! arithmetic and no sampling, so a call needs a few hundred bytes -- the bit-sliced state +//! (16, 32 or 64 bytes for the one-, two- and four-block entry points), the widened round key +//! (the same again) and the S-box circuit's spills. That is below what this harness resolves: +//! the process's own start-up reaches about 7.7 kB of stack before `main` runs, every bench +//! here reports exactly that peak, and none of massif's later snapshots lands inside the cipher. +//! So the massif number is the floor, not a measurement. +//! +//! The numbers that do mean something come from the compiler's frame-layout remarks +//! (`.claude/skills/memory-hygiene-in-rust`, section 5): build this binary with +//! `RUSTFLAGS="-C remark=prologepilog -C remark=stack-frame-layout"` and read the frame of each +//! `measure` closure, which is the operation's frame with nothing else in it. The shape below +//! is what makes that reading clean: the key or engine is built in an `#[inline(never)]` helper +//! so its frame is a sibling of the operation's, and the operation runs in a non-inlined, +//! non-returning closure so nothing crosses back across the boundary. The persistent cost, the +//! engine itself, is what `print_struct_sizes` prints. +//! +//! The point of comparison is that a table-driven AES adds 256 B (`AESLightEngine`) to 8 KiB +//! (T-tables) of static data on top of these numbers; this implementation adds zero. + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::core::hazmat::ElectronicCodeBook; +use bouncycastle::core::key_material::{KeyMaterial, KeyType}; + +/// 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 engine, i.e. the persistent cost of holding a key schedule. +fn print_struct_sizes() { + use core::mem::size_of; + + // FIPS 197 Sec 5.2: the schedule is 4 * (Nr + 1) words, so 176 / 208 / 240 bytes. The + // bit-sliced form is stored at the one-block width, so bit-slicing adds nothing to these. + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); +} + +/// Runs the operation in its own frame. Returns nothing, so no result crosses the boundary and +/// the closure's frame is exactly the operation's. +#[inline(never)] +fn measure(f: impl FnOnce()) { + f() +} + +/// The FIPS 197 Appendix A.1 key, so the engine under measurement is a known one. +const KEY_128: [u8; 16] = [ + 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c, +]; +/// FIPS 197 Appendix A.2. +const KEY_192: [u8; 24] = [ + 0x8e, 0x73, 0xb0, 0xf7, 0xda, 0x0e, 0x64, 0x52, 0xc8, 0x10, 0xf3, 0x2b, 0x80, 0x90, 0x79, 0xe5, + 0x62, 0xf8, 0xea, 0xd2, 0x52, 0x2c, 0x6b, 0x7b, +]; +/// FIPS 197 Appendix A.3. +const KEY_256: [u8; 32] = [ + 0x60, 0x3d, 0xeb, 0x10, 0x15, 0xca, 0x71, 0xbe, 0x2b, 0x73, 0xae, 0xf0, 0x85, 0x7d, 0x77, 0x81, + 0x1f, 0x35, 0x2c, 0x07, 0x3b, 0x61, 0x08, 0xd7, 0x2d, 0x98, 0x10, 0xa3, 0x09, 0x14, 0xdf, 0xf4, +]; + +/// Wraps a hard-coded key. `#[inline(never)]` so the wrapping is a sibling frame of whatever +/// uses the key, not part of it. +#[inline(never)] +fn key(bytes: &[u8; N]) -> KeyMaterial { + KeyMaterial::::from_bytes_as_type(bytes, KeyType::SymmetricCipherKey).unwrap() +} + +/// Expands the key into an engine, in its own frame, so the expansion's temporaries are popped +/// before an operation on the engine runs. +#[inline(never)] +fn load_aes128() -> AES128Internal { + AES128Internal::new(&key(&KEY_128)).unwrap() +} +#[inline(never)] +fn load_aes192() -> AES192Internal { + AES192Internal::new(&key(&KEY_192)).unwrap() +} +#[inline(never)] +fn load_aes256() -> AES256Internal { + AES256Internal::new(&key(&KEY_256)).unwrap() +} + +// ---- key expansion: the expansion is the operation, so it runs inside `measure` ------------ + +fn bench_aes128_key_expansion() { + eprintln!("AES128Internal::new (key expansion)"); + let key = key(&KEY_128); + measure(|| { + let aes = AES128Internal::new(&key).unwrap(); + print!("{aes:?}"); + }); +} + +fn bench_aes192_key_expansion() { + eprintln!("AES192Internal::new (key expansion)"); + let key = key(&KEY_192); + measure(|| { + let aes = AES192Internal::new(&key).unwrap(); + print!("{aes:?}"); + }); +} + +fn bench_aes256_key_expansion() { + eprintln!("AES256Internal::new (key expansion)"); + let key = key(&KEY_256); + measure(|| { + let aes = AES256Internal::new(&key).unwrap(); + print!("{aes:?}"); + }); +} + +// ---- the six entry points, per key length ------------------------------------------------- +// +// One block runs on u16 planes, two on u32, four on u64; the working state and the widened +// round key scale with that, so the three widths are measured separately. The blocks are +// stack arrays in the closure, never a heap buffer, so massif's --heap=no does not hide them. + +fn bench_aes128_encrypt_block() { + eprintln!("AES128Internal::encrypt_block"); + let aes = load_aes128(); + measure(|| { + let mut block = [0x11u8; 16]; + aes.encrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_aes128_decrypt_block() { + eprintln!("AES128Internal::decrypt_block"); + let aes = load_aes128(); + measure(|| { + let mut block = [0x11u8; 16]; + aes.decrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_aes128_encrypt_2blocks() { + eprintln!("AES128Internal::encrypt_2blocks"); + let aes = load_aes128(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16]]; + aes.encrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes128_decrypt_2blocks() { + eprintln!("AES128Internal::decrypt_2blocks"); + let aes = load_aes128(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16]]; + aes.decrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes128_encrypt_4blocks() { + eprintln!("AES128Internal::encrypt_4blocks"); + let aes = load_aes128(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16], [0x33u8; 16], [0x44u8; 16]]; + aes.encrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes128_decrypt_4blocks() { + eprintln!("AES128Internal::decrypt_4blocks"); + let aes = load_aes128(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16], [0x33u8; 16], [0x44u8; 16]]; + aes.decrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes192_encrypt_block() { + eprintln!("AES192Internal::encrypt_block"); + let aes = load_aes192(); + measure(|| { + let mut block = [0x11u8; 16]; + aes.encrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_aes192_decrypt_block() { + eprintln!("AES192Internal::decrypt_block"); + let aes = load_aes192(); + measure(|| { + let mut block = [0x11u8; 16]; + aes.decrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_aes192_encrypt_2blocks() { + eprintln!("AES192Internal::encrypt_2blocks"); + let aes = load_aes192(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16]]; + aes.encrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes192_decrypt_2blocks() { + eprintln!("AES192Internal::decrypt_2blocks"); + let aes = load_aes192(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16]]; + aes.decrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes192_encrypt_4blocks() { + eprintln!("AES192Internal::encrypt_4blocks"); + let aes = load_aes192(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16], [0x33u8; 16], [0x44u8; 16]]; + aes.encrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes192_decrypt_4blocks() { + eprintln!("AES192Internal::decrypt_4blocks"); + let aes = load_aes192(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16], [0x33u8; 16], [0x44u8; 16]]; + aes.decrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes256_encrypt_block() { + eprintln!("AES256Internal::encrypt_block"); + let aes = load_aes256(); + measure(|| { + let mut block = [0x11u8; 16]; + aes.encrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_aes256_decrypt_block() { + eprintln!("AES256Internal::decrypt_block"); + let aes = load_aes256(); + measure(|| { + let mut block = [0x11u8; 16]; + aes.decrypt_block(&mut block); + print!("{block:x?}"); + }); +} + +fn bench_aes256_encrypt_2blocks() { + eprintln!("AES256Internal::encrypt_2blocks"); + let aes = load_aes256(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16]]; + aes.encrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes256_decrypt_2blocks() { + eprintln!("AES256Internal::decrypt_2blocks"); + let aes = load_aes256(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16]]; + aes.decrypt_2blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes256_encrypt_4blocks() { + eprintln!("AES256Internal::encrypt_4blocks"); + let aes = load_aes256(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16], [0x33u8; 16], [0x44u8; 16]]; + aes.encrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn bench_aes256_decrypt_4blocks() { + eprintln!("AES256Internal::decrypt_4blocks"); + let aes = load_aes256(); + measure(|| { + let mut blocks = [[0x11u8; 16], [0x22u8; 16], [0x33u8; 16], [0x44u8; 16]]; + aes.decrypt_4blocks(&mut blocks); + print!("{blocks:x?}"); + }); +} + +fn main() { + print_struct_sizes() + // bench_do_nothing() + // bench_aes128_key_expansion() + // bench_aes192_key_expansion() + // bench_aes256_key_expansion() + // bench_aes128_encrypt_block() + // bench_aes128_decrypt_block() + // bench_aes128_encrypt_2blocks() + // bench_aes128_decrypt_2blocks() + // bench_aes128_encrypt_4blocks() + // bench_aes128_decrypt_4blocks() + // bench_aes192_encrypt_block() + // bench_aes192_decrypt_block() + // bench_aes192_encrypt_2blocks() + // bench_aes192_decrypt_2blocks() + // bench_aes192_encrypt_4blocks() + // bench_aes192_decrypt_4blocks() + // bench_aes256_encrypt_block() + // bench_aes256_decrypt_block() + // bench_aes256_encrypt_2blocks() + // bench_aes256_decrypt_2blocks() + // bench_aes256_encrypt_4blocks() + // bench_aes256_decrypt_4blocks() +} 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..53aebf83 --- /dev/null +++ b/mem_usage_benches/src/bench_ccm_mem_usage.rs @@ -0,0 +1,241 @@ +//! 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. +//! +//! # What it measures +//! +//! Peak stack from `ms_print`, `--heap=no --stacks=yes`, release, on x86-64, at +//! `DATA_LEN = 16384` and `AAD_LEN = 64`; every bench processes the same `DATA_LEN` bytes. +//! `bench_do_nothing`'s figure is the process's own start-up, below which nothing is visible; the +//! frame is sized to clear it by a wide margin so the comparisons are legible: + +#![allow(dead_code)] +#![allow(unused_imports)] + +use bouncycastle::aes::hazmat::{AES128Internal, AES192Internal, AES256Internal}; +use bouncycastle::cipher::modes::{Ccm, CcmDecryptor, CcmEncryptor}; +use bouncycastle::cipher::{Decrypting, Encrypting}; +use bouncycastle::core::key_material::{KeyMaterial, KeyType}; +use bouncycastle::core::traits::{ + AEADCipherDecryptor, AEADCipherEncryptor, SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; + +/// 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' frame: 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 trait paths all +/// read as "7.7 KB" and nothing can be compared. At 16 KiB every path clears that floor by a +/// wide margin. The AAD capacity is a protocol-header-sized 64 bytes; no bench sends AAD. +const DATA_LEN: usize = 16384; +const AAD_LEN: usize = 64; +const MESSAGE_LEN: usize = DATA_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_out` 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. +#[inline(never)] +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 +/// trait adapters are `Ccm` plus the `AAD_LEN` buffer and a few words, at any `DATA_LEN`. +#[inline(never)] +fn print_struct_sizes() { + use core::mem::size_of; + + eprintln!("--- Ccm: permutation + 3 blocks + 5 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 trait adapters: Ccm + AAD_LEN + bookkeeping, independent of DATA_LEN ---"); + eprintln!("CcmEncryptor<.., {AAD_LEN}, {DATA_LEN}> {:>7} B", size_of::()); + eprintln!("CcmDecryptor<.., {AAD_LEN}, {DATA_LEN}> {:>7} B", size_of::()); + eprintln!( + "CcmEncryptor<.., 64, 240> {:>7} B", + size_of::>() + ); + + print!("{}", size_of::>()); +} + +/// The direct path over the message: `Ccm` plus the caller's own buffers, and nothing else. +/// This is the baseline for `bench_streaming_encrypt`, `bench_streaming_decrypt` and +/// `bench_oneshot_encrypt_out_detached`. +#[inline(never)] +fn bench_direct_encrypt_detached() { + eprintln!("Ccm::encrypt_detached_out, {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_out(&k, &nonce, &[], plaintext, &mut ciphertext) + .unwrap(); + print!("{:x?}", &tag); +} + +/// The same message through the trait encryptor's **streaming** methods: `do_encrypt_init` +/// builds the value, `do_update_out` writes each chunk's ciphertext straight out, and the final +/// returns the tag. The caller's two arrays are the whole of the stack that scales. +#[inline(never)] +fn bench_streaming_encrypt() { + eprintln!( + "CcmEncryptor do_encrypt_init/do_update_out/do_encrypt_final_detachedtag_out, {MESSAGE_LEN} B in 1 KiB chunks" + ); + + let k = key::<16>(); + let plaintext = message(); + let plaintext = core::hint::black_box(&plaintext); + let mut ciphertext = [0u8; MESSAGE_LEN]; + let (mut enc, _nonce) = Aes128CcmEncryptor::do_encrypt_init(&k).unwrap(); + let mut written = 0; + for chunk in plaintext.chunks(1024) { + written += enc.do_encrypt_out(chunk, &mut ciphertext[written..]).unwrap(); + } + let mut last = [0u8; TAG_LEN]; + let (_, tag) = enc.do_encrypt_final_detachedtag_out(&mut last).unwrap(); + print!("{:x?}", &tag); +} + +/// The decrypting side of the same comparison, with the tag inline: the decryptor releases each +/// chunk's plaintext as it arrives into the caller's `opened` array and holds back only the tag. +/// +/// The sealed message is produced in place with the direct streaming API, so that the bench +/// holds two arrays -- `sealed` and `opened` -- like `bench_direct_encrypt_detached` does, and +/// only the streaming decrypt is under measurement. +#[inline(never)] +fn bench_streaming_decrypt() { + eprintln!( + "CcmDecryptor do_decrypt_init/do_update_out/do_decrypt_final, {MESSAGE_LEN} B in 1 KiB chunks" + ); + + let k = key::<16>(); + let nonce = [0x24u8; NONCE_LEN]; + let mut sealed = [0u8; MESSAGE_LEN + TAG_LEN]; + sealed[..MESSAGE_LEN].fill(core::hint::black_box(0xA5)); + let mut ccm = Aes128Ccm::::new(&k, &nonce, &[], MESSAGE_LEN).unwrap(); + ccm.do_encrypt(&mut sealed[..MESSAGE_LEN]).unwrap(); + let tag = ccm.do_encrypt_final().unwrap(); + sealed[MESSAGE_LEN..].copy_from_slice(&tag); + let sealed = core::hint::black_box(&sealed); + + let mut opened = [0u8; MESSAGE_LEN]; + let mut dec = Aes128CcmDecryptor::do_decrypt_init(&k, &nonce).unwrap(); + let mut written = 0; + for chunk in sealed.chunks(1024) { + written += dec.do_decrypt_out(chunk, &mut opened[written..]).unwrap(); + } + let (_, m) = dec.do_decrypt_final().unwrap(); + print!("{}", written + m); +} + +/// The trait encryptor's **one-shot**, which is the trait's own, provided over the streaming +/// adapter, so it should measure what `bench_streaming_encrypt` measures: the adapter value and +/// the DRBG the nonce is drawn from above `bench_direct_encrypt_detached`. +#[inline(never)] +fn bench_oneshot_encrypt_out_detached() { + eprintln!("CcmEncryptor::encrypt_detached_out, {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_detached_out(&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 +/// with a run-time length should use: the payload length is declared up front and encrypted in +/// place, so peak stack is the `Ccm` value plus one array. +#[inline(never)] +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(chunk).unwrap(); + } + let tag = ccm.do_encrypt_final().unwrap(); + print!("{:x?}", &tag); +} + +fn main() { + let which = std::env::args().nth(1).unwrap_or_default(); + match which.as_str() { + "nothing" => bench_do_nothing(), + "direct" => bench_direct_encrypt_detached(), + "stream_enc" => bench_streaming_encrypt(), + "stream_dec" => bench_streaming_decrypt(), + "oneshot" => bench_oneshot_encrypt_out_detached(), + "direct_stream" => bench_direct_streaming(), + _ => print_struct_sizes(), + } +} diff --git a/mem_usage_benches/bench_mldsa_mem_usage.rs b/mem_usage_benches/src/bench_mldsa_mem_usage.rs similarity index 99% rename from mem_usage_benches/bench_mldsa_mem_usage.rs rename to mem_usage_benches/src/bench_mldsa_mem_usage.rs index a57414e2..ca33d7d9 100644 --- a/mem_usage_benches/bench_mldsa_mem_usage.rs +++ b/mem_usage_benches/src/bench_mldsa_mem_usage.rs @@ -1,13 +1,17 @@ //! 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: //! -//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_mldsa_mem_usage > /dev/null +//! ```text +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_mldsa_mem_usage > /dev/null //! -//! ms_print massif.out.835000 +//! ms_print massif.out.* +//! ``` //! //! or, shoved all into one line: //! -//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_mldsa_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ```text +//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_mldsa_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ``` //! //! Make sure you build in release mode! //! diff --git a/mem_usage_benches/bench_mlkem_mem_usage.rs b/mem_usage_benches/src/bench_mlkem_mem_usage.rs similarity index 99% rename from mem_usage_benches/bench_mlkem_mem_usage.rs rename to mem_usage_benches/src/bench_mlkem_mem_usage.rs index 8a81b71d..2d2c0f2e 100644 --- a/mem_usage_benches/bench_mlkem_mem_usage.rs +++ b/mem_usage_benches/src/bench_mlkem_mem_usage.rs @@ -1,13 +1,17 @@ //! 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: //! -//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_mlkem_mem_usage > /dev/null +//! ```text +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_mlkem_mem_usage > /dev/null //! -//! ms_print massif.out.835000 +//! ms_print massif.out.* +//! ``` //! //! or, shoved all into one line: //! -//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_mlkem_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ```text +//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_mlkem_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ``` //! //! //! To measure code size, Claude suggests: @@ -873,7 +877,7 @@ fn bench_mlkem512_decaps() { /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ // let (pk, _sk) = MLKEM512::keygen_from_seed(&seed).unwrap(); - // let (_ss, ct) = MLKEM512::encaps_internal(&pk, None, [1u8; 32]); + // let (_ss, ct) = MLKEM512::encaps_with_randomness(&pk, None, [1u8; 32]); // use bouncycastle_hex as hex; // eprintln!("ct:\n{}", &hex::encode(ct)); @@ -956,7 +960,7 @@ fn bench_mlkem512_lowmemory_decaps() { /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ // let (pk, _sk) = MLKEM512::keygen_from_seed(&seed).unwrap(); - // let (_ss, ct) = MLKEM512::encaps_internal(&pk, None, [1u8; 32]); + // let (_ss, ct) = MLKEM512::encaps_with_randomness(&pk, None, [1u8; 32]); // use bouncycastle_hex as hex; // eprintln!("ct:\n{}", &hex::encode(ct)); @@ -1039,7 +1043,7 @@ fn bench_mlkem768_decaps() { /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ // let (pk, _sk) = MLKEM768::keygen_from_seed(&seed).unwrap(); - // let (_ss, ct) = MLKEM768::encaps_internal(&pk, None, [1u8; 32]); + // let (_ss, ct) = MLKEM768::encaps_with_randomness(&pk, None, [1u8; 32]); // use bouncycastle_hex as hex; // eprintln!("ct:\n{}", &hex::encode(ct)); @@ -1143,7 +1147,7 @@ fn bench_mlkem768_lowmemory_decaps() { /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ // let (pk, _sk) = MLKEM768::keygen_from_seed(&seed).unwrap(); - // let (_ss, ct) = MLKEM768::encaps_internal(&pk, None, [1u8; 32]); + // let (_ss, ct) = MLKEM768::encaps_with_randomness(&pk, None, [1u8; 32]); // use bouncycastle_hex as hex; // eprintln!("ct:\n{}", &hex::encode(ct)); @@ -1247,7 +1251,7 @@ fn bench_mlkem1024_decaps() { /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ // let (pk, _sk) = MLKEM1024::keygen_from_seed(&seed).unwrap(); - // let (_ss, ct) = MLKEM1024::encaps_internal(&pk, None, [1u8; 32]); + // let (_ss, ct) = MLKEM1024::encaps_with_randomness(&pk, None, [1u8; 32]); // use bouncycastle_hex as hex; // eprintln!("ct:\n{}", &hex::encode(ct)); @@ -1383,7 +1387,7 @@ fn bench_mlkem1024_lowmemory_decaps() { /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ // let (pk, _sk) = MLKEM1024::keygen_from_seed(&seed).unwrap(); - // let (_ss, ct) = MLKEM1024::encaps_internal(&pk, None, [1u8; 32]); + // let (_ss, ct) = MLKEM1024::encaps_with_randomness(&pk, None, [1u8; 32]); // use bouncycastle_hex as hex; // eprintln!("ct:\n{}", &hex::encode(ct)); diff --git a/mem_usage_benches/bench_sha3_mem_usage.rs b/mem_usage_benches/src/bench_sha3_mem_usage.rs similarity index 58% rename from mem_usage_benches/bench_sha3_mem_usage.rs rename to mem_usage_benches/src/bench_sha3_mem_usage.rs index e4155b61..006c0b63 100644 --- a/mem_usage_benches/bench_sha3_mem_usage.rs +++ b/mem_usage_benches/src/bench_sha3_mem_usage.rs @@ -1,13 +1,17 @@ //! 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: //! -//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_sha3_mem_usage > /dev/null +//! ```text +//! valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_sha3_mem_usage > /dev/null //! -//! ms_print massif.out.835000 +//! ms_print massif.out.* +//! ``` //! //! or, shoved all into one line: //! -//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_sha3_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ```text +//! clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_sha3_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* +//! ``` //! //! Make sure you build in release mode! //! @@ -21,9 +25,15 @@ #![allow(dead_code)] #![allow(unused_imports)] -use bouncycastle::core::traits::{Hash, Suspendable, XOF}; +use bouncycastle::core::traits::{Hash, Suspendable, XOF, XOFSqueezer}; +use bouncycastle::sha3::kmac::{KMAC128, KMAC256, KMACXOF128, KMACXOF256}; +use bouncycastle::sha3::parallelhash::{ + ParallelHash128, ParallelHash256, ParallelHashXOF128, ParallelHashXOF256, +}; +use bouncycastle::sha3::tuplehash::{TupleHash128, TupleHash256, TupleHashXOF128, TupleHashXOF256}; use bouncycastle::sha3::{ - SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN, + CSHAKE128, CSHAKE256, CSHAKESqueezer, SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, + SHAKE128Params, SHAKE256, SHAKE256Params, SHAKESqueezer, SUSPENDED_SHA3_STATE_LEN, }; /// A 1 KiB message so that the sponge is permuted several times. @@ -41,6 +51,24 @@ fn print_struct_sizes() { println!("size_of: {}", size_of::()); println!("size_of: {}", size_of::()); println!("SUSPENDED_SHA3_STATE_LEN: {}", SUSPENDED_SHA3_STATE_LEN); + println!("size_of: {}", size_of::>()); + + println!("\nSP 800-185"); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::>()); } fn bench_do_nothing() { @@ -81,9 +109,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); } @@ -91,9 +120,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/lib.rs b/mem_usage_benches/src/lib.rs similarity index 61% rename from mem_usage_benches/lib.rs rename to mem_usage_benches/src/lib.rs index a281a8b2..54d20fc5 100644 --- a/mem_usage_benches/lib.rs +++ b/mem_usage_benches/src/lib.rs @@ -1,3 +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/bench_mldsa_mem_usage.rs b/src/bench_mldsa_mem_usage.rs deleted file mode 100644 index 6d1adc14..00000000 --- a/src/bench_mldsa_mem_usage.rs +++ /dev/null @@ -1,471 +0,0 @@ -//! 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: -//! -//! > valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_mldsa_mem_usage > /dev/null -//! -//! > ms_print massif.out.835000 -//! -//! alternatively, as a one line command: -//! -//! > clear; clear; valgrind --tool=massif --heap=no --stacks=yes -- target/release/bench_mldsa_mem_usage > /dev/null; ms_print massif.out.*; rm massif.out.* -//! -//! Make sure you build in release mode! -//! -//! Note: -//! The code is using print!() to force the compiler not to optimize away the actual code. -//! It is printing important outputs for benchmarking to stderr so that the rest can be mapped to /dev/null -//! (this is because /usr/bin/time prints useful outputs to stderr as well) -//! -//! Main is at the bottom, controls which this was actually run. - -#![allow(dead_code)] -#![allow(unused_imports)] - -use bouncycastle_core_interface::key_material::{KeyMaterial256, KeyType}; -use bouncycastle_core_interface::traits::{Signature, SignaturePublicKey}; -use bouncycastle_hex as hex; -use bouncycastle_mldsa::MLDSA44PublicKey; - -/// This exists so that /usr/bin/time can be used to measure the base memory footprint of the cargo bench harness -fn bench_do_nothing() { - eprintln!("DoNothing"); - - print!("{}", 1 + 1); -} - -fn bench_mldsa44_keygen() { - use bouncycastle_mldsa::{MLDSATrait, MLDSA44}; - - eprintln!("MLDSA44/KeyGen"); - - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let (pk, _sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - println!("{:x?}", pk.encode()); -} - -fn bench_mldsa44_lowmem_keygen() { - use bouncycastle_mldsa_lowmemory::{MLDSATrait, MLDSA44}; - - eprintln!("MLDSA44_lowmemory/KeyGen"); - - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let (pk, _sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - println!("{:x?}", pk.encode()); -} - -fn bench_mldsa65_keygen() { - use bouncycastle_mldsa::{MLDSATrait, MLDSA65}; - - eprintln!("MLDSA65/KeyGen"); - - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let (pk, _sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - println!("{:x?}", pk.encode()); -} - -fn bench_mldsa65_lowmemory_keygen() { - use bouncycastle_mldsa_lowmemory::{MLDSATrait, MLDSA65}; - - eprintln!("MLDSA65_lowmemory/KeyGen"); - - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let (pk, _sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - println!("{:x?}", pk.encode()); -} - -fn bench_mldsa87_keygen() { - use bouncycastle_mldsa::{MLDSATrait, MLDSA87}; - - eprintln!("MLDSA87/KeyGen"); - - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let (pk, _sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - println!("{:x?}", pk.encode()); -} - -fn bench_mldsa87_lowmemory_keygen() { - use bouncycastle_mldsa_lowmemory::{MLDSATrait, MLDSA87}; - - eprintln!("MLDSA87_lowmemory/KeyGen"); - - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let (pk, _sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - println!("{:x?}", pk.encode()); -} - -fn bench_mldsa44_sign() { - use bouncycastle_mldsa::{MLDSATrait, MLDSA44}; - - eprintln!("MLDSA44/Sign"); - - // set up the seeds outside of the timing loop - // Doing different seeds so that the CPU doesn't cache them or do too much branch prediction - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - /*** ML-DSA-44 ***/ - // since the goal here is to measure peak memory usage; we're here making an assumption that - // mem usage of .sign will be higher than .keygen - let (_mldsa44_pk, mldsa44_sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - - let mu = MLDSA44::compute_mu_from_sk(&mldsa44_sk, msg, None).unwrap(); - let sig = MLDSA44::sign_mu_deterministic(&mldsa44_sk, &mu, [0u8; 32]).unwrap(); - print!("{:x?}", sig); -} - -fn bench_mldsa44_lowmemory_sign() { - use bouncycastle_mldsa_lowmemory::{MLDSATrait, MLDSA44}; - - eprintln!("MLDSA44_lowmemory/Sign"); - - // set up the seeds outside of the timing loop - // Doing different seeds so that the CPU doesn't cache them or do too much branch prediction - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - /*** ML-DSA-44 ***/ - let (_mldsa44_pk, mldsa44_sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - - let mu = MLDSA44::compute_mu_from_sk(&mldsa44_sk, msg, None).unwrap(); - let sig = MLDSA44::sign_mu_deterministic(&mldsa44_sk, &mu, [0u8; 32]).unwrap(); - print!("{:x?}", sig); -} - -fn bench_mldsa65_sign() { - use bouncycastle_mldsa::{MLDSATrait, MLDSA65}; - - eprintln!("MLDSA65/Sign"); - - // set up the seeds outside of the timing loop - // Doing different seeds so that the CPU doesn't cache them or do too much branch prediction - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - let (_pk, sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - - let mu = MLDSA65::compute_mu_from_sk(&sk, msg, None).unwrap(); - let sig = MLDSA65::sign_mu_deterministic(&sk, &mu, [0u8; 32]).unwrap(); - print!("{:x?}", sig); -} - -fn bench_mldsa65_lowmemory_sign() { - use bouncycastle_mldsa_lowmemory::{MLDSATrait, MLDSA65}; - - eprintln!("MLDSA65_lowmemory/Sign"); - - // set up the seeds outside of the timing loop - // Doing different seeds so that the CPU doesn't cache them or do too much branch prediction - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - /*** ML-DSA-44 ***/ - let (_mldsa44_pk, mldsa44_sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - - let mu = MLDSA65::compute_mu_from_sk(&mldsa44_sk, msg, None).unwrap(); - let sig = MLDSA65::sign_mu_deterministic(&mldsa44_sk, &mu, [0u8; 32]).unwrap(); - print!("{:x?}", sig); -} - -fn bench_mldsa87_sign() { - use bouncycastle_mldsa::{MLDSATrait, MLDSA87}; - - eprintln!("MLDSA87/Sign"); - - // set up the seeds outside of the timing loop - // Doing different seeds so that the CPU doesn't cache them or do too much branch prediction - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - let (_pk, sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - - let mu = MLDSA87::compute_mu_from_sk(&sk, msg, None).unwrap(); - let sig = MLDSA87::sign_mu_deterministic(&sk, &mu, [0u8; 32]).unwrap(); - print!("{:x?}", sig); -} - -fn bench_mldsa87_lowmemory_sign() { - use bouncycastle_mldsa_lowmemory::{MLDSATrait, MLDSA87}; - - eprintln!("MLDSA87_lowmemory/Sign"); - - // set up the seeds outside of the timing loop - // Doing different seeds so that the CPU doesn't cache them or do too much branch prediction - let seed = KeyMaterial256::from_bytes_as_type( - &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - KeyType::Seed, - ).unwrap(); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - /*** ML-DSA-44 ***/ - let (_mldsa44_pk, mldsa44_sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - - let mu = MLDSA87::compute_mu_from_sk(&mldsa44_sk, msg, None).unwrap(); - let sig = MLDSA87::sign_mu_deterministic(&mldsa44_sk, &mu, [0u8; 32]).unwrap(); - print!("{:x?}", sig); -} - -fn bench_mldsa44_verify() { - use bouncycastle_mldsa::{MLDSATrait, MLDSA44, MLDSA44_SIG_LEN, MLDSA44PublicKey}; - use bouncycastle_hex as hex; - - eprintln!("MLDSA44/Verify"); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ - // let seed = KeyMaterial256::from_bytes_as_type( - // &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - // KeyType::Seed, - // ).unwrap(); - // - // let (mldsa44_pk, _mldsa44_sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - - // eprintln!("pk:\n{}", &*hex::encode(&mldsa44_pk.encode())); - // let mu = MLDSA44::compute_mu_from_sk(&mldsa44_sk, msg, None).unwrap(); - // let sig = MLDSA44::sign_mu_deterministic(&mldsa44_sk, &mu, [0u8; 32]).unwrap(); - // eprintln!("sig:\n{}", &*hex::encode(sig)); - - let mldsa44_pk = MLDSA44PublicKey::from_bytes(&*hex::decode("d7b2b47254aae0db45e7930d4a98d2c97d8f1397d1789dafa17024b316e9bec94fc9946d42f19b79a7413bbaa33e7149cb42ed5115693ac041facb988adeb5fe0e1d8631184995b592c397d2294e2e14f90aa414ba3826899ac43f4cccacbc26e9a832b95118d5cb433cbef9660b00138e0817f61e762ca274c36ad554eb22aac1162e4ab01acba1e38c4efd8f80b65b333d0f72e55dfe71ce9c1ebb9889e7c56106c0fd73803a2aecfeafded7aa3cb2ceda54d12bd8cd36a78cf975943b47abd25e880ac452e5742ed1e8d1a82afa86e590c758c15ae4d2840d92bca1a5090f40496597fca7d8b9513f1a1bda6e950aaa98de467507d4a4f5a4f0599216582c3572f62eda8905ab3581670c4a02777a33e0ca7295fd8f4ff6d1a0a3a7683d65f5f5f7fc60da023e826c5f92144c02f7d1ba1075987553ea9367fcd76d990b7fa99cd45afdb8836d43e459f5187df058479709a01ea6835935fa70460990cd3dc1ba401ba94bab1dde41ac67ab3319dcaca06048d4c4eef27ee13a9c17d0538f430f2d642dc2415660de78877d8d8abc72523978c042e4285f4319846c44126242976844c10e556ba215b5a719e59d0c6b2a96d39859071fdcc2cde7524a7bedae54e85b318e854e8fe2b2f3edfac9719128270aafd1e5044c3a4fdafd9ff31f90784b8e8e4596144a0daf586511d3d9962b9ea95af197b4e5fc60f2b1ed15de3a5bef5f89bdc79d91051d9b2816e74fa54531efdc1cbe74d448857f476bcd58f21c0b653b3b76a4e076a6559a302718555cc63f74859aabab925f023861ca8cd0f7badb2871f67d55326d7451135ad45f4a1ba69118fbb2c8a30eec9392ef3f977066c9add5c710cc647b1514d217d958c7017c3e90fd20c04e674b90486e9370a31a001d32f473979e4906749e7e477fa0b74508f8a5f2378312b83c25bd388ca0b0fff7478baf42b71667edaac97c46b129643e586e5b055a0c211946d4f36e675bed5860fa042a315d9826164d6a9237c35a5fbf495490a5bd4df248b95c4aae7784b605673166ac4245b5b4b082a09e9323e62f2078c5b76783446defd736ad3a3702d49b089844900a61833397bc4419b30d7a97a0b387c1911474c4d41b53e32a977acb6f0ea75db65bb39e59e701e76957def6f2d44559c31a77122b5204e3b5c219f1688b14ed0bc0b801b3e6e82dcd43e9c0e9f41744cd9815bd1bc8820d8bb123f04facd1b1b685dd5a2b1b8dbbf3ed933670f095a180b4f192d08b10b8fabbdfcc2b24518e32eea0a5e0c904ca844780083f3b0cd2d0b8b6af67bc355b9494025dc7b0a78fa80e3a2dbfeb51328851d6078198e9493651ae787ec0251f922ba30e9f51df62a6d72784cf3dd205393176dfa324a512bd94970a36dd34a514a86791f0eb36f0145b09ab64651b4a0313b299611a2a1c48891627598768a3114060ba4443486df51522a1ce88b30985c216f8e6ed178dd567b304a0d4cafba882a28342f17a9aa26ae58db630083d2c358fdf566c3f5d62a428567bc9ea8ce95caa0f35474b0bfa8f339a250ab4dfcf2083be8eefbc1055e18fe15370eecb260566d83ff06b211aaec43ca29b54ccd00f8815a2465ef0b46515cc7e41f3124f09efff739309ab58b29a1459a00bce5038e938c9678f72eb0e4ee5fdaae66d9f8573fc97fc42b4959f4bf8b61d78433e86b0335d6e9191c4d8bf487b3905c108cfd6ac24b0ceb7dcb7cf51f84d0ed687b95eaeb1c533c06f0d97023d92a70825837b59ba6cb7d4e56b0a87c203862ae8f315ba5925e8edefa679369a2202766151f16a965f9f81ece76cc070b55869e4db9784cf05c830b3242c8312").unwrap()).unwrap(); - let sig = &*hex::decode("5e93b785c5119c3983a291b18420fdbe4bca53d5a3732922faaacd5a5d32a745c78d105ba10bee1ed8069f19e6c537bda16e89d39004c359d1fd381a0291f1c51f1c38edcdb315c8c69570d8f25f1655ba8ea83aff24b8b6be8de762342e347eab2caa6803ed705952dd6450c5185e9d60ce96e8dca423a02f646cea690164a226e4c3d6a515ce16290f19b2c626da9b450ecf665013c5e226b6c0ac5c07ce90e278f1b0134e385d13e74208a0b3ff052a362579f9207ea01f18a039aa1b97ae3452675b620771f8012ee7a4e55c98bfd2019ed8a3b00acea8e8ab28172faa42ca1fda83c5ffe81a45be736bdedd5fb300ce17078b380f620bdeebad693601372c85eacf79bc98e1b48f2ad7e5dce4279a1295bb2ba60a0c5e3726642d2336c5eb1d37c8623c7558241318d89bc783c4f00098077484623c217560a0c7aaf75dcaccb78ee69c207c27c8bf3965ccf58a80c88efcc7e5deb3615d5045a741c4dac0a021dd060d315d4ec2857eb664d728d0af973bea07e1ca563faa0e19996cea3770316c11a5066665662005ace98f6110e883bae060daa7b6d83379e0878796691708a32b85730de8b92d89f90a3660c949165b14612567662e162232296cbd143517a282e22c46b63606d3c14ed4559a5a1c459bab7f355007ad6f7e3b1e07445dfc96bd9b75080b3d4f68998490a26b5e090be2674071ab925bb650590856c59f8ba7488d2b72f840ac3eafe4dd91f0f51c4364112c1a139e3e942a597b93a1e3f4faded129c14b5978b315e2246a93146a79365f0f597a18340cca86bb15ceed39f175eab1e546535afb966f0a65a8f66f737ab02897eddfe92cf7786894843c2691464776c94bd450a1069138b26df83b2d1dd801143a8fdfdc2514cc5b5831ab53a75c55ef29f40e7c63d2c72abe97e2af14853be49be16f4730a159974970951439e55c1589d0f4a162e3517df9d7abc98d8a307216e7f1cb4627c9175c0eef23337e56d5281b83726fff40a148b0c48e8df3496a2118d80219aef8f40b29fba1f2f78786b67ffb7b7d47d406b765bd136610bedeb95cd7321f58f3b836c9258be35d78b498f3efe1db2b243d734fab159baed8807c3cccf83eb2eaf8a9af01a518d48c60e91a96812ad689c2d83cc4e8e9b3650422bed6f13c24adaad91c95b3e3cf354f0f6bc9ee8941a6b15b6975131d95233d8935de367efc6d86a45dac7d0f1ddd9aebd2c59c027fcda448801e93e733aca51874be9ab927a904f96ddb7a46b2da13261d522b23c950c01d5f5e112b76f851ff234f06f8d5e65b1319abcd79a180ae063d65b28c745878c06dbb69ba73293eab34434bf1a92fba691993bd0ff3edac76a12f80c0ada4b1969c7665589d530a67016a625403c537032904f2e104547cd3ea406260dd357fa06ea012a785826c160e99ffd065b0e3f33c7689d3552ab9e2e09fa7e55bbcef042242bcacad8a3da47bcc54a121f1526c8cd4cc5a892a8131cf4eefaf4248ddd6a11ec427ba378aae89aaf582ce1f4e32690a555e740761d358ad4e92bc38418aa782da916524fb09ab2ca6b3d3113d6f2c2a6a9b9d29d4e7489255252af075cbf9feacedae6f3ec0b070824689dd3c78ac143ed6776d95dd8f13d435a290bdca4c11318e5acce04469644e1374a9451b6204f3b3961b7dd239e306fef5f4f4e51b78b0fb9dcee69c3e790b231f2e65fd1ab1c2a75b07067d5c16dde00983a58ffcdaaaee16d2742e133ed737b48064c8a38eca35ab3fa18f6d62f642b12cfdc7980f2ab7db321fec9dcfe499b4fc1ee7eb297954056617c60a6640b92835d165c3c00a951952614488d5657ba0b5e90ae9e0ef7b3b9ecaebd81b8551b6d70e835b2734761639d42e76ffc5b3272b61c896b45b4bd18f30e58c440643ba159221cc6739a19a65f2911fae47b0d4cac4200a6f043b17a03ad393ecb823ed03c8b6cd68167e6c8234f7432557db272079ee899aede73b6b98d6003f45789a141b60d6db40cd2a5974571a4ad3667b889318ba60285d903a2eac01c21608838c40907de6bbabe042cf2ecdd97f549f95ec698d79222c65ba27c30d332a68d057aecdc9388aa34320e0aa74fdbd4d1b643cace216b6d8ad8f07a99955bfdb743a86b40fc61527baca434ac2a7fbeaa77111dc8098b17e800f59dd77ccb0e67707e60123d334e073a2f5a16ffbcd701389add57c3ceccb88b286ac1e6e3e6485af1a12ea241d14a1b5003d7f3bc9e957d4483c0f9f703b3a187d55e505817615fbc4ae0837616184245cfba61ce3b929e33f52b71cdd7b6a0da55c1f997510b1a9002ca4e0678373a3b1ab2897e6b423f15a440a636cc861491ef41ad0aa627d8e198a5ee7bd7b6cb2c9ce2a8cc015f0d206de4c49e2f87f310954a10d86e294f742ee186f4ae9815f699622792206cafba8f5621738160e6c5d611a8252c6f35085b604ef895164d4ea6ddd310c7d8f0c879fb1f884c5741d096b3d2da0ce1151790dda881d18cb6b19a9fed6f5254b7d52d5d92bbbe24c9d6a65604a0b8ed24ad5c197d683f598743c96b5960e8723732b5bd647e9dbeaa851d0e1cf6d2c070d4442762c28098c5cf5a54b2b5e69a99b10815bf0f477bb71f0d5d3a62ba2b3e29bf84d4b4e574707f5f74af704d277bd6ca38da21e2cdac549e5eae1de7a18ee534c8c2291c908caabf159e90e6549db94ba7a3f3d97dd398a75df5b1a7cdfb25410b7efc4ed00d9995b37b58bf91ed7a3510cffea82f9e1c2a3290406004d09057d63b770fa0e53103199544eba662a2c302cf39008f142d2b16963e95ab10be7c2610168608f353a2f2c41c7056dec1a8c7a6bfa0027f9dedacb7786b67ea2c494d43ba851cf9415c1bcc52f027ec02c65534f608e9d166d51dd431cdf5871f5cdd1579cc06079df075a25062ba7e70d9666c4e7fed34cea0ea0f11ade1eb2a9b397bcaaad1061270ecf497803a5fce7f41e6504fbec71a7de7d066b8261868afc49b9e685f0dcce75e2fcb3ba8cf19057e3941576baf58fb821bd4268f7fae3028601da022e9b468646abdb4fa6098a449b4267d509d9a33f4c3ebcc32dac094d48ed600e765787fb92b1974f74f7bb4c66eb2bbd02895e6a381c1c452eaab1ae4731cf632f61ae2c905921174a3bc9bb4cdc89d630264b614988f3abbea1bd617ffa53d71b7d8a371462b773351a2dccaedd7f59cd728fadee059067bd80c94c8c9a1ffca2dc4f848b829c0561385aa82cc98503d0bb66a6aa4fae0703d12e60e1460efbbcdf2412c13e7c684d1b01102026343a414344585f6e7072748baeb5bbc6d1e2effbfe060e2e3e5160797c9ea6bac7f11024404a52575f6c898c97aab2c3cceaf22f3f535f7b818396a1b1bce6000000000000000000000000000018253642").unwrap(); - assert_eq!(sig.len(), MLDSA44_SIG_LEN); - - if MLDSA44::verify(&mldsa44_pk, msg, None, &sig).is_ok() { - eprintln!("Verification succeeded!"); - } else { - panic!("Verification failed! -- figure that out"); - } -} - -fn bench_mldsa44_lowmemory_verify() { - use bouncycastle_mldsa_lowmemory::{MLDSATrait, MLDSA44, MLDSA44_SIG_LEN, MLDSA44PublicKey}; - use bouncycastle_hex as hex; - - eprintln!("MLDSA44_lowmemory/Verify"); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ - // let seed = KeyMaterial256::from_bytes_as_type( - // &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - // KeyType::Seed, - // ).unwrap(); - // - // let (mldsa44_pk, _mldsa44_sk) = MLDSA44::keygen_from_seed(&seed).unwrap(); - - // eprintln!("pk:\n{}", &*hex::encode(&mldsa44_pk.encode())); - // let mu = MLDSA44::compute_mu_from_sk(&mldsa44_sk, msg, None).unwrap(); - // let sig = MLDSA44::sign_mu_deterministic(&mldsa44_sk, &mu, [0u8; 32]).unwrap(); - // eprintln!("sig:\n{}", &*hex::encode(sig)); - - let mldsa44_pk = MLDSA44PublicKey::from_bytes(&*hex::decode("d7b2b47254aae0db45e7930d4a98d2c97d8f1397d1789dafa17024b316e9bec94fc9946d42f19b79a7413bbaa33e7149cb42ed5115693ac041facb988adeb5fe0e1d8631184995b592c397d2294e2e14f90aa414ba3826899ac43f4cccacbc26e9a832b95118d5cb433cbef9660b00138e0817f61e762ca274c36ad554eb22aac1162e4ab01acba1e38c4efd8f80b65b333d0f72e55dfe71ce9c1ebb9889e7c56106c0fd73803a2aecfeafded7aa3cb2ceda54d12bd8cd36a78cf975943b47abd25e880ac452e5742ed1e8d1a82afa86e590c758c15ae4d2840d92bca1a5090f40496597fca7d8b9513f1a1bda6e950aaa98de467507d4a4f5a4f0599216582c3572f62eda8905ab3581670c4a02777a33e0ca7295fd8f4ff6d1a0a3a7683d65f5f5f7fc60da023e826c5f92144c02f7d1ba1075987553ea9367fcd76d990b7fa99cd45afdb8836d43e459f5187df058479709a01ea6835935fa70460990cd3dc1ba401ba94bab1dde41ac67ab3319dcaca06048d4c4eef27ee13a9c17d0538f430f2d642dc2415660de78877d8d8abc72523978c042e4285f4319846c44126242976844c10e556ba215b5a719e59d0c6b2a96d39859071fdcc2cde7524a7bedae54e85b318e854e8fe2b2f3edfac9719128270aafd1e5044c3a4fdafd9ff31f90784b8e8e4596144a0daf586511d3d9962b9ea95af197b4e5fc60f2b1ed15de3a5bef5f89bdc79d91051d9b2816e74fa54531efdc1cbe74d448857f476bcd58f21c0b653b3b76a4e076a6559a302718555cc63f74859aabab925f023861ca8cd0f7badb2871f67d55326d7451135ad45f4a1ba69118fbb2c8a30eec9392ef3f977066c9add5c710cc647b1514d217d958c7017c3e90fd20c04e674b90486e9370a31a001d32f473979e4906749e7e477fa0b74508f8a5f2378312b83c25bd388ca0b0fff7478baf42b71667edaac97c46b129643e586e5b055a0c211946d4f36e675bed5860fa042a315d9826164d6a9237c35a5fbf495490a5bd4df248b95c4aae7784b605673166ac4245b5b4b082a09e9323e62f2078c5b76783446defd736ad3a3702d49b089844900a61833397bc4419b30d7a97a0b387c1911474c4d41b53e32a977acb6f0ea75db65bb39e59e701e76957def6f2d44559c31a77122b5204e3b5c219f1688b14ed0bc0b801b3e6e82dcd43e9c0e9f41744cd9815bd1bc8820d8bb123f04facd1b1b685dd5a2b1b8dbbf3ed933670f095a180b4f192d08b10b8fabbdfcc2b24518e32eea0a5e0c904ca844780083f3b0cd2d0b8b6af67bc355b9494025dc7b0a78fa80e3a2dbfeb51328851d6078198e9493651ae787ec0251f922ba30e9f51df62a6d72784cf3dd205393176dfa324a512bd94970a36dd34a514a86791f0eb36f0145b09ab64651b4a0313b299611a2a1c48891627598768a3114060ba4443486df51522a1ce88b30985c216f8e6ed178dd567b304a0d4cafba882a28342f17a9aa26ae58db630083d2c358fdf566c3f5d62a428567bc9ea8ce95caa0f35474b0bfa8f339a250ab4dfcf2083be8eefbc1055e18fe15370eecb260566d83ff06b211aaec43ca29b54ccd00f8815a2465ef0b46515cc7e41f3124f09efff739309ab58b29a1459a00bce5038e938c9678f72eb0e4ee5fdaae66d9f8573fc97fc42b4959f4bf8b61d78433e86b0335d6e9191c4d8bf487b3905c108cfd6ac24b0ceb7dcb7cf51f84d0ed687b95eaeb1c533c06f0d97023d92a70825837b59ba6cb7d4e56b0a87c203862ae8f315ba5925e8edefa679369a2202766151f16a965f9f81ece76cc070b55869e4db9784cf05c830b3242c8312").unwrap()).unwrap(); - let sig = &*hex::decode("5e93b785c5119c3983a291b18420fdbe4bca53d5a3732922faaacd5a5d32a745c78d105ba10bee1ed8069f19e6c537bda16e89d39004c359d1fd381a0291f1c51f1c38edcdb315c8c69570d8f25f1655ba8ea83aff24b8b6be8de762342e347eab2caa6803ed705952dd6450c5185e9d60ce96e8dca423a02f646cea690164a226e4c3d6a515ce16290f19b2c626da9b450ecf665013c5e226b6c0ac5c07ce90e278f1b0134e385d13e74208a0b3ff052a362579f9207ea01f18a039aa1b97ae3452675b620771f8012ee7a4e55c98bfd2019ed8a3b00acea8e8ab28172faa42ca1fda83c5ffe81a45be736bdedd5fb300ce17078b380f620bdeebad693601372c85eacf79bc98e1b48f2ad7e5dce4279a1295bb2ba60a0c5e3726642d2336c5eb1d37c8623c7558241318d89bc783c4f00098077484623c217560a0c7aaf75dcaccb78ee69c207c27c8bf3965ccf58a80c88efcc7e5deb3615d5045a741c4dac0a021dd060d315d4ec2857eb664d728d0af973bea07e1ca563faa0e19996cea3770316c11a5066665662005ace98f6110e883bae060daa7b6d83379e0878796691708a32b85730de8b92d89f90a3660c949165b14612567662e162232296cbd143517a282e22c46b63606d3c14ed4559a5a1c459bab7f355007ad6f7e3b1e07445dfc96bd9b75080b3d4f68998490a26b5e090be2674071ab925bb650590856c59f8ba7488d2b72f840ac3eafe4dd91f0f51c4364112c1a139e3e942a597b93a1e3f4faded129c14b5978b315e2246a93146a79365f0f597a18340cca86bb15ceed39f175eab1e546535afb966f0a65a8f66f737ab02897eddfe92cf7786894843c2691464776c94bd450a1069138b26df83b2d1dd801143a8fdfdc2514cc5b5831ab53a75c55ef29f40e7c63d2c72abe97e2af14853be49be16f4730a159974970951439e55c1589d0f4a162e3517df9d7abc98d8a307216e7f1cb4627c9175c0eef23337e56d5281b83726fff40a148b0c48e8df3496a2118d80219aef8f40b29fba1f2f78786b67ffb7b7d47d406b765bd136610bedeb95cd7321f58f3b836c9258be35d78b498f3efe1db2b243d734fab159baed8807c3cccf83eb2eaf8a9af01a518d48c60e91a96812ad689c2d83cc4e8e9b3650422bed6f13c24adaad91c95b3e3cf354f0f6bc9ee8941a6b15b6975131d95233d8935de367efc6d86a45dac7d0f1ddd9aebd2c59c027fcda448801e93e733aca51874be9ab927a904f96ddb7a46b2da13261d522b23c950c01d5f5e112b76f851ff234f06f8d5e65b1319abcd79a180ae063d65b28c745878c06dbb69ba73293eab34434bf1a92fba691993bd0ff3edac76a12f80c0ada4b1969c7665589d530a67016a625403c537032904f2e104547cd3ea406260dd357fa06ea012a785826c160e99ffd065b0e3f33c7689d3552ab9e2e09fa7e55bbcef042242bcacad8a3da47bcc54a121f1526c8cd4cc5a892a8131cf4eefaf4248ddd6a11ec427ba378aae89aaf582ce1f4e32690a555e740761d358ad4e92bc38418aa782da916524fb09ab2ca6b3d3113d6f2c2a6a9b9d29d4e7489255252af075cbf9feacedae6f3ec0b070824689dd3c78ac143ed6776d95dd8f13d435a290bdca4c11318e5acce04469644e1374a9451b6204f3b3961b7dd239e306fef5f4f4e51b78b0fb9dcee69c3e790b231f2e65fd1ab1c2a75b07067d5c16dde00983a58ffcdaaaee16d2742e133ed737b48064c8a38eca35ab3fa18f6d62f642b12cfdc7980f2ab7db321fec9dcfe499b4fc1ee7eb297954056617c60a6640b92835d165c3c00a951952614488d5657ba0b5e90ae9e0ef7b3b9ecaebd81b8551b6d70e835b2734761639d42e76ffc5b3272b61c896b45b4bd18f30e58c440643ba159221cc6739a19a65f2911fae47b0d4cac4200a6f043b17a03ad393ecb823ed03c8b6cd68167e6c8234f7432557db272079ee899aede73b6b98d6003f45789a141b60d6db40cd2a5974571a4ad3667b889318ba60285d903a2eac01c21608838c40907de6bbabe042cf2ecdd97f549f95ec698d79222c65ba27c30d332a68d057aecdc9388aa34320e0aa74fdbd4d1b643cace216b6d8ad8f07a99955bfdb743a86b40fc61527baca434ac2a7fbeaa77111dc8098b17e800f59dd77ccb0e67707e60123d334e073a2f5a16ffbcd701389add57c3ceccb88b286ac1e6e3e6485af1a12ea241d14a1b5003d7f3bc9e957d4483c0f9f703b3a187d55e505817615fbc4ae0837616184245cfba61ce3b929e33f52b71cdd7b6a0da55c1f997510b1a9002ca4e0678373a3b1ab2897e6b423f15a440a636cc861491ef41ad0aa627d8e198a5ee7bd7b6cb2c9ce2a8cc015f0d206de4c49e2f87f310954a10d86e294f742ee186f4ae9815f699622792206cafba8f5621738160e6c5d611a8252c6f35085b604ef895164d4ea6ddd310c7d8f0c879fb1f884c5741d096b3d2da0ce1151790dda881d18cb6b19a9fed6f5254b7d52d5d92bbbe24c9d6a65604a0b8ed24ad5c197d683f598743c96b5960e8723732b5bd647e9dbeaa851d0e1cf6d2c070d4442762c28098c5cf5a54b2b5e69a99b10815bf0f477bb71f0d5d3a62ba2b3e29bf84d4b4e574707f5f74af704d277bd6ca38da21e2cdac549e5eae1de7a18ee534c8c2291c908caabf159e90e6549db94ba7a3f3d97dd398a75df5b1a7cdfb25410b7efc4ed00d9995b37b58bf91ed7a3510cffea82f9e1c2a3290406004d09057d63b770fa0e53103199544eba662a2c302cf39008f142d2b16963e95ab10be7c2610168608f353a2f2c41c7056dec1a8c7a6bfa0027f9dedacb7786b67ea2c494d43ba851cf9415c1bcc52f027ec02c65534f608e9d166d51dd431cdf5871f5cdd1579cc06079df075a25062ba7e70d9666c4e7fed34cea0ea0f11ade1eb2a9b397bcaaad1061270ecf497803a5fce7f41e6504fbec71a7de7d066b8261868afc49b9e685f0dcce75e2fcb3ba8cf19057e3941576baf58fb821bd4268f7fae3028601da022e9b468646abdb4fa6098a449b4267d509d9a33f4c3ebcc32dac094d48ed600e765787fb92b1974f74f7bb4c66eb2bbd02895e6a381c1c452eaab1ae4731cf632f61ae2c905921174a3bc9bb4cdc89d630264b614988f3abbea1bd617ffa53d71b7d8a371462b773351a2dccaedd7f59cd728fadee059067bd80c94c8c9a1ffca2dc4f848b829c0561385aa82cc98503d0bb66a6aa4fae0703d12e60e1460efbbcdf2412c13e7c684d1b01102026343a414344585f6e7072748baeb5bbc6d1e2effbfe060e2e3e5160797c9ea6bac7f11024404a52575f6c898c97aab2c3cceaf22f3f535f7b818396a1b1bce6000000000000000000000000000018253642").unwrap(); - assert_eq!(sig.len(), MLDSA44_SIG_LEN); - - if MLDSA44::verify(&mldsa44_pk, msg, None, &sig).is_ok() { - eprintln!("Verification succeeded!"); - } else { - panic!("Verification failed! -- figure that out"); - } -} - -fn bench_mldsa65_verify() { - use bouncycastle_mldsa::{MLDSATrait, MLDSA65, MLDSA65_SIG_LEN, MLDSA65PublicKey}; - use bouncycastle_hex as hex; - - eprintln!("MLDSA65/Verify"); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ - - - // let seed = KeyMaterial256::from_bytes_as_type( - // &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - // KeyType::Seed, - // ).unwrap(); - // - // let (mldsa65_pk, mldsa65_sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - // - // eprintln!("pk:\n{}", &*hex::encode(&mldsa65_pk.encode())); - // let mu = MLDSA65::compute_mu_from_sk(&mldsa65_sk, msg, None).unwrap(); - // let sig = MLDSA65::sign_mu_deterministic(&mldsa65_sk, &mu, [0u8; 32]).unwrap(); - // eprintln!("sig:\n{}", &*hex::encode(sig)); - - let mldsa65_pk = MLDSA65PublicKey::from_bytes(&*hex::decode("48683d91978e31eb3dddb8b0473482d2b88a5f625949fd8f58a561e696bd4c27d05b38dbb2edf01e664efd81be1ea893688ce68aa2d51c5958f8bbc6eb4e89ee67d2c0320954d57212cac7229ff1d6eaf03928bd51511f8d88d847736c7de2730d5978e5410713160978867711bf5539a0bfc4c350c2be572baf0ee2e2fb16ccfea08028d99ac49aebb75937ddce111cdab62fff3cea8ba2233d1e56fbc5c5a1e726de63fadd2af016b119177fa3d971a2d9277173fce55b67745af0b7c21d597dbeb93e6a32f341c49a5a8be9e825088d1f2aa45155d6c8ae15367e4eb003b8fdf7851071949739f9fff09023eaf45104d2a84a45906eed4671a44dc28d27987bb55df69e9e8561f61a80a72699503865fed9b7ee72a8e17a19c408144f4b29afef7031c3a6d8571610b42c9f421245a88f197e16812b031159b65b9687e5b3e934c5225ae98a79ba73d2b399d73510effad19e53b8450f0ba8fce1012fd98d260a74aaaa13fae249a006b1c34f5ba0b882f26378222fb36f2283c243f0ffeb5f1bb414a0a70d55e3d40a56b6cbc88ae1f03b7b2882d98deea28e145c9dedfd8eaf1cef2ed94a8b050f8964f46d1ea0d0c2a43e0dda6182adbf4f6ed175b6742257859bf22f3a417ecf1f9d89317b5e539d587af16b9e1313e04514ffa64ba8b3ff2b8321f8811cb3fb022c8f644e70a4b80a2fbfee604abb7379091ea8e6c5c74dfc0283666b40c0793870028204a136bf5da9568eb798d349038bdb0c11e03445e7847cb5069c75cf28ac601c7799d958210ddbcb226e51afef9f1de47b073873d6d3f97456bede085082e74a298b2cd48f4b3093155f366c8fa601c6af858dfa32c08491b2a29887f90335949a5d6edaa679882a3a95d6bf6d970a221f4b9d3d8cbf384af81aac95e2b3294e04789ac83727a5dc04559f96af41d8a053516feeeebc52746eb6ab2819e09108710d835f011fa63065872ad334d5cdffb2b2310507e92fc993ae317da97f4f309cdaf0f67ed99d90215576083849f953b246d7fedb3fdb67679850a5ad404e64147fb7cf4f6aeddd05afb4b834968d1fe88014960dce5d942236526e12a478d69e5fbe6970310b308c06845018cfc7b2ab430a13a6b1ac7bb02cccbb3d911ac2f11068613fbe029bfdce02cf5cd38950ed72c83944edfbc75615af87f864c051f3c55456c5412863a40c06d1dab562bdff0571b8d3c3917bbd300880bba5e998239b95fa91b7d6416d4f398b3adbcd30983ed3592b4d9ef7d4236fd00f50d98aa53a235ac4172720f77d96172672980cfe8ff7a5a702783edc2ba31b2259015a112fc7f468a9c2f9464039002d30ef678b4cb798bc116216bf7a9a7c18ba03b7b58fd07515d3115049d3614be7a07e744300750df1d2c58753389059eafc3d785ccdd31c07648bedc03a5c3b8ad46d064d59c13d57374729fc4e295362e2a5191204530428bc1522afa28ff5fe1655e304ca5bc8c27ad0e0c6a39dd4df28956c14b38cc93682cefe402bbd5e82d29c464e44eb5d37b48fc568dfe0cc6e8e16baea05e5135590f19294e73e8367b0216dbb815030b9de55913f08039c42351c59e5515dd5af8e089a15e625e8f6dee639386c46497d7a263288774de581a7de9629b41b4424141f978fb8331208efdec3c6e0de39bc57063f3dcd6c470373c08891ea29cbc7cc6d6483b8889083ace86aa7b51b1c2cfe6e2ad18d97ce36fbc56ea42fae97e6a7ac114864478c366df1ebb1e7b11a9098504fd5975bdf1f49dc70002b63c1739a9d263fbad4073f6a9f6c2b8af4b4c332a103a0cffa5deeb2d062ca3c215fd360026be7c5164f4a4424ef74948804d66f46487732c8202c795478647b4ea71d627c086024cca354a41f0877b38f19b3774ad2095c8da53b069e21c76ae2d2007e16719ed40080d334f7da52e9f5a5990439caf083a95b833f02ad10a08c1a6d0f260c007285bd4a2f47703a5aef465287d253b18ac22514316210ff566814b10f87a293d6f199d3c3959990d0c1268b4f50d5f9fcefbbf237bd0c28b80182d6659741f14f10bfbb21bba12ab620aa2396f56c0686b4ea9017990224216b2fe8ad76c4a9148eef9a86a3635a6aa77bc1dcfb6fba59a77dfda9b7530dc0ca8648c8d973738e01bab8f08b4905e84aa4641bd602410cd97520265f2f231f2b35e15eb2fa04d2bd94d5a77abaf1e0e161010a990087f5b46ea988b2bc0512fda0fa923dadd6c45c5301d09483673265b5ab2e10f4ba520f6bbad564a5c3d5e27bdb080f7d20e13296a3181954c39c649c943ebe17df5c1f7aae0a8fe126c477585a5d4d648a0d008b6af5e8cd31be69a9296d4f3fd25ed86f221e4b93f65f5929967533624b9235750c30707550b58536d109a7131c5a5bbe4a5715567c12534aec7660761eebb9fae2891c774589b80e566ad557ddef7367196b7227ea9870ef09ddfec79d6b9319a6879b5205d76bf7aba5acf33afb59d17fc54e68383d6be5a08e9b66da53dcde008bb294b8582bd132cdcc49959fdbc21e52721880c8ad0352c79f03a43bbd84c4cdfdc6c529005e1e7cd9a349a7168a35569ba5dea818968d5a91466bd6e64e20bf62417198afc4e81c28dd77ed4028232398b52fbde86bc84f475b9016710ce2aabc11a06b4dbac901ec16cf365ca3f2d53813948a693a0f93e79c46ca5d5a6dca3d28ca50ad18bd13fca55059dd9b185f79f9c47196a4e81b2104bc460a051e02f2e8444f").unwrap()).unwrap(); - let sig = &*hex::decode("9061f15cbf2092f744fbcd799eb02414053c1b0f7412124bedc41cf9a3db0166469e874037d7f081e5f8d3d2033a0307d1c49ed01fe64578c4a6fabd80880cdf1911848f184d4bcf536ca795a0fb1aa19ab7ee3ba6b58bd64bbeac9f58650fff1ef5a97ab6916df962072e20e7c6be96090e3a781a504bc4442bd8889a0aa628907a74299f39fa836031f1bd68355bebe7ae93c1e361a9efbed1325d96227070461fcd6f151b8669d9229b977d9ee51fd2260c3e4a2e820416f9e074958dc3b3e2217e6312b7e0b582a048981cf6579f4bc7715b78c808e4c57e3b8aa38b05c04fcedf209f52c1e331ae83dbdff60ba450a17cc397568e54bc3f16ddf30b92747ce460d925b9be20a1d35e2aed97f124af2616a5361df28ba30e522dd08fa00fd28d1ac484d756a89e3a442fefe8332c56cd2a9fde691bdbda43f1cc54cef57bead96120b50c7d4695bdbb1303cc5ddda898e4eeb83083176e40e0232cdd1c3150371df05d6fdad7e1164d90393cf308e99edfeb31fed263e2866ee3b7f3937b399c974d87ba7b489efe3c9b80371d2928446adc31991ab0cefaaa080575b9ec81cfa133a9911c035a8058d0d3f2e34de4a9fb009bb4ccdb16de7b908574a7496725ff857556c1b33917e986c80f1014a9e3083add2fb35f345c5d06159e443329d0da099987b996c3731592b460c2ffd2955f7546f4216100ba43188803ff9b36969685f909fa2539323b8c8ec1c095a5085e554dd450e0e67ab670b6a11ebf6c25520fc13e364060f91f9b7f3d5cb48ff28b8fc83d4293f1f35ad6ff6ae4574ad7a1c6005fc0389a7b21386b0850a05d832fe6a14bb2b1db1f8e20bd09174946cd098b81c8f797e95f2143a949770cf1219bfef039db51a80fc247f65f41554c7173dd805ba82fdf47ab6d4bfd37dfe46fc47904421ae00dc005a22f9c4784b0ea9e665392a412245016d5c6d7673a6a180d228d4255a538e451ffd8b414d40304c0c888992e0ab6de1602109527417bc1c7eb782ae77a8c3cdfc1d13a1e874207898264e38080243109c5969649ac8383417e922ba115331142d0ed35440b15d40bee0cf58af37c0f0524ffac1c71ceed3bb82f76ab108a8ad1a0c8b78d9341148c642369be7bef59d46f49d70c83560607f140848ec9a7607d4a08f8b6e4447f5523f416981888a8de9647ffef79389e4983e5c9387698d0cc2d429322365ce7e7b5fd6d6eb921c813fcf06199fe1ca41e9cfe03b539f321671a2acad0963f876f9db7a1c4371b9f101005217995b5b6a40976246d245da603dba8dac812a5480c3476a99d0ffdf0ef943d72d912543148b2fe78e8b0159324fe9bcd4ced33cd212fe4f3dfd6d4c5e1958beb95ac6b533ace3e78015e3880b52bf45299263a4c0096f8ba5fe3a6298cab675cb7f382e7ef49720eb4cf47376e2d2574122ccf91129c858e948904fecefb91226ed42403ba12dd3258909a87dfcbf65cc3adc3d98d277fdcec7664e2292b7d27afbb5aafb405c20a34b2fe2c0849ee280bb891dfdf59f19b89b0246358db54cf3fdc66eaaaa750c8903f1d42678f3edf0b7530410aa881bc617f94346379854af4532e61f65aae7576c35faf55e155bd6787b4634d54191907e155c239e68480cdfa0c87054bfb62855f409a20d5335fb123e681e64ec847cd985b6062059f436aebac623c038b6c3405ac325191a8d1126a5ef8f38cccbf144a5c324c1e093cf99efbe10ca03d439bcfb8ba5e293b7d318837f7bc42a99964392369da76e79d71d1a2c248a11324a87ae1e3cbeab6fb0d0bcae1ef55e43dfb6f1b4cfb82c7a778fb828a3727ef07685fe38a74b3dd25d015322c2d9f245c08d8c2b43865694233782eb734436c4eddef5406208d6c4572c7371262fe02319cfbbcf2e23bed8aa969d1ae6f5f25ff6b8ebcf0925066f761a39bbff49f0c8dbc3be84f0c442b044ea01b669747e3c8293cfe9ccdf2ef063ae3d28d10720c279a2691616abd23b055cfc6c562125df4ad0fa6631304972ddc3674b1aaa7665bf621320d83eac8d5b371d7d719829f58b23458182558710de31d81ef9a47d8839c79640b2025d1965a418bc90e4115f1423311a8b64fcde0f2d2145ee535b0931b84bc8110445f2ff68d136ed709ddb7ea9ff75f3b4e8b4f836230ca9e81069477f634e07270af60ef96f72557a081d664abcf35548f699484653da645483ff2bf5998617ae8bfa62d56e714f3c0136e5035a3f78e06c2f470df7fd3380d14033f81e2aae6b4d90487dab76b9b3b8761fb56c36f5429da3d4346cb22e641ad8d7d2d80fa240d4e0154e6b3d2f1b3ef6cf174c08d062f575c83a4078174f874364df36a6328beeef69ba7f90e1df9fdcec9a2f15ebf04fa7d6756da2e5a59c9cbbcbc397d6fb28d0fc9a60534dff0752716ed079ad1ab19a224d1c8ae8a53242fd164989ff997489b6520eb3c0e97f4bcc1a9c3cbd44f008c03ef52cf7e626881d246925e0336c0ac668867f853da7820f914115a7c77ac31b66f46fbf97f66fa26416fc4581d459a4f2462d52cf0c79b278955aa73e8fa56e3c320f516bcc54c97e587199c15ab953cc37189b81c70cabb2559e445bcc9d8174ad7574e9acb02f43e0c34ff5e6746ee730ad41ff8eef93c2071c2649063dd92f343c06ef6abaf98f28d98d968071c12cc10a90c22d8b3b3480c76f7a51b7ec594b3435d2e3d779c1a15037697f3a058650472e47eecd5f32eb3243a516f0e703f9888c84690750648d6a9a876bf1f353db6891dc6d317d6e87ac088f42b5f6f20d799ece4fa7aaa928d2ac795e8de83d1e1c7fa2f9a4106693e981c21c63b3221c4fa2649f45f0c6e05dbf24011af16ab2e5fe94a640b485988037ebe1e8ad0b2623d95e9947f0726121d7828614e3b2d77a7a1f9a938bea9a1a7a2627b7d2e358c42ccc6c0b80a15a1c2f6e9aaf0495bdb7bb8d4b0e28a1ab5ab93ca0ff3e3f910c490c13486852534d5e12160835ec5916c5c68349c4e2d8fa956c643277edd3b6c81c88c010421705fd317ff9e3c94df0ed5305f530acbccf8dd0e87140cd38152664a572c168cd72595b7fac243c03f3fb33ef74a28c0e4469f94587c13704e9efe8010b2125aca78c22c33c82366e1a7c4028c2ae2e8d26e1a57e4297fac987f84a0a27f42b4c93a4f4d14569824b0880fb67407ed58f267ac403aa0b1f93784b4b4c67036037e60d58072611b0e90ca316976ef4e0b302cdad1b6dcca92efb8e1f6be2397967508be2c02a25ed0380ba1f7955f857c8fb043297780d136b2b064040c8e55143d715ea997e134ed973c98ef82786f0ccf66c17d863542180c66d54d08e116f2e35d995e214489ad0fad7a55fe9ebc1a777fe34141147c080b98d13463a3bbc6fc82f2fc95f4de7b3591d9c8cd4416917a4338095d5620104b7be13f5a131dd3f7aad5b559d11e8171dfb91e2bb1e47ac3810b1cdc1a1e370c867b7b7b50c4688dce545763157e02f47e1cc661d5bf2fbc336cfae080ab15728b1ab9dd199f2779d451e6178977fb658c17344cffb7aa3af5791a28fc8a089c85187753e5e313c8d1f0fe7755e28be444426a189e8bce2d2f79db31d4c3ca911a83455525355f95d159351cd731a88e55403851236ee2128f279d5be644c042453ae65d9e9f3b40d6c82bdeb002acdee061ecca3f2dceabef9a900e6e063d56ab39cb82dbc77a4677572d7616cd72c0f6d5b9b941dfda1fe7c896b8cc24d65a4322d712a84e94adfc8ed0cc56cc1ae97f775bd3cea5b20b524d9a7a916056e19af095d30171e5e14c7c998f78dc44845edf307363eab7913f680a5e5a1540a6f945507ffa67591f8d1a2920ab3b6e754e35379dd67870c242335e2717903ff3c687e5c33dc953416865d5f23bd752e55492b9d5d888d7b37ef33b0a6774d052b0987c066a2e01767207aa7fbfc393ca62874613dde3794f74fadb5d55b877b877a605918c812610fbcafad72ee245e6dd8721138d6bd3f4eedad853aed1ec437ad02ac937c80dae26fa5f70083bd346779b779387f7b3d2aae57770d8177928833281ccb7a38da24834fd9726fd17eb603cba9041e82bfeed0e33942dde1d48c271f5b39aa7230f41afb89d36f7976eee4f51a036743031c534f64685b94c990a93a5737fe628ee9cda8ed9c08b11d3836f833835c445b317a77ead7599d1a0c08873014510d36bb7ff5fb961277589ea48c32a60c87ec40681be067b17785ec44825bd89faa25249e735a628b6eebcc6cce4e0314c627588118c40b2e0d460d8d5ce358c56458f36914ca203f5a5381c6deb5a76bbc08c40a87437da0d0b571788a05e9f96d9bb770de8a0b1b960ff2a44a964c9b7939853742e83ce8deb79191b2d82454655f227079dd8c5b0216c8470b8e1ac70526301bbfa2bc4adca68a766ccb2a6e0ebf2e99905bf5242590b01703868b3faf841c11c383be145a40fea6375e18a01468e459603b5efdf8a4e9abd179280ae8b5947d78d2f0c4d37715eaa42bc37cf8730e41ffbf9826d46424f2922a96033cefaa8b4bbe4c8b89d43501fd5211d5392ca19a98ba127d9025b5c6e86ba024471940549a2b5d8e14961c9dc19696da1a5bffd01030d5e6100000000000000000000000000000000000000000000000005090f131a1f").unwrap(); - assert_eq!(sig.len(), MLDSA65_SIG_LEN); - - if MLDSA65::verify(&mldsa65_pk, msg, None, &sig).is_ok() { - eprintln!("Verification succeeded!"); - } else { - panic!("Verification failed! -- figure that out"); - } -} - -fn bench_mldsa65_lowmemory_verify() { - use bouncycastle_mldsa_lowmemory::{MLDSATrait, MLDSA65, MLDSA65_SIG_LEN, MLDSA65PublicKey}; - use bouncycastle_hex as hex; - - eprintln!("MLDSA65_lowmemory/Verify"); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ - - // let seed = KeyMaterial256::from_bytes_as_type( - // &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - // KeyType::Seed, - // ).unwrap(); - // - // let (mldsa65_pk, mldsa65_sk) = MLDSA65::keygen_from_seed(&seed).unwrap(); - // - // eprintln!("pk:\n{}", &*hex::encode(&mldsa65_pk.encode())); - // let mu = MLDSA65::compute_mu_from_sk(&mldsa65_sk, msg, None).unwrap(); - // let sig = MLDSA65::sign_mu_deterministic(&mldsa65_sk, &mu, [0u8; 32]).unwrap(); - // eprintln!("sig:\n{}", &*hex::encode(sig)); - - let mldsa65_pk = MLDSA65PublicKey::from_bytes(&*hex::decode("48683d91978e31eb3dddb8b0473482d2b88a5f625949fd8f58a561e696bd4c27d05b38dbb2edf01e664efd81be1ea893688ce68aa2d51c5958f8bbc6eb4e89ee67d2c0320954d57212cac7229ff1d6eaf03928bd51511f8d88d847736c7de2730d5978e5410713160978867711bf5539a0bfc4c350c2be572baf0ee2e2fb16ccfea08028d99ac49aebb75937ddce111cdab62fff3cea8ba2233d1e56fbc5c5a1e726de63fadd2af016b119177fa3d971a2d9277173fce55b67745af0b7c21d597dbeb93e6a32f341c49a5a8be9e825088d1f2aa45155d6c8ae15367e4eb003b8fdf7851071949739f9fff09023eaf45104d2a84a45906eed4671a44dc28d27987bb55df69e9e8561f61a80a72699503865fed9b7ee72a8e17a19c408144f4b29afef7031c3a6d8571610b42c9f421245a88f197e16812b031159b65b9687e5b3e934c5225ae98a79ba73d2b399d73510effad19e53b8450f0ba8fce1012fd98d260a74aaaa13fae249a006b1c34f5ba0b882f26378222fb36f2283c243f0ffeb5f1bb414a0a70d55e3d40a56b6cbc88ae1f03b7b2882d98deea28e145c9dedfd8eaf1cef2ed94a8b050f8964f46d1ea0d0c2a43e0dda6182adbf4f6ed175b6742257859bf22f3a417ecf1f9d89317b5e539d587af16b9e1313e04514ffa64ba8b3ff2b8321f8811cb3fb022c8f644e70a4b80a2fbfee604abb7379091ea8e6c5c74dfc0283666b40c0793870028204a136bf5da9568eb798d349038bdb0c11e03445e7847cb5069c75cf28ac601c7799d958210ddbcb226e51afef9f1de47b073873d6d3f97456bede085082e74a298b2cd48f4b3093155f366c8fa601c6af858dfa32c08491b2a29887f90335949a5d6edaa679882a3a95d6bf6d970a221f4b9d3d8cbf384af81aac95e2b3294e04789ac83727a5dc04559f96af41d8a053516feeeebc52746eb6ab2819e09108710d835f011fa63065872ad334d5cdffb2b2310507e92fc993ae317da97f4f309cdaf0f67ed99d90215576083849f953b246d7fedb3fdb67679850a5ad404e64147fb7cf4f6aeddd05afb4b834968d1fe88014960dce5d942236526e12a478d69e5fbe6970310b308c06845018cfc7b2ab430a13a6b1ac7bb02cccbb3d911ac2f11068613fbe029bfdce02cf5cd38950ed72c83944edfbc75615af87f864c051f3c55456c5412863a40c06d1dab562bdff0571b8d3c3917bbd300880bba5e998239b95fa91b7d6416d4f398b3adbcd30983ed3592b4d9ef7d4236fd00f50d98aa53a235ac4172720f77d96172672980cfe8ff7a5a702783edc2ba31b2259015a112fc7f468a9c2f9464039002d30ef678b4cb798bc116216bf7a9a7c18ba03b7b58fd07515d3115049d3614be7a07e744300750df1d2c58753389059eafc3d785ccdd31c07648bedc03a5c3b8ad46d064d59c13d57374729fc4e295362e2a5191204530428bc1522afa28ff5fe1655e304ca5bc8c27ad0e0c6a39dd4df28956c14b38cc93682cefe402bbd5e82d29c464e44eb5d37b48fc568dfe0cc6e8e16baea05e5135590f19294e73e8367b0216dbb815030b9de55913f08039c42351c59e5515dd5af8e089a15e625e8f6dee639386c46497d7a263288774de581a7de9629b41b4424141f978fb8331208efdec3c6e0de39bc57063f3dcd6c470373c08891ea29cbc7cc6d6483b8889083ace86aa7b51b1c2cfe6e2ad18d97ce36fbc56ea42fae97e6a7ac114864478c366df1ebb1e7b11a9098504fd5975bdf1f49dc70002b63c1739a9d263fbad4073f6a9f6c2b8af4b4c332a103a0cffa5deeb2d062ca3c215fd360026be7c5164f4a4424ef74948804d66f46487732c8202c795478647b4ea71d627c086024cca354a41f0877b38f19b3774ad2095c8da53b069e21c76ae2d2007e16719ed40080d334f7da52e9f5a5990439caf083a95b833f02ad10a08c1a6d0f260c007285bd4a2f47703a5aef465287d253b18ac22514316210ff566814b10f87a293d6f199d3c3959990d0c1268b4f50d5f9fcefbbf237bd0c28b80182d6659741f14f10bfbb21bba12ab620aa2396f56c0686b4ea9017990224216b2fe8ad76c4a9148eef9a86a3635a6aa77bc1dcfb6fba59a77dfda9b7530dc0ca8648c8d973738e01bab8f08b4905e84aa4641bd602410cd97520265f2f231f2b35e15eb2fa04d2bd94d5a77abaf1e0e161010a990087f5b46ea988b2bc0512fda0fa923dadd6c45c5301d09483673265b5ab2e10f4ba520f6bbad564a5c3d5e27bdb080f7d20e13296a3181954c39c649c943ebe17df5c1f7aae0a8fe126c477585a5d4d648a0d008b6af5e8cd31be69a9296d4f3fd25ed86f221e4b93f65f5929967533624b9235750c30707550b58536d109a7131c5a5bbe4a5715567c12534aec7660761eebb9fae2891c774589b80e566ad557ddef7367196b7227ea9870ef09ddfec79d6b9319a6879b5205d76bf7aba5acf33afb59d17fc54e68383d6be5a08e9b66da53dcde008bb294b8582bd132cdcc49959fdbc21e52721880c8ad0352c79f03a43bbd84c4cdfdc6c529005e1e7cd9a349a7168a35569ba5dea818968d5a91466bd6e64e20bf62417198afc4e81c28dd77ed4028232398b52fbde86bc84f475b9016710ce2aabc11a06b4dbac901ec16cf365ca3f2d53813948a693a0f93e79c46ca5d5a6dca3d28ca50ad18bd13fca55059dd9b185f79f9c47196a4e81b2104bc460a051e02f2e8444f").unwrap()).unwrap(); - let sig = &*hex::decode("9061f15cbf2092f744fbcd799eb02414053c1b0f7412124bedc41cf9a3db0166469e874037d7f081e5f8d3d2033a0307d1c49ed01fe64578c4a6fabd80880cdf1911848f184d4bcf536ca795a0fb1aa19ab7ee3ba6b58bd64bbeac9f58650fff1ef5a97ab6916df962072e20e7c6be96090e3a781a504bc4442bd8889a0aa628907a74299f39fa836031f1bd68355bebe7ae93c1e361a9efbed1325d96227070461fcd6f151b8669d9229b977d9ee51fd2260c3e4a2e820416f9e074958dc3b3e2217e6312b7e0b582a048981cf6579f4bc7715b78c808e4c57e3b8aa38b05c04fcedf209f52c1e331ae83dbdff60ba450a17cc397568e54bc3f16ddf30b92747ce460d925b9be20a1d35e2aed97f124af2616a5361df28ba30e522dd08fa00fd28d1ac484d756a89e3a442fefe8332c56cd2a9fde691bdbda43f1cc54cef57bead96120b50c7d4695bdbb1303cc5ddda898e4eeb83083176e40e0232cdd1c3150371df05d6fdad7e1164d90393cf308e99edfeb31fed263e2866ee3b7f3937b399c974d87ba7b489efe3c9b80371d2928446adc31991ab0cefaaa080575b9ec81cfa133a9911c035a8058d0d3f2e34de4a9fb009bb4ccdb16de7b908574a7496725ff857556c1b33917e986c80f1014a9e3083add2fb35f345c5d06159e443329d0da099987b996c3731592b460c2ffd2955f7546f4216100ba43188803ff9b36969685f909fa2539323b8c8ec1c095a5085e554dd450e0e67ab670b6a11ebf6c25520fc13e364060f91f9b7f3d5cb48ff28b8fc83d4293f1f35ad6ff6ae4574ad7a1c6005fc0389a7b21386b0850a05d832fe6a14bb2b1db1f8e20bd09174946cd098b81c8f797e95f2143a949770cf1219bfef039db51a80fc247f65f41554c7173dd805ba82fdf47ab6d4bfd37dfe46fc47904421ae00dc005a22f9c4784b0ea9e665392a412245016d5c6d7673a6a180d228d4255a538e451ffd8b414d40304c0c888992e0ab6de1602109527417bc1c7eb782ae77a8c3cdfc1d13a1e874207898264e38080243109c5969649ac8383417e922ba115331142d0ed35440b15d40bee0cf58af37c0f0524ffac1c71ceed3bb82f76ab108a8ad1a0c8b78d9341148c642369be7bef59d46f49d70c83560607f140848ec9a7607d4a08f8b6e4447f5523f416981888a8de9647ffef79389e4983e5c9387698d0cc2d429322365ce7e7b5fd6d6eb921c813fcf06199fe1ca41e9cfe03b539f321671a2acad0963f876f9db7a1c4371b9f101005217995b5b6a40976246d245da603dba8dac812a5480c3476a99d0ffdf0ef943d72d912543148b2fe78e8b0159324fe9bcd4ced33cd212fe4f3dfd6d4c5e1958beb95ac6b533ace3e78015e3880b52bf45299263a4c0096f8ba5fe3a6298cab675cb7f382e7ef49720eb4cf47376e2d2574122ccf91129c858e948904fecefb91226ed42403ba12dd3258909a87dfcbf65cc3adc3d98d277fdcec7664e2292b7d27afbb5aafb405c20a34b2fe2c0849ee280bb891dfdf59f19b89b0246358db54cf3fdc66eaaaa750c8903f1d42678f3edf0b7530410aa881bc617f94346379854af4532e61f65aae7576c35faf55e155bd6787b4634d54191907e155c239e68480cdfa0c87054bfb62855f409a20d5335fb123e681e64ec847cd985b6062059f436aebac623c038b6c3405ac325191a8d1126a5ef8f38cccbf144a5c324c1e093cf99efbe10ca03d439bcfb8ba5e293b7d318837f7bc42a99964392369da76e79d71d1a2c248a11324a87ae1e3cbeab6fb0d0bcae1ef55e43dfb6f1b4cfb82c7a778fb828a3727ef07685fe38a74b3dd25d015322c2d9f245c08d8c2b43865694233782eb734436c4eddef5406208d6c4572c7371262fe02319cfbbcf2e23bed8aa969d1ae6f5f25ff6b8ebcf0925066f761a39bbff49f0c8dbc3be84f0c442b044ea01b669747e3c8293cfe9ccdf2ef063ae3d28d10720c279a2691616abd23b055cfc6c562125df4ad0fa6631304972ddc3674b1aaa7665bf621320d83eac8d5b371d7d719829f58b23458182558710de31d81ef9a47d8839c79640b2025d1965a418bc90e4115f1423311a8b64fcde0f2d2145ee535b0931b84bc8110445f2ff68d136ed709ddb7ea9ff75f3b4e8b4f836230ca9e81069477f634e07270af60ef96f72557a081d664abcf35548f699484653da645483ff2bf5998617ae8bfa62d56e714f3c0136e5035a3f78e06c2f470df7fd3380d14033f81e2aae6b4d90487dab76b9b3b8761fb56c36f5429da3d4346cb22e641ad8d7d2d80fa240d4e0154e6b3d2f1b3ef6cf174c08d062f575c83a4078174f874364df36a6328beeef69ba7f90e1df9fdcec9a2f15ebf04fa7d6756da2e5a59c9cbbcbc397d6fb28d0fc9a60534dff0752716ed079ad1ab19a224d1c8ae8a53242fd164989ff997489b6520eb3c0e97f4bcc1a9c3cbd44f008c03ef52cf7e626881d246925e0336c0ac668867f853da7820f914115a7c77ac31b66f46fbf97f66fa26416fc4581d459a4f2462d52cf0c79b278955aa73e8fa56e3c320f516bcc54c97e587199c15ab953cc37189b81c70cabb2559e445bcc9d8174ad7574e9acb02f43e0c34ff5e6746ee730ad41ff8eef93c2071c2649063dd92f343c06ef6abaf98f28d98d968071c12cc10a90c22d8b3b3480c76f7a51b7ec594b3435d2e3d779c1a15037697f3a058650472e47eecd5f32eb3243a516f0e703f9888c84690750648d6a9a876bf1f353db6891dc6d317d6e87ac088f42b5f6f20d799ece4fa7aaa928d2ac795e8de83d1e1c7fa2f9a4106693e981c21c63b3221c4fa2649f45f0c6e05dbf24011af16ab2e5fe94a640b485988037ebe1e8ad0b2623d95e9947f0726121d7828614e3b2d77a7a1f9a938bea9a1a7a2627b7d2e358c42ccc6c0b80a15a1c2f6e9aaf0495bdb7bb8d4b0e28a1ab5ab93ca0ff3e3f910c490c13486852534d5e12160835ec5916c5c68349c4e2d8fa956c643277edd3b6c81c88c010421705fd317ff9e3c94df0ed5305f530acbccf8dd0e87140cd38152664a572c168cd72595b7fac243c03f3fb33ef74a28c0e4469f94587c13704e9efe8010b2125aca78c22c33c82366e1a7c4028c2ae2e8d26e1a57e4297fac987f84a0a27f42b4c93a4f4d14569824b0880fb67407ed58f267ac403aa0b1f93784b4b4c67036037e60d58072611b0e90ca316976ef4e0b302cdad1b6dcca92efb8e1f6be2397967508be2c02a25ed0380ba1f7955f857c8fb043297780d136b2b064040c8e55143d715ea997e134ed973c98ef82786f0ccf66c17d863542180c66d54d08e116f2e35d995e214489ad0fad7a55fe9ebc1a777fe34141147c080b98d13463a3bbc6fc82f2fc95f4de7b3591d9c8cd4416917a4338095d5620104b7be13f5a131dd3f7aad5b559d11e8171dfb91e2bb1e47ac3810b1cdc1a1e370c867b7b7b50c4688dce545763157e02f47e1cc661d5bf2fbc336cfae080ab15728b1ab9dd199f2779d451e6178977fb658c17344cffb7aa3af5791a28fc8a089c85187753e5e313c8d1f0fe7755e28be444426a189e8bce2d2f79db31d4c3ca911a83455525355f95d159351cd731a88e55403851236ee2128f279d5be644c042453ae65d9e9f3b40d6c82bdeb002acdee061ecca3f2dceabef9a900e6e063d56ab39cb82dbc77a4677572d7616cd72c0f6d5b9b941dfda1fe7c896b8cc24d65a4322d712a84e94adfc8ed0cc56cc1ae97f775bd3cea5b20b524d9a7a916056e19af095d30171e5e14c7c998f78dc44845edf307363eab7913f680a5e5a1540a6f945507ffa67591f8d1a2920ab3b6e754e35379dd67870c242335e2717903ff3c687e5c33dc953416865d5f23bd752e55492b9d5d888d7b37ef33b0a6774d052b0987c066a2e01767207aa7fbfc393ca62874613dde3794f74fadb5d55b877b877a605918c812610fbcafad72ee245e6dd8721138d6bd3f4eedad853aed1ec437ad02ac937c80dae26fa5f70083bd346779b779387f7b3d2aae57770d8177928833281ccb7a38da24834fd9726fd17eb603cba9041e82bfeed0e33942dde1d48c271f5b39aa7230f41afb89d36f7976eee4f51a036743031c534f64685b94c990a93a5737fe628ee9cda8ed9c08b11d3836f833835c445b317a77ead7599d1a0c08873014510d36bb7ff5fb961277589ea48c32a60c87ec40681be067b17785ec44825bd89faa25249e735a628b6eebcc6cce4e0314c627588118c40b2e0d460d8d5ce358c56458f36914ca203f5a5381c6deb5a76bbc08c40a87437da0d0b571788a05e9f96d9bb770de8a0b1b960ff2a44a964c9b7939853742e83ce8deb79191b2d82454655f227079dd8c5b0216c8470b8e1ac70526301bbfa2bc4adca68a766ccb2a6e0ebf2e99905bf5242590b01703868b3faf841c11c383be145a40fea6375e18a01468e459603b5efdf8a4e9abd179280ae8b5947d78d2f0c4d37715eaa42bc37cf8730e41ffbf9826d46424f2922a96033cefaa8b4bbe4c8b89d43501fd5211d5392ca19a98ba127d9025b5c6e86ba024471940549a2b5d8e14961c9dc19696da1a5bffd01030d5e6100000000000000000000000000000000000000000000000005090f131a1f").unwrap(); - assert_eq!(sig.len(), MLDSA65_SIG_LEN); - - if MLDSA65::verify(&mldsa65_pk, msg, None, &sig).is_ok() { - eprintln!("Verification succeeded!"); - } else { - panic!("Verification failed! -- figure that out"); - } -} - -fn bench_mldsa87_verify() { - use bouncycastle_mldsa::{MLDSATrait, MLDSA87, MLDSA87_SIG_LEN, MLDSA87PublicKey}; - use bouncycastle_hex as hex; - - eprintln!("MLDSA87/Verify"); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ - - // let seed = KeyMaterial256::from_bytes_as_type( - // &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - // KeyType::Seed, - // ).unwrap(); - // - // let (mldsa65_pk, mldsa65_sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - // - // eprintln!("pk:\n{}", &*hex::encode(&mldsa65_pk.encode())); - // let mu = MLDSA87::compute_mu_from_sk(&mldsa65_sk, msg, None).unwrap(); - // let sig = MLDSA87::sign_mu_deterministic(&mldsa65_sk, &mu, [0u8; 32]).unwrap(); - // eprintln!("sig:\n{}", &*hex::encode(sig)); - - let mldsa87_pk = MLDSA87PublicKey::from_bytes(&*hex::decode("9792bcec2f2430686a82fccf3c2f5ff665e771d7ab41b90258cfa7e90ec97124a73b323b9ba21ab64d767c433f5a521effe18f86e46a188952c4467e048b729e7fc4d115e7e48da1896d5fe119b10dcddef62cb307954074b42336e52836de61da941f8d37ea68ac8106fabe19070679af6008537120f70793b8ea9cc0e6e7b7b4c9a5c7421c60f24451ba1e933db1a2ee16c79559f21b3d1b8305850aa42afbb13f1f4d5b9f4835f9d87dfceb162d0ef4a7fdc4cba1743cd1c87bb4967da16cc8764b6569df8ee5bdcbffe9a4e05748e6fdf225af9e4eeb7773b62e8f85f9b56b548945551844fbd89806a4ac369bed2d256100f688a6ad5e0a709826dc4449e91e23c5506e642361ef5a313712f79bc4b3186861ca85a4bab17e7f943d1b8a333aa3ae7ce16b440d6018f9e04daf5725c7f1a93fad1a5a27b67895bd249aa91685de20af32c8b7e268c7f96877d0c85001135a4f0a8f1b8264fa6ebe5a349d8aecad1a16299ccf2fd9c7b85bace2ced3aa1276ba61ee78ed7e5ca5b67cdd458a9354030e6abbbabf56a0a2316fec9dba83b51d42fd3167f1e0f90855d5c66509b210265dc1e54ec44b43ba7cf9aef118b44d80912ce75166a6651e116cebe49229a7062c09931f71abd2293f76f7efc3215ba97800037e58e470bdbbb43c1b0439eaf79c54d93b44aac9efe9fbe151874cfb2a64cbee28cc4c0fe7775e5d870f1c02e5b2e3c5004c995f24c9b779cb753a277d0e71fd425eb6bc2ca56ce129db51f70740f31e63976b50c7312e9797d78c5b1ac24a5fa347cc916e0a83f5c3b675cd30b81e3fa10b93444e07397571cce98b28da51db9056bc728c5b0b1181e2fbd387b4c79ab1a5fefece37167af772ddad14eb4c3982da5a59d0e9eb173ec6315091170027a3ab5ef6aa129cb8585727b9358a28501d713a72f3f1db31714286f9b6408013af06045d75592fc0b7dd47c73ed9c75b11e9d7c69f7cadfc3280a9062c5273c43be1c34f87448864cea7b5c97d6d32f59bd5f25384653bb5c4faa45bea8b89402843e645b6b9269e2bd988ddacb033328ffb060450f7df080053e6969b251e875ecec32cfc592840d69ab69a75e06b379c535d95266b082f4f09c93162b33b0d9f7307a4eaaa52104437fed66f8ee3eabbd45d67b25a8133f496468b52baffdbfad93eef1a9818b5e42ec722788a3d8d3529fc777d2ba570801dfae01ec88302837c1fb9e0355727645ee1046c3f915f6ae82dad4fb6b0356a46518ffc834155c3b4fe6dafa6cc8a5ccf53c73a0849d8d44f7dcf72754e70e1b7dfb447bb4ef49d1a718f6171bbce200950e0ce926106b151a3e871d5ce49731bd6650a9b0ca972da1c5f136d44820ea6383c08f3b384cf2338e789c513f618cc5694a6f0cee104511e1ed7c5f23a1ebfd8a0db8424553240156dbf622831b0c643d1c551b6f3f7a98d29b85c2de05a65fa615eee16495bd90737672115b53e91c5d90028cf3f1a93953a153de53b44084e9ccff6b736693926daefebb2d77aa5ad689b92f31686669df16d1715cc58f7a2cfb72dd1a51e92f825993a74022be7e9eb6054654457094d14928f20215e7b222ac56b51adbec8d8bdb6983979a7e3a21b44b5d1518ca97d0b5195f51ed6a24350c89747e1edea51b448e3e9147054ce927873c90db394d86888e07dff177593d6f79e152302204aeb03be2386af3e24078bd028b1689f5e147c9f452c8ceb02ec59cc9db63a03576ceeafe98239023897da0236630a53c0de7f435a19869792fab36e7b9e635760f09069e6432e700035ac2a02879fff0a1e1bec522047193d94eb5df1efd53eea1144ca78940852f5ec9727904b366ede4f5e2d331fad5fc282ea2c47e923142771c3dd75a87357487def99e5f18e9d9ed623c175d02888c51f82c07a80d54716b3c3c2bdbe2e9f0a9bbaaebeb4d52936876406f5c00e8e4bbd0a5ec05797e6207c5ab6c88f1a688421bd05a114f4d7de2ac241fa0e8bedff47f762ddcbeaa91004f8d31e85095c81054994ad3826e344ba96040810fc0b2ad1de48cfade002c62e5a49a0731ab38344bc1636df16bf607d56855e56d684003c718e4bad9e5a099979fcddeeb1c4a7776cd37a3417cb0e184e29ef9bc0e87475ba663be09e00ab562eb7c0f7165f969a9b42414198ccf1bff2a2c8d689a414ece7662927665689e94db961ebaec5615cbc1a7895c6851ac961432ff1118d4607d32ef9dc732d51333be4b4d0e30ddea784eca8be47e741be9c19631dc470a52ef4dc13a4f3633fd434d787c170977b417df598e1d0dde506bb71d6f0bc17ec70e3b03cdc1965cb36993f633b0472e50d0923ac6c66fdf1d3e6459cc121f0f5f94d09e9dbcf5d690e23233838a0bacb7c638d1b2650a4308cd171b6855126d1da672a6ed85a8d78c286fb56f4ab3d21497528045c63262c8a42af2f9802c53b7bb8be28e78fe0b5ce45fbb7a1af1a3b28a8d94b7890e3c882e39bc98e9f0ad76025bf0dd2f00298e7141a226b3d7cee414f604d1e0ba54d11d5fe58bccea6ad77ad2e8c1caacf32459014b7b91001b1efa8ad172a523fb8e365b577121bf9fd88a2c60c21e821d7b6acb47a5a995e40caced5c223b8fe6de5e18e9d2e5893aefebb7aae7ff1a146260e2f110e939528213a0025a38ec79aabc861b25ebc509a4674c132aaacb7e0146f14efd11cfcaf4caa4f775a716ce325e0a435a4d349d720bcf137450afc45046fc1a1f83a9d329777a7084e4aadae7122ce97005930528eb3c7f7f1129b372887a371155a3ba201a25cbf1dcb64e7cdee092c3141fb5550fe3d0dd82e870e578b2b46500818113b8f6569773c677385b69a42b77dcba7acffd95fd4452e23aaa1d37e1da2151ea658d40a3596b27ac9f8129dc6cf0643772624b59f4f461230df471ca26087c3942d5c6687df6082835935a3f87cb762b0c3b1d0dda4a6533965bef1b7b8292e254c014d090fed857c44c1839c694c0a64e3fad90a11f534722b6ee1574f2e149d55d744de4887024e08511431c062750e16c74ab9f3242f2db3ffb12a8d6107faa229d6f6373b07f36d3932b3bdb04c19dd64eadd7f93c3c564c358a1c81dcf1c9c31e5b06568f97544c17dc15698c5cb38983a9afc42783faa773a52c9d8260690be9e3156aa5bc1509dea3f69587695cd6ff172ba83e6a6d8a7d6bbebbbcda3672731983f89bc5831dc37c3f3c5c56facc697f3cb20bd5dbadbd702e54844ac2f626901fe159db93dfd4773d8fe73562b846c1fc856d1802762840ebc72d7988bde75cbca70d319d32ce0cc0253bb2ad455723ee0c7f4736ce6e6665c5aca32a481c53839bc259167b013d0423395eeb9aaaee3206149a7d550d67fc5fdfe4a8a5c35d2510b664379ab8f72855a2af47abce2a632048eaf89e5cb4a88debc53a595103acce4f1cff18acff07afe1eb5716aa1e40b63134c3a3ae9579fa87f515be093c2d29db6d6b65c93661e00636b592704d093cc6716c2342eb1853d48c85c63ac8a2854462c7b77e7e3bd1eac5bca28ffaa00b5d349f8a547ad875b96a8c2b2910c9301309a3f9138a5693111f55b3c009ca947c39dfc82d98eb1caa4a9cbe885f786fa86e55be062222f8ba90a974073326b31212aece0a34a60").unwrap()).unwrap(); - let sig = &*hex::decode("781368e64dba542a7eacbd2257335cc943a03241009b797093c615f76a671a7591430441d80bb582304b33b9fce295e0dd57fe169355ddf4453a2aca62d8eb8109ef0d9cf3f5b0a94e04ad81b3e786014243ecde816551aa7fe01c639054256a491756bef59f5034f717ff4f85e70ba7731a49971415b6a7e7d816ab434b9f17a3095ede6fd432be2bfa82724045dda0dfff7a0281e9000939ccba3d8ab3245139c441648c76a6536127e4d1ef0df1531883ab78c8b41323617ad8db03d9908c9e08a9f7321c45051b3c94213347b11c4a84491de7a7be68701e47d7f0e0b33e767bef17694e4d33244ed92ebd74c85ab6c84441cddc14331e6ae8bd23674bda27f09c050d88f7d430feee7f15a72a24d653bb6bec54491b98362ce131d37c7d78a3f9a893db5abdccd6663593b88bc6c97f07f8eafccfd25e8180d918efbcd95bbf3da29f081e3e1932095939198e2a155b2d803a3e84ca4f34569df695c259faf3c0d8f0cd217ebd2dbad542b32fbb54e44aaf0b5dc739fafef2e46db8d68bfc35f44f038cb1f5231a1b5b134ae683e7f3297cc7a95bd191b310f68201450797fe3293cde1672dfeca4b493f53c768ea048a972a4cd84d39ef682957b8f28ba29487b4689b43fec2655823d9bb99ffcf31490366a9860a5d5b8e32a3b8bfeb6f55f88fb80c8c0142086f220e1f6f2862dabda58c3b6f5faa805b39cfac4b6d7ea7acdf1b0690063b0c1ea38c7c4755189966dc631055f153f71b77b114fa5c309316ba512330ea5cdb0bb176001e57461563d17259f35d0c30ef5ac838c0325402bab52c531469526ae3ee6293f7b5769d27e69fa81cd25a31cd095b126e70c57ac3169a5f585a11f1748d9d22f2564911c26a24b2153a78f3a06822f5f1963f237abeb48efd9a9cd478de579c5a0ba84d00e96fbde36d8ce20e7e948547fb6850834ff79d211830f6ee973359781d9d5008fb43a89354782fde4158177f5206ce1d38c889e99e4bb5b4ab34d6a05c42f5d719ea03dbc54adba75a3bb44a3c08c7556462f8c5b7c568a69242cf5be6098eba0a2249c1ca5b2109b6404a962abc1c159c6b48a79fb97e4a3337d99323746221297423f9bd1b12e78489e01e6a10f0fd6bba1cfd6ae1b75dbe69f8b8ee51a4e7f68ba2c407c9c0bad3892b29b0170ab75836fdd49a7ee3c2bb30f2c3d226bcec49140952170b0d160f97b30b7b7b096719538677ebb06922f26925227c8852acc107a8f173b38d96697584bd3dfe169b4073aa58a7bf371d5c4bb0eed30f08212defb3aec902d4546084176bf0f86d93cf36a4689a5e874b32d6b7d3c1e3fbcfd988c35dbc9a8d0a019ad6d7e15ec3ac97125db6abfe00beffe35a81666699a91e15945c62d646690b5b52de8b835ee9be53588fde5d63023b52b2b1f4610c237a829f5901a46042963cce7b85aa040adde02985e14e23c4eadb75221c607d24672e244c66c9c24c3cb7fd90bb23295c9d3d9da516bce3dd462d6660f9f91ef0618a4d4d3d6668c5d1e2e8ed433ebfe0762beb743324e11608f8b14b69ce4c221c1654ba4992a5af2d949c2939f95d1c8fb767af2a843cc7c78f57259d5c0c6ca83fca41ef5ecc4eeeea93e4518c24d3040f2cd90df3e535e989e606fa109e2c453ed7353db1cdb27137f005f9dd8d2aebfb7255a6098b690215e100cfe44ca0f2745fced48322bc9667ab16d2e1c0ff491b96a17b833d4fd44d31c2230ac835796e063ab03000f04f15c70560033763a48552cceaacade9ca5c8055f3745e179068a287183f2bc3ef6327dec5ac7cf7b052ef5a8873e697efde089688f43be464827c2fa83eb531b3674145e95c699c82990e684967dad319d9f64ab16cd9fe9b6c41232ac4ae3795fd8a76aa9b02e970242061c6da45a2af74ad9cb2a79935c92625e242f4bc7fce54d5c10a9e61f875162fa651b66057ba036f062d6d39d0502b93a5640b78c6c2fd20b02ff83676a87a94945d476c349803ea4fe60cdcea65bb2629e2bc09d4472ec63422dee2052f098deaf5531e6c9bed6672a8b699802efe0cded80c8455f585d1ba633d281f1a21adab48e63b44e0c2a4d7608cf98aabf8adc86bcb8f61e8b06cd2385f82e0a3cdd03cab152d5951859c4532f9168e78f17ba2a5772780327dcf4e62b4d26e443762fc488ae4cd4d1156dbd5782595cfd7697a514abc9b160c9ccf08edc86134a755b90e9bb543511e888e3157721a52d1bc5db33029fb335ea2114e21c03368c8d7f4d827960641772a4a32a738df60d19ec77ab09d22f57cc2523b9503b3f5b1cebc5ae15f885f159842db7359a1c89d3d82d3407068f15b6739626eb8c521fc8c5c7491f945d49f14e6989da340bdf49e7f8a792747aa658bc114143ba93f26022d001735b744639bbf22aab2a1851cfc934f9c69d3764fdea3d23db17998e6138cfd7cd9e9a47cb74193bd71aaf28cdd9d1eb595125546a4f4357ebbd1f410e3bf8557892de68509b5b98c5c229e942c910fdd3e54cb6ad54d8dd886cb97ecc06d1e401b8395d0bcb0db9a031dc66c9294f9053c68fc42042b1fa1671fc7d510b70916c0139cfebe3a91244527ce9439860cedb30908197be851cbd1d3b18ca541358449fb34fb5cd569630ed5f67b8795e87828f2ce3becfe457579d82333b0bbab094de391e1f8157bd431e365ca864630932bbebb48f45f8134424e18ab455029b54b19e2f3bfec5e44ad0ea5c03f53d8f925b635838aa7015a7c9e325bdfaff966ea9512dd50f87c8995cea7561c23f4fb06d964ab8f1913a6ca17e4ca60d6bc078e1784f89c673c91d955bcf45f58ca9709579d5e3831df12cfdb7516fd21878cb54243579b9346d2de4be25f508e84b1adc78cb91c03da3c4fd59e4529189838f74f6312820620a5996b791ffcb332f847094613f2148b862034fa89d0d0ff1808d902c5d1af64d5522492d61ecde4c73be89a33782cef1acc1dc327fb2eb9d17642209b85aa8b1dc57cbf067c7aa29da6b7e157d23e171d3ae6f3855834071791402c851ff2dd67109979f7ee5e09e64b4eefdee7112b55ce200bb8c8051e3428c305fa1d576bffbb25a70eb571168fc60dadd928b10cfd07de80a85b8df3edc372d488c21f0d5787611cc6fb73aeeb6f920a109294b49d3870f90de3b360d14df77ef95640bcac7a4dbca901a31db83e83f5c59ce327207ea9b27c3b978d30d53865c1b84764f025e8732d5007554ce5c9cc410b2eefd7e4d990c538557606a6bc47577a43768d30aa3e8598fd6f4fe7ac439f3931c58fd69d90765ac9f456ac7de085e14a0898c4557f5d3baaae07edc607de6900146b97b35aae570153dc107815ef9febdd4fd567d637fee8f8bfb4b3413ea6aead4846ab733a04f1e4bc32a3bbb1c16baf8d0bdb9ccb82fe46479ccfd040b5e64064e539b39c66e4501dc822873ac6119a4a112a1f7cd6df0e5f84356ce853ced34ce69a9e7383534983c51c50269bf8b9586a0e5ba905fd3bce080b00e7f7d48e55f489479b5771e995fb020e58feb74af65c3ee76aa4e69b5ba8bda249a1b2d62c08d418c3635d061846040843991ab475473da85d94981fb84425e7ad951ce0a42be642fe658b7aaae72b147cdc086c24b1571eb2272e2a72b15660d854ebe19ec7d9ab7ea17800d0b6ae727b39217467c662ba08e6f19193951eeff02806a7843eb5c71b2f04dcb605ecedc5128cd67703038c44bf20fe06f3ed8c1368fe38e72944d5c52fca46a45fd48d8fe5da64183d4d62ec01aa3d9d672ec67a01c17f21e02525f0513cce030c664fc8784763086608bc8099c204c255ffed1daf432ceb45fbd135e21e8190c5bfee192171faa77520481e69e87b7f76790bef76cb8d3c88f5c6e32fc59e7bd45351d66696b61d9f40726fb9a98000b68738cf7e34b98b6a4aaa2ac1d7b1407db89783f8077103ea9c9e89247eae078adfb36e21474c3bb1fe0c87687c6233a533a01e1081b93a3521d339f39c075609bace531994988ae314f77fc6034113a138c67eb7e03750cbec8d28bd21afedfefa8f091619ae500b4ca4599d019dc8ca4bf118d70b8676dfc796a4f6d986adba4c8574ed4abaea5465466220e5e53dc8fcb395d1e59d278673cbc4e3f40658df98ac2fd126a94922879e1a3be91c1acc20803c35fa764abbadab07bde85ff4bd9e0fb6f06baf5bf42b8a2cbf6c2f62606becc361552921a12d6c8236fde84db4bddac77e8872478cffc4e148c1c7acfedf6b17d98731c2de36f3cbef1f6f781a940e0874d5b74535bbe066b53064d43b13926570a9e1c4e6da206c8bd252caf2b62e7d223f7ac12939137f330be59374d7295a6c2dff92e07c727510e48d970593e47229fc8bc3bd5b8ea780dacff4d23063df65feda5f8f65b17a333e532acad7916780c74d6a70d38b367f3f6f4e947b85fc15235bbe46b26495d2780098db853a931377cfbedea620f2355ca21e81ce9e0078b0dd6cb70f23ed558682be3b3d594eefe85344e1f275428b316cc088995939298f2a2d15ac9b676ac3e9cb92f2a64dec7732a91fc761aa1b126ea575e3953177da6e1cd78faea824665330a81d9e24572b9860bf0aba4df8bd5d4e3e2c72bbfbb2a985c7ae2f077951fe8401e1d156ecada1e353817b20f41e0b2460a0caaf2b36d6a7f1b35125d797dcc714421027d14171765a646071ed952b6a5294eecf6a3a71c104c843a4a8b3efcc27467b20cb0a94abf5802229ea4d8312783e78791a50b3c0a88fe6497198cd4bf470dac46f34e50019fdee2040cfe99124b312b1122b83e51d878877cec0855f1158c445cfdc2253f4389d5e3a8ba1669abb5976a4617e85f543da9f5e30b10ca7481c8185392782b46fb0a0e5ab408b2945e3c79a1cc49fb7c27254a9b540e7397a5b655bf7e4f83184db32a128aa2e00a624d7dcd6b77efd151f1e5cba8890af9170fa06c555715dc1787e995ad19270973ae95b88dcafdaf62c28843d3f8b9c78dc8e37d911dab3f7ee9d4c7389c654bdcfc05056b360020140e57e31473258a4081e2a708f7caba90c356d0847098fc0762484086aba898a60b023d6a3060402406240748785d51caff52a0ef3dc2a45dadf80ac18502d24422a8cbb10192b88f4e9160206b2ed3e04114f2a339df269e2c36b8613ce37087471701755330cb559575366ee0fa2d3afcda32eede6dd906345daaa04812198e96c42239c242edb90059709f497da5b87705384aef2af22dc2edfef3c00d8c9156d8b3163fe7a7779e04f04911a8b934fb3072eee844484fede5e2ee96d338eefe2da986067ffe0218ada7de1d0e42d823d6b033918278888ba0608ab8f7be997bdf263689a36f5204c802ad836363779b4b0d6ce5083df0b98a2e2c700062a4fa5e57bc73bd45357e01d90c7954bc6904d1ce8166a9168da39a60c5cae8119bb6b9ab074fb2d0aee384fc2c0e4806811d6002b4e2401e7430b50cb0e8075f33d5386aecde256e169d95e2f9c6556c08ba042e68a53ce8aca9cc02818f7382f150dc04de0019b19c7a3ab0d72d6ed013d7a115d74b279f71fd6effef34049877e0b11e0659be938a5de684eaf23513095eb4a1bdf536c3c01a4655c4b4a0673214cef29a481d06a02cc9a5bfc7b8d846c33484cd67b1de98f60b69918f177b64558ca567a6237d35ff01771a42320ce02bb98f3e4ad4ac7db75611bd9961eac662a38b1f785970c99f3dd105ff586f61301c48d66708cbf7d53a733e357b6c256e8b73f0e1305a0bc137989e521100c2ee6259e607fe12198e8bcb988b0854668e40d7cae6adc3ba40ba121b7319d06d988a073d03097b9f5c1c07284b6473ae57bf154811b77baceb0412b8a6983bdd0ccd9e3bf014e520009cb26d5780eabef1bbafa5e25d41098a54c47fce8b68d395291d54284d33aa50b9664d1510b467c8a539361ca9a4448bc01fcb4c4e3ef475e8afb46a494ae13ee9ea8a1266825fba7f32b9712fde252698a68359b50141d90f5c4a06283ddb54ad7e1412ac5ebb12501f7a82b2a7f27b2dbab626c3db4074523b3211d3182ea261397a6f7b187cf2b8a356ded10812f1d305169aedf79b5ff1cf7c2d6e86ee11f28e96aa63b5a03f59fc960ac7d0572e91dda61905c0711a9b26344a2a10aa2041f2b13cb1a9a9a27774b6d0deddc9d81ea1b142ad7b72be47991f2c9261d6708156e38d00b074020766eb0c494392d65b82ca65f7c3352f9bb78325ecb6df596c8ae57826b08ccd6f1d529d2e25925c1ac972425bedeb88a5d0e3138ddc434da3462ebdf6b1239a21f141ec62cbe4bb993ba253b55a76d30fac19c2c1384ef6b9746c07787aa1fe913a1348390bd8c1f386a08c77cf7106c927ce24dffc9d6ee1b32354d95ed2923482531de6b390bf0f5eb80276e90e7ed11131c848bbabec4d317236269a0a3a7cbe0f1272f93949ca6d23ea2a7ee3f697791aab71533423066d400000000000000000000000000000000000000000000000000000000050e181f23292c2f").unwrap(); - assert_eq!(sig.len(), MLDSA87_SIG_LEN); - - if MLDSA87::verify(&mldsa87_pk, msg, None, &sig).is_ok() { - eprintln!("Verification succeeded!"); - } else { - panic!("Verification failed! -- figure that out"); - } -} - -fn bench_mldsa87_lowmemory_verify() { - use bouncycastle_mldsa_lowmemory::{MLDSATrait, MLDSA87, MLDSA87_SIG_LEN, MLDSA87PublicKey}; - use bouncycastle_hex as hex; - - eprintln!("MLDSA87/Verify"); - - let msg = b"The quick brown fox jumped over the lazy dog"; - - /* One-time setup of the KAT -- commented out so that keygen is not captured in the bench */ - - // let seed = KeyMaterial256::from_bytes_as_type( - // &hex::decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f").unwrap(), - // KeyType::Seed, - // ).unwrap(); - // - // let (mldsa65_pk, mldsa65_sk) = MLDSA87::keygen_from_seed(&seed).unwrap(); - // - // eprintln!("pk:\n{}", &*hex::encode(&mldsa65_pk.encode())); - // let mu = MLDSA87::compute_mu_from_sk(&mldsa65_sk, msg, None).unwrap(); - // let sig = MLDSA87::sign_mu_deterministic(&mldsa65_sk, &mu, [0u8; 32]).unwrap(); - // eprintln!("sig:\n{}", &*hex::encode(sig)); - - let mldsa87_pk = MLDSA87PublicKey::from_bytes(&*hex::decode("9792bcec2f2430686a82fccf3c2f5ff665e771d7ab41b90258cfa7e90ec97124a73b323b9ba21ab64d767c433f5a521effe18f86e46a188952c4467e048b729e7fc4d115e7e48da1896d5fe119b10dcddef62cb307954074b42336e52836de61da941f8d37ea68ac8106fabe19070679af6008537120f70793b8ea9cc0e6e7b7b4c9a5c7421c60f24451ba1e933db1a2ee16c79559f21b3d1b8305850aa42afbb13f1f4d5b9f4835f9d87dfceb162d0ef4a7fdc4cba1743cd1c87bb4967da16cc8764b6569df8ee5bdcbffe9a4e05748e6fdf225af9e4eeb7773b62e8f85f9b56b548945551844fbd89806a4ac369bed2d256100f688a6ad5e0a709826dc4449e91e23c5506e642361ef5a313712f79bc4b3186861ca85a4bab17e7f943d1b8a333aa3ae7ce16b440d6018f9e04daf5725c7f1a93fad1a5a27b67895bd249aa91685de20af32c8b7e268c7f96877d0c85001135a4f0a8f1b8264fa6ebe5a349d8aecad1a16299ccf2fd9c7b85bace2ced3aa1276ba61ee78ed7e5ca5b67cdd458a9354030e6abbbabf56a0a2316fec9dba83b51d42fd3167f1e0f90855d5c66509b210265dc1e54ec44b43ba7cf9aef118b44d80912ce75166a6651e116cebe49229a7062c09931f71abd2293f76f7efc3215ba97800037e58e470bdbbb43c1b0439eaf79c54d93b44aac9efe9fbe151874cfb2a64cbee28cc4c0fe7775e5d870f1c02e5b2e3c5004c995f24c9b779cb753a277d0e71fd425eb6bc2ca56ce129db51f70740f31e63976b50c7312e9797d78c5b1ac24a5fa347cc916e0a83f5c3b675cd30b81e3fa10b93444e07397571cce98b28da51db9056bc728c5b0b1181e2fbd387b4c79ab1a5fefece37167af772ddad14eb4c3982da5a59d0e9eb173ec6315091170027a3ab5ef6aa129cb8585727b9358a28501d713a72f3f1db31714286f9b6408013af06045d75592fc0b7dd47c73ed9c75b11e9d7c69f7cadfc3280a9062c5273c43be1c34f87448864cea7b5c97d6d32f59bd5f25384653bb5c4faa45bea8b89402843e645b6b9269e2bd988ddacb033328ffb060450f7df080053e6969b251e875ecec32cfc592840d69ab69a75e06b379c535d95266b082f4f09c93162b33b0d9f7307a4eaaa52104437fed66f8ee3eabbd45d67b25a8133f496468b52baffdbfad93eef1a9818b5e42ec722788a3d8d3529fc777d2ba570801dfae01ec88302837c1fb9e0355727645ee1046c3f915f6ae82dad4fb6b0356a46518ffc834155c3b4fe6dafa6cc8a5ccf53c73a0849d8d44f7dcf72754e70e1b7dfb447bb4ef49d1a718f6171bbce200950e0ce926106b151a3e871d5ce49731bd6650a9b0ca972da1c5f136d44820ea6383c08f3b384cf2338e789c513f618cc5694a6f0cee104511e1ed7c5f23a1ebfd8a0db8424553240156dbf622831b0c643d1c551b6f3f7a98d29b85c2de05a65fa615eee16495bd90737672115b53e91c5d90028cf3f1a93953a153de53b44084e9ccff6b736693926daefebb2d77aa5ad689b92f31686669df16d1715cc58f7a2cfb72dd1a51e92f825993a74022be7e9eb6054654457094d14928f20215e7b222ac56b51adbec8d8bdb6983979a7e3a21b44b5d1518ca97d0b5195f51ed6a24350c89747e1edea51b448e3e9147054ce927873c90db394d86888e07dff177593d6f79e152302204aeb03be2386af3e24078bd028b1689f5e147c9f452c8ceb02ec59cc9db63a03576ceeafe98239023897da0236630a53c0de7f435a19869792fab36e7b9e635760f09069e6432e700035ac2a02879fff0a1e1bec522047193d94eb5df1efd53eea1144ca78940852f5ec9727904b366ede4f5e2d331fad5fc282ea2c47e923142771c3dd75a87357487def99e5f18e9d9ed623c175d02888c51f82c07a80d54716b3c3c2bdbe2e9f0a9bbaaebeb4d52936876406f5c00e8e4bbd0a5ec05797e6207c5ab6c88f1a688421bd05a114f4d7de2ac241fa0e8bedff47f762ddcbeaa91004f8d31e85095c81054994ad3826e344ba96040810fc0b2ad1de48cfade002c62e5a49a0731ab38344bc1636df16bf607d56855e56d684003c718e4bad9e5a099979fcddeeb1c4a7776cd37a3417cb0e184e29ef9bc0e87475ba663be09e00ab562eb7c0f7165f969a9b42414198ccf1bff2a2c8d689a414ece7662927665689e94db961ebaec5615cbc1a7895c6851ac961432ff1118d4607d32ef9dc732d51333be4b4d0e30ddea784eca8be47e741be9c19631dc470a52ef4dc13a4f3633fd434d787c170977b417df598e1d0dde506bb71d6f0bc17ec70e3b03cdc1965cb36993f633b0472e50d0923ac6c66fdf1d3e6459cc121f0f5f94d09e9dbcf5d690e23233838a0bacb7c638d1b2650a4308cd171b6855126d1da672a6ed85a8d78c286fb56f4ab3d21497528045c63262c8a42af2f9802c53b7bb8be28e78fe0b5ce45fbb7a1af1a3b28a8d94b7890e3c882e39bc98e9f0ad76025bf0dd2f00298e7141a226b3d7cee414f604d1e0ba54d11d5fe58bccea6ad77ad2e8c1caacf32459014b7b91001b1efa8ad172a523fb8e365b577121bf9fd88a2c60c21e821d7b6acb47a5a995e40caced5c223b8fe6de5e18e9d2e5893aefebb7aae7ff1a146260e2f110e939528213a0025a38ec79aabc861b25ebc509a4674c132aaacb7e0146f14efd11cfcaf4caa4f775a716ce325e0a435a4d349d720bcf137450afc45046fc1a1f83a9d329777a7084e4aadae7122ce97005930528eb3c7f7f1129b372887a371155a3ba201a25cbf1dcb64e7cdee092c3141fb5550fe3d0dd82e870e578b2b46500818113b8f6569773c677385b69a42b77dcba7acffd95fd4452e23aaa1d37e1da2151ea658d40a3596b27ac9f8129dc6cf0643772624b59f4f461230df471ca26087c3942d5c6687df6082835935a3f87cb762b0c3b1d0dda4a6533965bef1b7b8292e254c014d090fed857c44c1839c694c0a64e3fad90a11f534722b6ee1574f2e149d55d744de4887024e08511431c062750e16c74ab9f3242f2db3ffb12a8d6107faa229d6f6373b07f36d3932b3bdb04c19dd64eadd7f93c3c564c358a1c81dcf1c9c31e5b06568f97544c17dc15698c5cb38983a9afc42783faa773a52c9d8260690be9e3156aa5bc1509dea3f69587695cd6ff172ba83e6a6d8a7d6bbebbbcda3672731983f89bc5831dc37c3f3c5c56facc697f3cb20bd5dbadbd702e54844ac2f626901fe159db93dfd4773d8fe73562b846c1fc856d1802762840ebc72d7988bde75cbca70d319d32ce0cc0253bb2ad455723ee0c7f4736ce6e6665c5aca32a481c53839bc259167b013d0423395eeb9aaaee3206149a7d550d67fc5fdfe4a8a5c35d2510b664379ab8f72855a2af47abce2a632048eaf89e5cb4a88debc53a595103acce4f1cff18acff07afe1eb5716aa1e40b63134c3a3ae9579fa87f515be093c2d29db6d6b65c93661e00636b592704d093cc6716c2342eb1853d48c85c63ac8a2854462c7b77e7e3bd1eac5bca28ffaa00b5d349f8a547ad875b96a8c2b2910c9301309a3f9138a5693111f55b3c009ca947c39dfc82d98eb1caa4a9cbe885f786fa86e55be062222f8ba90a974073326b31212aece0a34a60").unwrap()).unwrap(); - let sig = &*hex::decode("781368e64dba542a7eacbd2257335cc943a03241009b797093c615f76a671a7591430441d80bb582304b33b9fce295e0dd57fe169355ddf4453a2aca62d8eb8109ef0d9cf3f5b0a94e04ad81b3e786014243ecde816551aa7fe01c639054256a491756bef59f5034f717ff4f85e70ba7731a49971415b6a7e7d816ab434b9f17a3095ede6fd432be2bfa82724045dda0dfff7a0281e9000939ccba3d8ab3245139c441648c76a6536127e4d1ef0df1531883ab78c8b41323617ad8db03d9908c9e08a9f7321c45051b3c94213347b11c4a84491de7a7be68701e47d7f0e0b33e767bef17694e4d33244ed92ebd74c85ab6c84441cddc14331e6ae8bd23674bda27f09c050d88f7d430feee7f15a72a24d653bb6bec54491b98362ce131d37c7d78a3f9a893db5abdccd6663593b88bc6c97f07f8eafccfd25e8180d918efbcd95bbf3da29f081e3e1932095939198e2a155b2d803a3e84ca4f34569df695c259faf3c0d8f0cd217ebd2dbad542b32fbb54e44aaf0b5dc739fafef2e46db8d68bfc35f44f038cb1f5231a1b5b134ae683e7f3297cc7a95bd191b310f68201450797fe3293cde1672dfeca4b493f53c768ea048a972a4cd84d39ef682957b8f28ba29487b4689b43fec2655823d9bb99ffcf31490366a9860a5d5b8e32a3b8bfeb6f55f88fb80c8c0142086f220e1f6f2862dabda58c3b6f5faa805b39cfac4b6d7ea7acdf1b0690063b0c1ea38c7c4755189966dc631055f153f71b77b114fa5c309316ba512330ea5cdb0bb176001e57461563d17259f35d0c30ef5ac838c0325402bab52c531469526ae3ee6293f7b5769d27e69fa81cd25a31cd095b126e70c57ac3169a5f585a11f1748d9d22f2564911c26a24b2153a78f3a06822f5f1963f237abeb48efd9a9cd478de579c5a0ba84d00e96fbde36d8ce20e7e948547fb6850834ff79d211830f6ee973359781d9d5008fb43a89354782fde4158177f5206ce1d38c889e99e4bb5b4ab34d6a05c42f5d719ea03dbc54adba75a3bb44a3c08c7556462f8c5b7c568a69242cf5be6098eba0a2249c1ca5b2109b6404a962abc1c159c6b48a79fb97e4a3337d99323746221297423f9bd1b12e78489e01e6a10f0fd6bba1cfd6ae1b75dbe69f8b8ee51a4e7f68ba2c407c9c0bad3892b29b0170ab75836fdd49a7ee3c2bb30f2c3d226bcec49140952170b0d160f97b30b7b7b096719538677ebb06922f26925227c8852acc107a8f173b38d96697584bd3dfe169b4073aa58a7bf371d5c4bb0eed30f08212defb3aec902d4546084176bf0f86d93cf36a4689a5e874b32d6b7d3c1e3fbcfd988c35dbc9a8d0a019ad6d7e15ec3ac97125db6abfe00beffe35a81666699a91e15945c62d646690b5b52de8b835ee9be53588fde5d63023b52b2b1f4610c237a829f5901a46042963cce7b85aa040adde02985e14e23c4eadb75221c607d24672e244c66c9c24c3cb7fd90bb23295c9d3d9da516bce3dd462d6660f9f91ef0618a4d4d3d6668c5d1e2e8ed433ebfe0762beb743324e11608f8b14b69ce4c221c1654ba4992a5af2d949c2939f95d1c8fb767af2a843cc7c78f57259d5c0c6ca83fca41ef5ecc4eeeea93e4518c24d3040f2cd90df3e535e989e606fa109e2c453ed7353db1cdb27137f005f9dd8d2aebfb7255a6098b690215e100cfe44ca0f2745fced48322bc9667ab16d2e1c0ff491b96a17b833d4fd44d31c2230ac835796e063ab03000f04f15c70560033763a48552cceaacade9ca5c8055f3745e179068a287183f2bc3ef6327dec5ac7cf7b052ef5a8873e697efde089688f43be464827c2fa83eb531b3674145e95c699c82990e684967dad319d9f64ab16cd9fe9b6c41232ac4ae3795fd8a76aa9b02e970242061c6da45a2af74ad9cb2a79935c92625e242f4bc7fce54d5c10a9e61f875162fa651b66057ba036f062d6d39d0502b93a5640b78c6c2fd20b02ff83676a87a94945d476c349803ea4fe60cdcea65bb2629e2bc09d4472ec63422dee2052f098deaf5531e6c9bed6672a8b699802efe0cded80c8455f585d1ba633d281f1a21adab48e63b44e0c2a4d7608cf98aabf8adc86bcb8f61e8b06cd2385f82e0a3cdd03cab152d5951859c4532f9168e78f17ba2a5772780327dcf4e62b4d26e443762fc488ae4cd4d1156dbd5782595cfd7697a514abc9b160c9ccf08edc86134a755b90e9bb543511e888e3157721a52d1bc5db33029fb335ea2114e21c03368c8d7f4d827960641772a4a32a738df60d19ec77ab09d22f57cc2523b9503b3f5b1cebc5ae15f885f159842db7359a1c89d3d82d3407068f15b6739626eb8c521fc8c5c7491f945d49f14e6989da340bdf49e7f8a792747aa658bc114143ba93f26022d001735b744639bbf22aab2a1851cfc934f9c69d3764fdea3d23db17998e6138cfd7cd9e9a47cb74193bd71aaf28cdd9d1eb595125546a4f4357ebbd1f410e3bf8557892de68509b5b98c5c229e942c910fdd3e54cb6ad54d8dd886cb97ecc06d1e401b8395d0bcb0db9a031dc66c9294f9053c68fc42042b1fa1671fc7d510b70916c0139cfebe3a91244527ce9439860cedb30908197be851cbd1d3b18ca541358449fb34fb5cd569630ed5f67b8795e87828f2ce3becfe457579d82333b0bbab094de391e1f8157bd431e365ca864630932bbebb48f45f8134424e18ab455029b54b19e2f3bfec5e44ad0ea5c03f53d8f925b635838aa7015a7c9e325bdfaff966ea9512dd50f87c8995cea7561c23f4fb06d964ab8f1913a6ca17e4ca60d6bc078e1784f89c673c91d955bcf45f58ca9709579d5e3831df12cfdb7516fd21878cb54243579b9346d2de4be25f508e84b1adc78cb91c03da3c4fd59e4529189838f74f6312820620a5996b791ffcb332f847094613f2148b862034fa89d0d0ff1808d902c5d1af64d5522492d61ecde4c73be89a33782cef1acc1dc327fb2eb9d17642209b85aa8b1dc57cbf067c7aa29da6b7e157d23e171d3ae6f3855834071791402c851ff2dd67109979f7ee5e09e64b4eefdee7112b55ce200bb8c8051e3428c305fa1d576bffbb25a70eb571168fc60dadd928b10cfd07de80a85b8df3edc372d488c21f0d5787611cc6fb73aeeb6f920a109294b49d3870f90de3b360d14df77ef95640bcac7a4dbca901a31db83e83f5c59ce327207ea9b27c3b978d30d53865c1b84764f025e8732d5007554ce5c9cc410b2eefd7e4d990c538557606a6bc47577a43768d30aa3e8598fd6f4fe7ac439f3931c58fd69d90765ac9f456ac7de085e14a0898c4557f5d3baaae07edc607de6900146b97b35aae570153dc107815ef9febdd4fd567d637fee8f8bfb4b3413ea6aead4846ab733a04f1e4bc32a3bbb1c16baf8d0bdb9ccb82fe46479ccfd040b5e64064e539b39c66e4501dc822873ac6119a4a112a1f7cd6df0e5f84356ce853ced34ce69a9e7383534983c51c50269bf8b9586a0e5ba905fd3bce080b00e7f7d48e55f489479b5771e995fb020e58feb74af65c3ee76aa4e69b5ba8bda249a1b2d62c08d418c3635d061846040843991ab475473da85d94981fb84425e7ad951ce0a42be642fe658b7aaae72b147cdc086c24b1571eb2272e2a72b15660d854ebe19ec7d9ab7ea17800d0b6ae727b39217467c662ba08e6f19193951eeff02806a7843eb5c71b2f04dcb605ecedc5128cd67703038c44bf20fe06f3ed8c1368fe38e72944d5c52fca46a45fd48d8fe5da64183d4d62ec01aa3d9d672ec67a01c17f21e02525f0513cce030c664fc8784763086608bc8099c204c255ffed1daf432ceb45fbd135e21e8190c5bfee192171faa77520481e69e87b7f76790bef76cb8d3c88f5c6e32fc59e7bd45351d66696b61d9f40726fb9a98000b68738cf7e34b98b6a4aaa2ac1d7b1407db89783f8077103ea9c9e89247eae078adfb36e21474c3bb1fe0c87687c6233a533a01e1081b93a3521d339f39c075609bace531994988ae314f77fc6034113a138c67eb7e03750cbec8d28bd21afedfefa8f091619ae500b4ca4599d019dc8ca4bf118d70b8676dfc796a4f6d986adba4c8574ed4abaea5465466220e5e53dc8fcb395d1e59d278673cbc4e3f40658df98ac2fd126a94922879e1a3be91c1acc20803c35fa764abbadab07bde85ff4bd9e0fb6f06baf5bf42b8a2cbf6c2f62606becc361552921a12d6c8236fde84db4bddac77e8872478cffc4e148c1c7acfedf6b17d98731c2de36f3cbef1f6f781a940e0874d5b74535bbe066b53064d43b13926570a9e1c4e6da206c8bd252caf2b62e7d223f7ac12939137f330be59374d7295a6c2dff92e07c727510e48d970593e47229fc8bc3bd5b8ea780dacff4d23063df65feda5f8f65b17a333e532acad7916780c74d6a70d38b367f3f6f4e947b85fc15235bbe46b26495d2780098db853a931377cfbedea620f2355ca21e81ce9e0078b0dd6cb70f23ed558682be3b3d594eefe85344e1f275428b316cc088995939298f2a2d15ac9b676ac3e9cb92f2a64dec7732a91fc761aa1b126ea575e3953177da6e1cd78faea824665330a81d9e24572b9860bf0aba4df8bd5d4e3e2c72bbfbb2a985c7ae2f077951fe8401e1d156ecada1e353817b20f41e0b2460a0caaf2b36d6a7f1b35125d797dcc714421027d14171765a646071ed952b6a5294eecf6a3a71c104c843a4a8b3efcc27467b20cb0a94abf5802229ea4d8312783e78791a50b3c0a88fe6497198cd4bf470dac46f34e50019fdee2040cfe99124b312b1122b83e51d878877cec0855f1158c445cfdc2253f4389d5e3a8ba1669abb5976a4617e85f543da9f5e30b10ca7481c8185392782b46fb0a0e5ab408b2945e3c79a1cc49fb7c27254a9b540e7397a5b655bf7e4f83184db32a128aa2e00a624d7dcd6b77efd151f1e5cba8890af9170fa06c555715dc1787e995ad19270973ae95b88dcafdaf62c28843d3f8b9c78dc8e37d911dab3f7ee9d4c7389c654bdcfc05056b360020140e57e31473258a4081e2a708f7caba90c356d0847098fc0762484086aba898a60b023d6a3060402406240748785d51caff52a0ef3dc2a45dadf80ac18502d24422a8cbb10192b88f4e9160206b2ed3e04114f2a339df269e2c36b8613ce37087471701755330cb559575366ee0fa2d3afcda32eede6dd906345daaa04812198e96c42239c242edb90059709f497da5b87705384aef2af22dc2edfef3c00d8c9156d8b3163fe7a7779e04f04911a8b934fb3072eee844484fede5e2ee96d338eefe2da986067ffe0218ada7de1d0e42d823d6b033918278888ba0608ab8f7be997bdf263689a36f5204c802ad836363779b4b0d6ce5083df0b98a2e2c700062a4fa5e57bc73bd45357e01d90c7954bc6904d1ce8166a9168da39a60c5cae8119bb6b9ab074fb2d0aee384fc2c0e4806811d6002b4e2401e7430b50cb0e8075f33d5386aecde256e169d95e2f9c6556c08ba042e68a53ce8aca9cc02818f7382f150dc04de0019b19c7a3ab0d72d6ed013d7a115d74b279f71fd6effef34049877e0b11e0659be938a5de684eaf23513095eb4a1bdf536c3c01a4655c4b4a0673214cef29a481d06a02cc9a5bfc7b8d846c33484cd67b1de98f60b69918f177b64558ca567a6237d35ff01771a42320ce02bb98f3e4ad4ac7db75611bd9961eac662a38b1f785970c99f3dd105ff586f61301c48d66708cbf7d53a733e357b6c256e8b73f0e1305a0bc137989e521100c2ee6259e607fe12198e8bcb988b0854668e40d7cae6adc3ba40ba121b7319d06d988a073d03097b9f5c1c07284b6473ae57bf154811b77baceb0412b8a6983bdd0ccd9e3bf014e520009cb26d5780eabef1bbafa5e25d41098a54c47fce8b68d395291d54284d33aa50b9664d1510b467c8a539361ca9a4448bc01fcb4c4e3ef475e8afb46a494ae13ee9ea8a1266825fba7f32b9712fde252698a68359b50141d90f5c4a06283ddb54ad7e1412ac5ebb12501f7a82b2a7f27b2dbab626c3db4074523b3211d3182ea261397a6f7b187cf2b8a356ded10812f1d305169aedf79b5ff1cf7c2d6e86ee11f28e96aa63b5a03f59fc960ac7d0572e91dda61905c0711a9b26344a2a10aa2041f2b13cb1a9a9a27774b6d0deddc9d81ea1b142ad7b72be47991f2c9261d6708156e38d00b074020766eb0c494392d65b82ca65f7c3352f9bb78325ecb6df596c8ae57826b08ccd6f1d529d2e25925c1ac972425bedeb88a5d0e3138ddc434da3462ebdf6b1239a21f141ec62cbe4bb993ba253b55a76d30fac19c2c1384ef6b9746c07787aa1fe913a1348390bd8c1f386a08c77cf7106c927ce24dffc9d6ee1b32354d95ed2923482531de6b390bf0f5eb80276e90e7ed11131c848bbabec4d317236269a0a3a7cbe0f1272f93949ca6d23ea2a7ee3f697791aab71533423066d400000000000000000000000000000000000000000000000000000000050e181f23292c2f").unwrap(); - assert_eq!(sig.len(), MLDSA87_SIG_LEN); - - if MLDSA87::verify(&mldsa87_pk, msg, None, &sig).is_ok() { - eprintln!("Verification succeeded!"); - } else { - panic!("Verification failed! -- figure that out"); - } -} - - - -fn main() { - // bench_do_nothing(); - // bench_mldsa44_keygen(); - // bench_mldsa44_lowmem_keygen(); - // bench_mldsa65_keygen(); - // bench_mldsa65_lowmemory_keygen() - // bench_mldsa87_keygen(); - // bench_mldsa87_lowmemory_keygen() - // bench_mldsa44_sign(); - // bench_mldsa44_lowmemory_sign(); - // bench_mldsa65_sign(); - // bench_mldsa65_lowmemory_sign(); - // bench_mldsa87_sign(); - // bench_mldsa87_lowmemory_sign(); - // bench_mldsa44_verify(); - // bench_mldsa44_lowmemory_verify(); - // bench_mldsa65_verify(); - // bench_mldsa65_lowmemory_verify(); - // bench_mldsa87_verify(); - bench_mldsa87_lowmemory_verify(); -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index b46df8cd..ae250f33 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ +pub use bouncycastle_aes as aes; +pub use bouncycastle_ascon as ascon; pub use bouncycastle_base64 as base64; +pub use bouncycastle_cipher as cipher; pub use bouncycastle_core as core; pub use bouncycastle_factory as factory; pub use bouncycastle_hex as hex; @@ -11,3 +14,4 @@ pub use bouncycastle_mlkem_lowmemory as mlkem_lowmemory; pub use bouncycastle_rng as rng; pub use bouncycastle_sha2 as sha2; pub use bouncycastle_sha3 as sha3; +pub use bouncycastle_sm3 as sm3;