diff --git a/.claude/skills/commit-range-report/SKILL.md b/.claude/skills/commit-range-report/SKILL.md new file mode 100644 index 00000000..ed420440 --- /dev/null +++ b/.claude/skills/commit-range-report/SKILL.md @@ -0,0 +1,57 @@ +--- +name: commit-range-report +description: Write a Markdown report summarising a range of commits on the current branch - branch name and commit list, public API changes and new functionality with code examples, then a per-commit summary. Use when asked to report on, summarise or document the commits since a given commit or between two commits. +--- + +# Commit range report + +Produce a `.md` report for the commits from a start commit to an end commit (default: the branch +head), in this fixed structure: + +1. **Title and preamble** — one sentence on what the range delivers as a whole. +2. **Branch and commits** — the branch name, then a table of every commit in the range with its + full SHA and subject, oldest first. Note how they got there (squash merge of PR #N, cherry-pick, + new work) when the subjects say so. +3. **Public API changes and new functionality** — grouped by crate, describing the API *as it is at + the end of the range*, not each intermediate shape. For every new or changed public trait, type, + alias or CLI subcommand: a short prose explanation of what it is for and any design rule behind + it, then a code example. Traits are shown as their signatures (`pub trait ... { fn ...; }`); + types are shown in use, end to end (construct a key, call the API, assert the result). Include + the CLI with shell examples when subcommands were added. +4. **Summary of each commit** — one paragraph per commit, numbered to match the table: what changed, + why, how it was verified, and the `files changed, insertions, deletions` line from `git show --stat`. +5. **Verification at the head** — formatting, tests, docs, and any vector suites that ran. + +## Arguments + +`$ARGUMENTS` is ` []`. The start commit is **included** in the range. If the end +is omitted use `HEAD`. If no argument is given, ask for the start commit. + +## Procedure + +Gather facts from the tree and git, never from memory of the session: + +```sh +git rev-parse --abbrev-ref HEAD +git log --reverse --format='%H %s' ~1.. +for c in $(git log --reverse --format=%h ~1..); do echo "$c: $(git show --stat --format= $c | tail -1)"; done +git diff --stat ~1 # the whole range's footprint +``` + +For the API section, read the *current* source of every public item the range touched: trait +definitions (`awk '/^pub trait NAME/{p=1} p{print} p&&/^}/{exit}' file`), `pub use` / `pub struct` / +`pub type` lines, umbrella re-exports in `src/lib.rs`, and the CLI's `--help` output. Prefer taking +code examples from the crate's own doctests, since those are known to compile; adapt them minimally. +Quote spec citations exactly as the code does. Do not describe an API shape that a later commit in +the range replaced, except in the per-commit summary where it is history. + +For the per-commit summaries, read each commit's message and stat; where a commit was a squash merge +or a cherry-pick with conflict resolution, say how the conflicts were resolved if the message or the +diff makes it clear. + +## Output + +Save the report as `local/__report.md` unless the user names a path (`local/` is +excluded from git on this checkout via `.git/info/exclude`; create it if absent), and leave it +uncommitted unless asked to commit it. Tell the user where it is. Keep the prose +in the house style: short sentences, one idea each, code only in fenced blocks, no em-dashes. diff --git a/.gitignore b/.gitignore index 6d42084d..c1ef8598 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,13 @@ mutants.out*/ .idea/ .vscode/ + +# Claude Code: ignore personal/local state, but share team tooling +# (skills, slash commands, subagents, and project settings.json). +.claude/* +!.claude/settings.json +!.claude/skills/ +!.claude/commands/ +!.claude/agents/ +.claude/settings.local.json +.claude 2/ diff --git a/CLAUDE.md b/CLAUDE.md index 6f858b53..47afcb05 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,8 +41,8 @@ cargo run --release -p mem_usage_benches --bin bench_mldsa_mem_usage The workspace has three 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/*` — one sub-crate per primitive (`sha2`, `sha3`, `sm3`, `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::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). diff --git a/Cargo.toml b/Cargo.toml index 82b379fe..1a2e2714 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,10 @@ version = "0.1.3" # *** Internal Dependencies *** bouncycastle = { path = "./" } +bouncycastle-aes-lowmemory = { path = "./crypto/aes-lowmemory" } +bouncycastle-ascon = { path = "./crypto/ascon" } bouncycastle-base64 = { path = "./crypto/base64" } +bouncycastle-modes = { path = "./crypto/modes" } bouncycastle-core = { path = "crypto/core" } bouncycastle-core-test-framework = { path = "./crypto/core-test-framework" } bouncycastle-factory = { path = "./crypto/factory" } @@ -20,9 +23,11 @@ bouncycastle-mlkem = { path = "./crypto/mlkem" } bouncycastle-mlkem-lowmemory = { path = "./crypto/mlkem-lowmemory" } bouncycastle-mldsa = { path = "./crypto/mldsa" } bouncycastle-mldsa-lowmemory = { path = "./crypto/mldsa-lowmemory" } +bouncycastle-padding = { path = "./crypto/padding" } bouncycastle-rng = { path = "./crypto/rng" } bouncycastle-sha2 = { path = "./crypto/sha2" } bouncycastle-sha3 = { path = "./crypto/sha3" } +bouncycastle-sm3 = { path = "./crypto/sm3" } bouncycastle-utils = { path = "./crypto/utils" } @@ -41,6 +46,8 @@ version.workspace = true edition.workspace = true [dependencies] +bouncycastle-aes-lowmemory.workspace = true +bouncycastle-ascon.workspace = true bouncycastle-base64.workspace = true bouncycastle-core.workspace = true bouncycastle-factory.workspace = true @@ -51,6 +58,9 @@ bouncycastle-mldsa.workspace = true bouncycastle-mldsa-lowmemory.workspace = true bouncycastle-mlkem.workspace = true bouncycastle-mlkem-lowmemory.workspace = true +bouncycastle-modes.workspace = true +bouncycastle-padding.workspace = true bouncycastle-rng.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true +bouncycastle-sm3.workspace = true diff --git a/alpha_0.1.3_release_notes.md b/alpha_0.1.3_release_notes.md index 57f9e97c..3c23fbc3 100644 --- a/alpha_0.1.3_release_notes.md +++ b/alpha_0.1.3_release_notes.md @@ -2,8 +2,393 @@ ## Major features +* New algorithms added to crypto/ (PR #89): + * sm3 -- the SM3 hash (GB/T 32905-2016 / ISO/IEC 10118-3:2018), ported from bc-java. Implements `Hash`, + `Suspendable` and `AlgorithmOID`, supports bit-oriented (partial final byte) messages per GB/T 32905-2016 s. 5.2 + with the partial byte in ASN.1 BIT STRING order like SHA-2/SHA-3, and is registered in `HashFactory` + (`"SM3"`) with a `bc-rust sm3` CLI subcommand. + * HMAC-SM3, in the hmac crate, registered in `MACFactory` (`"HMAC-SM3"`) with a `bc-rust hmac-sm3` CLI subcommand. + * Test vectors are the GB/T 32905-2016 Appendix A examples plus the bc-java `SM3DigestTest` / `HMac` vectors, with + additional digests cross-checked against OpenSSL and bc-java. + +New crate `bouncycastle-aes-lowmemory` (`bouncycastle::aes_lowmemory`): AES-128/192/256 as a raw keyed block +permutation (NIST FIPS 197), re-exported from the umbrella crate. + +* **Constant-time and table-free.** The S-box is evaluated as a Boolean circuit -- the 113-gate Boyar-Peralta + straight-line program, 32 AND / 77 XOR / 4 XNOR -- over eight `u32` bit-planes, so there is no secret-indexed + memory access and no secret-dependent branch anywhere, including in the key schedule. A table-driven "light" + AES that removes the tables only from the cipher still leaks through `SUBWORD()` in the expansion. +* **Low memory.** No lookup tables at all (0 bytes, against 512 bytes for BC Java's `AESLightEngine` and 2-8 KiB + for T-table engines) and no heap allocation. The only persistent state is the key schedule, stored bit-sliced + in a compressed form that is exactly the FIPS 197 Sec 5.2 size: `Aes128` 176 B, `Aes192` 208 B, `Aes256` 240 B. +* **Both directions from one value.** Decryption follows FIPS 197 Algorithm 3 (the straight inverse cipher) rather + than the equivalent inverse cipher of Sec 5.3.5, so it uses the unmodified key schedule -- one stored schedule + encrypts and decrypts, with no second copy and no transformation at construction time. +* **Two-block entry points.** The bit-sliced state holds two blocks, so `encrypt_blocks2` / `decrypt_blocks2` are + the natural unit of work and roughly double single-block throughput. `encrypt_block` / `decrypt_block` are + provided but do twice the necessary work; modes whose blocks are independent (CTR, and CBC/CFB decryption) + should prefer the pair form. +* Verified against FIPS 197 Appendix A.1/A.2/A.3 (every schedule word), FIPS 197 Appendix B, an exhaustive check + of all 256 S-box and inverse S-box inputs against Tables 4 and 6, SP 800-38A Appendix F.1 (ECB, all three key + lengths, both directions), and 2138 NIST ACVP `ACVP-AES-ECB` cases from `bc-test-data` (skipped with a warning + if that repository is not checked out). +* Deliberately ships no CLI subcommand, no factory entry and no `core` cipher-trait impls: a raw permutation can + only offer ECB, and those are mode-of-operation concerns. `Algorithm` is implemented (name and security + strength); per-mode OIDs and the `BlockCipherEncryptor` / `BlockCipherDecryptor` impls belong to the mode crates. +* Ships the type aliases `AES_CBC_128` / `AES_CBC_192` / `AES_CBC_256`, `AES_CFB_128` / + `AES_CFB_192` / `AES_CFB_256` and `AES_ECB_128` / `AES_ECB_192` / `AES_ECB_256`, which fill in the + const parameters of `bouncycastle-modes`' `Cbc`, `Cfb` and `Ecb` and leave the direction as the type parameter. They are aliases only -- no new engine + code, and each one's doctest round-trips and shows that a misaligned length fails to compile. + +New crate `bouncycastle-modes` (`bouncycastle::modes`): block cipher modes of operation +(NIST SP 800-38A), providing **CBC** (Sec 6.2) and **CFB128** (Sec 6.3). Re-exported from the +umbrella crate. + +* `Cbc` and `Cfb` over any + `ElectronicCodeBook`, so the crate depends on no concrete cipher. The direction is a type parameter: + `BlockCipherEncryptor` is implemented only for `<_, Encrypting, _, _>` and `BlockCipherDecryptor` + only for `<_, Decrypting, _, _>`, making a wrong-direction call a compile error rather than a + runtime check. The two types have identical APIs and identical size, so swapping one for the other + is a one-word change. +* **The IV is generated, never accepted.** SP 800-38A Sec 5.3 requires the CBC *and CFB* IV to be + *unpredictable*, not merely unique, so `do_encrypt_init` draws one from the library's default + OS-backed DRBG (Appendix C's second recommended method) and returns it; there is no API for + supplying your own. Known-answer tests drive `do_encrypt_init_rng` with a fixed-output test RNG. + This matters more for CFB than for CBC: CFB XORs a keystream, so a repeated key-and-IV pair leaks + `P1 XOR P1'` outright rather than merely whether the blocks were equal. +* **Parallel decryption.** Sec 6.2 notes CBC decryption's inverse cipher calls can run in + parallel, so `do_decrypt_blocks` walks the ciphertext in eights through + `ElectronicCodeBook::decrypt_blocks8`, then pairs through `decrypt_blocks2`, then a one-block + remainder. A toy permutation that rotates its eight results proves the eight path is taken, and + only for full eights. Measured against an + otherwise identical permutation that does not override the pair methods, this is **1.83x** the + decryption throughput (67.9 vs 37.1 MiB/s, AES-128, 16 KiB, N=8). CBC encryption is serial by + construction and does not use it. +* Strictly block-aligned, as Sec 5.2 requires of CBC. Arbitrary-length data goes through + `bouncycastle-padding`'s `PaddedEncryptor` / `PaddedDecryptor`, which wrap either mode; no padding + logic lives in this crate. `crypto/modes/tests/cfb_tests.rs` round-trips every length from 0 to + `3 * BLOCK_LEN + 1` through PKCS7 to pin that the two crates compose. +* Verified against all six SP 800-38A Appendix F.2 vectors (CBC-AES128/192/256, Encrypt and + Decrypt), each checked in one call, one block at a time, in a `3 + 1` grouping that exercises the + pair remainder, and through the `_out` variant. Appendix D error propagation is tested + exhaustively for the IV (every one of the 128 bit positions flips exactly its own bit of P1) and + for a ciphertext bit error (affects exactly two blocks). +* Also verified against the **2150 NIST ACVP `ACVP-AES-CBC` AFT cases** from `bc-test-data` (all + three key lengths, both directions, 60 of them spanning 2-10 blocks). Each case is run twice -- + block by block, and in pairs with a one-block remainder -- so the `decrypt_blocks2` path is + exercised against real vectors, not only against the toy permutation. Unlike the ECB response + file, the CBC one carries only the answer against a `tcId`, so the request and response files are + joined; the 6 MCT groups are skipped and the count reported. These vectors were already in + `bc-test-data` and previously unused. +CFB (`Cfb`), SP 800-38A Sec 6.3: + +* **Full-block segment only.** Sec 6.3 parameterises CFB by a segment size `s` with `1 <= s <= b`; + `Cfb` implements `s = b` -- CFB128 for AES -- because that is the only segment size that is + block-aligned and therefore the only one that fits `BlockCipherEncryptor` / + `BlockCipherDecryptor`. With `s = b` the spec's `LSB_{b-s}(I_{j-1}) | C#_{j-1}` collapses to + `Ij = C_{j-1}` and `MSB_s(Oj)` to `Oj`, which the module docs derive step by step. **CFB8 and + CFB1 are different, non-interoperable modes and are not provided**; they need a `StreamCipher` + shape, and both the crate docs and the CLI help say so explicitly. +* **Decryption uses the forward cipher function.** Sec 6.3 applies `CIPH_K` in both directions, so + `Cfb<_, Decrypting, _, _>` never calls `decrypt_block` or `decrypt_blocks2`. This is pinned by a + test permutation whose inverse methods panic, run over both the pair and single-block paths -- so + the claim is enforced rather than merely documented. +* **Parallel decryption**, via `encrypt_blocks8` / `encrypt_blocks2` (eights, then pairs, then a single block, like CBC): Sec 6.3 notes CFB decryption's forward cipher + calls "can be performed in parallel if the input blocks are first constructed (in series) from the + IV and the ciphertext", and with `s = b` those input blocks simply *are* the IV followed by the + ciphertext. Measured against an otherwise identical permutation that does not override the pair + methods, this is **2.08x** the decryption throughput (110.9 vs 53.3 MiB/s, AES-128, 16 KiB, N=8). + In the same run CFB decryption was **1.37x** CBC decryption (110.9 vs 80.8 MiB/s), because the + bit-sliced engine's forward direction is cheaper than its inverse and CFB only ever needs the + forward one. CFB encryption is serial by construction and does not use the pair path -- verified, + not assumed: the swapped-pair test permutation produces identical ciphertext under `Cfb` encrypt. +* Same size as `Cbc` -- one permutation plus one block of feedback (192/224/256 B for + AES-128/192/256) -- because the keystream block `Oj` is recomputed per call and lives only in a + local, so no keystream outlives the call that used it. +* Verified against all six SP 800-38A **Appendix F.3.13-F.3.18** vectors (CFB128-AES128/192/256, + Encrypt and Decrypt) in the same four groupings as CBC. F.3 additionally tabulates the *output + blocks* -- the keystream -- so those are checked against the raw permutation too + (`Oj == CIPH_K(I_j)` and `Cj == Pj XOR Oj` for all four segments of all three key lengths), which + pins the mode's internals and not just its final output. As a transcription cross-check, CFB128 + is required to agree with **Appendix F.4.1 (OFB)** on the first block -- both compute + `C1 = P1 XOR CIPH_K(IV)` -- and to disagree from the second. +* Also verified against the **2138 NIST ACVP `ACVP-AES-CFB128` AFT cases** from `bc-test-data` (all + three key lengths, both directions, 54 of them spanning 2-10 blocks), each run twice, block by + block and in pairs with a remainder. The 6 MCT groups are skipped and the count reported. These + vectors were already in `bc-test-data` and previously unused. +* Appendix D error propagation is tested in the direction that distinguishes CFB from CBC. Table D.2 + gives CFB "SBE in the decryption of Cj": every one of the 128 bit positions of `C2` is flipped and + required to flip *exactly* that bit of `P2` (the block the attacker aimed at, unlike CBC where it + lands in `P3`), to randomise `P3`, and to leave `P1` and `P4` untouched. The IV case is checked + with real AES, where a corrupted IV must *randomise* `P1` rather than flip a bit in place, and + must not affect any later block -- with `s = b`, Appendix D's "first `i/s` (rounding up)" + segments is one segment for every bit position. +* Mutation-tested: `cargo mutants -p bouncycastle-modes` reports **0 surviving mutants** (72 + mutants, 39 caught, 33 unviable), including every `^`-to-`|`/`&` substitution and every + keystream-stubbing mutant in `cfb.rs`. +* Still not implemented, and listed in the crate docs: the CFB segment sizes below the block size + (`s = 8`, `s = 1`), and ECB, OFB and CTR. + +`cli`: six new subcommands -- `aes128-cbc`, `aes192-cbc`, `aes256-cbc`, `aes128-cfb`, `aes192-cfb` +and `aes256-cfb` -- each taking `encrypt` or `decrypt` and streaming stdin to stdout in 1 KiB +chunks. + +* All the mode-independent plumbing -- key loading, stdin framing, block-alignment enforcement, + hex/binary output -- lives once in `cli/src/block_mode_cmd.rs`, generic over the mode via + `BlockCipherEncryptor` / `BlockCipherDecryptor`. `aes_cbc_cmd.rs` and `aes_cfb_cmd.rs` are thin + dispatchers over it, so the two commands cannot drift apart on the parts that affect correctness. +* Key from `--key` (hex) or `--key-file` (binary or hex), with the usual note that secrets on the + command line end up in shell history. The key length must match the variant exactly. +* **The IV travels in the ciphertext**: since there is no API for supplying one, `encrypt` writes + the generated IV as the first 16 bytes of its output and `decrypt` reads it back from the first + 16 bytes of its input, so `encrypt | decrypt` composes with no `--iv` flag anywhere. The IV need + not be secret (SP 800-38A Sec 5.3), so this is sound. +* Input must be a whole number of 16-byte blocks. Unaligned input is rejected with a message saying + the commands apply no padding rather than being silently padded. +* The `-cfb` commands are **CFB128**, and both the subcommand help and the alignment error name the + segment size, because `CFB8` and `CFB1` are different modes that would silently produce + incompatible output. +* Reads need not respect block boundaries: bytes accumulate in a 1 KiB buffer that goes through the flat + `do_*_out::<1024>` when full, and the whole-block remainder at end of input goes one block at a time; verified by + round-tripping 64 KiB through `dd bs=3`. +* Verified against SP 800-38A F.2 (CBC) and F.3.13/F.3.15/F.3.17 (CFB128): prepending the spec's IV + to the spec's ciphertext and running `decrypt` reproduces the spec's plaintext for all three key + lengths in both modes. The CBC `encrypt` direction was cross-checked against an independent CBC + implementation under the IV the CLI generated. +* `cli/tests/aes_cbc_cli_tests.rs` (16 tests) drives the built binary as a subprocess via + `CARGO_BIN_EXE_bc-rust`, so all of the above is asserted by `cargo test` rather than by hand: + the F.2 vectors, round trips across the chunk boundary, a fresh IV per invocation, hex/binary + agreement, `--key-file` in both hex and binary, and every error path with its message. +* `cli/tests/aes_cfb_cli_tests.rs` (18 tests) mirrors that suite -- the shared plumbing is generic + over the mode, so a wiring mistake in the CFB dispatcher would not show up in the CBC tests -- and + adds three CFB-specific checks: the F.3 vectors, the Appendix D single-bit malleability observed + end to end through the pipe, and a guard that a CFB ciphertext does not decrypt as CBC or vice + versa (neither mode is authenticated, so the mismatch is otherwise silent). + +ECB (`Ecb`), SP 800-38A Sec 6.1: + +* **The raw permutation with the mode API, for interoperability only.** `Ecb` implements + `BlockCipherEncryptor` / `BlockCipherDecryptor` with `INIT_DATA_LEN = 0`: `do_encrypt_init` returns an empty array and + draws nothing from the RNG, `do_decrypt_init` takes one. Same direction typing, streaming and one-shot methods, + compile-time length checks and padding-layer composition as `Cbc` / `Cfb`, so a key-wrapping scheme, a legacy protocol + or a test-vector harness that needs ECB can use it through the same interface. The crate docs, the type docs and the + CLI help all say the same thing about it: **not a confidentiality mode for data** (Sec 6.1: "any given plaintext block + always gets encrypted to the same ciphertext block"). One block smaller than `Cbc` / `Cfb`, since nothing chains + (176 / 208 / 240 B for AES-128/192/256). +* **Both directions batch.** Sec 6.1 allows forward and inverse cipher calls "to be computed in parallel", so encryption + as well as decryption walks the blocks through `ElectronicCodeBook::{en,de}crypt_blocks8`, then the pair methods, then + a single block. The swapped-pair and rotated-eight test permutations prove both paths are taken in both directions. +* `aes128-ecb` / `aes192-ecb` / `aes256-ecb` CLI subcommands over the shared block-mode plumbing, which is now generic + over `INIT_DATA_LEN`: nothing is prepended on `encrypt` or consumed on `decrypt`, so output is exactly as long as + input. The per-command help carries the warning. +* Verified against all six SP 800-38A **Appendix F.1** vectors (ECB-AES128/192/256, Encrypt and Decrypt) in five + groupings each -- and, since there is no IV, `encrypt` is checked against the published ciphertext too, through the + streaming API and the one-shot. Each tabulated ciphertext block is also checked to be `CIPH_K` of its plaintext block + through the raw permutation. The **NIST ACVP `ACVP-AES-ECB`** set (2138 AFT cases) already used by `aes-lowmemory` + is run again through the mode API, both directions, in three groupings including one that reaches the eight-block + path. Structural tests pin the Sec 6.1 equations against a reference over the toy permutation, determinism and the + codebook property, Appendix D error propagation (a corrupted block randomises itself and nothing else, checked over + all 128 bit positions with real AES), the empty init data, and composition with `bouncycastle-padding`. + +`core`: new `ElectronicCodeBook` trait (`crypto/core/src/traits.rs`), the raw +keyed permutation -- `CIPH_K` / `CIPH^-1_K` of SP 800-38A Sec 5.1 -- that a mode is built on. +`new`, `encrypt_block`, `decrypt_block`, plus provided `encrypt_blocks2` / `decrypt_blocks2` that +default to two single-block calls and `encrypt_blocks8` / `decrypt_blocks8` that default to four pair +calls, all of which bit-sliced implementations override (AES the pair form, SM4 both). The block methods +are infallible; only `new` can fail, and only on the key. `bouncycastle-aes-lowmemory` implements +it for all three key lengths (the data-encryption traits are still deliberately not implemented +there). + +`core`: new `SymmetricCipherEncryptor` and +`SymmetricCipherDecryptor` traits, the arbitrary-length data API a +caller uses, as opposed to the block-aligned `BlockCipher*` traits a mode implements. Their shape is +taken from `PaddedEncryptor` / `PaddedDecryptor`, which now implement them: streaming +`do_{en,de}crypt_init[_rng]`, exact `update_out_len`, `do_update_out`, and a consuming `do_final` that +returns the `FINAL_LEN` trailing buffer (the padded block; a tag for an AEAD) paired with how many of its +bytes are output -- always `FINAL_LEN` except for a padding scheme that adds nothing to aligned data -- +and, for the decryptor, how many of them are data. `do_final_out`, the `_out` one-shots +(`encrypt_out[_rng]`, `decrypt_out`, with `encrypt_out_len` exact and `decrypt_out_max_len` an upper +bound, checked before any work is done) and the `std` `Vec` one-shots are provided over the streaming +methods, so an implementor writes six methods. The older one-shot-only `SymmetricCipher` trait is +unchanged for now; `AEADCipher` and `StreamCipher` still build on it and are the next to migrate. + +Testing: + +* `core-test-framework` gains `TestFrameworkSymmetricCipher::test_encryptor_decryptor`, which pins the + paired contract: one-shot round trips at every length up to a few final chunks, the `std` one-shots + against the `_out` ones, streaming in eight chunkings with `update_out_len` exact on every call, + `do_final_out` against `do_final`, a driven RNG reproducing its init data and determining the + ciphertext, corruption detection, short output buffers refused with the required length, and the + key-type and security-strength policy. The padded adapters run it. +* `core-test-framework` gains `TestFrameworkElectronicCodeBook`, which pins the trait contract: + both directions are inverses either way round, the permutation is injective, and the pair + methods are indistinguishable from two single-block calls **including their order** -- the check + that makes an override safe. +* Fixed a latent bug in `TestFrameworkBlockCipher`: it unwrapped `set_security_strength` at all + five strengths, which a key shorter than 32 bytes cannot carry, so the framework panicked for + any 16- or 24-byte key. It now skips the strengths the key length cannot hold. The bug was + invisible until now because nothing in the workspace implemented the block cipher traits. The + identical loop in `TestFrameworkSymmetricCipher` and `TestFrameworkAEADCipher` is still unfixed; + both still have no implementors, so it stays latent. (`TestFrameworkStreamCipher` has no + security-strength handling at all and is unaffected.) + +* Block cipher padding (PR #97): + * padding -- new crate (`bouncycastle-padding`, no_std, re-exported as `bouncycastle::padding`) providing `PKCS7`, + the padding scheme of RFC 5652 s. 6.3, for any block length 1..=255 (enforced at compile time). `unpad` examines + every byte with `Condition` mask arithmetic and has a single public decision point, so it does not leak a + padding oracle through timing or error detail. + * `PaddedEncryptor` / `PaddedDecryptor` adapt a block-aligned `BlockCipherEncryptor` / + `BlockCipherDecryptor` to arbitrary-length data: streaming `do_update_out` / `do_final(self)` plus one-shot + `encrypt_out` / `decrypt_out`, with exact output-length helpers. The buffered partial plaintext block is held in + a `Secret`, and the decryptor withholds one complete block until `do_final`, since only the last block carries + padding. + * `core` gains the `Padding` trait (in-place `pad(block, data_len)`, constant-time + `unpad(block) -> data_len`, and `ALWAYS_PADS`, whether the scheme appends a block to already-aligned data) and + `PaddingError { DataLengthTooLong, InvalidPadding, PaddingNotPermitted }`, wrapped as a new variant of + `SymmetricCipherError`. + * `NoPadding`: the absence of padding as a `Padding` scheme, for data that must already be a whole number of + blocks. `pad` never writes a byte and returns `PaddingNotPermitted` whenever called; `unpad` reports the whole + block as data; `ALWAYS_PADS` is false. Through `PaddedEncryptor` / `PaddedDecryptor` this *enforces* alignment + with the arbitrary-length API shape: an aligned message passes through with its length unchanged and no final + block, an unaligned one fails at `do_final` / `encrypt_out`, and an empty ciphertext decrypts to the empty + message. The test framework's `TestFrameworkSymmetricCipher` gained `required_alignment`, which makes it assert + that every unaligned length is refused. + * Tests are derived from the RFC 5652 padding rule; the adapters are driven with a toy XOR-CBC cipher implementing + the new block cipher traits, covering every data length, ten chunkings in both directions, tampering, malformed + lengths, and buffer sizing. Criterion bench included. + ## 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. + +SHA-2 (PR #88): + +* `Hash::do_final_partial_bits()` / `do_final_partial_bits_out()` are now implemented for SHA-224/256/384/512 + (FIPS 180-4 s. 5.1), bringing SHA-2 to parity with SHA-3 for messages whose length is not a multiple of 8 bits. + Previously these methods hit `unimplemented!()` -- a panic behind a `Result`-returning API. `num_partial_bits` may be + 0..=7 (0 behaves exactly as `do_final_out()`); larger values return `HashError::InvalidLength`. The trailing bits are + the most significant bits of `partial_byte`, the same convention as SHA-3 (see "Bit-oriented messages" below). +* Initial hash values are now compile-time constants (`const H0` on the params traits), removing a runtime + match-on-`OUTPUT_LEN` and its `panic!` arm. `HashAlgParams` for the public types is forwarded from the `*Params` + structs, so `OUTPUT_LEN` / `BLOCK_LEN` are defined once. +* Crate docs: fixed SHA-3/SHAKE copy-paste text, added a partial-bits usage example, "Memory Usage" and + "Security Considerations" sections, and documented the `*_NAME` constants. The 2^64-byte message-length limit is + now stated. + +Testing: + +* SHA-2 now runs the NIST CAVP SHAVS vector sets from bc-test-data (`crypto/sha2`: ShortMsg, LongMsg and Monte Carlo; + bit- and byte-oriented, ~12k cases of which ~5.4k are bit-length messages) using the same `../bc-test-data` lookup + convention as the mldsa/mlkem crates; the tests skip with a warning if the repo is not checked out. The SHAVS files + pack trailing message bits MSB-first (left-justified), which is the convention used by the API. Note that + `cargo mutants` runs in a copied tree where `../bc-test-data` does not resolve, so these tests do not contribute to + mutation coverage. + +Bit-oriented messages: + +* `Hash::do_final_partial_bits()` / `do_final_partial_bits_out()` and `XOF::absorb_last_partial_byte()` accept + `num_partial_bits` in 0..=7 (0 meaning the message ends on a byte boundary); larger values return + `HashError::InvalidLength` instead of panicking. +* The partial byte is taken as it arrives in the final octet of an ASN.1 BIT STRING (X.690 s. 8.6.2): the + `num_partial_bits` message bits are the most significant bits of `partial_byte`, leading bit first, and the low + `8 - num_partial_bits` bits (the BIT STRING's "unused bits") are ignored -- so for a BIT STRING with `unused` in + 1..=7, pass the final content octet with `num_partial_bits = 8 - unused`. The convention is the same for every hash + family; SHA-3/SHAKE reverse the bits internally into the FIPS 202 Appendix B.1 order that Keccak absorbs (bit 0 + first). `XOF::squeeze_partial_byte_final()` returns its bits the same way: in the most significant `num_bits` bits, + first output bit first, low bits zero. (Previously the API documented FIPS 202 B.1 order -- message bits in the + least significant bits, bit 0 first -- but SHA-2 in fact treated the low bits as a left-justified group, so the two + families only agreed on palindromic bit patterns. The BIT STRING convention is now applied uniformly.) +* Test vectors: the NIST CAVP SHAVS (SHA-2) bit-oriented files are left-justified and are passed to the API directly; + the SHA3VS files and the FIPS 202 example vectors use the Appendix B.1 packing and are bit-reversed by the harness. + +SHA-3 / SHAKE (PR #87): + +* Fixed `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. The existing test used + `0xFF`, which masked the second error. +* Fixed `XOF::absorb_last_partial_byte()` for `num_partial_bits == 4`: the 4 message bits plus the `1111` suffix + exactly filled a byte and the sponge did not switch to squeezing, so the first squeeze appended the suffix a second + time. Every SHAKE message with a bit length of 4 mod 8 was affected. Found by the new CAVP harness. +* `absorb_last_partial_byte()` and `do_final_partial_bits*()` now validate `num_partial_bits` before use; previously + SHA-3 accepted 8..15 and absorbed garbage, panicked for >= 16, and SHAKE rejected 0 with an error message claiming + `[0,7]`. +* Interleaving absorb -> squeeze -> absorb remains rejected with `HashError::InvalidState`; the `XOF` trait docs now + explain why (it is the duplex construction, not SHAKE). +* `HashAlgParams` for the SHA-3 types is now forwarded from the `*Params` structs, so `OUTPUT_LEN` / `BLOCK_LEN` are + defined once. Removed misleading leftover SHA-2 block-size comments. +* Crate docs gained "Memory Usage" and "Security Considerations" sections. + +Testing: + +* SHA-3 / SHAKE now run the NIST CAVP SHA3VS vector sets from bc-test-data (`crypto/sha3`: ShortMsg, LongMsg, Monte + Carlo and SHAKE VariableOut; bit- and byte-oriented, ~13k cases) using the same `../bc-test-data` lookup convention as + the mldsa/mlkem crates; the tests skip with a warning if the repo is not checked out. The vendored FIPS 202 example + vectors in `crypto/sha3/tests/data` were removed in favour of the bc-test-data copies. Note that `cargo mutants` runs + in a copied tree where `../bc-test-data` does not resolve, so these tests do not contribute to mutation coverage. + +SHA-512/224 and SHA-512/256: + +* `bouncycastle-sha2` adds SHA-512/t (FIPS 180-4 s. 5.3.6) as the generic `SHA512t`, with + `SHA512_224` and `SHA512_256` as the two NIST-approved instantiations; any other `T` fails to compile. The initial + hash value is derived at compile time by the s. 5.3.6 "SHA-512/t IV Generation Function" (the SHA-512 compression + function is now a `const fn`) and `const`-asserted against the words listed in s. 5.3.6.1 / s. 5.3.6.2. Names are + "SHA512/224" / "SHA512/256"; OIDs are id-sha512-224 { hashAlgs 5 } and id-sha512-256 { hashAlgs 6 }. Registered in + `HashFactory` and exposed as the `sha512-224` / `sha512-256` CLI subcommands. Every step of both SHA-2 compression + functions, the padding, parsing and truncation now carries a FIPS 180-4 section citation. +* `bouncycastle-hmac` adds `HMAC_SHA512_224` and `HMAC_SHA512_256` (names "HMAC-SHA512/224" / "HMAC-SHA512/256"; OIDs + id-hmacWithSHA512-224 { digestAlgorithm 12 } and id-hmacWithSHA512-256 { digestAlgorithm 13 }, RFC 8018 Appendix + B.1.2), registered in `MACFactory` and exposed as the `hmac-sha512-224` / `hmac-sha512-256` CLI subcommands. + +Testing: + +* The SHA-2 CAVP SHAVS harness (bit- and byte-oriented ShortMsg, LongMsg and Monte Carlo) now also runs the + SHA512_224 and SHA512_256 vector sets, and additionally re-feeds every whole-byte message through the streaming API + in uneven chunks. +* NIST publishes no full-length known-answer vectors for HMAC-SHA512/224 and /256; the tests use the 160-bit truncated + ACVP cases and compare the leading bytes, with full-length output cross-checked against OpenSSL. + +Housekeeping: + +* `no_std` progress: `std::marker::PhantomData` and `std::fmt` replaced with their `core::` equivalents in the SHA-3 + and Hash_DRBG crates, and the `Copy` types `KeyType` / `SecurityStrength` are now copied rather than `.clone()`d. + Removed a redundant second zeroization of the caller's output buffer in `Hash::hash_out()` / `XOF::hash_xof_out()`. + +Block cipher traits (PR #96): + +* The single `BlockCipher` streaming trait is split into `BlockCipherEncryptor` and `BlockCipherDecryptor` (mirroring + `KEMEncapsulator` / `KEMDecapsulator`) so the direction is encoded in the implementing type. Both, and + `ElectronicCodeBook`, are bounded on `Algorithm`, whose `MAX_SECURITY_STRENGTH` is the strength the `_init` + constructors enforce (a mode reports its permutation's name and strength); the `SymmetricCipher` one-shot API is no + longer a supertrait. +* The single-block `do_{en,de}crypt_block[_out]` methods are replaced by multi-block + `do_{en,de}crypt_blocks[_out]`, taking `&[[u8; BLOCK_LEN]; N]` so the block count is compile-time and + input/output lengths cannot disagree. +* `do_encrypt_init_rng(key, &mut dyn RNG)` is added alongside `do_encrypt_init`, matching the `encaps` / `encaps_rng` + pattern. +* The `do_{en,de}crypt_final[_out]` methods are removed: the traits are now strictly block-aligned, and padding of + arbitrary-length data belongs to a separate `PaddedEncryptor` / `PaddedDecryptor` layer built on top. +* One-shot static APIs are provided (default) methods implemented once in the traits -- `encrypt`, `encrypt_rng` on + `BlockCipherEncryptor` and `decrypt` on `BlockCipherDecryptor` -- so every block-aligned mode gets the + house-standard one-shot API at no cost to implementors. They take a flat `&mut [u8; LEN]` and work **in place** + (plaintext in, ciphertext out in the same bytes; `encrypt` returns the generated init data). `LEN` must be a whole + number of blocks, and this is enforced at **compile time** by an inline `const` assertion at the instantiating call + site, so there is no runtime length check and no error variant for it. Data whose length is only known at run + time goes block by block or through the padding layer. (Earlier forms took `[[u8; BLOCK_LEN]; N]`, then separate + input and output arrays; both were replaced before release.) +* The streaming API is flat and in place as well: `do_{en,de}crypt(&mut [u8; LEN])`, with the same compile-time + alignment check, are provided methods. The single block-shaped method left is the implementor hook + `do_{en,de}crypt_blocks(&mut [[u8; BLOCK_LEN]])`, which is what guarantees an implementation never sees a + partial block; an implementor writes only `do_{en,de}crypt_init[_rng]` and that hook. The hook takes a *slice* of + blocks rather than a `[[u8; BLOCK_LEN]; N]` array (it did at first): every whole number of blocks is valid, so + there is no length invariant for a const parameter to carry, and batching -- singly, in pairs, in eights -- is the + mode's decision. `do_{en,de}crypt` therefore hands the whole buffer to the hook in one call, and CBC + decryption chunks it into pairs for `decrypt_blocks2` itself. The data methods keep a + `Result` only for modes with a per-initialization data limit (counter-based modes); CBC never fails them. + +Testing: + +* The core-test-framework block cipher test now takes separate encryptor/decryptor type parameters, exercises N = 1 and + N = 2 (including mixed single/multi-block encrypt vs decrypt sequences), and checks the one-shots agree with the + streaming API and round-trip. diff --git a/cli/src/aes_cbc_cmd.rs b/cli/src/aes_cbc_cmd.rs new file mode 100644 index 00000000..d8f4a72c --- /dev/null +++ b/cli/src/aes_cbc_cmd.rs @@ -0,0 +1,68 @@ +//! 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::block_mode_cmd`], 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::block_mode_cmd::{BLOCK_LEN, BlockModeAction, decrypt_stream, encrypt_stream, load_key}; +use bouncycastle::aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::ElectronicCodeBook; +use bouncycastle::modes::{Cbc, Decrypting, Encrypting}; + +/// Names the mode in error messages. +const MODE: &str = "CBC"; + +pub(crate) fn aes128_cbc_cmd( + action: &BlockModeAction, + 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: &BlockModeAction, + 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: &BlockModeAction, + 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: &BlockModeAction, + key: &KeyMaterial, + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + match action { + BlockModeAction::Encrypt => { + encrypt_stream::, KEY_LEN, BLOCK_LEN>( + key, output_hex, MODE, + ) + } + BlockModeAction::Decrypt => { + decrypt_stream::, KEY_LEN, BLOCK_LEN>( + key, output_hex, MODE, + ) + } + } +} diff --git a/cli/src/aes_cfb_cmd.rs b/cli/src/aes_cfb_cmd.rs new file mode 100644 index 00000000..68c6be84 --- /dev/null +++ b/cli/src/aes_cfb_cmd.rs @@ -0,0 +1,79 @@ +//! AES-CFB128 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::block_mode_cmd`], shared with the `aes*-cbc` and +//! `aes*-ecb` commands. See that module 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). That is the only segment size `bouncycastle-modes` provides, because it is +//! the only block-aligned one. SP 800-38A also defines `s = 8` and `s = 1`, which are *not* +//! interoperable with these commands -- if you need `CFB8` or `CFB1`, this is not it. +//! +//! # 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::block_mode_cmd::{BLOCK_LEN, BlockModeAction, decrypt_stream, encrypt_stream, load_key}; +use bouncycastle::aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::ElectronicCodeBook; +use bouncycastle::modes::{Cfb, Decrypting, Encrypting}; + +/// Names the mode in error messages. Spelled with the segment size, because `CFB8` and `CFB1` are +/// different modes and a bare "CFB" in a diagnostic would be ambiguous. +const MODE: &str = "CFB128"; + +pub(crate) fn aes128_cfb_cmd( + action: &BlockModeAction, + 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: &BlockModeAction, + 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: &BlockModeAction, + 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: &BlockModeAction, + key: &KeyMaterial, + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + match action { + BlockModeAction::Encrypt => { + encrypt_stream::, KEY_LEN, BLOCK_LEN>( + key, output_hex, MODE, + ) + } + BlockModeAction::Decrypt => { + decrypt_stream::, KEY_LEN, BLOCK_LEN>( + key, output_hex, MODE, + ) + } + } +} diff --git a/cli/src/aes_ecb_cmd.rs b/cli/src/aes_ecb_cmd.rs new file mode 100644 index 00000000..d4dc6f4a --- /dev/null +++ b/cli/src/aes_ecb_cmd.rs @@ -0,0 +1,75 @@ +//! 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::block_mode_cmd`], 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::block_mode_cmd::{BLOCK_LEN, BlockModeAction, decrypt_stream, encrypt_stream, load_key}; +use bouncycastle::aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle::core::key_material::KeyMaterial; +use bouncycastle::core::traits::ElectronicCodeBook; +use bouncycastle::modes::{Decrypting, Ecb, Encrypting}; + +/// Names the mode in error messages. +const MODE: &str = "ECB"; + +pub(crate) fn aes128_ecb_cmd( + action: &BlockModeAction, + 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: &BlockModeAction, + 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: &BlockModeAction, + 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: &BlockModeAction, + key: &KeyMaterial, + output_hex: bool, +) where + P: ElectronicCodeBook, +{ + match action { + BlockModeAction::Encrypt => { + encrypt_stream::, KEY_LEN, 0>( + key, output_hex, MODE, + ) + } + BlockModeAction::Decrypt => { + decrypt_stream::, KEY_LEN, 0>( + key, output_hex, MODE, + ) + } + } +} diff --git a/cli/src/ascon_cmd.rs b/cli/src/ascon_cmd.rs new file mode 100644 index 00000000..d33ccb45 --- /dev/null +++ b/cli/src/ascon_cmd.rs @@ -0,0 +1,194 @@ +use std::io::{self, Read}; +use std::process::exit; + +use bouncycastle::ascon::ascon_aead128::AsconAead128; +use bouncycastle::ascon::ascon_cxof128::AsconCXof128; +use bouncycastle::ascon::ascon_hash256::AsconHash256; +use bouncycastle::ascon::ascon_xof128::AsconXof128; +use bouncycastle::core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle::core::traits::SecurityStrength; +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 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 = ciphertext||tag) or, with +/// `decrypt`, decrypts (stdin = ciphertext||tag, output = plaintext). Decryption exits with a +/// non-zero status if the authentication tag does not verify. +/// +/// 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 = require_16(load_bytes(nonce, nonce_file, "nonce"), "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, ad_opt, output_hex); + } else { + aead128_encrypt_stream(&key, &nonce, ad_opt, output_hex); + } +} + +fn aead128_encrypt_stream( + key: &KeyMaterial<16>, + nonce: &[u8; 16], + ad_opt: Option<&[u8]>, + output_hex: bool, +) { + let mut cipher = AsconAead128::new(key, nonce, ad_opt, true).unwrap(); + let mut buf = [0u8; 1024]; + loop { + let n = io::stdin().read(&mut buf).expect("Failed to read from stdin"); + if n == 0 { + break; + } + cipher.do_encrypt_update(&mut buf[..n]); + helpers::write_bytes_or_hex(&buf[..n], output_hex); + } + let tag = cipher.do_encrypt_final(); + helpers::write_bytes_or_hex(&tag, output_hex); + if output_hex { + println!(); + } +} + +/// Decrypts a stream whose final 16 bytes are the tag, which is only known once EOF is reached. +/// Holds back at most 16 bytes (the current tag candidate) in `tail`; every other byte is +/// released to `do_decrypt_update` (and written out) as soon as it is known not to be part of the +/// tag. +fn aead128_decrypt_stream( + key: &KeyMaterial<16>, + nonce: &[u8; 16], + ad_opt: Option<&[u8]>, + output_hex: bool, +) { + const TAG_LEN: usize = 16; + const CHUNK: usize = 1024; + + let mut cipher = AsconAead128::new(key, nonce, ad_opt, false).unwrap(); + let mut tail = [0u8; TAG_LEN]; + let mut tail_len = 0usize; + let mut work = [0u8; TAG_LEN + CHUNK]; + + loop { + let n = io::stdin().read(&mut work[TAG_LEN..]).expect("Failed to read from stdin"); + if n == 0 { + break; + } + work[TAG_LEN - tail_len..TAG_LEN].copy_from_slice(&tail[..tail_len]); + let total = tail_len + n; + + if total > TAG_LEN { + let releasable = total - TAG_LEN; + let window = &mut work[TAG_LEN - tail_len..TAG_LEN - tail_len + total]; + cipher.do_decrypt_update(&mut window[..releasable]); + helpers::write_bytes_or_hex(&window[..releasable], output_hex); + tail.copy_from_slice(&window[releasable..releasable + TAG_LEN]); + tail_len = TAG_LEN; + } else { + tail[..total].copy_from_slice(&work[TAG_LEN - tail_len..TAG_LEN - tail_len + total]); + tail_len = total; + } + } + + if tail_len < TAG_LEN { + eprintln!("Error: ciphertext is shorter than the 16-byte tag."); + exit(-1); + } + match cipher.do_decrypt_final(&tail) { + Ok(()) => { + if output_hex { + println!(); + } + } + Err(_) => { + eprintln!("Error: Ascon-AEAD128 authentication failed."); + exit(-1); + } + } +} diff --git a/cli/src/block_mode_cmd.rs b/cli/src/block_mode_cmd.rs new file mode 100644 index 00000000..7efe2ae4 --- /dev/null +++ b/cli/src/block_mode_cmd.rs @@ -0,0 +1,268 @@ +//! Shared plumbing for the block-cipher-mode subcommands: `aes{128,192,256}-{cbc,cfb,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`, `aes_cfb_cmd` and `aes_ecb_cmd` are thin dispatchers +//! over it, so the commands cannot drift apart on the parts that matter for correctness. +//! +//! # The IV travels in the ciphertext +//! +//! There is no `--iv` flag, and that is deliberate: `bouncycastle-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 +//! +//! All these modes 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. +//! Padding is the caller's business; the library offers `bouncycastle-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::write_bytes_or_hex; +use bouncycastle::core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle::core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, SecurityStrength}; +use bouncycastle::hex; +use clap::ValueEnum; +use std::io::{Read, Write}; +use std::process::exit; +use std::{fs, io}; + +/// 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 every mode subcommand. +#[derive(ValueEnum, Clone, Debug)] +pub(crate) enum BlockModeAction { + /// Encrypt stdin to stdout. + /// For CBC and CFB a freshly generated IV is written as the first 16 bytes of the output, so + /// that `decrypt` can read it back; ECB has no IV and writes none. Input length must be a + /// multiple of 16 bytes. + Encrypt, + /// Decrypt stdin to stdout. + /// For CBC and CFB the first 16 bytes of input are taken as the IV, as written by `encrypt`; + /// ECB has no IV and reads none. The remaining length must be a multiple of 16 bytes. + 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; try hex first, as the other commands do. + let raw = fs::read(key_file).unwrap_or_else(|e| { + eprintln!("Error: couldn't read key file '{key_file}': {e}"); + exit(-1); + }); + match hex::decode(&raw) { + Ok(decoded) => decoded, + Err(_) => raw, + } + } 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 and CFB, 0 for ECB, in which case nothing is +/// written ahead of the ciphertext. `mode` names the mode in error messages ("CBC", "CFB128", +/// "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(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(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 and CFB, 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(chunk).unwrap(); + } else { + for block in data.as_chunks_mut::().0 { + dec.do_decrypt(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) { + eprintln!( + "Error: input is not a whole number of {BLOCK_LEN}-byte blocks ({} trailing byte(s)). \ + {mode} is defined only on whole blocks (SP 800-38A Sec 5.2), and these commands apply \ + no padding, so the input must be padded by the caller.", + 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 { + println!(); + } + io::stdout().flush().unwrap_or_else(|e| { + eprintln!("Error: failed to flush stdout: {e}"); + exit(-1); + }); +} diff --git a/cli/src/helpers.rs b/cli/src/helpers.rs index 207f0ee0..2873e1e6 100644 --- a/cli/src/helpers.rs +++ b/cli/src/helpers.rs @@ -1,7 +1,7 @@ use bouncycastle::core::key_material::{ KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, }; -use bouncycastle::core::traits::SecurityStrength; +use bouncycastle::core::traits::{Hash, SecurityStrength, XOF}; use bouncycastle::hex; use std::fs::File; use std::io; @@ -116,3 +116,35 @@ pub(crate) fn parse_seed(bytes: &[u8]) -> Result { + 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); + } } } diff --git a/cli/src/main.rs b/cli/src/main.rs index c72af13a..e79cb2cc 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,3 +1,8 @@ +mod aes_cbc_cmd; +mod aes_cfb_cmd; +mod aes_ecb_cmd; +mod ascon_cmd; +mod block_mode_cmd; mod encoders_cmd; mod helpers; mod hkdf_cmd; @@ -7,9 +12,12 @@ mod mlkem_cmd; mod rng_cmd; mod sha2_cmd; mod sha3_cmd; +mod sm3_cmd; +use crate::block_mode_cmd::BlockModeAction; use crate::mac_cmd::HMACVariant; use crate::mldsa_cmd::MLDSAAction; +use crate::sha2_cmd::SHA2Variant; use clap::{Parser, Subcommand}; #[derive(Parser)] @@ -70,6 +78,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 { @@ -102,6 +126,14 @@ enum Subcommands { 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 { @@ -124,6 +156,80 @@ enum Subcommands { 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. + /// Encrypts by default (stdin = plaintext, output = ciphertext||tag); with --decrypt the + /// reverse. Decryption fails with a non-zero exit status if the tag does not verify. + /// 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. Must be unique per encryption under a given key. + #[arg(long)] + nonce: Option, + + /// A file containing the 128-bit nonce in hex or binary. + #[arg(long)] + nonce_file: Option, + + /// Associated data in hex (authenticated but not encrypted). + #[arg(long)] + ad: Option, + + /// Decrypt instead of encrypt. + #[arg(short, long)] + decrypt: bool, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// Perform HMAC-SHA256 of the content provided on stdin. /// Supports streaming update for low memory footprint. /// Note: in production uses, secrets should not be passed on the command-line because they get @@ -173,6 +279,80 @@ enum Subcommands { 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 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 @@ -271,6 +451,242 @@ enum Subcommands { 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 { + action: BlockModeAction, + + /// The 16-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte AES key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[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 { + action: BlockModeAction, + + /// The 24-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 24-byte AES key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[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 { + action: BlockModeAction, + + /// The 32-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte AES key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[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 and 1-bit CFB variants + /// are different modes and are NOT interoperable with this command. + /// + /// 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: this command is block-aligned and applies + /// no padding, so unaligned input is rejected rather than padded. + /// + /// 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 { + action: BlockModeAction, + + /// The 16-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte AES key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[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, block-alignment requirement and warnings; only the + /// key length differs. + AES192_CFB { + action: BlockModeAction, + + /// The 24-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 24-byte AES key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[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, block-alignment requirement and warnings; only the + /// key length differs. + AES256_CFB { + action: BlockModeAction, + + /// The 32-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte AES key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// AES-128 in ECB mode (NIST SP 800-38A Sec 6.1), streaming stdin to stdout. + /// + /// WARNING: ECB is NOT a confidentiality mode for data. Under a given key every plaintext + /// 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. + AES128_ECB { + action: BlockModeAction, + + /// The 16-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 16-byte AES key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 { + action: BlockModeAction, + + /// The 24-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 24-byte AES key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + + /// 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 { + action: BlockModeAction, + + /// The 32-byte AES key in hex. + /// The `key_file` option is preferred to avoid leaving key material in command history. + #[arg(long)] + key: Option, + + /// A file containing the 32-byte AES key, in binary or hex. + /// If both key and key_file options are provided, the file will be used. + #[arg(short, long)] + key_file: Option, + + #[arg(short)] + /// Output in hex format. + x: bool, + }, + /// The ML-KEM-512 key encapsulation algorithm. MLKEM512 { action: mlkem_cmd::MLKEMAction, @@ -502,16 +918,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 +947,42 @@ 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::AsconHash256 { x }) => { + ascon_cmd::hash256_cmd(*x); + } + Some(Subcommands::AsconXOF128 { length, x }) => { + ascon_cmd::xof128_cmd(*length, *x); + } + Some(Subcommands::AsconCXOF128 { length, customization, x }) => { + ascon_cmd::cxof128_cmd(customization, *length, *x); + } + Some(Subcommands::AsconAEAD128 { key, key_file, nonce, nonce_file, ad, decrypt, x }) => { + ascon_cmd::aead128_cmd(key, key_file, nonce, nonce_file, ad, *decrypt, *x); + } Some(Subcommands::HMAC_SHA256 { key, key_file, verify, x }) => { mac_cmd::mac_cmd(HMACVariant::SHA256, key, key_file, verify, *x) } 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, @@ -564,6 +1010,33 @@ fn main() { *len, *x, ), Some(Subcommands::RNG { len, x }) => rng_cmd::rng_cmd(*len, *x), + Some(Subcommands::AES128_CBC { action, key, key_file, x }) => { + aes_cbc_cmd::aes128_cbc_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES192_CBC { action, key, key_file, x }) => { + aes_cbc_cmd::aes192_cbc_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES256_CBC { action, key, key_file, x }) => { + aes_cbc_cmd::aes256_cbc_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES128_CFB { action, key, key_file, x }) => { + aes_cfb_cmd::aes128_cfb_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES192_CFB { action, key, key_file, x }) => { + aes_cfb_cmd::aes192_cfb_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES256_CFB { action, key, key_file, x }) => { + aes_cfb_cmd::aes256_cfb_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES128_ECB { action, key, key_file, x }) => { + aes_ecb_cmd::aes128_ecb_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES192_ECB { action, key, key_file, x }) => { + aes_ecb_cmd::aes192_ecb_cmd(action, key, key_file, *x); + } + Some(Subcommands::AES256_ECB { action, key, key_file, x }) => { + aes_ecb_cmd::aes256_ecb_cmd(action, 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/sha2_cmd.rs b/cli/src/sha2_cmd.rs index 3551c9d8..c719eca4 100644 --- a/cli/src/sha2_cmd.rs +++ b/cli/src/sha2_cmd.rs @@ -2,15 +2,26 @@ use bouncycastle::core::traits::Hash; use std::io; use std::io::{Read, Write}; -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), } } diff --git a/cli/src/sha3_cmd.rs b/cli/src/sha3_cmd.rs index b6107e0c..a6057d90 100644 --- a/cli/src/sha3_cmd.rs +++ b/cli/src/sha3_cmd.rs @@ -1,65 +1,21 @@ -use bouncycastle::core::traits::{Hash, XOF}; -use std::io; -use std::io::{Read, Write}; - use bouncycastle::sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SHAKE128, SHAKE256}; +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"); - } - - let out = sha3.do_final(); - - if output_hex { - for b in out.iter() { - print!("{b:02x}"); - } - } else { - io::stdout().write(&out).unwrap(); - } - println!(); -} - pub(crate) fn shake_cmd(bit_len: usize, output_len: usize, output_hex: bool) { match bit_len { - 128 => do_shake(SHAKE128::new(), output_len, output_hex), - 256 => do_shake(SHAKE256::new(), output_len, output_hex), + 128 => stream_xof(SHAKE128::new(), output_len, output_hex), + 256 => stream_xof(SHAKE256::new(), output_len, output_hex), _ => panic!("Unsupported algorithm: SHAKE-{}", bit_len), } } - -fn do_shake(mut shake: impl XOF, output_len: usize, output_hex: bool) { - let mut buf: [u8; 1024] = [0u8; 1024]; - // read from stdin - let mut bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - while bytes_read != 0 { - shake.absorb(&buf[..bytes_read]).expect("absorb before squeeze is infallible"); - bytes_read = io::stdin().read(&mut buf).expect("Failed to read from stdin"); - } - - let out = shake.squeeze(output_len); - if output_hex { - for b in out.iter() { - print!("{b:02x}"); - } - } else { - io::stdout().write(&out).unwrap(); - } - println!(); -} diff --git a/cli/src/sm3_cmd.rs b/cli/src/sm3_cmd.rs new file mode 100644 index 00000000..98630c64 --- /dev/null +++ b/cli/src/sm3_cmd.rs @@ -0,0 +1,28 @@ +use bouncycastle::core::traits::Hash; +use std::io; +use std::io::{Read, Write}; + +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(); + + if output_hex { + for b in out.iter() { + print!("{b:02x}"); + } + } else { + io::stdout().write_all(&out).unwrap(); + } + println!(); +} diff --git a/cli/tests/aes_cbc_cli_tests.rs b/cli/tests/aes_cbc_cli_tests.rs new file mode 100644 index 00000000..d659c0cd --- /dev/null +++ b/cli/tests/aes_cbc_cli_tests.rs @@ -0,0 +1,431 @@ +//! Tests for the `aes128-cbc` / `aes192-cbc` / `aes256-cbc` subcommands. +//! +//! These drive the built `bc-rust` binary as a subprocess, because the behaviour worth testing is +//! the command-line contract itself -- the IV riding in the first block, block-alignment +//! enforcement, exit codes, key loading -- none of which is reachable from the library API. +//! +//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the +//! current profile, so there is nothing to build or locate by hand. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; +use std::thread; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// SP 800-38A Appendix F IV, shared by every F.2 subsection. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// The four SP 800-38A Appendix F plaintext blocks. +const PLAINTEXT: &str = concat!( + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", +); + +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + +/// F.2.1 CBC-AES128.Encrypt ciphertext. +const CT_128: &str = concat!( + "7649abac8119b246cee98e9b12e9197d", + "5086cb9b507219ee95db113a917678b2", + "73bed6b8e3c1743b7116e69e22229516", + "3ff1caa1681fac09120eca307586e1a7", +); +/// F.2.3 CBC-AES192.Encrypt ciphertext. +const CT_192: &str = concat!( + "4f021db243bc633d7178183a9fa071e8", + "b4d9ada9ad7dedf4e5e738763f69145a", + "571b242012fb7ae07fa9baac3df102e0", + "08b0e27988598881d920a9e64f5615cd", +); +/// F.2.5 CBC-AES256.Encrypt ciphertext. +const CT_256: &str = concat!( + "f58c4c04d6e5f1ba779eabfb5f7bfbd6", + "9cfc4e967edb808d679f777bc6702c7d", + "39f23369a9d9bacfa530e26304231461", + "b2eb05e2c39be9fcda6c19078c6a9d1b", +); + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +/// +/// # Why stdin is written from a thread +/// +/// stdin, stdout and stderr are all pipes with a bounded buffer (typically 64 KiB). Writing all of +/// stdin from *this* thread before reading any output deadlocks as soon as the payload is large +/// enough: the child fills its stdout buffer and blocks, so it stops draining stdin, so our write +/// blocks too, and neither side can move. That is a hang rather than a failure, so it would surface +/// as a CI timeout. Writing on a separate thread leaves this one free to drain stdout and stderr +/// via `wait_with_output`, which breaks the cycle. `a_payload_larger_than_the_pipe_buffer_round_trips` +/// pins it. +/// +/// Dropping the pipe when the write finishes is what signals EOF to the child, so the writer thread +/// owns the handle (`take`, not `as_mut`) and must run to completion. +/// +/// # Why `BrokenPipe` is ignored +/// +/// The error-path tests hand a rejected key or a misaligned length to a command that `exit`s before +/// it reads stdin, so the write races the child's exit and loses. That is an expected outcome, not a +/// harness failure: those tests assert the exit status and stderr, both of which `wait_with_output` +/// still returns. Any *other* write error is a real problem and still panics. +/// `a_large_payload_on_an_error_path_does_not_break_the_harness` pins it. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || { + match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + // `stdin` drops here, closing the pipe so the child sees EOF and can exit. + }); + + // Drain stdout and stderr first: the writer may still be blocked on a full stdin buffer, and it + // cannot finish until the child consumes more, which it cannot do while its output is backed up. + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG or on `/dev/urandom`. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +// ---- the harness itself ------------------------------------------------------------------ +// +// These two pin `run`'s pipe handling. Both bugs they cover are timing-dependent: they pass on a +// fast machine with a small payload and fail on a slow or loaded runner, which is exactly how the +// first one reached CI. Forcing the condition with an oversized payload makes them deterministic +// instead of waiting for a bad day. The same pair exists in `aes_cfb_cli_tests.rs`, because each +// file has its own copy of `run`. + +/// Far beyond any pipe buffer, so a write cannot complete before the child has drained it. +const OVERSIZED: usize = 4 * 1024 * 1024; + +/// An error path must not take the harness down with it. +/// +/// `encrypt` with no `--key` prints its complaint and exits without reading stdin, so the write +/// loses the race and the pipe breaks. Before `run` tolerated `ErrorKind::BrokenPipe` this panicked +/// with "failed to write to stdin" (os error 109 on Windows, EPIPE elsewhere) instead of reporting +/// the CLI's actual error, which is what the other error-path tests assert on. +#[test] +fn a_large_payload_on_an_error_path_does_not_break_the_harness() { + let stderr = run_err(&["aes128-cbc", "encrypt"], &vec![0u8; OVERSIZED]); + assert!(stderr.contains("--key"), "the CLI's own error must still be reported: {stderr}"); +} + +/// A payload larger than the pipe buffer must round-trip rather than deadlock. +/// +/// This is the reason `run` writes stdin from a separate thread. Writing it inline wedges once both +/// pipes fill: the child blocks writing stdout, so it stops reading stdin, so the harness blocks +/// writing stdin. Nothing times out on its own -- the test just hangs until CI kills the job -- so +/// this is the check that would have caught it. +#[test] +fn a_payload_larger_than_the_pipe_buffer_round_trips() { + let plaintext = pseudo_random(OVERSIZED, 0xC0FFEE); + let ciphertext = run_ok(&["aes128-cbc", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(ciphertext.len(), plaintext.len() + 16, "IV plus the ciphertext"); + + let recovered = run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "{OVERSIZED} bytes should round trip"); +} + +// ---- the SP 800-38A F.2 vectors, through the CLI ----------------------------------------- + +/// `decrypt` reproduces the spec plaintext when handed the spec's IV followed by the spec's +/// ciphertext. +/// +/// This is the direction that can be pinned exactly: `encrypt` picks its own IV, so it cannot be +/// asked to reproduce a published ciphertext. `encrypt` is covered by the round-trip tests below +/// and, at the library level, by `crypto/modes/tests/sp800_38a_tests.rs`. +#[test] +fn decrypt_matches_sp800_38a_f2_vectors() { + for (cmd, key, ct) in [ + ("aes128-cbc", KEY_128, CT_128), + ("aes192-cbc", KEY_192, CT_192), + ("aes256-cbc", KEY_256, CT_256), + ] { + // The CLI expects the IV as the first block of its input, which is exactly how `encrypt` + // emits it. + let input = unhex(&format!("{IV}{ct}")); + let out = run_ok(&[cmd, "decrypt", "--key", key], &input); + assert_eq!( + tohex(&out), + PLAINTEXT, + "{cmd} decrypt should reproduce the Appendix F.2 plaintext" + ); + } +} + +/// The same, with `-x`, which should give the identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let input = unhex(&format!("{IV}{CT_128}")); + let binary = run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &input); + let hex_out = run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128, "-x"], &input); + + let hex_str = String::from_utf8(hex_out).expect("hex output is text"); + assert_eq!(hex_str.trim_end(), tohex(&binary)); + assert_eq!(hex_str.trim_end(), PLAINTEXT); +} + +// ---- round trips ------------------------------------------------------------------------ + +/// `encrypt | decrypt` recovers the input, for all three key lengths. +/// +/// Also checks the output length: the ciphertext is one block longer than the plaintext, because +/// the IV is prepended. +#[test] +fn encrypt_then_decrypt_round_trips() { + for (cmd, key) in [("aes128-cbc", KEY_128), ("aes192-cbc", KEY_192), ("aes256-cbc", KEY_256)] { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&[cmd, "encrypt", "--key", key], &plaintext); + assert_eq!( + ciphertext.len(), + plaintext.len() + 16, + "{cmd}: output should be the 16-byte IV plus the ciphertext" + ); + + let recovered = run_ok(&[cmd, "decrypt", "--key", key], &ciphertext); + assert_eq!(recovered, plaintext, "{cmd}: round trip"); + } +} + +/// Round trips at sizes that straddle the 1 KiB streaming chunk and the block boundary. +/// +/// 1024 is exactly one chunk; 1040 is a chunk plus one block, which exercises the tail path; 4112 +/// is four chunks plus a block; 65536 is many chunks. +#[test] +fn round_trips_across_chunk_boundaries() { + for size in [16usize, 32, 1024, 1040, 4096, 4112, 65536] { + let plaintext = pseudo_random(size, size as u32); + let ciphertext = run_ok(&["aes128-cbc", "encrypt", "--key", KEY_128], &plaintext); + let recovered = run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "{size} bytes should round trip"); + } +} + +/// A fresh IV per invocation, so the same plaintext under the same key gives different output. +/// +/// This is the operational requirement CBC lives or dies by, and the CLI is where it is easiest to +/// get wrong (e.g. by seeding from a fixed value). +#[test] +fn each_invocation_uses_a_fresh_iv() { + let plaintext = unhex(PLAINTEXT); + let mut seen = std::collections::BTreeSet::new(); + + for _ in 0..8 { + let ciphertext = run_ok(&["aes128-cbc", "encrypt", "--key", KEY_128], &plaintext); + let iv = ciphertext[..16].to_vec(); + assert!(seen.insert(iv), "the CLI reused an IV across invocations"); + // ...and the body differs too, not just the IV. + let recovered = run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext); + } +} + +// ---- key handling ----------------------------------------------------------------------- + +/// `--key-file` accepts both a hex file and a raw binary file, and agrees with `--key`. +#[test] +fn key_file_accepts_hex_and_binary() { + let dir = std::env::temp_dir().join(format!("bc_rust_cli_key_{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + + let hex_path = dir.join("key.hex"); + let bin_path = dir.join("key.bin"); + std::fs::write(&hex_path, KEY_128).expect("write hex key"); + std::fs::write(&bin_path, unhex(KEY_128)).expect("write binary key"); + + let input = unhex(&format!("{IV}{CT_128}")); + let expected = unhex(PLAINTEXT); + + for path in [&hex_path, &bin_path] { + let out = run_ok(&["aes128-cbc", "decrypt", "--key-file", path.to_str().unwrap()], &input); + assert_eq!(out, expected, "--key-file {path:?}"); + } + + std::fs::remove_dir_all(&dir).ok(); +} + +/// A key of the wrong length for the chosen variant is rejected, naming both lengths. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let stderr = run_err(&["aes256-cbc", "encrypt", "--key", KEY_128], &unhex(PLAINTEXT)); + assert!(stderr.contains("32-byte key"), "stderr should name the expected length: {stderr}"); + assert!(stderr.contains("16 bytes"), "stderr should name the supplied length: {stderr}"); +} + +/// Omitting the key entirely is an error, not a default. +#[test] +fn a_missing_key_is_rejected() { + let stderr = run_err(&["aes128-cbc", "encrypt"], &unhex(PLAINTEXT)); + assert!(stderr.contains("--key"), "stderr should mention the key options: {stderr}"); +} + +/// An all-zero key warns but proceeds, matching `helpers::parse_seed`'s stance. NIST publishes +/// all-zero-key vectors, so refusing outright would make some of them untestable from the CLI. +#[test] +fn an_all_zero_key_warns_but_proceeds() { + let zero_key = "0".repeat(32); + let out = run(&["aes128-cbc", "encrypt", "--key", &zero_key], &unhex(PLAINTEXT)); + assert!(out.status.success(), "an all-zero key should still work"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.to_lowercase().contains("warning"), "an all-zero key should warn: {stderr}"); + assert_eq!(out.stdout.len(), 16 + 64, "IV plus four ciphertext blocks"); +} + +// ---- block alignment and framing -------------------------------------------------------- + +/// Input that is not a whole number of blocks is rejected, with a message that explains why +/// rather than just failing. CBC has no answer for a partial block and there is no padding layer. +#[test] +fn unaligned_input_is_rejected_with_an_explanation() { + for extra in [1usize, 7, 15] { + let plaintext = pseudo_random(32 + extra, extra as u32); + let stderr = run_err(&["aes128-cbc", "encrypt", "--key", KEY_128], &plaintext); + assert!( + stderr.contains("whole number of 16-byte blocks"), + "stderr should explain the alignment requirement: {stderr}" + ); + assert!( + stderr.contains("padding"), + "stderr should point at the missing padding layer: {stderr}" + ); + } +} + +/// Decrypt input shorter than the IV it must start with is rejected, and says so. +#[test] +fn decrypt_input_shorter_than_the_iv_is_rejected() { + for len in [0usize, 1, 15] { + let stderr = run_err(&["aes128-cbc", "decrypt", "--key", KEY_128], &pseudo_random(len, 1)); + assert!( + stderr.contains("IV"), + "stderr should explain the missing IV (len {len}): {stderr}" + ); + } +} + +/// Decrypt input that carries the IV but then an unaligned body is rejected too. +#[test] +fn decrypt_rejects_an_unaligned_body() { + let mut input = unhex(IV); + input.extend_from_slice(&pseudo_random(20, 3)); // 20 is not a multiple of 16 + let stderr = run_err(&["aes128-cbc", "decrypt", "--key", KEY_128], &input); + assert!( + stderr.contains("whole number of 16-byte blocks"), + "stderr should explain the alignment requirement: {stderr}" + ); +} + +/// Empty input to `encrypt` produces just the IV: zero blocks in, zero blocks out. +/// +/// Worth pinning because it is the one input length that is block-aligned but has no blocks, and +/// it is easy for a streaming loop to mishandle. +#[test] +fn empty_input_produces_only_the_iv() { + let out = run_ok(&["aes128-cbc", "encrypt", "--key", KEY_128], &[]); + assert_eq!(out.len(), 16, "empty input should yield exactly the IV"); + + // ...and feeding that straight back gives empty output. + let back = run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &out); + assert!(back.is_empty(), "decrypting an IV with no body should give nothing"); +} + +// ---- cross-variant behaviour ------------------------------------------------------------ + +/// Decrypting with a different key length than was used to encrypt cannot succeed silently. +#[test] +fn the_three_variants_are_not_interchangeable() { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&["aes128-cbc", "encrypt", "--key", KEY_128], &plaintext); + + // Right length, wrong key: decryption "succeeds" but must not recover the plaintext. CBC is + // unauthenticated, so garbage out is the expected behaviour, not an error -- which is exactly + // why the crate docs insist on authenticating separately. + let wrong_key = "ff".repeat(16); + let out = run_ok(&["aes128-cbc", "decrypt", "--key", &wrong_key], &ciphertext); + assert_ne!(out, plaintext, "a wrong key must not recover the plaintext"); + assert_eq!(out.len(), plaintext.len(), "but the length is unchanged: CBC is unauthenticated"); +} + +/// The subcommands appear in `--help`, so they are discoverable. +#[test] +fn the_subcommands_are_listed_in_help() { + let out = run_ok(&["--help"], &[]); + let help = String::from_utf8_lossy(&out); + for cmd in ["aes128-cbc", "aes192-cbc", "aes256-cbc"] { + assert!(help.contains(cmd), "`--help` should list {cmd}"); + } +} + +/// Each subcommand's own help names the two actions and the IV convention. +#[test] +fn per_command_help_documents_the_iv_convention() { + let out = run_ok(&["aes128-cbc", "--help"], &[]); + let help = String::from_utf8_lossy(&out); + assert!(help.contains("encrypt"), "help should list the encrypt action"); + assert!(help.contains("decrypt"), "help should list the decrypt action"); + assert!( + help.contains("FIRST 16 BYTES") || help.contains("first 16 bytes"), + "help should explain where the IV goes: {help}" + ); +} diff --git a/cli/tests/aes_cfb_cli_tests.rs b/cli/tests/aes_cfb_cli_tests.rs new file mode 100644 index 00000000..571cfebe --- /dev/null +++ b/cli/tests/aes_cfb_cli_tests.rs @@ -0,0 +1,513 @@ +//! Tests for the `aes128-cfb` / `aes192-cfb` / `aes256-cfb` subcommands. +//! +//! These drive the built `bc-rust` binary as a subprocess, because the behaviour worth testing is +//! the command-line contract itself -- the IV riding in the first block, block-alignment +//! enforcement, exit codes, key loading -- none of which is reachable from the library API. +//! +//! The commands share all of that plumbing with `aes*-cbc` (`cli/src/block_mode_cmd.rs`), so this +//! file deliberately repeats the CBC suite's coverage rather than assuming it: the shared code is +//! generic over the mode, and a wiring mistake in the CFB dispatcher would not show up in the CBC +//! tests. What is *not* shared, and is tested only here, is the F.3 vectors, the CFB-specific +//! Appendix D error propagation, and the guard that CFB and CBC ciphertexts are not interchangeable. +//! +//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the +//! current profile, so there is nothing to build or locate by hand. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; +use std::thread; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// SP 800-38A Appendix F IV, shared by every F.3 subsection. +const IV: &str = "000102030405060708090a0b0c0d0e0f"; + +/// The four SP 800-38A Appendix F plaintext blocks. +const PLAINTEXT: &str = concat!( + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", +); + +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + +/// F.3.13 CFB128-AES128.Encrypt ciphertext. +const CT_128: &str = concat!( + "3b3fd92eb72dad20333449f8e83cfb4a", + "c8a64537a0b3a93fcde3cdad9f1ce58b", + "26751f67a3cbb140b1808cf187a4f4df", + "c04b05357c5d1c0eeac4c66f9ff7f2e6", +); +/// F.3.15 CFB128-AES192.Encrypt ciphertext. +const CT_192: &str = concat!( + "cdc80d6fddf18cab34c25909c99a4174", + "67ce7f7f81173621961a2b70171d3d7a", + "2e1e8a1dd59b88b1c8e60fed1efac4c9", + "c05f9f9ca9834fa042ae8fba584b09ff", +); +/// F.3.17 CFB128-AES256.Encrypt ciphertext. +const CT_256: &str = concat!( + "dc7e84bfda79164b7ecd8486985d3860", + "39ffed143b28b1c832113c6331e5407b", + "df10132415e54b92a13ed0a8267ae2f9", + "75a385741ab9cef82031623d55b1e471", +); + +/// F.2.1 CBC-AES128.Encrypt ciphertext, for the cross-mode guard. +const CBC_CT_128: &str = concat!( + "7649abac8119b246cee98e9b12e9197d", + "5086cb9b507219ee95db113a917678b2", + "73bed6b8e3c1743b7116e69e22229516", + "3ff1caa1681fac09120eca307586e1a7", +); + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +/// +/// # Why stdin is written from a thread +/// +/// stdin, stdout and stderr are all pipes with a bounded buffer (typically 64 KiB). Writing all of +/// stdin from *this* thread before reading any output deadlocks as soon as the payload is large +/// enough: the child fills its stdout buffer and blocks, so it stops draining stdin, so our write +/// blocks too, and neither side can move. That is a hang rather than a failure, so it would surface +/// as a CI timeout. Writing on a separate thread leaves this one free to drain stdout and stderr +/// via `wait_with_output`, which breaks the cycle. `a_payload_larger_than_the_pipe_buffer_round_trips` +/// pins it. +/// +/// Dropping the pipe when the write finishes is what signals EOF to the child, so the writer thread +/// owns the handle (`take`, not `as_mut`) and must run to completion. +/// +/// # Why `BrokenPipe` is ignored +/// +/// The error-path tests hand a rejected key or a misaligned length to a command that `exit`s before +/// it reads stdin, so the write races the child's exit and loses. That is an expected outcome, not a +/// harness failure: those tests assert the exit status and stderr, both of which `wait_with_output` +/// still returns. Any *other* write error is a real problem and still panics. +/// `a_large_payload_on_an_error_path_does_not_break_the_harness` pins it. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || { + match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + // `stdin` drops here, closing the pipe so the child sees EOF and can exit. + }); + + // Drain stdout and stderr first: the writer may still be blocked on a full stdin buffer, and it + // cannot finish until the child consumes more, which it cannot do while its output is backed up. + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG or on `/dev/urandom`. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +// ---- the harness itself ------------------------------------------------------------------ +// +// These two pin `run`'s pipe handling. Both bugs they cover are timing-dependent: they pass on a +// fast machine with a small payload and fail on a slow or loaded runner, which is exactly how the +// first one reached CI. Forcing the condition with an oversized payload makes them deterministic +// instead of waiting for a bad day. The same pair exists in `aes_cbc_cli_tests.rs`, because each +// file has its own copy of `run`. + +/// Far beyond any pipe buffer, so a write cannot complete before the child has drained it. +const OVERSIZED: usize = 4 * 1024 * 1024; + +/// An error path must not take the harness down with it. +/// +/// `encrypt` with no `--key` prints its complaint and exits without reading stdin, so the write +/// loses the race and the pipe breaks. Before `run` tolerated `ErrorKind::BrokenPipe` this panicked +/// with "failed to write to stdin" (os error 109 on Windows, EPIPE elsewhere) instead of reporting +/// the CLI's actual error, which is what the other error-path tests assert on. +#[test] +fn a_large_payload_on_an_error_path_does_not_break_the_harness() { + let stderr = run_err(&["aes128-cfb", "encrypt"], &vec![0u8; OVERSIZED]); + assert!(stderr.contains("--key"), "the CLI's own error must still be reported: {stderr}"); +} + +/// A payload larger than the pipe buffer must round-trip rather than deadlock. +/// +/// This is the reason `run` writes stdin from a separate thread. Writing it inline wedges once both +/// pipes fill: the child blocks writing stdout, so it stops reading stdin, so the harness blocks +/// writing stdin. Nothing times out on its own -- the test just hangs until CI kills the job -- so +/// this is the check that would have caught it. +#[test] +fn a_payload_larger_than_the_pipe_buffer_round_trips() { + let plaintext = pseudo_random(OVERSIZED, 0xC0FFEE); + let ciphertext = run_ok(&["aes128-cfb", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(ciphertext.len(), plaintext.len() + 16, "IV plus the ciphertext"); + + let recovered = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "{OVERSIZED} bytes should round trip"); +} + +// ---- the SP 800-38A F.3 vectors, through the CLI ----------------------------------------- + +/// `decrypt` reproduces the spec plaintext when handed the spec's IV followed by the spec's +/// ciphertext, for F.3.13/F.3.15/F.3.17 (CFB128-AES128/192/256). +/// +/// This is the direction that can be pinned exactly: `encrypt` picks its own IV, so it cannot be +/// asked to reproduce a published ciphertext. `encrypt` is covered by the round-trip tests below +/// and, at the library level, by `crypto/modes/tests/sp800_38a_cfb_tests.rs`. +#[test] +fn decrypt_matches_sp800_38a_f3_vectors() { + for (cmd, key, ct) in [ + ("aes128-cfb", KEY_128, CT_128), + ("aes192-cfb", KEY_192, CT_192), + ("aes256-cfb", KEY_256, CT_256), + ] { + // The CLI expects the IV as the first block of its input, which is exactly how `encrypt` + // emits it. + let input = unhex(&format!("{IV}{ct}")); + let out = run_ok(&[cmd, "decrypt", "--key", key], &input); + assert_eq!( + tohex(&out), + PLAINTEXT, + "{cmd} decrypt should reproduce the Appendix F.3 plaintext" + ); + } +} + +/// The same, with `-x`, which should give the identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let input = unhex(&format!("{IV}{CT_128}")); + let binary = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &input); + let hex_out = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128, "-x"], &input); + + let hex_str = String::from_utf8(hex_out).expect("hex output is text"); + assert_eq!(hex_str.trim_end(), tohex(&binary)); + assert_eq!(hex_str.trim_end(), PLAINTEXT); +} + +// ---- round trips ------------------------------------------------------------------------ + +/// `encrypt | decrypt` recovers the input, for all three key lengths. +/// +/// Also checks the output length: the ciphertext is one block longer than the plaintext, because +/// the IV is prepended. +#[test] +fn encrypt_then_decrypt_round_trips() { + for (cmd, key) in [("aes128-cfb", KEY_128), ("aes192-cfb", KEY_192), ("aes256-cfb", KEY_256)] { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&[cmd, "encrypt", "--key", key], &plaintext); + assert_eq!( + ciphertext.len(), + plaintext.len() + 16, + "{cmd}: output should be the 16-byte IV plus the ciphertext" + ); + + let recovered = run_ok(&[cmd, "decrypt", "--key", key], &ciphertext); + assert_eq!(recovered, plaintext, "{cmd}: round trip"); + } +} + +/// Round trips at sizes that straddle the 1 KiB streaming chunk and the block boundary. +/// +/// 1024 is exactly one chunk; 1040 is a chunk plus one block, which exercises the tail path; 4112 +/// is four chunks plus a block; 65536 is many chunks. +#[test] +fn round_trips_across_chunk_boundaries() { + for size in [16usize, 32, 1024, 1040, 4096, 4112, 65536] { + let plaintext = pseudo_random(size, size as u32); + let ciphertext = run_ok(&["aes128-cfb", "encrypt", "--key", KEY_128], &plaintext); + let recovered = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "{size} bytes should round trip"); + } +} + +/// A fresh IV per invocation, so the same plaintext under the same key gives different output. +/// +/// This matters even more for CFB than for CBC: CFB XORs a keystream, so a repeated key-and-IV pair +/// leaks the XOR of the two plaintexts outright, not merely whether blocks were equal. +#[test] +fn each_invocation_uses_a_fresh_iv() { + let plaintext = unhex(PLAINTEXT); + let mut seen = std::collections::BTreeSet::new(); + + for _ in 0..8 { + let ciphertext = run_ok(&["aes128-cfb", "encrypt", "--key", KEY_128], &plaintext); + let iv = ciphertext[..16].to_vec(); + assert!(seen.insert(iv), "the CLI reused an IV across invocations"); + // ...and the body differs too, not just the IV. + let recovered = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext); + } +} + +// ---- key handling ----------------------------------------------------------------------- + +/// `--key-file` accepts both a hex file and a raw binary file, and agrees with `--key`. +#[test] +fn key_file_accepts_hex_and_binary() { + let dir = std::env::temp_dir().join(format!("bc_rust_cfb_cli_key_{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + + let hex_path = dir.join("key.hex"); + let bin_path = dir.join("key.bin"); + std::fs::write(&hex_path, KEY_128).expect("write hex key"); + std::fs::write(&bin_path, unhex(KEY_128)).expect("write binary key"); + + let input = unhex(&format!("{IV}{CT_128}")); + let expected = unhex(PLAINTEXT); + + for path in [&hex_path, &bin_path] { + let out = run_ok(&["aes128-cfb", "decrypt", "--key-file", path.to_str().unwrap()], &input); + assert_eq!(out, expected, "--key-file {path:?}"); + } + + std::fs::remove_dir_all(&dir).ok(); +} + +/// A key of the wrong length for the chosen variant is rejected, naming both lengths. +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let stderr = run_err(&["aes256-cfb", "encrypt", "--key", KEY_128], &unhex(PLAINTEXT)); + assert!(stderr.contains("32-byte key"), "stderr should name the expected length: {stderr}"); + assert!(stderr.contains("16 bytes"), "stderr should name the supplied length: {stderr}"); +} + +/// Omitting the key entirely is an error, not a default. +#[test] +fn a_missing_key_is_rejected() { + let stderr = run_err(&["aes128-cfb", "encrypt"], &unhex(PLAINTEXT)); + assert!(stderr.contains("--key"), "stderr should mention the key options: {stderr}"); +} + +/// An all-zero key warns but proceeds, matching `helpers::parse_seed`'s stance. NIST publishes +/// all-zero-key vectors, so refusing outright would make some of them untestable from the CLI. +#[test] +fn an_all_zero_key_warns_but_proceeds() { + let zero_key = "0".repeat(32); + let out = run(&["aes128-cfb", "encrypt", "--key", &zero_key], &unhex(PLAINTEXT)); + assert!(out.status.success(), "an all-zero key should still work"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.to_lowercase().contains("warning"), "an all-zero key should warn: {stderr}"); + assert_eq!(out.stdout.len(), 16 + 64, "IV plus four ciphertext blocks"); +} + +// ---- block alignment and framing -------------------------------------------------------- + +/// Input that is not a whole number of blocks is rejected, with a message that explains why rather +/// than just failing. These commands are the `s = b` CFB variant, so they need whole blocks and +/// they do not pad. +#[test] +fn unaligned_input_is_rejected_with_an_explanation() { + for extra in [1usize, 7, 15] { + let plaintext = pseudo_random(32 + extra, extra as u32); + let stderr = run_err(&["aes128-cfb", "encrypt", "--key", KEY_128], &plaintext); + assert!( + stderr.contains("whole number of 16-byte blocks"), + "stderr should explain the alignment requirement: {stderr}" + ); + assert!( + stderr.contains("padding"), + "stderr should point at padding being the caller's job: {stderr}" + ); + assert!(stderr.contains("CFB128"), "stderr should name the mode: {stderr}"); + } +} + +/// Decrypt input shorter than the IV it must start with is rejected, and says so. +#[test] +fn decrypt_input_shorter_than_the_iv_is_rejected() { + for len in [0usize, 1, 15] { + let stderr = run_err(&["aes128-cfb", "decrypt", "--key", KEY_128], &pseudo_random(len, 1)); + assert!( + stderr.contains("IV"), + "stderr should explain the missing IV (len {len}): {stderr}" + ); + } +} + +/// Decrypt input that carries the IV but then an unaligned body is rejected too. +#[test] +fn decrypt_rejects_an_unaligned_body() { + let mut input = unhex(IV); + input.extend_from_slice(&pseudo_random(20, 3)); // 20 is not a multiple of 16 + let stderr = run_err(&["aes128-cfb", "decrypt", "--key", KEY_128], &input); + assert!( + stderr.contains("whole number of 16-byte blocks"), + "stderr should explain the alignment requirement: {stderr}" + ); +} + +/// Empty input to `encrypt` produces just the IV: zero blocks in, zero blocks out. +/// +/// Worth pinning because it is the one input length that is block-aligned but has no blocks, and +/// it is easy for a streaming loop to mishandle. +#[test] +fn empty_input_produces_only_the_iv() { + let out = run_ok(&["aes128-cfb", "encrypt", "--key", KEY_128], &[]); + assert_eq!(out.len(), 16, "empty input should yield exactly the IV"); + + // ...and feeding that straight back gives empty output. + let back = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &out); + assert!(back.is_empty(), "decrypting an IV with no body should give nothing"); +} + +// ---- SP 800-38A Appendix D, through the CLI ---------------------------------------------- + +/// Appendix D, Table D.2 for CFB: a bit error in `Cj` gives "SBE in the decryption of `Cj`" -- +/// **specific** bit errors, i.e. 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 the property that makes CFB tampering directly exploitable, which is why the subcommand +/// help warns about it, and it is also a sharp end-to-end check that the CLI is running CFB rather +/// than CBC: under CBC the controlled flip would land in `Pj+1`, not `Pj`. +#[test] +fn a_ciphertext_bit_flip_flips_the_same_plaintext_bit() { + let plaintext = unhex(PLAINTEXT); + let mut input = unhex(&format!("{IV}{CT_128}")); + + // Byte 3 of the second ciphertext block. Input layout is IV | C1 | C2 | C3 | C4, so C2 starts + // at offset 32. + const OFFSET: usize = 32 + 3; + const MASK: u8 = 0b0010_0000; + input[OFFSET] ^= MASK; + + let out = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &input); + assert_eq!(out.len(), 64); + + assert_eq!(&out[0..16], &plaintext[0..16], "P1 depends only on the IV, so it is unaffected"); + + let mut expected_p2 = plaintext[16..32].to_vec(); + expected_p2[3] ^= MASK; + assert_eq!(&out[16..32], &expected_p2[..], "P2 should show exactly the flipped bit"); + + assert_ne!(&out[32..48], &plaintext[32..48], "P3 is randomised: C2 feeds the next cipher call"); + assert_eq!( + &out[48..64], + &plaintext[48..64], + "P4 is unaffected: with s = b, damage stops at P3" + ); +} + +// ---- cross-variant and cross-mode behaviour --------------------------------------------- + +/// Decrypting with a different key length than was used to encrypt cannot succeed silently. +#[test] +fn the_three_variants_are_not_interchangeable() { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&["aes128-cfb", "encrypt", "--key", KEY_128], &plaintext); + + // Right length, wrong key: decryption "succeeds" but must not recover the plaintext. CFB is + // unauthenticated, so garbage out is the expected behaviour, not an error -- which is exactly + // why the crate docs insist on authenticating separately. + let wrong_key = "ff".repeat(16); + let out = run_ok(&["aes128-cfb", "decrypt", "--key", &wrong_key], &ciphertext); + assert_ne!(out, plaintext, "a wrong key must not recover the plaintext"); + assert_eq!(out.len(), plaintext.len(), "but the length is unchanged: CFB is unauthenticated"); +} + +/// CFB and CBC ciphertexts are not interchangeable, in either direction. +/// +/// The two commands 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 this is a clean comparison: each mode must reproduce the plaintext only from its +/// own ciphertext. +#[test] +fn cfb_and_cbc_are_not_interchangeable() { + let plaintext = unhex(PLAINTEXT); + let cfb_input = unhex(&format!("{IV}{CT_128}")); + let cbc_input = unhex(&format!("{IV}{CBC_CT_128}")); + + // Each mode with its own ciphertext: correct. + assert_eq!(run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &cfb_input), plaintext); + assert_eq!(run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &cbc_input), plaintext); + + // Each mode with the other's ciphertext: wrong, but silently so -- neither mode is + // authenticated, so there is nothing to detect the mismatch. + let cfb_reads_cbc = run_ok(&["aes128-cfb", "decrypt", "--key", KEY_128], &cbc_input); + assert_ne!(cfb_reads_cbc, plaintext, "CFB must not decrypt a CBC ciphertext"); + + let cbc_reads_cfb = run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &cfb_input); + assert_ne!(cbc_reads_cfb, plaintext, "CBC must not decrypt a CFB ciphertext"); +} + +// ---- discoverability -------------------------------------------------------------------- + +/// The subcommands appear in `--help`, so they are discoverable. +#[test] +fn the_subcommands_are_listed_in_help() { + let out = run_ok(&["--help"], &[]); + let help = String::from_utf8_lossy(&out); + for cmd in ["aes128-cfb", "aes192-cfb", "aes256-cfb"] { + assert!(help.contains(cmd), "`--help` should list {cmd}"); + } +} + +/// Each subcommand's own help names the two actions, the IV convention, and -- because `CFB8` and +/// `CFB1` are different, non-interoperable modes -- the segment size. +#[test] +fn per_command_help_documents_the_iv_convention_and_the_segment_size() { + let out = run_ok(&["aes128-cfb", "--help"], &[]); + let help = String::from_utf8_lossy(&out); + assert!(help.contains("encrypt"), "help should list the encrypt action"); + assert!(help.contains("decrypt"), "help should list the decrypt action"); + assert!( + help.contains("FIRST 16 BYTES") || help.contains("first 16 bytes"), + "help should explain where the IV goes: {help}" + ); + assert!(help.contains("CFB128"), "help should say which CFB variant this is: {help}"); +} diff --git a/cli/tests/aes_ecb_cli_tests.rs b/cli/tests/aes_ecb_cli_tests.rs new file mode 100644 index 00000000..ddbc66e0 --- /dev/null +++ b/cli/tests/aes_ecb_cli_tests.rs @@ -0,0 +1,414 @@ +//! Tests for the `aes128-ecb` / `aes192-ecb` / `aes256-ecb` subcommands. +//! +//! These drive the built `bc-rust` binary as a subprocess, because the behaviour worth testing is +//! the command-line contract itself -- no IV framing, block-alignment enforcement, exit codes, key +//! loading -- none of which is reachable from the library API. +//! +//! The commands share their plumbing with `aes*-cbc` and `aes*-cfb` (`cli/src/block_mode_cmd.rs`), +//! generic over the mode's `INIT_DATA_LEN`, which for ECB is 0. So this file repeats the key and +//! alignment coverage of the other suites (a wiring mistake in the ECB dispatcher would not show up +//! there) and adds what is ECB-specific: the F.1 vectors in *both* directions (no IV means `encrypt` +//! is reproducible), output exactly as long as input, determinism across invocations, the codebook +//! property, Appendix D error propagation confined to one block, and the guard that ECB and CBC +//! ciphertexts are not interchangeable. +//! +//! `CARGO_BIN_EXE_bc-rust` is set by cargo for integration tests and points at the binary for the +//! current profile, so there is nothing to build or locate by hand. + +use std::io::{ErrorKind, Write}; +use std::process::{Command, Output, Stdio}; +use std::thread; + +/// The path to the binary under test, resolved by cargo. +const BC_RUST: &str = env!("CARGO_BIN_EXE_bc-rust"); + +/// The four SP 800-38A Appendix F plaintext blocks. +const PLAINTEXT: &str = concat!( + "6bc1bee22e409f96e93d7e117393172a", + "ae2d8a571e03ac9c9eb76fac45af8e51", + "30c81c46a35ce411e5fbc1191a0a52ef", + "f69f2445df4f9b17ad2b417be66c3710", +); + +const KEY_128: &str = "2b7e151628aed2a6abf7158809cf4f3c"; +const KEY_192: &str = "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b"; +const KEY_256: &str = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"; + +/// F.1.1 ECB-AES128.Encrypt ciphertext. +const CT_128: &str = concat!( + "3ad77bb40d7a3660a89ecaf32466ef97", + "f5d3d58503b9699de785895a96fdbaaf", + "43b1cd7f598ece23881b00e3ed030688", + "7b0c785e27e8ad3f8223207104725dd4", +); +/// F.1.3 ECB-AES192.Encrypt ciphertext. +const CT_192: &str = concat!( + "bd334f1d6e45f25ff712a214571fa5cc", + "974104846d0ad3ad7734ecb3ecee4eef", + "ef7afd2270e2e60adce0ba2face6444e", + "9a4b41ba738d6c72fb16691603c18e0e", +); +/// F.1.5 ECB-AES256.Encrypt ciphertext. +const CT_256: &str = concat!( + "f3eed1bdb5d2a03c064b5a7e3db181f8", + "591ccb10d410ed26dc5ba74a31362870", + "b6ed21b99ca6f4f9f153e7b1beafed1d", + "23304b7a39f9f3ff067d8d8f9e24ecc7", +); + +/// F.2.1 CBC-AES128.Encrypt: the Appendix F IV and ciphertext, for the cross-mode guard. +const CBC_IV: &str = "000102030405060708090a0b0c0d0e0f"; +const CBC_CT_128: &str = concat!( + "7649abac8119b246cee98e9b12e9197d", + "5086cb9b507219ee95db113a917678b2", + "73bed6b8e3c1743b7116e69e22229516", + "3ff1caa1681fac09120eca307586e1a7", +); + +/// Runs `bc-rust ` with `stdin_bytes` on stdin and returns the completed output. +/// +/// # Why stdin is written from a thread +/// +/// stdin, stdout and stderr are all pipes with a bounded buffer (typically 64 KiB). Writing all of +/// stdin from *this* thread before reading any output deadlocks as soon as the payload is large +/// enough: the child fills its stdout buffer and blocks, so it stops draining stdin, so our write +/// blocks too, and neither side can move. That is a hang rather than a failure, so it would surface +/// as a CI timeout. Writing on a separate thread leaves this one free to drain stdout and stderr +/// via `wait_with_output`, which breaks the cycle. `a_payload_larger_than_the_pipe_buffer_round_trips` +/// pins it. +/// +/// Dropping the pipe when the write finishes is what signals EOF to the child, so the writer thread +/// owns the handle (`take`, not `as_mut`) and must run to completion. +/// +/// # Why `BrokenPipe` is ignored +/// +/// The error-path tests hand a rejected key or a misaligned length to a command that `exit`s before +/// it reads stdin, so the write races the child's exit and loses. That is an expected outcome, not a +/// harness failure: those tests assert the exit status and stderr, both of which `wait_with_output` +/// still returns. Any *other* write error is a real problem and still panics. +/// `a_large_payload_on_an_error_path_does_not_break_the_harness` pins it. +fn run(args: &[&str], stdin_bytes: &[u8]) -> Output { + let mut child = Command::new(BC_RUST) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn bc-rust"); + + let mut stdin = child.stdin.take().expect("stdin piped"); + let payload = stdin_bytes.to_vec(); + let writer = thread::spawn(move || { + match stdin.write_all(&payload) { + Ok(()) => {} + Err(e) if e.kind() == ErrorKind::BrokenPipe => {} + Err(e) => panic!("failed to write to stdin: {e}"), + } + // `stdin` drops here, closing the pipe so the child sees EOF and can exit. + }); + + // Drain stdout and stderr first: the writer may still be blocked on a full stdin buffer, and it + // cannot finish until the child consumes more, which it cannot do while its output is backed up. + let output = child.wait_with_output().expect("failed to wait for bc-rust"); + writer.join().expect("the stdin writer thread panicked"); + output +} + +/// Runs a command that is expected to succeed, returning stdout. +fn run_ok(args: &[&str], stdin_bytes: &[u8]) -> Vec { + let out = run(args, stdin_bytes); + assert!( + out.status.success(), + "expected success from {args:?}, got {:?}\nstderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + out.stdout +} + +/// Runs a command that is expected to fail, returning stderr as a string. +fn run_err(args: &[&str], stdin_bytes: &[u8]) -> String { + let out = run(args, stdin_bytes); + assert!( + !out.status.success(), + "expected failure from {args:?}, but it succeeded\nstdout: {:?}", + String::from_utf8_lossy(&out.stdout) + ); + String::from_utf8_lossy(&out.stderr).into_owned() +} + +fn unhex(s: &str) -> Vec { + assert!(s.len().is_multiple_of(2), "hex string must have even length"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} + +fn tohex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Deterministic pseudo-random bytes, so the tests do not depend on an RNG or on `/dev/urandom`. +fn pseudo_random(len: usize, seed: u32) -> Vec { + let mut state = seed.wrapping_mul(2_654_435_761).wrapping_add(1); + (0..len) + .map(|_| { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + (state >> 24) as u8 + }) + .collect() +} + +// ---- the harness itself ------------------------------------------------------------------ +// +// These two pin `run`'s pipe handling, as in the CBC and CFB suites; each file has its own `run`. + +/// Far beyond any pipe buffer, so a write cannot complete before the child has drained it. +const OVERSIZED: usize = 4 * 1024 * 1024; + +#[test] +fn a_large_payload_on_an_error_path_does_not_break_the_harness() { + let stderr = run_err(&["aes128-ecb", "encrypt"], &vec![0u8; OVERSIZED]); + assert!(stderr.contains("--key"), "the CLI's own error must still be reported: {stderr}"); +} + +#[test] +fn a_payload_larger_than_the_pipe_buffer_round_trips() { + let plaintext = pseudo_random(OVERSIZED, 0xC0FFEE); + let ciphertext = run_ok(&["aes128-ecb", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!( + ciphertext.len(), + plaintext.len(), + "no IV: the ciphertext is as long as the plaintext" + ); + let recovered = run_ok(&["aes128-ecb", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "{OVERSIZED} bytes should round trip"); +} + +// ---- the SP 800-38A F.1 vectors, through the CLI ----------------------------------------- + +/// With no IV, `encrypt` is reproducible, so both directions can be pinned to the published +/// vectors: F.1.1/F.1.3/F.1.5 encrypt and F.1.2/F.1.4/F.1.6 decrypt. +#[test] +fn both_directions_match_sp800_38a_f1_vectors() { + for (cmd, key, ct) in [ + ("aes128-ecb", KEY_128, CT_128), + ("aes192-ecb", KEY_192, CT_192), + ("aes256-ecb", KEY_256, CT_256), + ] { + let enc = run_ok(&[cmd, "encrypt", "--key", key], &unhex(PLAINTEXT)); + assert_eq!(tohex(&enc), ct, "{cmd} encrypt should reproduce the Appendix F.1 ciphertext"); + let dec = run_ok(&[cmd, "decrypt", "--key", key], &unhex(ct)); + assert_eq!( + tohex(&dec), + PLAINTEXT, + "{cmd} decrypt should reproduce the Appendix F.1 plaintext" + ); + } +} + +/// The same, with `-x`, which should give the identical answer in hex plus a trailing newline. +#[test] +fn hex_output_matches_binary_output() { + let binary = run_ok(&["aes128-ecb", "encrypt", "--key", KEY_128], &unhex(PLAINTEXT)); + let hex_out = run_ok(&["aes128-ecb", "encrypt", "--key", KEY_128, "-x"], &unhex(PLAINTEXT)); + let hex_str = String::from_utf8(hex_out).expect("hex output is text"); + assert_eq!(hex_str.trim_end(), tohex(&binary)); + assert_eq!(hex_str.trim_end(), CT_128); +} + +// ---- round trips and framing ------------------------------------------------------------ + +/// `encrypt | decrypt` recovers the input for all three key lengths, and nothing is prepended. +#[test] +fn encrypt_then_decrypt_round_trips_with_no_iv() { + for (cmd, key) in [("aes128-ecb", KEY_128), ("aes192-ecb", KEY_192), ("aes256-ecb", KEY_256)] { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&[cmd, "encrypt", "--key", key], &plaintext); + assert_eq!(ciphertext.len(), plaintext.len(), "{cmd}: no IV is written"); + let recovered = run_ok(&[cmd, "decrypt", "--key", key], &ciphertext); + assert_eq!(recovered, plaintext, "{cmd}: round trip"); + } +} + +/// Round trips at sizes that straddle the 1 KiB streaming chunk, the eight-block batch and the +/// block boundary: 128 is one eight; 144 is an eight plus one block; 1040 is a chunk plus a block. +#[test] +fn round_trips_across_chunk_and_batch_boundaries() { + for size in [16usize, 32, 128, 144, 1024, 1040, 4096, 4112, 65536] { + let plaintext = pseudo_random(size, size as u32); + let ciphertext = run_ok(&["aes128-ecb", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(ciphertext.len(), size); + let recovered = run_ok(&["aes128-ecb", "decrypt", "--key", KEY_128], &ciphertext); + assert_eq!(recovered, plaintext, "{size} bytes should round trip"); + } +} + +/// Empty input gives empty output in both directions: there is no IV to emit or require. +#[test] +fn empty_input_produces_empty_output() { + assert!(run_ok(&["aes128-ecb", "encrypt", "--key", KEY_128], &[]).is_empty()); + assert!(run_ok(&["aes128-ecb", "decrypt", "--key", KEY_128], &[]).is_empty()); +} + +// ---- 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. This is the reason the help text warns against using ECB for +/// data, and it is pinned so the command cannot quietly become something else. +#[test] +fn ecb_is_deterministic_and_shows_repeated_blocks() { + let block = unhex("00112233445566778899aabbccddeeff"); + let mut plaintext = block.clone(); + plaintext.extend_from_slice(&unhex("ffeeddccbbaa99887766554433221100")); + plaintext.extend_from_slice(&block); + + let first = run_ok(&["aes128-ecb", "encrypt", "--key", KEY_128], &plaintext); + let second = run_ok(&["aes128-ecb", "encrypt", "--key", KEY_128], &plaintext); + assert_eq!(first, second, "the same input gives the same output every time"); + assert_eq!(first[..16], first[32..], "equal plaintext blocks give equal ciphertext blocks"); + assert_ne!(first[..16], first[16..32]); +} + +// ---- key handling ----------------------------------------------------------------------- + +#[test] +fn key_file_accepts_hex_and_binary() { + let dir = std::env::temp_dir().join(format!("bc_rust_ecb_cli_key_{}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + let hex_path = dir.join("key.hex"); + let bin_path = dir.join("key.bin"); + std::fs::write(&hex_path, KEY_128).expect("write hex key"); + std::fs::write(&bin_path, unhex(KEY_128)).expect("write binary key"); + for path in [&hex_path, &bin_path] { + let out = run_ok( + &["aes128-ecb", "decrypt", "--key-file", path.to_str().unwrap()], + &unhex(CT_128), + ); + assert_eq!(out, unhex(PLAINTEXT), "--key-file {path:?}"); + } + std::fs::remove_dir_all(&dir).ok(); +} + +#[test] +fn a_key_of_the_wrong_length_is_rejected() { + let stderr = run_err(&["aes256-ecb", "encrypt", "--key", KEY_128], &unhex(PLAINTEXT)); + assert!(stderr.contains("32-byte key"), "stderr should name the expected length: {stderr}"); + assert!(stderr.contains("16 bytes"), "stderr should name the supplied length: {stderr}"); +} + +#[test] +fn a_missing_key_is_rejected() { + let stderr = run_err(&["aes128-ecb", "encrypt"], &unhex(PLAINTEXT)); + assert!(stderr.contains("--key"), "stderr should mention the key options: {stderr}"); +} + +#[test] +fn an_all_zero_key_warns_but_proceeds() { + let zero_key = "0".repeat(32); + let out = run(&["aes128-ecb", "encrypt", "--key", &zero_key], &unhex(PLAINTEXT)); + assert!(out.status.success(), "an all-zero key should still work"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.to_lowercase().contains("warning"), "an all-zero key should warn: {stderr}"); + assert_eq!(out.stdout.len(), 64, "four ciphertext blocks and no IV"); +} + +// ---- block alignment ------------------------------------------------------------------ + +/// Unaligned input is rejected in both directions, with the mode named and padding pointed at. +#[test] +fn unaligned_input_is_rejected_with_an_explanation() { + for extra in [1usize, 7, 15] { + for action in ["encrypt", "decrypt"] { + let data = pseudo_random(32 + extra, extra as u32); + let stderr = run_err(&["aes128-ecb", action, "--key", KEY_128], &data); + assert!(stderr.contains("whole number of 16-byte blocks"), "{action}: {stderr}"); + assert!(stderr.contains("padding"), "{action}: {stderr}"); + assert!(stderr.contains("ECB"), "{action}: stderr should name the mode: {stderr}"); + } + } +} + +// ---- 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] +fn a_ciphertext_bit_flip_randomises_only_its_own_block() { + let plaintext = unhex(PLAINTEXT); + let mut input = unhex(CT_128); + input[16 + 3] ^= 0b0010_0000; // byte 3 of C2 + + let out = run_ok(&["aes128-ecb", "decrypt", "--key", KEY_128], &input); + assert_eq!(out.len(), 64); + assert_eq!(&out[0..16], &plaintext[0..16], "P1 is unaffected"); + let differing: u32 = + out[16..32].iter().zip(&plaintext[16..32]).map(|(a, b)| (a ^ b).count_ones()).sum(); + assert!(differing > 1, "P2 should be randomised, not flipped in place ({differing} bit(s))"); + assert_eq!(&out[32..48], &plaintext[32..48], "P3 is unaffected: nothing chains"); + assert_eq!(&out[48..64], &plaintext[48..64], "P4 is unaffected"); +} + +// ---- cross-variant and cross-mode behaviour --------------------------------------------- + +#[test] +fn the_three_variants_are_not_interchangeable() { + let plaintext = unhex(PLAINTEXT); + let ciphertext = run_ok(&["aes128-ecb", "encrypt", "--key", KEY_128], &plaintext); + let wrong_key = "ff".repeat(16); + let out = run_ok(&["aes128-ecb", "decrypt", "--key", &wrong_key], &ciphertext); + assert_ne!(out, plaintext, "a wrong key must not recover the plaintext"); + assert_eq!(out.len(), plaintext.len(), "but the length is unchanged: ECB is unauthenticated"); +} + +/// ECB and CBC ciphertexts are not interchangeable. The CBC command frames an IV and the ECB +/// command does not, so feeding one to the other is the kind of mistake nothing but this catches: +/// the CBC ciphertext 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] +fn ecb_and_cbc_are_not_interchangeable() { + let plaintext = unhex(PLAINTEXT); + let ecb_ct = unhex(CT_128); + let cbc_input = unhex(&format!("{CBC_IV}{CBC_CT_128}")); + + assert_eq!(run_ok(&["aes128-ecb", "decrypt", "--key", KEY_128], &ecb_ct), plaintext); + assert_eq!(run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &cbc_input), plaintext); + + let ecb_reads_cbc = run_ok(&["aes128-ecb", "decrypt", "--key", KEY_128], &unhex(CBC_CT_128)); + assert_ne!(ecb_reads_cbc, plaintext, "ECB must not decrypt a CBC ciphertext"); + + let cbc_reads_ecb = run_ok(&["aes128-cbc", "decrypt", "--key", KEY_128], &ecb_ct); + assert_eq!(cbc_reads_ecb.len(), 48, "CBC consumes the first block as an IV"); + assert_ne!(cbc_reads_ecb, plaintext[16..].to_vec(), "CBC must not decrypt an ECB ciphertext"); +} + +// ---- discoverability -------------------------------------------------------------------- + +#[test] +fn the_subcommands_are_listed_in_help() { + let out = run_ok(&["--help"], &[]); + let help = String::from_utf8_lossy(&out); + for cmd in ["aes128-ecb", "aes192-ecb", "aes256-ecb"] { + assert!(help.contains(cmd), "`--help` should list {cmd}"); + } +} + +/// Each subcommand's own help names the two actions, says there is no IV, and carries the warning +/// that ECB is not for data. +#[test] +fn per_command_help_warns_and_documents_the_missing_iv() { + let out = run_ok(&["aes128-ecb", "--help"], &[]); + let help = String::from_utf8_lossy(&out); + assert!(help.contains("encrypt"), "help should list the encrypt action"); + assert!(help.contains("decrypt"), "help should list the decrypt action"); + assert!(help.contains("NO IV"), "help should say there is no IV: {help}"); + assert!(help.contains("WARNING"), "help should warn against using ECB for data: {help}"); + assert!(help.contains("ECB"), "help should name the mode: {help}"); +} diff --git a/crypto/aes-lowmemory/Cargo.toml b/crypto/aes-lowmemory/Cargo.toml new file mode 100644 index 00000000..f6cbff4d --- /dev/null +++ b/crypto/aes-lowmemory/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bouncycastle-aes-lowmemory" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true +# Only for the AES-CBC type aliases in `cbc.rs`; the engine itself does not use it. +bouncycastle-modes.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +bouncycastle-rng.workspace = true +criterion.workspace = true +serde_json = "1.0" + +[[bench]] +name = "aes_benches" +harness = false diff --git a/crypto/aes-lowmemory/benches/aes_benches.rs b/crypto/aes-lowmemory/benches/aes_benches.rs new file mode 100644 index 00000000..82d81003 --- /dev/null +++ b/crypto/aes-lowmemory/benches/aes_benches.rs @@ -0,0 +1,183 @@ +//! Criterion benchmarks for the bit-sliced AES engine. +//! +//! The comparison that matters here is `encrypt_block` against `encrypt_blocks2` over the same +//! number of bytes. The bit-sliced state holds two blocks, so a single-block call does twice the +//! necessary work; the two-block path should be close to twice the throughput. That ratio is the +//! argument for modes of operation using the two-block entry points wherever their blocks are +//! independent (CTR, and the decrypt direction of CBC and CFB). + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256, BLOCK_LEN}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::RNG; +use bouncycastle_rng as rng; +use criterion::{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 * BLOCK_LEN; + +fn random_blocks() -> Vec<[u8; BLOCK_LEN]> { + let mut blocks = vec![[0u8; 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_lowmemory::key expansion"); + + let key128 = key::<16>(); + group.bench_function("Aes128::new()", |b| { + b.iter(|| black_box(Aes128::new(black_box(&key128)).unwrap())) + }); + + let key192 = key::<24>(); + group.bench_function("Aes192::new()", |b| { + b.iter(|| black_box(Aes192::new(black_box(&key192)).unwrap())) + }); + + let key256 = key::<32>(); + group.bench_function("Aes256::new()", |b| { + b.iter(|| black_box(Aes256::new(black_box(&key256)).unwrap())) + }); + + group.finish(); +} + +fn bench_aes128(c: &mut Criterion) { + let aes = Aes128::new(&key::<16>()).unwrap(); + let blocks = random_blocks(); + + let mut group = c.benchmark_group("aes_lowmemory::Aes128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + aes.encrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .encrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + // `try_into` cannot fail: `chunks_exact_mut(2)` yields slices of length 2. + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.encrypt_blocks2(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + aes.decrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.decrypt_blocks2(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.finish(); +} + +fn bench_aes192(c: &mut Criterion) { + let aes = Aes192::new(&key::<24>()).unwrap(); + let blocks = random_blocks(); + + let mut group = c.benchmark_group("aes_lowmemory::Aes192"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + aes.encrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .encrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.encrypt_blocks2(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.finish(); +} + +fn bench_aes256(c: &mut Criterion) { + let aes = Aes256::new(&key::<32>()).unwrap(); + let blocks = random_blocks(); + + let mut group = c.benchmark_group("aes_lowmemory::Aes256"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + group.bench_function("16KiB -- .encrypt_block() x1024", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for block in buf.iter_mut() { + aes.encrypt_block(black_box(block)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .encrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.encrypt_blocks2(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.bench_function("16KiB -- .decrypt_blocks2() x512", |b| { + b.iter(|| { + let mut buf = blocks.clone(); + for pair in buf.chunks_exact_mut(2) { + let pair: &mut [[u8; BLOCK_LEN]; 2] = pair.try_into().unwrap(); + aes.decrypt_blocks2(black_box(pair)); + } + black_box(&buf); + }) + }); + + group.finish(); +} + +criterion_group!(benches, bench_key_expansion, bench_aes128, bench_aes192, bench_aes256); +criterion_main!(benches); diff --git a/crypto/aes-lowmemory/src/aes.rs b/crypto/aes-lowmemory/src/aes.rs new file mode 100644 index 00000000..08198459 --- /dev/null +++ b/crypto/aes-lowmemory/src/aes.rs @@ -0,0 +1,337 @@ +//! CIPHER() and INVCIPHER() (FIPS 197 Sec 5.1 and Sec 5.3), and the public engine types. + +use crate::bitslice::{Block, Planes, pack, unpack}; +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::traits::{Algorithm, ElectronicCodeBook, SecurityStrength}; +use bouncycastle_utils::secret::Secret; + +/// The AES block length in bytes: 16 (FIPS 197 Sec 3.4, `Nb` = 4 words). +pub const BLOCK_LEN: usize = 16; + +/// The AES keyed permutation, parameterised by key length. +/// +/// Use the aliases [`Aes128`], [`Aes192`] and [`Aes256`] rather than naming this directly. +/// `P` is sealed to the three parameter sets of FIPS 197 Sec 6.1, 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 [`Aes::decrypt_blocks2`]), and a constructed value is always +/// ready to use, so there is no `init()` or `reset()`. +pub struct Aes { + schedule: Secret, +} + +/// AES-128: 16-byte key, 10 rounds (FIPS 197 Sec 6.1). +pub type Aes128 = Aes; +/// AES-192: 24-byte key, 12 rounds (FIPS 197 Sec 6.1). +pub type Aes192 = Aes; +/// AES-256: 32-byte key, 14 rounds (FIPS 197 Sec 6.1). +pub type Aes256 = Aes; + +impl Aes

{ + /// 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 two blocks at once (FIPS 197 Sec 5.1, Algorithm 1). + /// + /// 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-13 are the final round, which omits MIXCOLUMNS(). + fn encrypt2(&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 two blocks 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 [`Aes`] 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-13 are the final one, which omits INVMIXCOLUMNS(). + fn decrypt2(&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 two blocks in place. + /// + /// This is the natural unit of work: the bit-sliced state holds two blocks, so two blocks cost + /// almost exactly what one does. Prefer this over two [`Aes::encrypt_block`] calls whenever + /// two blocks are available and independent -- which, for a mode of operation, means CTR, or + /// the decryption direction of CBC and CFB, but *not* CBC encryption, whose blocks are + /// serially dependent. + /// + /// Infallible: a constructed [`Aes`] is always usable and every input length is fixed. + pub fn encrypt_blocks2(&self, blocks: &mut [Block; 2]) { + let mut q = pack(&blocks[0], &blocks[1]); + self.encrypt2(&mut q); + let (a, b) = blocks.split_at_mut(1); + unpack(&q, &mut a[0], &mut b[0]); + } + + /// Decrypts two blocks in place. See [`Aes::encrypt_blocks2`]. + pub fn decrypt_blocks2(&self, blocks: &mut [Block; 2]) { + let mut q = pack(&blocks[0], &blocks[1]); + self.decrypt2(&mut q); + let (a, b) = blocks.split_at_mut(1); + unpack(&q, &mut a[0], &mut b[0]); + } + + /// Encrypts one block in place. + /// + /// The bit-sliced state always holds two blocks, so a single-block call duplicates the block + /// into both halves and discards one result: it does twice the necessary work. Use + /// [`Aes::encrypt_blocks2`] where two blocks are available. + /// + /// Duplicating the block costs exactly what filling the unused half with zeros would, and it + /// buys a free self-check: the two halves must come out equal, which `debug_assert` verifies. + /// That is the whole reason for the choice -- it is not a security property, since the unused + /// half is never returned either way. + pub fn encrypt_block(&self, block: &mut Block) { + let mut q = pack(block, block); + self.encrypt2(&mut q); + let mut discard = [0u8; BLOCK_LEN]; + unpack(&q, block, &mut discard); + debug_assert_eq!(*block, discard, "the two interleaved halves must agree"); + } + + /// Decrypts one block in place. See [`Aes::encrypt_block`] for the two-blocks-at-once caveat. + pub fn decrypt_block(&self, block: &mut Block) { + let mut q = pack(block, block); + self.decrypt2(&mut q); + let mut discard = [0u8; BLOCK_LEN]; + unpack(&q, block, &mut discard); + debug_assert_eq!(*block, discard, "the two interleaved halves must agree"); + } +} + +// 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 Aes128 { + /// 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 fn new(key: &KeyMaterial<16>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl Aes192 { + /// Expands a 24-byte key into an AES-192 schedule. See [`Aes128::new`] for the error cases. + pub fn new(key: &KeyMaterial<24>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl Aes256 { + /// Expands a 32-byte key into an AES-256 schedule. See [`Aes128::new`] for the error cases. + pub fn new(key: &KeyMaterial<32>) -> Result { + Self::validate(key)?; + Ok(Self { schedule: expand::(key.ref_to_bytes()) }) + } +} + +impl Algorithm for Aes128 { + const ALG_NAME: &'static str = Aes128Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl Algorithm for Aes192 { + const ALG_NAME: &'static str = Aes192Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; +} + +impl Algorithm for Aes256 { + const ALG_NAME: &'static str = Aes256Params::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +} + +// The three `ElectronicCodeBook` impls are one-line delegations to the inherent methods above. They +// are written out longhand rather than generated, for the `cargo mutants` reason given above. +// +// Each overrides `encrypt_blocks2` / `decrypt_blocks2`, because a pair of blocks is exactly what +// the bit-sliced state holds: the pair form costs barely more than one block, where the default +// (two single-block calls) would do four blocks' worth of work. + +impl ElectronicCodeBook<16, BLOCK_LEN> for Aes128 { + fn new(key: &KeyMaterial<16>) -> Result { + Aes128::new(key) + } + fn encrypt_block(&self, block: &mut Block) { + Aes::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + Aes::decrypt_block(self, block) + } + fn encrypt_blocks2(&self, blocks: &mut [Block; 2]) { + Aes::encrypt_blocks2(self, blocks) + } + fn decrypt_blocks2(&self, blocks: &mut [Block; 2]) { + Aes::decrypt_blocks2(self, blocks) + } +} + +impl ElectronicCodeBook<24, BLOCK_LEN> for Aes192 { + fn new(key: &KeyMaterial<24>) -> Result { + Aes192::new(key) + } + fn encrypt_block(&self, block: &mut Block) { + Aes::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + Aes::decrypt_block(self, block) + } + fn encrypt_blocks2(&self, blocks: &mut [Block; 2]) { + Aes::encrypt_blocks2(self, blocks) + } + fn decrypt_blocks2(&self, blocks: &mut [Block; 2]) { + Aes::decrypt_blocks2(self, blocks) + } +} + +impl ElectronicCodeBook<32, BLOCK_LEN> for Aes256 { + fn new(key: &KeyMaterial<32>) -> Result { + Aes256::new(key) + } + fn encrypt_block(&self, block: &mut Block) { + Aes::encrypt_block(self, block) + } + fn decrypt_block(&self, block: &mut Block) { + Aes::decrypt_block(self, block) + } + fn encrypt_blocks2(&self, blocks: &mut [Block; 2]) { + Aes::encrypt_blocks2(self, blocks) + } + fn decrypt_blocks2(&self, blocks: &mut [Block; 2]) { + Aes::decrypt_blocks2(self, blocks) + } +} + +impl core::fmt::Debug for Aes

{ + /// 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) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_engine_sizes_match_the_documented_memory_table() { + // The "Memory Usage" table in the crate docs quotes these, and the whole point of the + // crate is that they are this small: 4 * (Nr + 1) words of schedule, nothing else, and no + // tables anywhere. If the representation grows, the docs are wrong -- fix both. + assert_eq!(size_of::(), 176, "AES-128: 4 * (10 + 1) words"); + assert_eq!(size_of::(), 208, "AES-192: 4 * (12 + 1) words"); + assert_eq!(size_of::(), 240, "AES-256: 4 * (14 + 1) words"); + } + + #[test] + fn test_engine_size_is_exactly_the_schedule() { + // No round counter, no direction flag, no initialised marker: the schedule is all there + // is, which is what makes both directions available from one value at no extra cost. + assert_eq!(size_of::(), size_of::<::Schedule>()); + assert_eq!(size_of::(), size_of::<::Schedule>()); + assert_eq!(size_of::(), size_of::<::Schedule>()); + } + + #[test] + fn test_alg_names() { + assert_eq!(::ALG_NAME, "AES-128"); + assert_eq!(::ALG_NAME, "AES-192"); + assert_eq!(::ALG_NAME, "AES-256"); + } + + #[test] + fn test_max_security_strength_matches_the_key_length() { + assert_eq!( + ::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bytes(Aes128Params::KEY_LEN) + ); + assert_eq!( + ::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bytes(Aes192Params::KEY_LEN) + ); + assert_eq!( + ::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bytes(Aes256Params::KEY_LEN) + ); + } +} diff --git a/crypto/aes-lowmemory/src/bitslice.rs b/crypto/aes-lowmemory/src/bitslice.rs new file mode 100644 index 00000000..08ef77ff --- /dev/null +++ b/crypto/aes-lowmemory/src/bitslice.rs @@ -0,0 +1,210 @@ +//! 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 `u32` *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. +//! +//! Eight 32-bit planes hold 256 bits = 32 bytes, which is *two* 16-byte AES blocks. Both blocks +//! are always processed together; see the crate docs for why, and [`crate::aes`] for how a +//! single-block call fills the unused half. +//! +//! # The layout, derived +//! +//! [`ortho`] transposes, within each byte-lane of the eight words, the 8x8 bit matrix indexed by +//! (word number, bit number within the lane): +//! +//! ```text +//! after ortho: q[k] bit (8L + i) == before ortho: q[i] bit (8L + k) +//! ``` +//! +//! [`pack`] loads block A as four little-endian `u32`s into the even words and block B into the +//! odd words, so before `ortho` byte-lane `L` of word `2c` holds `A[4c + L]`. Substituting +//! `j = 4c + L` for the byte index, and FIPS 197 Eq (3.6) `s[r,c] = in[r + 4c]` -- which makes +//! `r = j mod 4` and `c = j div 4` -- gives the layout every mask in this crate depends on: +//! +//! ```text +//! q[k] bit (8r + 2c) == bit k of s[r,c] of block A +//! q[k] bit (8r + 2c + 1) == bit k of s[r,c] of block B +//! ``` +//! +//! In words: **the byte-lane of the word selects the state row `r`, and the bit-pair within that +//! lane selects the state column `c`; the low bit of the pair is block A and the high bit is +//! block B.** Written out, the bit position of `s[r,c]` within every plane is: +//! +//! ```text +//! c=0 c=1 c=2 c=3 +//! r=0 | 0 2 4 6 +//! r=1 | 8 10 12 14 (bit position of block A; +//! r=2 | 16 18 20 22 add 1 for block B) +//! r=3 | 24 26 28 30 +//! ``` +//! +//! This is why SHIFTROWS() becomes a rotation *within* a byte-lane (row `r` lives entirely in +//! lane `r`, and one column step is two bit positions), and why MIXCOLUMNS() uses rotations by +//! 8 and 16 (one and two rows). Both are derived from this table in [`crate::round`]. +//! +//! `test_layout_matches_the_documented_table` below pins the table exhaustively; every mask in +//! this crate is only correct relative to it. +//! +//! # Provenance +//! +//! The three-stage masked-swap transpose and the even/odd two-block packing are translated from +//! BearSSL `src/symcipher/aes_ct.c` (`br_aes_ct_ortho`) and `aes_ct_cbcdec.c` (the `q[0]`, +//! `q[2]`, `q[4]`, `q[6]` load order), by Thomas Pornin, MIT licensed. + +/// 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::BLOCK_LEN]; + +/// The eight bit-planes holding two blocks. See the module docs for the layout. +pub(crate) type Planes = [u32; 8]; + +/// 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. 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: u32, ch: u32, s: u32, x: u32, y: u32) -> (u32, u32) { + ((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(0x5555_5555, 0xAAAA_AAAA, 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(0x3333_3333, 0xCCCC_CCCC, 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(0x0F0F_0F0F, 0xF0F0_F0F0, 4, q[a], q[b]); + } +} + +/// Loads two blocks into the bit-planes. +/// +/// Block `a` goes into the even words and block `b` into the odd words as little-endian `u32`s, +/// then [`ortho`] transposes them into planes. +pub(crate) fn pack(a: &Block, b: &Block) -> Planes { + let mut q = [0u32; 8]; + for c in 0..4 { + // `try_into` cannot fail: the slice is a fixed 4-byte window of a 16-byte array. + q[2 * c] = u32::from_le_bytes(a[4 * c..4 * c + 4].try_into().unwrap()); + q[2 * c + 1] = u32::from_le_bytes(b[4 * c..4 * c + 4].try_into().unwrap()); + } + ortho(&mut q); + q +} + +/// Reads two blocks back out of the bit-planes; the exact inverse of [`pack`]. +pub(crate) fn unpack(q: &Planes, a: &mut Block, b: &mut Block) { + let mut q = *q; + ortho(&mut q); + for c in 0..4 { + a[4 * c..4 * c + 4].copy_from_slice(&q[2 * c].to_le_bytes()); + b[4 * c..4 * c + 4].copy_from_slice(&q[2 * c + 1].to_le_bytes()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A deterministic byte generator, so the tests do not depend on an RNG crate. + pub(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 + } + + #[test] + fn test_layout_matches_the_documented_table() { + // Pins the module doc table: q[k] bit (8r + 2c) is bit k of s[r,c] of block A, and + // bit (8r + 2c + 1) is bit k of s[r,c] of block B. Every mask in `round` depends on it. + let a = pseudo_random_block(1); + let b = pseudo_random_block(2); + let q = pack(&a, &b); + + for j in 0..16 { + let (r, c) = (j % 4, j / 4); + let pos = 8 * r + 2 * c; + for (k, plane) in q.iter().enumerate() { + assert_eq!( + (plane >> pos) & 1, + u32::from((a[j] >> k) & 1), + "block A: plane {k} bit {pos} should be bit {k} of byte {j}" + ); + assert_eq!( + (plane >> (pos + 1)) & 1, + u32::from((b[j] >> k) & 1), + "block B: plane {k} bit {} should be bit {k} of byte {j}", + pos + 1 + ); + } + } + } + + #[test] + fn test_ortho_is_an_involution() { + let mut q = [ + 0x0123_4567, 0x89AB_CDEF, 0xFEDC_BA98, 0x7654_3210, 0xDEAD_BEEF, 0x0000_0001, + 0xFFFF_FFFF, 0xA5A5_5A5A, + ]; + let original = q; + ortho(&mut q); + assert_ne!(q, original, "ortho should actually move bits"); + ortho(&mut q); + assert_eq!(q, original); + } + + #[test] + fn test_unpack_inverts_pack() { + for seed in 0..64 { + let a = pseudo_random_block(seed); + let b = pseudo_random_block(seed + 1000); + let mut out_a = [0u8; 16]; + let mut out_b = [0u8; 16]; + unpack(&pack(&a, &b), &mut out_a, &mut out_b); + assert_eq!(out_a, a); + assert_eq!(out_b, b); + } + } + + #[test] + fn test_the_two_halves_are_independent() { + // Changing block B must not disturb block A anywhere in the round-function pipeline; + // this pins that the interleave really is bit-parallel and not overlapping. + let a = pseudo_random_block(7); + let mut out_a1 = [0u8; 16]; + let mut out_a2 = [0u8; 16]; + let mut scratch = [0u8; 16]; + unpack(&pack(&a, &[0u8; 16]), &mut out_a1, &mut scratch); + unpack(&pack(&a, &pseudo_random_block(9)), &mut out_a2, &mut scratch); + assert_eq!(out_a1, out_a2); + assert_eq!(out_a1, a); + } +} diff --git a/crypto/aes-lowmemory/src/cbc.rs b/crypto/aes-lowmemory/src/cbc.rs new file mode 100644 index 00000000..d68f6e2a --- /dev/null +++ b/crypto/aes-lowmemory/src/cbc.rs @@ -0,0 +1,93 @@ +//! Type aliases for AES in CBC mode (NIST SP 800-38A Sec 6.2). +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Cbc` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. These aliases pin the AES values so +//! callers never spell them out. They add nothing to the engine: the permutation still implements +//! none of the data-encryption traits itself (see the crate docs), the mode does. + +use crate::{Aes128, Aes192, Aes256, BLOCK_LEN}; +use bouncycastle_modes::Cbc; + +/// AES-128 in CBC mode. `Dir` is [`bouncycastle_modes::Encrypting`] or +/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// +/// The IV is generated by encryption and returned; it is never supplied. Encryption and decryption +/// work in place. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CBC_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// // 48 bytes: three whole blocks. The length is checked at compile time. +/// let message = [0u8; 48]; +/// let mut data = message; +/// let iv = AES_CBC_128::::encrypt(&key, &mut data).unwrap(); +/// assert_ne!(data, message); +/// AES_CBC_128::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, message); +/// +/// // Streaming, a few blocks at a time: +/// let (mut enc, iv) = AES_CBC_128::::do_encrypt_init(&key).unwrap(); +/// let mut first = [0u8; 16]; +/// let mut rest = [1u8; 32]; +/// enc.do_encrypt(&mut first).unwrap(); +/// enc.do_encrypt(&mut rest).unwrap(); +/// let mut dec = AES_CBC_128::::do_decrypt_init(&key, &iv).unwrap(); +/// dec.do_decrypt(&mut first).unwrap(); +/// dec.do_decrypt(&mut rest).unwrap(); +/// assert_eq!(first, [0u8; 16]); +/// assert_eq!(rest, [1u8; 32]); +/// ``` +/// +/// A length that is not a whole number of blocks is a **compile** error, not a runtime one: +/// +/// ```compile_fail +/// use bouncycastle_aes_lowmemory::AES_CBC_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::BlockCipherEncryptor; +/// use bouncycastle_modes::Encrypting; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// // 47 bytes is not a multiple of 16: the inline const assertion in `encrypt` fails to compile. +/// let _ = AES_CBC_128::::encrypt(&key, &mut [0u8; 47]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CBC_128

= Cbc; + +/// AES-192 in CBC mode. See [`AES_CBC_128`]. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CBC_192; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<24>::from_bytes_as_type(&[0x42; 24], KeyType::SymmetricCipherKey).unwrap(); +/// let mut data = [0u8; 32]; +/// let iv = AES_CBC_192::::encrypt(&key, &mut data).unwrap(); +/// AES_CBC_192::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, [0u8; 32]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CBC_192 = Cbc; + +/// AES-256 in CBC mode. See [`AES_CBC_128`]. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CBC_256; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); +/// let mut data = [0u8; 32]; +/// let iv = AES_CBC_256::::encrypt(&key, &mut data).unwrap(); +/// AES_CBC_256::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, [0u8; 32]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CBC_256 = Cbc; diff --git a/crypto/aes-lowmemory/src/cfb.rs b/crypto/aes-lowmemory/src/cfb.rs new file mode 100644 index 00000000..5549188c --- /dev/null +++ b/crypto/aes-lowmemory/src/cfb.rs @@ -0,0 +1,96 @@ +//! Type aliases for AES in CFB mode (NIST SP 800-38A Sec 6.3). +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Cfb` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. These aliases pin the AES values so +//! callers never spell them out. They add nothing to the engine: the permutation still implements +//! none of the data-encryption traits itself (see the crate docs), the mode does. +//! +//! The segment size is the full block, so these are **CFB128**. SP 800-38A's `s = 8` and `s = 1` +//! variants are not block-aligned and are not implemented; see the `bouncycastle_modes::Cfb` docs. + +use crate::{Aes128, Aes192, Aes256, BLOCK_LEN}; +use bouncycastle_modes::Cfb; + +/// AES-128 in CFB128 mode. `Dir` is [`bouncycastle_modes::Encrypting`] or +/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// +/// The IV is generated by encryption and returned; it is never supplied. Encryption and decryption +/// work in place. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CFB_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// // 48 bytes: three whole blocks. The length is checked at compile time. +/// let message = [0u8; 48]; +/// let mut data = message; +/// let iv = AES_CFB_128::::encrypt(&key, &mut data).unwrap(); +/// assert_ne!(data, message); +/// AES_CFB_128::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, message); +/// +/// // Streaming, a few blocks at a time: +/// let (mut enc, iv) = AES_CFB_128::::do_encrypt_init(&key).unwrap(); +/// let mut first = [0u8; 16]; +/// let mut rest = [1u8; 32]; +/// enc.do_encrypt(&mut first).unwrap(); +/// enc.do_encrypt(&mut rest).unwrap(); +/// let mut dec = AES_CFB_128::::do_decrypt_init(&key, &iv).unwrap(); +/// dec.do_decrypt(&mut first).unwrap(); +/// dec.do_decrypt(&mut rest).unwrap(); +/// assert_eq!(first, [0u8; 16]); +/// assert_eq!(rest, [1u8; 32]); +/// ``` +/// +/// A length that is not a whole number of blocks is a **compile** error, not a runtime one: +/// +/// ```compile_fail +/// use bouncycastle_aes_lowmemory::AES_CFB_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::BlockCipherEncryptor; +/// use bouncycastle_modes::Encrypting; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// // 47 bytes is not a multiple of 16: the inline const assertion in `encrypt` fails to compile. +/// let _ = AES_CFB_128::::encrypt(&key, &mut [0u8; 47]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CFB_128 = Cfb; + +/// AES-192 in CFB128 mode. See [`AES_CFB_128`]. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CFB_192; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<24>::from_bytes_as_type(&[0x42; 24], KeyType::SymmetricCipherKey).unwrap(); +/// let mut data = [0u8; 32]; +/// let iv = AES_CFB_192::::encrypt(&key, &mut data).unwrap(); +/// AES_CFB_192::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, [0u8; 32]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CFB_192 = Cfb; + +/// AES-256 in CFB128 mode. See [`AES_CFB_128`]. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_CFB_256; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); +/// let mut data = [0u8; 32]; +/// let iv = AES_CFB_256::::encrypt(&key, &mut data).unwrap(); +/// AES_CFB_256::::decrypt(&key, &iv, &mut data).unwrap(); +/// assert_eq!(data, [0u8; 32]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_CFB_256 = Cfb; diff --git a/crypto/aes-lowmemory/src/ecb.rs b/crypto/aes-lowmemory/src/ecb.rs new file mode 100644 index 00000000..d9902f8f --- /dev/null +++ b/crypto/aes-lowmemory/src/ecb.rs @@ -0,0 +1,101 @@ +//! Type aliases for AES in ECB mode (NIST SP 800-38A Sec 6.1). +//! +//! `bouncycastle-modes` is deliberately cipher-agnostic, so `Ecb` takes the permutation, the +//! direction, and the `KEY_LEN` / `BLOCK_LEN` const parameters. These aliases pin the AES values so +//! callers never spell them out. +//! +//! **ECB is not a confidentiality mode for data.** Under a given key every plaintext block maps to +//! the same ciphertext block (Sec 6.1), so the structure of the plaintext shows through, and blocks +//! can be reordered, repeated or removed undetectably. These aliases exist for interoperability with +//! systems that use ECB and for driving test vectors; for data, use CBC or CFB under authentication, +//! or better an AEAD. See the crate docs, "A block permutation is not a cipher". + +use crate::{Aes128, Aes192, Aes256, BLOCK_LEN}; +use bouncycastle_modes::Ecb; + +/// AES-128 in ECB mode. `Dir` is [`bouncycastle_modes::Encrypting`] or +/// [`bouncycastle_modes::Decrypting`]; the wrong direction is a compile error, not a runtime check. +/// +/// There is no IV: `encrypt` returns an empty array and `decrypt` takes one. Encryption and +/// decryption work in place. **Not confidential for data** -- see the module docs. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_ECB_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +/// .expect("a 16-byte symmetric cipher key"); +/// // 48 bytes: three whole blocks. The length is checked at compile time. +/// let message = [0u8; 48]; +/// let mut data = message; +/// let no_iv: [u8; 0] = AES_ECB_128::::encrypt(&key, &mut data).unwrap(); +/// assert_ne!(data, message); +/// // The codebook property: three equal plaintext blocks give three equal ciphertext blocks. +/// assert_eq!(data[..16], data[16..32]); +/// assert_eq!(data[..16], data[32..]); +/// AES_ECB_128::::decrypt(&key, &no_iv, &mut data).unwrap(); +/// assert_eq!(data, message); +/// +/// // Streaming, a few blocks at a time: +/// let (mut enc, _) = AES_ECB_128::::do_encrypt_init(&key).unwrap(); +/// let mut first = [0u8; 16]; +/// let mut rest = [1u8; 32]; +/// enc.do_encrypt(&mut first).unwrap(); +/// enc.do_encrypt(&mut rest).unwrap(); +/// let mut dec = AES_ECB_128::::do_decrypt_init(&key, &[]).unwrap(); +/// dec.do_decrypt(&mut first).unwrap(); +/// dec.do_decrypt(&mut rest).unwrap(); +/// assert_eq!(first, [0u8; 16]); +/// assert_eq!(rest, [1u8; 32]); +/// ``` +/// +/// A length that is not a whole number of blocks is a **compile** error, not a runtime one: +/// +/// ```compile_fail +/// use bouncycastle_aes_lowmemory::AES_ECB_128; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::BlockCipherEncryptor; +/// use bouncycastle_modes::Encrypting; +/// +/// let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +/// // 47 bytes is not a multiple of 16: the inline const assertion in `encrypt` fails to compile. +/// let _ = AES_ECB_128::::encrypt(&key, &mut [0u8; 47]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_ECB_128 = Ecb; + +/// AES-192 in ECB mode. See [`AES_ECB_128`]. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_ECB_192; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<24>::from_bytes_as_type(&[0x42; 24], KeyType::SymmetricCipherKey).unwrap(); +/// let mut data = [0u8; 32]; +/// let no_iv = AES_ECB_192::::encrypt(&key, &mut data).unwrap(); +/// AES_ECB_192::::decrypt(&key, &no_iv, &mut data).unwrap(); +/// assert_eq!(data, [0u8; 32]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_ECB_192 = Ecb; + +/// AES-256 in ECB mode. See [`AES_ECB_128`]. +/// +/// ``` +/// use bouncycastle_aes_lowmemory::AES_ECB_256; +/// use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +/// use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +/// use bouncycastle_modes::{Decrypting, Encrypting}; +/// +/// let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey).unwrap(); +/// let mut data = [0u8; 32]; +/// let no_iv = AES_ECB_256::::encrypt(&key, &mut data).unwrap(); +/// AES_ECB_256::::decrypt(&key, &no_iv, &mut data).unwrap(); +/// assert_eq!(data, [0u8; 32]); +/// ``` +#[allow(non_camel_case_types)] +pub type AES_ECB_256 = Ecb; diff --git a/crypto/aes-lowmemory/src/lib.rs b/crypto/aes-lowmemory/src/lib.rs new file mode 100644 index 00000000..43adfd40 --- /dev/null +++ b/crypto/aes-lowmemory/src/lib.rs @@ -0,0 +1,217 @@ +//! A constant-time, table-free AES block cipher engine (NIST FIPS 197). +//! +//! This crate provides the raw AES keyed permutation -- [`Aes128`], [`Aes192`] and [`Aes256`] -- +//! implemented as a Boolean circuit over bit-planes rather than as byte substitutions through a +//! lookup table. That makes it both smaller and constant-time; see [Design](#design). +//! +//! It is a *permutation*, not a cipher you can encrypt data with. See +//! [Security Considerations](#security-considerations). +//! +//! # Usage Examples +//! +//! ## Encrypting and decrypting a single block +//! +//! ``` +//! use bouncycastle_aes_lowmemory::Aes128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! 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 = Aes128::new(&key).expect("a valid AES-128 key"); +//! +//! // FIPS 197 Appendix B. +//! 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]); +//! +//! // The same value decrypts, from the same schedule -- there is no separate decryptor. +//! aes.decrypt_block(&mut block); +//! assert_eq!(block, [0x32, 0x43, 0xf6, 0xa8, 0x88, 0x5a, 0x30, 0x8d, +//! 0x31, 0x31, 0x98, 0xa2, 0xe0, 0x37, 0x07, 0x34]); +//! ``` +//! +//! ## Two blocks at a time +//! +//! The bit-sliced state holds two blocks, so two independent blocks cost barely more than one. +//! Where a caller has two, [`Aes::encrypt_blocks2`] is roughly twice the throughput of two +//! [`Aes::encrypt_block`] calls: +//! +//! ``` +//! use bouncycastle_aes_lowmemory::Aes256; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! let aes = Aes256::new(&key).expect("a valid AES-256 key"); +//! +//! let mut blocks = [[0u8; 16], [1u8; 16]]; +//! aes.encrypt_blocks2(&mut blocks); +//! aes.decrypt_blocks2(&mut blocks); +//! assert_eq!(blocks, [[0u8; 16], [1u8; 16]]); +//! ``` +//! +//! ## Modes of operation +//! +//! To encrypt more than one block, use a mode of operation from `bouncycastle-modes`. This crate +//! provides aliases that fill in the const parameters, with the direction left as the type +//! parameter: [`AES_CBC_128`], [`AES_CBC_192`] and [`AES_CBC_256`] for CBC (SP 800-38A Sec 6.2), +//! and [`AES_CFB_128`], [`AES_CFB_192`] and [`AES_CFB_256`] for CFB128 (Sec 6.3). The two are +//! interchangeable at the call site -- swap `AES_CBC_256` for `AES_CFB_256` in the example below +//! and nothing else changes. [`AES_ECB_128`], [`AES_ECB_192`] and [`AES_ECB_256`] give ECB +//! (Sec 6.1) the same shape with no IV, for interoperability and test vectors only -- see +//! [A block permutation is not a cipher](#a-block-permutation-is-not-a-cipher). +//! +//! ``` +//! use bouncycastle_aes_lowmemory::AES_CBC_256; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +//! use bouncycastle_modes::{Decrypting, Encrypting}; +//! +//! let key = KeyMaterial::<32>::from_bytes_as_type(&[0x42; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! // 48 bytes: three whole blocks. A length that is not a multiple of 16 would not compile. +//! let plaintext = [0x5Au8; 48]; +//! +//! // Encryption is in place. The IV is generated for you and returned; there is no API for +//! // supplying one. +//! let mut data = plaintext; +//! let iv = AES_CBC_256::::encrypt(&key, &mut data).unwrap(); +//! assert_ne!(data, plaintext); +//! AES_CBC_256::::decrypt(&key, &iv, &mut data).unwrap(); +//! assert_eq!(data, plaintext); +//! ``` +//! +//! There is no one-shot static on the permutation, because `Aes128::new(&key)?.encrypt_block(..)` +//! already *is* the one shot. Data-level one-shots belong to the modes of operation, which take +//! arbitrary-length input and generate their own initialisation data. +//! +//! # Design +//! +//! ## Why not a 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 +//! while the lookup remains. +//! +//! Bouncy Castle's `AESLightEngine` in the Java and C# ports keeps two 256-byte S-box tables for +//! exactly this reason -- to be *small*, not to be constant-time -- and leaks through both the +//! cipher and the key schedule. +//! +//! ## Bit-slicing +//! +//! This crate has no tables at all. The state is transposed so that each of eight `u32` 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. +//! +//! Eight 32-bit words hold 32 bytes, which is two AES blocks, so blocks are processed in pairs. +//! 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 `bitslice` +//! and `round` modules -- those two module docs are the place to start when reading the source. +//! +//! 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 [`Aes`] 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 compressed so that bit-slicing costs nothing in space: +//! +//! | Type | Key | `Nr` | Schedule (persistent) | Tables | +//! |---|---|---|---|---| +//! | [`Aes128`] | 16 B | 10 | 176 B | 0 B | +//! | [`Aes192`] | 24 B | 12 | 208 B | 0 B | +//! | [`Aes256`] | 32 B | 14 | 240 B | 0 B | +//! +//! Per-call stack usage is independent of key length: 32 bytes of bit-sliced state for the two +//! blocks, 32 bytes for the round key expanded from its compressed form, plus the S-box circuit's +//! temporaries, most of which the compiler keeps in registers. +//! +//! For comparison, `AESLightEngine` carries 512 bytes of tables and a T-table implementation +//! carries 2-8 KiB, in both cases *on top of* a key schedule of this same size. +//! +//! Measure with `cargo run --release -p mem_usage_benches --bin bench_aes_mem_usage`. +//! +//! # Security Considerations +//! +//! ## A block permutation is not a cipher +//! +//! [`Aes128`] and friends transform exactly 16 bytes. Using them directly on data means ECB, +//! which is not confidential: identical plaintext blocks produce identical ciphertext blocks, so +//! structure in the plaintext survives encryption. **Do not do it.** Use a mode of operation, and +//! prefer an authenticated one so that ciphertext tampering is detected. +//! +//! The [`AES_ECB_128`] / [`AES_ECB_192`] / [`AES_ECB_256`] aliases give that same block-by-block +//! operation the mode API, so that systems and specifications which require ECB -- and test-vector +//! harnesses -- can use it through the same interface as the other modes. They do not make it +//! confidential; the warning above applies to them unchanged. +//! +//! ## Constant-time properties +//! +//! By construction there is no secret-dependent memory access and no secret-dependent branch, +//! in the cipher *or* in the key schedule -- SUBWORD() goes through the same circuit as +//! SUBBYTES(). The only branches are the round loops, which count over the public `Nr`. +//! +//! 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 32-byte working state is not scrubbed after a block. 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 two-block structure, the transpose, and the SHIFTROWS()/MIXCOLUMNS() mask and +//! rotation constants are translated from BearSSL's `aes_ct` implementation by Thomas Pornin +//! (MIT licence). Each constant is re-derived from the documented bit layout in the comments, +//! and each is 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 aes; +mod bitslice; +mod cbc; +mod cfb; +mod ecb; +mod round; +mod sbox; +mod schedule; + +pub use aes::{Aes, Aes128, Aes192, Aes256, BLOCK_LEN}; +pub use bitslice::Block; +pub use cbc::{AES_CBC_128, AES_CBC_192, AES_CBC_256}; +pub use cfb::{AES_CFB_128, AES_CFB_192, AES_CFB_256}; +pub use ecb::{AES_ECB_128, AES_ECB_192, AES_ECB_256}; +pub use schedule::{Aes128Params, Aes192Params, Aes256Params, AesParams}; diff --git a/crypto/aes-lowmemory/src/round.rs b/crypto/aes-lowmemory/src/round.rs new file mode 100644 index 00000000..b42406cf --- /dev/null +++ b/crypto/aes-lowmemory/src/round.rs @@ -0,0 +1,507 @@ +//! 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]` +//! of block A sits at bit position `8r + 2c` (and block B at `8r + 2c + 1`). Two consequences +//! drive every constant below: +//! +//! * **A row is a byte-lane.** All of row `r` lives in bits `8r..8r+8` of every plane, and +//! stepping one column along that row is a step of two bit positions. So SHIFTROWS(), which +//! only permutes within rows, is a rotation *inside* each byte-lane, by `2r` positions. +//! * **Rotating a whole plane by 8 changes the row.** `x.rotate_right(8)` brings the contents of +//! lane `r+1` into lane `r`, so `rotate_right(8)` reads "the next row down" and +//! `rotate_right(16)` 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. +//! +//! Provenance: the mask and rotation constants are translated from BearSSL +//! `src/symcipher/aes_ct_enc.c` and `aes_ct_dec.c` (MIT, Thomas Pornin). Each is re-derived from +//! the layout in the comments below, and each is pinned by a test in this file against a +//! byte-wise reference written directly from the FIPS 197 equations. + +use crate::bitslice::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 by [`crate::schedule`], so the whole +/// transformation -- all four columns of both blocks -- 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 byte-lane `r` of every plane and +/// one column is two bit positions, so the new column `c` must take what is two-bits-times-`r` +/// further up the lane: a **rotate right by `2r` within lane `r`**. Rotating right, not left, +/// because taking from a higher column index means pulling data down towards bit 0. +/// +/// Written out per lane rather than as a loop, so the shift amounts stay compile-time constants: +/// +/// * lane 0 (`r = 0`): rotate by 0, so bits `0..8` pass through untouched. +/// * lane 1 (`r = 1`): rotate right by 2. Bits 10..16 drop to 8..14; bits 8..10 wrap to 14..16. +/// * lane 2 (`r = 2`): rotate right by 4. Bits 20..24 drop to 16..20; bits 16..20 wrap up. +/// * lane 3 (`r = 3`): rotate right by 6. Bits 30..32 drop to 24..26; bits 24..30 wrap up. +/// +/// Both interleaved blocks move together, since a column step of two positions carries the A and +/// B bits of that column as a pair. +/// +/// Translated from BearSSL `aes_ct_enc.c:shift_rows`. +#[inline(always)] +pub(crate) fn shift_rows(q: &mut Planes) { + for plane in q.iter_mut() { + let x = *plane; + *plane = (x & 0x0000_00FF) + | ((x & 0x0000_FC00) >> 2) + | ((x & 0x0000_0300) << 6) + | ((x & 0x00F0_0000) >> 4) + | ((x & 0x000F_0000) << 4) + | ((x & 0xC000_0000) >> 6) + | ((x & 0x3F00_0000) << 2); + } +} + +/// 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 lane rotation +/// reversed: **rotate left by `2r` within lane `r`**. The masks are the complementary halves of +/// the forward ones. +/// +/// Translated from BearSSL `aes_ct_dec.c:inv_shift_rows`. +#[inline(always)] +pub(crate) fn inv_shift_rows(q: &mut Planes) { + for plane in q.iter_mut() { + let x = *plane; + *plane = (x & 0x0000_00FF) + | ((x & 0x0000_3F00) << 2) + | ((x & 0x0000_C000) >> 6) + | ((x & 0x000F_0000) << 4) + | ((x & 0x00F0_0000) >> 4) + | ((x & 0x0300_0000) << 6) + | ((x & 0xFC00_0000) >> 2); + } +} + +/// 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 `rotate_right(8)` and "two rows down" is +/// `rotate_right(16)` (see the module docs), with `p` the state planes and `r` = `p` rotated by 8: +/// +/// * `p[k]` is bit `k` of `s[r]`, `r[k]` is bit `k` of `s[r+1]`, +/// * `rotate_right(16)` of those two 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_right(16)`, which is the `rotr16(..)` 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 +/// `test_mix_columns_matches_equation_5_8`. +#[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_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] ^ (p[0] ^ r[0]).rotate_right(16); + q[1] = p[0] ^ r[0] ^ p[7] ^ r[7] ^ r[1] ^ (p[1] ^ r[1]).rotate_right(16); + q[2] = p[1] ^ r[1] ^ r[2] ^ (p[2] ^ r[2]).rotate_right(16); + q[3] = p[2] ^ r[2] ^ p[7] ^ r[7] ^ r[3] ^ (p[3] ^ r[3]).rotate_right(16); + q[4] = p[3] ^ r[3] ^ p[7] ^ r[7] ^ r[4] ^ (p[4] ^ r[4]).rotate_right(16); + q[5] = p[4] ^ r[4] ^ r[5] ^ (p[5] ^ r[5]).rotate_right(16); + q[6] = p[5] ^ r[5] ^ r[6] ^ (p[6] ^ r[6]).rotate_right(16); + q[7] = p[6] ^ r[6] ^ r[7] ^ (p[7] ^ r[7]).rotate_right(16); +} + +/// 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, `rotate_right(16)` 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, `test_inv_mix_columns_matches_equation_5_15` checks it against a byte-wise +/// reference written straight from Eq 5.15, and `test_inv_mix_columns_inverts_mix_columns` +/// checks the two 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_right(8)); + + q[0] = p[5] ^ p[6] ^ p[7] ^ r[0] ^ r[5] ^ r[7] + ^ (p[0] ^ p[5] ^ p[6] ^ r[0] ^ r[5]).rotate_right(16); + q[1] = p[0] ^ p[5] ^ r[0] ^ r[1] ^ r[5] ^ r[6] ^ r[7] + ^ (p[1] ^ p[5] ^ p[7] ^ r[1] ^ r[5] ^ r[6]).rotate_right(16); + q[2] = p[0] ^ p[1] ^ p[6] ^ r[1] ^ r[2] ^ r[6] ^ r[7] + ^ (p[0] ^ p[2] ^ p[6] ^ r[2] ^ r[6] ^ r[7]).rotate_right(16); + q[3] = p[0] ^ p[1] ^ p[2] ^ p[5] ^ p[6] ^ r[0] ^ r[2] ^ r[3] ^ r[5] + ^ (p[0] ^ p[1] ^ p[3] ^ p[5] ^ p[6] ^ p[7] ^ r[0] ^ r[3] ^ r[5] ^ r[7]).rotate_right(16); + q[4] = p[1] ^ p[2] ^ p[3] ^ p[5] ^ r[1] ^ r[3] ^ r[4] ^ r[5] ^ r[6] ^ r[7] + ^ (p[1] ^ p[2] ^ p[4] ^ p[5] ^ p[7] ^ r[1] ^ r[4] ^ r[5] ^ r[6]).rotate_right(16); + q[5] = p[2] ^ p[3] ^ p[4] ^ p[6] ^ r[2] ^ r[4] ^ r[5] ^ r[6] ^ r[7] + ^ (p[2] ^ p[3] ^ p[5] ^ p[6] ^ r[2] ^ r[5] ^ r[6] ^ r[7]).rotate_right(16); + q[6] = p[3] ^ p[4] ^ p[5] ^ p[7] ^ r[3] ^ r[5] ^ r[6] ^ r[7] + ^ (p[3] ^ p[4] ^ p[6] ^ p[7] ^ r[3] ^ r[6] ^ r[7]).rotate_right(16); + q[7] = p[4] ^ p[5] ^ p[6] ^ r[4] ^ r[6] ^ r[7] + ^ (p[4] ^ p[5] ^ p[7] ^ r[4] ^ r[7]).rotate_right(16); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bitslice::{pack, unpack}; + + /// Runs a plane transformation over one block placed in both halves, returning the A half. + fn apply(f: fn(&mut Planes), block: [u8; 16]) -> [u8; 16] { + let mut q = pack(&block, &block); + f(&mut q); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, b, "the two interleaved blocks must transform identically"); + a + } + + /// A block whose bytes are all distinct, so any mask error that moves a byte to the wrong + /// position is visible. + fn distinct_block() -> [u8; 16] { + core::array::from_fn(|i| (i as u8).wrapping_mul(17).wrapping_add(3)) + } + + // ---- byte-wise references, written from the FIPS 197 equations ---------------------- + // These use `state[r + 4c] == s[r,c]` (Eq 3.6). They exist only to check the plane + // implementations and are deliberately naive. + + /// Eq 5.5: `s'[r,c] = s[r,(c + r) mod 4]`. + fn ref_shift_rows(s: &[u8; 16]) -> [u8; 16] { + let mut o = [0u8; 16]; + for r in 0..4 { + for c in 0..4 { + o[r + 4 * c] = s[r + 4 * ((c + r) % 4)]; + } + } + o + } + + /// Eq 5.12: `s'[r,c] = s[r,(c - r) mod 4]`. + fn ref_inv_shift_rows(s: &[u8; 16]) -> [u8; 16] { + let mut o = [0u8; 16]; + for r in 0..4 { + for c in 0..4 { + o[r + 4 * c] = s[r + 4 * ((c + 4 - r) % 4)]; + } + } + o + } + + /// Eq 4.5 XTIMES(): multiply by `{02}` in GF(2^8). + fn xtimes(b: u8) -> u8 { + (b << 1) ^ if b & 0x80 != 0 { 0x1b } else { 0 } + } + + /// General GF(2^8) multiplication. Test-only; it branches on `b` and must never see secrets. + fn gf_mul(mut a: u8, mut b: u8) -> u8 { + let mut product = 0u8; + for _ in 0..8 { + if b & 1 != 0 { + product ^= a; + } + b >>= 1; + a = xtimes(a); + } + product + } + + /// Multiplication of a column by a fixed matrix, exactly as FIPS 197 Sec 4.3 defines it. + /// + /// Eq 4.8 gives the output word `[d0,d1,d2,d3]` from the input word `[b0,b1,b2,b3]` and the + /// matrix word `[a0,a1,a2,a3]`: + /// + /// ```text + /// d0 = (a0.b0) + (a3.b1) + (a2.b2) + (a1.b3) + /// d1 = (a1.b0) + (a0.b1) + (a3.b2) + (a2.b3) + /// d2 = (a2.b0) + (a1.b1) + (a0.b2) + (a3.b3) + /// d3 = (a3.b0) + (a2.b1) + (a1.b2) + (a0.b3) + /// ``` + /// + /// so entry `(r,k)` of the matrix is `a[(r - k) mod 4]`, which is what the indexing below is. + /// Both MIXCOLUMNS() and INVMIXCOLUMNS() use this same convention; only the word differs. + fn ref_mix_columns(s: &[u8; 16], coeffs: [u8; 4]) -> [u8; 16] { + let mut o = [0u8; 16]; + for c in 0..4 { + for r in 0..4 { + let mut v = 0u8; + for k in 0..4 { + v ^= gf_mul(s[k + 4 * c], coeffs[(r + 4 - k) % 4]); + } + o[r + 4 * c] = v; + } + } + o + } + + /// Eq 5.6: `[a0, a1, a2, a3] = [{02}, {01}, {01}, {03}]`. + /// + /// Note the order: it is *not* `[{02},{03},{01},{01}]`, which is the first row of the matrix + /// in Eq 5.7 rather than the defining word. Feeding the matrix row in here instead of the + /// word silently transposes the matrix, which happens to leave INVMIXCOLUMNS() passing, so + /// this is a comment worth keeping. + const MIX_COEFFS: [u8; 4] = [0x02, 0x01, 0x01, 0x03]; + /// Eq 5.13: `[a0, a1, a2, a3] = [{0e}, {09}, {0d}, {0b}]`. + const INV_MIX_COEFFS: [u8; 4] = [0x0e, 0x09, 0x0d, 0x0b]; + + /// Eq 5.8, transcribed literally, as a cross-check on [`ref_mix_columns`]. + /// + /// ```text + /// s'0,c = ({02}.s0,c) + ({03}.s1,c) + s2,c + s3,c + /// s'1,c = s0,c + ({02}.s1,c) + ({03}.s2,c) + s3,c + /// s'2,c = s0,c + s1,c + ({02}.s2,c) + ({03}.s3,c) + /// s'3,c = ({03}.s0,c) + s1,c + s2,c + ({02}.s3,c) + /// ``` + #[rustfmt::skip] + fn ref_mix_columns_literal(s: &[u8; 16]) -> [u8; 16] { + let mut o = [0u8; 16]; + for c in 0..4 { + let (s0, s1, s2, s3) = (s[4 * c], s[4 * c + 1], s[4 * c + 2], s[4 * c + 3]); + o[4 * c] = gf_mul(0x02, s0) ^ gf_mul(0x03, s1) ^ s2 ^ s3; + o[4 * c + 1] = s0 ^ gf_mul(0x02, s1) ^ gf_mul(0x03, s2) ^ s3; + o[4 * c + 2] = s0 ^ s1 ^ gf_mul(0x02, s2) ^ gf_mul(0x03, s3); + o[4 * c + 3] = gf_mul(0x03, s0) ^ s1 ^ s2 ^ gf_mul(0x02, s3); + } + o + } + + /// Eq 5.15, transcribed literally, as a cross-check on [`ref_mix_columns`]. + /// + /// ```text + /// s'0,c = ({0e}.s0,c) + ({0b}.s1,c) + ({0d}.s2,c) + ({09}.s3,c) + /// s'1,c = ({09}.s0,c) + ({0e}.s1,c) + ({0b}.s2,c) + ({0d}.s3,c) + /// s'2,c = ({0d}.s0,c) + ({09}.s1,c) + ({0e}.s2,c) + ({0b}.s3,c) + /// s'3,c = ({0b}.s0,c) + ({0d}.s1,c) + ({09}.s2,c) + ({0e}.s3,c) + /// ``` + #[rustfmt::skip] + fn ref_inv_mix_columns_literal(s: &[u8; 16]) -> [u8; 16] { + let mut o = [0u8; 16]; + for c in 0..4 { + let (s0, s1, s2, s3) = (s[4 * c], s[4 * c + 1], s[4 * c + 2], s[4 * c + 3]); + o[4 * c] = gf_mul(0x0e, s0) ^ gf_mul(0x0b, s1) ^ gf_mul(0x0d, s2) ^ gf_mul(0x09, s3); + o[4 * c + 1] = gf_mul(0x09, s0) ^ gf_mul(0x0e, s1) ^ gf_mul(0x0b, s2) ^ gf_mul(0x0d, s3); + o[4 * c + 2] = gf_mul(0x0d, s0) ^ gf_mul(0x09, s1) ^ gf_mul(0x0e, s2) ^ gf_mul(0x0b, s3); + o[4 * c + 3] = gf_mul(0x0b, s0) ^ gf_mul(0x0d, s1) ^ gf_mul(0x09, s2) ^ gf_mul(0x0e, s3); + } + o + } + + // ---- tests -------------------------------------------------------------------------- + + #[test] + fn test_the_two_reference_forms_agree() { + // Eq 5.7 (matrix, via the Sec 4.3 convention) against Eq 5.8 (explicit bytes), and the + // same for Eq 5.14 against Eq 5.15. This is what pins the coefficient word order: get + // MIX_COEFFS wrong and these disagree, independently of the plane implementation. + for seed in 0..32u8 { + let block: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(37) ^ seed); + assert_eq!(ref_mix_columns(&block, MIX_COEFFS), ref_mix_columns_literal(&block)); + assert_eq!( + ref_mix_columns(&block, INV_MIX_COEFFS), + ref_inv_mix_columns_literal(&block) + ); + } + } + + #[test] + fn test_xtimes_reference_matches_the_spec_example() { + // FIPS 197 Sec 4.2 works through {57} . {13}; the intermediate XTIMES() chain from + // Eq 4.5 is {57}, {ae}, {47}, {8e}, {07}. + assert_eq!(xtimes(0x57), 0xae); + assert_eq!(xtimes(0xae), 0x47); + assert_eq!(xtimes(0x47), 0x8e); + assert_eq!(xtimes(0x8e), 0x07); + // and the product itself, {57} . {13} = {fe}. + assert_eq!(gf_mul(0x57, 0x13), 0xfe); + } + + #[test] + fn test_shift_rows_matches_equation_5_5() { + for seed in 0..32u8 { + let block: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(31) ^ seed); + assert_eq!(apply(shift_rows, block), ref_shift_rows(&block)); + } + assert_eq!(apply(shift_rows, distinct_block()), ref_shift_rows(&distinct_block())); + } + + #[test] + fn test_inv_shift_rows_matches_equation_5_12() { + for seed in 0..32u8 { + let block: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(31) ^ seed); + assert_eq!(apply(inv_shift_rows, block), ref_inv_shift_rows(&block)); + } + } + + #[test] + fn test_inv_shift_rows_inverts_shift_rows() { + let block = distinct_block(); + let mut q = pack(&block, &block); + shift_rows(&mut q); + inv_shift_rows(&mut q); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, block); + } + + #[test] + fn test_shift_rows_is_a_bit_permutation() { + // Push a single set bit through and require exactly one bit out, with the induced map on + // bit positions a bijection. That is the real invariant behind the seven masked terms: + // their destination ranges are pairwise disjoint and together cover all 32 bits. + // + // It also explains a known `cargo mutants` result. The `| -> ^` mutants in [`shift_rows`] + // and [`inv_shift_rows`] survive, because on disjoint operands `|` and `^` compute the + // same function -- they are equivalent programs, not a gap in the tests, and no test can + // kill them. What *would* be a bug is masks that overlap or fail to cover, and this test + // is what rules that out. + for (name, f) in [ + ("shift_rows", shift_rows as fn(&mut Planes)), + ("inv_shift_rows", inv_shift_rows as fn(&mut Planes)), + ] { + let mut destinations = [false; 32]; + for bit in 0..32 { + let mut q: Planes = [1u32 << bit; 8]; + f(&mut q); + for plane in q { + assert_eq!( + plane.count_ones(), + 1, + "{name}: bit {bit} must map to exactly one bit, got {plane:#034b}" + ); + } + let dest = q[0].trailing_zeros() as usize; + assert!(!destinations[dest], "{name}: two source bits both map to bit {dest}"); + destinations[dest] = true; + } + assert!( + destinations.iter().all(|&hit| hit), + "{name}: the masks must cover all 32 bit positions" + ); + } + } + + #[test] + fn test_shift_rows_leaves_row_zero_alone() { + // Row 0 is bytes 0, 4, 8, 12 in the Eq 3.6 layout, and Eq 5.5 does not move it. + let block = distinct_block(); + let out = apply(shift_rows, block); + for c in 0..4 { + assert_eq!(out[4 * c], block[4 * c], "row 0, column {c}"); + } + } + + #[test] + fn test_mix_columns_matches_equation_5_8() { + for seed in 0..32u8 { + let block: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(37) ^ seed); + assert_eq!(apply(mix_columns, block), ref_mix_columns(&block, MIX_COEFFS)); + } + assert_eq!( + apply(mix_columns, distinct_block()), + ref_mix_columns(&distinct_block(), MIX_COEFFS) + ); + } + + #[test] + fn test_inv_mix_columns_matches_equation_5_15() { + for seed in 0..32u8 { + let block: [u8; 16] = core::array::from_fn(|i| (i as u8).wrapping_mul(37) ^ seed); + assert_eq!(apply(inv_mix_columns, block), ref_mix_columns(&block, INV_MIX_COEFFS)); + } + } + + #[test] + fn test_inv_mix_columns_inverts_mix_columns() { + let block = distinct_block(); + let mut q = pack(&block, &block); + mix_columns(&mut q); + inv_mix_columns(&mut q); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, block); + } + + #[test] + fn test_add_round_key_is_its_own_inverse() { + let block = distinct_block(); + let key = pack(&[0xA5u8; 16], &[0x5Au8; 16]); + let mut q = pack(&block, &block); + add_round_key(&mut q, &key); + add_round_key(&mut q, &key); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, block); + } + + #[test] + fn test_add_round_key_xors_the_expected_bytes() { + let block = distinct_block(); + let key_block = [0xA5u8; 16]; + let key = pack(&key_block, &key_block); + let mut q = pack(&block, &block); + add_round_key(&mut q, &key); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + for i in 0..16 { + assert_eq!(a[i], block[i] ^ key_block[i]); + } + } +} diff --git a/crypto/aes-lowmemory/src/sbox.rs b/crypto/aes-lowmemory/src/sbox.rs new file mode 100644 index 00000000..8e68d2e3 --- /dev/null +++ b/crypto/aes-lowmemory/src/sbox.rs @@ -0,0 +1,381 @@ +//! 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 sixteen byte positions of two blocks at once, one pass of the circuit +//! substitutes all 32 bytes -- the whole SUBBYTES() transformation of two blocks -- rather than +//! one byte. +//! +//! # 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 `test_sbox_matches_fips197_table_4`, which checks all 256 inputs against Table 4. +//! +//! # 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. `test_sbox_matches_fips197_table_4` is what +//! pins this down -- reversing it produces a wrong S-box, not a subtly different one. + +use crate::bitslice::Planes; + +/// SUBBYTES(): applies the AES S-box to every byte position of both blocks 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. +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 `test_sbox_matches_fips197_table_4`. + 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 both blocks 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 exhaustively against Table 6 by `test_inv_sbox_matches_fips197_table_6`. +/// +/// The derivation and the layer below are from BearSSL `aes_ct_dec.c` +/// (`br_aes_ct_bitslice_invSbox`). +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`. +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; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::bitslice::{pack, unpack}; + + /// FIPS 197 Table 4 (SBOX), transcribed from the published PDF. Test-only: the + /// implementation evaluates the S-box as a Boolean circuit and never indexes a table. + #[rustfmt::skip] + const SBOX_TABLE_4: [u8; 256] = [ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, + ]; + + /// FIPS 197 Table 6 (INVSBOX), transcribed from the published PDF. Test-only. + #[rustfmt::skip] + const INVSBOX_TABLE_6: [u8; 256] = [ + 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, + 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, + 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, + 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, + 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, + 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, + 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, + 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, + 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, + 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, + 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, + 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, + 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, + 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, + 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d, + ]; + + /// Runs a plane transformation over a block placed in both halves, returning the A half. + /// + /// Filling both halves means a wrong interleave shows up as a difference between the two + /// blocks rather than silently passing. + fn apply(f: fn(&mut Planes), block: [u8; 16]) -> [u8; 16] { + let mut q = pack(&block, &block); + f(&mut q); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, b, "the two interleaved blocks must transform identically"); + a + } + + #[test] + fn test_sbox_matches_fips197_table_4() { + // Exhaustive over the whole domain: this is the test that makes the 113 gates + // trustworthy, so it must stay exhaustive. + for x in 0..=255u8 { + let out = apply(sbox, [x; 16]); + assert!( + out.iter().all(|&b| b == out[0]), + "all 16 byte positions must substitute alike, x={x:#04x}" + ); + assert_eq!( + out[0], SBOX_TABLE_4[x as usize], + "SBOX({x:#04x}) should be {:#04x}", + SBOX_TABLE_4[x as usize] + ); + } + } + + #[test] + fn test_inv_sbox_matches_fips197_table_6() { + for x in 0..=255u8 { + let out = apply(inv_sbox, [x; 16]); + assert_eq!( + out[0], INVSBOX_TABLE_6[x as usize], + "INVSBOX({x:#04x}) should be {:#04x}", + INVSBOX_TABLE_6[x as usize] + ); + } + } + + #[test] + fn test_inv_sbox_inverts_sbox() { + for x in 0..=255u8 { + let mut q = pack(&[x; 16], &[x.wrapping_add(1); 16]); + sbox(&mut q); + inv_sbox(&mut q); + let mut a = [0u8; 16]; + let mut b = [0u8; 16]; + unpack(&q, &mut a, &mut b); + assert_eq!(a, [x; 16]); + assert_eq!(b, [x.wrapping_add(1); 16]); + } + } + + #[test] + fn test_sbox_worked_example_from_section_5_1_1() { + // FIPS 197 Sec 5.1.1: "if s(r,c) = {53} ... s'(r,c) = {ed}". + assert_eq!(apply(sbox, [0x53; 16])[0], 0xed); + assert_eq!(SBOX_TABLE_4[0x53], 0xed); + } + + #[test] + fn test_the_two_spec_tables_are_inverses() { + // Guards the transcription of both tables against a typo in either one. + for x in 0..=255u8 { + assert_eq!(INVSBOX_TABLE_6[SBOX_TABLE_4[x as usize] as usize], x); + } + } + + #[test] + fn test_sbox_operates_on_each_byte_position_independently() { + // A block of distinct values, so a mask error that mixes byte positions is caught. + let block: [u8; 16] = core::array::from_fn(|i| (i as u8) * 17); + let out = apply(sbox, block); + for i in 0..16 { + assert_eq!(out[i], SBOX_TABLE_4[block[i] as usize], "byte position {i}"); + } + } +} diff --git a/crypto/aes-lowmemory/src/schedule.rs b/crypto/aes-lowmemory/src/schedule.rs new file mode 100644 index 00000000..9ae50e38 --- /dev/null +++ b/crypto/aes-lowmemory/src/schedule.rs @@ -0,0 +1,461 @@ +//! 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 in a **compressed** bit-sliced form: because +//! bit-slicing is a permutation of bits it does not change the size, and because both interleaved +//! blocks are encrypted under the same key the two halves of a bit-sliced round key are +//! identical, so only one of every pair of words needs keeping. [`round_key`] re-doubles a single +//! round key onto the stack when the round loop needs it. +//! +//! The alternative -- storing the doubled 8-plane form -- would need 352, 416 or 480 bytes, 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 eight 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::{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]` is the spec's `Rcon[j]`, since the spec counts from 1. +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 Sec 6.1 defines exactly three: AES-128, AES-192 and AES-256. 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 AesParamsSealed {} + +/// The per-key-length constants of FIPS 197 Sec 6.1. +/// +/// 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. +pub trait AesParams: AesParamsSealed { + /// Key length in bytes: 16, 24 or 32 (FIPS 197 Sec 6.1). + const KEY_LEN: usize; + /// `Nk`, the key length in 32-bit words: 4, 6 or 8 (FIPS 197 Sec 6.1). + const NK: usize; + /// `Nr`, the number of rounds: 10, 12 or 14 (FIPS 197 Sec 6.1). + const NR: usize; + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + const ALG_NAME: &'static str; + /// `[u32; 4 * (NR + 1)]` -- the compressed schedule. See the module docs. + type Schedule: ZeroizablePrimitive + AsRef<[u32]> + AsMut<[u32]>; +} + +/// AES-128 parameters: 16-byte key, `Nk` = 4, `Nr` = 10 (FIPS 197 Sec 6.1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Aes128Params; +/// AES-192 parameters: 24-byte key, `Nk` = 6, `Nr` = 12 (FIPS 197 Sec 6.1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Aes192Params; +/// AES-256 parameters: 32-byte key, `Nk` = 8, `Nr` = 14 (FIPS 197 Sec 6.1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Aes256Params; + +impl AesParamsSealed for Aes128Params {} +impl AesParamsSealed for Aes192Params {} +impl AesParamsSealed 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) +} + +/// 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`. In the layout of [`crate::bitslice`], the bit positions `8L + i` for +/// `i = 0..8` are all four columns of row `L`, in both blocks. So the transposed state holds byte +/// `L` of `word` in every position of row `L`, one S-box pass substitutes all four bytes (sixteen +/// 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(&mut q); + ortho(&mut q); + q[0] +} + +/// KEYEXPANSION() (FIPS 197 Sec 5.2, Algorithm 2), returning the compressed bit-sliced schedule. +/// +/// `key` must be exactly `P::KEY_LEN` bytes; [`crate::aes`] 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 decompress 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 compressed bit-sliced form, one 4-word round key at a time. + // Both interleaved blocks use the same key, so each round key is bit-sliced with the word + // duplicated into both halves; the two halves are then identical and one bit of each pair is + // redundant, so the even-position bits of the first word and the odd-position bits of the + // second are packed into a single stored word. + for base in (0..w.len()).step_by(4) { + let mut q: Planes = [0u32; 8]; + for j in 0..4 { + q[2 * j] = w[base + j]; + q[2 * j + 1] = w[base + j]; + } + ortho(&mut q); + for j in 0..4 { + // The two masks are complementary, so the operands are disjoint and `|` and `^` agree. + // That is why `cargo mutants` reports the `| -> ^` mutant here as surviving. + w[base + j] = (q[2 * j] & 0x5555_5555) | (q[2 * j + 1] & 0xAAAA_AAAA); + } + } + + schedule +} + +/// Re-doubles round key `round` of a compressed schedule into its eight-plane form. +/// +/// The inverse of the packing at the end of [`expand`]: the even-position bits are spread back +/// over both positions of each pair, and likewise the odd-position bits, giving the two identical +/// halves that [`crate::round::add_round_key`] expects. Eight words of stack, built fresh each +/// round rather than stored. +/// +/// Translated from BearSSL `aes_ct.c:br_aes_ct_skey_expand`. +#[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 = [0u32; 8]; + for j in 0..4 { + let packed = w[4 * round + j]; + let even = packed & 0x5555_5555; + let odd = packed & 0xAAAA_AAAA; + // `even` occupies only even bit positions and `even << 1` only odd ones (and vice versa + // for `odd`), so both spreads combine disjoint operands and `|` and `^` agree. Hence the + // two `| -> ^` mutants `cargo mutants` reports here as surviving. + sk[2 * j] = even | (even << 1); + sk[2 * j + 1] = odd | (odd >> 1); + } + 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`] undoes the pair-compression, and [`ortho`] then undoes the bit-slicing, + /// leaving the duplicated pre-slicing words with `w[4*round + j]` in position `2j`. 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 mut q = round_key::

(schedule, i / 4); + ortho(&mut q); + let j = i % 4; + assert_eq!(q[2 * j], q[2 * j + 1], "both interleaved halves hold the same round key"); + q[2 * j] + } + + /// 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_inverts_the_compression() { + // Round-tripping a known schedule: expand(), then round_key() for every round, and check + // the recovered planes match bit-slicing the classical words directly. + 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 got = round_key::(&schedule, round); + let mut expected: Planes = [0u32; 8]; + for j in 0..4 { + expected[2 * j] = w[4 * round + j]; + expected[2 * j + 1] = w[4 * round + j]; + } + ortho(&mut expected); + assert_eq!(got, expected, "round {round}"); + } + } + + #[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-lowmemory/summary.md b/crypto/aes-lowmemory/summary.md new file mode 100644 index 00000000..cf300350 --- /dev/null +++ b/crypto/aes-lowmemory/summary.md @@ -0,0 +1,485 @@ +# `crypto/aes-lowmemory` — implementation summary + +A constant-time, table-free AES block cipher engine (NIST FIPS 197), added on branch +`feature/officialfrancismendoza/100-AES-lightengine-CBC-mode`. + +This document is the reviewer's orientation: what was built, why the design is the way it is, what +was verified and how, and — importantly — the three places where the working plan or model recall +turned out to be wrong. For end-user documentation see the crate docs in +[`src/lib.rs`](src/lib.rs); for the reasoning behind each individual constant, see the module docs +in [`src/bitslice.rs`](src/bitslice.rs) and [`src/round.rs`](src/round.rs), which are the right +place to start reading the source. + +--- + +## 1. What this crate is (and is not) + +It provides the **raw AES keyed permutation** — `Aes128`, `Aes192`, `Aes256` — transforming exactly +16 bytes at a time. It is not something you can encrypt data with: used directly on data it *is* +ECB, which is not confidential. Modes of operation and padding are separate layers. + +Consistent with the earlier scoping decision for the AES engine, the crate deliberately ships: + +* **no CLI subcommand** — a bare permutation can only offer ECB, +* **no factory registration**, +* **no `core` cipher-trait implementations** (`SymmetricCipher` / `BlockCipherEncryptor` / + `BlockCipherDecryptor`) — those traits are about encrypting *data* and generating initialisation + data, which are mode-of-operation concerns, +* **no `AlgorithmOID`** — NIST CSOR assigns AES OIDs per mode, never to the bare cipher. + +It does implement `core::traits::Algorithm` (name and maximum security strength), which is +metadata rather than a data-encryption API. + +--- + +## 2. Design + +### 2.1 Why there is no lookup table + +FIPS 197 Sec 5.1.1 presents the S-box as a 256-entry 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. A table indexed by a byte of the state is indexed by **secret data**, so on any CPU +with a data cache the access pattern, and therefore the timing, depends on the key. That is the +standard, repeatedly-demonstrated AES cache-timing attack, and it cannot be fixed while the lookup +remains. + +Bouncy Castle's `AESLightEngine` in the Java and C# ports keeps two 256-byte S-box tables in order +to be *small*, not to be constant-time, and leaks through both the cipher and the key schedule. + +This crate has no tables at all. The consequence worth stating plainly: **the low-memory AES and +the constant-time AES are the same implementation here.** Removing the tables is what makes it both. + +### 2.2 Bit-slicing + +The state is transposed so that each of eight `u32` words holds one *bit position* of every byte: +word `q[k]` collects bit `k` of all the bytes. In that representation the S-box becomes a fixed +Boolean circuit and one `&` or `^` applies a gate to every byte position at once. Nothing is ever +indexed by a secret and nothing branches on one. + +Eight 32-bit words hold 256 bits = 32 bytes = **two** AES blocks, so blocks are processed in pairs. +ShiftRows and MixColumns become masks and rotations in the same representation, and the key +schedule is stored already bit-sliced, so no transposition happens inside the round loop. + +### 2.3 The bit layout — derived, not assumed + +`ortho` transposes, within each byte-lane of the eight words, the 8×8 bit matrix indexed by +(word number, bit number within the lane): + +``` +after ortho: q[k] bit (8L + i) == before ortho: q[i] bit (8L + k) +``` + +`pack` loads block A as four little-endian `u32`s into the even words and block B into the odd +words, so before `ortho` byte-lane `L` of word `2c` holds `A[4c + L]`. Substituting `j = 4c + L` +and FIPS 197 Eq (3.6) `s[r,c] = in[r + 4c]` — which makes `r = j mod 4`, `c = j div 4` — gives: + +``` +q[k] bit (8r + 2c) == bit k of s[r,c] of block A +q[k] bit (8r + 2c + 1) == bit k of s[r,c] of block B +``` + +**The byte-lane of the word selects the state row `r`; the bit-pair within that lane selects the +state column `c`; the low bit of the pair is block A and the high bit is block B.** + +``` + c=0 c=1 c=2 c=3 + r=0 | 0 2 4 6 + r=1 | 8 10 12 14 (bit position of block A; + r=2 | 16 18 20 22 add 1 for block B) + r=3 | 24 26 28 30 +``` + +Everything else follows from this table: + +* **ShiftRows** only permutes within rows, and a row is a byte-lane, so it is a rotation *inside* + each byte-lane by `2r` positions (one column = two bit positions). +* **MixColumns** combines the four rows of a column, and `rotate_right(8)` moves one row, so it is + expressible with rotations by 8 and 16 plus the `{1b}` reduction, with no shuffling. + +`test_layout_matches_the_documented_table` pins this exhaustively. Every mask in the crate is only +correct relative to it, which is why it is written down rather than left implicit. + +### 2.4 Both directions from one key schedule + +Decryption follows **FIPS 197 Algorithm 3** (the straight inverse cipher), not the equivalent +inverse cipher of Sec 5.3.5. Algorithm 3 applies InvMixColumns *after* AddRoundKey, so it uses the +**unmodified** key schedule; Sec 5.3.5 reorders the round and needs a separate schedule with +InvMixColumns applied to every round key (Algorithm 5, `KEYEXPANSIONEIC()`). + +Following Algorithm 3 is what lets one `Aes` value encrypt *and* decrypt from a single stored +schedule — no second copy, no transformation at construction time, no direction flag. That is the +whole reason both directions are available at 176–240 bytes of state. + +### 2.5 Typing the three key sizes + +The schedule length `4·(Nr+1)` (44/52/60 words) cannot be written as an expression over another +const generic parameter, so a params trait is used instead — the same pattern as the +`HashDRBG80090AParams_*` types in `bouncycastle-rng`: + +```rust +pub trait AesParams: AesParamsSealed { + const KEY_LEN: usize; // 16 | 24 | 32 (FIPS 197 Sec 6.1) + const NK: usize; // 4 | 6 | 8 + const NR: usize; // 10 | 12 | 14 + const ALG_NAME: &'static str; + type Schedule: ZeroizablePrimitive + AsRef<[u32]> + AsMut<[u32]>; +} +``` + +`AesParams` has a **private** supertrait, so only the three types in `schedule.rs` can implement +it and no downstream crate can instantiate the cipher with an unapproved key length or round count. +(This is what `#![allow(private_bounds)]` in `lib.rs` is for.) + +The three `new` constructors and `Algorithm` impls are written out **longhand rather than with +`macro_rules!`**, because `cargo mutants` cannot see into macro bodies and a macro would hide the +key checks and security-strength constants from mutation testing. + +### 2.6 Memory + +No lookup tables, no heap allocation. The only persistent state is the key schedule, stored in a +compressed bit-sliced form: bit-slicing is a permutation of bits so it does not change the size, and +because both interleaved blocks use the same key the two halves of a bit-sliced round key are +identical, so one word of each pair is redundant. `round_key` re-doubles a single round key onto the +stack when the round loop needs it. + +| Type | Key | `Nr` | Schedule (persistent) | Tables | +|---|---|---|---|---| +| `Aes128` | 16 B | 10 | 176 B | 0 B | +| `Aes192` | 24 B | 12 | 208 B | 0 B | +| `Aes256` | 32 B | 14 | 240 B | 0 B | + +These are **measured**, not asserted — `cargo run --release -p mem_usage_benches --bin bench_aes_mem_usage` +prints exactly 176/208/240, and `test_engine_sizes_match_the_documented_memory_table` pins them so +the doc table cannot drift. + +Two things deliberately avoided: storing the doubled 8-plane schedule (352/416/480 B), and +mirroring BearSSL's `uint32_t skey[120]` 480-byte scratch buffer during expansion. `expand` writes +the classical schedule into the final array and then rewrites it in place, one round key at a time, +using eight words of stack. + +Per-call stack usage is independent of key length: 32 B of bit-sliced state for the two blocks, +32 B for the expanded round key, plus circuit temporaries that mostly stay in registers. + +### 2.7 API surface + +```rust +Aes128::new(&KeyMaterial<16>) -> Result // and 24 / 32 +aes.encrypt_block(&mut [u8; 16]) // infallible +aes.decrypt_block(&mut [u8; 16]) +aes.encrypt_blocks2(&mut [[u8; 16]; 2]) // the natural unit of work +aes.decrypt_blocks2(&mut [[u8; 16]; 2]) +``` + +No `init()`, no `reset()`, no direction flag: constructors set up state and a constructed value is +always ready. There are no one-shot statics on the permutation because +`Aes128::new(&key)?.encrypt_block(..)` already *is* the one shot; data-level one-shots belong to the +modes, which take arbitrary-length input and generate their own initialisation data. + +`encrypt_blocks2` / `decrypt_blocks2` are the pair form and roughly double throughput. A +single-block call duplicates the block into both halves and discards one result, so it does twice +the necessary work — modes whose blocks are independent (CTR, and the decrypt direction of CBC and +CFB) should prefer the pair form; CBC *encryption* cannot, since its blocks are serially dependent. + +Duplicating rather than zero-filling the unused half costs the same and buys a free self-check (the +two halves must agree, which `debug_assert` verifies). It is not a security property — the unused +half is never returned either way. + +--- + +## 3. Files + +### New crate + +| File | Lines | Contents | +|---|---|---| +| `Cargo.toml` | 18 | deps: `core`, `utils`; dev-deps: `hex`, `rng`, `criterion`, `serde_json` | +| [`src/lib.rs`](src/lib.rs) | 175 | Crate docs: Usage Examples, Design, Memory Usage, Security Considerations, Provenance | +| [`src/bitslice.rs`](src/bitslice.rs) | 210 | `ortho`, `pack`, `unpack`; the layout table and its exhaustive test | +| [`src/sbox.rs`](src/sbox.rs) | 377 | The 113-gate circuit; `inv_sbox`; Tables 4 and 6 for tests | +| [`src/round.rs`](src/round.rs) | 507 | AddRoundKey, ShiftRows, MixColumns and inverses; byte-wise references | +| [`src/schedule.rs`](src/schedule.rs) | 456 | `AesParams`, `expand` (Alg 2), `round_key`; Appendix A tables | +| [`src/aes.rs`](src/aes.rs) | 276 | `Aes

`, the three aliases, Alg 1 and Alg 3, key validation | +| [`tests/fips197_tests.rs`](tests/fips197_tests.rs) | 230 | Appendix B; two-block path; key handling | +| [`tests/sp800_38a_tests.rs`](tests/sp800_38a_tests.rs) | 176 | SP 800-38A F.1.1–F.1.6 | +| [`tests/acvp_tests.rs`](tests/acvp_tests.rs) | 266 | NIST ACVP `ACVP-AES-ECB` loader | +| [`benches/aes_benches.rs`](benches/aes_benches.rs) | 183 | criterion; key expansion and 16 KiB throughput, 1-block vs 2-block | + +### Changed elsewhere + +* `Cargo.toml` — `bouncycastle-aes-lowmemory` in `workspace.dependencies` and in the umbrella + `[dependencies]`. +* `src/lib.rs` — `pub use bouncycastle_aes_lowmemory as aes_lowmemory;`. +* `mem_usage_benches/bench_aes_mem_usage.rs` (new, 131 lines), plus its `[[bin]]` entry in + `mem_usage_benches/Cargo.toml` and a `mod` line in `mem_usage_benches/lib.rs`. +* `alpha_0.1.3_release_notes.md` — a "Major features" entry. + +--- + +## 4. Verification + +58 tests, all passing. The strategy is that **no expected value anywhere was written from +recall** — every one is transcribed from a downloaded specification PDF or an official vector file. + +| Source | What is checked | +|---|---| +| FIPS 197 Table 4 / Table 6 | **Exhaustive**: all 256 inputs to `sbox` and `inv_sbox`. This is what makes the 113 gates trustworthy, so it must stay exhaustive. | +| FIPS 197 Sec 5.1.1 | The worked example `S[{53}] = {ed}`. | +| FIPS 197 Eq 5.5 / 5.8 / 5.12 / 5.15 | ShiftRows and MixColumns and their inverses, against byte-wise references written from the equations — plus a second literal transcription of Eq 5.8/5.15 cross-checking the matrix form. | +| FIPS 197 Sec 4.2 / Eq 4.5 | The test-only `xtimes`/`gf_mul` helpers against the Sec 4.2 worked chain and `{57}·{13} = {fe}`. | +| FIPS 197 Table 5 | `RCON` re-derived by repeated XTIMES and compared. | +| FIPS 197 Appendix A.1/A.2/A.3 | **Every one of the 156 schedule words**, for all three key lengths. | +| FIPS 197 Appendix B | The worked AES-128 block, both directions, and via the two-block path in both slots. | +| SP 800-38A F.1.1–F.1.6 | ECB known answers, all three key lengths, both directions. | +| NIST ACVP `ACVP-AES-ECB` | **2138 cases** (AES-128: 588, AES-192: 720, AES-256: 830), each checked in *both* directions and through both the single-block and two-block paths. | + +### Why Appendix A is tested inside `src/schedule.rs` + +The key schedule is deliberately not public API (a `Secret` field). A round-trip through the cipher +**cannot** validate it: a wrong `w[i]` is used by encryption and decryption alike, so the round trip +still succeeds. The Appendix A tests therefore live in the module, where `round_key` + `ortho` +decompress the stored schedule back to classical words so every `w[i]` can be compared against the +appendix directly. `tests/fips197_tests.rs` says so explicitly, so nobody mistakes its round-trip +test for schedule validation. + +### The ACVP loader + +Vectors come from `bc-test-data` at `crypto/aes_tdes_vectors/AES/ACVP-AES-ECB.4014527.rsp.json`. +If that repository is not checked out the test prints a warning and passes, matching the ML-KEM / +ML-DSA convention — `cargo test` stays green for someone who has only cloned this repo. A +`checked > 1000` assertion guards against a silently-empty run. + +The response file records `key`, `pt` and `ct` for every case regardless of the group's declared +direction, so each is checked both ways; the request file's group metadata is not needed. + +Two details worth knowing: + +* Some AFT cases have multi-block plaintexts, so the loader iterates blocks (ECB). +* The set includes **all-zero keys** (the GFSbox-style groups). `KeyMaterial` tags an all-zero + buffer `Zeroized` and refuses to promote it outside a hazardous closure — which is the right + default, and `Aes128::new` rejecting it is itself tested. The *test* opts in via + `do_hazardous_operations`; the engine's guard was **not** weakened to accommodate NIST. + +### Only the ECB file belongs to this crate + +`bc-test-data` ships thirteen ACVP AES vector sets, one per mode. This crate consumes only +`ACVP-AES-ECB`, because that is the set that tests the permutation rather than a mode. +`ACVP-AES-CBC` is consumed by [`crypto/modes/tests/acvp_tests.rs`](../modes/tests/acvp_tests.rs) +(2150 AFT cases). The remaining eleven — `CBC-CS1/2/3`, `CFB8`, `CFB128`, `OFB`, `CTR`, `KW`, +`KWP`, `FF1`, `FF3-1` — are unused because those modes are unimplemented, not because they are +untested. The table in the ACVP test module's docs records which file goes where, so adding a mode +includes wiring up its file. + +### Constant-time hygiene audit + +Mechanically checked, not merely claimed: + +* **Every** indexing expression in non-test code is a literal constant (`q[0]`…`q[7]`), a loop + counter over a fixed public range, or `4*round + j` where `round` counts over the public `Nr`. + Not one index is derived from key or state bytes. +* The only branches in non-test code are on `i % Nk` and `Nk > 6` (public parameters) in the key + expansion, and on key *metadata* (type, length, security strength) once at construction. None on + key or state bytes. +* `SUBWORD()` in the key expansion goes through the same bit-sliced circuit as `SUBBYTES()`. A + table-driven "light" AES that removes the tables only from the cipher still leaks through the + schedule; this one does not. + +Caveats are stated in the crate docs rather than glossed: the compiler is not contractually obliged +to preserve straight-line codegen; the 32-byte working state is not scrubbed after a block (only the +schedule is `Secret`); and constant-time execution says nothing about power or EM side channels. + +### Gates + +* `cargo fmt --all -- --check` — clean. +* `cargo build --workspace`, `cargo test --workspace` — clean, no failures. +* `cargo doc -p bouncycastle-aes-lowmemory --no-deps` — **zero warnings**. +* `cargo clippy -p bouncycastle-aes-lowmemory --all-targets` — **zero warnings** for this crate. +* `./dev_scripts/quality_stats.sh ./crypto/aes-lowmemory` — `Err()` in core code: **3**, exactly the + three key rejections in `validate`. `unwrap()` in core code: 4, each a + `try_into()` on a fixed-size window of a fixed-size array with a preceding justification comment. + (Note: `cloc` and `bc` are not installed locally, so the line-count and ratio fields print 0.) + +### Mutation testing + +`cargo mutants -p bouncycastle-aes-lowmemory` — complete run, 32 minutes: + +``` +791 mutants tested: 762 caught, 19 missed, 10 unviable, 0 timeouts +``` + +Every one of the 19 misses was investigated. **18 are provable XOR/OR equivalences and no test can +kill them; 1 was a real coverage gap, since fixed.** + +#### The 18 equivalences + +| Count | Site | Mutation | +|---|---|---| +| 6 | `round.rs` `shift_rows` | `\|` → `^` | +| 6 | `round.rs` `inv_shift_rows` | `\|` → `^` | +| 2 | `bitslice.rs` `ortho::swap` | `\|` → `^` | +| 2 | `schedule.rs` `round_key` | `\|` → `^` | +| 1 | `schedule.rs` `expand` | `\|` → `^` | +| 1 | `sbox.rs` `sbox` (the `t37` gate) | `^` → `\|` | + +`a | b` and `a ^ b` differ only where both operands have a set bit, so wherever the operands are +provably disjoint the two are the same function and no test can distinguish them. This is the +"XOR/OR equivalences in crypto code are acceptable" category named in `CLAUDE.md`. Each site is +disjoint for a different reason: + +* **`shift_rows` / `inv_shift_rows`** — the seven masked terms have pairwise-disjoint destination + bit ranges that together cover all 32 bits. +* **`ortho::swap`** — the masks are complementary and the shift equals the field width. +* **`expand`** — the compression combines `& 0x5555_5555` with `& 0xAAAA_AAAA`, complementary masks. +* **`round_key`** — `even` occupies only even bit positions and `even << 1` only odd ones (and + conversely for `odd`). +* **`sbox`, the `t37 = t36 ^ t34` gate** — the interesting one, because it is a gate *inside* the + circuit rather than a mask combination, and because a surviving mutant there would suggest the + exhaustive Table 4 test had a hole. It does not: brute-forcing all 256 inputs shows `t36` and + `t34` are **never both 1**, so XOR and OR agree, and the mutant changes the output for 0 of 256 + inputs. Sweeping the same mutation across every XOR gate confirms `t37` is the **only one of the + 77** with that property — every other `^ → |` mutant in the circuit is killed. So the exhaustive + test is exactly as strong as claimed; this gate just happens to have disjoint operands. + +Rather than leave the `shift_rows` case as an assertion, the underlying invariant is now tested: +`test_shift_rows_is_a_bit_permutation` pushes a single set bit through and requires exactly one bit +out, with the induced map a bijection on all 32 positions — precisely the disjointness and coverage +property, and it *would* fail if a mask ever overlapped or failed to cover. Every one of the six +sites also carries an in-code comment explaining why its mutant survives, so the next reader does +not have to repeat this investigation. + +#### The one real gap, fixed + +**`< → >` in `Aes

::validate`.** There was no test for a key whose security strength is *below* +the level its length implies; because `from_bytes_as_type` always tags a key at its length-implied +strength, neither `<` nor `>` was ever true and the two comparisons behaved identically. +`a_key_carrying_too_low_a_security_strength_is_rejected` now covers it (a 32-byte key lowered to +128-bit must be rejected by `Aes256::new`), and the fix was confirmed by hand-applying the mutation +and watching that test fail, then reverting. + +This mutant still appears in the run output above, which analysed the pre-fix source — the fix +landed while the run was in flight. Re-running `cargo mutants` should therefore report **18 missed, +763 caught**, all 18 being the documented equivalences. + +#### Unviable + +The 10 unviable mutants are all `replace with Err(...)` / `with ()` on functions whose return +type does not admit the substituted value (`validate`, `Debug::fmt`, `encrypt2`). `cargo mutants` +counts these as unviable rather than missed; they are a property of the config's `error_values` +list, not a coverage gap. + +--- + +## 5. Three corrections worth flagging to reviewers + +### 5.1 The working plan's bit-layout claim is wrong + +`bc-rust-aes-lowmemory-plan.md` §2 states the layout is "`q[k]` bit `2·j` is bit k of byte j of +block A". That is **false**. The correct layout, derived in §2.3 above and pinned exhaustively, is +`q[k]` bit `(8r + 2c)`. Anyone checking the ShiftRows or MixColumns constants against the plan's +version will conclude, wrongly, that they are all broken. The plan's own instruction — "Any place +BearSSL's constants and your FIPS 197 derivation disagree: the spec wins; re-derive, then look for +the misunderstanding (it will be in the layout table)" — turned out to point at the plan itself. + +### 5.2 FIPS 197 Eq 5.6 is `[{02},{01},{01},{03}]` + +Not `[{02},{03},{01},{01}]`, which is the first *row* of the Eq 5.7 matrix rather than the defining +word of Sec 4.3. Sec 4.3 Eq (4.8) defines matrix entry `(r,k)` as `a[(r-k) mod 4]`, and both +MixColumns and InvMixColumns use that same convention — Eq 5.13's `[{0e},{09},{0d},{0b}]` is +correct as printed. + +This one was written into a test constant from memory and caught by the failing test. It is worth +recording because of *how* it fails: supplying the matrix row instead of the defining word silently +transposes the matrix, which leaves the InvMixColumns test **passing**, so only the forward test +detects it. A literal transcription of Eq 5.8 and Eq 5.15 was added as a second, independent +reference (`test_the_two_reference_forms_agree`) so the convention is pinned from both directions, +and `MIX_COEFFS` carries a comment about the trap. + +### 5.3 The plan's "PR B" is unnecessary + +The plan calls for downloading CAVP AESAVS `.rsp` files and opening a PR against `bcgit/bc-test-data` +to add them. `bc-test-data` **already** ships NIST ACVP AES vectors for every mode, including +`crypto/aes_tdes_vectors/AES/ACVP-AES-ECB.4014527.{req,rsp}.json` — 2138 AFT cases across all three +key lengths, more coverage than the AESAVS KAT/MMT files would have provided. No PR to +`bc-test-data` is needed. `serde_json` as a dev-dependency is the established way to read these +files (see the ML-KEM and ML-DSA suites). + +--- + +## 6. Scope deliberately not implemented + +| Item | Why | +|---|---| +| `ElectronicCodeBook` trait impls, and `encrypt_blocks2`/`decrypt_blocks2` as trait methods | The trait does not exist in `crypto/core`, which has the mode-level `BlockCipher` / `BlockCipherEncryptor` / `BlockCipherDecryptor`. Introducing it is the plan's separate "PR A". The two-block entry points are inherent methods for now; promoting them to provided trait methods is a one-line delegation once the trait lands. | +| `core-test-framework` conformance test | Follows from the above — there is no test suite for a raw permutation yet. | +| ACVP MCT (Monte Carlo) groups — 6 cases | Their expected `resultsArray` comes from a chained key/plaintext update rule defined in the ACVP AES specification, not in FIPS 197. Implementing it from anything other than that specification would be guesswork. The test reports the skip count so the gap is visible rather than silent. | +| CLI subcommand | A bare permutation only does ECB. `aes128-cbc-*` / `-cfb-*` belong with the modes crate. | +| Factory registration | No `BlockCipherFactory` exists; not adding one here. | +| bc-java `AESLightEngine` cross-check | The plan marks it developer-local rather than committed, and 2138 ACVP vectors plus the spec appendices make it redundant. | + +--- + +## 7. Provenance and attribution + +* **Normative reference: NIST FIPS 197** (including Update 1). Every transformation cites its + section, algorithm and equation numbers, verified against a freshly downloaded copy of the PDF. +* **The S-box circuit** is the 113-gate straight-line program `SLP_AES_113.txt` from Peralta's + circuit collection — 32 AND, 77 XOR, 4 XNOR — described in J. Boyar and R. Peralta, "A new + combinational logic minimization technique with applications to cryptology", + . The gate list was transcribed **mechanically** from the + SLP file (`+` → `^`, `x` → `&`, `#` → `!(..^..)`, names unchanged apart from case) and the result + diffed against the generator output to rule out transcription error. It is not meaningful line by + line and should not be "tidied"; it is verified as a whole by the exhaustive Table 4 test. +* **The bit-sliced two-block structure**, the transpose, and the ShiftRows/MixColumns mask and + rotation constants are translated from BearSSL's `aes_ct` implementation by Thomas Pornin + (`src/symcipher/aes_ct.c`, `aes_ct_enc.c`, `aes_ct_dec.c`, `aes_ct_cbcdec.c`), **MIT licensed**. + Each constant is re-derived from the documented layout in the comments and pinned by a test + against a byte-wise reference written from the FIPS 197 equations. + +Two notes on where the sources disagree, both resolved in favour of the SLP file: + +* Its bottom linear transformation (`tc1..tc26`) **differs from** BearSSL's (`t46..t67`), and its + `t17`/`t21` are re-associated relative to BearSSL's. Both compute the same S-box. +* The SLP numbers inputs and outputs with `U0`/`S0` as the **most significant** bit, so `U0` is + plane `q[7]`. Reversing this produces a wrong S-box, not a subtly different one; the exhaustive + Table 4 test is what pins it. + +**Open question for maintainers:** how attribution for the BearSSL translation and the +Boyar–Peralta circuit should be recorded — file headers only (current state), a top-level `NOTICE` +file, or both. This is a licensing/policy call rather than a technical one. + +--- + +## 8. Reproducing the checks + +```sh +cargo build -p bouncycastle-aes-lowmemory +cargo test -p bouncycastle-aes-lowmemory # 58 tests +cargo test -p bouncycastle-aes-lowmemory --test acvp_tests -- --nocapture # prints the ACVP count +cargo doc -p bouncycastle-aes-lowmemory --no-deps # expect zero warnings +cargo clippy -p bouncycastle-aes-lowmemory --all-targets +cargo fmt --all -- --check +cargo bench -p bouncycastle-aes-lowmemory +cargo mutants -p bouncycastle-aes-lowmemory +./dev_scripts/quality_stats.sh ./crypto/aes-lowmemory + +# struct sizes; add the massif recipe in the file header for stack measurement +cargo run --release -p mem_usage_benches --bin bench_aes_mem_usage +``` + +The ACVP tests additionally need `bc-test-data` cloned as a sibling of this repository; without it +they print a warning and pass. + +--- + +## 9. Open items before merge + +1. **Decide the attribution form** for the BearSSL translation and the Boyar–Peralta circuit (§7): + file headers only (current state), a top-level `NOTICE`, or both. A licensing/policy call rather + than a technical one. +2. **Confirm the PR base branch.** The plan specifies `release/0.1.3alpha`, set explicitly — GitHub + defaults to `main`. +3. Decide whether `ElectronicCodeBook` (plan PR A) lands before or after this crate, since it + determines whether the two-block entry points become trait methods now or later (§6). +4. Note in the PR description that the plan's layout claim (§5.1) and PR B (§5.3) are superseded, so + the plan document does not mislead the next reader. +5. Optionally re-run `cargo mutants` to confirm the expected 18 missed / 763 caught (§4). The 19th + miss was fixed while the recorded run was in flight, so the numbers above under-report by one. diff --git a/crypto/aes-lowmemory/tests/acvp_tests.rs b/crypto/aes-lowmemory/tests/acvp_tests.rs new file mode 100644 index 00000000..f8d518f0 --- /dev/null +++ b/crypto/aes-lowmemory/tests/acvp_tests.rs @@ -0,0 +1,285 @@ +//! 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 thirteen 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) and `crypto/modes/tests/acvp_ecb_tests.rs` (the `Ecb` mode) | +//! | `ACVP-AES-CBC` | `crypto/modes/tests/acvp_tests.rs` | +//! | `ACVP-AES-CBC-CS1` / `-CS2` / `-CS3` | nothing yet (ciphertext stealing is unimplemented) | +//! | `ACVP-AES-CFB128` | `crypto/modes/tests/acvp_cfb_tests.rs` | +//! | `ACVP-AES-CFB8` | nothing yet (sub-block CFB is unimplemented) | +//! | `ACVP-AES-OFB` | nothing yet (OFB is unimplemented) | +//! | `ACVP-AES-CTR` | nothing yet (CTR is unimplemented) | +//! | `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_lowmemory::{Aes128, Aes192, Aes256, BLOCK_LEN}; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::traits::SecurityStrength; +use bouncycastle_hex as hex; +use serde_json::Value; +use std::fs; +use std::path::{Path, PathBuf}; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = [ + "../../../bc-test-data/crypto/aes_tdes_vectors/AES", + "../bc-test-data/crypto/aes_tdes_vectors/AES", +]; + +const RESPONSE_FILE: &str = "ACVP-AES-ECB.4014527.rsp.json"; + +/// Locates the ACVP AES directory, or `None` if `bc-test-data` is not checked out. +fn test_data_dir() -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate); + if path.join(RESPONSE_FILE).exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); \ + ACVP AES-ECB tests will be skipped" + ); + None +} + +/// 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 `Aes128::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() % 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 = Aes128::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 = Aes192::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 = Aes256::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(BLOCK_LEN) { + // Cannot fail: the length is asserted block-aligned above. + let mut block: [u8; 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() % BLOCK_LEN, 0, "ACVP ECB data must be block-aligned"); + let mut blocks: Vec<[u8; BLOCK_LEN]> = + data.chunks(BLOCK_LEN).map(|c| c.try_into().unwrap()).collect(); + + match key.len() { + 16 => { + let km = cipher_key::<16>(key); + let aes = Aes128::new(&km).unwrap(); + run_pairwise(&mut blocks, encrypt, |p, e| { + if e { aes.encrypt_blocks2(p) } else { aes.decrypt_blocks2(p) } + }); + } + 24 => { + let km = cipher_key::<24>(key); + let aes = Aes192::new(&km).unwrap(); + run_pairwise(&mut blocks, encrypt, |p, e| { + if e { aes.encrypt_blocks2(p) } else { aes.decrypt_blocks2(p) } + }); + } + 32 => { + let km = cipher_key::<32>(key); + let aes = Aes256::new(&km).unwrap(); + run_pairwise(&mut blocks, encrypt, |p, e| { + if e { aes.encrypt_blocks2(p) } else { aes.decrypt_blocks2(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; BLOCK_LEN]], + encrypt: bool, + transform: impl Fn(&mut [[u8; 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; 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(dir) = test_data_dir() else { return }; + + let contents = fs::read_to_string(dir.join(RESPONSE_FILE)).expect("readable response file"); + let parsed: Value = serde_json::from_str(&contents).expect("valid ACVP JSON"); + + // 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_blocks2", + key.len() * 8 + ); + assert_eq!( + ecb_pairwise(&key, &ct, false), + pt, + "tcId {tc_id}: AES-{} decrypt via decrypt_blocks2", + 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-lowmemory/tests/electronic_code_book_tests.rs b/crypto/aes-lowmemory/tests/electronic_code_book_tests.rs new file mode 100644 index 00000000..2098315e --- /dev/null +++ b/crypto/aes-lowmemory/tests/electronic_code_book_tests.rs @@ -0,0 +1,25 @@ +//! `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 pair methods are indistinguishable from two +//! single-block calls *including their order*, and the key checks behave. That last pair of +//! properties matters here specifically: this crate overrides `encrypt_blocks2` and +//! `decrypt_blocks2`, so the default implementation is not what runs. + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256, BLOCK_LEN}; +use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; + +#[test] +fn aes128_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<16, BLOCK_LEN, Aes128>(); +} + +#[test] +fn aes192_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<24, BLOCK_LEN, Aes192>(); +} + +#[test] +fn aes256_conforms_to_electronic_code_book() { + TestFrameworkElectronicCodeBook::new().test::<32, BLOCK_LEN, Aes256>(); +} diff --git a/crypto/aes-lowmemory/tests/fips197_tests.rs b/crypto/aes-lowmemory/tests/fips197_tests.rs new file mode 100644 index 00000000..d1261b8d --- /dev/null +++ b/crypto/aes-lowmemory/tests/fips197_tests.rs @@ -0,0 +1,230 @@ +//! Known-answer tests from NIST FIPS 197 itself. +//! +//! Appendix B -- the worked single-block AES-128 encryption -- plus its inverse, the two-block +//! path, 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 decompressed and compared directly. +//! +//! Known-answer coverage for AES-192 and AES-256, which Appendix B does not reach, is in +//! `sp800_38a_tests.rs` and `acvp_tests.rs`. +//! +//! All values here are transcribed from the published FIPS 197 (Update 1) PDF. + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::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 = Aes128::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 = Aes128::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 = Aes128::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, + ]; + + // Pairing the Appendix B block with an unrelated one must not disturb either half. + let other = [0xAAu8; 16]; + let mut other_alone = other; + aes.encrypt_block(&mut other_alone); + + let mut pair = [input, other]; + aes.encrypt_blocks2(&mut pair); + assert_eq!(pair[0], expected); + assert_eq!(pair[1], other_alone); + + // ...and in the other slot, which is a different bit position in the interleave. + let mut pair = [other, input]; + aes.encrypt_blocks2(&mut pair); + assert_eq!(pair[0], other_alone); + assert_eq!(pair[1], expected); +} + +/// 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 = Aes128::new(&key_material(&KEY_128)).unwrap(); + let aes192 = Aes192::new(&key_material(&KEY_192)).unwrap(); + let aes256 = Aes256::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 = Aes128::new(&key_material::<16>(&shared[..16].try_into().unwrap())).unwrap(); + let aes192 = Aes192::new(&key_material::<24>(&shared[..24].try_into().unwrap())).unwrap(); + let aes256 = Aes256::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!(Aes128::new(&key).is_err()); + + let key = KeyMaterial::<16>::from_bytes_as_type(&[0x01; 16], KeyType::MACKey).unwrap(); + assert!(Aes128::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!(Aes256::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!( + Aes256::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!(Aes256::new(&good).is_ok()); +} + +#[test] +fn a_correctly_typed_key_of_each_length_is_accepted() { + assert!(Aes128::new(&key_material(&KEY_128)).is_ok()); + assert!(Aes192::new(&key_material(&KEY_192)).is_ok()); + assert!(Aes256::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 = Aes128::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-lowmemory/tests/sp800_38a_tests.rs b/crypto/aes-lowmemory/tests/sp800_38a_tests.rs new file mode 100644 index 00000000..8e975eca --- /dev/null +++ b/crypto/aes-lowmemory/tests/sp800_38a_tests.rs @@ -0,0 +1,176 @@ +//! 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. `acvp_tests.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_lowmemory::{Aes128, Aes192, Aes256, BLOCK_LEN}; +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; 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 = Aes128::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 = Aes128::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 = Aes192::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 = Aes192::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 = Aes256::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 = Aes256::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 interleave: a mistake in which bit of each pair belongs to +/// which block shows up here and nowhere in the single-block tests, because a single-block call +/// puts the same data in both halves. +#[test] +fn two_block_path_matches_the_f_1_vectors() { + let aes = Aes128::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_blocks2(&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_blocks2(&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 = Aes256::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_blocks2(&mut forward); + aes.encrypt_blocks2(&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])); +} diff --git a/crypto/ascon/Cargo.toml b/crypto/ascon/Cargo.toml new file mode 100644 index 00000000..25a58829 --- /dev/null +++ b/crypto/ascon/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bouncycastle-ascon" +version.workspace = true +edition.workspace = true + +[features] +# `std` gates the ergonomic, allocating (`Vec`-returning) one-shot cipher APIs, mirroring the +# `std` feature of `bouncycastle-core`. On by default; a future `--no-default-features` build is +# what will let the crate move toward `#![no_std]`. +default = ["std"] +std = ["bouncycastle-core/std"] + +[dependencies] +bouncycastle-core.workspace = true +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..eebe3f17 --- /dev/null +++ b/crypto/ascon/benches/ascon_benches.rs @@ -0,0 +1,93 @@ +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 -- ::hash_xof_out()"), + |b| { + b.iter(|| { + AsconXof128::new().hash_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 -- ::hash_xof_out()"), + |b| { + b.iter(|| { + AsconCXof128::with_customization(customization) + .unwrap() + .hash_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..1771461d --- /dev/null +++ b/crypto/ascon/src/ascon_aead128.rs @@ -0,0 +1,724 @@ +//! 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. + +use core::fmt::{self, Debug, Display, Formatter}; + +use bouncycastle_core::errors::{KeyMaterialError, SuspendableError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{ + AEADCipher, Algorithm, RNG, SecurityStrength, SuspendableKeyed, SymmetricCipher, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use bouncycastle_utils::ct::ct_eq_bytes; +use bouncycastle_utils::secret::Secret; + +use crate::permutation::{AsconState, load_u64_le, p8, p12, store_u64_le}; + +/// Length in bytes of the Ascon-AEAD128 key. +pub const KEY_LEN: usize = 16; +/// Length in bytes of the Ascon-AEAD128 nonce. +pub const NONCE_LEN: usize = 16; +/// Length in bytes of the Ascon-AEAD128 authentication tag. +pub const TAG_LEN: usize = 16; +const RATE: usize = 16; + +/// Ascon-AEAD128 initial value (SP 800-232 Table 14). +const ASCON_IV: u64 = 0x00001000808C0001; + +/// State machine for enforcing the call order and remembering the direction (encrypt/decrypt). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum StateMachine { + EncInit, + EncAad, + EncData, + DecInit, + DecAad, + DecData, +} + +impl StateMachine { + // Stable u8 encoding used when suspending/resuming the AEAD state machine. + fn to_u8(self) -> u8 { + match self { + StateMachine::EncInit => 0, + StateMachine::EncAad => 1, + StateMachine::EncData => 2, + StateMachine::DecInit => 4, + StateMachine::DecAad => 5, + StateMachine::DecData => 6, + } + } + + fn from_u8(v: u8) -> Option { + Some(match v { + 0 => StateMachine::EncInit, + 1 => StateMachine::EncAad, + 2 => StateMachine::EncData, + 4 => StateMachine::DecInit, + 5 => StateMachine::DecAad, + 6 => StateMachine::DecData, + _ => return None, + }) + } + + fn is_encrypt(self) -> bool { + matches!(self, StateMachine::EncInit | StateMachine::EncAad | StateMachine::EncData) + } + + fn is_init(self) -> bool { + matches!(self, StateMachine::EncInit | StateMachine::DecInit) + } +} + +/// An implementation of the Ascon-AEAD128 algorithm (NIST SP 800-232). +/// +/// A single instance performs one operation (encryption or decryption) under one (key, nonce) pair. +/// See [`AsconAead128::new`] 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`]). + 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) + } + + /// Create a new streaming instance. + /// * `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. + /// * `ad` is optional associated data (authenticated, not encrypted); processed immediately. + /// * `for_encryption` is true for encryption, false for decryption. + pub 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 { + aead.do_update_aad(ad_bytes); + } + 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::IncorrectOutputBufferLength( + "Ascon-AEAD128 output buffer too small (need plaintext length + 16)", + 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::IncorrectOutputBufferLength( + "Ascon-AEAD128 output buffer too small", + 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) { + 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 => { + panic!( + "Ascon-AEAD128: associated data must be processed before plaintext/ciphertext" + ) + } + } + } + + // 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. + pub fn do_update_aad(&mut self, input: &[u8]) { + if input.is_empty() { + return; + } + 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..]; + } + } + } + + /// Encrypt `data` in place (SP 800-232 §4.1.1 step 3). Every byte is transformed and emitted + /// immediately; nothing is buffered across calls. + pub fn do_encrypt_update(&mut self, data: &mut [u8]) { + if !self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_encrypt_update called on a decryptor"); + } + self.check_data(); + + let mut data = data; + while !data.is_empty() { + if self.pos == 0 && data.len() >= RATE { + let c0 = self.state[0] ^ load_u64_le(data, 0); + let c1 = self.state[1] ^ load_u64_le(data, 8); + store_u64_le(data, 0, c0); + store_u64_le(data, 8, c1); + self.state[0] = c0; + self.state[1] = c1; + p8(&mut self.state); + data = &mut data[RATE..]; + } else { + data[0] = self.encrypt_byte(data[0]); + data = &mut data[1..]; + } + } + } + + /// Finish encryption; returns the 128-bit tag (SP 800-232 §4.1.1 steps 3-4). Pads the final + /// (possibly empty) plaintext block; no further bytes are emitted here since every + /// plaintext/ciphertext byte was already written by `do_encrypt_update`. + pub fn do_encrypt_final(mut self) -> [u8; TAG_LEN] { + if !self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_encrypt_final called on a decryptor"); + } + self.check_data(); + // Padding of the final (possibly empty) plaintext block (Eq. 27). + self.xor_state_byte(self.pos, 0x01); + self.finish_data() + } + + /// Decrypt `data` in place (SP 800-232 §4.1.2 step 3). Every byte is transformed and emitted + /// immediately; the plaintext is **not** authenticated until [`AsconAead128::do_decrypt_final`] + /// returns `Ok`. + pub fn do_decrypt_update(&mut self, data: &mut [u8]) { + if self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_decrypt_update called on an encryptor"); + } + self.check_data(); + + let mut data = data; + while !data.is_empty() { + if self.pos == 0 && data.len() >= RATE { + let t0 = load_u64_le(data, 0); + let t1 = load_u64_le(data, 8); + store_u64_le(data, 0, self.state[0] ^ t0); + store_u64_le(data, 8, self.state[1] ^ t1); + self.state[0] = t0; + self.state[1] = t1; + p8(&mut self.state); + data = &mut data[RATE..]; + } else { + data[0] = self.decrypt_byte(data[0]); + data = &mut data[1..]; + } + } + } + + /// Finish decryption, checking `tag` in constant time (SP 800-232 §4.1.2 steps 3-4). + pub fn do_decrypt_final(mut self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + if self.state_machine.is_encrypt() { + panic!("Ascon-AEAD128: do_decrypt_final called on an encryptor"); + } + self.check_data(); + // Padding of the final (possibly empty) ciphertext block (Eq. 47). + self.xor_state_byte(self.pos, 0x01); + let computed = self.finish_data(); + + if !ct_eq_bytes(&computed, tag) { + return Err(SymmetricCipherError::AEADTagCheckFailed); + } + Ok(()) + } +} + +impl Algorithm for AsconAead128 { + const ALG_NAME: &'static str = "Ascon-AEAD128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +// Ascon-AEAD128 as a `SymmetricCipher`: the "basic" (non-AEAD) view. The init data is the 128-bit +// nonce, and the ciphertext produced by these APIs is `Ascon ciphertext || 16-byte tag` (empty AAD). +impl SymmetricCipher for AsconAead128 { + #[cfg(feature = "std")] + fn encrypt( + key: &KeyMaterial, + plaintext: &[u8], + ) -> Result<([u8; NONCE_LEN], Vec), SymmetricCipherError> { + let mut ciphertext = vec![0u8; plaintext.len() + TAG_LEN]; + let (nonce, written) = Self::encrypt_out(key, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((nonce, ciphertext)) + } + + fn encrypt_out( + key: &KeyMaterial, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize), SymmetricCipherError> { + let _ = Self::checked_key(key)?; + let nonce = Self::fresh_nonce()?; + // No associated data for the plain SymmetricCipher view; the tag is appended to + // `ciphertext`. `encrypt` itself checks that `ciphertext` is long enough. + let written = Self::encrypt(key, &nonce, None, plaintext, ciphertext)?; + Ok((nonce, written)) + } + + #[cfg(feature = "std")] + fn decrypt( + key: &KeyMaterial, + init_data: [u8; NONCE_LEN], + ciphertext: &[u8], + ) -> Result, SymmetricCipherError> { + if ciphertext.len() < TAG_LEN { + return Err(SymmetricCipherError::GenericError( + "Ascon-AEAD128 ciphertext shorter than tag", + )); + } + let mut plaintext = vec![0u8; ciphertext.len() - TAG_LEN]; + let written = Self::decrypt_out(key, init_data, ciphertext, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } + + fn decrypt_out( + key: &KeyMaterial, + init_data: [u8; NONCE_LEN], + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let _ = Self::checked_key(key)?; + if ciphertext.len() < TAG_LEN { + return Err(SymmetricCipherError::GenericError( + "Ascon-AEAD128 ciphertext shorter than tag", + )); + } + let pt_len = ciphertext.len() - TAG_LEN; + if plaintext.len() < pt_len { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "Ascon-AEAD128 plaintext buffer too small", + pt_len, + )); + } + // `ciphertext` is `Ascon ciphertext || 16-byte tag`; `decrypt` splits it internally. + Self::decrypt(key, &init_data, None, ciphertext, plaintext) + } +} + +// Ascon-AEAD128 as an `AEADCipher`: the full AEAD view with associated data and a separate tag. +impl AEADCipher for AsconAead128 { + #[cfg(feature = "std")] + fn aead_encrypt( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ) -> Result<([u8; NONCE_LEN], Vec, [u8; TAG_LEN]), SymmetricCipherError> { + let mut ciphertext = vec![0u8; plaintext.len()]; + let (nonce, written, tag) = Self::aead_encrypt_out(key, aad, plaintext, &mut ciphertext)?; + ciphertext.truncate(written); + Ok((nonce, ciphertext, tag)) + } + + fn aead_encrypt_out( + key: &KeyMaterial, + aad: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; NONCE_LEN], usize, [u8; TAG_LEN]), SymmetricCipherError> { + let _ = Self::checked_key(key)?; + if ciphertext.len() < plaintext.len() { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "Ascon-AEAD128 ciphertext buffer too small", + plaintext.len(), + )); + } + let nonce = Self::fresh_nonce()?; + let aad_opt = if aad.is_empty() { None } else { Some(aad) }; + let mut cipher = Self::new(key, &nonce, aad_opt, true)?; + ciphertext[..plaintext.len()].copy_from_slice(plaintext); + cipher.do_encrypt_update(&mut ciphertext[..plaintext.len()]); + let tag = cipher.do_encrypt_final(); + Ok((nonce, plaintext.len(), tag)) + } + + fn do_aead_encrypt_final(self) -> Result<[u8; TAG_LEN], SymmetricCipherError> { + Ok(self.do_encrypt_final()) + } + + #[cfg(feature = "std")] + fn aead_decrypt( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + ) -> Result, SymmetricCipherError> { + let mut plaintext = vec![0u8; ciphertext.len()]; + let written = Self::aead_decrypt_out(key, nonce, aad, ciphertext, tag, &mut plaintext)?; + plaintext.truncate(written); + Ok(plaintext) + } + + fn aead_decrypt_out( + key: &KeyMaterial, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8; TAG_LEN], + plaintext: &mut [u8], + ) -> Result { + let _ = Self::checked_key(key)?; + if plaintext.len() < ciphertext.len() { + return Err(SymmetricCipherError::IncorrectOutputBufferLength( + "Ascon-AEAD128 plaintext buffer too small", + ciphertext.len(), + )); + } + let aad_opt = if aad.is_empty() { None } else { Some(aad) }; + let mut cipher = Self::new(key, nonce, aad_opt, false)?; + plaintext[..ciphertext.len()].copy_from_slice(ciphertext); + cipher.do_decrypt_update(&mut plaintext[..ciphertext.len()]); + match cipher.do_decrypt_final(tag) { + Ok(()) => Ok(ciphertext.len()), + Err(e) => { + // A failed tag check must not leave plaintext in the caller's buffer. + plaintext[..ciphertext.len()].fill(0); + Err(e) + } + } + } + + fn do_aead_decrypt_final(self, tag: &[u8; TAG_LEN]) -> Result<(), SymmetricCipherError> { + self.do_decrypt_final(tag) + } +} + +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..4a0b055f --- /dev/null +++ b/crypto/ascon/src/ascon_cxof128.rs @@ -0,0 +1,218 @@ +//! 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]`). + +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{Algorithm, SecurityStrength, Suspendable, XOF}; +use bouncycastle_utils::secret::Secret; + +use crate::sponge::{RATE, Sponge}; + +/// Maximum customization-string length in bytes (2048 bits, per SP 800-232 §5.3). +const MAX_CUSTOMIZATION_BYTES: usize = 256; + +/// 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 }) + } + + // Squeeze `output.len()` bytes of output. May be called multiple times; the first call ends the + // absorb phase by padding and absorbing the final block. Returns the number of bytes written. + fn squeeze_into(&mut self, output: &mut [u8]) -> usize { + let written = output.len(); + if !self.sponge.squeezing() { + self.sponge.pad_and_absorb(); + } + self.sponge.squeeze(output); + written + } +} + +impl Default for AsconCXof128 { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm for AsconCXof128 { + const ALG_NAME: &'static str = "Ascon-CXOF128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl XOF for AsconCXof128 { + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { + self.sponge.absorb(data); + let mut out = vec![0u8; result_len]; + self.squeeze_into(&mut out); + out + } + + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.sponge.absorb(data); + self.squeeze_into(output) + } + + fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { + if self.sponge.squeezing() { + return Err(HashError::InvalidState( + "Ascon-CXOF128 cannot absorb after squeezing has begun", + )); + } + self.sponge.absorb(data); + Ok(()) + } + + fn absorb_last_partial_byte( + &mut self, + _partial_byte: u8, + _num_partial_bits: usize, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte input")) + } + + fn squeeze(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.squeeze_into(&mut out); + out + } + + fn squeeze_out(&mut self, output: &mut [u8]) -> usize { + self.squeeze_into(output) + } + + fn squeeze_partial_byte_final(self, _num_bits: usize) -> Result { + Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte output")) + } + + fn squeeze_partial_byte_final_out( + self, + _num_bits: usize, + _output: &mut u8, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-CXOF128 does not support partial byte output")) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} + +/// Length in bytes of the serialized state of [`AsconCXof128`]. +/// Layout: 3-byte library version || 1-byte state tag || 40-byte sponge state (5 × u64 LE) +/// || 8-byte rate buffer || 1-byte buffer position || 1-byte squeezing flag. +/// +/// Note: the customization string is absorbed at construction time and is not part of the +/// suspended state; resuming continues the message-absorb / squeeze phase already in progress. +pub const SUSPENDED_ASCON_CXOF128_STATE_LEN: usize = 54; + +// Distinguishes an Ascon-CXOF128 serialized state from the other (same-shaped) Ascon sponge states. +const CXOF128_STATE_TAG: u8 = 0x03; + +impl Suspendable for AsconCXof128 { + fn suspend(self) -> [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_CXOF128_STATE_LEN]; + // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_CXOF128_STATE_LEN - 3 = 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 = 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()); + debug_assert!(self.sponge.buf_pos() <= RATE); + out[49] = self.sponge.buf_pos() as u8; + out[50] = self.sponge.squeezing() as u8; + + out_to_return + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_CXOF128_STATE_LEN], + ) -> Result { + // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_CXOF128_STATE_LEN - 3 = 51 bytes. + 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 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; + let squeezing = match input[50] { + 0 => false, + 1 => true, + _ => return Err(SuspendableError::InvalidData), + }; + // While absorbing, buf_pos must be < RATE (a full buffer is drained immediately); once + // squeezing, buf_pos may equal RATE (meaning "no leftover squeezed byte buffered"). + let valid_pos = if squeezing { buf_pos <= RATE } else { buf_pos < RATE }; + if !valid_pos { + return Err(SuspendableError::InvalidData); + } + + Ok(AsconCXof128 { sponge: Sponge::from_parts(s, buf, buf_pos, squeezing) }) + } +} diff --git a/crypto/ascon/src/ascon_hash256.rs b/crypto/ascon/src/ascon_hash256.rs new file mode 100644 index 00000000..9d2b87d5 --- /dev/null +++ b/crypto/ascon/src/ascon_hash256.rs @@ -0,0 +1,185 @@ +//! 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::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{Algorithm, Hash, HashAlgParams, SecurityStrength, Suspendable}; +use bouncycastle_utils::secret::Secret; + +use crate::sponge::{RATE, Sponge}; + +const DIGEST_BYTES: usize = 32; + +/// Ascon-Hash256 hash function (NIST SP 800-232 §5.1), producing a 256-bit digest. +#[derive(Clone)] +pub struct AsconHash256 { + sponge: Sponge, +} + +impl AsconHash256 { + /// Creates a new AsconHash256 instance. + pub fn new() -> Self { + // Precomputed state after the initialization permutation (SP 800-232 Table 12). + Self { + sponge: Sponge::from_state([ + 0x9B1E_5494_E934_D681, 0x4BC3_A01E_3337_51D2, 0xAE65_396C_6B34_B81A, + 0x3C7F_D4A4_D56A_4DB3, 0x1A5C_4649_06C5_976D, + ]), + } + } + + /// One-shot hash of `data`, returning the 32-byte digest. + pub fn digest(data: &[u8]) -> [u8; DIGEST_BYTES] { + let mut hasher = Self::new(); + hasher.sponge.absorb(data); + let mut out = [0u8; DIGEST_BYTES]; + hasher.squeeze_into(&mut out); + out + } + + // Pad, absorb the final block, and squeeze the four 64-bit digest blocks (SP 800-232 + // Algorithm 5). The 32-byte digest is exactly RATE * 4 bytes, so a single generic + // `Sponge::squeeze()` call over the whole output produces all four blocks with no leftover. + fn squeeze_into(&mut self, output: &mut [u8; DIGEST_BYTES]) { + self.sponge.pad_and_absorb(); + self.sponge.squeeze(output); + } +} + +impl Default for AsconHash256 { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm for AsconHash256 { + const ALG_NAME: &'static str = "Ascon-Hash256"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl HashAlgParams for AsconHash256 { + const OUTPUT_LEN: usize = DIGEST_BYTES; + const BLOCK_LEN: usize = RATE; +} + +impl Hash for AsconHash256 { + fn block_bitlen(&self) -> usize { + RATE * 8 + } + + fn output_len(&self) -> usize { + DIGEST_BYTES + } + + fn hash(mut self, data: &[u8]) -> Vec { + self.sponge.absorb(data); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + out.to_vec() + } + + fn hash_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.sponge.absorb(data); + output.fill(0); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + let n = core::cmp::min(output.len(), DIGEST_BYTES); + output[..n].copy_from_slice(&out[..n]); + n + } + + fn do_update(&mut self, data: &[u8]) { + self.sponge.absorb(data); + } + + fn do_final(mut self) -> Vec { + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + out.to_vec() + } + + fn do_final_out(mut self, output: &mut [u8]) -> usize { + output.fill(0); + let mut out = [0u8; DIGEST_BYTES]; + self.squeeze_into(&mut out); + let n = core::cmp::min(output.len(), DIGEST_BYTES); + output[..n].copy_from_slice(&out[..n]); + n + } + + fn do_final_partial_bits( + self, + _partial_byte: u8, + _num_partial_bits: usize, + ) -> Result, HashError> { + Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + } + + fn do_final_partial_bits_out( + self, + _partial_byte: u8, + _num_partial_bits: usize, + _output: &mut [u8], + ) -> Result { + Err(HashError::InvalidInput("Ascon-Hash256 does not support partial byte input")) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} + +/// Length in bytes of the serialized state of [`AsconHash256`]. +/// Layout: 3-byte library version || 1-byte state tag || 40-byte sponge state (5 × u64 LE) +/// || 8-byte rate buffer || 1-byte buffer position. +pub const SUSPENDED_ASCON_HASH256_STATE_LEN: usize = 53; + +// Distinguishes an Ascon-Hash256 serialized state from the other (same-shaped) Ascon sponge states. +const HASH256_STATE_TAG: u8 = 0x01; + +impl Suspendable for AsconHash256 { + fn suspend(self) -> [u8; SUSPENDED_ASCON_HASH256_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_HASH256_STATE_LEN]; + // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_HASH256_STATE_LEN - 3 = 50 bytes. + let out: &mut [u8; SUSPENDED_ASCON_HASH256_STATE_LEN - 3] = + add_lib_ver(&mut out_to_return).try_into().unwrap(); + + out[0] = HASH256_STATE_TAG; + let state = self.sponge.state_words(); + for i in 0..5 { + out[1 + i * 8..1 + i * 8 + 8].copy_from_slice(&state[i].to_le_bytes()); + } + out[41..49].copy_from_slice(&self.sponge.buf_bytes()); + // buf_pos is always < RATE (8) before squeezing has begun, so it fits in one byte. + debug_assert!(self.sponge.buf_pos() < RATE); + out[49] = self.sponge.buf_pos() as u8; + + out_to_return + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_HASH256_STATE_LEN], + ) -> Result { + // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_HASH256_STATE_LEN - 3 = 50 bytes. + let input: &[u8; SUSPENDED_ASCON_HASH256_STATE_LEN - 3] = + check_lib_ver(&serialized_state, None)?.try_into().unwrap(); + + if input[0] != HASH256_STATE_TAG { + return Err(SuspendableError::InvalidData); + } + let mut s = Secret::<[u64; 5]>::new(); + for i in 0..5 { + // infallible: each slice is exactly 8 bytes (1+i*8..1+i*8+8) by construction. + s[i] = u64::from_le_bytes(input[1 + i * 8..1 + i * 8 + 8].try_into().unwrap()); + } + let mut buf = Secret::<[u8; RATE]>::new(); + buf.copy_from_slice(&input[41..49]); + let buf_pos = input[49] as usize; + if buf_pos >= RATE { + return Err(SuspendableError::InvalidData); + } + + Ok(AsconHash256 { sponge: Sponge::from_parts(s, buf, buf_pos, false) }) + } +} diff --git a/crypto/ascon/src/ascon_xof128.rs b/crypto/ascon/src/ascon_xof128.rs new file mode 100644 index 00000000..0b6e8a8f --- /dev/null +++ b/crypto/ascon/src/ascon_xof128.rs @@ -0,0 +1,172 @@ +//! Ascon-XOF128 extendable-output function (NIST SP 800-232 §5.2). +//! +//! Sponge mode over `Ascon-p[12]` with rate = 64 bits, capacity = 256 bits. Supports the streaming +//! absorb/squeeze API of SP 800-232 §5.4 (squeeze may be called repeatedly). + +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{Algorithm, SecurityStrength, Suspendable, XOF}; +use bouncycastle_utils::secret::Secret; + +use crate::sponge::{RATE, Sponge}; + +/// 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, + ]), + } + } + + // Squeeze `output.len()` bytes of output. May be called multiple times; the first call ends the + // absorb phase by padding and absorbing the final block. Returns the number of bytes written. + fn squeeze_into(&mut self, output: &mut [u8]) -> usize { + let written = output.len(); + if !self.sponge.squeezing() { + self.sponge.pad_and_absorb(); + } + self.sponge.squeeze(output); + written + } +} + +impl Default for AsconXof128 { + fn default() -> Self { + Self::new() + } +} + +impl Algorithm for AsconXof128 { + const ALG_NAME: &'static str = "Ascon-XOF128"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl XOF for AsconXof128 { + fn hash_xof(mut self, data: &[u8], result_len: usize) -> Vec { + self.sponge.absorb(data); + let mut out = vec![0u8; result_len]; + self.squeeze_into(&mut out); + out + } + + fn hash_xof_out(mut self, data: &[u8], output: &mut [u8]) -> usize { + self.sponge.absorb(data); + self.squeeze_into(output) + } + + fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> { + if self.sponge.squeezing() { + return Err(HashError::InvalidState( + "Ascon-XOF128 cannot absorb after squeezing has begun", + )); + } + self.sponge.absorb(data); + Ok(()) + } + + fn absorb_last_partial_byte( + &mut self, + _partial_byte: u8, + _num_partial_bits: usize, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte input")) + } + + fn squeeze(&mut self, num_bytes: usize) -> Vec { + let mut out = vec![0u8; num_bytes]; + self.squeeze_into(&mut out); + out + } + + fn squeeze_out(&mut self, output: &mut [u8]) -> usize { + self.squeeze_into(output) + } + + fn squeeze_partial_byte_final(self, _num_bits: usize) -> Result { + Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte output")) + } + + fn squeeze_partial_byte_final_out( + self, + _num_bits: usize, + _output: &mut u8, + ) -> Result<(), HashError> { + Err(HashError::InvalidInput("Ascon-XOF128 does not support partial byte output")) + } + + fn max_security_strength(&self) -> SecurityStrength { + SecurityStrength::_128bit + } +} + +/// Length in bytes of the serialized state of [`AsconXof128`]. +/// Layout: 3-byte library version || 1-byte state tag || 40-byte sponge state (5 × u64 LE) +/// || 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 the other (same-shaped) Ascon sponge states. +const XOF128_STATE_TAG: u8 = 0x02; + +impl Suspendable for AsconXof128 { + fn suspend(self) -> [u8; SUSPENDED_ASCON_XOF128_STATE_LEN] { + let mut out_to_return = [0u8; SUSPENDED_ASCON_XOF128_STATE_LEN]; + // infallible: add_lib_ver returns a slice of exactly SUSPENDED_ASCON_XOF128_STATE_LEN - 3 = 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 = 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()); + debug_assert!(self.sponge.buf_pos() <= RATE); + out[49] = self.sponge.buf_pos() as u8; + out[50] = self.sponge.squeezing() as u8; + + out_to_return + } + + fn from_suspended( + serialized_state: [u8; SUSPENDED_ASCON_XOF128_STATE_LEN], + ) -> Result { + // infallible: check_lib_ver returns a slice of exactly SUSPENDED_ASCON_XOF128_STATE_LEN - 3 = 51 bytes. + 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 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; + let squeezing = match input[50] { + 0 => false, + 1 => true, + _ => return Err(SuspendableError::InvalidData), + }; + // While absorbing, buf_pos must be < RATE (a full buffer is drained immediately); once + // squeezing, buf_pos may equal RATE (meaning "no leftover squeezed byte buffered"). + let valid_pos = if squeezing { buf_pos <= RATE } else { buf_pos < RATE }; + if !valid_pos { + return Err(SuspendableError::InvalidData); + } + + Ok(AsconXof128 { sponge: Sponge::from_parts(s, buf, buf_pos, squeezing) }) + } +} diff --git a/crypto/ascon/src/lib.rs b/crypto/ascon/src/lib.rs new file mode 100644 index 00000000..71a7e2da --- /dev/null +++ b/crypto/ascon/src/lib.rs @@ -0,0 +1,133 @@ +//! 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; +//! +//! // One-shot: +//! let digest = AsconHash256::digest(b"hello world"); +//! assert_eq!(digest.len(), 32); +//! +//! // Streaming: +//! let mut h = AsconHash256::new(); +//! h.do_update(b"hello "); +//! h.do_update(b"world"); +//! let mut out = [0u8; 32]; +//! h.do_final_out(&mut out); +//! assert_eq!(out, digest); +//! ``` +//! +//! Authenticated encryption (one-shot): +//! ``` +//! use bouncycastle_ascon::ascon_aead128::AsconAead128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42u8; 16], KeyType::SymmetricCipherKey).unwrap(); +//! let nonce = [1u8; 16]; // MUST be unique per encryption under a given key +//! let ad = b"associated data"; +//! let plaintext = b"secret message"; +//! +//! let mut ct = vec![0u8; plaintext.len() + 16]; // ciphertext || 16-byte tag +//! let n = AsconAead128::encrypt(&key, &nonce, Some(ad), plaintext, &mut ct).unwrap(); +//! ct.truncate(n); +//! +//! let mut pt = vec![0u8; ct.len() - 16]; +//! let m = AsconAead128::decrypt(&key, &nonce, Some(ad), &ct, &mut pt).unwrap(); +//! pt.truncate(m); +//! assert_eq!(&pt, plaintext); +//! ``` +//! +//! Authenticated encryption (streaming, in place): +//! ``` +//! 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]; +//! +//! let mut buf = *b"secret message!!"; // transformed in place +//! let mut enc = AsconAead128::new(&key, &nonce, Some(b"associated data"), true).unwrap(); +//! enc.do_encrypt_update(&mut buf); // now ciphertext +//! let tag = enc.do_encrypt_final(); +//! +//! let mut dec = AsconAead128::new(&key, &nonce, Some(b"associated data"), false).unwrap(); +//! dec.do_decrypt_update(&mut buf); // now plaintext again, but not yet authenticated +//! dec.do_decrypt_final(&tag).unwrap(); // now authenticated +//! assert_eq!(&buf, b"secret message!!"); +//! ``` +//! +//! Extendable output: +//! ``` +//! use bouncycastle_ascon::ascon_xof128::AsconXof128; +//! use bouncycastle_core::traits::XOF; +//! +//! let out = AsconXof128::new().hash_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. +//! - **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`] and the +//! `SymmetricCipher`/`AEADCipher` trait impls) zeroize their output buffer before returning that +//! error. The streaming API ([`ascon_aead128::AsconAead128::do_decrypt_update`] / +//! [`ascon_aead128::AsconAead128::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 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..1529e98e --- /dev/null +++ b/crypto/ascon/tests/aead128_tests.rs @@ -0,0 +1,622 @@ +//! Ascon-AEAD128 tests (NIST SP 800-232). +//! +//! - A small embedded set of NIST LWC known-answer vectors (always-on correctness, no external +//! repo required). The full sweep lives in `bc_test_data.rs`. +//! - Behavioral / contract tests (round-trips, streaming chunk-boundary equivalence, authentication +//! failures, determinism), driven through the inherent explicit-nonce API. +//! - The shared `AEADCipher` conformance framework (`core-test-framework`), which exercises the +//! generic `SymmetricCipher` / `AEADCipher` trait surface with internally-generated nonces. + +use bouncycastle_ascon::ascon_aead128::AsconAead128; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::traits::SecurityStrength; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkAEADCipher; +use bouncycastle_hex as hex; + +// All embedded vectors use this fixed key/nonce (the NIST LWC KAT convention). +const KEY: [u8; 16] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, +]; +const NONCE: [u8; 16] = [ + 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, +]; + +const PT_SIZES: [usize; 10] = [0, 1, 15, 16, 17, 31, 32, 33, 64, 100]; +const CHUNK_SIZES: [usize; 6] = [1, 3, 7, 13, 16, 17]; + +/// Embedded NIST LWC Ascon-AEAD128 vectors `(plaintext, associated_data, ciphertext||tag)` in hex. +/// Key = Nonce = 000102…0F. Spans empty input, AD-only (incl. a full 32-byte AD block), partial PT +/// with AD, and a multi-block plaintext. (Counts 1, 2, 5, 33, 68, 69, 153, 1057 of +/// LWC_AEAD_KAT_128_128.txt.) +const AEAD_KAT: &[(&str, &str, &str)] = &[ + ("", "", "4427D64B8E1E1451FC445960F0839BB0"), + ("", "00", "103AB79D913A0321287715A979BB8585"), + ("", "00010203", "C6FF3CF70575B144B955820D9BC7685E"), + ( + "", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "22133A313FBF0B38029A45870AADC542", + ), + ("0001", "00", "25FB41D2732019820A0F8BAB4248B35E7B0B"), + ("0001", "0001", "49E57017A30E8073D1FA284AC8346110F89F"), + ( + "00010203", + "000102030405060708090A0B0C0D0E0F10111213", + "C305EB0E9A9A7833C5F6FB36BD82F1C78C322678", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "", + "E770D289D2A44AEE7CD0A48ECE5274E381BAD7E163DCC4970F7873610DEBBEB1A28657F6E82FE53D08B09EFF9330BD2B", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn ad_opt(ad: &[u8]) -> Option<&[u8]> { + if ad.is_empty() { None } else { Some(ad) } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +/// Build a `KeyMaterial<16>` suitable for `AsconAead128`. The NIST LWC KAT vectors include an +/// all-zero key (Count=1), which `KeyMaterial::from_bytes_as_type` would otherwise tag +/// `KeyType::Zeroized` / `SecurityStrength::None`; force the type/strength the way a caller who +/// knows the provenance of the key would (see `cli/src/helpers.rs::parse_seed`). +fn key_material(key: &[u8; 16]) -> KeyMaterial<16> { + let mut km = KeyMaterial::<16>::from_bytes_as_type(key, KeyType::SymmetricCipherKey).unwrap(); + do_hazardous_operations(&mut km, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::_128bit) + }) + .unwrap(); + km +} + +fn enc_oneshot(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8]) -> Vec { + let km = key_material(key); + let mut out = vec![0u8; pt.len() + 16]; + let n = AsconAead128::encrypt(&km, nonce, ad_opt(ad), pt, &mut out).unwrap(); + out.truncate(n); + out +} + +fn dec_oneshot( + key: &[u8; 16], + nonce: &[u8; 16], + ad: &[u8], + ct: &[u8], +) -> Result, SymmetricCipherError> { + let km = key_material(key); + let mut out = vec![0u8; ct.len()]; + let n = AsconAead128::decrypt(&km, nonce, ad_opt(ad), ct, &mut out)?; + out.truncate(n); + Ok(out) +} + +fn enc_chunked(key: &[u8; 16], nonce: &[u8; 16], ad: &[u8], pt: &[u8], chunk: usize) -> Vec { + let km = key_material(key); + let mut cipher = AsconAead128::new(&km, nonce, ad_opt(ad), true).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(&km, nonce, ad_opt(ad), false).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(&km, &NONCE, None, true).unwrap(); + for piece in ad.chunks(chunk) { + e.do_update_aad(piece); + } + 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})"); + } +} + +/* -------------------------------------------------------------------------- */ +/* Trait-driven streaming sweep (this is what would have caught F1/F2) */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead_trait_streaming_sweep() { + use bouncycastle_core::traits::AEADCipher; + + 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(&km, &NONCE, ad_opt_, true).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_aead_encrypt_final().unwrap(); + 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(&km, &NONCE, ad_opt_, false).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_aead_decrypt_final(&tag_arr).unwrap(); + assert_eq!(back, pt, "pt_len={pt_len} ad_len={ad_len} chunk={chunk}"); + } + } + } +} + +#[test] +fn do_aead_decrypt_final_rejects_wrong_tag() { + use bouncycastle_core::traits::AEADCipher; + + let km = key_material(&KEY); + let pt = pattern(20); + let mut d = AsconAead128::new(&km, &NONCE, None, false).unwrap(); + let mut buf = pt.clone(); + d.do_decrypt_update(&mut buf); + let wrong_tag = [0xFFu8; 16]; + assert!(matches!( + d.do_aead_decrypt_final(&wrong_tag), + Err(SymmetricCipherError::AEADTagCheckFailed) + )); +} + +/* -------------------------------------------------------------------------- */ +/* std-only Vec-returning trait wrappers */ +/* -------------------------------------------------------------------------- */ + +// `TestFrameworkSymmetricCipher`/`TestFrameworkAEADCipher` only exercise the `_out` (buffer-based) +// entry points, so the `#[cfg(feature = "std")]` `Vec`-returning wrappers (`encrypt`, `decrypt`, +// `aead_encrypt`, `aead_decrypt`) are otherwise never called by any test. +#[test] +fn aead128_std_vec_wrappers_round_trip() { + use bouncycastle_core::traits::{AEADCipher, SymmetricCipher}; + + let km = key_material(&KEY); + let msg = pattern(40); + + let (nonce, ct) = >::encrypt(&km, &msg).unwrap(); + assert_eq!(ct.len(), msg.len() + 16); + let pt = >::decrypt(&km, nonce, &ct).unwrap(); + assert_eq!(pt, msg); + + let (nonce, ct, tag) = + >::aead_encrypt(&km, b"aad", &msg).unwrap(); + assert_eq!(ct.len(), msg.len()); + let pt = >::aead_decrypt(&km, &nonce, b"aad", &ct, &tag) + .unwrap(); + assert_eq!(pt, msg); + + // Tampering must still be rejected through these entry points too. + assert!( + >::aead_decrypt( + &km, &nonce, b"wrong-aad", &ct, &tag + ) + .is_err() + ); +} + +// None of the length checks in the `SymmetricCipher`/`AEADCipher` `_out` entry points are ever +// triggered by `TestFrameworkSymmetricCipher`/`TestFrameworkAEADCipher` (which always pass a +// generously-sized fixed buffer), nor by the inherent one-shot `encrypt`/`decrypt` tests above +// (which always size their own buffer correctly). Exercise every one directly. +#[test] +fn aead128_undersized_buffers_are_rejected() { + use bouncycastle_core::traits::{AEADCipher, SymmetricCipher}; + + let km = key_material(&KEY); + let msg = pattern(40); + + // SymmetricCipher::encrypt_out: ciphertext buffer shorter than plaintext.len() + 16. + let mut too_small = vec![0u8; msg.len() + 15]; + match >::encrypt_out(&km, &msg, &mut too_small) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, needed)) => { + assert_eq!(needed, msg.len() + 16); + } + other => panic!("expected IncorrectOutputBufferLength, got {other:?}"), + } + + // SymmetricCipher::decrypt / decrypt_out: ciphertext shorter than the 16-byte tag. + let short = [0u8; 8]; + match >::decrypt(&km, NONCE, &short) { + Err(SymmetricCipherError::GenericError(_)) => {} + other => panic!("expected GenericError, got {other:?}"), + } + let mut pt_buf = [0u8; 8]; + match >::decrypt_out(&km, NONCE, &short, &mut pt_buf) { + Err(SymmetricCipherError::GenericError(_)) => {} + other => panic!("expected GenericError, got {other:?}"), + } + + // SymmetricCipher::decrypt_out: valid-length ciphertext, but undersized plaintext buffer. + let ct = enc_oneshot(&KEY, &NONCE, &[], &msg); + let mut too_small_pt = vec![0u8; msg.len() - 1]; + match >::decrypt_out(&km, NONCE, &ct, &mut too_small_pt) + { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, needed)) => { + assert_eq!(needed, msg.len()); + } + other => panic!("expected IncorrectOutputBufferLength, got {other:?}"), + } + + // decrypt / decrypt_out: ciphertext of exactly 16 bytes (an empty plaintext plus the tag) is + // the boundary case and must NOT be rejected as "too short". + let empty_ct = enc_oneshot(&KEY, &NONCE, &[], &[]); + assert_eq!(empty_ct.len(), 16); + assert_eq!( + >::decrypt(&km, NONCE, &empty_ct).unwrap(), + Vec::::new() + ); + let mut empty_pt_buf = [0u8; 0]; + assert_eq!( + >::decrypt_out( + &km, NONCE, &empty_ct, &mut empty_pt_buf + ) + .unwrap(), + 0 + ); + + // decrypt_out: a plaintext buffer *larger* than needed must succeed, not be rejected. + let mut oversized_pt = vec![0xAAu8; msg.len() + 5]; + let n = + >::decrypt_out(&km, NONCE, &ct, &mut oversized_pt) + .unwrap(); + assert_eq!(n, msg.len()); + assert_eq!(&oversized_pt[..n], &msg[..]); + + // AEADCipher::aead_encrypt_out: ciphertext buffer shorter than the plaintext. + let mut too_small = vec![0u8; msg.len() - 1]; + match >::aead_encrypt_out( + &km, b"aad", &msg, &mut too_small, + ) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, needed)) => { + assert_eq!(needed, msg.len()); + } + other => panic!("expected IncorrectOutputBufferLength, got {other:?}"), + } + + // AEADCipher::aead_decrypt_out: plaintext buffer shorter than the ciphertext. + let (nonce, ct, tag) = + >::aead_encrypt(&km, b"aad", &msg).unwrap(); + let mut too_small_pt = vec![0u8; ct.len() - 1]; + match >::aead_decrypt_out( + &km, &nonce, b"aad", &ct, &tag, &mut too_small_pt, + ) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, needed)) => { + assert_eq!(needed, ct.len()); + } + other => panic!("expected IncorrectOutputBufferLength, got {other:?}"), + } +} + +/* -------------------------------------------------------------------------- */ +/* 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(&km, &NONCE, None, true).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(&km, &NONCE, None, false).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(&km, &NONCE, None, true).unwrap(); + let mut buf = [0u8; 4]; + e.do_decrypt_update(&mut buf); +} + +/* -------------------------------------------------------------------------- */ +/* AEADCipher trait conformance (shared core-test-framework) */ +/* -------------------------------------------------------------------------- */ + +#[test] +fn aead128_trait_framework() { + // Exercises the generic SymmetricCipher<16,16> + AEADCipher<16,16,16> surface: internally + // generated (random, distinct) nonces, key-type / key-strength enforcement, and the AEAD + // tamper-detection contract (modified ciphertext / AAD / tag must fail the tag check, and + // must never leave plaintext in the output buffer). + TestFrameworkAEADCipher::new().test::<16, 16, 16, AsconAead128>(); +} + +#[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(&km, &NONCE, Some(ad), true).unwrap(); + let mut out = vec![0u8; pt.len() + 16]; + out[..pt.len()].copy_from_slice(&pt); + e.do_encrypt_update(&mut out[..18]); + + TestFrameworkSuspendableKeyedState::new().test(&e, &km); + + let serialized = e.clone().suspend(); + let mut resumed = AsconAead128::from_suspended(serialized, &km).unwrap(); + resumed.do_encrypt_update(&mut out[18..pt.len()]); + let tag = resumed.do_encrypt_final(); + out[pt.len()..].copy_from_slice(&tag); + assert_eq!(out, ct_ref, "resumed AEAD ciphertext must match one-shot encryption"); + + // A corrupted state tag must be rejected (the tag is the byte after the 3-byte version prefix). + let mut busted = serialized; + busted[3] ^= 0xFF; + assert!(matches!( + AsconAead128::from_suspended(busted, &km), + Err(SuspendableError::InvalidData) + )); + + // An unknown call-state discriminant must be rejected. + let last = serialized.len() - 1; + let pos_offset = serialized.len() - 2; + let mut bad_state = serialized; + bad_state[last] = 200; + assert!(matches!( + AsconAead128::from_suspended(bad_state, &km), + Err(SuspendableError::InvalidData) + )); + + // A nonzero byte position while still in an *Init state must be rejected. + let mut inconsistent = serialized; + inconsistent[pos_offset] = 3; // pos = 3 + inconsistent[last] = 0; // EncInit + assert!(matches!( + AsconAead128::from_suspended(inconsistent, &km), + Err(SuspendableError::InvalidData) + )); + + // pos >= RATE (16) must be rejected. + let mut bad_pos = serialized; + bad_pos[pos_offset] = 16; + assert!(matches!( + AsconAead128::from_suspended(bad_pos, &km), + Err(SuspendableError::InvalidData) + )); +} diff --git a/crypto/ascon/tests/bc_test_data.rs b/crypto/ascon/tests/bc_test_data.rs new file mode 100644 index 00000000..01525a94 --- /dev/null +++ b/crypto/ascon/tests/bc_test_data.rs @@ -0,0 +1,242 @@ +//! Test against the bc-test-data repo. +//! Requires that the bc-test-data repository is cloned and available for testing at +//! "../bc-test-data" relative to the root of this git project (or "../../../bc-test-data" relative +//! to this crate). When the repo is absent these tests print a warning and are skipped. +//! +//! The NIST SP 800-232 ASCON known-answer test (KAT) vectors live under +//! `bc-test-data/crypto/ascon//`. These full sweeps (1025–1089 cases each) complement the +//! small embedded vector sets in the per-primitive test files. + +#[cfg(test)] +mod bc_test_data { + use bouncycastle_ascon::ascon_aead128::AsconAead128; + use bouncycastle_ascon::ascon_cxof128::AsconCXof128; + use bouncycastle_ascon::ascon_hash256::AsconHash256; + use bouncycastle_ascon::ascon_xof128::AsconXof128; + use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, + }; + use bouncycastle_core::traits::{SecurityStrength, XOF}; + use bouncycastle_hex as hex; + use std::collections::BTreeMap; + use std::fs; + use std::path::Path; + use std::sync::Once; + + const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto/ascon"; + const TEST_DATA_PATH: &str = "../bc-test-data/crypto/ascon"; + + static TEST_DATA_CHECK: Once = Once::new(); + + fn get_test_data(filename: &str) -> Result { + let found: u8; + if Path::new(TEST_DATA_PATH_RELATIVE).exists() { + found = 1; + } else if Path::new(TEST_DATA_PATH).exists() { + found = 2; + } else { + found = 3; + }; + + // just print once + TEST_DATA_CHECK.call_once(|| match found { + 1 => println!("bc-test-data found at: {:?}", TEST_DATA_PATH_RELATIVE), + 2 => println!("bc-test-data found at: {:?}", TEST_DATA_PATH), + _ => println!("WARNING: bc-test-data directory not found; tests will be skipped"), + }); + + let contents = if Path::new(TEST_DATA_PATH_RELATIVE).exists() { + fs::read_to_string(TEST_DATA_PATH_RELATIVE.to_string() + "/" + filename).unwrap() + } else if Path::new(TEST_DATA_PATH).exists() { + fs::read_to_string(TEST_DATA_PATH.to_string() + "/" + filename).unwrap() + } else { + return Err(()); + }; + + Ok(contents) + } + + fn decode_hex(value: &str) -> Vec { + let clean = value.trim(); + if clean.is_empty() { Vec::new() } else { hex::decode(clean).expect("valid hex") } + } + + /// Parse a NIST LWC KAT file: blank-line-delimited `Tag = Value` cases. + fn parse_kat(contents: &str) -> Vec> { + let mut cases = Vec::new(); + let mut current = BTreeMap::new(); + + for raw in contents.lines() { + let line = raw.trim(); + if line.is_empty() { + if !current.is_empty() { + cases.push(std::mem::take(&mut current)); + } + continue; + } + if line.starts_with('#') { + continue; + } + if let Some((key, value)) = line.split_once('=') { + let key = key.trim().to_string(); + let value = value.trim().to_string(); + if key == "Count" && !current.is_empty() { + cases.push(std::mem::take(&mut current)); + } + current.insert(key, value); + } + } + if !current.is_empty() { + cases.push(current); + } + cases + } + + fn field<'a>(case: &'a BTreeMap, names: &[&str]) -> &'a str { + for name in names { + if let Some(v) = case.get(*name) { + return v.as_str(); + } + } + panic!("missing field {names:?}; case had {:?}", case.keys().collect::>()); + } + + fn to_16(bytes: &[u8], what: &str) -> [u8; 16] { + bytes.try_into().unwrap_or_else(|_| panic!("{what} must be 16 bytes, got {}", bytes.len())) + } + + /// Build a `KeyMaterial<16>` for a KAT key. The NIST LWC vectors include an all-zero key + /// (Count=1), which `KeyMaterial::from_bytes_as_type` would otherwise tag + /// `KeyType::Zeroized` / `SecurityStrength::None`; force the type/strength the way a caller + /// who knows the provenance of the key would (see `cli/src/helpers.rs::parse_seed`). + fn key_material(key: &[u8; 16]) -> KeyMaterial<16> { + let mut km = + KeyMaterial::<16>::from_bytes_as_type(key, KeyType::SymmetricCipherKey).unwrap(); + do_hazardous_operations(&mut km, |k| { + k.set_key_type(KeyType::SymmetricCipherKey)?; + k.set_security_strength(SecurityStrength::_128bit) + }) + .unwrap(); + km + } + + #[test] + fn ascon_aead128_kat() { + let contents = match get_test_data("asconaead128/LWC_AEAD_KAT_128_128.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no AEAD cases parsed"); + + for case in &cases { + let key = key_material(&to_16(&decode_hex(field(case, &["Key", "K"])), "key")); + let nonce = to_16(&decode_hex(field(case, &["Nonce", "N"])), "nonce"); + let ad = decode_hex(field(case, &["AD", "A"])); + let pt = decode_hex(field(case, &["PT", "P"])); + let expected_ct = decode_hex(field(case, &["CT", "C"])); + let ad_opt = if ad.is_empty() { None } else { Some(ad.as_slice()) }; + + // One-shot encrypt. + let mut ct = vec![0u8; pt.len() + 16]; + let n = AsconAead128::encrypt(&key, &nonce, ad_opt, &pt, &mut ct).unwrap(); + ct.truncate(n); + assert_eq!(ct, expected_ct, "encrypt mismatch (Count {})", field(case, &["Count"])); + + // One-shot decrypt round-trip. + let mut pt_out = vec![0u8; expected_ct.len()]; + let m = AsconAead128::decrypt(&key, &nonce, ad_opt, &expected_ct, &mut pt_out) + .expect("decrypt should authenticate"); + pt_out.truncate(m); + assert_eq!(pt_out, pt, "decrypt mismatch (Count {})", field(case, &["Count"])); + + // Byte-at-a-time streaming encrypt/decrypt, through the inherent API. + let mut enc = AsconAead128::new(&key, &nonce, ad_opt, true).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(&key, &nonce, ad_opt, false).unwrap(); + let mut stream_pt = expected_ct[..pt.len()].to_vec(); + for byte in stream_pt.iter_mut() { + dec.do_decrypt_update(core::slice::from_mut(byte)); + } + dec.do_decrypt_final(&tag).expect("streaming decrypt should authenticate"); + assert_eq!( + stream_pt, + pt, + "streaming decrypt mismatch (Count {})", + field(case, &["Count"]) + ); + } + println!("Ascon-AEAD128: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_hash256_kat() { + let contents = match get_test_data("asconhash256/LWC_HASH_KAT_256.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no Hash256 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let expected = decode_hex(field(case, &["MD"])); + assert_eq!( + AsconHash256::digest(&msg).as_slice(), + expected.as_slice(), + "Hash256 mismatch (Count {})", + field(case, &["Count"]) + ); + } + println!("Ascon-Hash256: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_xof128_kat() { + let contents = match get_test_data("asconxof128/LWC_XOF_KAT_128_512.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no XOF128 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let expected = decode_hex(field(case, &["MD", "Output"])); + let got = AsconXof128::new().hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "XOF128 mismatch (Count {})", field(case, &["Count"])); + } + println!("Ascon-XOF128: {} KAT cases passed", cases.len()); + } + + #[test] + fn ascon_cxof128_kat() { + let contents = match get_test_data("asconcxof128/LWC_CXOF_KAT_128_512.txt") { + Ok(c) => c, + Err(()) => return, + }; + let cases = parse_kat(&contents); + assert!(!cases.is_empty(), "no CXOF128 cases parsed"); + + for case in &cases { + let msg = decode_hex(field(case, &["Msg"])); + let z = decode_hex(field(case, &["Z", "Customization"])); + let expected = decode_hex(field(case, &["MD", "Output"])); + let got = AsconCXof128::with_customization(&z).unwrap().hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "CXOF128 mismatch (Count {})", field(case, &["Count"])); + } + println!("Ascon-CXOF128: {} KAT cases passed", cases.len()); + } +} diff --git a/crypto/ascon/tests/cxof128_tests.rs b/crypto/ascon/tests/cxof128_tests.rs new file mode 100644 index 00000000..5478ba58 --- /dev/null +++ b/crypto/ascon/tests/cxof128_tests.rs @@ -0,0 +1,221 @@ +//! Ascon-CXOF128 tests (NIST SP 800-232 §5.3). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus +//! domain-separation, streaming/byte-at-a-time equivalence, trait-API, and misuse-guard tests. + +use bouncycastle_ascon::ascon_cxof128::AsconCXof128; +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::XOF; +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().hash_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 via `Default`) only applies to the empty-Z vectors; the + // non-empty-Z vectors are covered by `cxof128_prefix_property_and_streaming` below. + if z.is_empty() { + // AsconCXof128 has no absorb_last_partial_byte / squeeze_partial_byte_final support, so + // that part of the framework is disabled; everything else (hash_xof, streaming, prefix + // property, chunked absorb, absorb-after-squeeze) is exercised here. + TestFrameworkXOF { enable_partial_byte_tests: false } + .test_xof::(&msg, &expected); + } + } +} + +#[test] +fn cxof128_domain_separation() { + let msg = pattern(48); + + let out_z1 = AsconCXof128::with_customization(b"context-1").unwrap().hash_xof(&msg, 64); + let out_z2 = AsconCXof128::with_customization(b"context-2").unwrap().hash_xof(&msg, 64); + assert_ne!(out_z1, out_z2, "different customization strings must give different output"); + + // Empty-customization CXOF128 must differ from XOF128 (different IV). + let cxof_empty = AsconCXof128::new().hash_xof(&msg, 64); + let xof = AsconXof128::new().hash_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().hash_xof(&msg, 100); + + // Squeezing in several calls yields the same stream (prefix property). + let mut x = AsconCXof128::with_customization(z).unwrap(); + x.absorb(&msg).unwrap(); + let mut piecewise = Vec::new(); + for n in [30usize, 40, 30] { + let mut part = vec![0u8; n]; + x.squeeze_out(&mut part); + piecewise.extend_from_slice(&part); + } + assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + + // Absorbing in chunks equals one-shot absorb. + for chunk in [1usize, 8, 9, 64] { + let mut xc = AsconCXof128::with_customization(z).unwrap(); + for piece in msg.chunks(chunk) { + xc.absorb(piece).unwrap(); + } + let mut got = vec![0u8; 100]; + xc.squeeze_out(&mut got); + assert_eq!(got, full, "chunked absorb mismatch (chunk={chunk})"); + } +} + +#[test] +fn cxof128_byte_at_a_time_matches_one_shot() { + let msg = pattern(40); // > 8 bytes so byte-at-a-time absorb triggers full-block absorption + let cref = AsconCXof128::with_customization(b"zz").unwrap().hash_xof(&msg, 48); + let mut c = AsconCXof128::with_customization(b"zz").unwrap(); + for &b in &msg { + c.absorb(&[b]).unwrap(); + } + let mut o = [0u8; 48]; + c.squeeze_out(&mut o); + assert_eq!(o.to_vec(), cref, "CXOF128 byte-at-a-time absorb mismatch"); +} + +#[test] +fn cxof128_unsupported_partial_ops_return_err() { + let mut c = AsconCXof128::new(); + assert!(c.absorb_last_partial_byte(0, 3).is_err()); + assert!(AsconCXof128::new().squeeze_partial_byte_final(3).is_err()); + let mut b = 0u8; + assert!(AsconCXof128::new().squeeze_partial_byte_final_out(3, &mut b).is_err()); +} + +#[test] +fn cxof128_absorb_after_squeeze_errors() { + let mut x = AsconCXof128::with_customization(b"z").unwrap(); + x.absorb(b"data").unwrap(); + let mut out = [0u8; 8]; + x.squeeze_out(&mut out); + // Absorbing after squeezing has begun is reported as an error rather than a panic. + assert!(matches!(x.absorb(b"more"), Err(HashError::InvalidState(_)))); +} + +#[test] +fn cxof128_suspendable_state() { + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::Suspendable; + 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 r = AsconCXof128::with_customization(z).unwrap(); + r.absorb(&data).unwrap(); + let mut expected = [0u8; 40]; + r.squeeze_out(&mut expected); + + // Suspend mid-absorb, resume, finish, and confirm the squeezed output matches. (The + // customization string was already absorbed at construction and is not part of the state.) + let mut x = AsconCXof128::with_customization(z).unwrap(); + x.absorb(&data[..5]).unwrap(); + TestFrameworkSuspendableState::new().test(&x); + + let serialized = x.clone().suspend(); + let mut resumed = AsconCXof128::from_suspended(serialized).unwrap(); + resumed.absorb(&data[5..]).unwrap(); + let mut out = [0u8; 40]; + resumed.squeeze_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 (same serialized length) must be rejected by + // Ascon-CXOF128 via the state tag. + let mut xof = AsconXof128::new(); + xof.absorb(&data).unwrap(); + 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 once squeezing has begun. + let mut bad = serialized; + let len = bad.len(); + bad[len - 2] = 8; // buf_pos = RATE + bad[len - 1] = 0; // squeezing = false + assert!(matches!(AsconCXof128::from_suspended(bad), Err(SuspendableError::InvalidData))); + + // Suspend mid-squeeze (not just mid-absorb) and confirm resuming continues the same stream. + let mut sq = AsconCXof128::with_customization(z).unwrap(); + sq.absorb(&data).unwrap(); + let mut head = [0u8; 5]; + sq.squeeze_out(&mut head); + let squeezing_state = sq.clone().suspend(); + let mut resumed_sq = AsconCXof128::from_suspended(squeezing_state).unwrap(); + let mut tail = [0u8; 35]; + resumed_sq.squeeze_out(&mut tail); + let mut combined = Vec::new(); + combined.extend_from_slice(&head); + combined.extend_from_slice(&tail); + assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); +} + +#[test] +fn cxof128_customization_length_bound() { + // SP 800-232 §5.3: the customization string shall be at most 2048 bits (256 bytes). + let ok = vec![0u8; 256]; + assert!(AsconCXof128::with_customization(&ok).is_ok()); + + let too_long = vec![0u8; 257]; + assert!(matches!(AsconCXof128::with_customization(&too_long), Err(HashError::InvalidInput(_)))); +} diff --git a/crypto/ascon/tests/hash256_tests.rs b/crypto/ascon/tests/hash256_tests.rs new file mode 100644 index 00000000..8e6ee545 --- /dev/null +++ b/crypto/ascon/tests/hash256_tests.rs @@ -0,0 +1,152 @@ +//! Ascon-Hash256 tests (NIST SP 800-232 §5.1). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus +//! streaming-equivalence, one-shot/trait-API, metadata, and unsupported-partial-op tests. + +use bouncycastle_ascon::ascon_hash256::AsconHash256; +use bouncycastle_core::traits::{Hash, HashAlgParams}; +use bouncycastle_core_test_framework::hash::TestFrameworkHash; +use bouncycastle_hex as hex; + +/// Embedded NIST LWC Ascon-Hash256 vectors `(message, digest)` in hex, spanning empty, sub-block, +/// exact-block, and multi-block messages. (Counts 1, 2, 9, 17, 33 of LWC_HASH_KAT_256.txt.) +const HASH_KAT: &[(&str, &str)] = &[ + ("", "0B3BE5850F2F6B98CAF29F8FDEA89B64A1FA70AA249B8F839BD53BAA304D92B2"), + ("00", "0728621035AF3ED2BCA03BF6FDE900F9456F5330E4B5EE23E7F6A1E70291BC80"), + ("0001020304050607", "B88E497AE8E6FB641B87EF622EB8F2FCA0ED95383F7FFEBE167ACF1099BA764F"), + ( + "000102030405060708090A0B0C0D0E0F", + "3158C1940A2FBADBD68AB661777859B94A689E4EFC375911467ADDD641835C38", + ), + ( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F", + "BD9D3D60A66B53868EAB2A5C74539A518A1F60F01EB176C60E43DEE81680B33E", + ), +]; + +fn dh(s: &str) -> Vec { + let s = s.trim(); + if s.is_empty() { Vec::new() } else { hex::decode(s).expect("valid hex") } +} + +fn pattern(len: usize) -> Vec { + (0..len).map(|i| (i as u8).wrapping_mul(7).wrapping_add(1)).collect() +} + +#[test] +fn hash256_embedded_kat() { + for (msg_hex, md_hex) in HASH_KAT { + let msg = dh(msg_hex); + let expected = dh(md_hex); + assert_eq!(AsconHash256::digest(&msg).as_slice(), expected.as_slice(), "msg={msg_hex}"); + + // AsconHash256 has no do_final_partial_bits support, so that part of the framework + // is disabled; everything else (hash/hash_out/do_update+do_final(_out), truncation, + // oversized-buffer zero-fill) is exercised here. + TestFrameworkHash { enable_partial_byte_tests: false } + .test_hash::(&msg, &expected); + } +} + +#[test] +fn hash256_streaming_matches_one_shot() { + let msg = pattern(100); + let expected = AsconHash256::digest(&msg); + + // One-shot APIs agree. + assert_eq!(AsconHash256::new().hash(&msg), expected.to_vec()); + let mut buf = [0u8; 32]; + let mut h = AsconHash256::new(); + h.do_update(&msg); + h.do_final_out(&mut buf); + assert_eq!(buf, expected); + + // Chunked do_update agrees for a range of chunk sizes. + for chunk in [1usize, 7, 8, 9, 16, 33] { + let mut hasher = AsconHash256::new(); + for piece in msg.chunks(chunk) { + hasher.do_update(piece); + } + let mut got = [0u8; 32]; + hasher.do_final_out(&mut got); + assert_eq!(got, expected, "chunked hash mismatch (chunk={chunk})"); + } + + // Byte-at-a-time do_update() agrees. + let mut hasher = AsconHash256::new(); + for &b in &msg { + hasher.do_update(&[b]); + } + let mut got = [0u8; 32]; + hasher.do_final_out(&mut got); + assert_eq!(got, expected, "byte-at-a-time hash mismatch"); +} + +#[test] +fn hash256_metadata_accessors() { + assert_eq!(AsconHash256::OUTPUT_LEN, 32); + let h = AsconHash256::new(); + assert_eq!(h.output_len(), 32); + assert_eq!(h.block_bitlen(), 64); +} + +#[test] +fn hash256_do_final_out_truncates_to_buffer() { + let msg = pattern(50); + let expected = AsconHash256::digest(&msg); + + let mut h = AsconHash256::new(); + h.do_update(&msg); + let mut o = [0u8; 16]; + assert_eq!(h.do_final_out(&mut o), 16); + assert_eq!(o, expected[..16]); +} + +#[test] +fn hash256_hash_out_zeroizes_past_output_len() { + let msg = pattern(50); + let expected = AsconHash256::digest(&msg); + + let mut o = [0xEEu8; 64]; + assert_eq!(AsconHash256::new().hash_out(&msg, &mut o), 32); + assert_eq!(&o[..32], &expected[..]); + assert_eq!(&o[32..], &[0u8; 32]); +} + +#[test] +fn hash256_unsupported_partial_ops_return_err() { + assert!(AsconHash256::new().do_final_partial_bits(0, 3).is_err()); + let mut o = [0u8; 32]; + assert!(AsconHash256::new().do_final_partial_bits_out(0, 3, &mut o).is_err()); +} + +#[test] +fn hash256_suspendable_state() { + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::Suspendable; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; + + let data: Vec = (0..37u8).collect(); + let expected = AsconHash256::digest(&data).to_vec(); + + // Suspend mid-absorb, resume, finish, and confirm the digest matches an uninterrupted run. + let mut h = AsconHash256::new(); + h.do_update(&data[..7]); + TestFrameworkSuspendableState::new().test(&h); + + let serialized = h.clone().suspend(); + let mut resumed = AsconHash256::from_suspended(serialized).unwrap(); + resumed.do_update(&data[7..]); + assert_eq!(resumed.do_final(), expected, "resumed digest must match uninterrupted digest"); + + // A corrupted state tag must be rejected (the tag is the byte after the 3-byte version prefix). + let mut busted = serialized; + busted[3] ^= 0xFF; + assert!(matches!(AsconHash256::from_suspended(busted), Err(SuspendableError::InvalidData))); + + // An out-of-range buffer position must be rejected (buf_pos is the final byte). + let mut bad_pos = serialized; + let last = bad_pos.len() - 1; + bad_pos[last] = 99; // >= RATE (8) + assert!(matches!(AsconHash256::from_suspended(bad_pos), Err(SuspendableError::InvalidData))); +} diff --git a/crypto/ascon/tests/xof128_tests.rs b/crypto/ascon/tests/xof128_tests.rs new file mode 100644 index 00000000..22ed9c0a --- /dev/null +++ b/crypto/ascon/tests/xof128_tests.rs @@ -0,0 +1,183 @@ +//! Ascon-XOF128 tests (NIST SP 800-232 §5.2). +//! +//! Embedded NIST LWC known-answer vectors (always-on; full sweep in `bc_test_data.rs`) plus the +//! prefix property, streaming/byte-at-a-time equivalence, trait-API, and misuse-guard tests. + +use bouncycastle_ascon::ascon_xof128::AsconXof128; +use bouncycastle_core::errors::HashError; +use bouncycastle_core::traits::XOF; +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().hash_xof(&msg, expected.len()); + assert_eq!(got, expected, "msg={msg_hex}"); + // AsconXof128 has no absorb_last_partial_byte / squeeze_partial_byte_final support, so that + // part of the framework is disabled; everything else (hash_xof, streaming, prefix property, + // chunked absorb, absorb-after-squeeze) is exercised here. + TestFrameworkXOF { enable_partial_byte_tests: false } + .test_xof::(&msg, &expected); + } +} + +#[test] +fn xof128_prefix_property_and_streaming() { + let msg = pattern(70); + let full = AsconXof128::new().hash_xof(&msg, 100); + + // Squeezing in several calls yields the same stream (prefix property). + let mut x = AsconXof128::new(); + x.absorb(&msg).unwrap(); + let mut piecewise = Vec::new(); + for n in [30usize, 40, 30] { + let mut part = vec![0u8; n]; + x.squeeze_out(&mut part); + piecewise.extend_from_slice(&part); + } + assert_eq!(piecewise, full, "incremental squeeze must equal a single squeeze"); + + // Absorbing in chunks equals one-shot absorb. + for chunk in [1usize, 8, 9, 64] { + let mut xc = AsconXof128::new(); + for piece in msg.chunks(chunk) { + xc.absorb(piece).unwrap(); + } + let mut got = vec![0u8; 100]; + xc.squeeze_out(&mut got); + assert_eq!(got, full, "chunked absorb mismatch (chunk={chunk})"); + } +} + +#[test] +fn xof128_byte_at_a_time_matches_one_shot() { + let msg = pattern(40); // > 8 bytes so byte-at-a-time absorb triggers full-block absorption + let xref = AsconXof128::new().hash_xof(&msg, 48); + let mut x = AsconXof128::new(); + for &b in &msg { + x.absorb(&[b]).unwrap(); + } + let mut o = [0u8; 48]; + x.squeeze_out(&mut o); + assert_eq!(o.to_vec(), xref, "XOF128 byte-at-a-time absorb mismatch"); +} + +#[test] +fn xof128_unsupported_partial_ops_return_err() { + let mut x = AsconXof128::new(); + assert!(x.absorb_last_partial_byte(0, 3).is_err()); + assert!(AsconXof128::new().squeeze_partial_byte_final(3).is_err()); + let mut b = 0u8; + assert!(AsconXof128::new().squeeze_partial_byte_final_out(3, &mut b).is_err()); +} + +#[test] +fn xof128_absorb_after_squeeze_errors() { + let mut x = AsconXof128::new(); + x.absorb(b"data").unwrap(); + let mut out = [0u8; 8]; + x.squeeze_out(&mut out); + // Absorbing after squeezing has begun is a usage error; the trait API reports it as an error + // rather than panicking. + assert!(matches!(x.absorb(b"more"), Err(HashError::InvalidState(_)))); +} + +#[test] +fn xof128_suspendable_state() { + use bouncycastle_ascon::ascon_cxof128::AsconCXof128; + use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::traits::Suspendable; + use bouncycastle_core_test_framework::suspendable_state::TestFrameworkSuspendableState; + + let data: Vec = (0..30u8).collect(); + + // Reference: uninterrupted absorb + squeeze. + let mut r = AsconXof128::new(); + r.absorb(&data).unwrap(); + let mut expected = [0u8; 40]; + r.squeeze_out(&mut expected); + + // Suspend mid-absorb, resume, finish, and confirm the squeezed output matches. + let mut x = AsconXof128::new(); + x.absorb(&data[..5]).unwrap(); + TestFrameworkSuspendableState::new().test(&x); + + let serialized = x.clone().suspend(); + let mut resumed = AsconXof128::from_suspended(serialized).unwrap(); + resumed.absorb(&data[5..]).unwrap(); + let mut out = [0u8; 40]; + resumed.squeeze_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 (same serialized length) must be rejected by + // Ascon-XOF128 via the state tag. + let mut c = AsconCXof128::with_customization(b"z").unwrap(); + c.absorb(&data).unwrap(); + 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; // buf_pos = RATE + bad[len - 1] = 0; // squeezing = false + assert!(matches!(AsconXof128::from_suspended(bad), Err(SuspendableError::InvalidData))); + + // Suspend mid-squeeze (not just mid-absorb) and confirm resuming continues the same stream. + let mut sq = AsconXof128::new(); + sq.absorb(&data).unwrap(); + let mut head = [0u8; 5]; + sq.squeeze_out(&mut head); + let squeezing_state = sq.clone().suspend(); + let mut resumed_sq = AsconXof128::from_suspended(squeezing_state).unwrap(); + let mut tail = [0u8; 35]; + resumed_sq.squeeze_out(&mut tail); + let mut combined = Vec::new(); + combined.extend_from_slice(&head); + combined.extend_from_slice(&tail); + assert_eq!(combined, expected, "resuming mid-squeeze must continue the same output stream"); +} diff --git a/crypto/core-test-framework/src/electronic_code_book.rs b/crypto/core-test-framework/src/electronic_code_book.rs new file mode 100644 index 00000000..4691e3f9 --- /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::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::traits::{ElectronicCodeBook, 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_blocks2` agrees with two `encrypt_block` calls **including their order**, and + /// likewise for `decrypt_blocks2` -- this is what pins an override to the default's + /// 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_blocks8` / `decrypt_blocks8` likewise agree with eight 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 override that swapped the two results, or that 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_blocks2(&mut paired); + assert_eq!(paired, singly, "encrypt_blocks2 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_blocks2(&mut paired); + assert_eq!(paired, singly, "decrypt_blocks2 must match two decrypt_block calls"); + + // Round-trip through the pair methods alone. + let mut buf = [*a, *b]; + perm.encrypt_blocks2(&mut buf); + perm.decrypt_blocks2(&mut buf); + assert_eq!(buf, [*a, *b], "decrypt_blocks2 must invert encrypt_blocks2"); + } + + // The eight-block methods must be indistinguishable from eight single-block calls, in every + // slot, whether they are the trait default (four pair calls) or an override. + let eights = blocks.as_chunks::<8>().0; + assert!( + !eights.is_empty(), + "DUMMY_SEED should hold at least eight blocks; test setup problem" + ); + for eight in eights.iter() { + let mut singly = *eight; + for block in singly.iter_mut() { + perm.encrypt_block(block); + } + let mut batched = *eight; + perm.encrypt_blocks8(&mut batched); + assert_eq!(batched, singly, "encrypt_blocks8 must match eight encrypt_block calls"); + + let mut singly = *eight; + for block in singly.iter_mut() { + perm.decrypt_block(block); + } + let mut batched = *eight; + perm.decrypt_blocks8(&mut batched); + assert_eq!(batched, singly, "decrypt_blocks8 must match eight decrypt_block calls"); + + let mut buf = *eight; + perm.encrypt_blocks8(&mut buf); + perm.decrypt_blocks8(&mut buf); + assert_eq!(buf, *eight, "decrypt_blocks8 must invert encrypt_blocks8"); + } + + // 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_blocks2(&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/hash.rs b/crypto/core-test-framework/src/hash.rs index 6c880ba9..44037462 100644 --- a/crypto/core-test-framework/src/hash.rs +++ b/crypto/core-test-framework/src/hash.rs @@ -99,7 +99,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 +119,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 +185,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(); diff --git a/crypto/core-test-framework/src/lib.rs b/crypto/core-test-framework/src/lib.rs index 2dced83d..45d922e4 100644 --- a/crypto/core-test-framework/src/lib.rs +++ b/crypto/core-test-framework/src/lib.rs @@ -14,6 +14,7 @@ // properly document everything. #![forbid(missing_docs)] +pub mod electronic_code_book; pub mod hash; pub mod kdf; pub mod kem; diff --git a/crypto/core-test-framework/src/symmetric_ciphers.rs b/crypto/core-test-framework/src/symmetric_ciphers.rs index 57fc0ee1..cf55b3b5 100644 --- a/crypto/core-test-framework/src/symmetric_ciphers.rs +++ b/crypto/core-test-framework/src/symmetric_ciphers.rs @@ -1,23 +1,29 @@ //! Generic behaviour tests for the symmetric cipher traits. -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::traits::{ - AEADCipher, BlockCipher, SecurityStrength, StreamCipher, SymmetricCipher, + AEADCipher, BlockCipherDecryptor, BlockCipherEncryptor, SecurityStrength, StreamCipher, + SymmetricCipher, 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 `PaddedEncryptor` over `NoPadding` -- means only multiples of it + /// round-trip, and every other length must be *rejected* by `do_final` / `encrypt_out` with a + /// `PaddingError`, which the test then asserts instead. + pub required_alignment: usize, } impl TestFrameworkSymmetricCipher { /// pub fn new() -> Self { - Self {} + Self { required_alignment: 1 } } /// Test all the members of trait SymmetricCipher against the given input-output pair. @@ -83,11 +89,18 @@ impl TestFrameworkSymmetricCipher { SecurityStrength::_192bit, SecurityStrength::_256bit, ]; + let mut strengths_tested = 0; 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. + // A key can only carry a strength its length supports (a 16-byte key cannot be + // tagged at 192- or 256-bit), so strengths above the key length do not apply to + // this cipher. + 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 C::encrypt_out(&key, msg, &mut ct) { Ok(_) => { @@ -105,6 +118,263 @@ impl TestFrameworkSymmetricCipher { _ => panic!("Unexpected error"), }; } + assert!(strengths_tested > 0, "strength sweep must not be vacuous"); + } +} + +impl TestFrameworkSymmetricCipher { + /// 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_max_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_final_out` agrees with `do_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_out_rng`; + /// * 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; + /// * 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, + const FINAL_LEN: usize, + E: SymmetricCipherEncryptor, + D: SymmetricCipherDecryptor, + >( + &self, + ) { + 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); + + // 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.update_out_len(len)]; + enc.do_update_out(msg, &mut buf).unwrap(); + assert!( + matches!(enc.do_final(), Err(SymmetricCipherError::PaddingError(_))), + "len {len}: streaming do_final must refuse an unaligned message" + ); + 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_max_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_max_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"); + } + + // streaming in every chunking agrees with the one-shot + let len = 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]; + 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.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "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_final_out(&mut last).unwrap(); + assert!(last_len <= FINAL_LEN, "do_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_max_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_final and via do_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.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact (decrypt, chunk {chunk})"); + rec.extend_from_slice(&buf[..n]); + } + let (block, data_len) = if use_out { + let mut block = [0u8; FINAL_LEN]; + let data_len = dec.do_final_out(&mut block).unwrap(); + (block, data_len) + } else { + dec.do_final().unwrap() + }; + rec.extend_from_slice(&block[..data_len]); + assert_eq!(rec, msg, "streamed round trip (chunk {chunk}, do_final_out {use_out})"); + } + } + + // 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.update_out_len(len)]; + let n = enc.do_update_out(msg, &mut streamed).unwrap(); + streamed.truncate(n); + let (last, last_len) = enc.do_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_out_rng( + &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" + ); + + // 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_max_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:?}"), + } + } + + // 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::IncorrectOutputBufferLength(_, n)) => assert_eq!(n, need), + other => panic!("encrypt_out into a short buffer: {other:?}"), + } + let need = D::decrypt_out_max_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::IncorrectOutputBufferLength(_, n)) => assert_eq!(n, need), + other => panic!("decrypt_out into a short buffer: {other:?}"), + } + } + let (mut enc, _) = E::do_encrypt_init(&key).unwrap(); + let need = enc.update_out_len(len); + if need > 0 { + let mut short = vec![0u8; need - 1]; + match enc.do_update_out(msg, &mut short) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, n)) => assert_eq!(n, need), + other => panic!("do_update_out into a short buffer: {other:?}"), + } + } + + // error case: KeyMaterial of the 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!("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 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() { + // 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(_)) => { + assert!(ss < &D::MAX_SECURITY_STRENGTH) + } + _ => panic!("Unexpected error"), + }; + } } } @@ -124,7 +394,8 @@ impl TestFrameworkBlockCipher { const KEY_LEN: usize, const INIT_DATA_LEN: usize, const BLOCK_LEN: usize, - C: BlockCipher, + E: BlockCipherEncryptor, + D: BlockCipherDecryptor, >( &self, ) { @@ -135,42 +406,88 @@ impl TestFrameworkBlockCipher { .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(); + 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 ct = encryptor.do_encrypt_block(msg_chunk).unwrap(); - let pt = decryptor.do_decrypt_block(&ct).unwrap(); - assert_eq!(msg_chunk, &pt); + let mut buf = *msg_chunk; + encryptor.do_encrypt(&mut buf).unwrap(); + decryptor.do_decrypt(&mut buf).unwrap(); + assert_eq!(msg_chunk, &buf); } - // 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); - - let pt_bytes_written = decryptor.do_decrypt_block_out(&ct, &mut pt).unwrap(); - assert_eq!(pt_bytes_written, BLOCK_LEN); + // 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(&mut buf).unwrap(); + decryptor.do_decrypt_blocks(&mut buf).unwrap(); + assert_eq!(msg_pair, &buf); + + // encrypt together, decrypt one at a time + let mut buf = *msg_pair; + encryptor.do_encrypt_blocks(&mut buf).unwrap(); + for (msg_chunk, block) in msg_pair.iter().zip(buf.iter_mut()) { + decryptor.do_decrypt(block).unwrap(); + assert_eq!(msg_chunk, block); + } - assert_eq!(msg_chunk, &pt); + // encrypt one at a time, decrypt together + let mut buf = *msg_pair; + for block in buf.iter_mut() { + encryptor.do_encrypt(block).unwrap(); + } + decryptor.do_decrypt_blocks(&mut buf).unwrap(); + assert_eq!(msg_pair, &buf); } - // 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); + // 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 iv = E::encrypt(&key, &mut buf).unwrap(); + let ct = buf; + D::decrypt(&key, &iv, &mut buf).unwrap(); + 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(&mut buf).unwrap(); + assert_eq!(buf, *one_block); + + // 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(&mut expected).unwrap(); + let mut buf = *one_block; + let iv = E::encrypt_rng(&key, &mut FixedSeedRNG::::new(pinned), &mut buf) + .unwrap(); + 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 C::do_encrypt_init(&mac_key) { + match E::do_encrypt_init(&mac_key) { Err(SymmetricCipherError::KeyMaterialError(_)) => { /* good */ } _ => panic!("Unexpected error"), }; @@ -188,21 +505,31 @@ impl TestFrameworkBlockCipher { SecurityStrength::_192bit, SecurityStrength::_256bit, ]; + let mut strengths_tested = 0; 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; + } + // 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 C::do_encrypt_init(&key) { + match E::do_encrypt_init(&key) { Ok(_) => { - if ss >= &C::MAX_SECURITY_STRENGTH { /* good */ + if ss >= &E::MAX_SECURITY_STRENGTH { /* good */ } else { panic!("Should have been a strong enough key"); } } Err(SymmetricCipherError::KeyMaterialError(_)) => { - if ss < &C::MAX_SECURITY_STRENGTH { /* good */ + if ss < &E::MAX_SECURITY_STRENGTH { /* good */ } else { panic!("Should not have accepted a key weaker than algorithm"); } @@ -210,6 +537,7 @@ impl TestFrameworkBlockCipher { _ => panic!("Unexpected error"), }; } + assert!(strengths_tested > 0, "strength sweep must not be vacuous"); } } @@ -263,15 +591,21 @@ impl TestFrameworkAEADCipher { // 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; + pt[..ct_bytes_written].fill(0xAA); 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"), }; + assert!( + pt[..ct_bytes_written].iter().all(|&b| b == 0), + "AEAD must not leave plaintext in the output buffer after a failed 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 + pt[..ct_bytes_written].fill(0xAA); match C::aead_decrypt_out( &key, &nonce, @@ -283,8 +617,13 @@ impl TestFrameworkAEADCipher { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } _ => panic!("Expected TagCheckFailed error"), }; + assert!( + pt[..ct_bytes_written].iter().all(|&b| b == 0), + "AEAD must not leave plaintext in the output buffer after a failed tag check" + ); // messing with the tag causes the aead_decrypt to fail + pt[..ct_bytes_written].fill(0xAA); match C::aead_decrypt_out( &key, &nonce, @@ -296,6 +635,10 @@ impl TestFrameworkAEADCipher { Err(SymmetricCipherError::AEADTagCheckFailed) => { /* good */ } _ => panic!("Expected TagCheckFailed error"), }; + assert!( + pt[..ct_bytes_written].iter().all(|&b| b == 0), + "AEAD must not leave plaintext in the output buffer after a failed tag check" + ); // multiple invocations give different nonces let (nonce1, _ct_bytes_written, _tag) = @@ -326,11 +669,18 @@ impl TestFrameworkAEADCipher { SecurityStrength::_192bit, SecurityStrength::_256bit, ]; + let mut strengths_tested = 0; 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. + // A key can only carry a strength its length supports (a 16-byte key cannot be + // tagged at 192- or 256-bit), so strengths above the key length do not apply to + // this cipher. + 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; // 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. @@ -352,6 +702,7 @@ impl TestFrameworkAEADCipher { check_strength(C::aead_encrypt_out(&key, aad, msg, &mut ct).map(|_| ())); check_strength(C::encrypt_out(&key, msg, &mut ct).map(|_| ())); } + assert!(strengths_tested > 0, "strength sweep must not be vacuous"); } } diff --git a/crypto/core-test-framework/src/xof.rs b/crypto/core-test-framework/src/xof.rs index 9ec5040b..a430702d 100644 --- a/crypto/core-test-framework/src/xof.rs +++ b/crypto/core-test-framework/src/xof.rs @@ -16,27 +16,90 @@ impl TestFrameworkXOF { Self { enable_partial_byte_tests: true } } - /// 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. + /// Test the members of trait [`XOF`] against the given input and expected output. /// `expected_output` is the result of squeezing `expected_output.len()` bytes after absorbing - /// `input`. + /// `input`; since every [`XOF`] has the prefix property, this also doubles as a prefix for + /// deriving shorter expected outputs by truncation. + /// + /// Covers one-shot vs. streaming equivalence, the prefix property, chunked absorb, and 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. pub fn test_xof(&self, input: &[u8], expected_output: &[u8]) { + let n = expected_output.len(); + + /*** fn hash_xof(self, data: &[u8], result_len: usize) -> Vec ***/ + assert_eq!(X::default().hash_xof(input, n), expected_output); + + /*** fn hash_xof_out(self, data: &[u8], output: &mut [u8]) -> usize ***/ + let mut out = vec![0u8; n]; + assert_eq!(X::default().hash_xof_out(input, &mut out), n); + assert_eq!(out, expected_output); + /*** fn absorb(&mut self, data: &[u8]) -> Result<(), HashError> ***/ - // Absorbing is fine, repeatedly, right up until the first squeeze. - let mut xof = X::default(); - for chunk in input.chunks(16) { - xof.absorb(chunk).expect("absorb() before any squeeze must succeed"); + /*** fn squeeze(&mut self, num_bytes: usize) -> Vec ***/ + let mut x = X::default(); + x.absorb(input).expect("absorb() before any squeeze must succeed"); + assert_eq!(x.squeeze(n), expected_output); + + /*** fn squeeze_out(&mut self, output: &mut [u8]) -> usize ***/ + let mut x = X::default(); + x.absorb(input).expect("absorb() before any squeeze must succeed"); + let mut out = vec![0u8; n]; + assert_eq!(x.squeeze_out(&mut out), n); + assert_eq!(out, expected_output); + + /*** Absorbing in chunks must equal absorbing in one shot. ***/ + let mut x = X::default(); + for chunk in input.chunks(3.max(input.len() / 5)) { + x.absorb(chunk).expect("absorb() before any squeeze must succeed"); + } + assert_eq!(x.squeeze(n), expected_output); + + /*** Prefix property: squeeze(k) for k < n must equal a truncation of squeeze(n). ***/ + for k in 0..n { + let mut x = X::default(); + x.absorb(input).expect("absorb() before any squeeze must succeed"); + assert_eq!(x.squeeze(k), &expected_output[..k], "prefix property failed at k={k}"); + } + + /*** Squeezing in multiple calls must equal squeezing the same total in one call. Uses a + mix of call sizes, including single bytes, so that whatever the rate of the underlying + sponge is, some calls fall entirely within an already-squeezed-but-not-yet-consumed + block (exercising the internal leftover-byte bookkeeping) and some straddle a block + boundary. ***/ + if n >= 2 { + let mut x = X::default(); + x.absorb(input).expect("absorb() before any squeeze must succeed"); + let mut piecewise = Vec::with_capacity(n); + let mut remaining = n; + let mut call_len = 1usize; + while remaining > 0 { + let this_call = call_len.min(remaining); + piecewise.extend(x.squeeze(this_call)); + remaining -= this_call; + call_len = (call_len % 5) + 1; // cycle 1,2,3,4,5,1,2,... + } + assert_eq!(piecewise, expected_output, "multi-call squeeze must match one-shot"); + } + + /*** Byte-at-a-time squeeze must also match (exercises every possible internal buffer + position at least once, for any rate up to n bytes). ***/ + let mut x = X::default(); + x.absorb(input).expect("absorb() before any squeeze must succeed"); + let mut byte_at_a_time = Vec::with_capacity(n); + for _ in 0..n { + byte_at_a_time.extend(x.squeeze(1)); } + assert_eq!(byte_at_a_time, expected_output, "byte-at-a-time squeeze must match one-shot"); // "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()); + let _ = xof.squeeze(n); assert!( matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), "absorb() after squeeze() must return InvalidState" @@ -45,7 +108,7 @@ impl TestFrameworkXOF { // ... 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()]; + let mut output = vec![0u8; n]; xof.squeeze_out(&mut output); assert!( matches!(xof.absorb(b"more input"), Err(HashError::InvalidState(_))), @@ -58,13 +121,13 @@ impl TestFrameworkXOF { // 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. - let split = expected_output.len() / 2; + let split = n / 2; 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]; + let mut second_half = vec![0u8; n - split]; xof.squeeze_out(&mut second_half); assert_eq!( @@ -83,7 +146,7 @@ impl TestFrameworkXOF { // 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()); + let _ = xof.squeeze(n); assert!( matches!(xof.absorb_last_partial_byte(0x01, 3), Err(HashError::InvalidState(_))), "absorb_last_partial_byte() after squeeze() must return InvalidState" @@ -99,7 +162,7 @@ impl TestFrameworkXOF { 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 expected_partial_output = xof.squeeze(n); let mut xof = X::default(); xof.absorb(input).expect("absorb() before any squeeze must succeed"); @@ -120,20 +183,20 @@ impl TestFrameworkXOF { // ... and, again, the rejections must leave the object usable for further squeezing. assert_eq!( - xof.squeeze(expected_output.len()), + xof.squeeze(n), expected_partial_output, "the output stream must be unchanged by a rejected absorb / num_bits: {num_bits}" ); } - // Helper: the output stream of `input` finished with the low `num_bits` bits of + // Helper: the output stream of `input` finished with the top `num_bits` bits of // `partial_byte`. let partial_absorb_output = |partial_byte: u8, num_bits: usize| -> Vec { let mut xof = X::default(); xof.absorb(input).expect("absorb() before any squeeze must succeed"); xof.absorb_last_partial_byte(partial_byte, num_bits) .expect("absorb_last_partial_byte() must succeed for num_bits in 0..=7"); - xof.squeeze(expected_output.len()) + xof.squeeze(n) }; // "0 is a valid value and means the message ends on a byte boundary (equivalent to @@ -147,17 +210,18 @@ impl TestFrameworkXOF { ); } - // "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. + // "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_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}" + "the low 8 - num_bits = {} bits must be ignored / partial_byte: {partial_byte:#04X}", + 8 - num_bits ); } } @@ -179,14 +243,15 @@ impl TestFrameworkXOF { /*** 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; + // "in the most significant num_bits bits of the returned u8, first output bit first, with + // the low 8 - num_bits "unused" bits zero." + // They are the first bits of the next byte of the output stream, which `expected_output` + // gives us: after squeezing `split` bytes, the next byte is expected_output[split]. In + // that byte the first output bit is the LSB (FIPS 202 B.1 / the byte-oriented stream), so + // the expected partial byte is the bit-reversal of it, masked to the top num_bits bits. 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; let mut xof = X::default(); xof.absorb(input).expect("absorb() before any squeeze must succeed"); @@ -197,13 +262,13 @@ impl TestFrameworkXOF { 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}" + expected_output[split].reverse_bits() & mask, + "the squeezed bits must be the first bits of the next output byte, MSB-first / num_bits: {num_bits}" ); assert_eq!( partial_byte & !mask, 0x00, - "the unused high bits of the result must be zero / num_bits: {num_bits}" + "the unused low bits of the result must be zero / num_bits: {num_bits}" ); // "The same as XOF::squeeze_partial_byte_final, but writes into the provided output diff --git a/crypto/core-test-framework/summary.md b/crypto/core-test-framework/summary.md new file mode 100644 index 00000000..738de37a --- /dev/null +++ b/crypto/core-test-framework/summary.md @@ -0,0 +1,189 @@ +# `crypto/core-test-framework` — changes for `ElectronicCodeBook` and CBC + +Changes made on branch `feature/officialfrancismendoza/100-AES-lightengine-CBC-mode` while adding +`crypto/aes-lowmemory` and `crypto/modes`. Two things: a **new** per-trait suite for +`core::traits::ElectronicCodeBook`, and a **bug fix** to the existing `TestFrameworkBlockCipher`. + +For what this crate is for in general, see its [`src/lib.rs`](src/lib.rs) docs: one KAT-style +harness per `core` trait, so that behaviour which should be consistent across implementations of a +trait — error handling, input/output lengths, `KeyMaterial` entropy enforcement — is asserted once +here rather than re-written per implementation. + +--- + +## 1. New: `TestFrameworkElectronicCodeBook` + +[`src/electronic_code_book.rs`](src/electronic_code_book.rs), registered as `pub mod electronic_code_book;` +in [`src/lib.rs`](src/lib.rs). + +`core::traits::ElectronicCodeBook` is new in this branch: the raw keyed +permutation (`CIPH_K` / `CIPH^-1_K` of SP 800-38A Sec 5.1) that a mode of operation is built on. +It needed a conformance suite like every other `core` trait. + +```rust +TestFrameworkElectronicCodeBook::new().test::(); +``` + +### What it checks, and why each check exists + +| Check | What it catches | +|---|---| +| `decrypt_block` inverts `encrypt_block`, **and vice versa** | A direction implemented only one way round. A mode may call either direction first, so both orders are exercised. | +| Neither direction is the identity | A stub, or a key schedule that never got applied. | +| Distinct blocks give distinct outputs | An implementation that is not injective — e.g. one masking part of the block away. A permutation must be. | +| `encrypt_blocks2` == two `encrypt_block` calls, **including their order**; same for decrypt | The whole reason the pair methods are safe to override. See below. | +| The pair methods round-trip each other | A pair path correct in one direction only. | +| Identical inputs give identical outputs from `*_blocks2` | Lanes that are not actually independent — a real hazard for a bit-sliced implementation that interleaves two blocks in one word. | +| A key of the wrong `KeyType` is rejected | A seed or MAC key being reused as a cipher key. | +| The security-strength policy matches `BlockCipher::MAX_SECURITY_STRENGTH` | A `new()` that accepts a key weaker than the algorithm, or rejects one strong enough. | + +### The order check is the load-bearing one + +`ElectronicCodeBook::encrypt_blocks2` and `decrypt_blocks2` are *provided* methods: the default is +two single-block calls, and implementations are free to override them. `bouncycastle-aes-lowmemory` +does, because a pair of blocks is exactly what its bit-sliced state holds, so the pair form costs +barely more than one block. + +An override is therefore a place where an implementation can silently disagree with the trait's +semantics — most easily by returning the two results in the wrong order, which round-trips +perfectly and so passes any test that only checks encrypt-then-decrypt. Asserting equality against +two explicit single-block calls, slot by slot, is what makes an override trustworthy. That check is +the reason this suite is worth having rather than leaving each implementor to test itself. + +The mirror image of this check lives in `crypto/modes/tests/common/mod.rs` as `SwappedPairToy`, a +permutation whose pair methods deliberately swap their results, used to prove the *mode* really +takes the pair path. + +### Current implementors + +* `crypto/aes-lowmemory/tests/electronic_code_book_tests.rs` — AES-128, AES-192, AES-256. +* `crypto/modes/tests/cbc_tests.rs` — the toy permutation, checked before anything is concluded + from it. + +--- + +## 2. Fixed: `TestFrameworkBlockCipher` panicked for any key under 32 bytes + +### The bug + +`TestFrameworkBlockCipher::test` ended with a loop that tagged the test key at each of the five +`SecurityStrength` values and checked the `_init` constructor's accept/reject decision against +`MAX_SECURITY_STRENGTH`: + +```rust +for ss in security_strengths.iter() { + do_hazardous_operations(&mut key, |key| key.set_security_strength(ss.clone())).unwrap(); + // ... +} +``` + +`KeyMaterial::set_security_strength` enforces a key-length guard — a key cannot be tagged at a +strength its own length cannot carry — and it enforces it **even inside a +`do_hazardous_operations` closure**. So for a 16-byte key the loop reached `_192bit`, got +`Err(SecurityStrength("Security strength cannot be larger than key length."))`, and the `unwrap()` +panicked. The comment above the loop asserted the opposite ("bypasses the key-length guard"), which +is what made it look correct. + +The result: the harness was unusable for AES-128 or AES-192, i.e. for most block ciphers. + +### Why nobody had noticed + +Nothing in the workspace implemented `BlockCipherEncryptor`/`BlockCipherDecryptor`. The traits +landed in PR #96 with the harness written against them but no implementor — the toy XOR-CBC cipher +that would have exercised it lives in `crypto/padding`, which is PR #97 and has not merged to this +branch. `crypto/modes`' CBC is the first implementor in the tree, and it hit the panic immediately. + +### The fix + +Skip the strengths the key length cannot hold, rather than unwrapping the error: + +```rust +if ss > &SecurityStrength::from_bytes(KEY_LEN) { + continue; +} +``` + +For a 16-byte key this tests `None`, `_112bit` and `_128bit` — which still spans the +`MAX_SECURITY_STRENGTH` boundary for AES-128, so the accept/reject decision is still exercised on +both sides. Nothing is lost; the skipped cases were never reachable. + +### What **not** to do instead + +Do not relax the guard in `KeyMaterial::set_security_strength`. `core`'s +`test_hazardous_ops_error_handling` requires it to stay enforced even inside +`do_hazardous_operations`. A comment at the fix says so, because "make the setter permissive" is +the tempting one-line alternative and it breaks a core test. This is the same conclusion reached +independently on the ASCON branch. + +--- + +## 3. Still outstanding: the same bug, twice more + +The identical loop appears in two other suites in +[`src/symmetric_ciphers.rs`](src/symmetric_ciphers.rs) and is **not** fixed: + +| Suite | Loop at | Implementors in tree | Status | +|---|---|---|---| +| `TestFrameworkSymmetricCipher` | line 87 | 0 | latent, unfixed | +| `TestFrameworkBlockCipher` | line 240 | 1 (`crypto/modes`) | **fixed** | +| `TestFrameworkAEADCipher` | line 386 | 0 | latent, unfixed | +| `TestFrameworkStreamCipher` | — | 0 | unaffected (no strength handling) | + +Both unfixed suites will panic the first time anything implements their trait with a key shorter +than 32 bytes — which for `AEADCipher` includes ASCON-128 and AES-128-GCM. They were left alone to +keep this change scoped to what CBC needed; the fix is the same three lines in each. Worth doing +before the next implementor arrives rather than after. + +Note that `TestFrameworkStreamCipher` is a different case: it has no security-strength handling at +all, so there is nothing to fix there and nothing being checked either. + +--- + +## 4. Unchanged but newly exercised: `FixedSeedRNG` + +[`src/fixed_seed_rng.rs`](src/fixed_seed_rng.rs) already existed and was not modified. It is worth +recording that it is now what makes CBC's known-answer tests possible. + +`Cbc` deliberately has no API for a caller-supplied IV — SP 800-38A Sec 5.3 requires the CBC IV to +be *unpredictable*, so `do_encrypt_init` generates one and returns it. That leaves a problem for +testing: Appendix F.2 specifies the IV, and there is no way to pass it in. + +`BlockCipherEncryptor::do_encrypt_init_rng(key, &mut dyn RNG)` is the seam. +`FixedSeedRNG::<16>::new(iv)` emits the vector's IV as its first sixteen bytes, so the test can pin +the IV without the production API ever accepting one. `crypto/modes/tests/sp800_38a_tests.rs` +asserts the returned init data really is the expected IV before comparing any ciphertext, so a +change that ignored the RNG could not pass silently. + +This is the pattern to reuse for CFB, OFB and CTR when they land. + +--- + +## 5. Verification + +```sh +cargo build -p bouncycastle-core-test-framework +cargo test --workspace # 517 tests, 0 failures +cargo fmt --all -- --check +``` + +This crate has no tests of its own — it *is* tests — so it is verified by its consumers. The two +new suites are exercised by: + +* `cargo test -p bouncycastle-aes-lowmemory --test electronic_code_book_tests` (3 tests) +* `cargo test -p bouncycastle-modes --test cbc_tests` (11 tests, including + `cbc_conforms_to_the_block_cipher_framework`, which is what the §2 fix unblocked, and + `the_toy_permutation_conforms_to_the_trait`) + +--- + +## 6. Open items + +1. **Fix the same loop in `TestFrameworkSymmetricCipher` and `TestFrameworkAEADCipher`** (§3). + Three lines each, and the next implementor of either trait will otherwise hit the panic. +2. **Decide whether the `Default` impl added to `TestFrameworkElectronicCodeBook` should be added to + the other suites** for consistency — they all have `new()` and no `Default`, which clippy + flags on new code but not on existing code. +3. When `crypto/padding` (PR #97) merges, its toy XOR-CBC cipher becomes a second + `TestFrameworkBlockCipher` implementor. Worth re-running that suite then: an XOR-based cipher has + `encrypt_block == decrypt_block`, which is exactly the property `crypto/modes`' non-XOR toy was + chosen to avoid, so it may expose gaps this branch's tests do not. diff --git a/crypto/core/src/errors.rs b/crypto/core/src/errors.rs index 7be5197e..53a987af 100644 --- a/crypto/core/src/errors.rs +++ b/crypto/core/src/errors.rs @@ -176,12 +176,35 @@ pub enum SymmetricCipherError { /// KeyMaterialError(KeyMaterialError), /// + PaddingError(PaddingError), + /// RNGError(RNGError), /// StateError(&'static str), } +/// Errors from a [`crate::traits::Padding`] scheme. +#[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum PaddingError { + /// `pad()` was asked to pad more data than fits in a block alongside at least one byte of padding. + /// The usize is the maximum permitted data length (`BLOCK_LEN - 1`). + DataLengthTooLong(usize), + /// `unpad()` found the block does not carry well-formed padding. Deliberately carries no detail + /// about *how* the padding was malformed. + InvalidPadding, + /// `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/traits.rs b/crypto/core/src/traits.rs index 22652570..34a69fa3 100644 --- a/crypto/core/src/traits.rs +++ b/crypto/core/src/traits.rs @@ -33,7 +33,7 @@ pub trait AEADCipher, @@ -41,7 +41,7 @@ pub trait AEADCipher 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 + /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) 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. @@ -70,7 +70,7 @@ pub trait AEADCipher Result; - /// All AEAD ciphers will also be either a [`BlockCipher`] or a [`StreamCipher`], and so will already + /// All AEAD ciphers will also be either a block cipher ([`BlockCipherEncryptor`] / [`BlockCipherDecryptor`]) 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. @@ -95,73 +95,252 @@ pub trait AlgorithmOID { const OID_DER: &'static [u8]; } -/// The basic functions of a block cipher. +/// 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 +{ + /// 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; + /// The implementor hook: decrypts consecutive whole blocks in place. See + /// [`BlockCipherEncryptor::do_encrypt_blocks`]; callers should normally use the flat + /// [`BlockCipherDecryptor::do_decrypt`] instead. + fn do_decrypt_blocks( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result<(), SymmetricCipherError>; + + /// 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`]. + fn do_decrypt( + &mut self, + data: &mut [u8; LEN], + ) -> Result<(), SymmetricCipherError> { + 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(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`]. + fn decrypt( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + data: &mut [u8; LEN], + ) -> Result<(), SymmetricCipherError> { + Self::do_decrypt_init(key, init_data)?.do_decrypt(data) + } +} + +/// The encryption half of a block cipher's streaming 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 +/// (`PaddedEncryptor` / `PaddedDecryptor`) built on top of this trait. +/// +/// Encryption and decryption are separate traits (as with [`KEMEncapsulator`] / [`KEMDecapsulator`]) so +/// that the direction can be encoded in the type, and so that a policy can permit decryption of an +/// algorithm 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 one-shot APIs to be usable securely in all contexts, the init data will be generated +/// In order for these APIs to be usable securely in all contexts, the init data will be generated /// securely by the block cipher implementation and returned along with the ciphertext, and there is no API for the /// user to provide the init data. If you require this functionality, see the documentation for the underlying implementation. -pub trait BlockCipher: - SymmetricCipher + Sized +/// +/// # 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 { - /// 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. + /// 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>; - /// 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. - fn do_decrypt_init( + /// As [`BlockCipherEncryptor::do_encrypt_init`], but sources randomness from the provided RNG. + fn do_encrypt_init_rng( 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( - &mut self, - ciphertext: &[u8; BLOCK_LEN], - plaintext: &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( + 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 eights -- 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`] instead. + fn do_encrypt_blocks( &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( + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result<(), SymmetricCipherError>; + + /// Streaming: encrypts `LEN` bytes, a whole number of blocks, in place. A sequence of calls + /// is equivalent to one call over the concatenation. + /// + /// `LEN % BLOCK_LEN == 0` is checked **at compile time**; see the trait docs. The whole buffer + /// then goes to [`BlockCipherEncryptor::do_encrypt_blocks`] in one call. + fn do_encrypt( &mut self, - ciphertext: &[u8; BLOCK_LEN], - plaintext: &mut [u8; BLOCK_LEN], - ) -> Result; + data: &mut [u8; LEN], + ) -> Result<(), SymmetricCipherError> { + 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(blocks) + } + + /// One-shot: encrypts `LEN` bytes in place under a fresh init, and returns the generated init + /// data. `LEN % BLOCK_LEN == 0` is checked **at compile time**; see the trait docs. + fn encrypt( + key: &KeyMaterial, + data: &mut [u8; LEN], + ) -> Result<[u8; INIT_DATA_LEN], SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init(key)?; + enc.do_encrypt(data)?; + Ok(init_data) + } + /// As [`BlockCipherEncryptor::encrypt`], but sources randomness from the provided RNG. + fn encrypt_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + data: &mut [u8; LEN], + ) -> Result<[u8; INIT_DATA_LEN], SymmetricCipherError> { + let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; + enc.do_encrypt(data)?; + Ok(init_data) + } +} + +/// A keyed block permutation: the `CIPH_K` / `CIPH^-1_K` of NIST SP 800-38A Sec 5.1. +/// +/// This is the raw primitive a mode of operation is built on, not something to encrypt data with. +/// It transforms exactly one block, so applying it directly to data is ECB (Sec 6.1), which is not +/// confidential -- the trait is named for the mode it *is* when used that way, as a reminder. [`BlockCipherEncryptor`] and [`BlockCipherDecryptor`] are the *mode* traits -- +/// they carry initialization data and chaining state; this one carries only a key schedule. +/// +/// Implementors are expected to hold that 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. + /// + /// Provided as two [`ElectronicCodeBook::encrypt_block`] calls. Bit-sliced implementations + /// override it, because a pair of blocks is their natural unit of work and costs barely more + /// than one; see `bouncycastle-aes-lowmemory`. + /// + /// Overrides must be indistinguishable from the default, 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_blocks2(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { + let [a, b] = blocks; + self.encrypt_block(a); + self.encrypt_block(b); + } + + /// The inverse cipher function on two *independent* blocks, in place. + /// See [`ElectronicCodeBook::encrypt_blocks2`]. + fn decrypt_blocks2(&self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { + let [a, b] = blocks; + self.decrypt_block(a); + self.decrypt_block(b); + } + + /// The forward cipher function on eight *independent* blocks, in place. + /// + /// Provided as four [`ElectronicCodeBook::encrypt_blocks2`] calls, so an implementation that + /// overrides only the pair form gets its benefit here too. An engine whose natural unit is + /// larger than a pair overrides this directly: a bit-sliced engine whose S-box circuit + /// substitutes four blocks per pass runs eight blocks as two full passes rather than four + /// half-empty pair calls. + /// + /// Overrides must be indistinguishable from the default, including the order of the eight + /// results. `TestFrameworkElectronicCodeBook` pins that. + /// + /// Modes with parallel structure chunk their data into eights first, then pairs, then single + /// blocks; see CBC decryption in `bouncycastle-modes`. + fn encrypt_blocks8(&self, blocks: &mut [[u8; BLOCK_LEN]; 8]) { + // Eight is a multiple of two, so the remainder is empty. + let (pairs, _) = blocks.as_mut_slice().as_chunks_mut::<2>(); + for pair in pairs { + self.encrypt_blocks2(pair); + } + } + + /// The inverse cipher function on eight *independent* blocks, in place. + /// See [`ElectronicCodeBook::encrypt_blocks8`]. + fn decrypt_blocks8(&self, blocks: &mut [[u8; BLOCK_LEN]; 8]) { + let (pairs, _) = blocks.as_mut_slice().as_chunks_mut::<2>(); + for pair in pairs { + self.decrypt_blocks2(pair); + } + } } /// A hash function is a cryptographic primitive that takes an input of any length and produces a fixed-size output. @@ -210,9 +389,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 +734,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 Padding { + /// 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< @@ -989,6 +1207,8 @@ pub trait SuspendableKeyed: Sized { ) -> Result; } +// todo -- migrate AEADCipher and StreamCipher onto SymmetricCipherEncryptor / +// SymmetricCipherDecryptor (below), which are the split form of this trait, and retire this one. /// 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 @@ -1038,6 +1258,278 @@ pub trait SymmetricCipher: Alg ) -> Result; } +/// 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_final`](Self::do_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::update_out_len), +/// [`do_update_out`](Self::do_update_out), [`do_final`](Self::do_final) and +/// [`decrypt_out_max_len`](Self::decrypt_out_max_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_update_out) will write if + /// given `input_len` more bytes of ciphertext. Depends on what is already buffered. + fn update_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::update_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_final`](Self::do_final) is the plaintext. + /// + /// # Errors + /// [`SymmetricCipherError::IncorrectOutputBufferLength`] if `plaintext` is shorter than + /// [`update_out_len`](Self::update_out_len), carrying the required length. Nothing is + /// consumed in that case. + fn do_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result; + + /// 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, or not a + /// whole number of blocks); [`SymmetricCipherError::PaddingError`] or + /// [`SymmetricCipherError::AEADTagCheckFailed`] if the check fails. In every error case the + /// caller learns only that decryption failed, not where. + fn do_final(self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError>; + + /// As [`do_final`](Self::do_final), writing the final buffer into `plaintext`. Returns the + /// number of leading bytes of it that are data. + fn do_final_out(self, plaintext: &mut [u8; FINAL_LEN]) -> Result { + let (buffer, data_len) = self.do_final()?; + *plaintext = buffer; + 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_max_len(ciphertext_len: usize) -> usize; + + /// One-shot: decrypts `ciphertext` into `plaintext`, which needs + /// [`decrypt_out_max_len`](Self::decrypt_out_max_len) bytes. Returns the number of plaintext + /// bytes written. + /// + /// Provided as `do_decrypt_init`, one `do_update_out` and `do_final`. + /// + /// # Errors + /// [`SymmetricCipherError::IncorrectOutputBufferLength`] 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 { + let needed = Self::decrypt_out_max_len(ciphertext.len()); + if plaintext.len() < needed { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("plaintext", needed)); + } + let mut dec = Self::do_decrypt_init(key, init_data)?; + let written = dec.do_update_out(ciphertext, plaintext)?; + let (last, data_len) = dec.do_final()?; + // `decrypt_out_max_len` bounds `written + data_len`, so this fits in `plaintext[..needed]`. + plaintext[written..written + data_len].copy_from_slice(&last[..data_len]); + Ok(written + data_len) + } + + #[cfg(feature = "std")] + /// 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. + fn decrypt( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ciphertext: &[u8], + ) -> Result, SymmetricCipherError> { + let mut plaintext = vec![0u8; Self::decrypt_out_max_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_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-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_final`](Self::do_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::update_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_out_rng`](Self::encrypt_out_rng) +/// are provided over the streaming methods. An implementor writes only the two `_init` +/// constructors, [`update_out_len`](Self::update_out_len), [`do_update_out`](Self::do_update_out), +/// [`do_final`](Self::do_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. + 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_update_out) will write if + /// given `input_len` more bytes of plaintext. Depends on what is already buffered. + fn update_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::update_out_len) of `plaintext.len()`. A sequence of calls + /// is equivalent to one call over the concatenation. + /// + /// # Errors + /// [`SymmetricCipherError::IncorrectOutputBufferLength`] if `ciphertext` is shorter than + /// [`update_out_len`](Self::update_out_len), carrying the required length. Nothing is + /// consumed in that case. + fn do_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result; + + /// 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. + fn do_final(self) -> Result<([u8; FINAL_LEN], usize), SymmetricCipherError>; + + /// As [`do_final`](Self::do_final), writing the final buffer into `ciphertext`. Returns the + /// number of leading bytes of it that are output. + fn do_final_out(self, ciphertext: &mut [u8; FINAL_LEN]) -> Result { + let (buffer, out_len) = self.do_final()?; + *ciphertext = buffer; + 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_final`](Self::do_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. + /// + /// Provided as `do_encrypt_init`, one `do_update_out` and `do_final`. + /// + /// # Errors + /// [`SymmetricCipherError::IncorrectOutputBufferLength`] 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> { + let needed = Self::encrypt_out_len(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("ciphertext", needed)); + } + let (mut enc, init_data) = Self::do_encrypt_init(key)?; + let written = enc.do_update_out(plaintext, ciphertext)?; + let (last, last_len) = enc.do_final()?; + // `encrypt_out_len` is exactly `written + last_len`, so this fits in `ciphertext[..needed]`. + ciphertext[written..written + last_len].copy_from_slice(&last[..last_len]); + Ok((init_data, written + last_len)) + } + + /// As [`encrypt_out`](Self::encrypt_out), but sources randomness from the provided RNG. + fn encrypt_out_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result<([u8; INIT_DATA_LEN], usize), SymmetricCipherError> { + let needed = Self::encrypt_out_len(plaintext.len()); + if ciphertext.len() < needed { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("ciphertext", needed)); + } + let (mut enc, init_data) = Self::do_encrypt_init_rng(key, rng)?; + let written = enc.do_update_out(plaintext, ciphertext)?; + let (last, last_len) = enc.do_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, + 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, @@ -1078,9 +1570,11 @@ pub trait XOF: Default { 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. + /// The partial byte is taken as it arrives in the final octet of an ASN.1 BIT STRING + /// (X.690 s. 8.6.2.1): the `num_bits` message bits are the most significant bits of + /// `partial_byte`, leading bit first, and the low `8 - num_bits` bits (the BIT STRING's "unused + /// bits") are ignored. This is the same convention as [`Hash::do_final_partial_bits`]; see there + /// for the relationship to the FIPS 202 Appendix B.1 bit order and to the NIST test vector files. /// 0 is a valid value and means the message ends on a byte boundary (equivalent to [`XOF::absorb`]). /// `num_bits` must be in `0..=7`; larger values return [`HashError::InvalidLength`]. /// @@ -1101,10 +1595,11 @@ pub trait XOF: Default { 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`]. + /// The bits are returned as they would be placed in the final octet of an ASN.1 BIT STRING + /// (X.690 s. 8.6.2.1): in the most significant `num_bits` bits of the returned u8, first output + /// bit first, with the low `8 - num_bits` "unused" bits zero. This matches the input convention of + /// [`XOF::absorb_last_partial_byte`]. (FIPS 202 Appendix B.1 orders the bits of an output byte + /// LSB-first; the implementation converts.) /// 0 is a valid value and requests no bits, so the result is `0x00`. /// `num_bits` must be in `0..=7`; larger values return [`HashError::InvalidLength`]. /// This is a final call and consumes self. diff --git a/crypto/factory/Cargo.toml b/crypto/factory/Cargo.toml index d3060ebd..6f8f5317 100644 --- a/crypto/factory/Cargo.toml +++ b/crypto/factory/Cargo.toml @@ -4,11 +4,13 @@ version.workspace = true edition.workspace = true [dependencies] +bouncycastle-ascon.workspace = true bouncycastle-core.workspace = true bouncycastle-hkdf.workspace = true bouncycastle-hmac.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..1d3893a4 100644 --- a/crypto/factory/src/hash_factory.rs +++ b/crypto/factory/src/hash_factory.rs @@ -28,12 +28,18 @@ 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_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. @@ -48,6 +54,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 +65,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 +94,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 +126,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 +143,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 +160,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 +179,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 +196,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 +213,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 +232,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 +253,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 +275,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 +293,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 +306,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/mac_factory.rs b/crypto/factory/src/mac_factory.rs index f9a46768..d5d415ab 100644 --- a/crypto/factory/src/mac_factory.rs +++ b/crypto/factory/src/mac_factory.rs @@ -75,12 +75,17 @@ use bouncycastle_core::errors::MACError; use bouncycastle_core::key_material::KeyMaterialTrait; use bouncycastle_core::traits::{MAC, SecurityStrength}; use bouncycastle_hmac as hmac; +use bouncycastle_hmac::HMAC_SM3_NAME; use bouncycastle_hmac::{ HMAC_SHA3_224_NAME, HMAC_SHA3_256_NAME, HMAC_SHA3_384_NAME, HMAC_SHA3_512_NAME, }; -use bouncycastle_hmac::{HMAC_SHA224_NAME, HMAC_SHA256_NAME, HMAC_SHA384_NAME, HMAC_SHA512_NAME}; +use bouncycastle_hmac::{ + HMAC_SHA224_NAME, HMAC_SHA256_NAME, HMAC_SHA384_NAME, HMAC_SHA512_224_NAME, + HMAC_SHA512_256_NAME, HMAC_SHA512_NAME, +}; use bouncycastle_sha2 as sha2; use bouncycastle_sha3 as sha3; +use bouncycastle_sm3 as sm3; /*** Defaults ***/ /// @@ -106,6 +111,10 @@ pub enum MACFactory { /// HMAC_SHA512(hmac::HMAC), /// + HMAC_SHA512_224(hmac::HMAC), + /// + HMAC_SHA512_256(hmac::HMAC), + /// HMAC_SHA3_224(hmac::HMAC), /// HMAC_SHA3_256(hmac::HMAC), @@ -113,6 +122,8 @@ pub enum MACFactory { HMAC_SHA3_384(hmac::HMAC), /// HMAC_SHA3_512(hmac::HMAC), + /// + HMAC_SM3(hmac::HMAC), } impl MACFactory { @@ -138,10 +149,17 @@ impl MACFactory { HMAC_SHA256_NAME => Ok(Self::HMAC_SHA256(hmac::HMAC::::new(key)?)), HMAC_SHA384_NAME => Ok(Self::HMAC_SHA384(hmac::HMAC::::new(key)?)), HMAC_SHA512_NAME => Ok(Self::HMAC_SHA512(hmac::HMAC::::new(key)?)), + HMAC_SHA512_224_NAME => { + Ok(Self::HMAC_SHA512_224(hmac::HMAC::::new(key)?)) + } + HMAC_SHA512_256_NAME => { + Ok(Self::HMAC_SHA512_256(hmac::HMAC::::new(key)?)) + } HMAC_SHA3_224_NAME => Ok(Self::HMAC_SHA3_224(hmac::HMAC::::new(key)?)), HMAC_SHA3_256_NAME => Ok(Self::HMAC_SHA3_256(hmac::HMAC::::new(key)?)), HMAC_SHA3_384_NAME => Ok(Self::HMAC_SHA3_384(hmac::HMAC::::new(key)?)), HMAC_SHA3_512_NAME => Ok(Self::HMAC_SHA3_512(hmac::HMAC::::new(key)?)), + HMAC_SM3_NAME => Ok(Self::HMAC_SM3(hmac::HMAC::::new(key)?)), _ => Err(FactoryError::UnsupportedAlgorithm(format!( "The algorithm: \"{}\" is not a known MAC", alg_name @@ -167,10 +185,13 @@ impl MAC for MACFactory { 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(), } } @@ -180,10 +201,13 @@ impl MAC for MACFactory { 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), } } @@ -195,10 +219,13 @@ impl MAC for MACFactory { 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), } } @@ -208,10 +235,13 @@ impl MAC for MACFactory { 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), } } @@ -221,10 +251,13 @@ impl MAC for MACFactory { 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), } } @@ -234,10 +267,13 @@ impl MAC for MACFactory { 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(), } } @@ -249,10 +285,13 @@ impl MAC for MACFactory { 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), } } @@ -262,10 +301,13 @@ impl MAC for MACFactory { 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), } } @@ -275,10 +317,13 @@ impl MAC for MACFactory { 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/xof_factory.rs b/crypto/factory/src/xof_factory.rs index c3d97473..9cc2fb7e 100644 --- a/crypto/factory/src/xof_factory.rs +++ b/crypto/factory/src/xof_factory.rs @@ -34,6 +34,8 @@ //! ``` 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_sha3 as sha3; @@ -54,6 +56,8 @@ pub enum XOFFactory { SHAKE128(sha3::SHAKE128), /// SHAKE256(sha3::SHAKE256), + /// + AsconXof128(AsconXof128), } impl Default for XOFFactory { @@ -75,6 +79,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 @@ -87,6 +92,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.hash_xof(data, result_len), Self::SHAKE256(h) => h.hash_xof(data, result_len), + Self::AsconXof128(h) => h.hash_xof(data, result_len), } } @@ -96,6 +102,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.hash_xof_out(data, output), Self::SHAKE256(h) => h.hash_xof_out(data, output), + Self::AsconXof128(h) => h.hash_xof_out(data, output), } } @@ -103,6 +110,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.absorb(data), Self::SHAKE256(h) => h.absorb(data), + Self::AsconXof128(h) => h.absorb(data), } } @@ -114,6 +122,7 @@ impl XOF for XOFFactory { 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::AsconXof128(h) => h.absorb_last_partial_byte(partial_byte, num_partial_bits), } } @@ -121,6 +130,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.squeeze(num_bytes), Self::SHAKE256(h) => h.squeeze(num_bytes), + Self::AsconXof128(h) => h.squeeze(num_bytes), } } @@ -130,6 +140,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.squeeze_out(output), Self::SHAKE256(h) => h.squeeze_out(output), + Self::AsconXof128(h) => h.squeeze_out(output), } } @@ -137,6 +148,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => h.squeeze_partial_byte_final(num_bits), Self::SHAKE256(h) => h.squeeze_partial_byte_final(num_bits), + Self::AsconXof128(h) => h.squeeze_partial_byte_final(num_bits), } } @@ -150,6 +162,7 @@ impl XOF for XOFFactory { 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::AsconXof128(h) => h.squeeze_partial_byte_final_out(num_bits, output), } } @@ -157,6 +170,7 @@ impl XOF for XOFFactory { match self { Self::SHAKE128(h) => KDF::max_security_strength(h), Self::SHAKE256(h) => XOF::max_security_strength(h), + Self::AsconXof128(h) => XOF::max_security_strength(h), } } } diff --git a/crypto/factory/tests/hash_factory_tests.rs b/crypto/factory/tests/hash_factory_tests.rs index 31d216bc..47328cda 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] @@ -101,6 +164,30 @@ mod hash_factory_tests { 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"); } + #[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] fn test_defaults() { // All the ways to get "default" diff --git a/crypto/factory/tests/mac_factory_tests.rs b/crypto/factory/tests/mac_factory_tests.rs index 912a7587..dbe96743 100644 --- a/crypto/factory/tests/mac_factory_tests.rs +++ b/crypto/factory/tests/mac_factory_tests.rs @@ -22,7 +22,147 @@ 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_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_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_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/xof_factory_tests.rs b/crypto/factory/tests/xof_factory_tests.rs index 7e414f94..574dbc68 100644 --- a/crypto/factory/tests/xof_factory_tests.rs +++ b/crypto/factory/tests/xof_factory_tests.rs @@ -1,4 +1,31 @@ #[cfg(test)] mod tests { - // todo + use bouncycastle_ascon::ASCON_XOF128_NAME; + use bouncycastle_ascon::ascon_xof128::AsconXof128; + use bouncycastle_core::traits::XOF; + use bouncycastle_core_test_framework::DUMMY_SEED; + use bouncycastle_factory::AlgorithmFactory; + use bouncycastle_factory::FactoryError; + use bouncycastle_factory::xof_factory::XOFFactory; + + #[test] + fn ascon_xof_round_trip() { + let direct = AsconXof128::new().hash_xof(&DUMMY_SEED[..512], 64); + + // Construct by literal name and by the crate's name constant; both must match the direct + // implementation. + let by_name = XOFFactory::new("Ascon-XOF128").unwrap(); + assert_eq!(by_name.hash_xof(&DUMMY_SEED[..512], 64), direct); + + let by_const = XOFFactory::new(ASCON_XOF128_NAME).unwrap(); + assert_eq!(by_const.hash_xof(&DUMMY_SEED[..512], 64), direct); + } + + #[test] + fn unknown_xof_name_is_rejected() { + assert!(matches!( + XOFFactory::new("Ascon-XOF999"), + Err(FactoryError::UnsupportedAlgorithm(_)) + )); + } } diff --git a/crypto/hmac/Cargo.toml b/crypto/hmac/Cargo.toml index ebb14077..1c046ffe 100644 --- a/crypto/hmac/Cargo.toml +++ b/crypto/hmac/Cargo.toml @@ -8,6 +8,7 @@ bouncycastle-core.workspace = true bouncycastle-rng.workspace = true bouncycastle-sha2.workspace = true bouncycastle-sha3.workspace = true +bouncycastle-sm3.workspace = true bouncycastle-utils.workspace = true [dev-dependencies] diff --git a/crypto/hmac/benches/hmac_benches.rs b/crypto/hmac/benches/hmac_benches.rs index 0e9dd039..830e5fa3 100644 --- a/crypto/hmac/benches/hmac_benches.rs +++ b/crypto/hmac/benches/hmac_benches.rs @@ -1,6 +1,6 @@ use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterial512, KeyType}; use bouncycastle_core::traits::{MAC, RNG}; -use bouncycastle_hmac::{HMAC_SHA256, HMAC_SHA512}; +use bouncycastle_hmac::{HMAC_SHA256, HMAC_SHA512, HMAC_SM3}; use bouncycastle_rng as rng; use criterion::{Criterion, Throughput, criterion_group, criterion_main}; use std::hint::black_box; @@ -51,5 +51,28 @@ fn bench_hmac_sha512(c: &mut Criterion) { group.finish(); } -criterion_group!(benches, bench_hmac_sha256, bench_hmac_sha512); +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 = KeyMaterial512::from_bytes_as_type(&data_block[..64], KeyType::MACKey).unwrap(); + let mut out = [0u8; 64]; + + 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_sha256, bench_hmac_sha512, bench_hmac_sm3); criterion_main!(benches); diff --git a/crypto/hmac/src/lib.rs b/crypto/hmac/src/lib.rs index 26d16999..42598f02 100644 --- a/crypto/hmac/src/lib.rs +++ b/crypto/hmac/src/lib.rs @@ -190,9 +190,11 @@ use bouncycastle_core::traits::{ }; use bouncycastle_rng::{HashDRBG_SHA256, HashDRBG_SHA512}; use bouncycastle_sha2::{ - SHA224, SHA256, SHA384, SHA512, SUSPENDED_SHA256_STATE_LEN, SUSPENDED_SHA512_STATE_LEN, + SHA224, SHA256, SHA384, SHA512, SHA512_224, SHA512_256, SUSPENDED_SHA256_STATE_LEN, + SUSPENDED_SHA512_STATE_LEN, }; use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512, SUSPENDED_SHA3_STATE_LEN}; +use bouncycastle_sm3::{SM3, SUSPENDED_SM3_STATE_LEN}; use bouncycastle_utils::{ct, secret::Secret}; use core::fmt::{Debug, Display, Formatter}; @@ -206,6 +208,10 @@ 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"; +/// pub const HMAC_SHA3_224_NAME: &str = "HMAC-SHA3-224"; /// pub const HMAC_SHA3_256_NAME: &str = "HMAC-SHA3-256"; @@ -213,6 +219,8 @@ pub const HMAC_SHA3_256_NAME: &str = "HMAC-SHA3-256"; pub const HMAC_SHA3_384_NAME: &str = "HMAC-SHA3-384"; /// pub const HMAC_SHA3_512_NAME: &str = "HMAC-SHA3-512"; +/// +pub const HMAC_SM3_NAME: &str = "HMAC-SM3"; /*** Type aliases ***/ /// Public type for HMAC using SHA224. @@ -267,6 +275,32 @@ impl AlgorithmOID for HMAC_SHA512 { const OID_DER: &'static [u8] = &[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; +impl Algorithm for HMAC_SHA512_224 { + const ALG_NAME: &'static str = HMAC_SHA512_224_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_112bit; +} +/// Defined in RFC 8018 Appendix B.1.2: id-hmacWithSHA512-224 { digestAlgorithm 12 } +impl AlgorithmOID for HMAC_SHA512_224 { + const OID: &'static [u32] = &[1, 2, 840, 113549, 2, 12]; + const 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; +impl Algorithm for HMAC_SHA512_256 { + const ALG_NAME: &'static str = HMAC_SHA512_256_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} +/// Defined in RFC 8018 Appendix B.1.2: id-hmacWithSHA512-256 { digestAlgorithm 13 } +impl AlgorithmOID for HMAC_SHA512_256 { + const OID: &'static [u32] = &[1, 2, 840, 113549, 2, 13]; + const OID_DER: &'static [u8] = &[0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x0d]; +} + /// Public type for HKDF using SHA3_224. #[allow(non_camel_case_types)] pub type HMAC_SHA3_224 = HMAC; @@ -323,11 +357,25 @@ impl AlgorithmOID for HMAC_SHA3_512 { &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x10]; } +/// Public type for HMAC using SM3 (GB/T 32905-2016). Block length 64 bytes. +#[allow(non_camel_case_types)] +pub type HMAC_SM3 = HMAC; +impl Algorithm for HMAC_SM3 { + const ALG_NAME: &'static str = HMAC_SM3_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} +/// Assigned by the Chinese OSCCA (GM/T 0006): sm3-with-key / hmac-sm3 { sm3 2 } = 1.2.156.10197.1.401.2 +impl AlgorithmOID for HMAC_SM3 { + const OID: &'static [u32] = &[1, 2, 156, 10197, 1, 401, 2]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x2A, 0x81, 0x1C, 0xCF, 0x55, 0x01, 0x83, 0x11, 0x02]; +} + // The internal key buffer must be able to hold a key up to the *block length* of the underlying hash: // per RFC 2104, a key no longer than the block is used verbatim (only longer keys are pre-hashed down // to the output length). So the buffer size is a const parameter of the struct, set per hash to its // block length by the type aliases below. Block lengths (bytes): SHA-224/256 = 64, SHA-384/512 = 128, -// SHA3-224 = 144, SHA3-256 = 136, SHA3-384 = 104, SHA3-512 = 72. +// SHA-512/224 = SHA-512/256 = 128, SHA3-224 = 144, SHA3-256 = 136, SHA3-384 = 104, SHA3-512 = 72. // // The default is used only when `HMAC` is written without an explicit buffer size; it is the // largest block length across all supported hashes, so it is always large enough. @@ -554,6 +602,10 @@ 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; /// Length in bytes of the serialized state of [`HMAC_SHA3_224`]. pub const SUSPENDED_HMAC_SHA3_224_STATE_LEN: usize = SUSPENDED_SHA3_STATE_LEN; /// Length in bytes of the serialized state of [`HMAC_SHA3_256`]. @@ -562,6 +614,8 @@ pub const SUSPENDED_HMAC_SHA3_256_STATE_LEN: usize = SUSPENDED_SHA3_STATE_LEN; pub const SUSPENDED_HMAC_SHA3_384_STATE_LEN: usize = SUSPENDED_SHA3_STATE_LEN; /// Length in bytes of the serialized state of [`HMAC_SHA3_512`]. pub const SUSPENDED_HMAC_SHA3_512_STATE_LEN: usize = SUSPENDED_SHA3_STATE_LEN; +/// Length in bytes of the serialized state of [`HMAC_SM3`]. +pub const SUSPENDED_HMAC_SM3_STATE_LEN: usize = SUSPENDED_SM3_STATE_LEN; /// HMAC is a keyed algorithm, so it implements [`SuspendableKeyed`] (rather than /// [`Suspendable`]) for suspending and resuming in-progress operations. @@ -638,7 +692,10 @@ impl_hmac_keygen!(SHA224, 64, 28, HashDRBG_SHA256); impl_hmac_keygen!(SHA256, 64, 32, HashDRBG_SHA256); impl_hmac_keygen!(SHA384, 128, 48, HashDRBG_SHA512); impl_hmac_keygen!(SHA512, 128, 64, HashDRBG_SHA512); +impl_hmac_keygen!(SHA512_224, 128, 28, HashDRBG_SHA512); +impl_hmac_keygen!(SHA512_256, 128, 32, HashDRBG_SHA512); impl_hmac_keygen!(SHA3_224, 144, 28, HashDRBG_SHA256); impl_hmac_keygen!(SHA3_256, 136, 32, HashDRBG_SHA256); impl_hmac_keygen!(SHA3_384, 104, 48, HashDRBG_SHA512); impl_hmac_keygen!(SHA3_512, 72, 64, HashDRBG_SHA512); +impl_hmac_keygen!(SM3, 64, 32, HashDRBG_SHA256); diff --git a/crypto/hmac/tests/hmac_tests.rs b/crypto/hmac/tests/hmac_tests.rs index 6b211c3b..0331f536 100644 --- a/crypto/hmac/tests/hmac_tests.rs +++ b/crypto/hmac/tests/hmac_tests.rs @@ -12,6 +12,7 @@ mod hmac_tests { use bouncycastle_hmac::*; use bouncycastle_sha2::*; use bouncycastle_sha3::{SHA3_224, SHA3_256, SHA3_384, SHA3_512}; + use bouncycastle_sm3::SM3; #[test] fn simple_tests() { @@ -74,6 +75,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(); @@ -85,6 +92,9 @@ mod hmac_tests { _ = HMAC::::new(&key).unwrap(); _ = HMAC_SHA3_512::new(&key).unwrap(); + + _ = HMAC::::new(&key).unwrap(); + _ = HMAC_SM3::new(&key).unwrap(); } #[test] @@ -279,10 +289,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)] @@ -602,6 +713,65 @@ 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(); + key_material::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; @@ -657,7 +827,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 = @@ -709,8 +882,11 @@ 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); } diff --git a/crypto/mldsa-lowmemory/src/aux_functions.rs b/crypto/mldsa-lowmemory/src/aux_functions.rs index 27264207..5eaf55f3 100644 --- a/crypto/mldsa-lowmemory/src/aux_functions.rs +++ b/crypto/mldsa-lowmemory/src/aux_functions.rs @@ -1,12 +1,14 @@ //! Implements auxiliary functions for ML-DSA as defined in Section 7 of FIPS 204. -// use crate::matrix::{Matrix, Vector}; use crate::mldsa::{G, H, POLY_T0PACKED_LEN}; -use crate::mldsa::{ - MLDSA44_GAMMA1, MLDSA44_GAMMA2, MLDSA65_GAMMA1, MLDSA65_GAMMA2, N, POLY_T1PACKED_LEN, d, q, +use crate::mldsa::{N, POLY_T1PACKED_LEN, d, q}; +use crate::params::{ + GAMMA1_2_POW_17, GAMMA1_2_POW_19, GAMMA2_Q_MINUS_1_OVER_32, GAMMA2_Q_MINUS_1_OVER_88, + MLDSAParams, }; use crate::polynomial::Polynomial; use bouncycastle_core::traits::XOF; +use bouncycastle_utils::secret::ZeroizablePrimitive; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) /// Output: An integer modulo 𝑞 or ⊥. @@ -32,8 +34,8 @@ pub(crate) fn coeff_from_three_bytes(b: &[u8; 3]) -> Result { /// Input: Integer 𝑏 ∈ {0, 1, … , 15}. /// Output: An integer between −𝜂 and 𝜂, or ⊥. #[inline(always)] -pub(crate) fn coeff_from_half_byte(b: u8) -> Result { - if ETA == 2 && b < 15 { +pub(crate) fn coeff_from_half_byte(b: u8) -> Result { + if P::eta == 2 && b < 15 { // Original code is bad because '%' is not constant-time. // Ok(2 - (b % 5) as i32) // TODO: Verify whether this function is constant time and whether it can be further optimized @@ -44,7 +46,7 @@ pub(crate) fn coeff_from_half_byte(b: u8) -> Result { }; Ok(2 - b as i32) } else { - if ETA == 4 && b < 9 { Ok(4 - b as i32) } else { Err(()) } + if P::eta == 4 && b < 9 { Ok(4 - b as i32) } else { Err(()) } } } @@ -64,16 +66,6 @@ pub(crate) fn simple_bit_pack_t1(w: &Polynomial) -> [u8; POLY_T1PACKED_LEN] { output } -/// As defined in Algorithm 17, this gives the length of a packed bitstring representing a polynomial -/// whose coefficients have been rounded to \[-eta, eta], which is 32*bitlen(2*eta). -pub const fn bitlen_eta(eta: usize) -> usize { - match eta { - 2 => 32 * 3, - 4 => 32 * 4, - _ => panic!("Invalid eta value"), - } -} - /// A variant of Algorithm 17 BitPack specific to a=eta, b=eta /// Encodes a polynomial 𝑤 into a byte string. /// Input: 𝑎, 𝑏 ∈ ℕ and 𝑤 ∈ 𝑅 such that the coefficients of 𝑤 are all in \[−eta, eta]. @@ -81,14 +73,14 @@ pub const fn bitlen_eta(eta: usize) -> usize { // `match ETA` folds away per monomorphization (ETA is a const generic), so ETA = 2 // and ETA = 4 each compile to just their own arm, leaving no dispatch at runtime. #[inline(always)] -pub(crate) fn bit_pack_eta(w: &Polynomial, r: &mut [u8]) { - debug_assert_eq!(r.len(), bitlen_eta(ETA)); +pub(crate) fn bit_pack_eta(w: &Polynomial, r: &mut [u8]) { + debug_assert_eq!(r.len(), P::POLY_ETA_PACKED_LEN); // temp swap space let mut t: [u8; 8] = [0; 8]; - match ETA { - // MLDSA44 and MLDSA87 + match P::eta { + // MLDSA-44 and MLDSA-87 2 => { let eta: i32 = 2; for i in 0..N / 8 { @@ -106,7 +98,7 @@ pub(crate) fn bit_pack_eta(w: &Polynomial, r: &mut [u8]) { r[3 * i + 2] = (t[5] >> 1) | (t[6] << 2) | (t[7] << 5); } } - // MLDSA65 + // MLDSA-65 4 => { let eta: i32 = 4; for i in 0..N / 2 { @@ -163,20 +155,22 @@ pub(crate) fn bit_pack_t0(t0: &Polynomial) -> [u8; POLY_T0PACKED_LEN] { } /// A variant of Algorithm 17 specific to packing z in the signature value in \[−𝛾1 + 1, 𝛾1]. -pub(crate) fn bitpack_gamma1( - z: &Polynomial, - out: &mut [u8; POLY_Z_PACKED_LEN], -) { +/// The destination is a slice rather than a `P::PolyZPacked`: the only caller writes straight into +/// its window of the signature buffer, which is chunked at runtime because the chunk size +/// `P::POLY_Z_PACKED_LEN` cannot be a const generic argument. +pub(crate) fn bitpack_gamma1(z: &Polynomial, out: &mut [u8]) { + debug_assert_eq!(out.len(), P::POLY_Z_PACKED_LEN); out.fill(0); let mut t: [u32; 4] = [0; 4]; - match GAMMA1 { - MLDSA44_GAMMA1 => { + match P::gamma1 { + // MLDSA-44 + GAMMA1_2_POW_17 => { for i in 0..N / 4 { - t[0] = (GAMMA1 - z[4 * i]) as u32; - t[1] = (GAMMA1 - z[4 * i + 1]) as u32; - t[2] = (GAMMA1 - z[4 * i + 2]) as u32; - t[3] = (GAMMA1 - z[4 * i + 3]) as u32; + t[0] = (P::gamma1 - z[4 * i]) as u32; + t[1] = (P::gamma1 - z[4 * i + 1]) as u32; + t[2] = (P::gamma1 - z[4 * i + 2]) as u32; + t[3] = (P::gamma1 - z[4 * i + 3]) as u32; out[9 * i] = t[0] as u8; out[9 * i + 1] = (t[0] >> 8) as u8; @@ -189,11 +183,11 @@ pub(crate) fn bitpack_gamma1( out[9 * i + 8] = (t[3] >> 10) as u8; } } - // MLDSA-65 and 87 have the same GAMMA1 value - MLDSA65_GAMMA1 => { + // MLDSA-65 and -87 have the same GAMMA1 value + GAMMA1_2_POW_19 => { for i in 0..N / 2 { - t[0] = (GAMMA1 - z[2 * i]) as u32; - t[1] = (GAMMA1 - z[2 * i + 1]) as u32; + t[0] = (P::gamma1 - z[2 * i]) as u32; + t[1] = (P::gamma1 - z[2 * i + 1]) as u32; out[5 * i] = t[0] as u8; out[5 * i + 1] = (t[0] >> 8) as u8; @@ -215,8 +209,6 @@ pub(crate) fn bitpack_gamma1( /// /// Note: caller is responsible for ensuring correct input array size pub(crate) fn simple_bit_unpack_t1(v: &[u8; POLY_T1PACKED_LEN]) -> Polynomial { - // debug_assert_eq!(v.len(), POLY_T1PACKED_LEN); - let mut w = Polynomial::new(); for i in 0..N / 4 { @@ -239,10 +231,10 @@ pub(crate) fn simple_bit_unpack_t1(v: &[u8; POLY_T1PACKED_LEN]) -> Polynomial { // `match ETA` folds away per monomorphization (ETA is a const generic), so ETA = 2 // and ETA = 4 each compile to just their own arm, leaving no dispatch at runtime. #[inline(always)] -pub(crate) fn bit_unpack_eta_out(v: &[u8], w: &mut Polynomial) { - debug_assert_eq!(v.len(), bitlen_eta(ETA)); +pub(crate) fn bit_unpack_eta_out(v: &[u8], w: &mut Polynomial) { + debug_assert_eq!(v.len(), P::POLY_ETA_PACKED_LEN); - match ETA { + match P::eta { // MLDSA44 and MLDSA87 2 => { let eta: i32 = 2; @@ -291,11 +283,12 @@ pub(crate) fn bit_unpack_eta_out(v: &[u8], w: &mut Polynomial) // `match ETA` folds away per monomorphization (ETA is a const generic), so ETA = 2 // and ETA = 4 each compile to just their own arm, leaving no dispatch at runtime. #[inline(always)] -pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { +pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { let mut w = Polynomial::new(); - match GAMMA1 { - MLDSA44_GAMMA1 => { + match P::gamma1 { + // MLDSA-44 + GAMMA1_2_POW_17 => { // const gamma1: i32 = 1<<17; for i in 0..N / 4 { w[4 * i] = (((v[9 * i] as i32) | ((v[9 * i + 1] as i32) << 8)) @@ -311,14 +304,14 @@ pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { | ((v[9 * i + 8] as i32) << 10)) & 0x3FFFF; - w[4 * i] = GAMMA1 - w[4 * i]; - w[4 * i + 1] = GAMMA1 - w[4 * i + 1]; - w[4 * i + 2] = GAMMA1 - w[4 * i + 2]; - w[4 * i + 3] = GAMMA1 - w[4 * i + 3]; + w[4 * i] = P::gamma1 - w[4 * i]; + w[4 * i + 1] = P::gamma1 - w[4 * i + 1]; + w[4 * i + 2] = P::gamma1 - w[4 * i + 2]; + w[4 * i + 3] = P::gamma1 - w[4 * i + 3]; } } - // MLDSA-65 and 87 have the same GAMMA1 value - MLDSA65_GAMMA1 => { + // MLDSA-65 and -87 have the same GAMMA1 value + GAMMA1_2_POW_19 => { // const gamma1: i32 = 1<<19; for i in 0..N / 2 { w[2 * i] = (((v[5 * i] as i32) | ((v[5 * i + 1] as i32) << 8)) @@ -328,8 +321,8 @@ pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { | ((v[5 * i + 4] as i32) << 12)) & 0xFFFFF; - w[2 * i] = GAMMA1 - w[2 * i]; - w[2 * i + 1] = GAMMA1 - w[2 * i + 1]; + w[2 * i] = P::gamma1 - w[2 * i]; + w[2 * i + 1] = P::gamma1 - w[2 * i + 1]; } } _ => { @@ -341,51 +334,40 @@ pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { } /// Part of unpacking the sig value -pub(crate) fn unpack_c_tilde(sig: &[u8]) -> &[u8; LAMBDA_over_4] { - sig[..LAMBDA_over_4].try_into().unwrap() +pub(crate) fn unpack_c_tilde(sig: &[u8]) -> P::SigCTilde { + let mut c_tilde = ::ZEROED; + c_tilde.as_mut().copy_from_slice(&sig[..P::C_TILDE_LEN]); + c_tilde } + /// Part of unpacking the sig value -pub(crate) fn unpack_z_row< - const GAMMA1: i32, - const GAMMA1_MINUS_BETA: i32, - const LAMBDA_over_4: usize, - const POLY_Z_PACKED_LEN: usize, - const SIG_LEN: usize, ->( +pub(crate) fn unpack_z_row( idx: usize, sig: &[u8; SIG_LEN], ) -> Result { - // assert: idx < l, but here there is no access to l + debug_assert!(idx < P::l); // skip to the start of the z's - let pos = LAMBDA_over_4; - let z = bit_unpack_gamma1::( - &sig[pos + idx * POLY_Z_PACKED_LEN..pos + (idx + 1) * POLY_Z_PACKED_LEN], + let pos = P::C_TILDE_LEN; + let z = bit_unpack_gamma1::

( + &sig[pos + idx * P::POLY_Z_PACKED_LEN..pos + (idx + 1) * P::POLY_Z_PACKED_LEN], ); // Perform the norm check from // Alg 8; Line 13 (first half) return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] - if z.check_norm::() { Err(()) } else { Ok(z) } + if z.check_norm(P::gamma1_minus_beta) { Err(()) } else { Ok(z) } } /// Part of unpacking the sig value -pub(crate) fn unpack_h_row< - const GAMMA1: i32, - const k: usize, - const l: usize, - const OMEGA: i32, - const LAMBDA_over_4: usize, - const POLY_Z_PACKED_LEN: usize, - const SIG_LEN: usize, ->( +pub(crate) fn unpack_h_row( row: usize, sig: &[u8; SIG_LEN], ) -> Option { - debug_assert!(row < k); + debug_assert!(row < P::k); let mut h = Polynomial::new(); // skip over the other stuff in the encoded sig value - let pos = LAMBDA_over_4 + l * POLY_Z_PACKED_LEN; + let pos = P::C_TILDE_LEN + P::l * P::POLY_Z_PACKED_LEN; // This inlines Algorithm 21 HintBitUnpack(𝑦) @@ -394,15 +376,15 @@ pub(crate) fn unpack_h_row< // let mut idx = 0usize; // This row calc is a bit weird because technically it's supposed to be done at the end // of the previous loop - let idx = if row == 0 { 0 } else { sig[pos + OMEGA as usize + row - 1] as usize }; + let idx = if row == 0 { 0 } else { sig[pos + P::omega as usize + row - 1] as usize }; // 3: for 𝑖 from 0 to 𝑘 − 1 do // ▷ reconstruct 𝐡[𝑖] // for i in 0..k { // 4: if 𝑦[𝜔 + 𝑖] < Index or 𝑦[𝜔 + 𝑖] > 𝜔 then return ⊥ // mutants note: don't have test vectors that exercise this condition - if sig[pos + (OMEGA as usize) + row] < (idx as u8) - || sig[pos + (OMEGA as usize) + row] > OMEGA as u8 + if sig[pos + (P::omega as usize) + row] < (idx as u8) + || sig[pos + (P::omega as usize) + row] > P::omega as u8 { return None; } @@ -410,7 +392,7 @@ pub(crate) fn unpack_h_row< // 6: First ← Index // 7: while Index < 𝑦[𝜔 + 𝑖] do // ▷ 𝑦[𝜔 + 𝑖] says how far one can advance Index - for j in idx..sig[pos + OMEGA as usize + row] as usize { + for j in idx..sig[pos + P::omega as usize + row] as usize { // 8: if Index > First then // 9: if 𝑦[Index − 1] ≥ 𝑦[Index] then return ⊥ // ▷ malformed input @@ -427,9 +409,9 @@ pub(crate) fn unpack_h_row< // ▷ read any leftover bytes in the first 𝜔 bytes of 𝑦 for malformed (nonzero) bytes // mutants note: - if row == k - 1 { - let idx = sig[pos + OMEGA as usize + row] as usize; - for j in idx..OMEGA as usize { + if row == P::k - 1 { + let idx = sig[pos + P::omega as usize + row] as usize; + for j in idx..P::omega as usize { if sig[pos + j] != 0 { return None; } @@ -443,9 +425,7 @@ pub(crate) fn unpack_h_row< /// Samples a polynomial 𝑐 ∈ 𝑅 with coefficients from {−1, 0, 1} and Hamming weight 𝜏 ≤ 64. /// Input: A seed 𝜌 ∈ 𝔹𝜆/4 /// Output: A polynomial 𝑐 in 𝑅. -pub(crate) fn sample_in_ball( - rho: &[u8; LAMBDA_over_4], -) -> Polynomial { +pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 1: 𝑐 ← 0 let mut c = Polynomial::new(); @@ -453,7 +433,7 @@ pub(crate) fn sample_in_ball( // 3: ctx ← H.Absorb(ctx, 𝜌) // 4: (ctx, 𝑠) ← H.Squeeze(ctx, 8) let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); + h.absorb(rho.as_ref()).expect("absorb before squeeze is infallible"); let mut s = [0u8; 8]; h.squeeze_out(&mut s); @@ -469,7 +449,7 @@ pub(crate) fn sample_in_ball( // let mut pos = 8; // let mut b; let mut j = [0u8]; - for i in (N - TAU as usize)..N { + for i in (N - P::tau as usize)..N { // 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. @@ -557,7 +537,7 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { /// This is supposed to take a rho: [u8; 66], which is: 𝜌||IntegerToBytes(𝑠, 1)||IntegerToBytes(𝑟, 1) /// but to avoid needing to copy bytes and allocate more memory, /// here that is split into a [u8;64] and a [u8;2] -pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) -> Polynomial { +pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) -> Polynomial { let mut a = Polynomial::new(); let mut j: usize = 0; let mut h = H::new(); @@ -574,8 +554,8 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2] let mut idx: usize = 0; while j < N { - let z0 = coeff_from_half_byte::(z_arr[idx] & 0x0F); // equiv to % 16 (but faster, and more importantly, constant-time) - let z1 = coeff_from_half_byte::(z_arr[idx] >> 4); // equiv to div_floor(16) (but faster, and more importantly, constant-time) + let z0 = coeff_from_half_byte::

(z_arr[idx] & 0x0F); // equiv to % 16 (but faster, and more importantly, constant-time) + let z1 = coeff_from_half_byte::

(z_arr[idx] >> 4); // equiv to div_floor(16) (but faster, and more importantly, constant-time) if z0.is_ok() { a[j] = z0.unwrap(); @@ -600,21 +580,19 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2] /// Samples a vector 𝐲 ∈ 𝑅ℓ such that each polynomial 𝐲[𝑟] has coefficients between −𝛾1 + 1 and 𝛾1. /// Input: A seed 𝜌 ∈ 𝔹64 and a nonnegative integer 𝜇. /// Output: Vector 𝐲 ∈ 𝑅ℓ . -pub(crate) fn expand_mask_poly( - rho: &[u8; 64], - nonce: u16, -) -> Polynomial { +pub(crate) fn expand_mask_poly(rho: &[u8; 64], nonce: u16) -> Polynomial { // 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) // ▷ 𝛾1 is always a power of 2 // 3: 𝜌′ ← 𝜌||IntegerToBytes(𝜇 + 𝑟, 2) - // 32c = GAMMA1_MASK_LEN; // 4: 𝑣 ← H(𝜌′, 32𝑐) + // 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"); - let mut v = [0u8; GAMMA1_MASK_LEN]; - h.squeeze_out(&mut v); - bit_unpack_gamma1::(&v) + let mut v = ::ZEROED; + h.squeeze_out(v.as_mut()); + bit_unpack_gamma1::

(v.as_ref()) } /// Algorithm 35 Power2Round(𝑟) @@ -657,7 +635,7 @@ fn test_power_2_round() { // the hope here is that the compiler will aggressively inline this function, // and optimize away the branching. #[inline(always)] -pub(crate) fn decompose(r: i32) -> (i32, i32) { +pub(crate) fn decompose(r: i32) -> (i32, i32) { // 1: 𝑟+ ← 𝑟 mod 𝑞 // 2: 𝑟0 ← 𝑟+ mod±(2𝛾2) // 3: if 𝑟+ − 𝑟0 = 𝑞 − 1 then @@ -672,14 +650,15 @@ pub(crate) fn decompose(r: i32) -> (i32, i32) { let mut r1: i32; let mut r0 = (r + 127) >> 7; - match GAMMA2 { - MLDSA44_GAMMA2 => { + match P::gamma2 { + // MLDSO-44 + GAMMA2_Q_MINUS_1_OVER_88 => { // (q - 1) / 88 r0 = (r0 * 11275 + (1 << 23)) >> 24; r0 ^= ((43 - r0) >> 31) & r0; } - // ML-DSA65 and 87 have the same GAMMA2 - MLDSA65_GAMMA2 => { + // ML-DSA-65 and -87 have the same GAMMA2 + GAMMA2_Q_MINUS_1_OVER_32 => { // (q - 1) / 32; r0 = (r0 * 1025 + (1 << 21)) >> 22; r0 &= 15; @@ -690,7 +669,7 @@ pub(crate) fn decompose(r: i32) -> (i32, i32) { } } - r1 = r - r0 * 2 * GAMMA2; + r1 = r - r0 * 2 * P::gamma2; // mutants note: the choice of (q - 1) is a bit arbitrary in that after doing the bit-shifting, // this seems to work out mathematically equivalent to doing q/2, or (q+3)/2, but here it is left as (q-1)/2 @@ -704,10 +683,10 @@ pub(crate) fn decompose(r: i32) -> (i32, i32) { /// Returns 𝑟1 from the output of Decompose (𝑟). /// Input: 𝑟 ∈ ℤ𝑞. /// Output: Integer 𝑟1. -pub(crate) fn high_bits(r: i32) -> i32 { +pub(crate) fn high_bits(r: i32) -> i32 { // 1: (𝑟1, 𝑟0) ← Decompose(𝑟) // 2: return 𝑟1 - let (r1, _) = decompose::(r); + let (r1, _) = decompose::

(r); r1 } @@ -715,10 +694,10 @@ pub(crate) fn high_bits(r: i32) -> i32 { /// Returns 𝑟0 from the output of Decompose (𝑟). /// Input: 𝑟 ∈ ℤ𝑞. /// Output: Integer 𝑟0. -pub(crate) fn low_bits(r: i32) -> i32 { +pub(crate) fn low_bits(r: i32) -> i32 { // 1: (𝑟1, 𝑟0) ← Decompose(𝑟) // 2: return 𝑟0 - let (_, r0) = decompose::(r); + let (_, r0) = decompose::

(r); r0 } @@ -726,27 +705,28 @@ pub(crate) fn low_bits(r: i32) -> i32 { /// Computes hint bit indicating whether adding 𝑧 to 𝑟 alters the high bits of 𝑟. /// Input: 𝑧, 𝑟 ∈ ℤ𝑞. /// Output: Boolean. -pub(crate) fn make_hint(z: i32, r: i32) -> i32 { +pub(crate) fn make_hint(z: i32, r: i32) -> i32 { + // Naïve implementation: // // 1: 𝑟1 ← HighBits(𝑟) - // let r1 = high_bits::(r); + // let r1 = high_bits::

(r); // // // 2: 𝑣1 ← HighBits(𝑟 + 𝑧) - // let v1 = high_bits::(r + z); + // let v1 = high_bits::

(r + z); // // // 3: return [[𝑟1 ≠ 𝑣1]] // if r1 != v1 { 1 } else { 0 } // By the powers of someone much more clever than me, this is equivalent. // mutants note: we do not have KATs that exercise all branches of this if - if z <= GAMMA2 || z > q - GAMMA2 || (z == q - GAMMA2 && r == 0) { 0 } else { 1 } + if z <= P::gamma2 || z > q - P::gamma2 || (z == q - P::gamma2 && r == 0) { 0 } else { 1 } } /// Algorithm 40 UseHint(ℎ, 𝑟) /// Returns the high bits of 𝑟 adjusted according to hint ℎ. /// Input: Boolean ℎ, 𝑟 ∈ ℤ𝑞. /// Output: 𝑟1 ∈ ℤ with 0 ≤ 𝑟1 ≤ (𝑞−1) / 2*gamma2). -pub(super) fn use_hint(a: i32, hint: i32) -> i32 { - let (a0, a1) = decompose::(a); +pub(super) fn use_hint(a: i32, hint: i32) -> i32 { + let (a0, a1) = decompose::

(a); if hint == 0 { return a0; @@ -754,8 +734,9 @@ pub(super) fn use_hint(a: i32, hint: i32) -> i32 { debug_assert!(hint == 1); - match GAMMA2 { - MLDSA44_GAMMA2 => { + match P::gamma2 { + // MLDSA-44 + GAMMA2_Q_MINUS_1_OVER_88 => { // mutants note: this passes unit tests if it's a1 >= 0 // it is left like this because it matches the spec if a1 > 0 { @@ -764,8 +745,8 @@ pub(super) fn use_hint(a: i32, hint: i32) -> i32 { if a0 == 0 { 43 } else { a0 - 1 } } } - // ML-DSA65 and 87 have the same GAMMA2 - MLDSA65_GAMMA2 => { + // ML-DSA65 and -87 have the same GAMMA2 + GAMMA2_Q_MINUS_1_OVER_32 => { // mutants note: this passes unit tests if it's a0 >= 0 // it is left like this because it matches the spec if a1 > 0 { (a0 + 1) & 15 } else { (a0 - 1) & 15 } diff --git a/crypto/mldsa-lowmemory/src/hash_mldsa.rs b/crypto/mldsa-lowmemory/src/hash_mldsa.rs index 33ccdff1..9b8599d7 100644 --- a/crypto/mldsa-lowmemory/src/hash_mldsa.rs +++ b/crypto/mldsa-lowmemory/src/hash_mldsa.rs @@ -66,29 +66,15 @@ //! But a simple [`HashMLDSA::keygen`] is provided. use crate::mldsa::{H, MLDSA_MU_LEN, MLDSA_RND_LEN, MLDSATrait}; -use crate::mldsa::{ - MLDSA44_BETA, MLDSA44_C_TILDE, MLDSA44_ETA, MLDSA44_FULL_SK_LEN, MLDSA44_GAMMA1, - MLDSA44_GAMMA1_MASK_LEN, MLDSA44_GAMMA1_MINUS_BETA, MLDSA44_GAMMA2, MLDSA44_GAMMA2_MINUS_BETA, - MLDSA44_LAMBDA, MLDSA44_LAMBDA_over_4, MLDSA44_OMEGA, MLDSA44_PK_LEN, - MLDSA44_POLY_W1_PACKED_LEN, MLDSA44_POLY_Z_PACKED_LEN, MLDSA44_S1_PACKED_LEN, - MLDSA44_S2_PACKED_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN, MLDSA44_TAU, MLDSA44_k, MLDSA44_l, -}; -use crate::mldsa::{MLDSA44_T1_PACKED_LEN, MLDSA65_T1_PACKED_LEN, MLDSA87_T1_PACKED_LEN}; -use crate::mldsa::{ - MLDSA65_BETA, MLDSA65_C_TILDE, MLDSA65_ETA, MLDSA65_FULL_SK_LEN, MLDSA65_GAMMA1, - MLDSA65_GAMMA1_MASK_LEN, MLDSA65_GAMMA1_MINUS_BETA, MLDSA65_GAMMA2, MLDSA65_GAMMA2_MINUS_BETA, - MLDSA65_LAMBDA, MLDSA65_LAMBDA_over_4, MLDSA65_OMEGA, MLDSA65_PK_LEN, - MLDSA65_POLY_W1_PACKED_LEN, MLDSA65_POLY_Z_PACKED_LEN, MLDSA65_S1_PACKED_LEN, - MLDSA65_S2_PACKED_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN, MLDSA65_TAU, MLDSA65_k, MLDSA65_l, -}; -use crate::mldsa::{ - MLDSA87_BETA, MLDSA87_C_TILDE, MLDSA87_ETA, MLDSA87_FULL_SK_LEN, MLDSA87_GAMMA1, - MLDSA87_GAMMA1_MASK_LEN, MLDSA87_GAMMA1_MINUS_BETA, MLDSA87_GAMMA2, MLDSA87_GAMMA2_MINUS_BETA, - MLDSA87_LAMBDA, MLDSA87_LAMBDA_over_4, MLDSA87_OMEGA, MLDSA87_PK_LEN, - MLDSA87_POLY_W1_PACKED_LEN, MLDSA87_POLY_Z_PACKED_LEN, MLDSA87_S1_PACKED_LEN, - MLDSA87_S2_PACKED_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN, MLDSA87_TAU, MLDSA87_k, MLDSA87_l, -}; +use crate::mldsa::{MLDSA44_FULL_SK_LEN, MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN}; +use crate::mldsa::{MLDSA65_FULL_SK_LEN, MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN}; +use crate::mldsa::{MLDSA87_FULL_SK_LEN, MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN}; use crate::mldsa_keys::{MLDSAPrivateKeyInternalTrait, MLDSAPublicKeyInternalTrait}; +use crate::params::{ + HashMLDSA44_with_SHA256Params, HashMLDSA44_with_SHA512Params, HashMLDSA65_with_SHA256Params, + HashMLDSA65_with_SHA512Params, HashMLDSA87_with_SHA256Params, HashMLDSA87_with_SHA512Params, + HashMLDSAParams, +}; use crate::{ MLDSA, MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyTrait, MLDSAPublicKeyTrait, @@ -100,9 +86,7 @@ use bouncycastle_core::traits::{ SignatureVerifier, Signer, XOF, }; use bouncycastle_rng::HashDRBG_SHA512; -use bouncycastle_sha2::{SHA256, SHA512}; use core::marker::PhantomData; - // Imports needed only for docs #[allow(unused_imports)] use crate::mldsa::MuBuilder; @@ -124,153 +108,73 @@ pub const HASH_ML_DSA_87_WITH_SHA512_NAME: &str = "HashML-DSA-87_with_SHA512"; /*** Pub Types ***/ -/// The HashML-DSA-44_with_SHA512 signature algorithm. +impl< + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, + const PH_LEN: usize, + const PK_LEN: usize, + const SK_LEN: usize, + const FULL_SK_LEN: usize, + const SIG_LEN: usize, +> Algorithm for HashMLDSA +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + +/// The HashML-DSA-44_with_SHA256 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA44_with_SHA256 = HashMLDSA< - SHA256, - 32, + HashMLDSA44_with_SHA256Params, + MLDSA44PublicKey, + MLDSA44PrivateKey, + { HashMLDSA44_with_SHA256Params::PH_LEN }, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_FULL_SK_LEN, MLDSA44_SIG_LEN, - MLDSA44PublicKey, - MLDSA44PrivateKey, - MLDSA44_TAU, - MLDSA44_LAMBDA, - MLDSA44_GAMMA1, - MLDSA44_GAMMA2, - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, - MLDSA44_BETA, - MLDSA44_OMEGA, - MLDSA44_C_TILDE, - MLDSA44_POLY_Z_PACKED_LEN, - MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_S1_PACKED_LEN, - MLDSA44_S2_PACKED_LEN, - MLDSA44_T1_PACKED_LEN, - MLDSA44_LAMBDA_over_4, - MLDSA44_GAMMA1_MINUS_BETA, - MLDSA44_GAMMA2_MINUS_BETA, - MLDSA44_GAMMA1_MASK_LEN, >; -impl Algorithm for HashMLDSA44_with_SHA256 { - const ALG_NAME: &'static str = HASH_ML_DSA_44_with_SHA256_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} - /// The HashML-DSA-65_with_SHA256 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA65_with_SHA256 = HashMLDSA< - SHA256, - 32, + HashMLDSA65_with_SHA256Params, + MLDSA65PublicKey, + MLDSA65PrivateKey, + { HashMLDSA65_with_SHA256Params::PH_LEN }, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_FULL_SK_LEN, MLDSA65_SIG_LEN, - MLDSA65PublicKey, - MLDSA65PrivateKey, - MLDSA65_TAU, - MLDSA65_LAMBDA, - MLDSA65_GAMMA1, - MLDSA65_GAMMA2, - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, - MLDSA65_BETA, - MLDSA65_OMEGA, - MLDSA65_C_TILDE, - MLDSA65_POLY_Z_PACKED_LEN, - MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_S1_PACKED_LEN, - MLDSA65_S2_PACKED_LEN, - MLDSA65_T1_PACKED_LEN, - MLDSA65_LAMBDA_over_4, - MLDSA65_GAMMA1_MINUS_BETA, - MLDSA65_GAMMA2_MINUS_BETA, - MLDSA65_GAMMA1_MASK_LEN, >; -impl Algorithm for HashMLDSA65_with_SHA256 { - const ALG_NAME: &'static str = HASH_ML_DSA_65_WITH_SHA256_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} - /// The HashML-DSA-87_with_SHA256 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA87_with_SHA256 = HashMLDSA< - SHA256, - 32, + HashMLDSA87_with_SHA256Params, + MLDSA87PublicKey, + MLDSA87PrivateKey, + { HashMLDSA87_with_SHA256Params::PH_LEN }, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_FULL_SK_LEN, MLDSA87_SIG_LEN, - MLDSA87PublicKey, - MLDSA87PrivateKey, - MLDSA87_TAU, - MLDSA87_LAMBDA, - MLDSA87_GAMMA1, - MLDSA87_GAMMA2, - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, - MLDSA87_BETA, - MLDSA87_OMEGA, - MLDSA87_C_TILDE, - MLDSA87_POLY_Z_PACKED_LEN, - MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_S1_PACKED_LEN, - MLDSA87_S2_PACKED_LEN, - MLDSA87_T1_PACKED_LEN, - MLDSA87_LAMBDA_over_4, - MLDSA87_GAMMA1_MINUS_BETA, - MLDSA87_GAMMA2_MINUS_BETA, - MLDSA87_GAMMA1_MASK_LEN, >; -impl Algorithm for HashMLDSA87_with_SHA256 { - const ALG_NAME: &'static str = HASH_ML_DSA_87_with_SHA256_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} - /// The HashML-DSA-44_with_SHA512 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA44_with_SHA512 = HashMLDSA< - SHA512, - 64, + HashMLDSA44_with_SHA512Params, + MLDSA44PublicKey, + MLDSA44PrivateKey, + { HashMLDSA44_with_SHA512Params::PH_LEN }, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_FULL_SK_LEN, MLDSA44_SIG_LEN, - MLDSA44PublicKey, - MLDSA44PrivateKey, - MLDSA44_TAU, - MLDSA44_LAMBDA, - MLDSA44_GAMMA1, - MLDSA44_GAMMA2, - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, - MLDSA44_BETA, - MLDSA44_OMEGA, - MLDSA44_C_TILDE, - MLDSA44_POLY_Z_PACKED_LEN, - MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_S1_PACKED_LEN, - MLDSA44_S2_PACKED_LEN, - MLDSA44_T1_PACKED_LEN, - MLDSA44_LAMBDA_over_4, - MLDSA44_GAMMA1_MINUS_BETA, - MLDSA44_GAMMA2_MINUS_BETA, - MLDSA44_GAMMA1_MASK_LEN, >; - -impl Algorithm for HashMLDSA44_with_SHA512 { - const ALG_NAME: &'static str = HASH_ML_DSA_44_with_SHA512_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} /// Assigned by NIST in the Computer Security Objects Register: id-hash-ml-dsa-44-with-sha512 { sigAlgs 32 } impl AlgorithmOID for HashMLDSA44_with_SHA512 { const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 32]; @@ -281,39 +185,15 @@ impl AlgorithmOID for HashMLDSA44_with_SHA512 { /// The HashML-DSA-65_with_SHA512 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA65_with_SHA512 = HashMLDSA< - SHA512, - 64, + HashMLDSA65_with_SHA512Params, + MLDSA65PublicKey, + MLDSA65PrivateKey, + { HashMLDSA65_with_SHA512Params::PH_LEN }, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_FULL_SK_LEN, MLDSA65_SIG_LEN, - MLDSA65PublicKey, - MLDSA65PrivateKey, - MLDSA65_TAU, - MLDSA65_LAMBDA, - MLDSA65_GAMMA1, - MLDSA65_GAMMA2, - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, - MLDSA65_BETA, - MLDSA65_OMEGA, - MLDSA65_C_TILDE, - MLDSA65_POLY_Z_PACKED_LEN, - MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_S1_PACKED_LEN, - MLDSA65_S2_PACKED_LEN, - MLDSA65_T1_PACKED_LEN, - MLDSA65_LAMBDA_over_4, - MLDSA65_GAMMA1_MINUS_BETA, - MLDSA65_GAMMA2_MINUS_BETA, - MLDSA65_GAMMA1_MASK_LEN, >; - -impl Algorithm for HashMLDSA65_with_SHA512 { - const ALG_NAME: &'static str = HASH_ML_DSA_65_WITH_SHA512_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; -} /// Assigned by NIST in the Computer Security Objects Register: id-hash-ml-dsa-65-with-sha512 { sigAlgs 33 } impl AlgorithmOID for HashMLDSA65_with_SHA512 { const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 33]; @@ -324,39 +204,15 @@ impl AlgorithmOID for HashMLDSA65_with_SHA512 { /// The HashML-DSA-87_with_SHA512 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA87_with_SHA512 = HashMLDSA< - SHA512, - 64, + HashMLDSA87_with_SHA512Params, + MLDSA87PublicKey, + MLDSA87PrivateKey, + { HashMLDSA87_with_SHA512Params::PH_LEN }, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_FULL_SK_LEN, MLDSA87_SIG_LEN, - MLDSA87PublicKey, - MLDSA87PrivateKey, - MLDSA87_TAU, - MLDSA87_LAMBDA, - MLDSA87_GAMMA1, - MLDSA87_GAMMA2, - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, - MLDSA87_BETA, - MLDSA87_OMEGA, - MLDSA87_C_TILDE, - MLDSA87_POLY_Z_PACKED_LEN, - MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_S1_PACKED_LEN, - MLDSA87_S2_PACKED_LEN, - MLDSA87_T1_PACKED_LEN, - MLDSA87_LAMBDA_over_4, - MLDSA87_GAMMA1_MINUS_BETA, - MLDSA87_GAMMA2_MINUS_BETA, - MLDSA87_GAMMA1_MASK_LEN, >; - -impl Algorithm for HashMLDSA87_with_SHA512 { - const ALG_NAME: &'static str = HASH_ML_DSA_87_WITH_SHA512_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; -} /// Assigned by NIST in the Computer Security Objects Register: id-hash-ml-dsa-87-with-sha512 { sigAlgs 34 } impl AlgorithmOID for HashMLDSA87_with_SHA512 { const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 34]; @@ -371,55 +227,17 @@ impl AlgorithmOID for HashMLDSA87_with_SHA512 { /// by specifying the hash function to use (in the verifier), and specifying the bytes of the OID to /// to use as its domain separator in constructing the message representative M'. pub struct HashMLDSA< - HASH: Hash + AlgorithmOID + Default, - const HASH_LEN: usize, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, + const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > { - _phantom: PhantomData<(PK, SK)>, + _phantom: PhantomData<(P, PK, SK)>, signer_rnd: Option<[u8; MLDSA_RND_LEN]>, @@ -433,7 +251,7 @@ pub struct HashMLDSA< pk: Option, /// Hash function instance for streaming message hashing - hash: HASH, + hash: P::PreHash, /// Since HashML-DSA does message buffering in the external pre-hash, not in mu, /// this needs to be saved for later @@ -442,83 +260,16 @@ pub struct HashMLDSA< } impl< - HASH: Hash + AlgorithmOID + Default, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> - HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> HashMLDSA { /// Generate a keypair, sourcing randomness from bouncycastle's default os-backed RNG. /// @@ -527,64 +278,12 @@ impl< /// Keys are interchangeable between MLDSA and HashMLDSA. /// Error condition: basically only on RNG failures. pub fn keygen() -> Result<(PK, SK), SignatureError> { - MLDSA::< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::keygen() + MLDSA::::keygen() } /// Imports a secret key from a seed. pub fn keygen_from_seed(seed: &KeyMaterial<32>) -> Result<(PK, SK), SignatureError> { - MLDSA::< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::keygen_internal(seed) + MLDSA::::keygen_internal(seed) } /// Algorithm 7 ML-DSA.Sign_internal(𝑠𝑘, 𝑀′, 𝑟𝑛𝑑) @@ -651,40 +350,15 @@ impl< 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(HASH::OID_DER).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"); let mut mu = [0u8; MLDSA_MU_LEN]; let bytes_written = h.squeeze_out(&mut mu); debug_assert_eq!(bytes_written, MLDSA_MU_LEN); // 24: 𝜎 ← ML-DSA.Sign_internal(𝑠𝑘, 𝑀', 𝑟𝑛𝑑) - let bytes_written = MLDSA::< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::sign_mu_deterministic_out(sk, &mu, rnd, output)?; + let bytes_written = MLDSA::::sign_mu_deterministic_out(sk, &mu, rnd, output)?; Ok(bytes_written) } @@ -725,7 +399,7 @@ impl< sk: None, seed: Some(seed.clone()), pk: None, - hash: HASH::default(), + hash: ::default(), ctx, ctx_len, }) @@ -733,83 +407,17 @@ impl< } impl< - HASH: Hash + AlgorithmOID + Default, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > Signer - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > + for HashMLDSA { /// Algorithm 4 HashML-DSA.Sign(𝑠𝑘, 𝑀 , 𝑐𝑡𝑥, PH) /// Generate a “pre-hash” ML-DSA signature. @@ -829,7 +437,7 @@ impl< output.fill(0); let mut ph_m = [0u8; PH_LEN]; - _ = HASH::default().hash_out(msg, &mut ph_m); + _ = ::default().hash_out(msg, &mut ph_m); Self::sign_ph_out(sk, &ph_m, ctx, output) } @@ -841,7 +449,7 @@ impl< sk: Some(sk.clone()), seed: None, pk: None, - hash: HASH::default(), + hash: ::default(), ctx, ctx_len, }) @@ -899,87 +507,21 @@ impl< } impl< - HASH: Hash + AlgorithmOID + Default, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > SignatureVerifier - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > + for HashMLDSA { fn verify(pk: &PK, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8]) -> Result<(), SignatureError> { let mut ph_m = [0u8; PH_LEN]; - _ = HASH::default().hash_out(msg, &mut ph_m); + _ = ::default().hash_out(msg, &mut ph_m); Self::verify_ph(pk, &ph_m, ctx, sig) } @@ -992,7 +534,7 @@ impl< sk: None, seed: None, pk: Some(pk.clone()), - hash: HASH::default(), + hash: ::default(), ctx, ctx_len, }) @@ -1013,83 +555,17 @@ impl< } impl< - HASH: Hash + AlgorithmOID + Default, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > PHSigner - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > + for HashMLDSA { fn sign_ph( sk: &SK, @@ -1121,83 +597,17 @@ impl< } impl< - HASH: Hash + AlgorithmOID + Default, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > PHSignatureVerifier - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > + for HashMLDSA { fn verify_ph( pk: &PK, @@ -1229,37 +639,14 @@ impl< 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(HASH::OID_DER).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"); let mut mu = [0u8; MLDSA_MU_LEN]; _ = h.squeeze_out(&mut mu); - MLDSA::< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::verify_mu(pk, &mu, sig_sized) + MLDSA::::verify_mu( + pk, &mu, sig_sized, + ) } } diff --git a/crypto/mldsa-lowmemory/src/lib.rs b/crypto/mldsa-lowmemory/src/lib.rs index 02b03d40..51a4780b 100644 --- a/crypto/mldsa-lowmemory/src/lib.rs +++ b/crypto/mldsa-lowmemory/src/lib.rs @@ -234,6 +234,7 @@ pub mod hash_mldsa; mod low_memory_helpers; pub mod mldsa; mod mldsa_keys; +mod params; mod polynomial; /*** Exported types ***/ diff --git a/crypto/mldsa-lowmemory/src/low_memory_helpers.rs b/crypto/mldsa-lowmemory/src/low_memory_helpers.rs index 81505ef5..7fb15a42 100644 --- a/crypto/mldsa-lowmemory/src/low_memory_helpers.rs +++ b/crypto/mldsa-lowmemory/src/low_memory_helpers.rs @@ -2,13 +2,11 @@ //! and other intermediate values by never holding the whole thing in memory at once, but re-constructing //! what it needs in pieces, which generally means handling the matrices and vectors row-wise or entry-wise. -use crate::aux_functions::{ - bit_unpack_eta_out, bitlen_eta, expand_mask_poly, rej_ntt_poly, unpack_z_row, -}; -use crate::mldsa::d; +use crate::aux_functions::{bit_unpack_eta_out, expand_mask_poly, rej_ntt_poly, unpack_z_row}; +use crate::params::MLDSAParams; use crate::polynomial::Polynomial; use bouncycastle_core::errors::SignatureError; -use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; #[inline(always)] pub(crate) fn expandA_elem(rho: &[u8; 32], i: usize, j: usize) -> Polynomial { @@ -17,19 +15,19 @@ pub(crate) fn expandA_elem(rho: &[u8; 32], i: usize, j: usize) -> Polynomial { /// Compute a row of the core signing operation /// Alg 7: 12: 𝐰 ← NTT−1(𝐀_hat ∘ NTT(𝐲)) -pub(crate) fn compute_w_row( +pub(crate) fn compute_w_row( rho: &[u8; 32], rho_p_p: &[u8; 64], kappa: u16, row: usize, ) -> Polynomial { - let mut y_hat = expand_mask_poly::(rho_p_p, kappa); + let mut y_hat = expand_mask_poly::

(rho_p_p, kappa); y_hat.ntt(); let mut acc = rej_ntt_poly(rho, &[0u8, row as u8]); acc.multiply_ntt(&y_hat); - for col in 1..l { - y_hat = expand_mask_poly::(rho_p_p, kappa + col as u16); + for col in 1..P::l { + y_hat = expand_mask_poly::

(rho_p_p, kappa + col as u16); y_hat.ntt(); let mut tmp = rej_ntt_poly(rho, &[col as u8, row as u8]); tmp.multiply_ntt(&y_hat); @@ -42,14 +40,7 @@ pub(crate) fn compute_w_row( +pub(crate) fn compute_wp_approx_row( rho: &[u8; 32], sig: &[u8; SIG_LEN], t1: &Polynomial, @@ -64,18 +55,13 @@ pub(crate) fn compute_wp_approx_row< // ) // ▷ 𝐰'_approx = 𝐀𝐳 − 𝑐𝐭1 ⋅ 2^𝑑 - let mut z_i = - unpack_z_row::( - 0, sig, - )?; + let mut z_i = unpack_z_row::(0, sig)?; z_i.ntt(); let mut Az_acc = rej_ntt_poly(rho, &[0u8, idx as u8]); Az_acc.multiply_ntt(&z_i); - for col in 1..l { - z_i = unpack_z_row::( - col, sig, - )?; + for col in 1..P::l { + z_i = unpack_z_row::(col, sig)?; z_i.ntt(); // [Optimization Note]: @@ -88,7 +74,7 @@ pub(crate) fn compute_wp_approx_row< let ct1 = compute_ct1(t1.clone(), c.clone()); fn compute_ct1(mut t1_i: Polynomial, mut c: Polynomial) -> Polynomial { - t1_i.shift_left::(); + t1_i.shift_left_d(); t1_i.ntt(); c.ntt(); t1_i.multiply_ntt(&c); @@ -103,18 +89,14 @@ pub(crate) fn compute_wp_approx_row< Ok(Az_acc) } -pub(crate) fn compute_z_component< - const GAMMA1: i32, - const GAMMA1_MASK_LEN: usize, - const GAMMA1_MINUS_BETA: i32, ->( +pub(crate) fn compute_z_component( s1: &Polynomial, rho_p_p: &[u8; 64], c_hat: &Polynomial, kappa: u16, col: usize, ) -> Result, SignatureError> { - let y = expand_mask_poly::(rho_p_p, kappa + col as u16); + let y = expand_mask_poly::

(rho_p_p, kappa + col as u16); let mut s1_hat = s1.clone(); s1_hat.ntt(); s1_hat.multiply_ntt(c_hat); @@ -123,10 +105,10 @@ pub(crate) fn compute_z_component< let mut z = cs1; z.add_ntt(&y); - if z.check_norm::() { Ok(None) } else { Ok(Some(z)) } + if z.check_norm(P::gamma1_minus_beta) { Ok(None) } else { Ok(Some(z)) } } -pub(crate) fn compute_w0cs2_component( +pub(crate) fn compute_w0cs2_component( s2: &Polynomial, w: &Polynomial, c_hat: &Polynomial, @@ -144,12 +126,12 @@ pub(crate) fn compute_w0cs2_component(); + w0cs2.low_bits::

(); w0cs2.sub(&cs2); - if w0cs2.check_norm::() { None } else { Some(w0cs2) } + if w0cs2.check_norm(P::gamma2_minus_beta) { None } else { Some(w0cs2) } } -pub(crate) fn compute_ct0_component( +pub(crate) fn compute_ct0_component( t0_row: &Polynomial, c_hat: &Polynomial, ) -> Option { @@ -159,18 +141,20 @@ pub(crate) fn compute_ct0_component( let mut ct0 = t0_hat; // rename ct0.inv_ntt(); - if ct0.check_norm::() { None } else { Some(ct0) } + if ct0.check_norm(P::gamma2) { None } else { Some(ct0) } } /// Unpack a single s value from the packed representation. -pub(crate) fn s_unpack( - s_packed: &Secret<[u8; S_PACKED_LEN]>, +/// +/// `B` is the packed buffer type, which is `P::S1Packed` or `P::S2Packed` depending on which of +/// the two secret vectors is being unpacked. +pub(crate) fn s_unpack>( + s_packed: &Secret, idx: usize, ) -> Polynomial { let mut s = Polynomial::new(); - bit_unpack_eta_out::( - &s_packed[idx * bitlen_eta(eta)..(idx + 1) * bitlen_eta(eta)], - &mut s, - ); + let packed = (**s_packed).as_ref(); + let width = P::POLY_ETA_PACKED_LEN; + bit_unpack_eta_out::

(&packed[idx * width..(idx + 1) * width], &mut s); s } diff --git a/crypto/mldsa-lowmemory/src/mldsa.rs b/crypto/mldsa-lowmemory/src/mldsa.rs index 87e618c0..b1658579 100644 --- a/crypto/mldsa-lowmemory/src/mldsa.rs +++ b/crypto/mldsa-lowmemory/src/mldsa.rs @@ -384,15 +384,14 @@ //! } //! ``` -use crate::aux_functions::{ - bitlen_eta, bitpack_gamma1, sample_in_ball, unpack_c_tilde, unpack_h_row, -}; +use crate::aux_functions::{bitpack_gamma1, sample_in_ball, unpack_c_tilde, unpack_h_row}; use crate::low_memory_helpers::{ compute_ct0_component, compute_w_row, compute_w0cs2_component, compute_wp_approx_row, compute_z_component, s_unpack, }; use crate::mldsa_keys::{MLDSAPrivateKeyInternalTrait, MLDSAPrivateKeyTrait}; use crate::mldsa_keys::{MLDSAPublicKeyInternalTrait, MLDSAPublicKeyTrait}; +use crate::params::{MLDSA44Params, MLDSA65Params, MLDSA87Params, MLDSAParams}; use crate::{ MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87PrivateKey, MLDSA87PublicKey, @@ -413,7 +412,7 @@ use crate::hash_mldsa; use bouncycastle_core::key_material::{KeyMaterial256, KeyMaterialTrait}; #[allow(unused_imports)] use bouncycastle_core::traits::{PHSignatureVerifier, PHSigner}; -use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; /*** Constants ***/ /// @@ -441,110 +440,34 @@ pub const MLDSA_SEED_LEN: usize = 32; pub(crate) const POLY_T0PACKED_LEN: usize = 416; pub(crate) const POLY_T1PACKED_LEN: usize = 320; -/* ML-DSA-44 params */ +/*** Re-exporting length constants that a caller will need instead of the entire Params objects which contains a bunch of internal algorithm detail ***/ -/// Length of the \[u8] holding a ML-DSA-44 public key. -pub const MLDSA44_PK_LEN: usize = 1312; -/// Length of the \[u8] holding a ML-DSA-44 private key, which in this implementation is just a 32-byte seed. -pub const MLDSA44_SK_LEN: usize = MLDSA_SEED_LEN; +/// Length of the \[u8] holding an ML-DSA-44 public key. +pub const MLDSA44_PK_LEN: usize = MLDSA44Params::PK_LEN; +/// Length of the \[u8] holding an ML-DSA-44 private key, which in this implementation is just a 32-byte seed. +pub const MLDSA44_SK_LEN: usize = MLDSA44Params::SK_LEN; /// The length of the FIPS representation of the private key, which can be produced by [`MLDSAPrivateKeyTrait::encode_full_sk`] -pub const MLDSA44_FULL_SK_LEN: usize = 2560; -/// Length of the \[u8] holding a ML-DSA-44 signature value. -pub const MLDSA44_SIG_LEN: usize = 2420; -pub(crate) const MLDSA44_TAU: i32 = 39; -pub(crate) const MLDSA44_LAMBDA: i32 = 128; -pub(crate) const MLDSA44_GAMMA1: i32 = 1 << 17; -pub(crate) const MLDSA44_GAMMA2: i32 = (q - 1) / 88; // mutants note: because of the bitshifting, the "- 1" ends up not mattering -pub(crate) const MLDSA44_k: usize = 4; -pub(crate) const MLDSA44_l: usize = 4; -pub(crate) const MLDSA44_ETA: usize = 2; -pub(crate) const MLDSA44_BETA: i32 = 78; -pub(crate) const MLDSA44_OMEGA: i32 = 80; - -// Useful derived values -pub(crate) const MLDSA44_C_TILDE: usize = 32; -pub(crate) const MLDSA44_POLY_Z_PACKED_LEN: usize = 576; -pub(crate) const MLDSA44_POLY_W1_PACKED_LEN: usize = 192; -pub(crate) const MLDSA44_S1_PACKED_LEN: usize = bitlen_eta(MLDSA44_ETA) * MLDSA44_l; // 384 bytes -pub(crate) const MLDSA44_S2_PACKED_LEN: usize = bitlen_eta(MLDSA44_ETA) * MLDSA44_k; // 384 bytes -pub(crate) const MLDSA44_T1_PACKED_LEN: usize = POLY_T1PACKED_LEN * MLDSA44_k; // 768 bytes -pub(crate) const MLDSA44_LAMBDA_over_4: usize = 128 / 4; -pub(crate) const MLDSA44_GAMMA1_MINUS_BETA: i32 = MLDSA44_GAMMA1 - MLDSA44_BETA; // mutants note: there is a test vector for this in the regular implementation, but its sk seed is not known here, so can't test it here. -pub(crate) const MLDSA44_GAMMA2_MINUS_BETA: i32 = MLDSA44_GAMMA2 - MLDSA44_BETA; // mutants note: there is a test vector for this in the regular implementation, but its sk seed is not known here, so can't test it here. - -// Alg 32 -// 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) -pub(crate) const MLDSA44_GAMMA1_MASK_LEN: usize = 576; // 32*(1 + bitlen (𝛾1 − 1) ) - -/* ML-DSA-65 params */ - -/// Length of the \[u8] holding a ML-DSA-65 public key. -pub const MLDSA65_PK_LEN: usize = 1952; -/// Length of the \[u8] holding a ML-DSA-65 private key, which in this implementation is just a 32-byte seed. -pub const MLDSA65_SK_LEN: usize = MLDSA_SEED_LEN; +pub const MLDSA44_FULL_SK_LEN: usize = MLDSA44Params::FULL_SK_LEN; +/// Length of the \[u8] holding an ML-DSA-44 signature value. +pub const MLDSA44_SIG_LEN: usize = MLDSA44Params::SIG_LEN; + +/// Length of the \[u8] holding an ML-DSA-65 public key. +pub const MLDSA65_PK_LEN: usize = MLDSA65Params::PK_LEN; +/// Length of the \[u8] holding an ML-DSA-65 private key, which in this implementation is just a 32-byte seed. +pub const MLDSA65_SK_LEN: usize = MLDSA65Params::SK_LEN; /// The length of the FIPS representation of the private key, which can be produced by [`MLDSAPrivateKeyTrait::encode_full_sk`] -pub const MLDSA65_FULL_SK_LEN: usize = 4032; -/// Length of the \[u8] holding a ML-DSA-65 signature value. -pub const MLDSA65_SIG_LEN: usize = 3309; -pub(crate) const MLDSA65_TAU: i32 = 49; -pub(crate) const MLDSA65_LAMBDA: i32 = 192; -pub(crate) const MLDSA65_GAMMA1: i32 = 1 << 19; -pub(crate) const MLDSA65_GAMMA2: i32 = (q - 1) / 32; // mutants note: because of the bitshifting, the "- 1" ends up not mattering -pub(crate) const MLDSA65_k: usize = 6; -pub(crate) const MLDSA65_l: usize = 5; -pub(crate) const MLDSA65_ETA: usize = 4; -pub(crate) const MLDSA65_BETA: i32 = 196; -pub(crate) const MLDSA65_OMEGA: i32 = 55; - -// Useful derived values -pub(crate) const MLDSA65_C_TILDE: usize = 48; -pub(crate) const MLDSA65_POLY_Z_PACKED_LEN: usize = 640; -pub(crate) const MLDSA65_POLY_W1_PACKED_LEN: usize = 128; -pub(crate) const MLDSA65_S1_PACKED_LEN: usize = bitlen_eta(MLDSA65_ETA) * MLDSA65_l; // 640 bytes -pub(crate) const MLDSA65_S2_PACKED_LEN: usize = bitlen_eta(MLDSA65_ETA) * MLDSA65_k; // 768 bytes -pub(crate) const MLDSA65_T1_PACKED_LEN: usize = POLY_T1PACKED_LEN * MLDSA65_k; // 1152 bytes -pub(crate) const MLDSA65_LAMBDA_over_4: usize = 192 / 4; -pub(crate) const MLDSA65_GAMMA1_MINUS_BETA: i32 = MLDSA65_GAMMA1 - MLDSA65_BETA; // mutants note: there is a test vector for this in the regular implementation, but its sk seed is not known here, so can't test it here. -pub(crate) const MLDSA65_GAMMA2_MINUS_BETA: i32 = MLDSA65_GAMMA2 - MLDSA65_BETA; // mutants note: there is a test vector for this in the regular implementation, but its sk seed is not known here, so can't test it here. - -// Alg 32 -// 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) -pub(crate) const MLDSA65_GAMMA1_MASK_LEN: usize = 640; - -/* ML-DSA-87 params */ - -/// Length of the \[u8] holding a ML-DSA-87 public key. -pub const MLDSA87_PK_LEN: usize = 2592; -/// Length of the \[u8] holding a ML-DSA-87 private key, which in this implementation is just a 32-byte seed. -pub const MLDSA87_SK_LEN: usize = MLDSA_SEED_LEN; +pub const MLDSA65_FULL_SK_LEN: usize = MLDSA65Params::FULL_SK_LEN; +/// Length of the \[u8] holding an ML-DSA-65 signature value. +pub const MLDSA65_SIG_LEN: usize = MLDSA65Params::SIG_LEN; + +/// Length of the \[u8] holding an ML-DSA-87 public key. +pub const MLDSA87_PK_LEN: usize = MLDSA87Params::PK_LEN; +/// Length of the \[u8] holding an ML-DSA-87 private key, which in this implementation is just a 32-byte seed. +pub const MLDSA87_SK_LEN: usize = MLDSA87Params::SK_LEN; /// The length of the FIPS representation of the private key, which can be produced by [`MLDSAPrivateKeyTrait::encode_full_sk`] -pub const MLDSA87_FULL_SK_LEN: usize = 4896; -/// Length of the \[u8] holding a ML-DSA-87 signature value. -pub const MLDSA87_SIG_LEN: usize = 4627; -pub(crate) const MLDSA87_TAU: i32 = 60; -pub(crate) const MLDSA87_LAMBDA: i32 = 256; -pub(crate) const MLDSA87_GAMMA1: i32 = 1 << 19; -pub(crate) const MLDSA87_GAMMA2: i32 = (q - 1) / 32; // mutants note: because of the bitshifting, the "- 1" ends up not mattering -pub(crate) const MLDSA87_k: usize = 8; -pub(crate) const MLDSA87_l: usize = 7; -pub(crate) const MLDSA87_ETA: usize = 2; -pub(crate) const MLDSA87_BETA: i32 = 120; -pub(crate) const MLDSA87_OMEGA: i32 = 75; - -// Useful derived values -pub(crate) const MLDSA87_C_TILDE: usize = 64; -pub(crate) const MLDSA87_POLY_Z_PACKED_LEN: usize = 640; -pub(crate) const MLDSA87_POLY_W1_PACKED_LEN: usize = 128; -pub(crate) const MLDSA87_S1_PACKED_LEN: usize = bitlen_eta(MLDSA87_ETA) * MLDSA87_l; // 672 bytes -pub(crate) const MLDSA87_S2_PACKED_LEN: usize = bitlen_eta(MLDSA87_ETA) * MLDSA87_k; // 768 bytes -pub(crate) const MLDSA87_T1_PACKED_LEN: usize = POLY_T1PACKED_LEN * MLDSA87_k; // 1024 bytes -pub(crate) const MLDSA87_LAMBDA_over_4: usize = 256 / 4; -pub(crate) const MLDSA87_GAMMA1_MINUS_BETA: i32 = MLDSA87_GAMMA1 - MLDSA87_BETA; // mutants note: there is a test vector for this in the regular implementation, but its sk seed is not known here, so can't test it here. -pub(crate) const MLDSA87_GAMMA2_MINUS_BETA: i32 = MLDSA87_GAMMA2 - MLDSA87_BETA; // mutants note: there is a test vector for this in the regular implementation, but its sk seed is not known here, so can't test it here. - -// Alg 32 -// 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) -pub(crate) const MLDSA87_GAMMA1_MASK_LEN: usize = 640; +pub const MLDSA87_FULL_SK_LEN: usize = MLDSA87Params::FULL_SK_LEN; +/// Length of the \[u8] holding an ML-DSA-87 signature value. +pub const MLDSA87_SIG_LEN: usize = MLDSA87Params::SIG_LEN; // Typedefs just to make the algorithms look more like the FIPS 204 sample code. pub(crate) type H = SHAKE256; @@ -554,175 +477,84 @@ pub(crate) type G = SHAKE128; /// The ML-DSA-44 algorithm. pub type MLDSA44 = MLDSA< + MLDSA44Params, + MLDSA44PublicKey, + MLDSA44PrivateKey, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_FULL_SK_LEN, MLDSA44_SIG_LEN, - MLDSA44PublicKey, - MLDSA44PrivateKey, - MLDSA44_TAU, - MLDSA44_LAMBDA, - MLDSA44_GAMMA1, - MLDSA44_GAMMA2, - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, - MLDSA44_BETA, - MLDSA44_OMEGA, - MLDSA44_C_TILDE, - MLDSA44_POLY_Z_PACKED_LEN, - MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_S1_PACKED_LEN, - MLDSA44_S2_PACKED_LEN, - MLDSA44_T1_PACKED_LEN, - MLDSA44_LAMBDA_over_4, - MLDSA44_GAMMA1_MINUS_BETA, - MLDSA44_GAMMA2_MINUS_BETA, - MLDSA44_GAMMA1_MASK_LEN, >; -impl Algorithm for MLDSA44 { - const ALG_NAME: &'static str = ML_DSA_44_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} -/// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 } -impl AlgorithmOID for MLDSA44 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 17]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11]; -} - /// The ML-DSA-65 algorithm. pub type MLDSA65 = MLDSA< + MLDSA65Params, + MLDSA65PublicKey, + MLDSA65PrivateKey, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_FULL_SK_LEN, MLDSA65_SIG_LEN, - MLDSA65PublicKey, - MLDSA65PrivateKey, - MLDSA65_TAU, - MLDSA65_LAMBDA, - MLDSA65_GAMMA1, - MLDSA65_GAMMA2, - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, - MLDSA65_BETA, - MLDSA65_OMEGA, - MLDSA65_C_TILDE, - MLDSA65_POLY_Z_PACKED_LEN, - MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_S1_PACKED_LEN, - MLDSA65_S2_PACKED_LEN, - MLDSA65_T1_PACKED_LEN, - MLDSA65_LAMBDA_over_4, - MLDSA65_GAMMA1_MINUS_BETA, - MLDSA65_GAMMA2_MINUS_BETA, - MLDSA65_GAMMA1_MASK_LEN, >; -impl Algorithm for MLDSA65 { - const ALG_NAME: &'static str = ML_DSA_65_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; -} -/// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-65 { sigAlgs 18 } -impl AlgorithmOID for MLDSA65 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 18]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12]; -} - /// The ML-DSA-87 algorithm. pub type MLDSA87 = MLDSA< + MLDSA87Params, + MLDSA87PublicKey, + MLDSA87PrivateKey, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_FULL_SK_LEN, MLDSA87_SIG_LEN, - MLDSA87PublicKey, - MLDSA87PrivateKey, - MLDSA87_TAU, - MLDSA87_LAMBDA, - MLDSA87_GAMMA1, - MLDSA87_GAMMA2, - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, - MLDSA87_BETA, - MLDSA87_OMEGA, - MLDSA87_C_TILDE, - MLDSA87_POLY_Z_PACKED_LEN, - MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_S1_PACKED_LEN, - MLDSA87_S2_PACKED_LEN, - MLDSA87_T1_PACKED_LEN, - MLDSA87_LAMBDA_over_4, - MLDSA87_GAMMA1_MINUS_BETA, - MLDSA87_GAMMA2_MINUS_BETA, - MLDSA87_GAMMA1_MASK_LEN, >; -impl Algorithm for MLDSA87 { - const ALG_NAME: &'static str = ML_DSA_87_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, + const PK_LEN: usize, + const SK_LEN: usize, + const FULL_SK_LEN: usize, + const SIG_LEN: usize, +> Algorithm for MLDSA +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -/// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-87 { sigAlgs 19 } -impl AlgorithmOID for MLDSA87 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 19]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13]; + +/// The OIDs NIST assigned in the Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 }, +/// id-ml-dsa-65 { sigAlgs 18 } and id-ml-dsa-87 { sigAlgs 19 }. As with [`Algorithm`], the values +/// belong to the parameter set, so one impl covers all three. +impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, + const PK_LEN: usize, + const SK_LEN: usize, + const FULL_SK_LEN: usize, + const SIG_LEN: usize, +> AlgorithmOID for MLDSA +{ + const OID: &'static [u32] = P::OID; + const OID_DER: &'static [u8] = P::OID_DER; } /// The core internal implementation of the ML-DSA algorithm. /// This needs to be public for the compiler to be able to find it, but there shouldn't ever /// be a need to use this directly. Please use the named public types. pub struct MLDSA< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_VEC_H_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > { - _phantom: PhantomData<(PK, SK)>, + _phantom: PhantomData<(P, PK, SK)>, /// used for streaming the message for both signing and verifying mu_builder: MuBuilder, @@ -740,79 +572,15 @@ pub struct MLDSA< } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> - MLDSA< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> MLDSA { /// Performs the first step of key generation to transform the single provided seed into a set of internal intermediate seeds. /// @@ -831,95 +599,16 @@ impl< } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - eta, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> - MLDSATrait< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - LAMBDA, - GAMMA2, - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - eta, - > - for MLDSA< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - eta, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> MLDSATrait + for MLDSA { /*** Key Generation and PK / SK consistency checks ***/ @@ -1089,8 +778,8 @@ impl< // They are uncompresso as-needed, and only one polynomial at a time. // Storing these in memory can be avoided, but then all the sites where they are used // will require calls to sk.compute_s1_row() and sk.compute_s2_row(), which are fairly expensive. - let s1_packed: Secret<[u8; S1_PACKED_LEN]> = sk.compute_s1_packed(); - let s2_packed: Secret<[u8; S2_PACKED_LEN]> = sk.compute_s2_packed(); + let s1_packed: Secret = sk.compute_s1_packed(); + let s2_packed: Secret = sk.compute_s2_packed(); // 6: 𝜇 ← H(BytesToBits(𝑡𝑟)||𝑀 ′, 64) // skip: mu has already been provided @@ -1111,14 +800,14 @@ impl< // ▷ initialize counter 𝜅 let mut kappa: u16 = 0; - let z_offset = LAMBDA_over_4; - let hint_offset = LAMBDA_over_4 + l * POLY_Z_PACKED_LEN; + let z_offset = P::C_TILDE_LEN; + let hint_offset = P::C_TILDE_LEN + P::l * P::POLY_Z_PACKED_LEN; loop { // FIPS 204 s. 6.2 allows: // "Implementations may limit the number of iterations in this loop to not exceed a finite maximum value." // mutants note: there is no test for this because we don't have access to a KAT that will exceed this limit. - if kappa > 1000 * k as u16 { + if kappa > 1000 * P::k as u16 { return Err(SignatureError::GenericError( "Rejection sampling loop exceeded max iterations, try again with a different signing nonce.", )); @@ -1129,45 +818,39 @@ impl< // scope for hash let mut hash = H::new(); hash.absorb(mu).expect("absorb before squeeze is infallible"); - for row in 0..k { - let mut w = compute_w_row::( - &sk.rho(), - &rho_p_p, - kappa, - row, - ); - w.high_bits::(); - hash.absorb(&w.w1_encode::()) + 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"); } - let mut sig_val_c_tilde = [0u8; LAMBDA_over_4]; - hash.squeeze_out(&mut sig_val_c_tilde); + let mut sig_val_c_tilde = ::ZEROED; + hash.squeeze_out(sig_val_c_tilde.as_mut()); sig_val_c_tilde }; // 16: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) // 17: 𝑐_hat ← NTT(𝑐) // optimization note: c_hat is used basically until the end, it can't really be scoped - let mut c_hat = sample_in_ball::(&sig_val_c_tilde); + let mut c_hat = sample_in_ball::

(&sig_val_c_tilde); c_hat.ntt(); output.fill(0); - output[..LAMBDA_over_4].copy_from_slice(&sig_val_c_tilde); + output[..P::C_TILDE_LEN].copy_from_slice(sig_val_c_tilde.as_ref()); - let (z_chunks, z_remainder) = output[z_offset..z_offset + l * POLY_Z_PACKED_LEN] - .as_chunks_mut::(); - debug_assert_eq!(z_chunks.len(), l); - debug_assert_eq!(z_remainder.len(), 0); + // The 𝐳 coordinates occupy `P::l` consecutive `P::POLY_Z_PACKED_LEN`-byte windows + // starting at `z_offset`. + debug_assert!(z_offset + P::l * P::POLY_Z_PACKED_LEN <= SIG_LEN); // 18-23 (z path): compute and encode each z polynomial directly into the caller buffer. let mut rejected = false; - for col in 0..l { - let z = match compute_z_component::( + for col in 0..P::l { + let z = match compute_z_component::

( // [Optimization Note]: // This is one of the places that a row of s1 can be re-computed instead of unpacked from the compressed form. // weirdly, in perf testing, this actually caused memory usage to go by a small amount; // maybe because re-computing the intermediates adds more to the widest point of the alg? // &sk.compute_s1_row(col), - &s_unpack::(&s1_packed, col), + &s_unpack::(&s1_packed, col), &rho_p_p, &c_hat, kappa, @@ -1180,25 +863,25 @@ impl< } }; - bitpack_gamma1::(&z, &mut z_chunks[col]); + let start = z_offset + col * P::POLY_Z_PACKED_LEN; + bitpack_gamma1::

(&z, &mut output[start..start + P::POLY_Z_PACKED_LEN]); } if rejected { // mutants note: we don't have access to a test vector that exercises this - kappa += l as u16; + kappa += P::l as u16; continue; } // 19-28 (hint path): recompute rows as needed and write the packed hint directly. let mut hint_count = 0usize; - for row in 0..k { - let mut w = - compute_w_row::(&sk.rho(), &rho_p_p, kappa, row); - let mut tmp = match compute_w0cs2_component::( + for row in 0..P::k { + let mut w = compute_w_row::

(&sk.rho(), &rho_p_p, kappa, row); + let mut tmp = match compute_w0cs2_component::

( // [Optimization Note]: // This is one of the places that a row of s1 can be re-computed instead of unpacked from the compressed form. // &sk.compute_s2_row(row), - &s_unpack::(&s2_packed, row), + &s_unpack::(&s2_packed, row), &w, &c_hat, ) { @@ -1209,7 +892,7 @@ impl< } }; - let ct0 = match compute_ct0_component::( + let ct0 = match compute_ct0_component::

( // [Optimization Note]: // This is one of the places that a row of s1 can be re-computed instead of unpacked from the compressed form. // &sk.compute_t0_row(row), &c_hat) { @@ -1226,13 +909,13 @@ impl< tmp.add_ntt(&ct0); tmp.conditional_add_q(); - w.high_bits::(); - let (hint_row, weight) = tmp.make_hint_row::(&w); + w.high_bits::

(); + let (hint_row, weight) = tmp.make_hint_row::

(&w); let next_hint_count = hint_count + weight as usize; // mutants note: don't have a test vector that exercises this condition, // not even in bc-test-data - if next_hint_count > OMEGA as usize { + if next_hint_count > P::omega as usize { rejected = true; break; } @@ -1244,11 +927,11 @@ impl< } } debug_assert_eq!(hint_count, next_hint_count); - output[hint_offset + OMEGA as usize + row] = hint_count as u8; + output[hint_offset + P::omega as usize + row] = hint_count as u8; } if rejected { - kappa += l as u16; + kappa += P::l as u16; continue; } @@ -1325,40 +1008,24 @@ impl< // skip because this function is being handed mu // 8: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) - let c = sample_in_ball::(unpack_c_tilde(sig)); + let c = sample_in_ball::

(&unpack_c_tilde::

(sig)); // 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"); - for row in 0..k { + for row in 0..P::k { let mut wp_approx = match { // 9: 𝐰′_approx ← NTT−1(𝐀_hat ∘ NTT(𝐳) − NTT(𝑐) ∘ NTT(𝐭1 ⋅ 2^𝑑)) - compute_wp_approx_row::< - GAMMA1, - GAMMA1_MINUS_BETA, - l, - POLY_Z_PACKED_LEN, - LAMBDA_over_4, - SIG_LEN, - >(pk.rho(), sig, &pk.unpack_t1_row(row), &c, row) + compute_wp_approx_row::(pk.rho(), sig, &pk.unpack_t1_row(row), &c, row) } { Ok(wp_approx) => wp_approx, // means the norm check on z failed Err(_) => return Err(SignatureError::SignatureVerificationFailed), }; - let h_i = match unpack_h_row::< - GAMMA1, - k, - l, - OMEGA, - LAMBDA_over_4, - POLY_Z_PACKED_LEN, - SIG_LEN, - >(row, &sig) - { + let h_i = match unpack_h_row::(row, &sig) { Some(h_i) => h_i, // means there were more than OMEGA bits set in the hint None => return Err(SignatureError::SignatureVerificationFailed), @@ -1366,19 +1033,22 @@ impl< // 10: 𝐰1′ ← UseHint(𝐡, 𝐰'_approx) // ▷ reconstruction of signer’s commitment - wp_approx.use_hint::(&h_i); - hash.absorb(&wp_approx.w1_encode::()) + wp_approx.use_hint::

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

().as_ref()) .expect("absorb before squeeze is infallible"); } - let mut c_tilde_p = [0u8; LAMBDA_over_4]; - hash.squeeze_out(&mut c_tilde_p); + let mut c_tilde_p = ::ZEROED; + hash.squeeze_out(c_tilde_p.as_mut()); // Verification is also done in constant time // 13 (second half): return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] and [[𝑐 ̃ = 𝑐′ ]] // note: the first half of this check (the norm check) is buried in unpack_z_row(), // which is called from compute_wp_approx_row() - if bouncycastle_utils::ct::ct_eq_bytes(unpack_c_tilde::(sig), &c_tilde_p) { + if bouncycastle_utils::ct::ct_eq_bytes( + unpack_c_tilde::

(sig).as_ref(), + c_tilde_p.as_ref(), + ) { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) @@ -1388,40 +1058,14 @@ impl< /// Trait for all three of the ML-DSA algorithm variants. pub trait MLDSATrait< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const ETA: usize, >: Sized { /// Runs a key generation using the library's default RNG, seeded from the OS. @@ -1436,7 +1080,7 @@ pub trait MLDSATrait< // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), SignatureError> { // Source the seed from the provided RNG - if rng.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if rng.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } let mut seed = KeyMaterial::<32>::new(); @@ -1615,79 +1259,15 @@ pub trait MLDSATrait< } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> Signer - for MLDSA< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> Signer for MLDSA { fn sign(sk: &SK, msg: &[u8], ctx: Option<&[u8]>) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; @@ -1769,79 +1349,16 @@ impl< } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait - + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > + MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - ETA, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - >, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > SignatureVerifier - for MLDSA< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > + for MLDSA { fn verify(pk: &PK, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8]) -> Result<(), SignatureError> { let mu = MuBuilder::compute_mu(&pk.compute_tr(), msg, ctx)?; diff --git a/crypto/mldsa-lowmemory/src/mldsa_keys.rs b/crypto/mldsa-lowmemory/src/mldsa_keys.rs index 2863068a..76477c63 100644 --- a/crypto/mldsa-lowmemory/src/mldsa_keys.rs +++ b/crypto/mldsa-lowmemory/src/mldsa_keys.rs @@ -1,30 +1,18 @@ use crate::aux_functions::{ - bit_pack_eta, bit_pack_t0, bitlen_eta, power_2_round, rej_bounded_poly, simple_bit_pack_t1, + bit_pack_eta, bit_pack_t0, power_2_round, rej_bounded_poly, simple_bit_pack_t1, simple_bit_unpack_t1, }; use crate::low_memory_helpers::{expandA_elem, s_unpack}; -use crate::mldsa::{H, N, POLY_T0PACKED_LEN}; -use crate::mldsa::{ - MLDSA44_ETA, MLDSA44_FULL_SK_LEN, MLDSA44_GAMMA2, MLDSA44_LAMBDA, MLDSA44_PK_LEN, - MLDSA44_S1_PACKED_LEN, MLDSA44_S2_PACKED_LEN, MLDSA44_SK_LEN, MLDSA44_k, MLDSA44_l, -}; -use crate::mldsa::{ - MLDSA44_T1_PACKED_LEN, MLDSA65_T1_PACKED_LEN, MLDSA87_T1_PACKED_LEN, POLY_T1PACKED_LEN, -}; -use crate::mldsa::{ - MLDSA65_ETA, MLDSA65_FULL_SK_LEN, MLDSA65_GAMMA2, MLDSA65_LAMBDA, MLDSA65_PK_LEN, - MLDSA65_S1_PACKED_LEN, MLDSA65_S2_PACKED_LEN, MLDSA65_SK_LEN, MLDSA65_k, MLDSA65_l, -}; -use crate::mldsa::{ - MLDSA87_ETA, MLDSA87_FULL_SK_LEN, MLDSA87_GAMMA2, MLDSA87_LAMBDA, MLDSA87_PK_LEN, - MLDSA87_S1_PACKED_LEN, MLDSA87_S2_PACKED_LEN, MLDSA87_SK_LEN, MLDSA87_k, MLDSA87_l, -}; -use crate::{ML_DSA_44_NAME, ML_DSA_65_NAME, ML_DSA_87_NAME}; +use crate::mldsa::{H, N, POLY_T0PACKED_LEN, POLY_T1PACKED_LEN}; +use crate::mldsa::{MLDSA44_FULL_SK_LEN, MLDSA44_PK_LEN, MLDSA44_SK_LEN}; +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::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; use bouncycastle_core::traits::{SecurityStrength, SignaturePrivateKey, SignaturePublicKey, XOF}; -use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; use core::fmt; use core::fmt::{Debug, Display, Formatter}; use core::ops::DerefMut; @@ -36,63 +24,37 @@ use crate::polynomial::Polynomial; /* Pub Types */ /// ML-DSA-44 Public Key -pub type MLDSA44PublicKey = MLDSAPublicKey; +pub type MLDSA44PublicKey = MLDSAPublicKey; /// ML-DSA-44 Private Key -pub type MLDSA44PrivateKey = MLDSASeedPrivateKey< - MLDSA44_LAMBDA, - MLDSA44_GAMMA2, - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, - MLDSA44_S1_PACKED_LEN, - MLDSA44_S2_PACKED_LEN, - MLDSA44_T1_PACKED_LEN, - MLDSA44_PK_LEN, - MLDSA44_SK_LEN, - MLDSA44_FULL_SK_LEN, ->; +pub type MLDSA44PrivateKey = + MLDSASeedPrivateKey; /// ML-DSA-65 Public Key -pub type MLDSA65PublicKey = MLDSAPublicKey; +pub type MLDSA65PublicKey = MLDSAPublicKey; /// ML-DSA-65 Private Key -pub type MLDSA65PrivateKey = MLDSASeedPrivateKey< - MLDSA65_LAMBDA, - MLDSA65_GAMMA2, - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, - MLDSA65_S1_PACKED_LEN, - MLDSA65_S2_PACKED_LEN, - MLDSA65_T1_PACKED_LEN, - MLDSA65_PK_LEN, - MLDSA65_SK_LEN, - MLDSA65_FULL_SK_LEN, ->; +pub type MLDSA65PrivateKey = + MLDSASeedPrivateKey; /// ML-DSA-87 Public Key -pub type MLDSA87PublicKey = MLDSAPublicKey; +pub type MLDSA87PublicKey = MLDSAPublicKey; /// ML-DSA-87 Private Key -pub type MLDSA87PrivateKey = MLDSASeedPrivateKey< - MLDSA87_LAMBDA, - MLDSA87_GAMMA2, - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, - MLDSA87_S1_PACKED_LEN, - MLDSA87_S2_PACKED_LEN, - MLDSA87_T1_PACKED_LEN, - MLDSA87_PK_LEN, - MLDSA87_SK_LEN, - MLDSA87_FULL_SK_LEN, ->; +pub type MLDSA87PrivateKey = + MLDSASeedPrivateKey; /// An ML-DSA public key. -#[derive(Clone)] -pub struct MLDSAPublicKey { +pub struct MLDSAPublicKey { pub(crate) rho: [u8; 32], - pub(crate) t1_packed: [u8; T1_PACKED_LEN], + pub(crate) t1_packed: P::T1Packed, +} + +// Written out rather than derived: `#[derive(Clone)]` would demand `P: Clone`, and `P` is a +// marker for the parameter set that is never stored, only used to name the field types. +impl Clone for MLDSAPublicKey { + fn clone(&self) -> Self { + Self { rho: self.rho, t1_packed: self.t1_packed } + } } /// General trait for all ML-DSA public keys types. -pub trait MLDSAPublicKeyTrait: +pub trait MLDSAPublicKeyTrait: SignaturePublicKey { /// Algorithm 23 pkDecode(𝑝𝑘) @@ -110,15 +72,10 @@ pub trait MLDSAPublicKeyTrait [u8; 64]; } -pub(crate) trait MLDSAPublicKeyInternalTrait< - const k: usize, - const T1_PACKED_LEN: usize, - const PK_LEN: usize, -> -{ +pub(crate) trait MLDSAPublicKeyInternalTrait { /// Not exposing a constructor publicly because the user should get an instance either by /// running a keygen, or by decoding an existing key. - fn new(rho: [u8; 32], t1_packed: [u8; T1_PACKED_LEN]) -> Self; + fn new(rho: [u8; 32], t1_packed: P::T1Packed) -> Self; /// Get a ref to rho fn rho(&self) -> &[u8; 32]; @@ -127,11 +84,13 @@ pub(crate) trait MLDSAPublicKeyInternalTrait< fn unpack_t1_row(&self, row: usize) -> Polynomial; } -impl - MLDSAPublicKeyTrait for MLDSAPublicKey +impl MLDSAPublicKeyTrait + for MLDSAPublicKey { fn pk_decode(pk: &[u8; PK_LEN]) -> Self { - Self { rho: pk[..32].try_into().unwrap(), t1_packed: pk[32..].try_into().unwrap() } + let mut t1_packed = ::ZEROED; + t1_packed.as_mut().copy_from_slice(&pk[32..]); + Self { rho: pk[..32].try_into().unwrap(), t1_packed } } fn compute_tr(&self) -> [u8; 64] { @@ -142,11 +101,10 @@ impl } } -impl - MLDSAPublicKeyInternalTrait - for MLDSAPublicKey +impl MLDSAPublicKeyInternalTrait + for MLDSAPublicKey { - fn new(rho: [u8; 32], t1_packed: [u8; T1_PACKED_LEN]) -> Self { + fn new(rho: [u8; 32], t1_packed: P::T1Packed) -> Self { Self { rho, t1_packed } } @@ -156,16 +114,14 @@ impl fn unpack_t1_row(&self, row: usize) -> Polynomial { simple_bit_unpack_t1( - &self.t1_packed[row * POLY_T1PACKED_LEN..(row + 1) * POLY_T1PACKED_LEN] + self.t1_packed.as_ref()[row * POLY_T1PACKED_LEN..(row + 1) * POLY_T1PACKED_LEN] .try_into() - .unwrap(), + .expect("a T1Packed row is exactly POLY_T1PACKED_LEN bytes"), ) } } -impl SignaturePublicKey - for MLDSAPublicKey -{ +impl SignaturePublicKey for MLDSAPublicKey { /// Algorithm 22 pkEncode(𝜌, 𝐭1) /// Encodes a public key for ML-DSA into a byte string. /// Input:𝜌 ∈ 𝔹32, 𝐭1 ∈ 𝑅𝑘 with coefficients in [0, 2bitlen (𝑞−1)−𝑑 − 1]. @@ -186,7 +142,7 @@ impl SignatureP out.fill(0); out[..32].copy_from_slice(&self.rho); - out[32..].copy_from_slice(&self.t1_packed); + out[32..].copy_from_slice(self.t1_packed.as_ref()); PK_LEN } @@ -202,14 +158,9 @@ impl SignatureP } } -impl Eq - for MLDSAPublicKey -{ -} +impl Eq for MLDSAPublicKey {} -impl PartialEq - for MLDSAPublicKey -{ +impl PartialEq for MLDSAPublicKey { fn eq(&self, other: &Self) -> bool { let self_encoded = self.encode(); let other_encoded = other.encode(); @@ -217,41 +168,31 @@ impl PartialEq } } -impl fmt::Debug - for MLDSAPublicKey -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; - write!(f, "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", alg, self.compute_tr(),) +impl fmt::Debug for MLDSAPublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", + P::ALG_NAME, + self.compute_tr(), + ) } } -impl Display - for MLDSAPublicKey -{ +impl Display for MLDSAPublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; - write!(f, "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", alg, self.compute_tr(),) + write!( + f, + "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", + P::ALG_NAME, + self.compute_tr(), + ) } } /// General trait for all ML-DSA private keys types. pub trait MLDSAPrivateKeyTrait< - const k: usize, - const l: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, + P: MLDSAParams, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, @@ -269,7 +210,7 @@ pub trait MLDSAPrivateKeyTrait< /// or else compute `tr` once and store it. fn tr(&self) -> [u8; 64]; /// Returns the full public key, and has the side-effect of setting the public key hash tr in this MLDSASeedSK object. - fn derive_pk(&self) -> MLDSAPublicKey; + fn derive_pk(&self) -> MLDSAPublicKey; /// This produces the full private key in the encoding specified in FIPS 204 Algorithm 24 skEncode() /// so that it is compatible with other implementations. /// @@ -294,20 +235,13 @@ pub trait MLDSAPrivateKeyTrait< } /// Internal structure for holding a seed-based private key for ML-DSA. -#[derive(Clone, PartialEq, Eq)] pub struct MLDSASeedPrivateKey< - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, + P: MLDSAParams, const PK_LEN: usize, const SK_LEN: usize, const FULL_SK_LEN: usize, > { + _phantom: core::marker::PhantomData

, // note: KeyMaterial is inherently Secret seed: KeyMaterial<32>, // public seed rho does not need to be secret @@ -315,108 +249,61 @@ pub struct MLDSASeedPrivateKey< rho_prime: Secret<[u8; 64]>, K: Secret<[u8; 32]>, } -impl< - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const PK_LEN: usize, - const SK_LEN: usize, - const FULL_SK_LEN: usize, -> Debug - for MLDSASeedPrivateKey< - LAMBDA, - GAMMA2, - k, - l, - eta, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > +// Written out rather than derived: the derives would demand `P: Clone` / `P: Eq`, and `P` is a +// marker for the parameter set that is never stored, only used to name the field types. +impl Clone + for MLDSASeedPrivateKey +{ + fn clone(&self) -> Self { + Self { + _phantom: core::marker::PhantomData, + seed: self.seed.clone(), + rho: self.rho, + rho_prime: self.rho_prime.clone(), + K: self.K.clone(), + } + } +} + +impl PartialEq + for MLDSASeedPrivateKey +{ + fn eq(&self, other: &Self) -> bool { + // Compared through `KeyMaterial`/`Secret`'s own `PartialEq`, which is constant-time: do + // not deref to the inner arrays, as that would select the array's variable-time `==`. + let seed = self.seed == other.seed; + let rho = self.rho == other.rho; + let rho_prime = self.rho_prime == other.rho_prime; + let K = self.K == other.K; + seed & rho & rho_prime & K + } +} + +impl Eq + for MLDSASeedPrivateKey +{ +} + +impl Debug + for MLDSASeedPrivateKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; + let alg = P::ALG_NAME; write!(f, "MLDSASeedPrivateKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", alg, self.tr(),) } } -impl< - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const PK_LEN: usize, - const SK_LEN: usize, - const FULL_SK_LEN: usize, -> Display - for MLDSASeedPrivateKey< - LAMBDA, - GAMMA2, - k, - l, - eta, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > +impl Display + for MLDSASeedPrivateKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; + let alg = P::ALG_NAME; write!(f, "MLDSASeedPrivateKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", alg, self.tr(),) } } -impl< - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const PK_LEN: usize, - const SK_LEN: usize, - const FULL_SK_LEN: usize, -> - MLDSASeedPrivateKey< - LAMBDA, - GAMMA2, - k, - l, - eta, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > +impl + MLDSASeedPrivateKey { /// Create a new MLDSASeedPrivateKey from a 32-byte KeyMaterial. /// Seed SecurityStrength must match algorithm security strength: 128-bit (ML-DSA-44), 192-bit (ML-DSA-65), or 256-bit (ML-DSA-87), @@ -430,13 +317,13 @@ impl< )); } - if seed.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if seed.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(SignatureError::KeyGenError("SecurityStrength")); } let (rho, rho_prime, K) = Self::compute_rhos_and_K(&seed); - Ok(Self { seed: seed.clone(), rho, rho_prime, K }) + Ok(Self { _phantom: core::marker::PhantomData, seed: seed.clone(), rho, rho_prime, K }) } fn compute_rhos_and_K( @@ -451,8 +338,8 @@ impl< let mut h = H::default(); h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(l as u8).to_le_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); debug_assert_eq!(bytes_written, 32); let bytes_written = h.squeeze_out(rho_prime.deref_mut()); @@ -466,26 +353,26 @@ impl< fn compute_t_row( &self, idx: usize, - s1_packed: &Secret<[u8; S1_PACKED_LEN]>, - s2_packed: &Secret<[u8; S2_PACKED_LEN]>, + s1_packed: &Secret, + s2_packed: &Secret, ) -> Polynomial { - debug_assert!(idx < k); + debug_assert!(idx < P::k); // [Optimization Note]: // This is one of the places that a row of s1 can be re-computed instead of expanded from the compressed form. // let mut s1 = self.compute_s1_row(0); - let mut s1_hat_i = s_unpack::(s1_packed, 0); + let mut s1_hat_i = s_unpack::(s1_packed, 0); s1_hat_i.ntt(); let mut t_i = { let mut t_hat_i = expandA_elem(&self.rho, idx, 0); t_hat_i.multiply_ntt(&s1_hat_i); - for col in 1..l { + for col in 1..P::l { // [Optimization Note]: // This is one of the places that a row of s1 can be re-computed instead of expanded from the compressed form. // s1 = self.compute_s1_row(col); - let mut s1_hat = s_unpack::(s1_packed, col); + let mut s1_hat = s_unpack::(s1_packed, col); s1_hat.ntt(); let mut A_elem = expandA_elem(&self.rho, idx, col); A_elem.multiply_ntt(&s1_hat); @@ -499,7 +386,7 @@ impl< // [Optimization Note]: // This is one of the places that a row of s2 can be re-computed instead of unpacked from the compressed form. // let s2 = self.compute_s2_row(idx); - let s2 = s_unpack::(s2_packed, idx); + let s2 = s_unpack::(s2_packed, idx); t_i.add_ntt(&s2); t_i.conditional_add_q(); @@ -507,37 +394,11 @@ impl< } } -impl< - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const PK_LEN: usize, - const SK_LEN: usize, - const FULL_SK_LEN: usize, -> SignaturePrivateKey - for MLDSASeedPrivateKey< - LAMBDA, - GAMMA2, - k, - l, - eta, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > +impl + SignaturePrivateKey for MLDSASeedPrivateKey { /// Encodes the private key seed. fn encode(&self) -> [u8; SK_LEN] { - debug_assert_eq!(SK_LEN, /* seed */ 32); - self.seed.ref_to_bytes().try_into().unwrap() } @@ -564,42 +425,9 @@ impl< } } -impl< - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const PK_LEN: usize, - const SK_LEN: usize, - const FULL_SK_LEN: usize, -> - MLDSAPrivateKeyTrait< - k, - l, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > - for MLDSASeedPrivateKey< - LAMBDA, - GAMMA2, - k, - l, - eta, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > +impl + MLDSAPrivateKeyTrait + for MLDSASeedPrivateKey { fn from_keymaterial(seed: &KeyMaterial<32>) -> Result { Self::new(seed) @@ -610,26 +438,26 @@ impl< } fn tr(&self) -> [u8; 64] { - let pk: MLDSAPublicKey = self.derive_pk(); + let pk: MLDSAPublicKey = self.derive_pk(); pk.compute_tr() } - fn derive_pk(&self) -> MLDSAPublicKey { + fn derive_pk(&self) -> MLDSAPublicKey { // The goal here is to get t1, which is built and compressed one row at a time. - let s1_packed: Secret<[u8; S1_PACKED_LEN]> = self.compute_s1_packed(); - let s2_packed: Secret<[u8; S2_PACKED_LEN]> = self.compute_s2_packed(); + let s1_packed: Secret = self.compute_s1_packed(); + let s2_packed: Secret = self.compute_s2_packed(); - let mut t1_packed = [0u8; T1_PACKED_LEN]; - debug_assert_eq!(T1_PACKED_LEN, POLY_T1PACKED_LEN * k); + let mut t1_packed = ::ZEROED; + debug_assert_eq!(P::T1_PACKED_LEN, POLY_T1PACKED_LEN * P::k); - for i in 0..k { - t1_packed[i * POLY_T1PACKED_LEN..(i + 1) * POLY_T1PACKED_LEN].copy_from_slice( + for i in 0..P::k { + t1_packed.as_mut()[i * POLY_T1PACKED_LEN..(i + 1) * POLY_T1PACKED_LEN].copy_from_slice( &simple_bit_pack_t1(&self.compute_t1_row(i, &s1_packed, &s2_packed)), ); } - MLDSAPublicKey::::new(self.rho.clone(), t1_packed) + MLDSAPublicKey::::new(self.rho.clone(), t1_packed) } fn encode_full_sk(&self) -> [u8; FULL_SK_LEN] { let mut out = [0; FULL_SK_LEN]; @@ -655,21 +483,21 @@ impl< // 3: 𝑠𝑘 ← 𝑠𝑘 || BitPack (𝐬1[𝑖], 𝜂, 𝜂) // 4: end for let s1_packed = self.compute_s1_packed(); - out[off..off + S1_PACKED_LEN].copy_from_slice(&*s1_packed); - off += S1_PACKED_LEN; + out[off..off + P::S1_PACKED_LEN].copy_from_slice((*s1_packed).as_ref()); + off += P::S1_PACKED_LEN; // 5: for 𝑖 from 0 to 𝑘 − 1 do // 6: 𝑠𝑘 ← 𝑠𝑘 || BitPack (𝐬2[𝑖], 𝜂, 𝜂) // 7: end for let s2_packed = self.compute_s2_packed(); - out[off..off + S2_PACKED_LEN].copy_from_slice(&*s2_packed); - off += S2_PACKED_LEN; + out[off..off + P::S2_PACKED_LEN].copy_from_slice((*s2_packed).as_ref()); + off += P::S2_PACKED_LEN; // 8: for 𝑖 from 0 to 𝑘 − 1 do // 9: 𝑠𝑘 ← 𝑠𝑘 || BitPack (𝐭0[𝑖], 2𝑑−1 − 1, 2𝑑−1) // 10: end for - debug_assert_eq!(off + k * POLY_T0PACKED_LEN, FULL_SK_LEN); - for row in 0..k { + debug_assert_eq!(off + P::k * POLY_T0PACKED_LEN, FULL_SK_LEN); + for row in 0..P::k { let t0_i = self.compute_t0_row(row, &s1_packed, &s2_packed); out[off..off + POLY_T0PACKED_LEN].copy_from_slice(&bit_pack_t0(&t0_i)); off += POLY_T0PACKED_LEN; @@ -685,13 +513,7 @@ impl< } pub(crate) trait MLDSAPrivateKeyInternalTrait< - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, + P: MLDSAParams, const PK_LEN: usize, const SK_LEN: usize, >: Sized @@ -706,7 +528,7 @@ pub(crate) trait MLDSAPrivateKeyInternalTrait< /// Private key component. /// The packed representation sticks around for the whole computation, so /// we'll wrap in as a Secret. - fn compute_s1_packed(&self) -> Secret<[u8; S1_PACKED_LEN]>; + fn compute_s1_packed(&self) -> Secret; /// A single entry of a privacy key vector. /// These tend to be used very transiently, so we won't bother wrapping it as a Secret. @@ -715,62 +537,28 @@ pub(crate) trait MLDSAPrivateKeyInternalTrait< /// Private key component. /// The packed representation sticks around for the whole computation, so /// we'll wrap in as a Secret. - fn compute_s2_packed(&self) -> Secret<[u8; S2_PACKED_LEN]>; + fn compute_s2_packed(&self) -> Secret; /// Public key component. fn compute_t0_row( &self, idx: usize, - s1_packed: &Secret<[u8; S1_PACKED_LEN]>, - s2_packed: &Secret<[u8; S2_PACKED_LEN]>, + s1_packed: &Secret, + s2_packed: &Secret, ) -> Polynomial; /// Public key component. fn compute_t1_row( &self, idx: usize, - s1_packed: &Secret<[u8; S1_PACKED_LEN]>, - s2_packed: &Secret<[u8; S2_PACKED_LEN]>, + s1_packed: &Secret, + s2_packed: &Secret, ) -> Polynomial; } -impl< - const LAMBDA: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const eta: usize, - const S1_PACKED_LEN: usize, - const S2_PACKED_LEN: usize, - const T1_PACKED_LEN: usize, - const PK_LEN: usize, - const SK_LEN: usize, - const FULL_SK_LEN: usize, -> - MLDSAPrivateKeyInternalTrait< - LAMBDA, - GAMMA2, - k, - l, - eta, - S1_PACKED_LEN, - S2_PACKED_LEN, - PK_LEN, - SK_LEN, - > - for MLDSASeedPrivateKey< - LAMBDA, - GAMMA2, - k, - l, - eta, - S1_PACKED_LEN, - S2_PACKED_LEN, - T1_PACKED_LEN, - PK_LEN, - SK_LEN, - FULL_SK_LEN, - > +impl + MLDSAPrivateKeyInternalTrait + for MLDSASeedPrivateKey { fn rho(&self) -> &[u8; 32] { &self.rho @@ -781,35 +569,31 @@ impl< } fn compute_s1_row(&self, idx: usize) -> Polynomial { - debug_assert!(idx < l); - rej_bounded_poly::(&self.rho_prime, &(idx as u16).to_le_bytes()) + debug_assert!(idx < P::l); + rej_bounded_poly::

(&self.rho_prime, &(idx as u16).to_le_bytes()) } - fn compute_s1_packed(&self) -> Secret<[u8; S1_PACKED_LEN]> { - let mut s1_packed: Secret<[u8; S1_PACKED_LEN]> = Secret::new(); - for idx in 0..l { + fn compute_s1_packed(&self) -> Secret { + let mut s1_packed: Secret = Secret::new(); + let width = P::POLY_ETA_PACKED_LEN; + for idx in 0..P::l { let s1_i = self.compute_s1_row(idx); - bit_pack_eta::( - &s1_i, - &mut s1_packed[idx * bitlen_eta(eta)..(idx + 1) * bitlen_eta(eta)], - ); + bit_pack_eta::

(&s1_i, &mut s1_packed.as_mut()[idx * width..(idx + 1) * width]); } s1_packed } fn compute_s2_row(&self, idx: usize) -> Polynomial { - debug_assert!(idx < k); - rej_bounded_poly::(&self.rho_prime, &((idx + l) as u16).to_le_bytes()) + debug_assert!(idx < P::k); + rej_bounded_poly::

(&self.rho_prime, &((idx + P::l) as u16).to_le_bytes()) } - fn compute_s2_packed(&self) -> Secret<[u8; S2_PACKED_LEN]> { - let mut s2_packed: Secret<[u8; S2_PACKED_LEN]> = Secret::new(); - for idx in 0..k { + fn compute_s2_packed(&self) -> Secret { + let mut s2_packed: Secret = Secret::new(); + let width = P::POLY_ETA_PACKED_LEN; + for idx in 0..P::k { let s2_i = self.compute_s2_row(idx); - bit_pack_eta::( - &s2_i, - &mut s2_packed[idx * bitlen_eta(eta)..(idx + 1) * bitlen_eta(eta)], - ); + bit_pack_eta::

(&s2_i, &mut s2_packed.as_mut()[idx * width..(idx + 1) * width]); } s2_packed } @@ -817,8 +601,8 @@ impl< fn compute_t0_row( &self, idx: usize, - s1_packed: &Secret<[u8; S1_PACKED_LEN]>, - s2_packed: &Secret<[u8; S2_PACKED_LEN]>, + s1_packed: &Secret, + s2_packed: &Secret, ) -> Polynomial { let mut t0 = self.compute_t_row(idx, s1_packed, s2_packed); for j in 0..N { @@ -831,8 +615,8 @@ impl< fn compute_t1_row( &self, idx: usize, - s1_packed: &Secret<[u8; S1_PACKED_LEN]>, - s2_packed: &Secret<[u8; S2_PACKED_LEN]>, + s1_packed: &Secret, + s2_packed: &Secret, ) -> Polynomial { let mut t1 = self.compute_t_row(idx, s1_packed, s2_packed); for j in 0..N { diff --git a/crypto/mldsa-lowmemory/src/params.rs b/crypto/mldsa-lowmemory/src/params.rs new file mode 100644 index 00000000..a07f1200 --- /dev/null +++ b/crypto/mldsa-lowmemory/src/params.rs @@ -0,0 +1,520 @@ +//! The three ML-DSA parameter sets of FIPS 204, Section 4, as a sealed trait with one type per set. +//! +//! This mirrors `bouncycastle_mldsa::params`, minus the vector and matrix types: this crate never +//! materializes 𝐀̂ or a whole polynomial vector, so the only parameter-sized types it needs are +//! byte buffers. +//! +//! # Derived parameters +//! +//! FIPS 204, Table 1 assigns eight independent values per set (𝜏, 𝜆, 𝛾1, 𝛾2, (𝑘, ℓ), 𝜂, 𝜔) plus the +//! three sizes of Table 2. Everything else this implementation needs is a function of those, so it +//! is written once as a defaulted associated const rather than three times as a hand-computed +//! number. `params::tests` checks every derivation against the values tabulated in FIPS 204. + +use crate::hash_mldsa::{ + HASH_ML_DSA_44_with_SHA256_NAME, HASH_ML_DSA_44_with_SHA512_NAME, + HASH_ML_DSA_65_WITH_SHA256_NAME, HASH_ML_DSA_65_WITH_SHA512_NAME, + HASH_ML_DSA_87_WITH_SHA512_NAME, HASH_ML_DSA_87_with_SHA256_NAME, +}; +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_sha2::{SHA256, SHA512}; +use bouncycastle_utils::secret::ZeroizablePrimitive; + +/// `bitlen 𝑥`, the length of the binary expansion of 𝑥 (FIPS 204, Section 2.3). +/// +/// `bitlen 0` is 0; every use below has a positive argument. +pub(crate) const fn bitlen(x: u32) -> usize { + if x == 0 { 0 } else { x.ilog2() as usize + 1 } +} + +/// A fixed-size byte buffer whose length depends on the parameter set. +/// +/// [`ZeroizablePrimitive`] rather than [`Default`] supplies the all-zero value, because `Default` +/// for arrays stops at 32 elements and every buffer here is longer than that. +trait ByteBuffer: ZeroizablePrimitive + AsRef<[u8]> + AsMut<[u8]> {} +impl ByteBuffer for [u8; N] {} + +/// A crate-private (aka "sealed") trait that prevents a new ML-DSA parameter set from being defined +/// outside this crate. +trait MLDSAParamsInternalTrait {} + +/// One ML-DSA parameter set: the values of FIPS 204, Table 1 and Table 2, and the types whose size +/// they determine. +/// +/// Sealed via a private supertrait, so [`MLDSA44Params`], [`MLDSA65Params`] and [`MLDSA87Params`] +/// are the only implementations. +pub trait MLDSAParams: MLDSAParamsInternalTrait { + /* FIPS 204, Table 1: the values assigned by each parameter set. */ + + /// 𝜏, the number of ±1's in the polynomial 𝑐. + const tau: i32; + /// 𝜆, the collision strength of 𝑐̃, in bits. + const lambda: i32; + /// 𝛾1, the coefficient range of 𝐲. Always a power of two. + const gamma1: i32; + /// 𝛾2, the low-order rounding range. + const gamma2: i32; + /// 𝑘, the number of rows of 𝐀. + const k: usize; + /// ℓ, the number of columns of 𝐀. + const l: usize; + /// 𝜂, the private key range. + const eta: usize; + /// 𝜔, the maximum number of 1's in the hint 𝐡. + const omega: i32; + + /* FIPS 204, Table 2: sizes in bytes of keys and signatures. */ + + /// The length of an encoded public key. + const PK_LEN: usize; + /// The length of the FIPS 204 encoding of a private key. + /// + /// Named `FULL_SK_LEN` rather than `SK_LEN` because this crate's private keys are held as the + /// 32-byte seed 𝜉 and expanded on demand; see [`MLDSAParams::SK_LEN`]. + const FULL_SK_LEN: usize; + /// The length of a signature. + const SIG_LEN: usize; + + /* Algorithm meta-data */ + + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + const ALG_NAME: &'static str; + /// The strength claimed for this parameter set, as reported by `Algorithm::MAX_SECURITY_STRENGTH`. + const MAX_SECURITY_STRENGTH: SecurityStrength; + /// The OID in component form, as reported by `AlgorithmOID::OID`. + const OID: &'static [u32]; + /// The DER encoding of [`MLDSAParams::OID`], as reported by `AlgorithmOID::OID_DER`. + const OID_DER: &'static [u8]; + + /* Derived. Never written out per parameter set -- see the module docs. */ + + /// The length of a private key as this crate stores it: the 32-byte seed 𝜉, for every + /// parameter set. FIPS 204, Section 4 notes that 𝜉 "is sufficient to generate the other parts + /// of the private key". + const SK_LEN: usize = MLDSA_SEED_LEN; + + /// 𝛽, which FIPS 204, Table 1 defines as "𝛽 = 𝜏 ⋅ 𝜂". + const beta: i32 = Self::tau * Self::eta as i32; + + /// The length of the commitment hash 𝑐̃, which FIPS 204, Algorithm 26 (sigEncode) gives as + /// 𝑐̃ ∈ 𝔹^(𝜆/4). + const C_TILDE_LEN: usize = Self::lambda as usize / 4; + + /// The packed length of one coordinate of 𝐳: FIPS 204, Algorithm 26 (sigEncode) writes each of + /// the ℓ coordinates as 𝔹^(32⋅(1+bitlen (𝛾1−1))). + /// + /// This is also the number of bytes ExpandMask squeezes per coordinate: FIPS 204, + /// Algorithm 34, line 1 sets 𝑐 ← 1 + bitlen (𝛾1 − 1) and line 4 squeezes 32𝑐 bytes. + const POLY_Z_PACKED_LEN: usize = 32 * (1 + bitlen(Self::gamma1 as u32 - 1)); + + /// The packed length of one coordinate of 𝐰1: FIPS 204, Algorithm 28 (w1Encode) outputs + /// 𝔹^(32𝑘⋅bitlen ((𝑞−1)/(2𝛾2)−1)) for all 𝑘 coordinates together. + const POLY_W1_PACKED_LEN: usize = 32 * bitlen(((q - 1) / (2 * Self::gamma2)) as u32 - 1); + + /// The packed length of one coordinate of 𝐬1 or 𝐬2: FIPS 204, Algorithm 24 (skEncode), line 3 + /// packs each with BitPack(𝐬1[𝑖], 𝜂, 𝜂), and Algorithm 17 (BitPack) outputs + /// 𝔹^(32⋅bitlen (𝑎+𝑏)), so 32⋅bitlen (2𝜂). + const POLY_ETA_PACKED_LEN: usize = 32 * bitlen(2 * Self::eta as u32); + + /// The packed length of the whole of 𝐬1, i.e. all ℓ coordinates. + const S1_PACKED_LEN: usize = Self::POLY_ETA_PACKED_LEN * Self::l; + + /// The packed length of the whole of 𝐬2, i.e. all 𝑘 coordinates. + const S2_PACKED_LEN: usize = Self::POLY_ETA_PACKED_LEN * Self::k; + + /// The packed length of the whole of 𝐭1, i.e. 𝑘 coordinates of SimpleBitPack output. + const T1_PACKED_LEN: usize = POLY_T1PACKED_LEN * Self::k; + + /// 𝛾1 − 𝛽, the rejection bound on ‖𝐳‖∞ (FIPS 204, Algorithm 7, line 23). + const gamma1_minus_beta: i32 = Self::gamma1 - Self::beta; + + /// 𝛾2 − 𝛽, the rejection bound on ‖𝐫0‖∞ (FIPS 204, Algorithm 7, line 23). + const gamma2_minus_beta: i32 = Self::gamma2 - Self::beta; + + /* Types whose size depends on the parameter set. */ + + /// The commitment hash 𝑐̃, of [`MLDSAParams::C_TILDE_LEN`] bytes. + type SigCTilde: ByteBuffer; + /// One packed coordinate of 𝐳, of [`MLDSAParams::POLY_Z_PACKED_LEN`] bytes. + /// + /// ExpandMask squeezes into a buffer of this same length; see + /// [`MLDSAParams::POLY_Z_PACKED_LEN`]. + type PolyZPacked: ByteBuffer; + /// One packed coordinate of 𝐰1, of [`MLDSAParams::POLY_W1_PACKED_LEN`] bytes. + type PolyW1Packed: ByteBuffer; + /// The whole of 𝐬1 packed, of [`MLDSAParams::S1_PACKED_LEN`] bytes. + type S1Packed: ByteBuffer; + /// The whole of 𝐬2 packed, of [`MLDSAParams::S2_PACKED_LEN`] bytes. + type S2Packed: ByteBuffer; + /// The whole of 𝐭1 packed, of [`MLDSAParams::T1_PACKED_LEN`] bytes. + type T1Packed: ByteBuffer; +} + +/// The ML-DSA-44 parameter set (FIPS 204, Table 1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLDSA44Params; +/// The ML-DSA-65 parameter set (FIPS 204, Table 1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLDSA65Params; +/// The ML-DSA-87 parameter set (FIPS 204, Table 1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLDSA87Params; + +impl MLDSAParamsInternalTrait for MLDSA44Params {} +impl MLDSAParamsInternalTrait for MLDSA65Params {} +impl MLDSAParamsInternalTrait for MLDSA87Params {} + +impl MLDSAParams for MLDSA44Params { + const tau: i32 = 39; + const lambda: i32 = 128; + const gamma1: i32 = 1 << 17; + // mutants note: because of the bitshifting, the "- 1" ends up not mattering. + const gamma2: i32 = (q - 1) / 88; + const k: usize = 4; + const l: usize = 4; + const eta: usize = 2; + const omega: i32 = 80; + + const PK_LEN: usize = 1312; + const FULL_SK_LEN: usize = 2560; + const SIG_LEN: usize = 2420; + + const ALG_NAME: &'static str = ML_DSA_44_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + /// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 17]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11]; + + type SigCTilde = [u8; 32]; // 𝜆/4 = 128/4 + type PolyZPacked = [u8; 576]; // 32 * (1 + bitlen(2^17 - 1)) = 32 * 18 + type PolyW1Packed = [u8; 192]; // 32 * bitlen(44 - 1) = 32 * 6 + type S1Packed = [u8; 384]; // 96 * 4 + type S2Packed = [u8; 384]; // 96 * 4 + type T1Packed = [u8; 1280]; // 320 * 4 +} + +impl MLDSAParams for MLDSA65Params { + const tau: i32 = 49; + const lambda: i32 = 192; + const gamma1: i32 = 1 << 19; + // mutants note: because of the bitshifting, the "- 1" ends up not mattering. + const gamma2: i32 = (q - 1) / 32; + const k: usize = 6; + const l: usize = 5; + const eta: usize = 4; + const omega: i32 = 55; + + const PK_LEN: usize = 1952; + const FULL_SK_LEN: usize = 4032; + const SIG_LEN: usize = 3309; + + const ALG_NAME: &'static str = ML_DSA_65_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; + /// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-65 { sigAlgs 18 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 18]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12]; + + type SigCTilde = [u8; 48]; // 𝜆/4 = 192/4 + type PolyZPacked = [u8; 640]; // 32 * (1 + bitlen(2^19 - 1)) = 32 * 20 + type PolyW1Packed = [u8; 128]; // 32 * bitlen(16 - 1) = 32 * 4 + type S1Packed = [u8; 640]; // 128 * 5 + type S2Packed = [u8; 768]; // 128 * 6 + type T1Packed = [u8; 1920]; // 320 * 6 +} + +impl MLDSAParams for MLDSA87Params { + const tau: i32 = 60; + const lambda: i32 = 256; + const gamma1: i32 = 1 << 19; + // mutants note: because of the bitshifting, the "- 1" ends up not mattering. + const gamma2: i32 = (q - 1) / 32; + const k: usize = 8; + const l: usize = 7; + const eta: usize = 2; + const omega: i32 = 75; + + const PK_LEN: usize = 2592; + const FULL_SK_LEN: usize = 4896; + const SIG_LEN: usize = 4627; + + const ALG_NAME: &'static str = ML_DSA_87_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; + /// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-87 { sigAlgs 19 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 19]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13]; + + type SigCTilde = [u8; 64]; // 𝜆/4 = 256/4 + type PolyZPacked = [u8; 640]; // 32 * (1 + bitlen(2^19 - 1)) = 32 * 20 + type PolyW1Packed = [u8; 128]; // 32 * bitlen(16 - 1) = 32 * 4 + type S1Packed = [u8; 672]; // 96 * 7 + type S2Packed = [u8; 768]; // 96 * 8 + type T1Packed = [u8; 2560]; // 320 * 8 +} + +/// The two distinct values 𝛾1 takes across the three parameter sets (FIPS 204, Table 1). +/// +/// The bit-packing routines have one layout per distinct 𝛾1, so they dispatch on these rather than +/// on the parameter set. ML-DSA-65 and ML-DSA-87 share the second value. +pub(crate) const GAMMA1_2_POW_17: i32 = MLDSA44Params::gamma1; +/// See [`GAMMA1_2_POW_17`]. +pub(crate) const GAMMA1_2_POW_19: i32 = MLDSA65Params::gamma1; + +/// The two distinct values 𝛾2 takes across the three parameter sets (FIPS 204, Table 1). +/// +/// As with 𝛾1, the routines that depend on 𝛾2 have one form per distinct value rather than one per +/// parameter set. ML-DSA-65 and ML-DSA-87 share the second value. +pub(crate) const GAMMA2_Q_MINUS_1_OVER_88: i32 = MLDSA44Params::gamma2; +/// See [`GAMMA2_Q_MINUS_1_OVER_88`]. +pub(crate) const GAMMA2_Q_MINUS_1_OVER_32: i32 = MLDSA65Params::gamma2; + +/// The weaker of two security strengths. +/// +/// [`SecurityStrength`]'s discriminants are assigned in increasing order of strength, so comparing +/// them as integers orders them. A `const fn` because the strength of a HashML-DSA pairing is a +/// defaulted associated const. +const fn weaker_of(a: SecurityStrength, b: SecurityStrength) -> SecurityStrength { + if (a as u8) <= (b as u8) { a } else { b } +} + +/// A crate-private (aka "sealed") trait that prevents a new HashML-DSA pairing from being defined +/// outside this crate. +trait HashMLDSAParamsInternalTrait {} + +/// One HashML-DSA algorithm: an ML-DSA parameter set paired with a pre-hash function. +/// +/// FIPS 204, Algorithm 4 (HashML-DSA.Sign) leaves the choice of PH open, so an instantiation is a +/// pairing rather than a single parameter set. Everything that varies across the pairings lives +/// here, so [`crate::hash_mldsa::HashMLDSA`] takes one type rather than a parameter set plus a +/// hash function plus a digest length. +/// +/// Sealed via a private supertrait, so the six types below are the only implementations. +pub trait HashMLDSAParams: HashMLDSAParamsInternalTrait { + /// The ML-DSA parameter set underneath. + type MLDSA: MLDSAParams; + /// PH, the pre-hash function. + type PreHash: Hash + HashAlgParams + AlgorithmOID + Default; + + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + /// + /// Written out per pairing rather than derived: it is the two component names spliced + /// together, and `&'static str` cannot be concatenated in a const context. + const ALG_NAME: &'static str; + + /* Derived. Never written out per pairing. */ + + /// The length of the pre-hash `ph`, which is just PH's output length. + const PH_LEN: usize = ::OUTPUT_LEN; + + /// The strength claimed for the pairing, as reported by `Algorithm::MAX_SECURITY_STRENGTH`. + /// + /// A HashML-DSA signature is no stronger than either of its two components, so this is the + /// weaker of the two. That is what caps, for example, HashML-DSA-87_with_SHA256 at 128 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = weaker_of( + ::MAX_SECURITY_STRENGTH, + ::MAX_SECURITY_STRENGTH, + ); +} + +/// The HashML-DSA-44_with_SHA256 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA44_with_SHA256Params; +/// The HashML-DSA-65_with_SHA256 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA65_with_SHA256Params; +/// The HashML-DSA-87_with_SHA256 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA87_with_SHA256Params; +/// The HashML-DSA-44_with_SHA512 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA44_with_SHA512Params; +/// The HashML-DSA-65_with_SHA512 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA65_with_SHA512Params; +/// The HashML-DSA-87_with_SHA512 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA87_with_SHA512Params; + +impl HashMLDSAParamsInternalTrait for HashMLDSA44_with_SHA256Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA65_with_SHA256Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA87_with_SHA256Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA44_with_SHA512Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA65_with_SHA512Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA87_with_SHA512Params {} + +impl HashMLDSAParams for HashMLDSA44_with_SHA256Params { + type MLDSA = MLDSA44Params; + type PreHash = SHA256; + const ALG_NAME: &'static str = HASH_ML_DSA_44_with_SHA256_NAME; +} +impl HashMLDSAParams for HashMLDSA65_with_SHA256Params { + type MLDSA = MLDSA65Params; + type PreHash = SHA256; + const ALG_NAME: &'static str = HASH_ML_DSA_65_WITH_SHA256_NAME; +} +impl HashMLDSAParams for HashMLDSA87_with_SHA256Params { + type MLDSA = MLDSA87Params; + type PreHash = SHA256; + const ALG_NAME: &'static str = HASH_ML_DSA_87_with_SHA256_NAME; +} +impl HashMLDSAParams for HashMLDSA44_with_SHA512Params { + type MLDSA = MLDSA44Params; + type PreHash = SHA512; + const ALG_NAME: &'static str = HASH_ML_DSA_44_with_SHA512_NAME; +} +impl HashMLDSAParams for HashMLDSA65_with_SHA512Params { + type MLDSA = MLDSA65Params; + type PreHash = SHA512; + const ALG_NAME: &'static str = HASH_ML_DSA_65_WITH_SHA512_NAME; +} +impl HashMLDSAParams for HashMLDSA87_with_SHA512Params { + type MLDSA = MLDSA87Params; + type PreHash = SHA512; + const ALG_NAME: &'static str = HASH_ML_DSA_87_WITH_SHA512_NAME; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mldsa::d; + + /// FIPS 204, Table 1, transcribed column by column: the eight values each parameter set + /// assigns. `(tau, lambda, gamma1, gamma2, k, l, eta, omega)`. + const TABLE_1: [(i32, i32, i32, i32, usize, usize, usize, i32); 3] = [ + (39, 128, 131072, (q - 1) / 88, 4, 4, 2, 80), + (49, 192, 524288, (q - 1) / 32, 6, 5, 4, 55), + (60, 256, 524288, (q - 1) / 32, 8, 7, 2, 75), + ]; + + /// FIPS 204, Table 2, transcribed row by row: `(private key, public key, signature)` in bytes. + /// The private key column is the full FIPS encoding, which this crate calls `FULL_SK_LEN`. + const TABLE_2: [(usize, usize, usize); 3] = + [(2560, 1312, 2420), (4032, 1952, 3309), (4896, 2592, 4627)]; + + /// FIPS 204, Table 1 also tabulates 𝛽, which it labels "𝛽 = 𝜏 ⋅ 𝜂". + const TABLE_1_BETA: [i32; 3] = [78, 196, 120]; + + fn check_table_1(i: usize) { + let (tau, lambda, gamma1, gamma2, k, l, eta, omega) = TABLE_1[i]; + assert_eq!(P::tau, tau, "{}: 𝜏", P::ALG_NAME); + assert_eq!(P::lambda, lambda, "{}: 𝜆", P::ALG_NAME); + assert_eq!(P::gamma1, gamma1, "{}: 𝛾1", P::ALG_NAME); + assert_eq!(P::gamma2, gamma2, "{}: 𝛾2", P::ALG_NAME); + assert_eq!(P::k, k, "{}: 𝑘", P::ALG_NAME); + assert_eq!(P::l, l, "{}: ℓ", P::ALG_NAME); + assert_eq!(P::eta, eta, "{}: 𝜂", P::ALG_NAME); + assert_eq!(P::omega, omega, "{}: 𝜔", P::ALG_NAME); + assert_eq!(P::beta, TABLE_1_BETA[i], "{}: 𝛽 = 𝜏 ⋅ 𝜂", P::ALG_NAME); + } + + fn check_table_2(i: usize) { + let (full_sk_len, pk_len, sig_len) = TABLE_2[i]; + assert_eq!(P::FULL_SK_LEN, full_sk_len, "{}: private key size", P::ALG_NAME); + assert_eq!(P::PK_LEN, pk_len, "{}: public key size", P::ALG_NAME); + assert_eq!(P::SIG_LEN, sig_len, "{}: signature size", P::ALG_NAME); + // This crate stores the seed, not the expanded key, for every parameter set. + assert_eq!(P::SK_LEN, 32, "{}: stored private key size", P::ALG_NAME); + } + + /// Each of the three sizes of Table 2 also has a formula in FIPS 204, and the two must agree. + /// Table 2 is what is written down above; this is what re-derives it. + fn check_table_2_formulas() { + // Algorithm 22 (pkEncode): 𝑝𝑘 ∈ 𝔹^(32+32𝑘(bitlen (𝑞−1)−𝑑)). + let pk_len = 32 + 32 * P::k * (bitlen((q - 1) as u32) - d as usize); + assert_eq!(P::PK_LEN, pk_len, "{}: Algorithm 22 output size", P::ALG_NAME); + + // Algorithm 24 (skEncode): 𝑠𝑘 ∈ 𝔹^(32+32+64+32⋅((𝑘+ℓ)⋅bitlen (2𝜂)+𝑑𝑘)). + let full_sk_len = + 32 + 32 + 64 + 32 * ((P::k + P::l) * bitlen(2 * P::eta as u32) + d as usize * P::k); + assert_eq!(P::FULL_SK_LEN, full_sk_len, "{}: Algorithm 24 output size", P::ALG_NAME); + + // Algorithm 26 (sigEncode): 𝜎 ∈ 𝔹^(𝜆/4+ℓ⋅32⋅(1+bitlen (𝛾1−1))+𝜔+𝑘). + let sig_len = P::lambda as usize / 4 + + P::l * 32 * (1 + bitlen(P::gamma1 as u32 - 1)) + + P::omega as usize + + P::k; + assert_eq!(P::SIG_LEN, sig_len, "{}: Algorithm 26 output size", P::ALG_NAME); + } + + /// The associated types must be exactly as long as the consts that describe them; they are + /// written out by hand per parameter set, so this guards against a typo in one of them. + fn check_associated_type_sizes() { + for (got, want, what) in [ + (size_of::(), P::C_TILDE_LEN, "SigCTilde"), + (size_of::(), P::POLY_Z_PACKED_LEN, "PolyZPacked"), + (size_of::(), P::POLY_W1_PACKED_LEN, "PolyW1Packed"), + (size_of::(), P::S1_PACKED_LEN, "S1Packed"), + (size_of::(), P::S2_PACKED_LEN, "S2Packed"), + (size_of::(), P::T1_PACKED_LEN, "T1Packed"), + ] { + assert_eq!(got, want, "{}: {} vs its length const", P::ALG_NAME, what); + } + } + + #[test] + fn test_parameter_sets_match_fips204_table_1() { + check_table_1::(0); + check_table_1::(1); + check_table_1::(2); + } + + #[test] + fn test_sizes_match_fips204_table_2() { + check_table_2::(0); + check_table_2::(1); + check_table_2::(2); + } + + #[test] + fn test_table_2_sizes_agree_with_the_encoding_formulas() { + check_table_2_formulas::(); + check_table_2_formulas::(); + check_table_2_formulas::(); + } + + #[test] + fn test_associated_types_are_the_length_their_consts_claim() { + check_associated_type_sizes::(); + check_associated_type_sizes::(); + check_associated_type_sizes::(); + } + + #[test] + fn test_bitlen_matches_its_definition() { + // FIPS 204 Section 2.3 defines bitlen 𝑥 as the length of the binary expansion of 𝑥. + assert_eq!(bitlen(0), 0); + assert_eq!(bitlen(1), 1); + assert_eq!(bitlen(2), 2); + assert_eq!(bitlen(3), 2); + assert_eq!(bitlen(4), 3); + // The two arguments the derivations above actually use, plus bitlen(𝑞 − 1) = 23. + assert_eq!(bitlen((1 << 17) - 1), 17); + assert_eq!(bitlen((1 << 19) - 1), 19); + assert_eq!(bitlen((q - 1) as u32), 23); + } + + #[test] + fn test_gamma_dispatch_constants_cover_every_parameter_set() { + // The packing routines dispatch on these; a parameter set whose 𝛾 is neither value would + // fall through to a panic at runtime rather than fail to compile, so pin them here. + for gamma1 in [MLDSA44Params::gamma1, MLDSA65Params::gamma1, MLDSA87Params::gamma1] { + assert!(gamma1 == GAMMA1_2_POW_17 || gamma1 == GAMMA1_2_POW_19); + } + for gamma2 in [MLDSA44Params::gamma2, MLDSA65Params::gamma2, MLDSA87Params::gamma2] { + assert!(gamma2 == GAMMA2_Q_MINUS_1_OVER_88 || gamma2 == GAMMA2_Q_MINUS_1_OVER_32); + } + assert_ne!(GAMMA1_2_POW_17, GAMMA1_2_POW_19); + assert_ne!(GAMMA2_Q_MINUS_1_OVER_88, GAMMA2_Q_MINUS_1_OVER_32); + } +} diff --git a/crypto/mldsa-lowmemory/src/polynomial.rs b/crypto/mldsa-lowmemory/src/polynomial.rs index 51e81ea4..6b38bbbe 100644 --- a/crypto/mldsa-lowmemory/src/polynomial.rs +++ b/crypto/mldsa-lowmemory/src/polynomial.rs @@ -1,7 +1,9 @@ //! Represents a polynomial over the ML-DSA ring. use crate::aux_functions::{high_bits, low_bits, make_hint, use_hint}; -use crate::mldsa::{MLDSA44_POLY_W1_PACKED_LEN, MLDSA65_POLY_W1_PACKED_LEN, N, q, q_inv}; +use crate::mldsa::{N, d, q, q_inv}; +use crate::params::{GAMMA2_Q_MINUS_1_OVER_32, GAMMA2_Q_MINUS_1_OVER_88, MLDSAParams}; +use bouncycastle_utils::secret::ZeroizablePrimitive; use core::ops::{Index, IndexMut}; /// A polynomial over the ML-DSA ring. @@ -75,19 +77,25 @@ impl Polynomial { } } - pub(crate) fn high_bits(&mut self) { + pub(crate) fn high_bits(&mut self) { for i in 0..N { - self[i] = high_bits::(self[i]); + self[i] = high_bits::

(self[i]); } } - pub(crate) fn low_bits(&mut self) { + pub(crate) fn low_bits(&mut self) { for i in 0..N { - self[i] = low_bits::(self[i]); + self[i] = low_bits::

(self[i]); } } - pub(crate) fn check_norm(&self) -> bool { + /// Tests whether any coefficient has absolute value at least `bound`. + /// + /// `bound` is a runtime argument rather than a const generic because every call site passes a + /// value derived from the parameter set (𝛾1 − 𝛽, 𝛾2 − 𝛽, or 𝛾2), and an associated const of a + /// type parameter cannot be used as a const generic argument. It is still a constant after + /// monomorphization, so this costs nothing. + pub(crate) fn check_norm(&self, bound: i32) -> bool { // Fine that this is not constant-time (returns true early) because it is used in a rejection loop. // IE the early quit here leads to rejection and continuing to the top of the rejection loop, or failing // the signature validation. @@ -97,33 +105,38 @@ impl Polynomial { // if bound > (q - 1) / 8 { // return true; // } - // but since BOUND is a constant here, a debug_assert is done to ensure the value is what is expected. - debug_assert!(BOUND <= (q - 1) / 8); + // but since every caller passes a parameter-set constant, a debug_assert is done instead + // to ensure the value is what is expected. + debug_assert!(bound <= (q - 1) / 8); let mut t: i32; for x in self.coeffs.iter() { t = *x >> 31; t = *x - (t & (2 * *x)); - if t >= BOUND { + if t >= bound { return true; } } false } - pub(crate) fn shift_left(&mut self) { + /// Multiplies every coefficient by 2^𝑑. + /// + /// 𝑑 is 13 for all three parameter sets (FIPS 204, Table 1), so it is read from the global + /// constant rather than being passed in. + pub(crate) fn shift_left_d(&mut self) { for x in self.coeffs.iter_mut() { *x <<= d; } } /// Creates the hint vector, and also returns its hamming weight (ie the number of 1's). - pub(crate) fn make_hint_row(&self, r: &Self) -> (Self, i32) { + pub(crate) fn make_hint_row(&self, r: &Self) -> (Self, i32) { let mut out = Polynomial::new(); let mut count = 0i32; for i in 0..N { - let x = make_hint::(self[i], r[i]); + let x = make_hint::

(self[i], r[i]); out[i] = x; count += x; } @@ -131,7 +144,14 @@ impl Polynomial { (out, count) } - pub(crate) fn w1_encode(&self) -> [u8; POLY_W1_PACKED_LEN] { + /// SimpleBitPack(𝐰1[𝑖], (𝑞 − 1)/(2𝛾2) − 1), the per-coordinate body of + /// FIPS 204, Algorithm 28 (w1Encode), line 3. + /// + /// The coefficient width is bitlen ((𝑞 − 1)/(2𝛾2) − 1), which is 6 bits for 𝛾2 = (𝑞 − 1)/88 and + /// 4 bits for 𝛾2 = (𝑞 − 1)/32, so there is one packing layout per distinct 𝛾2 rather than one + /// per parameter set. `P::gamma2` is a constant after monomorphization, so only the matching + /// arm survives. + pub(crate) fn w1_encode(&self) -> P::PolyW1Packed { // It might seem counter-intuitive for a low-memory implementation to create a tmp buffer // rather than work in the provided buffer, but experimentation shows that // rust is roughly an order of magnitude faster working in a scope-local array than @@ -141,18 +161,21 @@ impl Polynomial { // several hundred physical memory writes. // So while it looks odd to use a scope variable in a low-memory implementation, it's way faster // while seemingly maintaining the same physical memory footprint. - let mut r = [0u8; POLY_W1_PACKED_LEN]; + let mut out = ::ZEROED; + let r = out.as_mut(); - match POLY_W1_PACKED_LEN { - MLDSA44_POLY_W1_PACKED_LEN => { + match P::gamma2 { + // ML-DSA-44: (𝑞 − 1)/(2𝛾2) − 1 = 43, so four 6-bit coefficients pack into three bytes. + GAMMA2_Q_MINUS_1_OVER_88 => { for i in 0..N / 4 { r[3 * i] = ((self[4 * i]) as u8) | ((self[4 * i + 1] << 6) as u8); r[3 * i + 1] = ((self[4 * i + 1] >> 2) as u8) | ((self[4 * i + 2] << 4) as u8); r[3 * i + 2] = ((self[4 * i + 2] >> 4) as u8) | ((self[4 * i + 3] << 2) as u8); } } - // ML-DSA65 and 87 share a POLY_W1_PACKED_LEN value - MLDSA65_POLY_W1_PACKED_LEN => { + // ML-DSA-65 and ML-DSA-87 share this 𝛾2: (𝑞 − 1)/(2𝛾2) − 1 = 15, so two 4-bit + // coefficients pack into one byte. + GAMMA2_Q_MINUS_1_OVER_32 => { for i in 0..N / 2 { r[i] = ((self[2 * i]) | (self[2 * i + 1] << 4)) as u8; } @@ -162,7 +185,7 @@ impl Polynomial { } } - r + out } /// Algorithm 41 NTT(𝑤) @@ -244,9 +267,9 @@ impl Polynomial { } } - pub(crate) fn use_hint(&mut self, h: &Polynomial) { + pub(crate) fn use_hint(&mut self, h: &Polynomial) { for i in 0..N { - self[i] = use_hint::(self[i], h[i]); + self[i] = use_hint::

(self[i], h[i]); } } } diff --git a/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs index 92ffb445..6920ca41 100644 --- a/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/hash_mldsa_tests.rs @@ -6,7 +6,7 @@ mod hash_mldsa_tests { use super::*; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; - use bouncycastle_core::traits::{Hash, PHSignatureVerifier}; + use bouncycastle_core::traits::{Hash, PHSignatureVerifier, PHSigner}; use bouncycastle_core_test_framework::signature::TestFrameworkSignature; use bouncycastle_mldsa_lowmemory::{ HashMLDSA44_with_SHA256, HashMLDSA44_with_SHA512, HashMLDSA65_with_SHA256, @@ -234,4 +234,68 @@ mod hash_mldsa_tests { _ => panic!("Expected error"), } } + + #[test] + fn algorithm_names_strengths_and_oids() { + use bouncycastle_core::traits::{Algorithm, AlgorithmOID, SecurityStrength}; + + // `Algorithm` is implemented once, generically over the pairing, so nothing else states + // these per algorithm. + assert_eq!(HashMLDSA44_with_SHA256::ALG_NAME, "HashML-DSA-44_with_SHA256"); + assert_eq!(HashMLDSA65_with_SHA256::ALG_NAME, "HashML-DSA-65_with_SHA256"); + assert_eq!(HashMLDSA87_with_SHA256::ALG_NAME, "HashML-DSA-87_with_SHA256"); + assert_eq!(HashMLDSA44_with_SHA512::ALG_NAME, "HashML-DSA-44_with_SHA512"); + assert_eq!(HashMLDSA65_with_SHA512::ALG_NAME, "HashML-DSA-65_with_SHA512"); + assert_eq!(HashMLDSA87_with_SHA512::ALG_NAME, "HashML-DSA-87_with_SHA512"); + + // Derived as the weaker of the two components: SHA-256 caps every pairing it appears in at + // 128 bits; with SHA-512 the ML-DSA parameter set is what binds. + assert_eq!(HashMLDSA44_with_SHA256::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(HashMLDSA65_with_SHA256::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(HashMLDSA87_with_SHA256::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(HashMLDSA44_with_SHA512::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(HashMLDSA65_with_SHA512::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!(HashMLDSA87_with_SHA512::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); + + // NIST's Computer Security Objects Register: id-hash-ml-dsa-44-with-sha512 { sigAlgs 32 }, + // -65- { sigAlgs 33 }, -87- { sigAlgs 34 }. The three SHA-256 pairings carry no OID in + // this implementation, which is why `AlgorithmOID` is still written out per alias rather + // than derived from the pairing like the name and strength above. + assert_eq!(HashMLDSA44_with_SHA512::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 32]); + assert_eq!(HashMLDSA65_with_SHA512::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 33]); + assert_eq!(HashMLDSA87_with_SHA512::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 34]); + + for (oid, der) in [ + (HashMLDSA44_with_SHA512::OID, HashMLDSA44_with_SHA512::OID_DER), + (HashMLDSA65_with_SHA512::OID, HashMLDSA65_with_SHA512::OID_DER), + (HashMLDSA87_with_SHA512::OID, HashMLDSA87_with_SHA512::OID_DER), + ] { + assert_eq!(der[0], 0x06, "DER tag must be OBJECT IDENTIFIER"); + assert_eq!(der[1] as usize, der.len() - 2, "DER length must match the content"); + assert_eq!( + &der[2..], + &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, *oid.last().unwrap() as u8] + ); + } + } + + #[test] + fn prehash_lengths_match_the_hash_functions() { + // `PH_LEN` is still a const generic on `HashMLDSA` -- `PHSigner` takes it as one -- but + // no alias hard-codes it: each passes `{ ...Params::PH_LEN }`, which is the pre-hash's own + // `HashAlgParams::OUTPUT_LEN`. So there is only one value, and nothing in the chain can + // disagree with itself. What is left to check is whether that value matches the digest + // the hash actually produces, which is what this test does. + let msg = b"The quick brown fox"; + let ph256: [u8; 32] = SHA256::default().hash(msg).try_into().unwrap(); + let ph512: [u8; 64] = SHA512::default().hash(msg).try_into().unwrap(); + + let (pk, sk) = HashMLDSA65_with_SHA256::keygen().unwrap(); + let sig = HashMLDSA65_with_SHA256::sign_ph(&sk, &ph256, None).unwrap(); + HashMLDSA65_with_SHA256::verify_ph(&pk, &ph256, None, &sig).unwrap(); + + let (pk, sk) = HashMLDSA65_with_SHA512::keygen().unwrap(); + let sig = HashMLDSA65_with_SHA512::sign_ph(&sk, &ph512, None).unwrap(); + HashMLDSA65_with_SHA512::verify_ph(&pk, &ph512, None, &sig).unwrap(); + } } diff --git a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs index c1b4848d..69832aa2 100644 --- a/crypto/mldsa-lowmemory/tests/mldsa_tests.rs +++ b/crypto/mldsa-lowmemory/tests/mldsa_tests.rs @@ -907,6 +907,62 @@ mod mldsa_tests { MLDSA44::sign_mu_deterministic_out(&sk, &mu, [1u8; 32], &mut sig_buf).unwrap(); MLDSA44::verify(&pk, msg, None, &sig_buf).unwrap(); } + + #[test] + fn algorithm_names_and_oids() { + 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 + // blanket impls, or a typo in a parameter set, is a test failure rather than a silently + // mislabelled algorithm or an unparseable OID. + assert_eq!(MLDSA44::ALG_NAME, "ML-DSA-44"); + assert_eq!(MLDSA65::ALG_NAME, "ML-DSA-65"); + assert_eq!(MLDSA87::ALG_NAME, "ML-DSA-87"); + + assert_eq!(MLDSA44::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(MLDSA65::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!(MLDSA87::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); + + // NIST's Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 }, + // id-ml-dsa-65 { sigAlgs 18 }, id-ml-dsa-87 { sigAlgs 19 }. + assert_eq!(MLDSA44::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 17]); + assert_eq!(MLDSA65::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 18]); + assert_eq!(MLDSA87::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 19]); + + for (oid, der) in [ + (MLDSA44::OID, MLDSA44::OID_DER), + (MLDSA65::OID, MLDSA65::OID_DER), + (MLDSA87::OID, MLDSA87::OID_DER), + ] { + assert_eq!(der[0], 0x06, "DER tag must be OBJECT IDENTIFIER"); + assert_eq!(der[1] as usize, der.len() - 2, "DER length must match the content"); + assert_eq!( + &der[2..], + &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, *oid.last().unwrap() as u8] + ); + } + } + + #[test] + fn stored_private_key_is_the_seed_but_the_full_encoding_is_the_fips_one() { + // This crate is the seed-holding implementation, so `SK_LEN` is 32 for every parameter + // set while the parameter set's `FULL_SK_LEN` is FIPS 204, Table 2's private key column. + // The two are separate consts; this pins that they have not been conflated. + assert_eq!([MLDSA44_SK_LEN, MLDSA65_SK_LEN, MLDSA87_SK_LEN], [32, 32, 32]); + assert_eq!([MLDSA44_PK_LEN, MLDSA65_PK_LEN, MLDSA87_PK_LEN], [1312, 1952, 2592]); + assert_eq!([MLDSA44_SIG_LEN, MLDSA65_SIG_LEN, MLDSA87_SIG_LEN], [2420, 3309, 4627]); + + // `FULL_SK_LEN` is not a public constant, so it is checked through the encoding it sizes. + let seed = KeyMaterial256::from_bytes_as_type(&[7u8; 32], KeyType::Seed).unwrap(); + let (_, sk44) = MLDSA44::keygen_from_seed(&seed).unwrap(); + let (_, sk65) = MLDSA65::keygen_from_seed(&seed).unwrap(); + let (_, sk87) = MLDSA87::keygen_from_seed(&seed).unwrap(); + assert_eq!(sk44.encode().len(), 32, "the stored private key is the seed"); + assert_eq!(sk44.encode_full_sk().len(), 2560); + assert_eq!(sk65.encode_full_sk().len(), 4032); + assert_eq!(sk87.encode_full_sk().len(), 4896); + } } struct Kat { diff --git a/crypto/mldsa/src/aux_functions.rs b/crypto/mldsa/src/aux_functions.rs index 9eadbb89..bf0c2f91 100644 --- a/crypto/mldsa/src/aux_functions.rs +++ b/crypto/mldsa/src/aux_functions.rs @@ -1,14 +1,14 @@ //! Implements auxiliary functions for ML-DSA as defined in Section 7 of FIPS 204. -use crate::matrix::{Matrix, Vector}; -use crate::mldsa::{G, H, q_inv}; -use crate::mldsa::{ - MLDSA44_GAMMA1, MLDSA44_GAMMA2, MLDSA65_GAMMA1, MLDSA65_GAMMA2, N, POLY_T0PACKED_LEN, - POLY_T1PACKED_LEN, d, q, +use crate::matrix::{MatrixTrait, VectorTrait}; +use crate::mldsa::{G, H, N, POLY_T0PACKED_LEN, POLY_T1PACKED_LEN, d, q, q_inv}; +use crate::params::{ + GAMMA1_2_POW_17, GAMMA1_2_POW_19, GAMMA2_Q_MINUS_1_OVER_32, GAMMA2_Q_MINUS_1_OVER_88, + MLDSAParams, }; use crate::polynomial::Polynomial; use bouncycastle_core::traits::XOF; -use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; /// Algorithm 14 CoeffFromThreeBytes(𝑏0, 𝑏1, 𝑏2) /// Output: An integer modulo 𝑞 or ⊥. @@ -34,8 +34,8 @@ pub(crate) fn coeff_from_three_bytes(b: &[u8; 3]) -> Result { /// Input: Integer 𝑏 ∈ {0, 1, … , 15}. /// Output: An integer between −𝜂 and 𝜂, or ⊥. #[inline(always)] -pub(crate) fn coeff_from_half_byte(b: u8) -> Result { - if ETA == 2 && b < 15 { +pub(crate) fn coeff_from_half_byte(b: u8) -> Result { + if P::eta == 2 && b < 15 { // Original code is bad because '%' is not constant-time. // Ok(2 - (b % 5) as i32) // I'm still not convinced this is constant-time, but maybe it's closer? And I can't come up with anything better. @@ -46,7 +46,7 @@ pub(crate) fn coeff_from_half_byte(b: u8) -> Result { }; Ok(2 - b as i32) } else { - if ETA == 4 && b < 9 { Ok(4 - b as i32) } else { Err(()) } + if P::eta == 4 && b < 9 { Ok(4 - b as i32) } else { Err(()) } } } @@ -66,16 +66,6 @@ pub(crate) fn simple_bit_pack_t1(w: &Polynomial) -> [u8; POLY_T1PACKED_LEN] { output } -/// As defined in Algorithm 17, this gives the length of a packed bitstring representing a polynomial -/// whose coefficients have been rounded to \[-eta, eta], which is 32*bitlen(2*eta). -pub const fn bitlen_eta(eta: usize) -> usize { - match eta { - 2 => 32 * 3, - 4 => 32 * 4, - _ => panic!("Invalid eta value"), - } -} - /// A variant of Algorithm 17 BitPack specific to a=eta, b=eta /// Encodes a polynomial 𝑤 into a byte string. /// Input: 𝑎, 𝑏 ∈ ℕ and 𝑤 ∈ 𝑅 such that the coefficients of 𝑤 are all in \[−eta, eta]. @@ -84,13 +74,17 @@ pub const fn bitlen_eta(eta: usize) -> usize { // the hope here is that the compiler will aggressively inline this function, // and optimize away the branching. #[inline(always)] -pub(crate) fn bit_pack_eta(w: &Polynomial, r: &mut [u8]) { - debug_assert!(r.len() >= bitlen_eta(ETA)); +pub(crate) fn bit_pack_eta(w: &Polynomial, r: &mut [u8]) { + // `>=` rather than `==`: skEncode reuses one buffer sized for the largest 𝜂 across all three + // parameter sets and copies out only the first `POLY_ETA_PACKED_LEN` bytes, so for 𝜂 = 2 the + // buffer is deliberately longer than what gets written. Exactly that many bytes are written out + // either way, so this still catches an undersized buffer. + debug_assert!(r.len() >= P::POLY_ETA_PACKED_LEN); // temp swap space let mut t: [u8; 8] = [0; 8]; - match ETA { + match P::eta { // MLDSA44 and MLDSA87 2 => { let eta: i32 = 2; @@ -166,19 +160,21 @@ pub(crate) fn bit_pack_t0(t0: &Polynomial) -> [u8; POLY_T0PACKED_LEN] { } /// A variant of Algorithm 17 specific to packing z in the signature value in \[−𝛾1 + 1, 𝛾1]. -pub(crate) fn bitpack_gamma1( - z: &Polynomial, -) -> [u8; POLY_Z_PACKED_LEN] { - let mut r = [0u8; POLY_Z_PACKED_LEN]; +pub(crate) fn bitpack_gamma1(z: &Polynomial) -> P::PolyZPacked { + let mut out = ::ZEROED; + let r = out.as_mut(); let mut t: [u32; 4] = [0; 4]; - match GAMMA1 { - MLDSA44_GAMMA1 => { + // One layout per distinct 𝛾1 rather than one per parameter set; `P::gamma1` is a constant + // after monomorphization, so only the matching arm survives. + match P::gamma1 { + // MLDSA-44 + GAMMA1_2_POW_17 => { for i in 0..N / 4 { - t[0] = (GAMMA1 - z[4 * i]) as u32; - t[1] = (GAMMA1 - z[4 * i + 1]) as u32; - t[2] = (GAMMA1 - z[4 * i + 2]) as u32; - t[3] = (GAMMA1 - z[4 * i + 3]) as u32; + t[0] = (P::gamma1 - z[4 * i]) as u32; + t[1] = (P::gamma1 - z[4 * i + 1]) as u32; + t[2] = (P::gamma1 - z[4 * i + 2]) as u32; + t[3] = (P::gamma1 - z[4 * i + 3]) as u32; r[9 * i] = t[0] as u8; r[9 * i + 1] = (t[0] >> 8) as u8; @@ -191,11 +187,11 @@ pub(crate) fn bitpack_gamma1( r[9 * i + 8] = (t[3] >> 10) as u8; } } - // MLDSA-65 and 87 have the same GAMMA1 value - MLDSA65_GAMMA1 => { + // MLDSA-65 and -87 have the same GAMMA1 value + GAMMA1_2_POW_19 => { for i in 0..N / 2 { - t[0] = (GAMMA1 - z[2 * i]) as u32; - t[1] = (GAMMA1 - z[2 * i + 1]) as u32; + t[0] = (P::gamma1 - z[2 * i]) as u32; + t[1] = (P::gamma1 - z[2 * i + 1]) as u32; r[5 * i] = t[0] as u8; r[5 * i + 1] = (t[0] >> 8) as u8; @@ -209,7 +205,7 @@ pub(crate) fn bitpack_gamma1( } } - r + out } /// A specific instantiation of Algorithm 18 SimpleBitUnpack(v, 𝑏) with the constants set for unpacking the t1 vector @@ -241,12 +237,12 @@ pub(crate) fn simple_bit_unpack_t1(v: &[u8; POLY_T1PACKED_LEN]) -> Polynomial { // the hope here is that the compiler will aggressively inline this function, // and optimize away the branching. #[inline(always)] -pub(crate) fn bit_unpack_eta(v: &[u8]) -> Polynomial { - debug_assert_eq!(v.len(), bitlen_eta(ETA)); +pub(crate) fn bit_unpack_eta(v: &[u8]) -> Polynomial { + debug_assert_eq!(v.len(), P::POLY_ETA_PACKED_LEN); let mut w = Polynomial::new(); - match ETA { + match P::eta { // MLDSA44 and MLDSA87 2 => { let eta: i32 = 2; @@ -331,11 +327,12 @@ pub(crate) fn bit_unpack_t0(a: &[u8; POLY_T0PACKED_LEN]) -> Polynomial { /// When 𝑎 + 𝑏 + 1 is a power of 2, the coefficients are in [−𝑎, 𝑏]. /// /// Note: caller is responsible for ensuring correct input array size -pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { +pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { let mut w = Polynomial::new(); - match GAMMA1 { - MLDSA44_GAMMA1 => { + match P::gamma1 { + // MLDSA-44 + GAMMA1_2_POW_17 => { for i in 0..N / 4 { w[4 * i] = (((v[9 * i] as i32) | ((v[9 * i + 1] as i32) << 8)) | ((v[9 * i + 2] as i32) << 16)) @@ -350,14 +347,14 @@ pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { | ((v[9 * i + 8] as i32) << 10)) & 0x3FFFF; - w[4 * i] = GAMMA1 - w[4 * i]; - w[4 * i + 1] = GAMMA1 - w[4 * i + 1]; - w[4 * i + 2] = GAMMA1 - w[4 * i + 2]; - w[4 * i + 3] = GAMMA1 - w[4 * i + 3]; + w[4 * i] = P::gamma1 - w[4 * i]; + w[4 * i + 1] = P::gamma1 - w[4 * i + 1]; + w[4 * i + 2] = P::gamma1 - w[4 * i + 2]; + w[4 * i + 3] = P::gamma1 - w[4 * i + 3]; } } - // MLDSA-65 and 87 have the same GAMMA1 value - MLDSA65_GAMMA1 => { + // MLDSA-65 and -87 have the same GAMMA1 value + GAMMA1_2_POW_19 => { for i in 0..N / 2 { w[2 * i] = (((v[5 * i] as i32) | ((v[5 * i + 1] as i32) << 8)) | ((v[5 * i + 2] as i32) << 16)) @@ -366,8 +363,8 @@ pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { | ((v[5 * i + 4] as i32) << 12)) & 0xFFFFF; - w[2 * i] = GAMMA1 - w[2 * i]; - w[2 * i + 1] = GAMMA1 - w[2 * i + 1]; + w[2 * i] = P::gamma1 - w[2 * i]; + w[2 * i + 1] = P::gamma1 - w[2 * i + 1]; } } _ => { @@ -384,43 +381,36 @@ pub(crate) fn bit_unpack_gamma1(v: &[u8]) -> Polynomial { /// Output: Signature 𝜎 ∈ 𝔹𝜆/4+ℓ⋅32⋅(1+bitlen (𝛾1−1))+𝜔+𝑘. /// /// Returns the number of bytes written to the output buffer. -pub(crate) fn sig_encode< - const GAMMA1: i32, - const k: usize, - const l: usize, - const LAMBDA_over_4: usize, - const OMEGA: i32, - const POLY_Z_PACKED_LEN: usize, - const SIG_LEN: usize, ->( - c_tilde: &[u8; LAMBDA_over_4], - z: &Vector, - h: &Vector, +pub(crate) fn sig_encode( + c_tilde: &P::SigCTilde, + z: &P::VecL, + h: &P::VecK, output: &mut [u8; SIG_LEN], ) -> usize { + debug_assert_eq!(SIG_LEN, P::SIG_LEN); output.fill(0); let mut pos = 0; - output[..LAMBDA_over_4].copy_from_slice(c_tilde); - pos += LAMBDA_over_4; + output[..P::C_TILDE_LEN].copy_from_slice(c_tilde.as_ref()); + pos += P::C_TILDE_LEN; - for i in 0..l { - output[pos..pos + POLY_Z_PACKED_LEN] - .copy_from_slice(&bitpack_gamma1::(&z.elems[i])); - pos += POLY_Z_PACKED_LEN; + for i in 0..P::l { + output[pos..pos + P::POLY_Z_PACKED_LEN] + .copy_from_slice(bitpack_gamma1::

(&z.elems()[i]).as_ref()); + pos += P::POLY_Z_PACKED_LEN; } // This inlines Algorithm 20 HintBitPack(𝐡) let mut m: usize = 0; - for i in 0..k { + for i in 0..P::k { for j in 0..N { - if h.elems[i][j] != 0 { + if h.elems()[i][j] != 0 { output[pos + m] = j as u8; m += 1; } - output[pos + OMEGA as usize + i] = m as u8; + output[pos + P::omega as usize + i] = m as u8; } } @@ -432,29 +422,22 @@ pub(crate) fn sig_encode< /// Input: Signature 𝜎 ∈ 𝔹𝜆/4+ℓ⋅32⋅(1+bitlen (𝛾1−1))+𝜔+𝑘. /// Output: 𝑐 ∈ 𝔹𝜆/4, 𝐳 ∈ 𝑅ℓ with coefficients in \[−𝛾1 + 1, 𝛾1], 𝐡 ∈ 𝑅𝑘, or ⊥. /// Output: (c_tilde, z, h) -pub(crate) fn sig_decode< - const GAMMA1: i32, - const k: usize, - const l: usize, - const LAMBDA_over_4: usize, - const OMEGA: i32, - const POLY_Z_PACKED_LEN: usize, - const SIG_LEN: usize, ->( +pub(crate) fn sig_decode( sig: &[u8; SIG_LEN], -) -> Result<([u8; LAMBDA_over_4], Vector, Vector), ()> { - let mut c_tilde = [0u8; LAMBDA_over_4]; - let mut z: Vector = Vector::::new(); - let mut h: Vector = Vector::::new(); +) -> Result<(P::SigCTilde, P::VecL, P::VecK), ()> { + debug_assert_eq!(SIG_LEN, P::SIG_LEN); + let mut c_tilde = ::ZEROED; + let mut z = P::VecL::new(); + let mut h = P::VecK::new(); let mut pos: usize = 0; - c_tilde.copy_from_slice(&sig[..LAMBDA_over_4]); - pos += LAMBDA_over_4; + c_tilde.as_mut().copy_from_slice(&sig[..P::C_TILDE_LEN]); + pos += P::C_TILDE_LEN; - for i in 0..l { - z.elems[i] = bit_unpack_gamma1::(&sig[pos..pos + POLY_Z_PACKED_LEN]); - pos += POLY_Z_PACKED_LEN; + for i in 0..P::l { + z.elems_mut()[i] = bit_unpack_gamma1::

(&sig[pos..pos + P::POLY_Z_PACKED_LEN]); + pos += P::POLY_Z_PACKED_LEN; } // This inlines Algorithm 21 HintBitUnpack(𝑦) @@ -465,12 +448,12 @@ pub(crate) fn sig_decode< // 3: for 𝑖 from 0 to 𝑘 − 1 do // ▷ reconstruct 𝐡[𝑖] - for i in 0..k { + for i in 0..P::k { // 4: if 𝑦[𝜔 + 𝑖] < Index or 𝑦[𝜔 + 𝑖] > 𝜔 then return ⊥ // todo: this needs a specific test for malformed signature values. Maybe crucible coveres this case? // ... could hide an assert here and see if it triggers. - if sig[pos + (OMEGA as usize) + i] < (idx as u8) - || sig[pos + (OMEGA as usize) + i] > OMEGA as u8 + if sig[pos + (P::omega as usize) + i] < (idx as u8) + || sig[pos + (P::omega as usize) + i] > P::omega as u8 { return Err(()); } @@ -478,7 +461,7 @@ pub(crate) fn sig_decode< // 6: First ← Index // 7: while Index < 𝑦[𝜔 + 𝑖] do // ▷ 𝑦[𝜔 + 𝑖] says how far one can advance Index - for j in idx..sig[pos + OMEGA as usize + i] as usize { + for j in idx..sig[pos + P::omega as usize + i] as usize { // 8: if Index > First then // 9: if 𝑦[Index − 1] ≥ 𝑦[Index] then return ⊥ // ▷ malformed input @@ -486,17 +469,17 @@ pub(crate) fn sig_decode< return Err(()); } // 12: 𝐡[𝑖]_𝑦[Index] ← 1 - h.elems[i][sig[pos + j] as usize] = 1; + h.elems_mut()[i][sig[pos + j] as usize] = 1; // 13: Index ← Index + 1 // > done by for loop } - idx = sig[pos + OMEGA as usize + i] as usize; + idx = sig[pos + P::omega as usize + i] as usize; } // ▷ read any leftover bytes in the first 𝜔 bytes of 𝑦 for malformed (nonzero) bytes - for j in idx..OMEGA as usize { + for j in idx..P::omega as usize { if sig[pos + j] != 0 { return Err(()); } @@ -509,9 +492,7 @@ pub(crate) fn sig_decode< /// Samples a polynomial 𝑐 ∈ 𝑅 with coefficients from {−1, 0, 1} and Hamming weight 𝜏 ≤ 64. /// Input: A seed 𝜌 ∈ 𝔹𝜆/4 /// Output: A polynomial 𝑐 in 𝑅. -pub(crate) fn sample_in_ball( - rho: &[u8; LAMBDA_over_4], -) -> Polynomial { +pub(crate) fn sample_in_ball(rho: &P::SigCTilde) -> Polynomial { // 1: 𝑐 ← 0 let mut c = Polynomial::new(); @@ -519,7 +500,7 @@ pub(crate) fn sample_in_ball( // 3: ctx ← H.Absorb(ctx, 𝜌) // 4: (ctx, 𝑠) ← H.Squeeze(ctx, 8) let mut h = H::new(); - h.absorb(rho).expect("absorb before squeeze is infallible"); + h.absorb(rho.as_ref()).expect("absorb before squeeze is infallible"); let mut s = [0u8; 8]; h.squeeze_out(&mut s); @@ -535,7 +516,7 @@ pub(crate) fn sample_in_ball( // let mut pos = 8; // let mut b; let mut j = [0u8]; - for i in (N - TAU as usize)..N { + for i in (N - P::tau as usize)..N { // 7: (ctx, 𝑗) ← H.Squeeze(ctx, 1) // 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 @@ -624,7 +605,7 @@ pub(crate) fn rej_ntt_poly(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { /// This is supposed to take a rho: [u8; 66], which is: 𝜌||IntegerToBytes(𝑠, 1)||IntegerToBytes(𝑟, 1) /// but to avoid needing to copy bytes and allocate more memory, /// that is split into a [u8;64] and a [u8;2] -pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) -> Polynomial { +pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2]) -> Polynomial { let mut a = Polynomial::new(); let mut j: usize = 0; let mut h = H::new(); @@ -640,8 +621,8 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2] let mut idx: usize = 0; while j < N { - let z0 = coeff_from_half_byte::(z_arr[idx] & 0x0F); // equiv to % 16 (but faster, and more importantly, constant-time) - let z1 = coeff_from_half_byte::(z_arr[idx] >> 4); // equiv to div_floor(16) (but faster, and more importantly, constant-time) + let z0 = coeff_from_half_byte::

(z_arr[idx] & 0x0F); // equiv to % 16 (but faster, and more importantly, constant-time) + let z1 = coeff_from_half_byte::

(z_arr[idx] >> 4); // equiv to div_floor(16) (but faster, and more importantly, constant-time) if z0.is_ok() { a[j] = z0.unwrap(); @@ -667,12 +648,12 @@ pub(crate) fn rej_bounded_poly(rho: &[u8; 64], nonce: &[u8; 2] /// in other words: derives the public matrix from the public seed. /// Input: A seed 𝜌 ∈ 𝔹32 .̂ /// Output: Matrix  ∈ (𝑇𝑞)𝑘×ℓ . -pub(crate) fn expandA(rho: &[u8; 32]) -> Matrix { - let mut A_hat = Matrix::::new(); +pub(crate) fn expandA(rho: &[u8; 32]) -> P::MatrixA { + let mut A_hat = P::MatrixA::new(); - for r in 0..k { - for s in 0..l { - A_hat.elems[r][s] = rej_ntt_poly(rho, &[s as u8, r as u8]); + for r in 0..P::k { + for s in 0..P::l { + A_hat.set_elem(r, s, rej_ntt_poly(rho, &[s as u8, r as u8])); } } @@ -685,18 +666,16 @@ pub(crate) fn expandA(rho: &[u8; 32]) -> Matrix< /// Input: A seed 𝜌 ∈ 𝔹64 . /// Output: Vectors 𝐬1, 𝐬2 of secret polynomials in 𝑅 /// Note that this returns Secret> because s1, s2 are always part of a private key. -pub(crate) fn expandS( - rho: &[u8; 64], -) -> (Secret>, Secret>) { - let mut s1: Secret> = Secret::new(); - let mut s2: Secret> = Secret::new(); - - for r in 0..l { - s1.elems[r] = rej_bounded_poly::(rho, &(r as u16).to_le_bytes()); +pub(crate) fn expandS(rho: &[u8; 64]) -> (Secret, Secret) { + let mut s1: Secret = Secret::new(); + let mut s2: Secret = Secret::new(); + + for r in 0..P::l { + s1.elems_mut()[r] = rej_bounded_poly::

(rho, &(r as u16).to_le_bytes()); } - for r in 0..k { - s2.elems[r] = rej_bounded_poly::(rho, &(r as u16 + l as u16).to_le_bytes()); + for r in 0..P::k { + s2.elems_mut()[r] = rej_bounded_poly::

(rho, &(r as u16 + P::l as u16).to_le_bytes()); } (s1, s2) @@ -704,13 +683,13 @@ pub(crate) fn expandS( /// Implements the meta-function described in FIPS 204 section 7.4 for applying power_2_round to a vector. /// ((𝐫1\[𝑖])𝑗, (𝐫0\[𝑖])𝑗) = Power2Round((𝐫\[𝑖])𝑗). -pub(crate) fn power_2_round_vec(v: &Vector) -> (Vector, Vector) { - let mut r1 = Vector::::new(); - let mut r0 = Vector::::new(); +pub(crate) fn power_2_round_vec(v: &V) -> (V, V) { + let mut r1 = V::new(); + let mut r0 = V::new(); - for i in 0..LEN { + for i in 0..V::LEN { for j in 0..N { - (r1.elems[i][j], r0.elems[i][j]) = power_2_round(v.elems[i][j]); + (r1.elems_mut()[i][j], r0.elems_mut()[i][j]) = power_2_round(v.elems()[i][j]); } } @@ -721,17 +700,15 @@ pub(crate) fn power_2_round_vec(v: &Vector) -> (Vector( - rho: &[u8; 64], - mu: u16, -) -> Vector { - let mut y = Vector::::new(); +pub(crate) fn expand_mask(rho: &[u8; 64], mu: u16) -> P::VecL { + let mut y = P::VecL::new(); // 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) // ▷ 𝛾1 is always a power of 2 - // 32c = GAMMA1_MASK_LEN; + // 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`]. - for r in 0..l { + for r in 0..P::l { // 3: 𝜌′ ← 𝜌||IntegerToBytes(𝜇 + 𝑟, 2) // 4: 𝑣 ← H(𝜌′, 32𝑐) let v = { @@ -739,13 +716,13 @@ pub(crate) fn expand_mask::ZEROED; + h.squeeze_out(v.as_mut()); v }; // 5: 𝐲[𝑟] ← BitUnpack(𝑣, 𝛾1 − 1, 𝛾1) - y.elems[r] = bit_unpack_gamma1::(&v); + y.elems_mut()[r] = bit_unpack_gamma1::

(v.as_ref()); } y @@ -788,7 +765,7 @@ fn test_power_2_round() { /// Decomposes 𝑟 into (𝑟1, 𝑟0) such that 𝑟 ≡ 𝑟1(2𝛾2) + 𝑟0 mod 𝑞. /// Input: 𝑟 ∈ ℤ𝑞. /// Output: Integers (𝑟1, 𝑟0). -pub(crate) fn decompose(r: i32) -> (i32, i32) { +pub(crate) fn decompose(r: i32) -> (i32, i32) { // 1: 𝑟+ ← 𝑟 mod 𝑞 // 2: 𝑟0 ← 𝑟+ mod±(2𝛾2) // 3: if 𝑟+ − 𝑟0 = 𝑞 − 1 then @@ -803,14 +780,15 @@ pub(crate) fn decompose(r: i32) -> (i32, i32) { let mut r1: i32; let mut r0 = (r + 127) >> 7; - match GAMMA2 { - MLDSA44_GAMMA2 => { + match P::gamma2 { + // MLDSA-44 + GAMMA2_Q_MINUS_1_OVER_88 => { // (q - 1) / 88 r0 = (r0 * 11275 + (1 << 23)) >> 24; r0 ^= ((43 - r0) >> 31) & r0; } - // ML-DSA65 and 87 have the same GAMMA2 - MLDSA65_GAMMA2 => { + // ML-DSA-65 and -87 have the same GAMMA2 + GAMMA2_Q_MINUS_1_OVER_32 => { // (q - 1) / 32; r0 = (r0 * 1025 + (1 << 21)) >> 22; r0 &= 15; @@ -821,7 +799,7 @@ pub(crate) fn decompose(r: i32) -> (i32, i32) { } } - r1 = r - r0 * 2 * GAMMA2; + r1 = r - r0 * 2 * P::gamma2; // mutants note: the choice of (q - 1) is a bit arbitrary in that after doing the bit-shifting, // this seems to work out mathematically equivalent if doing q/2, or (q+3)/2, but here it is left as (q-1)/2 @@ -835,10 +813,10 @@ pub(crate) fn decompose(r: i32) -> (i32, i32) { /// Returns 𝑟1 from the output of Decompose (𝑟). /// Input: 𝑟 ∈ ℤ𝑞. /// Output: Integer 𝑟1. -pub(crate) fn high_bits(r: i32) -> i32 { +pub(crate) fn high_bits(r: i32) -> i32 { // 1: (𝑟1, 𝑟0) ← Decompose(𝑟) // 2: return 𝑟1 - let (r1, _) = decompose::(r); + let (r1, _) = decompose::

(r); r1 } @@ -846,10 +824,10 @@ pub(crate) fn high_bits(r: i32) -> i32 { /// Returns 𝑟0 from the output of Decompose (𝑟). /// Input: 𝑟 ∈ ℤ𝑞. /// Output: Integer 𝑟0. -pub(crate) fn low_bits(r: i32) -> i32 { +pub(crate) fn low_bits(r: i32) -> i32 { // 1: (𝑟1, 𝑟0) ← Decompose(𝑟) // 2: return 𝑟0 - let (_, r0) = decompose::(r); + let (_, r0) = decompose::

(r); r0 } @@ -857,32 +835,29 @@ pub(crate) fn low_bits(r: i32) -> i32 { /// Computes hint bit indicating whether adding 𝑧 to 𝑟 alters the high bits of 𝑟. /// Input: 𝑧, 𝑟 ∈ ℤ𝑞. /// Output: Boolean. -pub(crate) fn make_hint(z: i32, r: i32) -> i32 { +pub(crate) fn make_hint(z: i32, r: i32) -> i32 { // // 1: 𝑟1 ← HighBits(𝑟) - // let r1 = high_bits::(r); + // let r1 = high_bits::

(r); // // // 2: 𝑣1 ← HighBits(𝑟 + 𝑧) - // let v1 = high_bits::(r + z); + // let v1 = high_bits::

(r + z); // // // 3: return [[𝑟1 ≠ 𝑣1]] // if r1 != v1 { 1 } else { 0 } // By the powers of someone much more clever than me, this is equivalent. // mutants note: we do not have KATs that exercise all branches of this if - if z <= GAMMA2 || z > q - GAMMA2 || (z == q - GAMMA2 && r == 0) { 0 } else { 1 } + if z <= P::gamma2 || z > q - P::gamma2 || (z == q - P::gamma2 && r == 0) { 0 } else { 1 } } /// Creates the hint vector from two Vector's, and also returns its hamming weight (ie the number of 1's). -pub(crate) fn make_hint_vecs( - r: &Vector, - s: &Vector, -) -> (Vector, i32) { - let mut out = Vector::::new(); +pub(crate) fn make_hint_vecs(r: &P::VecK, s: &P::VecK) -> (P::VecK, i32) { + let mut out = P::VecK::new(); let mut count = 0i32; - for i in 0..k { - let (w, c) = r.elems[i].make_hint::(&s.elems[i]); - out.elems[i] = w; + for i in 0..P::k { + let (w, c) = r.elems()[i].make_hint::

(&s.elems()[i]); + out.elems_mut()[i] = w; // mutants note: this chains up to hint_hamming_weight > OMEGA and there is no test KAT that triggers this branch count += c; @@ -895,8 +870,8 @@ pub(crate) fn make_hint_vecs( /// Returns the high bits of 𝑟 adjusted according to hint ℎ. /// Input: Boolean ℎ, 𝑟 ∈ ℤ𝑞. /// Output: 𝑟1 ∈ ℤ with 0 ≤ 𝑟1 ≤ (𝑞−1) / 2*gamma2). -pub(super) fn use_hint(a: i32, hint: i32) -> i32 { - let (a0, a1) = decompose::(a); +pub(super) fn use_hint(a: i32, hint: i32) -> i32 { + let (a0, a1) = decompose::

(a); if hint == 0 { return a0; @@ -904,8 +879,8 @@ pub(super) fn use_hint(a: i32, hint: i32) -> i32 { debug_assert!(hint == 1); - match GAMMA2 { - MLDSA44_GAMMA2 => { + match P::gamma2 { + GAMMA2_Q_MINUS_1_OVER_88 => { // mutants note: this passes unit tests if it's a1 >= 0 // it is left like this because it matches the spec if a1 > 0 { @@ -915,7 +890,7 @@ pub(super) fn use_hint(a: i32, hint: i32) -> i32 { } } // ML-DSA65 and 87 have the same GAMMA2 - MLDSA65_GAMMA2 => { + GAMMA2_Q_MINUS_1_OVER_32 => { // mutants note: this passes unit tests if it's a1 >= 0 // it is left like this because it matches the spec if a1 > 0 { (a0 + 1) & 15 } else { (a0 - 1) & 15 } @@ -926,23 +901,20 @@ pub(super) fn use_hint(a: i32, hint: i32) -> i32 { } } -pub(crate) fn use_hint_polys( +pub(crate) fn use_hint_polys( wp_approx: &Polynomial, h: &Polynomial, out: &mut Polynomial, ) { for i in 0..N { - out[i] = use_hint::(wp_approx[i], h[i]); + out[i] = use_hint::

(wp_approx[i], h[i]); } } -pub(crate) fn use_hint_vecs( - h: &Vector, - wp_approx: &Vector, -) -> Vector { - let mut out = Vector::::new(); - for i in 0..k { - use_hint_polys::(&wp_approx.elems[i], &h.elems[i], &mut out.elems[i]); +pub(crate) fn use_hint_vecs(h: &P::VecK, wp_approx: &P::VecK) -> P::VecK { + let mut out = P::VecK::new(); + for i in 0..P::k { + use_hint_polys::

(&wp_approx.elems()[i], &h.elems()[i], &mut out.elems_mut()[i]); } out diff --git a/crypto/mldsa/src/hash_mldsa.rs b/crypto/mldsa/src/hash_mldsa.rs index 5e1b535a..35747605 100644 --- a/crypto/mldsa/src/hash_mldsa.rs +++ b/crypto/mldsa/src/hash_mldsa.rs @@ -66,29 +66,19 @@ //! But a simple [`HashMLDSA::keygen`] is provided. use crate::mldsa::{H, MLDSA_MU_LEN, MLDSA_RND_LEN, MLDSATrait}; -use crate::mldsa::{ - MLDSA44_BETA, MLDSA44_C_TILDE, MLDSA44_ETA, MLDSA44_GAMMA1, MLDSA44_GAMMA1_MASK_LEN, - MLDSA44_GAMMA1_MINUS_BETA, MLDSA44_GAMMA2, MLDSA44_GAMMA2_MINUS_BETA, MLDSA44_LAMBDA, - MLDSA44_LAMBDA_over_4, MLDSA44_OMEGA, MLDSA44_PK_LEN, MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_POLY_Z_PACKED_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN, MLDSA44_TAU, MLDSA44_k, MLDSA44_l, -}; -use crate::mldsa::{ - MLDSA65_BETA, MLDSA65_C_TILDE, MLDSA65_ETA, MLDSA65_GAMMA1, MLDSA65_GAMMA1_MASK_LEN, - MLDSA65_GAMMA1_MINUS_BETA, MLDSA65_GAMMA2, MLDSA65_GAMMA2_MINUS_BETA, MLDSA65_LAMBDA, - MLDSA65_LAMBDA_over_4, MLDSA65_OMEGA, MLDSA65_PK_LEN, MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_POLY_Z_PACKED_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN, MLDSA65_TAU, MLDSA65_k, MLDSA65_l, -}; -use crate::mldsa::{ - MLDSA87_BETA, MLDSA87_C_TILDE, MLDSA87_ETA, MLDSA87_GAMMA1, MLDSA87_GAMMA1_MASK_LEN, - MLDSA87_GAMMA1_MINUS_BETA, MLDSA87_GAMMA2, MLDSA87_GAMMA2_MINUS_BETA, MLDSA87_LAMBDA, - MLDSA87_LAMBDA_over_4, MLDSA87_OMEGA, MLDSA87_PK_LEN, MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_POLY_Z_PACKED_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN, MLDSA87_TAU, MLDSA87_k, MLDSA87_l, -}; +use crate::mldsa::{MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN}; +use crate::mldsa::{MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN}; +use crate::mldsa::{MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN}; use crate::mldsa_keys::{MLDSAPrivateKeyInternalTrait, MLDSAPublicKeyInternalTrait}; +use crate::params::{ + HashMLDSA44_with_SHA256Params, HashMLDSA44_with_SHA512Params, HashMLDSA65_with_SHA256Params, + HashMLDSA65_with_SHA512Params, HashMLDSA87_with_SHA256Params, HashMLDSA87_with_SHA512Params, + HashMLDSAParams, MLDSAParams, +}; use crate::{ MLDSA, MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyExpanded, MLDSAPrivateKeyTrait, - MLDSAPublicKeyExpanded, MLDSAPublicKeyTrait, Matrix, + MLDSAPublicKeyExpanded, MLDSAPublicKeyTrait, }; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; @@ -97,7 +87,6 @@ use bouncycastle_core::traits::{ SignatureVerifier, Signer, XOF, }; use bouncycastle_rng::HashDRBG_SHA512; -use bouncycastle_sha2::{SHA256, SHA512}; use core::marker::PhantomData; // Imports needed only for docs @@ -121,137 +110,53 @@ pub const HASH_ML_DSA_87_WITH_SHA512_NAME: &str = "HashML-DSA-87_with_SHA512"; /*** Pub Types ***/ -/// The HashML-DSA-44_with_SHA512 signature algorithm. +/// The HashML-DSA-44_with_SHA256 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA44_with_SHA256 = HashMLDSA< - SHA256, - 32, + HashMLDSA44_with_SHA256Params, + MLDSA44PublicKey, + MLDSA44PrivateKey, + { HashMLDSA44_with_SHA256Params::PH_LEN }, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_SIG_LEN, - MLDSA44PublicKey, - MLDSA44PrivateKey, - MLDSA44_TAU, - MLDSA44_LAMBDA, - MLDSA44_GAMMA1, - MLDSA44_GAMMA2, - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, - MLDSA44_BETA, - MLDSA44_OMEGA, - MLDSA44_C_TILDE, - MLDSA44_POLY_Z_PACKED_LEN, - MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_LAMBDA_over_4, - MLDSA44_GAMMA1_MINUS_BETA, - MLDSA44_GAMMA2_MINUS_BETA, - MLDSA44_GAMMA1_MASK_LEN, >; -impl Algorithm for HashMLDSA44_with_SHA256 { - const ALG_NAME: &'static str = HASH_ML_DSA_44_with_SHA256_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} - /// The HashML-DSA-65_with_SHA256 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA65_with_SHA256 = HashMLDSA< - SHA256, - 32, + HashMLDSA65_with_SHA256Params, + MLDSA65PublicKey, + MLDSA65PrivateKey, + { HashMLDSA65_with_SHA256Params::PH_LEN }, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_SIG_LEN, - MLDSA65PublicKey, - MLDSA65PrivateKey, - MLDSA65_TAU, - MLDSA65_LAMBDA, - MLDSA65_GAMMA1, - MLDSA65_GAMMA2, - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, - MLDSA65_BETA, - MLDSA65_OMEGA, - MLDSA65_C_TILDE, - MLDSA65_POLY_Z_PACKED_LEN, - MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_LAMBDA_over_4, - MLDSA65_GAMMA1_MINUS_BETA, - MLDSA65_GAMMA2_MINUS_BETA, - MLDSA65_GAMMA1_MASK_LEN, >; -impl Algorithm for HashMLDSA65_with_SHA256 { - const ALG_NAME: &'static str = HASH_ML_DSA_65_WITH_SHA256_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} - /// The HashML-DSA-87_with_SHA256 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA87_with_SHA256 = HashMLDSA< - SHA256, - 32, + HashMLDSA87_with_SHA256Params, + MLDSA87PublicKey, + MLDSA87PrivateKey, + { HashMLDSA87_with_SHA256Params::PH_LEN }, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_SIG_LEN, - MLDSA87PublicKey, - MLDSA87PrivateKey, - MLDSA87_TAU, - MLDSA87_LAMBDA, - MLDSA87_GAMMA1, - MLDSA87_GAMMA2, - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, - MLDSA87_BETA, - MLDSA87_OMEGA, - MLDSA87_C_TILDE, - MLDSA87_POLY_Z_PACKED_LEN, - MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_LAMBDA_over_4, - MLDSA87_GAMMA1_MINUS_BETA, - MLDSA87_GAMMA2_MINUS_BETA, - MLDSA87_GAMMA1_MASK_LEN, >; -impl Algorithm for HashMLDSA87_with_SHA256 { - const ALG_NAME: &'static str = HASH_ML_DSA_87_with_SHA256_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} - /// The HashML-DSA-44_with_SHA512 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA44_with_SHA512 = HashMLDSA< - SHA512, - 64, + HashMLDSA44_with_SHA512Params, + MLDSA44PublicKey, + MLDSA44PrivateKey, + { HashMLDSA44_with_SHA512Params::PH_LEN }, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_SIG_LEN, - MLDSA44PublicKey, - MLDSA44PrivateKey, - MLDSA44_TAU, - MLDSA44_LAMBDA, - MLDSA44_GAMMA1, - MLDSA44_GAMMA2, - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, - MLDSA44_BETA, - MLDSA44_OMEGA, - MLDSA44_C_TILDE, - MLDSA44_POLY_Z_PACKED_LEN, - MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_LAMBDA_over_4, - MLDSA44_GAMMA1_MINUS_BETA, - MLDSA44_GAMMA2_MINUS_BETA, - MLDSA44_GAMMA1_MASK_LEN, >; - -impl Algorithm for HashMLDSA44_with_SHA512 { - const ALG_NAME: &'static str = HASH_ML_DSA_44_with_SHA512_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} /// Assigned by NIST in the Computer Security Objects Register: id-hash-ml-dsa-44-with-sha512 { sigAlgs 32 } impl AlgorithmOID for HashMLDSA44_with_SHA512 { const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 32]; @@ -262,35 +167,14 @@ impl AlgorithmOID for HashMLDSA44_with_SHA512 { /// The HashML-DSA-65_with_SHA512 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA65_with_SHA512 = HashMLDSA< - SHA512, - 64, + HashMLDSA65_with_SHA512Params, + MLDSA65PublicKey, + MLDSA65PrivateKey, + { HashMLDSA65_with_SHA512Params::PH_LEN }, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_SIG_LEN, - MLDSA65PublicKey, - MLDSA65PrivateKey, - MLDSA65_TAU, - MLDSA65_LAMBDA, - MLDSA65_GAMMA1, - MLDSA65_GAMMA2, - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, - MLDSA65_BETA, - MLDSA65_OMEGA, - MLDSA65_C_TILDE, - MLDSA65_POLY_Z_PACKED_LEN, - MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_LAMBDA_over_4, - MLDSA65_GAMMA1_MINUS_BETA, - MLDSA65_GAMMA2_MINUS_BETA, - MLDSA65_GAMMA1_MASK_LEN, >; - -impl Algorithm for HashMLDSA65_with_SHA512 { - const ALG_NAME: &'static str = HASH_ML_DSA_65_WITH_SHA512_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; -} /// Assigned by NIST in the Computer Security Objects Register: id-hash-ml-dsa-65-with-sha512 { sigAlgs 33 } impl AlgorithmOID for HashMLDSA65_with_SHA512 { const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 33]; @@ -301,35 +185,14 @@ impl AlgorithmOID for HashMLDSA65_with_SHA512 { /// The HashML-DSA-87_with_SHA512 signature algorithm. #[allow(non_camel_case_types)] pub type HashMLDSA87_with_SHA512 = HashMLDSA< - SHA512, - 64, + HashMLDSA87_with_SHA512Params, + MLDSA87PublicKey, + MLDSA87PrivateKey, + { HashMLDSA87_with_SHA512Params::PH_LEN }, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_SIG_LEN, - MLDSA87PublicKey, - MLDSA87PrivateKey, - MLDSA87_TAU, - MLDSA87_LAMBDA, - MLDSA87_GAMMA1, - MLDSA87_GAMMA2, - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, - MLDSA87_BETA, - MLDSA87_OMEGA, - MLDSA87_C_TILDE, - MLDSA87_POLY_Z_PACKED_LEN, - MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_LAMBDA_over_4, - MLDSA87_GAMMA1_MINUS_BETA, - MLDSA87_GAMMA2_MINUS_BETA, - MLDSA87_GAMMA1_MASK_LEN, >; - -impl Algorithm for HashMLDSA87_with_SHA512 { - const ALG_NAME: &'static str = HASH_ML_DSA_87_WITH_SHA512_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; -} /// Assigned by NIST in the Computer Security Objects Register: id-hash-ml-dsa-87-with-sha512 { sigAlgs 34 } impl AlgorithmOID for HashMLDSA87_with_SHA512 { const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 34]; @@ -337,6 +200,21 @@ impl AlgorithmOID for HashMLDSA87_with_SHA512 { &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x22]; } +impl< + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, + const PH_LEN: usize, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +> Algorithm for HashMLDSA +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; +} + /// An instance of the HashML-DSA algorithm. /// /// The code is exposing the HashMLDSA struct this way so that alternative hash functions can be used @@ -344,32 +222,16 @@ impl AlgorithmOID for HashMLDSA87_with_SHA512 { /// by specifying the hash function to use (in the verifier), and specifying the bytes of the OID to /// to use as its domain separator in constructing the message representative M'. pub struct HashMLDSA< - HASH: Hash + AlgorithmOID + Default, - const HASH_LEN: usize, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, + const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > { - _phantom: PhantomData<(PK, SK)>, + _phantom: PhantomData<(P, PK, SK)>, signer_rnd: Option<[u8; MLDSA_RND_LEN]>, @@ -383,7 +245,7 @@ pub struct HashMLDSA< pk: Option, /// Hash function instance for streaming message hashing - hash: HASH, + hash: P::PreHash, /// Since HashML-DSA does message buffering in the external pre-hash, not in mu, /// this needs to be saved for later @@ -392,56 +254,15 @@ pub struct HashMLDSA< } impl< - HASH: Hash + AlgorithmOID + Default, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MASK_LEN: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, -> - HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> HashMLDSA { /// Generate a keypair, sourcing randomness from bouncycastle's default os-backed RNG. /// @@ -450,60 +271,16 @@ impl< /// Keygen, and keys in general, are interchangeable between MLDSA and HashMLDSA. /// Error condition: basically only on RNG failures. pub fn keygen() -> Result<(PK, SK), SignatureError> { - MLDSA::< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::keygen() + MLDSA::::keygen() } /// Imports a secret key from a seed. pub fn keygen_from_seed(seed: &KeyMaterial<32>) -> Result<(PK, SK), SignatureError> { - MLDSA::< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::keygen_internal(seed) + MLDSA::::keygen_internal(seed) } /// Same as [`Signer::sign`], but signs from an [`MLDSAPrivateKeyExpanded`]. pub fn sign_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; SIG_LEN], SignatureError> { @@ -514,7 +291,7 @@ impl< } /// Same as [`Signer::sign_out`], but signs from an [`MLDSAPrivateKeyExpanded`]. pub fn sign_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], @@ -522,12 +299,12 @@ impl< output.fill(0); let mut ph_m = [0u8; PH_LEN]; - _ = HASH::default().hash_out(msg, &mut ph_m); + _ = ::default().hash_out(msg, &mut ph_m); Self::sign_ph_with_expanded_key_out(sk, &ph_m, ctx, output) } /// Same as [`PHSigner::sign_ph`], but signs from an [`MLDSAPrivateKeyExpanded`]. pub fn sign_ph_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, ph: &[u8; PH_LEN], ctx: Option<&[u8]>, ) -> Result<[u8; SIG_LEN], SignatureError> { @@ -538,7 +315,7 @@ impl< } /// Same as [`PHSigner::sign_ph_out`], but signs from an [`MLDSAPrivateKeyExpanded`]. pub fn sign_ph_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, ph: &[u8; PH_LEN], ctx: Option<&[u8]>, output: &mut [u8; SIG_LEN], @@ -564,7 +341,7 @@ impl< /// prevent accidental nonce reuse, this function moves `rnd`. pub fn sign_ph_deterministic( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&::MatrixA>, ctx: Option<&[u8]>, ph: &[u8; PH_LEN], rnd: [u8; 32], @@ -587,7 +364,7 @@ impl< /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. pub fn sign_ph_deterministic_out( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&::MatrixA>, ctx: Option<&[u8]>, ph: &[u8; PH_LEN], rnd: [u8; 32], @@ -615,7 +392,8 @@ impl< 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(HASH::OID_DER).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"); let mut mu = [0u8; MLDSA_MU_LEN]; let bytes_written = h.squeeze_out(&mut mu); @@ -625,29 +403,10 @@ impl< }; // 24: 𝜎 ← ML-DSA.Sign_internal(𝑠𝑘, 𝑀', 𝑟𝑛𝑑) - let bytes_written = MLDSA::< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::sign_mu_deterministic_out(sk, A_hat, &mu, rnd, output)?; + let bytes_written = + MLDSA::::sign_mu_deterministic_out( + sk, A_hat, &mu, rnd, output, + )?; Ok(bytes_written) } @@ -688,27 +447,27 @@ impl< sk: None, seed: Some(seed.clone()), pk: None, - hash: HASH::default(), + hash: ::default(), ctx, ctx_len, }) } /// Same as [`SignatureVerifier::verify`], but verifies from an [`MLDSAPublicKeyExpanded`]. pub fn verify_with_expanded_key( - pk: &MLDSAPublicKeyExpanded, + pk: &MLDSAPublicKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], ) -> Result<(), SignatureError> { let mut ph_m = [0u8; PH_LEN]; - _ = HASH::default().hash_out(msg, &mut ph_m); + _ = ::default().hash_out(msg, &mut ph_m); Self::verify_ph_internal(&pk.pk, Some(&pk.A_hat()), &ph_m, ctx, sig) } fn verify_ph_internal( pk: &PK, - A_hat: Option<&Matrix>, + A_hat: Option<&::MatrixA>, ph: &[u8; PH_LEN], ctx: Option<&[u8]>, sig: &[u8], @@ -738,7 +497,8 @@ impl< 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(HASH::OID_DER).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"); let mut mu = [0u8; MLDSA_MU_LEN]; _ = h.squeeze_out(&mut mu); @@ -747,107 +507,32 @@ impl< }; match A_hat { - Some(A_hat) => MLDSA::< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::verify_mu(pk, Some(A_hat), &mu, sig_sized), - None => MLDSA::< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - >::verify_mu(pk, Some(&pk.A_hat()), &mu, sig_sized), + Some(A_hat) => MLDSA::::verify_mu( + pk, + Some(A_hat), + &mu, + sig_sized, + ), + None => MLDSA::::verify_mu( + pk, + Some(&pk.A_hat()), + &mu, + sig_sized, + ), } } } impl< - HASH: Hash + AlgorithmOID + Default, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> Signer - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> Signer for HashMLDSA { /// Algorithm 4 HashML-DSA.Sign(𝑠𝑘, 𝑀 , 𝑐𝑡𝑥, PH) /// Generate a “pre-hash” ML-DSA signature. @@ -867,7 +552,7 @@ impl< output.fill(0); let mut ph_m = [0u8; PH_LEN]; - _ = HASH::default().hash_out(msg, &mut ph_m); + _ = ::default().hash_out(msg, &mut ph_m); Self::sign_ph_out(sk, &ph_m, ctx, output) } @@ -879,7 +564,7 @@ impl< sk: Some(sk.clone()), seed: None, pk: None, - hash: HASH::default(), + hash: ::default(), ctx, ctx_len, }) @@ -946,60 +631,19 @@ impl< } impl< - HASH: Hash + AlgorithmOID + Default, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> SignatureVerifier - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> SignatureVerifier for HashMLDSA { fn verify(pk: &PK, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8]) -> Result<(), SignatureError> { let mut ph_m = [0u8; PH_LEN]; - _ = HASH::default().hash_out(msg, &mut ph_m); + _ = ::default().hash_out(msg, &mut ph_m); Self::verify_ph(pk, &ph_m, ctx, sig) } @@ -1012,7 +656,7 @@ impl< sk: None, seed: None, pk: Some(pk.clone()), - hash: HASH::default(), + hash: ::default(), ctx, ctx_len, }) @@ -1033,56 +677,16 @@ impl< } impl< - HASH: Hash + AlgorithmOID + Default, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MASK_LEN: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, > PHSigner - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > + for HashMLDSA { fn sign_ph( sk: &SK, @@ -1114,56 +718,16 @@ impl< } impl< - HASH: Hash + AlgorithmOID + Default, + P: HashMLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + + MLDSAPrivateKeyInternalTrait, const PH_LEN: usize, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MASK_LEN: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, > PHSignatureVerifier - for HashMLDSA< - HASH, - PH_LEN, - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > + for HashMLDSA { fn verify_ph( pk: &PK, diff --git a/crypto/mldsa/src/lib.rs b/crypto/mldsa/src/lib.rs index 2bea9873..15d0cb01 100644 --- a/crypto/mldsa/src/lib.rs +++ b/crypto/mldsa/src/lib.rs @@ -148,6 +148,7 @@ pub mod hash_mldsa; mod matrix; pub mod mldsa; mod mldsa_keys; +mod params; mod polynomial; /*** Exported types ***/ @@ -172,14 +173,6 @@ pub use mldsa::ML_DSA_44_NAME; pub use mldsa::ML_DSA_65_NAME; pub use mldsa::ML_DSA_87_NAME; -pub use hash_mldsa::HASH_ML_DSA_44_with_SHA256_NAME; -pub use hash_mldsa::HASH_ML_DSA_65_WITH_SHA256_NAME; -pub use hash_mldsa::HASH_ML_DSA_87_with_SHA256_NAME; - -pub use hash_mldsa::HASH_ML_DSA_44_with_SHA512_NAME; -pub use hash_mldsa::HASH_ML_DSA_65_WITH_SHA512_NAME; -pub use hash_mldsa::HASH_ML_DSA_87_WITH_SHA512_NAME; - pub use mldsa::{MLDSA_MU_LEN, MLDSA_RND_LEN, MLDSA_SEED_LEN, MLDSA_TR_LEN}; pub use mldsa::{MLDSA44_PK_LEN, MLDSA44_SIG_LEN, MLDSA44_SK_LEN}; pub use mldsa::{MLDSA65_PK_LEN, MLDSA65_SIG_LEN, MLDSA65_SK_LEN}; @@ -187,4 +180,10 @@ pub use mldsa::{MLDSA87_PK_LEN, MLDSA87_SIG_LEN, MLDSA87_SK_LEN}; pub use mldsa::SUSPENDED_MU_BUILDER_STATE_LEN; -pub use matrix::Matrix; +pub use hash_mldsa::HASH_ML_DSA_44_with_SHA256_NAME; +pub use hash_mldsa::HASH_ML_DSA_65_WITH_SHA256_NAME; +pub use hash_mldsa::HASH_ML_DSA_87_with_SHA256_NAME; + +pub use hash_mldsa::HASH_ML_DSA_44_with_SHA512_NAME; +pub use hash_mldsa::HASH_ML_DSA_65_WITH_SHA512_NAME; +pub use hash_mldsa::HASH_ML_DSA_87_WITH_SHA512_NAME; diff --git a/crypto/mldsa/src/matrix.rs b/crypto/mldsa/src/matrix.rs index 916fe47d..e08bb62f 100644 --- a/crypto/mldsa/src/matrix.rs +++ b/crypto/mldsa/src/matrix.rs @@ -3,11 +3,32 @@ 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_utils::secret::ZeroizablePrimitive; use core::ops::{Index, IndexMut}; +/// The operations this crate performs on the public matrix 𝐀̂. +/// +/// [`Matrix`] is the only implementation; see the module docs for why the trait exists. +pub(crate) trait MatrixTrait: Sized + Clone { + /// The vector this matrix can be applied to: an element of 𝑅^ℓ. + type VecL: VectorTrait; + /// The vector applying this matrix produces: an element of 𝑅^𝑘. + type VecK: VectorTrait; + + /// A matrix with every coefficient set to zero. + fn new() -> Self; + + /// Overwrites the polynomial at `elems[row][col]`. + fn set_elem(&mut self, row: usize, col: usize, p: Polynomial); + + /// Algorithm 48 MatrixVectorNTT(𝐌, 𝐯) + /// Computes the product 𝐌 ∘̂ 𝐯_hat of a matrix 𝐌_hat and a vector 𝐯_hat over 𝑇𝑞. + fn matrix_vector_ntt(&self, v: &Self::VecL) -> Self::VecK; +} + /// A matrix over the ML-DSA ring. #[derive(Clone)] pub struct Matrix { @@ -47,8 +68,88 @@ impl Matrix { } } +impl MatrixTrait for Matrix { + type VecL = Vector; + type VecK = Vector; + + fn new() -> Self { + Matrix::new() + } + + fn set_elem(&mut self, row: usize, col: usize, p: Polynomial) { + self.elems[row][col] = p; + } + + fn matrix_vector_ntt(&self, v: &Vector) -> Vector { + Matrix::matrix_vector_ntt(self, v) + } +} + +/// The operations this crate performs on a vector of polynomials, i.e. on an element of 𝑅^LEN. +/// +/// [`Vector`] is the only implementation; the trait exists so that code generic over a parameter +/// set can operate on [`MLDSAParams::VecK`] and [`MLDSAParams::VecL`] without knowing their length. +pub trait VectorTrait: + Sized + Copy + ZeroizablePrimitive + Index + IndexMut +{ + /// The number of polynomial coordinates, i.e. 𝑘 or ℓ. + const LEN: usize; + + /// A vector with every coefficient set to zero. + fn new() -> Self; + + /// The coordinates, for iteration and chunking. + fn elems(&self) -> &[Polynomial]; + /// The coordinates, for iteration and chunking. + fn elems_mut(&mut self) -> &mut [Polynomial]; + + /// Algorithm 46 AddVectorNTT(𝐯, 𝐰)̂ + /// Computes the sum 𝐯_hat + 𝐰_hat of two vectors 𝐯_hat, 𝐰_hat over 𝑇𝑞. + fn add_vector_ntt(&mut self, s: &Self); + + /// Subtracts another vector from this one, coordinatewise. + fn sub_vector(&self, s: &Self) -> Self; + + /// Algorithm 47 ScalarVectorNTT(𝑐,̂ 𝐯)̂ + /// Computes the product 𝑐_hat * 𝐯_hat of a scalar 𝑐_hat and a vector 𝐯_hat over 𝑇𝑞. + fn scalar_vector_ntt(&self, w: &Polynomial) -> Self; + + /// Adds 𝑞 to every negative coefficient. + fn conditional_add_q(&mut self); + + /// Montgomery-reduces every coefficient. + fn reduce(&mut self); + + /// Applies Algorithm 41 NTT(𝑤) to every coordinate. + fn ntt(&mut self); + + /// Applies Algorithm 42 NTT−1(𝑤_hat) to every coordinate. + fn inv_ntt(&mut self); + + /// Applies Algorithm 37 HighBits(𝑟) coefficientwise. + fn high_bits(&self) -> Self; + + /// Applies Algorithm 38 LowBits(𝑟) coefficientwise. + fn low_bits(&self) -> Self; + + /// Multiplies every coefficient by 2^𝑑. + fn shift_left_d(&self) -> Self; + + /// Tests whether any coefficient of any coordinate has absolute value at least `bound`. + /// See `Polynomial::check_norm` for why `bound` is not a const generic. + fn check_norm(&self, bound: i32) -> bool; + + /// Algorithm 28 w1Encode(𝐰1), fed straight into `h` rather than into a buffer. + fn w1_encode_and_hash(&self, h: &mut H); +} + +/// A vector of `LEN` polynomials, i.e. an element of 𝑅^LEN. +/// +/// Public only because it is the value of [`MLDSAParams::VecK`] and [`MLDSAParams::VecL`]; its +/// fields and operations are crate-private, so from outside it is an opaque handle. Reach it +/// through [`VectorTrait`]. #[derive(Clone, Copy)] -pub(crate) struct Vector { +pub struct Vector { pub(crate) elems: [Polynomial; LEN], } @@ -75,21 +176,37 @@ impl Vector { pub(crate) const fn new() -> Self { Self { elems: [Polynomial::new(); LEN] } } +} + +impl VectorTrait for Vector { + const LEN: usize = LEN; + + fn new() -> Self { + Vector::new() + } + + fn elems(&self) -> &[Polynomial] { + &self.elems + } + + fn elems_mut(&mut self) -> &mut [Polynomial] { + &mut self.elems + } /// Algorithm 46 AddVectorNTT(𝐯, 𝐰)̂ /// Computes the sum 𝐯_hat + 𝐰_hat of two vectors 𝐯_hat, 𝐰_hat over 𝑇𝑞. /// Input: ℓ ∈ ℕ, v_hat ∈ T^ℓ, w_hat ∈ 𝑇^ℓ /// Output: u_hat ∈ T^ℓ_𝑞. /// Add another vector to this vector - pub(crate) fn add_vector_ntt(&mut self, s: &Self) { + fn add_vector_ntt(&mut self, s: &Self) { for i in 0..LEN { // perform montgomery addition of each polynomial in the vector self[i].add_ntt(&s[i]); } } - pub(crate) fn sub_vector(&self, s: &Self) -> Self { - let mut out = self.clone(); + fn sub_vector(&self, s: &Self) -> Self { + let mut out = *self; for i in 0..LEN { out[i].sub(&s[i]); } @@ -100,8 +217,8 @@ impl Vector { /// Computes the product 𝑐_hat * 𝐯_hat of a scalar 𝑐_hat and a vector 𝐯_hat over 𝑇𝑞. /// Input: 𝑐_hat ∈ 𝑇𝑞, ℓ ∈ ℕ, 𝐯_hat ∈ 𝑇^ℓ /// Output: 𝑞 . - pub(crate) fn scalar_vector_ntt(&self, w: &Polynomial) -> Self { - let mut s_hat = Self::new(); + fn scalar_vector_ntt(&self, w: &Polynomial) -> Self { + let mut s_hat = Vector::::new(); for i in 0..LEN { s_hat[i] = multiply_ntt(&self[i], &w); } @@ -109,63 +226,63 @@ impl Vector { s_hat } - pub(crate) fn conditional_add_q(&mut self) { + fn conditional_add_q(&mut self) { for i in 0..LEN { self[i].conditional_add_q(); } } - pub(crate) fn reduce(&mut self) { + fn reduce(&mut self) { for i in 0..LEN { self[i].reduce(); } } - pub(crate) fn ntt(&mut self) { + fn ntt(&mut self) { for i in 0..LEN { self[i].ntt(); } } - pub(crate) fn inv_ntt(&mut self) { + fn inv_ntt(&mut self) { for i in 0..LEN { self[i].inv_ntt(); } } - pub(crate) fn high_bits(&self) -> Self { - let mut s = Self::new(); + fn high_bits(&self) -> Self { + let mut s = Vector::::new(); for i in 0..LEN { - s[i] = self[i].high_bits::(); + s[i] = self[i].high_bits::

(); } s } - pub(crate) fn low_bits(&self) -> Self { - let mut s = Self::new(); + fn low_bits(&self) -> Self { + let mut s = Vector::::new(); for i in 0..LEN { - s[i] = self[i].low_bits::(); + s[i] = self[i].low_bits::

(); } s } - pub(crate) fn shift_left(&self) -> Self { - let mut out = self.clone(); + fn shift_left_d(&self) -> Self { + let mut out = *self; for i in 0..LEN { - out[i].shift_left::(); + out[i].shift_left_d(); } out } - pub(crate) fn check_norm(&self) -> bool { + fn check_norm(&self, bound: i32) -> bool { // Fine that this is not constant-time because it is used in a rejection loop -- the early quit leads to rejection. for x in self.elems.iter() { - if x.check_norm::() { + if x.check_norm(bound) { return true; } } @@ -177,7 +294,7 @@ impl Vector { /// Input: 𝐰1 ∈ 𝑅𝑘 whose polynomial coordinates have coefficients in \[0, (𝑞 − 1)/(2𝛾2) − 1]. /// Output: A byte string representation 𝐰1_tilde ∈ 𝔹32𝑘⋅bitlen ((𝑞−1)/(2𝛾2)−1) /// Optimized from FIPS 204 to feed into the hash one row at a time to reduce overall memory footprint. - pub(crate) fn w1_encode_and_hash(&self, h: &mut H) { + fn w1_encode_and_hash(&self, h: &mut H) { // 1: 𝐰̃1 ← () // Nothing needs to be allocated since it is being fed into the hash row-wise @@ -185,8 +302,7 @@ impl Vector { // 3: 𝐰̃1 ← 𝐰̃1 || SimpleBitPack (𝐰1[𝑖], (𝑞 − 1)/(2𝛾2) − 1) // 4: end for for w in self.elems.iter() { - h.absorb(&w.w1_encode::()) - .expect("absorb before squeeze is infallible"); + h.absorb(w.w1_encode::

().as_ref()).expect("absorb before squeeze is infallible"); } } } diff --git a/crypto/mldsa/src/mldsa.rs b/crypto/mldsa/src/mldsa.rs index 842841a4..9e003579 100644 --- a/crypto/mldsa/src/mldsa.rs +++ b/crypto/mldsa/src/mldsa.rs @@ -479,9 +479,10 @@ use crate::aux_functions::{ expand_mask, expandA, expandS, make_hint_vecs, power_2_round_vec, sample_in_ball, sig_decode, sig_encode, use_hint_vecs, }; -use crate::matrix::{Matrix, Vector}; +use crate::matrix::{MatrixTrait, VectorTrait}; use crate::mldsa_keys::{MLDSAPrivateKeyInternalTrait, MLDSAPrivateKeyTrait}; use crate::mldsa_keys::{MLDSAPublicKeyInternalTrait, MLDSAPublicKeyTrait}; +use crate::params::{MLDSA44Params, MLDSA65Params, MLDSA87Params, MLDSAParams}; use crate::{ MLDSA44PrivateKey, MLDSA44PublicKey, MLDSA65PrivateKey, MLDSA65PublicKey, MLDSA87PrivateKey, MLDSA87PublicKey, MLDSAPrivateKeyExpanded, MLDSAPublicKeyExpanded, @@ -493,7 +494,7 @@ use bouncycastle_core::traits::{ }; use bouncycastle_rng::HashDRBG_SHA512; use bouncycastle_sha3::{SHAKE128, SHAKE256, SUSPENDED_SHA3_STATE_LEN}; -use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; use core::marker::PhantomData; // imports needed just for docs @@ -511,7 +512,7 @@ pub const ML_DSA_65_NAME: &str = "ML-DSA-65"; /// pub const ML_DSA_87_NAME: &str = "ML-DSA-87"; -// From FIPS 204 Table 1 and Table 2 +/*** From FIPS 204 Table 1 and Table 2 ***/ // Constants that are the same for all parameter sets pub(crate) const N: usize = 256; @@ -529,95 +530,28 @@ pub const MLDSA_MU_LEN: usize = 64; pub(crate) const POLY_T0PACKED_LEN: usize = 416; pub(crate) const POLY_T1PACKED_LEN: usize = 320; -/* ML-DSA-44 params */ - -/// Length of the \[u8] holding a ML-DSA-44 public key. -pub const MLDSA44_PK_LEN: usize = 1312; -/// Length of the \[u8] holding a ML-DSA-44 private key. -pub const MLDSA44_SK_LEN: usize = 2560; -/// Length of the \[u8] holding a ML-DSA-44 signature value. -pub const MLDSA44_SIG_LEN: usize = 2420; -pub(crate) const MLDSA44_TAU: i32 = 39; -pub(crate) const MLDSA44_LAMBDA: i32 = 128; -pub(crate) const MLDSA44_GAMMA1: i32 = 1 << 17; -pub(crate) const MLDSA44_GAMMA2: i32 = (q - 1) / 88; // mutants note: because of the bitshifting, the "- 1" ends up not mattering -pub(crate) const MLDSA44_k: usize = 4; -pub(crate) const MLDSA44_l: usize = 4; -pub(crate) const MLDSA44_ETA: usize = 2; -pub(crate) const MLDSA44_BETA: i32 = 78; -pub(crate) const MLDSA44_OMEGA: i32 = 80; - -// Useful derived values -pub(crate) const MLDSA44_C_TILDE: usize = 32; -pub(crate) const MLDSA44_POLY_Z_PACKED_LEN: usize = 576; -pub(crate) const MLDSA44_POLY_W1_PACKED_LEN: usize = 192; -pub(crate) const MLDSA44_LAMBDA_over_4: usize = 128 / 4; -pub(crate) const MLDSA44_GAMMA1_MINUS_BETA: i32 = MLDSA44_GAMMA1 - MLDSA44_BETA; -pub(crate) const MLDSA44_GAMMA2_MINUS_BETA: i32 = MLDSA44_GAMMA2 - MLDSA44_BETA; - -// Alg 32 -// 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) -pub(crate) const MLDSA44_GAMMA1_MASK_LEN: usize = 576; // 32*(1 + bitlen (𝛾1 − 1) ) - -/* ML-DSA-65 params */ - -/// Length of the \[u8] holding a ML-DSA-65 public key. -pub const MLDSA65_PK_LEN: usize = 1952; -/// Length of the \[u8] holding a ML-DSA-65 private key. -pub const MLDSA65_SK_LEN: usize = 4032; -/// Length of the \[u8] holding a ML-DSA-65 signature value. -pub const MLDSA65_SIG_LEN: usize = 3309; -pub(crate) const MLDSA65_TAU: i32 = 49; -pub(crate) const MLDSA65_LAMBDA: i32 = 192; -pub(crate) const MLDSA65_GAMMA1: i32 = 1 << 19; -pub(crate) const MLDSA65_GAMMA2: i32 = (q - 1) / 32; // mutants note: because of the bitshifting, the "- 1" ends up not mattering -pub(crate) const MLDSA65_k: usize = 6; -pub(crate) const MLDSA65_l: usize = 5; -pub(crate) const MLDSA65_ETA: usize = 4; -pub(crate) const MLDSA65_BETA: i32 = 196; -pub(crate) const MLDSA65_OMEGA: i32 = 55; - -// Useful derived values -pub(crate) const MLDSA65_C_TILDE: usize = 48; -pub(crate) const MLDSA65_POLY_Z_PACKED_LEN: usize = 640; -pub(crate) const MLDSA65_POLY_W1_PACKED_LEN: usize = 128; -pub(crate) const MLDSA65_LAMBDA_over_4: usize = 192 / 4; -pub(crate) const MLDSA65_GAMMA1_MINUS_BETA: i32 = MLDSA65_GAMMA1 - MLDSA65_BETA; -pub(crate) const MLDSA65_GAMMA2_MINUS_BETA: i32 = MLDSA65_GAMMA2 - MLDSA65_BETA; - -// Alg 32 -// 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) -pub(crate) const MLDSA65_GAMMA1_MASK_LEN: usize = 640; - -/* ML-DSA-87 params */ - -/// Length of the \[u8] holding a ML-DSA-87 public key. -pub const MLDSA87_PK_LEN: usize = 2592; -/// Length of the \[u8] holding a ML-DSA-87 private key. -pub const MLDSA87_SK_LEN: usize = 4896; -/// Length of the \[u8] holding a ML-DSA-87 signature value. -pub const MLDSA87_SIG_LEN: usize = 4627; -pub(crate) const MLDSA87_TAU: i32 = 60; -pub(crate) const MLDSA87_LAMBDA: i32 = 256; -pub(crate) const MLDSA87_GAMMA1: i32 = 1 << 19; -pub(crate) const MLDSA87_GAMMA2: i32 = (q - 1) / 32; // mutants note: because of the bitshifting, the "- 1" ends up not mattering -pub(crate) const MLDSA87_k: usize = 8; -pub(crate) const MLDSA87_l: usize = 7; -pub(crate) const MLDSA87_ETA: usize = 2; -pub(crate) const MLDSA87_BETA: i32 = 120; -pub(crate) const MLDSA87_OMEGA: i32 = 75; - -// Useful derived values -pub(crate) const MLDSA87_C_TILDE: usize = 64; -pub(crate) const MLDSA87_POLY_Z_PACKED_LEN: usize = 640; -pub(crate) const MLDSA87_POLY_W1_PACKED_LEN: usize = 128; -pub(crate) const MLDSA87_LAMBDA_over_4: usize = 256 / 4; -pub(crate) const MLDSA87_GAMMA1_MINUS_BETA: i32 = MLDSA87_GAMMA1 - MLDSA87_BETA; -pub(crate) const MLDSA87_GAMMA2_MINUS_BETA: i32 = MLDSA87_GAMMA2 - MLDSA87_BETA; - -// Alg 32 -// 1: 𝑐 ← 1 + bitlen (𝛾1 − 1) -pub(crate) const MLDSA87_GAMMA1_MASK_LEN: usize = 640; +/*** Re-exporting length constants that a caller will need instead of the entire Params objects which contains a bunch of internal algorithm detail ***/ + +/// Length of the \[u8] holding an ML-DSA-44 public key. +pub const MLDSA44_PK_LEN: usize = MLDSA44Params::PK_LEN; +/// Length of the \[u8] holding an ML-DSA-44 private key. +pub const MLDSA44_SK_LEN: usize = MLDSA44Params::SK_LEN; +/// Length of the \[u8] holding an ML-DSA-44 signature value. +pub const MLDSA44_SIG_LEN: usize = MLDSA44Params::SIG_LEN; + +/// Length of the \[u8] holding an ML-DSA-65 public key. +pub const MLDSA65_PK_LEN: usize = MLDSA65Params::PK_LEN; +/// Length of the \[u8] holding an ML-DSA-65 private key. +pub const MLDSA65_SK_LEN: usize = MLDSA65Params::SK_LEN; +/// Length of the \[u8] holding an ML-DSA-65 signature value. +pub const MLDSA65_SIG_LEN: usize = MLDSA65Params::SIG_LEN; + +/// Length of the \[u8] holding an ML-DSA-87 public key. +pub const MLDSA87_PK_LEN: usize = MLDSA87Params::PK_LEN; +/// Length of the \[u8] holding an ML-DSA-87 private key. +pub const MLDSA87_SK_LEN: usize = MLDSA87Params::SK_LEN; +/// Length of the \[u8] holding an ML-DSA-87 signature value. +pub const MLDSA87_SIG_LEN: usize = MLDSA87Params::SIG_LEN; // Typedefs just to make the algorithms look more like the FIPS 204 sample code. pub(crate) type H = SHAKE256; @@ -627,110 +561,63 @@ pub(crate) type G = SHAKE128; /// The ML-DSA-44 algorithm. pub type MLDSA44 = MLDSA< + MLDSA44Params, + MLDSA44PublicKey, + MLDSA44PrivateKey, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_SIG_LEN, - MLDSA44PublicKey, - MLDSA44PrivateKey, - MLDSA44_TAU, - MLDSA44_LAMBDA, - MLDSA44_GAMMA1, - MLDSA44_GAMMA2, - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, - MLDSA44_BETA, - MLDSA44_OMEGA, - MLDSA44_C_TILDE, - MLDSA44_POLY_Z_PACKED_LEN, - MLDSA44_POLY_W1_PACKED_LEN, - MLDSA44_LAMBDA_over_4, - MLDSA44_GAMMA1_MINUS_BETA, - MLDSA44_GAMMA2_MINUS_BETA, - MLDSA44_GAMMA1_MASK_LEN, >; -impl Algorithm for MLDSA44 { - const ALG_NAME: &'static str = ML_DSA_44_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} -/// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 } -impl AlgorithmOID for MLDSA44 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 17]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11]; -} - /// The ML-DSA-65 algorithm. pub type MLDSA65 = MLDSA< + MLDSA65Params, + MLDSA65PublicKey, + MLDSA65PrivateKey, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_SIG_LEN, - MLDSA65PublicKey, - MLDSA65PrivateKey, - MLDSA65_TAU, - MLDSA65_LAMBDA, - MLDSA65_GAMMA1, - MLDSA65_GAMMA2, - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, - MLDSA65_BETA, - MLDSA65_OMEGA, - MLDSA65_C_TILDE, - MLDSA65_POLY_Z_PACKED_LEN, - MLDSA65_POLY_W1_PACKED_LEN, - MLDSA65_LAMBDA_over_4, - MLDSA65_GAMMA1_MINUS_BETA, - MLDSA65_GAMMA2_MINUS_BETA, - MLDSA65_GAMMA1_MASK_LEN, >; -impl Algorithm for MLDSA65 { - const ALG_NAME: &'static str = ML_DSA_65_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; -} -/// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-65 { sigAlgs 18 } -impl AlgorithmOID for MLDSA65 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 18]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12]; -} - /// The ML-DSA-87 algorithm. pub type MLDSA87 = MLDSA< + MLDSA87Params, + MLDSA87PublicKey, + MLDSA87PrivateKey, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_SIG_LEN, - MLDSA87PublicKey, - MLDSA87PrivateKey, - MLDSA87_TAU, - MLDSA87_LAMBDA, - MLDSA87_GAMMA1, - MLDSA87_GAMMA2, - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, - MLDSA87_BETA, - MLDSA87_OMEGA, - MLDSA87_C_TILDE, - MLDSA87_POLY_Z_PACKED_LEN, - MLDSA87_POLY_W1_PACKED_LEN, - MLDSA87_LAMBDA_over_4, - MLDSA87_GAMMA1_MINUS_BETA, - MLDSA87_GAMMA2_MINUS_BETA, - MLDSA87_GAMMA1_MASK_LEN, >; -impl Algorithm for MLDSA87 { - const ALG_NAME: &'static str = ML_DSA_87_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +/// The name and claimed strength of an ML-DSA algorithm are properties of its parameter set, so +/// one impl covers all three; `MLDSAParams` is sealed, so those are the only three that exist. +impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +> Algorithm for MLDSA +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -/// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-87 { sigAlgs 19 } -impl AlgorithmOID for MLDSA87 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 19]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13]; + +/// The OIDs NIST assigned in the Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 }, +/// id-ml-dsa-65 { sigAlgs 18 } and id-ml-dsa-87 { sigAlgs 19 }. As with [`Algorithm`], the values +/// belong to the parameter set, so one impl covers all three. +impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, + const PK_LEN: usize, + const SK_LEN: usize, + const SIG_LEN: usize, +> AlgorithmOID for MLDSA +{ + const OID: &'static [u32] = P::OID; + const OID_DER: &'static [u8] = P::OID_DER; } /// The core internal implementation of the ML-DSA algorithm. @@ -738,30 +625,14 @@ impl AlgorithmOID for MLDSA87 { /// but it shouldn't ever need to be used directly. /// Please use the named public types [`MLDSA44`], [`MLDSA65`], [`MLDSA87`] instead. pub struct MLDSA< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_VEC_H_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, > { - _phantom: PhantomData<(PK, SK)>, + _phantom: PhantomData<(P, PK, SK)>, /// used for streaming the message for both signing and verifying mu_builder: MuBuilder, @@ -779,52 +650,13 @@ pub struct MLDSA< } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> - MLDSA< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> MLDSA { /// Implements Algorithm 6 of FIPS 204 /// Note: NIST has made a special exception in the FIPS 204 FAQ that this _internal function @@ -844,7 +676,7 @@ impl< )); } - if seed.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if seed.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(SignatureError::KeyGenError( "Seed SecurityStrength must match algorithm security strength", )); @@ -859,8 +691,8 @@ impl< // scope for h let mut h = H::default(); h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(l as u8).to_le_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); debug_assert_eq!(bytes_written, 32); let mut rho_prime: [u8; 64] = [0u8; 64]; @@ -870,7 +702,7 @@ impl< debug_assert_eq!(bytes_written, 32); // 4: (𝐬1, 𝐬2) ← ExpandS(𝜌′) - let (mut s1, s2) = expandS::(&rho_prime); + let (mut s1, s2) = expandS::

(&rho_prime); s1.ntt(); (s1, s2) @@ -879,7 +711,7 @@ impl< let t_hat = { // scope for s1_hat // 3: 𝐀_hat ← ExpandA(𝜌) ▷ 𝐀 is generated and stored in NTT representation as 𝐀 - let A_hat = expandA::(&rho); + let A_hat = expandA::

(&rho); // 5: 𝐭 ← NTT−1(𝐀 ∘ NTT(𝐬1)) + 𝐬2 // ▷ compute 𝐭 = 𝐀𝐬1 + 𝐬2 @@ -896,7 +728,7 @@ impl< // 6: (𝐭1, 𝐭0) ← Power2Round(𝐭) // ▷ compress 𝐭 // ▷ PowerTwoRound is applied componentwise (see explanatory text in Section 7.4) - power_2_round_vec::(&t) + power_2_round_vec(&t) }; // 8: 𝑝𝑘 ← pkEncode(𝜌, 𝐭1) @@ -928,7 +760,7 @@ impl< /// modified to take an externally-computed mu instead of M', and to take the public matrix A_hat fn sign_internal( sk: &SK, - A_hat: &Matrix, + A_hat: &P::MatrixA, mu: &[u8; 64], rnd: [u8; 32], output: &mut [u8; SIG_LEN], @@ -972,22 +804,22 @@ impl< // ▷ rejection sampling loop // these need to be outside the loop because they form the encoded signature value - let mut sig_val_c_tilde = [0u8; LAMBDA_over_4]; - let mut sig_val_z: Vector; - let mut sig_val_h: Vector; + let mut sig_val_c_tilde = ::ZEROED; + let mut sig_val_z: P::VecL; + let mut sig_val_h: P::VecK; loop { // FIPS 204 s. 6.2 allows: // "Implementations may limit the number of iterations in this loop to not exceed a finite maximum value." // mutants note: there is no test for this because, at this point, // we don't know of a KAT that will exceed this limit. - if kappa > 1000 * k as u16 { + if kappa > 1000 * P::k as u16 { return Err(SignatureError::GenericError( "Rejection sampling loop exceeded max iterations, try again with a different signing nonce.", )); } // 11: 𝐲 ∈ 𝑅^ℓ ← ExpandMask(𝜌″, 𝜅) - let mut y = expand_mask::(&rho_p_p, kappa); + let mut y = expand_mask::

(&rho_p_p, kappa); let w = { // scope for y_hat @@ -1002,7 +834,7 @@ impl< // 13: 𝐰1 ← HighBits(𝐰) // ▷ signer’s commitment - let w1 = w.high_bits::(); + let w1 = w.high_bits::

(); { // scope for h @@ -1010,15 +842,15 @@ impl< // ▷ commitment hash let mut hash = H::new(); hash.absorb(mu).expect("absorb before squeeze is infallible"); - w1.w1_encode_and_hash::(&mut hash); - hash.squeeze_out(&mut sig_val_c_tilde); + w1.w1_encode_and_hash::

(&mut hash); + hash.squeeze_out(sig_val_c_tilde.as_mut()); } // 16: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) // ▷ verifier’s challenge let c_hat = { // scope for c - let mut c = sample_in_ball::(&sig_val_c_tilde); + let mut c = sample_in_ball::

(&sig_val_c_tilde); // 17: 𝑐_hat ← NTT(𝑐) c.ntt(); @@ -1038,8 +870,8 @@ impl< // ▷ validity checks // This is done out-of-order on purpose for performance reasons: // rejection sampling check is done before any extra heavy computation - if sig_val_z.check_norm::() { - kappa += l as u16; + if sig_val_z.check_norm(P::gamma1_minus_beta) { + kappa += P::l as u16; continue; }; @@ -1048,7 +880,7 @@ impl< cs2.inv_ntt(); // 21: 𝐫0 ← LowBits(𝐰 − ⟨⟨𝑐𝐬2⟩⟩) - let mut r0 = w.sub_vector(&cs2).low_bits::(); + let mut r0 = w.sub_vector(&cs2).low_bits::

(); // 23 (second half): if ||𝐳||∞ ≥ 𝛾1 − 𝛽 or ||𝐫0||∞ ≥ 𝛾2 − 𝛽 then (z, h) ← ⊥ // ▷ validity checks @@ -1058,8 +890,8 @@ impl< // and checking whether ‖r0‖∞ < γ2 − β and r1 = w1, it is equivalent to just check that // ‖w0 − cs2‖∞ < γ2 − β, where w0 is the low part of w. If this check passes, w0 − cs2 // is the low part of w − cs2." - if r0.check_norm::() { - kappa += l as u16; + if r0.check_norm(P::gamma2_minus_beta) { + kappa += P::l as u16; continue; }; @@ -1071,8 +903,8 @@ impl< // This is done out-of-order on purpose for performance reasons: // rejection sampling check is done before any extra heavy computation // mutants note: there is currently no unit test that triggers this branch - if ct0.check_norm::() { - kappa += l as u16; + if ct0.check_norm(P::gamma2) { + kappa += P::l as u16; continue; }; @@ -1083,15 +915,15 @@ impl< let hint_hamming_weight: i32; sig_val_h = { // scope for hint - let (hint, inner_hint_hamming_weight) = make_hint_vecs::(&r0, &w1); + let (hint, inner_hint_hamming_weight) = make_hint_vecs::

(&r0, &w1); hint_hamming_weight = inner_hint_hamming_weight; hint }; // 28 (second half): if ||⟨⟨𝑐𝐭0⟩⟩||∞ ≥ 𝛾2 or the number of 1’s in 𝐡 is greater than 𝜔, then (z, h) ← ⊥ // mutants note: there is no test KAT that triggers this branch - if hint_hamming_weight > OMEGA { - kappa += l as u16; + if hint_hamming_weight > P::omega { + kappa += P::l as u16; continue; }; @@ -1106,9 +938,7 @@ impl< // 33: 𝜎 ← sigEncode(𝑐, 𝐳̃ mod±𝑞, 𝐡) let bytes_written = - sig_encode::( - &sig_val_c_tilde, &sig_val_z, &sig_val_h, output, - ); + sig_encode::(&sig_val_c_tilde, &sig_val_z, &sig_val_h, output); Ok(bytes_written) } @@ -1119,7 +949,7 @@ impl< /// Input: Signature 𝜎 ∈ 𝔹𝜆/4+ℓ⋅32⋅(1+bitlen (𝛾1−1))+𝜔+𝑘. fn verify_internal( pk: &PK, - A_hat: &Matrix, + A_hat: &P::MatrixA, mu: &[u8; 64], sig: &[u8; SIG_LEN], ) -> Result<(), SignatureError> { @@ -1129,12 +959,11 @@ impl< // 2: (𝑐_tilde, 𝐳, 𝐡) ← sigDecode(𝜎) // ▷ signer’s commitment hash c_tilde, response 𝐳, and hint 𝐡 // 3: if 𝐡 = ⊥ then return false - let (c_tilde, z, h) = - sig_decode::(&sig) - .map_err(|_| SignatureError::SignatureVerificationFailed)?; + let (c_tilde, z, h) = sig_decode::(&sig) + .map_err(|_| SignatureError::SignatureVerificationFailed)?; // 13 (first half) return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] - if z.check_norm::() { + if z.check_norm(P::gamma1_minus_beta) { return Err(SignatureError::SignatureVerificationFailed); } @@ -1151,7 +980,7 @@ impl< // 8: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) let c_hat = { - let mut c = sample_in_ball::(&c_tilde); + let mut c = sample_in_ball::

(&c_tilde); c.ntt(); c @@ -1173,7 +1002,7 @@ impl< }; let ct1 = { // potential optimization -- pre-compute this on key load? - let mut t1_shift_hat = pk.t1().shift_left::(); + let mut t1_shift_hat = pk.t1().shift_left_d(); t1_shift_hat.ntt(); t1_shift_hat.scalar_vector_ntt(&c_hat) }; @@ -1183,23 +1012,23 @@ impl< // 10: 𝐰1′ ← UseHint(𝐡, 𝐰'_approx) // ▷ reconstruction of signer’s commitment - use_hint_vecs::(&h, &wp_approx) + use_hint_vecs::

(&h, &wp_approx) }; // 12: 𝑐_tilde_p ← H(𝜇||w1Encode(𝐰1'), 𝜆/4) // ▷ hash it; this should match 𝑐_tilde let c_tilde_p = { - let mut c_tilde_p = [0u8; LAMBDA_over_4]; + let mut c_tilde_p = ::ZEROED; let mut hash = H::new(); hash.absorb(mu).expect("absorb before squeeze is infallible"); - w1p.w1_encode_and_hash::(&mut hash); - hash.squeeze_out(&mut c_tilde_p); + w1p.w1_encode_and_hash::

(&mut hash); + hash.squeeze_out(c_tilde_p.as_mut()); c_tilde_p }; // verification probably doesn't technically need to be constant-time, but why not? // 13 (second half): return [[ ||𝐳||∞ < 𝛾1 − 𝛽]] and [[𝑐 ̃ = 𝑐′ ]] - if bouncycastle_utils::ct::ct_eq_bytes(&c_tilde, &c_tilde_p) { + if bouncycastle_utils::ct::ct_eq_bytes(c_tilde.as_ref(), c_tilde_p.as_ref()) { Ok(()) } else { Err(SignatureError::SignatureVerificationFailed) @@ -1208,52 +1037,13 @@ impl< } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> MLDSATrait - for MLDSA< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> MLDSATrait for MLDSA { fn keygen_from_seed(seed: &KeyMaterial<32>) -> Result<(PK, SK), SignatureError> { Self::keygen_internal(seed) @@ -1290,21 +1080,21 @@ impl< MuBuilder::compute_mu(tr, msg, ctx) } fn compute_mu_from_pk( - pk: &impl MLDSAPublicKeyTrait, + pk: &impl MLDSAPublicKeyTrait, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; 64], SignatureError> { MuBuilder::compute_mu(&pk.compute_tr(), msg, ctx) } fn compute_mu_from_sk( - sk: &impl MLDSAPrivateKeyTrait, + sk: &impl MLDSAPrivateKeyTrait, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; 64], SignatureError> { MuBuilder::compute_mu(&sk.tr(), msg, ctx) } fn sign_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; SIG_LEN], SignatureError> { @@ -1313,7 +1103,7 @@ impl< } fn sign_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, out: &mut [u8; SIG_LEN], @@ -1326,7 +1116,7 @@ impl< fn sign_mu( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], ) -> Result<[u8; SIG_LEN], SignatureError> { let mut out: [u8; SIG_LEN] = [0u8; SIG_LEN]; @@ -1336,7 +1126,7 @@ impl< } fn sign_mu_out( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], output: &mut [u8; SIG_LEN], ) -> Result { @@ -1348,8 +1138,8 @@ impl< Self::sign_mu_deterministic_out(sk, A_hat, mu, rnd, output) } fn sign_mu_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, - A_hat: Option<&Matrix>, + sk: &MLDSAPrivateKeyExpanded, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], ) -> Result<[u8; SIG_LEN], SignatureError> { let mut out: [u8; SIG_LEN] = [0u8; SIG_LEN]; @@ -1358,8 +1148,8 @@ impl< Ok(out) } fn sign_mu_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, - A_hat: Option<&Matrix>, + sk: &MLDSAPrivateKeyExpanded, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], out: &mut [u8; SIG_LEN], ) -> Result { @@ -1370,7 +1160,7 @@ impl< fn sign_mu_deterministic( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], rnd: [u8; 32], ) -> Result<[u8; SIG_LEN], SignatureError> { @@ -1381,7 +1171,7 @@ impl< } fn sign_mu_deterministic_out( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], rnd: [u8; 32], output: &mut [u8; SIG_LEN], @@ -1409,7 +1199,7 @@ impl< /// This is a middle ground between keygen_from_seed()+sign_mu() and /// the fully streamed low-memory implementation. // TODO: benchmark peak memory + runtime against - // keygen_from_seed() + sign_mu_deterministic() to confirm the separate path earns being kept. + // keygen_from_seed() + sign_mu_deterministic() to confirm the separate path earns being kept. // Note: this path intentionally avoids the public key entirely // (no pkEncode / tr = H(pk)) since μ is supplied externally. fn sign_mu_deterministic_from_seed_out( @@ -1436,7 +1226,7 @@ impl< )); } - if seed.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if seed.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(SignatureError::KeyGenError( "Seed SecurityStrength must match algorithm security strength: 128-bit (ML-DSA-44), 192-bit (ML-DSA-65), or 256-bit (ML-DSA-87).", )); @@ -1453,8 +1243,8 @@ impl< let (rho, rho_prime, K) = { let mut h = H::default(); h.absorb(seed.ref_to_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(k as u8).to_le_bytes()).expect("absorb before squeeze is infallible"); - h.absorb(&(l as u8).to_le_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 mut rho = [0u8; 32]; let bytes_written = h.squeeze_out(&mut rho); debug_assert_eq!(bytes_written, 32); @@ -1481,7 +1271,7 @@ impl< }; // 4: (𝐬1, 𝐬2) ← ExpandS(𝜌′) - let (s1, s2) = expandS::(&rho_prime); + let (s1, s2) = expandS::

(&rho_prime); (rho, rho_p_p, s1, s2) }; @@ -1494,7 +1284,7 @@ impl< // as 20 or even 80 times. So moving expandA() inside the loop would be a pretty drastic speed-for-memory tradeoff // whose generality falls out of the scope of this implementation. // It is left as an optimization that can be made by users that require further reduction of memory usage - let A_hat = expandA::(&rho); + let A_hat = expandA::

(&rho); // Alg 7; 8: 𝜅 ← 0 // ▷ initialize counter 𝜅 @@ -1507,21 +1297,21 @@ impl< // ▷ rejection sampling loop // these need to be outside the loop because they form the encoded signature value - let mut sig_val_c_tilde = [0u8; LAMBDA_over_4]; - let mut sig_val_z: Vector; - let mut sig_val_h: Vector; + let mut sig_val_c_tilde = ::ZEROED; + let mut sig_val_z: P::VecL; + let mut sig_val_h: P::VecK; loop { // FIPS 204 s. 6.2 allows: // "Implementations may limit the number of iterations in this loop to not exceed a finite maximum value." // mutants note: there is no test for this because we don't know of a KAT that will exceed this limit. - if kappa > 1000 * k as u16 { + if kappa > 1000 * P::k as u16 { return Err(SignatureError::GenericError( "Rejection sampling loop exceeded max iterations, try again with a different signing nonce.", )); } // Alg 7; 11: 𝐲 ∈ 𝑅^ℓ ← ExpandMask(𝜌″, 𝜅) - let mut y = expand_mask::(&rho_p_p, kappa); + let mut y = expand_mask::

(&rho_p_p, kappa); let w = { // scope for y_hat @@ -1536,7 +1326,7 @@ impl< // Alg 7; 13: 𝐰1 ← HighBits(𝐰) // ▷ signer’s commitment - let w1 = w.high_bits::(); + let w1 = w.high_bits::

(); { // scope for h @@ -1544,22 +1334,22 @@ impl< // ▷ commitment hash let mut hash = H::new(); hash.absorb(mu).expect("absorb before squeeze is infallible"); - w1.w1_encode_and_hash::(&mut hash); - hash.squeeze_out(&mut sig_val_c_tilde); + w1.w1_encode_and_hash::

(&mut hash); + hash.squeeze_out(sig_val_c_tilde.as_mut()); } // Alg 7; 16: 𝑐 ∈ 𝑅𝑞 ← SampleInBall(c_tilde) // ▷ verifier’s challenge let c_hat = { // scope for c - let mut c = sample_in_ball::(&sig_val_c_tilde); + let mut c = sample_in_ball::

(&sig_val_c_tilde); // 17: 𝑐_hat ← NTT(𝑐) c.ntt(); c }; - let t_hat: Vector; + let t_hat: P::VecK; sig_val_z = { // scope for s1_hat, cs1 // Alg 7; 2: 𝐬1̂_hat ← NTT(𝐬1) @@ -1591,13 +1381,13 @@ impl< // ▷ validity checks // This is done out-of-order on purpose for performance reasons: // rejection sampling check is done before any extra heavy computation - if sig_val_z.check_norm::() { - kappa += l as u16; + if sig_val_z.check_norm(P::gamma1_minus_beta) { + kappa += P::l as u16; continue; }; - let t0: Vector; - let mut r0: Vector = { + let t0: P::VecK; + let mut r0: P::VecK = { // scope for s2_hat and cs2 // 3: 𝐬2̂_hat ← NTT(𝐬2) let mut s2_hat = s2.clone(); @@ -1608,7 +1398,7 @@ impl< cs2.inv_ntt(); // 21: 𝐫0 ← LowBits(𝐰 − ⟨⟨𝑐𝐬2⟩⟩) - let r0 = w.sub_vector(&cs2).low_bits::(); + let r0 = w.sub_vector(&cs2).low_bits::

(); // while s2_hat is in scope, derive t0 let mut t = t_hat; @@ -1619,7 +1409,7 @@ impl< // 6: (𝐭1, 𝐭0) ← Power2Round(𝐭) // ▷ compress 𝐭 // ▷ PowerTwoRound is applied componentwise (see explanatory text in Section 7.4) - let (_t1tmp, t0tmp) = power_2_round_vec::(&t); + let (_t1tmp, t0tmp) = power_2_round_vec(&t); t0 = t0tmp; r0 @@ -1627,14 +1417,14 @@ impl< // Alg 7; 23 (second half): if ||𝐳||∞ ≥ 𝛾1 − 𝛽 or ||𝐫0||∞ ≥ 𝛾2 − 𝛽 then (z, h) ← ⊥ // ▷ validity checks - if r0.check_norm::() { + if r0.check_norm(P::gamma2_minus_beta) { // mutants note: mutants thinks this can be replaced with -=, but in practice that makes // the rejection sampling loop go forever, so is a false positive. - kappa += l as u16; + kappa += P::l as u16; continue; }; - let ct0: Vector = { + let ct0: P::VecK = { // scope for t0_hat // 4: 𝐭0̂_hat ← NTT(𝐭0)̂ let mut t0_hat = t0.clone(); @@ -1650,8 +1440,8 @@ impl< // out-of-order on purpose for performance reasons: // might as well do the rejection sampling check before any extra heavy computation // mutants note: there is currently no unit test that triggers this branch - if ct0.check_norm::() { - kappa += l as u16; + if ct0.check_norm(P::gamma2) { + kappa += P::l as u16; continue; }; @@ -1662,15 +1452,15 @@ impl< let hint_hamming_weight: i32; sig_val_h = { // scope for hint - let (hint, inner_hint_hamming_weight) = make_hint_vecs::(&r0, &w1); + let (hint, inner_hint_hamming_weight) = make_hint_vecs::

(&r0, &w1); hint_hamming_weight = inner_hint_hamming_weight; hint }; // Alg 7; 28 (second half): if ||⟨⟨𝑐𝐭0⟩⟩||∞ ≥ 𝛾2 or the number of 1’s in 𝐡 is greater than 𝜔, then (z, h) ← ⊥ // mutants note: there is currently no unit test that triggers this branch - if hint_hamming_weight > OMEGA { - kappa += l as u16; + if hint_hamming_weight > P::omega { + kappa += P::l as u16; continue; }; @@ -1686,9 +1476,7 @@ impl< // Alg 7; 33: 𝜎 ← sigEncode(𝑐, 𝐳̃ mod±𝑞, 𝐡) let bytes_written = - sig_encode::( - &sig_val_c_tilde, &sig_val_z, &sig_val_h, output, - ); + sig_encode::(&sig_val_c_tilde, &sig_val_z, &sig_val_h, output); Ok(bytes_written) } @@ -1711,7 +1499,7 @@ impl< } fn verify_with_expanded_key( - pk: &MLDSAPublicKeyExpanded, + pk: &MLDSAPublicKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], @@ -1725,7 +1513,7 @@ impl< fn verify_mu( pk: &PK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], sig: &[u8; SIG_LEN], ) -> Result<(), SignatureError> { @@ -1738,16 +1526,12 @@ impl< /// Trait for all three of the ML-DSA algorithm variants. pub trait MLDSATrait< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const LAMBDA: i32, - const k: usize, - const l: usize, - const ETA: usize, >: Sized { /// Runs a key generation using the library's default RNG, seeded from the OS. @@ -1762,7 +1546,7 @@ pub trait MLDSATrait< // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), SignatureError> { // Source the seed from the provided RNG - if rng.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if rng.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } let mut seed = KeyMaterial256::new(); @@ -1829,26 +1613,26 @@ pub trait MLDSATrait< ) -> Result<[u8; 64], SignatureError>; /// Same as [`MLDSATrait::compute_mu_from_tr`], but extracts tr from the public key. fn compute_mu_from_pk( - pk: &impl MLDSAPublicKeyTrait, + pk: &impl MLDSAPublicKeyTrait, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; 64], SignatureError>; /// Same as [`MLDSATrait::compute_mu_from_tr`], but extracts tr from the private key. // dev note: defined sk this way so that it accepts either MLDSAPrivateKey or MLDSAPRivateKeyExpanded fn compute_mu_from_sk( - sk: &impl MLDSAPrivateKeyTrait, + sk: &impl MLDSAPrivateKeyTrait, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; 64], SignatureError>; /// Same as [`Signer::sign`], but signs from an [`MLDSAPrivateKeyExpanded`]. fn sign_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, ) -> Result<[u8; SIG_LEN], SignatureError>; /// Same as [`MLDSATrait::sign_with_expanded_key`], but takes an output array. fn sign_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, + sk: &MLDSAPrivateKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, out: &mut [u8; SIG_LEN], @@ -1861,7 +1645,7 @@ pub trait MLDSATrait< /// Optionally, takes a pre-expanded public matrix `A_hat`. fn sign_mu( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], ) -> Result<[u8; SIG_LEN], SignatureError>; /// Performs an ML-DSA signature using the provided external message representative `mu`. @@ -1876,20 +1660,20 @@ pub trait MLDSATrait< /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. fn sign_mu_out( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], output: &mut [u8; SIG_LEN], ) -> Result; /// Same as [`MLDSATrait::sign_mu`], but signs from an [`MLDSAPrivateKeyExpanded`]. fn sign_mu_with_expanded_key( - sk: &MLDSAPrivateKeyExpanded, - A_hat: Option<&Matrix>, + sk: &MLDSAPrivateKeyExpanded, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], ) -> Result<[u8; SIG_LEN], SignatureError>; /// Same as [`MLDSATrait::sign_mu_out`], but signs from an [`MLDSAPrivateKeyExpanded`]. fn sign_mu_with_expanded_key_out( - sk: &MLDSAPrivateKeyExpanded, - A_hat: Option<&Matrix>, + sk: &MLDSAPrivateKeyExpanded, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], output: &mut [u8; SIG_LEN], ) -> Result; @@ -1916,7 +1700,7 @@ pub trait MLDSATrait< /// prevent accidental nonce reuse, this function moves `rnd`. fn sign_mu_deterministic( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], rnd: [u8; 32], ) -> Result<[u8; SIG_LEN], SignatureError>; @@ -1945,7 +1729,7 @@ pub trait MLDSATrait< /// Returns the number of bytes written to the output buffer. Can be called with an oversized buffer. fn sign_mu_deterministic_out( sk: &SK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], rnd: [u8; 32], output: &mut [u8; SIG_LEN], @@ -1978,7 +1762,7 @@ pub trait MLDSATrait< ) -> Result; /// Same as [`SignatureVerifier::verify`], but signs from an expanded key object. fn verify_with_expanded_key( - pk: &MLDSAPublicKeyExpanded, + pk: &MLDSAPublicKeyExpanded, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8], @@ -1989,59 +1773,20 @@ pub trait MLDSATrait< /// Optionally, takes a pre-expanded public matrix `A_hat`. fn verify_mu( pk: &PK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, mu: &[u8; 64], sig: &[u8; SIG_LEN], ) -> Result<(), SignatureError>; } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> Signer - for MLDSA< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> Signer for MLDSA { fn sign(sk: &SK, msg: &[u8], ctx: Option<&[u8]>) -> Result<[u8; SIG_LEN], SignatureError> { let mut out = [0u8; SIG_LEN]; @@ -2125,52 +1870,13 @@ impl< } impl< + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const SIG_LEN: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, - const TAU: i32, - const LAMBDA: i32, - const GAMMA1: i32, - const GAMMA2: i32, - const k: usize, - const l: usize, - const ETA: usize, - const BETA: i32, - const OMEGA: i32, - const C_TILDE: usize, - const POLY_Z_PACKED_LEN: usize, - const POLY_W1_PACKED_LEN: usize, - const LAMBDA_over_4: usize, - const GAMMA1_MINUS_BETA: i32, - const GAMMA2_MINUS_BETA: i32, - const GAMMA1_MASK_LEN: usize, -> SignatureVerifier - for MLDSA< - PK_LEN, - SK_LEN, - SIG_LEN, - PK, - SK, - TAU, - LAMBDA, - GAMMA1, - GAMMA2, - k, - l, - ETA, - BETA, - OMEGA, - C_TILDE, - POLY_Z_PACKED_LEN, - POLY_W1_PACKED_LEN, - LAMBDA_over_4, - GAMMA1_MINUS_BETA, - GAMMA2_MINUS_BETA, - GAMMA1_MASK_LEN, - > +> SignatureVerifier for MLDSA { fn verify(pk: &PK, msg: &[u8], ctx: Option<&[u8]>, sig: &[u8]) -> Result<(), SignatureError> { let mu = MuBuilder::compute_mu(&pk.compute_tr(), msg, ctx)?; diff --git a/crypto/mldsa/src/mldsa_keys.rs b/crypto/mldsa/src/mldsa_keys.rs index 456a26c8..5d4dee7d 100644 --- a/crypto/mldsa/src/mldsa_keys.rs +++ b/crypto/mldsa/src/mldsa_keys.rs @@ -1,14 +1,11 @@ use crate::aux_functions::{ - bit_pack_eta, bit_pack_t0, bit_unpack_eta, bit_unpack_t0, bitlen_eta, expandA, - power_2_round_vec, simple_bit_pack_t1, simple_bit_unpack_t1, + bit_pack_eta, bit_pack_t0, bit_unpack_eta, bit_unpack_t0, expandA, power_2_round_vec, + simple_bit_pack_t1, simple_bit_unpack_t1, }; -use crate::matrix::{Matrix, Vector}; +use crate::matrix::{MatrixTrait, VectorTrait}; use crate::mldsa::H; -use crate::mldsa::{MLDSA44_ETA, MLDSA44_PK_LEN, MLDSA44_SK_LEN, MLDSA44_k, MLDSA44_l}; -use crate::mldsa::{MLDSA65_ETA, MLDSA65_PK_LEN, MLDSA65_SK_LEN, MLDSA65_k, MLDSA65_l}; -use crate::mldsa::{MLDSA87_ETA, MLDSA87_PK_LEN, MLDSA87_SK_LEN, MLDSA87_k, MLDSA87_l}; use crate::mldsa::{POLY_T0PACKED_LEN, POLY_T1PACKED_LEN}; -use crate::{ML_DSA_44_NAME, ML_DSA_65_NAME, ML_DSA_87_NAME}; +use crate::params::{MLDSA44Params, MLDSA65Params, MLDSA87Params, MLDSAParams}; use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::KeyMaterial; use bouncycastle_core::traits::{SignaturePrivateKey, SignaturePublicKey, XOF}; @@ -25,71 +22,77 @@ use crate::polynomial::Polynomial; /* Pub Types */ /// ML-DSA-44 Public Key -pub type MLDSA44PublicKey = MLDSAPublicKey; +pub type MLDSA44PublicKey = MLDSAPublicKey; /// ML-DSA-44 Private Key pub type MLDSA44PrivateKey = - MLDSAPrivateKey; + MLDSAPrivateKey; /// ML-DSA-65 Public Key -pub type MLDSA65PublicKey = MLDSAPublicKey; +pub type MLDSA65PublicKey = MLDSAPublicKey; /// ML-DSA-65 Private Key pub type MLDSA65PrivateKey = - MLDSAPrivateKey; + MLDSAPrivateKey; /// ML-DSA-87 Public Key -pub type MLDSA87PublicKey = MLDSAPublicKey; +pub type MLDSA87PublicKey = MLDSAPublicKey; /// ML-DSA-87 Private Key pub type MLDSA87PrivateKey = - MLDSAPrivateKey; + MLDSAPrivateKey; /* Pre-expanded keys for repeated operations */ /// ML-DSA-44 Public Key with a pre-expanded public matrix A for repeated encaps operations. pub type MLDSA44PublicKeyExpanded = - MLDSAPublicKeyExpanded; + MLDSAPublicKeyExpanded; /// ML-DSA-44 Private Key with a pre-expanded public matrix A for repeated decaps operations. pub type MLDSA44PrivateKeyExpanded = MLDSAPrivateKeyExpanded< - MLDSA44_k, - MLDSA44_l, - MLDSA44_ETA, + MLDSA44Params, MLDSA44PublicKey, MLDSA44PrivateKey, - MLDSA44_SK_LEN, - MLDSA44_PK_LEN, + { MLDSA44Params::SK_LEN }, + { MLDSA44Params::PK_LEN }, >; /// ML-DSA-65 Public Key with a pre-expanded public matrix A for repeated encaps operations. pub type MLDSA65PublicKeyExpanded = - MLDSAPublicKeyExpanded; + MLDSAPublicKeyExpanded; /// ML-DSA-65 Private Key with a pre-expanded public matrix A for repeated decaps operations. pub type MLDSA65PrivateKeyExpanded = MLDSAPrivateKeyExpanded< - MLDSA65_k, - MLDSA65_l, - MLDSA65_ETA, + MLDSA65Params, MLDSA65PublicKey, MLDSA65PrivateKey, - MLDSA65_SK_LEN, - MLDSA65_PK_LEN, + { MLDSA65Params::SK_LEN }, + { MLDSA65Params::PK_LEN }, >; /// ML-DSA-87 Public Key with a pre-expanded public matrix A for repeated encaps operations. pub type MLDSA87PublicKeyExpanded = - MLDSAPublicKeyExpanded; + MLDSAPublicKeyExpanded; /// ML-DSA-87 Private Key with a pre-expanded public matrix A for repeated decaps operations. pub type MLDSA87PrivateKeyExpanded = MLDSAPrivateKeyExpanded< - MLDSA87_k, - MLDSA87_l, - MLDSA87_ETA, + MLDSA87Params, MLDSA87PublicKey, MLDSA87PrivateKey, - MLDSA87_SK_LEN, - MLDSA87_PK_LEN, + { MLDSA87Params::SK_LEN }, + { MLDSA87Params::PK_LEN }, >; /// An ML-DSA public key. -#[derive(Clone)] -pub struct MLDSAPublicKey { +/// +/// `PK_LEN` duplicates `MLDSAParams::PK_LEN`; it has to be carried separately because +/// [`SignaturePublicKey`] takes the encoded length as a const generic parameter, and an associated +/// const of a type parameter may not be used as a const generic argument. The type aliases below +/// wire the two together. +pub struct MLDSAPublicKey { rho: [u8; 32], - t1: Vector, + t1: P::VecK, +} + +// Written out rather than derived: `#[derive(Clone)]` would demand `P: Clone`, and `P` is a +// marker for the parameter set that is never stored, only used to name the field types. +impl Clone for MLDSAPublicKey { + fn clone(&self) -> Self { + Self { rho: self.rho, t1: self.t1 } + } } -impl MLDSAPublicKey { +impl MLDSAPublicKey { /// Algorithm 22 pkEncode(𝜌, 𝐭1) /// Encodes a public key for ML-DSA into a byte string. /// Input:𝜌 ∈ 𝔹32, 𝐭1 ∈ 𝑅𝑘 with coefficients in [0, 2bitlen (𝑞−1)−𝑑 − 1]. @@ -102,11 +105,11 @@ impl MLDSAPublicKey(); // that should divide evenly the remainder of the array - debug_assert_eq!(pk_chunks.len(), k); + debug_assert_eq!(pk_chunks.len(), P::k); debug_assert_eq!(last_chunk.len(), 0); - for (pk_chunk, t1_i) in pk_chunks.into_iter().zip(&self.t1.elems) { - pk_chunk.copy_from_slice(&simple_bit_pack_t1(&t1_i)); + for (pk_chunk, t1_i) in pk_chunks.into_iter().zip(self.t1.elems()) { + pk_chunk.copy_from_slice(&simple_bit_pack_t1(t1_i)); } PK_LEN @@ -114,7 +117,7 @@ impl MLDSAPublicKey: +pub trait MLDSAPublicKeyTrait: SignaturePublicKey { /// Algorithm 23 pkDecode(𝑝𝑘) @@ -124,7 +127,7 @@ pub trait MLDSAPublicKeyTrait Self; /// Get a copy of the expanded public matrix A_hat - fn A_hat(&self) -> Matrix; + fn A_hat(&self) -> P::MatrixA; /// Compute the public key hash (tr) from the public key. /// @@ -135,43 +138,43 @@ pub trait MLDSAPublicKeyTrait [u8; 64]; } -pub(crate) trait MLDSAPublicKeyInternalTrait: +pub(crate) trait MLDSAPublicKeyInternalTrait: SignaturePublicKey { /// Not exposing a constructor publicly because you should have to get an instance either by /// running a keygen, or by decoding an existing key. - fn new(rho: [u8; 32], t1: Vector) -> Self; + fn new(rho: [u8; 32], t1: P::VecK) -> Self; /// Get a ref to t1 - fn t1(&self) -> &Vector; + fn t1(&self) -> &P::VecK; } -impl MLDSAPublicKeyTrait - for MLDSAPublicKey +impl MLDSAPublicKeyTrait + for MLDSAPublicKey { // todo: block a t1 of all zeros? Maybe add to consistency_check() ? fn pk_decode(pk: &[u8; PK_LEN]) -> Self { let rho = pk[0..32].try_into().unwrap(); - let mut t1 = Vector::::new(); + let mut t1 = P::VecK::new(); let (pk_chunks, last_chunk) = pk[32..].as_chunks::(); // that should divide evenly the remainder of the array - debug_assert_eq!(pk_chunks.len(), k); + debug_assert_eq!(pk_chunks.len(), P::k); debug_assert_eq!(last_chunk.len(), 0); - for (t1_i, pk_chunk) in t1.elems.iter_mut().zip(pk_chunks) { + for (t1_i, pk_chunk) in t1.elems_mut().iter_mut().zip(pk_chunks) { // 3: 𝐭1[𝑖] ← SimpleBitUnpack(𝑧𝑖, 2bitlen (𝑞−1)−𝑑 − 1) // ▷ This is always in the correct range // Therefore, we don't need to check that the coeeffs are in range t1_i.coeffs.copy_from_slice(&simple_bit_unpack_t1(pk_chunk).coeffs); } - Self::new(rho, t1) + >::new(rho, t1) } - fn A_hat(&self) -> Matrix { - expandA::(&self.rho) + fn A_hat(&self) -> P::MatrixA { + expandA::

(&self.rho) } fn compute_tr(&self) -> [u8; 64] { @@ -182,21 +185,19 @@ impl MLDSAPublicKeyTrait MLDSAPublicKeyInternalTrait - for MLDSAPublicKey +impl MLDSAPublicKeyInternalTrait + for MLDSAPublicKey { - fn new(rho: [u8; 32], t1: Vector) -> Self { + fn new(rho: [u8; 32], t1: P::VecK) -> Self { Self { rho, t1 } } - fn t1(&self) -> &Vector { + fn t1(&self) -> &P::VecK { &self.t1 } } -impl SignaturePublicKey - for MLDSAPublicKey -{ +impl SignaturePublicKey for MLDSAPublicKey { fn encode(&self) -> [u8; PK_LEN] { let mut pk = [0u8; PK_LEN]; let bytes_written = self.encode_out(&mut pk); @@ -218,15 +219,13 @@ impl SignaturePublicKey>::pk_decode(&bytes_sized)) } } -impl Eq for MLDSAPublicKey {} +impl Eq for MLDSAPublicKey {} -impl PartialEq - for MLDSAPublicKey -{ +impl PartialEq for MLDSAPublicKey { fn eq(&self, other: &Self) -> bool { let self_encoded = self.encode(); let other_encoded = other.encode(); @@ -234,50 +233,54 @@ impl PartialEq } } -impl Debug for MLDSAPublicKey { +impl Debug for MLDSAPublicKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; - write!(f, "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", alg, self.compute_tr(),) + write!( + f, + "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", + P::ALG_NAME, + >::compute_tr(self), + ) } } -impl Display for MLDSAPublicKey { +impl Display for MLDSAPublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; - write!(f, "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", alg, self.compute_tr(),) + write!( + f, + "MLDSAPublicKey {{ alg: {}, pub_key_hash (tr): {:x?} }}", + P::ALG_NAME, + >::compute_tr(self), + ) } } /// A fully expanded ML-DSA public key that includes the intermediate values needed for performing /// multiple verification operations against the same public key, which causes the public key struct /// to take up more memory, but results in more efficient repeated verify() operations. -#[derive(Clone)] pub struct MLDSAPublicKeyExpanded< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, const PK_LEN: usize, > { pub(crate) pk: PK, - pub(crate) A_hat: Matrix, + pub(crate) A_hat: P::MatrixA, +} + +/// See the note on [`MLDSAPublicKey`]'s `Clone` for why this is not derived. +impl, const PK_LEN: usize> Clone + for MLDSAPublicKeyExpanded +{ + fn clone(&self) -> Self { + Self { pk: self.pk.clone(), A_hat: self.A_hat.clone() } + } } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> SignaturePublicKey for MLDSAPublicKeyExpanded +> SignaturePublicKey for MLDSAPublicKeyExpanded { fn encode(&self) -> [u8; PK_LEN] { self.pk.encode() @@ -296,16 +299,15 @@ impl< )); } let bytes_sized: [u8; PK_LEN] = bytes[..PK_LEN].try_into().unwrap(); - Ok(Self::pk_decode(&bytes_sized)) + Ok(>::pk_decode(&bytes_sized)) } } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> PartialEq for MLDSAPublicKeyExpanded +> PartialEq for MLDSAPublicKeyExpanded { fn eq(&self, other: &Self) -> bool { self.pk.eq(&other.pk) @@ -313,66 +315,50 @@ impl< } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> Eq for MLDSAPublicKeyExpanded +> Eq for MLDSAPublicKeyExpanded { } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> Debug for MLDSAPublicKeyExpanded +> Debug for MLDSAPublicKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPublicKeyExpanded {{ alg: {}, pub_key_hash (tr): {:x?} }}", - alg, - self.compute_tr(), + P::ALG_NAME, + self.pk.compute_tr(), ) } } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> Display for MLDSAPublicKeyExpanded +> Display for MLDSAPublicKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPublicKeyExpanded {{ alg: {}, pub_key_hash (tr): {:x?} }}", - alg, - self.compute_tr(), + P::ALG_NAME, + self.pk.compute_tr(), ) } } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> From<&PK> for MLDSAPublicKeyExpanded +> From<&PK> for MLDSAPublicKeyExpanded { /// Fully expands the intermediate values needed for performing multiple encaps operations /// against the same public key, which causes the MLKEMPublicKey struct to take up @@ -384,11 +370,10 @@ impl< } impl< - const k: usize, - const l: usize, - PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyTrait + MLDSAPublicKeyInternalTrait, const PK_LEN: usize, -> MLDSAPublicKeyTrait for MLDSAPublicKeyExpanded +> MLDSAPublicKeyTrait for MLDSAPublicKeyExpanded { fn pk_decode(pk: &[u8; PK_LEN]) -> Self { let pk1 = PK::pk_decode(pk); @@ -396,7 +381,7 @@ impl< Self { pk: pk1, A_hat } } - fn A_hat(&self) -> Matrix { + fn A_hat(&self) -> P::MatrixA { self.A_hat.clone() } @@ -407,15 +392,10 @@ impl< /// An ML-DSA private key. /// +/// See [`MLDSAPublicKey`] for why `SK_LEN` and `PK_LEN` are carried alongside `P`. +// // Dev note: This will automatically inherit the [`Secret`] protections because [`Polynomial`] wraps the underlying data with [`Secret`]. -#[derive(Clone)] -pub struct MLDSAPrivateKey< - const k: usize, - const l: usize, - const eta: usize, - const SK_LEN: usize, - const PK_LEN: usize, -> { +pub struct MLDSAPrivateKey { rho: [u8; 32], K: Secret<[u8; 32]>, tr: [u8; 64], @@ -425,16 +405,31 @@ pub struct MLDSAPrivateKey< // So we are going to hold them as s1_hat, s2_hat, and t0_hat. // Note: these are not necessarily in their reduced form; so you'll need to reduce them before // inv_ntt()'ing them or hashing them. - s1_hat: Secret>, - s2_hat: Secret>, - t0_hat: Vector, + s1_hat: Secret, + s2_hat: Secret, + t0_hat: P::VecK, // note: KeyMaterial is inherently Secret seed: Option>, } -impl - MLDSAPrivateKey +/// See the note on [`MLDSAPublicKey`]'s `Clone` for why this is not derived. +impl Clone + for MLDSAPrivateKey { + fn clone(&self) -> Self { + Self { + rho: self.rho, + K: self.K.clone(), + tr: self.tr, + s1_hat: self.s1_hat.clone(), + s2_hat: self.s2_hat.clone(), + t0_hat: self.t0_hat, + seed: self.seed.clone(), + } + } +} + +impl MLDSAPrivateKey { /// Algorithm 24 skEncode(𝜌, 𝐾, 𝑡𝑟, 𝐬1, 𝐬2, 𝐭0) /// Encodes a secret key for ML-DSA into a byte string. /// Input: 𝜌 ∈ 𝔹32, 𝐾 ∈ 𝔹32, 𝑡𝑟 ∈ 𝔹64 , 𝐬1 ∈ 𝑅ℓ with coefficients in [−𝜂, 𝜂], 𝐬2 ∈ 𝑅𝑘 with @@ -452,47 +447,44 @@ impl(&s1_i, &mut buf); + bit_pack_eta::

(&s1_i, &mut buf); sk_chunk.copy_from_slice(&buf[..eta_pack_len]); } - off += l * bitlen_eta(eta); + off += P::l * eta_pack_len; - let sk_chunks = out[off..off + k * bitlen_eta(eta)].chunks_mut(bitlen_eta(eta)); - debug_assert_eq!(sk_chunks.len(), k); - for (sk_chunk, s2_hat_i) in sk_chunks.into_iter().zip(&self.s2_hat.elems) { + let sk_chunks = out[off..off + P::k * eta_pack_len].chunks_mut(eta_pack_len); + debug_assert_eq!(sk_chunks.len(), P::k); + for (sk_chunk, s2_hat_i) in sk_chunks.into_iter().zip(self.s2_hat.elems()) { // Deviation from the FIPS: // We are holding these in ntt form, so need to convert back to standard form - let mut s2_hat_i = s2_hat_i.clone(); - s2_hat_i.reduce(); - s2_hat_i.inv_ntt(); - let s2_i = s2_hat_i; + let mut s2_i = *s2_hat_i; + s2_i.reduce(); + s2_i.inv_ntt(); - bit_pack_eta::(&s2_i, &mut buf); + bit_pack_eta::

(&s2_i, &mut buf); sk_chunk.copy_from_slice(&buf[..eta_pack_len]); } - off += k * bitlen_eta(eta); + off += P::k * eta_pack_len; - let sk_chunks = out[off..off + k * POLY_T0PACKED_LEN].chunks_mut(POLY_T0PACKED_LEN); - debug_assert_eq!(sk_chunks.len(), k); - for (sk_chunk, t0_hat_i) in sk_chunks.into_iter().zip(&self.t0_hat.elems) { + let sk_chunks = out[off..off + P::k * POLY_T0PACKED_LEN].chunks_mut(POLY_T0PACKED_LEN); + debug_assert_eq!(sk_chunks.len(), P::k); + for (sk_chunk, t0_hat_i) in sk_chunks.into_iter().zip(self.t0_hat.elems()) { // Deviation from the FIPS: // We are holding these in ntt form, so need to convert back to standard form - let mut t0_hat_i = t0_hat_i.clone(); - t0_hat_i.reduce(); - t0_hat_i.inv_ntt(); - let t0_i = t0_hat_i; + let mut t0_i = *t0_hat_i; + t0_i.reduce(); + t0_i.inv_ntt(); sk_chunk.copy_from_slice(&bit_pack_t0(&t0_i)); } @@ -502,13 +494,8 @@ impl: SignaturePrivateKey +pub trait MLDSAPrivateKeyTrait: + SignaturePrivateKey { /// Get a ref to the seed, if there is one stored with this private key fn seed(&self) -> Option<&KeyMaterial<32>>; @@ -517,10 +504,10 @@ pub trait MLDSAPrivateKeyTrait< fn tr(&self) -> &[u8; 64]; /// Get the public matrix A_hat. - fn A_hat(&self) -> Matrix; + fn A_hat(&self) -> P::MatrixA; /// This is a partial implementation of keygen_internal(), and probably not allowed in FIPS mode. - fn derive_pk(&self) -> MLDSAPublicKey; + fn derive_pk(&self) -> MLDSAPublicKey; /// Algorithm 25 skDecode(𝑠𝑘) /// Reverses the procedure skEncode. /// Input: Private key 𝑠𝑘 ∈ 𝔹32+32+64+32⋅((ℓ+𝑘)⋅bitlen (2𝜂)+𝑑𝑘). @@ -533,9 +520,7 @@ pub trait MLDSAPrivateKeyTrait< } pub(crate) trait MLDSAPrivateKeyInternalTrait< - const k: usize, - const l: usize, - const eta: usize, + P: MLDSAParams, const SK_LEN: usize, const PK_LEN: usize, > @@ -546,23 +531,23 @@ pub(crate) trait MLDSAPrivateKeyInternalTrait< rho: [u8; 32], K: Secret<[u8; 32]>, tr: [u8; 64], - s1_hat: Secret>, - s2_hat: Secret>, - t0_hat: Vector, + s1_hat: Secret, + s2_hat: Secret, + t0_hat: P::VecK, seed: Option>, ) -> Self; /// Get a ref to K fn K(&self) -> &Secret<[u8; 32]>; /// Get a ref to s1 - fn s1_hat(&self) -> &Vector; + fn s1_hat(&self) -> &P::VecL; /// Get a ref to s2 - fn s2_hat(&self) -> &Vector; + fn s2_hat(&self) -> &P::VecK; /// Get a ref to t0 - fn t0_hat(&self) -> &Vector; + fn t0_hat(&self) -> &P::VecK; } -impl - MLDSAPrivateKeyTrait for MLDSAPrivateKey +impl + MLDSAPrivateKeyTrait for MLDSAPrivateKey { fn seed(&self) -> Option<&KeyMaterial<32>> { match self.seed { @@ -575,18 +560,18 @@ impl Matrix { - expandA::(&self.rho) + fn A_hat(&self) -> P::MatrixA { + expandA::

(&self.rho) } - fn derive_pk(&self) -> MLDSAPublicKey { + fn derive_pk(&self) -> MLDSAPublicKey { // 5: 𝐭 ← NTT−1(𝐀 ∘ NTT(𝐬1)) + 𝐬2 // ▷ compute 𝐭 = 𝐀𝐬1 + 𝐬2 let mut t = { // scope for A_hat // 3: 𝐀 ← ExpandA(𝜌) // ▷ 𝐀 is generated and stored in NTT representation as 𝐀 - let A_hat = expandA::(&self.rho); + let A_hat = expandA::

(&self.rho); let mut t_ntt = A_hat.matrix_vector_ntt(&self.s1_hat); t_ntt.inv_ntt(); @@ -596,7 +581,7 @@ impl = self.s2_hat.clone(); s2.reduce(); s2.inv_ntt(); @@ -606,9 +591,9 @@ impl(&t); + let (t1, _) = power_2_round_vec(&t); - MLDSAPublicKey::::new(self.rho.clone(), t1) + as MLDSAPublicKeyInternalTrait>::new(self.rho, t1) } fn sk_decode(sk: &[u8; SK_LEN]) -> Result { // Construct the (Secret-protected) key up front and unpack each field directly into it, @@ -621,23 +606,25 @@ impl::new(), + t0_hat: P::VecK::new(), seed: None, }; key.K.copy_from_slice(&sk[32..64]); let mut off = 128; + let eta_pack_len = P::POLY_ETA_PACKED_LEN; + let eta = P::eta as i32; // unpack s1 directly into key.s1_hat so that we don't make additional non-Secret copies. - let sk_chunks = sk[off..off + (l * bitlen_eta(eta))].chunks(bitlen_eta(eta)); - debug_assert_eq!(sk_chunks.len(), l); - for (s1_i, sk_chunk) in key.s1_hat.elems.iter_mut().zip(sk_chunks) { + let sk_chunks = sk[off..off + (P::l * eta_pack_len)].chunks(eta_pack_len); + debug_assert_eq!(sk_chunks.len(), P::l); + for (s1_i, sk_chunk) in key.s1_hat.elems_mut().iter_mut().zip(sk_chunks) { // 3: 𝐬1[𝑖] ← BitUnpack(𝑦𝑖, 𝜂, 𝜂) // ▷ this may lie outside [−𝜂, 𝜂] if input is malformed - s1_i.coeffs.copy_from_slice(&bit_unpack_eta::(&sk_chunk).coeffs); + s1_i.coeffs.copy_from_slice(&bit_unpack_eta::

(sk_chunk).coeffs); // check that the coefficients are within the expected range for coeff in s1_i.coeffs.iter() { - if *coeff < -(eta as i32) || *coeff > (eta as i32) { + if *coeff < -eta || *coeff > eta { return Err(SignatureError::DecodingError("Invalid or corrupted key")); } } @@ -645,19 +632,19 @@ impl(&sk_chunk).coeffs); + s2_i.coeffs.copy_from_slice(&bit_unpack_eta::

(sk_chunk).coeffs); // check that the coefficients are within the expected range for coeff in s2_i.coeffs.iter() { - if *coeff < -(eta as i32) || *coeff > (eta as i32) { + if *coeff < -eta || *coeff > eta { return Err(SignatureError::DecodingError("Invalid or corrupted key")); } } @@ -665,17 +652,17 @@ impl(); + sk[off..off + (P::k * POLY_T0PACKED_LEN)].as_chunks::(); // that should divide evenly the remainder of the array - debug_assert_eq!(sk_chunks.len(), k); + debug_assert_eq!(sk_chunks.len(), P::k); debug_assert_eq!(last_chunk.len(), 0); - for (t0_i, sk_chunk) in key.t0_hat.elems.iter_mut().zip(sk_chunks) { + for (t0_i, sk_chunk) in key.t0_hat.elems_mut().iter_mut().zip(sk_chunks) { t0_i.coeffs.copy_from_slice(&bit_unpack_t0(sk_chunk).coeffs); } // Deviation from the FIPS: @@ -686,49 +673,40 @@ impl - MLDSAPrivateKeyInternalTrait - for MLDSAPrivateKey +impl + MLDSAPrivateKeyInternalTrait for MLDSAPrivateKey { fn new( rho: [u8; 32], K: Secret<[u8; 32]>, tr: [u8; 64], - s1_hat: Secret>, - s2_hat: Secret>, - t0_hat: Vector, + s1_hat: Secret, + s2_hat: Secret, + t0_hat: P::VecK, seed: Option>, ) -> Self { - Self { - rho: rho.clone(), - K: K.clone(), - tr: tr.clone(), - s1_hat: s1_hat.clone(), - s2_hat: s2_hat.clone(), - t0_hat: t0_hat.clone(), - seed: seed.clone(), - } + Self { rho, K, tr, s1_hat, s2_hat, t0_hat, seed } } fn K(&self) -> &Secret<[u8; 32]> { &self.K } - fn s1_hat(&self) -> &Vector { + fn s1_hat(&self) -> &P::VecL { &self.s1_hat } - fn s2_hat(&self) -> &Vector { + fn s2_hat(&self) -> &P::VecK { &self.s2_hat } - fn t0_hat(&self) -> &Vector { + fn t0_hat(&self) -> &P::VecK { &self.t0_hat } } -impl - SignaturePrivateKey for MLDSAPrivateKey +impl SignaturePrivateKey + for MLDSAPrivateKey { fn encode(&self) -> [u8; SK_LEN] { let mut out = [0u8; SK_LEN]; @@ -752,17 +730,17 @@ impl>::sk_decode(&bytes_sized) } } -impl Eq - for MLDSAPrivateKey +impl Eq + for MLDSAPrivateKey { } -impl - PartialEq for MLDSAPrivateKey +impl PartialEq + for MLDSAPrivateKey { fn eq(&self, other: &Self) -> bool { let self_encoded = self.encode(); @@ -772,20 +750,14 @@ impl - fmt::Debug for MLDSAPrivateKey +impl fmt::Debug + for MLDSAPrivateKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPrivateKey {{ alg: {}, pub_key_hash (tr): {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.tr, self.seed.is_some(), ) @@ -793,20 +765,14 @@ impl - Display for MLDSAPrivateKey +impl Display + for MLDSAPrivateKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPrivateKey {{ alg: {}, pub_key_hash (tr): {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.tr, self.seed.is_some(), ) @@ -816,32 +782,39 @@ impl, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, > { _phantom: core::marker::PhantomData, pub(crate) sk: SK, - pub(crate) A_hat: Matrix, + pub(crate) A_hat: P::MatrixA, +} + +/// See the note on [`MLDSAPublicKey`]'s `Clone` for why this is not derived. +impl< + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, + const SK_LEN: usize, + const PK_LEN: usize, +> Clone for MLDSAPrivateKeyExpanded +{ + fn clone(&self) -> Self { + Self { _phantom: core::marker::PhantomData, sk: self.sk.clone(), A_hat: self.A_hat.clone() } + } } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> PartialEq for MLDSAPrivateKeyExpanded +> PartialEq for MLDSAPrivateKeyExpanded { fn eq(&self, other: &Self) -> bool { self.sk.eq(&other.sk) @@ -849,40 +822,28 @@ impl< } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Eq for MLDSAPrivateKeyExpanded +> Eq for MLDSAPrivateKeyExpanded { } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Debug for MLDSAPrivateKeyExpanded +> Debug for MLDSAPrivateKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPrivateKeyExpanded {{ alg: {}, pub_key_hash (tr): {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.sk.tr(), self.sk.seed().is_some(), ) @@ -890,27 +851,18 @@ impl< } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Display for MLDSAPrivateKeyExpanded +> Display for MLDSAPrivateKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 4 => ML_DSA_44_NAME, - 6 => ML_DSA_65_NAME, - 8 => ML_DSA_87_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLDSAPrivateKeyExpanded {{ alg: {}, pub_key_hash (tr): {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.sk.tr(), self.sk.seed().is_some(), ) @@ -918,35 +870,30 @@ impl< } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> From<&SK> for MLDSAPrivateKeyExpanded +> From<&SK> for MLDSAPrivateKeyExpanded { /// Fully expands the intermediate values needed for performing multiple encaps operations /// against the same public key, which causes the MLKEMPublicKey struct to take up fn from(sk: &SK) -> Self { - let A_hat = sk.derive_pk().A_hat(); + let A_hat = + as MLDSAPublicKeyTrait>::A_hat(&sk.derive_pk()); Self { _phantom: core::marker::PhantomData, sk: sk.clone(), A_hat } } } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> SignaturePrivateKey for MLDSAPrivateKeyExpanded +> SignaturePrivateKey for MLDSAPrivateKeyExpanded { fn encode(&self) -> [u8; SK_LEN] { self.sk.encode() @@ -965,16 +912,12 @@ impl< } impl< - const k: usize, - const l: usize, - const eta: usize, - PK: MLDSAPublicKeyInternalTrait, - SK: MLDSAPrivateKeyTrait - + MLDSAPrivateKeyInternalTrait, + P: MLDSAParams, + PK: MLDSAPublicKeyInternalTrait, + SK: MLDSAPrivateKeyTrait + MLDSAPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> MLDSAPrivateKeyTrait - for MLDSAPrivateKeyExpanded +> MLDSAPrivateKeyTrait for MLDSAPrivateKeyExpanded { fn seed(&self) -> Option<&KeyMaterial<32>> { self.sk.seed() @@ -984,17 +927,18 @@ impl< self.sk.tr() } - fn A_hat(&self) -> Matrix { + fn A_hat(&self) -> P::MatrixA { self.sk.A_hat() } - fn derive_pk(&self) -> MLDSAPublicKey { + fn derive_pk(&self) -> MLDSAPublicKey { self.sk.derive_pk() } fn sk_decode(sk: &[u8; SK_LEN]) -> Result { let sk1 = SK::sk_decode(sk)?; - let A_hat = sk1.derive_pk().A_hat(); + let A_hat = + as MLDSAPublicKeyTrait>::A_hat(&sk1.derive_pk()); Ok(Self { _phantom: core::marker::PhantomData, sk: sk1, A_hat }) } diff --git a/crypto/mldsa/src/params.rs b/crypto/mldsa/src/params.rs new file mode 100644 index 00000000..67c2ab76 --- /dev/null +++ b/crypto/mldsa/src/params.rs @@ -0,0 +1,510 @@ +//! The three ML-DSA parameter sets of FIPS 204, Section 4, and the six HashML-DSA pairings built on +//! them, each as a sealed trait with one type per set. +//! +//! # Derived parameters +//! +//! FIPS 204, Table 1 assigns eight independent values per set (𝜏, 𝜆, 𝛾1, 𝛾2, (𝑘, ℓ), 𝜂, 𝜔) plus the +//! three sizes of Table 2. Everything else this implementation needs is a function of those, so it +//! is written once as a defaulted associated const rather than three times as a hand-computed +//! number. `params::tests` checks every derivation against the values tabulated in FIPS 204. + +use crate::hash_mldsa::{ + HASH_ML_DSA_44_with_SHA256_NAME, HASH_ML_DSA_44_with_SHA512_NAME, + HASH_ML_DSA_65_WITH_SHA256_NAME, HASH_ML_DSA_65_WITH_SHA512_NAME, + HASH_ML_DSA_87_WITH_SHA512_NAME, HASH_ML_DSA_87_with_SHA256_NAME, +}; +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_sha2::{SHA256, SHA512}; +use bouncycastle_utils::secret::ZeroizablePrimitive; + +/// `bitlen 𝑥`, the length of the binary expansion of 𝑥 (FIPS 204, Section 2.3). +/// +/// `bitlen 0` is 0; every use below has a positive argument. +pub(crate) const fn bitlen(x: u32) -> usize { + if x == 0 { 0 } else { x.ilog2() as usize + 1 } +} + +/// A fixed-size byte buffer whose length depends on the parameter set. +/// +/// [`ZeroizablePrimitive`] rather than [`Default`] supplies the all-zero value, because `Default` +/// for arrays stops at 32 elements and every buffer here is longer than that. +trait ByteBuffer: ZeroizablePrimitive + AsRef<[u8]> + AsMut<[u8]> {} +impl ByteBuffer for [u8; N] {} + +/// A crate-private (aka "sealed") trait that prevents a new ML-DSA parameter set from being defined +/// outside this crate. +trait MLDSAParamsInternalTrait {} + +/// One ML-DSA parameter set: the values of FIPS 204, Table 1 and Table 2, and the types whose size +/// they determine. +/// +/// Sealed via a private supertrait, so [`MLDSA44Params`], [`MLDSA65Params`] and [`MLDSA87Params`] +/// are the only implementations. +pub trait MLDSAParams: MLDSAParamsInternalTrait { + /* FIPS 204, Table 1: the values assigned by each parameter set. */ + + /// 𝜏, the number of ±1's in the polynomial 𝑐. + const tau: i32; + /// 𝜆, the collision strength of 𝑐̃, in bits. + const lambda: i32; + /// 𝛾1, the coefficient range of 𝐲. Always a power of two. + const gamma1: i32; + /// 𝛾2, the low-order rounding range. + const gamma2: i32; + /// 𝑘, the number of rows of 𝐀. + const k: usize; + /// ℓ, the number of columns of 𝐀. + const l: usize; + /// 𝜂, the private key range. + const eta: usize; + /// 𝜔, the maximum number of 1's in the hint 𝐡. + const omega: i32; + + /* FIPS 204, Table 2: sizes in bytes of keys and signatures. */ + + /// The length of an encoded public key. + const PK_LEN: usize; + /// The length of an encoded private key. + const SK_LEN: usize; + /// The length of a signature. + const SIG_LEN: usize; + + /* Algorithm meta-data */ + + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + const ALG_NAME: &'static str; + /// The strength claimed for this parameter set, as reported by `Algorithm::MAX_SECURITY_STRENGTH`. + const MAX_SECURITY_STRENGTH: SecurityStrength; + /// The OID in component form, as reported by `AlgorithmOID::OID`. + const OID: &'static [u32]; + /// The DER encoding of [`MLDSAParams::OID`], as reported by `AlgorithmOID::OID_DER`. + const OID_DER: &'static [u8]; + + /* Derived. Never written out per parameter set -- see the module docs. */ + + /// 𝛽, which FIPS 204, Table 1 defines as "𝛽 = 𝜏 ⋅ 𝜂". + const beta: i32 = Self::tau * Self::eta as i32; + + /// The length of the commitment hash 𝑐̃, which FIPS 204, Algorithm 26 (sigEncode) gives as + /// 𝑐̃ ∈ 𝔹^(𝜆/4). + const C_TILDE_LEN: usize = Self::lambda as usize / 4; + + /// The packed length of one coordinate of 𝐳: FIPS 204, Algorithm 26 (sigEncode) writes each of + /// the ℓ coordinates as 𝔹^(32⋅(1+bitlen (𝛾1−1))). + /// + /// This is also the number of bytes ExpandMask squeezes per coordinate: FIPS 204, + /// Algorithm 34, line 1 sets 𝑐 ← 1 + bitlen (𝛾1 − 1) and line 4 squeezes 32𝑐 bytes. + const POLY_Z_PACKED_LEN: usize = 32 * (1 + bitlen(Self::gamma1 as u32 - 1)); + + /// The packed length of one coordinate of 𝐬1 or 𝐬2: FIPS 204, Algorithm 24 (skEncode), line 3 + /// packs each with BitPack(𝐬1[𝑖], 𝜂, 𝜂), and Algorithm 17 (BitPack) outputs + /// 𝔹^(32⋅bitlen (𝑎+𝑏)), so 32⋅bitlen (2𝜂). + const POLY_ETA_PACKED_LEN: usize = 32 * bitlen(2 * Self::eta as u32); + + /// The packed length of one coordinate of 𝐰1: FIPS 204, Algorithm 28 (w1Encode) outputs + /// 𝔹^(32𝑘⋅bitlen ((𝑞−1)/(2𝛾2)−1)) for all 𝑘 coordinates together. + const POLY_W1_PACKED_LEN: usize = 32 * bitlen(((q - 1) / (2 * Self::gamma2)) as u32 - 1); + + /// 𝛾1 − 𝛽, the rejection bound on ‖𝐳‖∞ (FIPS 204, Algorithm 7, line 23). + const gamma1_minus_beta: i32 = Self::gamma1 - Self::beta; + + /// 𝛾2 − 𝛽, the rejection bound on ‖𝐫0‖∞ (FIPS 204, Algorithm 7, line 23). + const gamma2_minus_beta: i32 = Self::gamma2 - Self::beta; + + /* Types whose size depends on the parameter set. */ + + /// A vector of 𝑘 polynomials, i.e. an element of 𝑅^𝑘. + type VecK: VectorTrait; + /// A vector of ℓ polynomials, i.e. an element of 𝑅^ℓ. + type VecL: VectorTrait; + /// The 𝑘 × ℓ public matrix 𝐀̂. + type MatrixA: MatrixTrait; + + /// The commitment hash 𝑐̃, of [`MLDSAParams::C_TILDE_LEN`] bytes. + type SigCTilde: ByteBuffer; + /// One packed coordinate of 𝐳, of [`MLDSAParams::POLY_Z_PACKED_LEN`] bytes. + /// + /// ExpandMask squeezes into a buffer of this same length; see + /// [`MLDSAParams::POLY_Z_PACKED_LEN`]. + type PolyZPacked: ByteBuffer; + /// One packed coordinate of 𝐰1, of [`MLDSAParams::POLY_W1_PACKED_LEN`] bytes. + type PolyW1Packed: ByteBuffer; +} + +/// The ML-DSA-44 parameter set (FIPS 204, Table 1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLDSA44Params; +/// The ML-DSA-65 parameter set (FIPS 204, Table 1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLDSA65Params; +/// The ML-DSA-87 parameter set (FIPS 204, Table 1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLDSA87Params; + +impl MLDSAParamsInternalTrait for MLDSA44Params {} +impl MLDSAParamsInternalTrait for MLDSA65Params {} +impl MLDSAParamsInternalTrait for MLDSA87Params {} + +impl MLDSAParams for MLDSA44Params { + const tau: i32 = 39; + const lambda: i32 = 128; + const gamma1: i32 = 1 << 17; + // mutants note: because of the bitshifting, the "- 1" ends up not mattering. + const gamma2: i32 = (q - 1) / 88; + const k: usize = 4; + const l: usize = 4; + const eta: usize = 2; + const omega: i32 = 80; + + const PK_LEN: usize = 1312; + const SK_LEN: usize = 2560; + const SIG_LEN: usize = 2420; + + const ALG_NAME: &'static str = ML_DSA_44_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + /// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 17]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11]; + + type VecK = Vector<4>; + type VecL = Vector<4>; + type MatrixA = Matrix<4, 4>; + + type SigCTilde = [u8; 32]; // 𝜆/4 = 128/4 + type PolyZPacked = [u8; 576]; // 32 * (1 + bitlen(2^17 - 1)) = 32 * 18 + type PolyW1Packed = [u8; 192]; // 32 * bitlen(44 - 1) = 32 * 6 +} + +impl MLDSAParams for MLDSA65Params { + const tau: i32 = 49; + const lambda: i32 = 192; + const gamma1: i32 = 1 << 19; + // mutants note: because of the bitshifting, the "- 1" ends up not mattering. + const gamma2: i32 = (q - 1) / 32; + const k: usize = 6; + const l: usize = 5; + const eta: usize = 4; + const omega: i32 = 55; + + const PK_LEN: usize = 1952; + const SK_LEN: usize = 4032; + const SIG_LEN: usize = 3309; + + const ALG_NAME: &'static str = ML_DSA_65_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; + /// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-65 { sigAlgs 18 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 18]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12]; + + type VecK = Vector<6>; + type VecL = Vector<5>; + type MatrixA = Matrix<6, 5>; + + type SigCTilde = [u8; 48]; // 𝜆/4 = 192/4 + type PolyZPacked = [u8; 640]; // 32 * (1 + bitlen(2^19 - 1)) = 32 * 20 + type PolyW1Packed = [u8; 128]; // 32 * bitlen(16 - 1) = 32 * 4 +} + +impl MLDSAParams for MLDSA87Params { + const tau: i32 = 60; + const lambda: i32 = 256; + const gamma1: i32 = 1 << 19; + // mutants note: because of the bitshifting, the "- 1" ends up not mattering. + const gamma2: i32 = (q - 1) / 32; + const k: usize = 8; + const l: usize = 7; + const eta: usize = 2; + const omega: i32 = 75; + + const PK_LEN: usize = 2592; + const SK_LEN: usize = 4896; + const SIG_LEN: usize = 4627; + + const ALG_NAME: &'static str = ML_DSA_87_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; + /// Assigned by NIST in the Computer Security Objects Register: id-ml-dsa-87 { sigAlgs 19 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 3, 19]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13]; + + type VecK = Vector<8>; + type VecL = Vector<7>; + type MatrixA = Matrix<8, 7>; + + type SigCTilde = [u8; 64]; // 𝜆/4 = 256/4 + type PolyZPacked = [u8; 640]; // 32 * (1 + bitlen(2^19 - 1)) = 32 * 20 + type PolyW1Packed = [u8; 128]; // 32 * bitlen(16 - 1) = 32 * 4 +} + +/// The two distinct values 𝛾1 takes across the three parameter sets (FIPS 204, Table 1). +/// +/// The bit-packing routines have one layout per distinct 𝛾1, so they dispatch on these rather than +/// on the parameter set. ML-DSA-65 and ML-DSA-87 share the second value. +pub(crate) const GAMMA1_2_POW_17: i32 = MLDSA44Params::gamma1; +/// See [`GAMMA1_2_POW_17`]. +pub(crate) const GAMMA1_2_POW_19: i32 = MLDSA65Params::gamma1; + +/// The two distinct values 𝛾2 takes across the three parameter sets (FIPS 204, Table 1). +/// +/// As with 𝛾1, the routines that depend on 𝛾2 have one form per distinct value rather than one per +/// parameter set. ML-DSA-65 and ML-DSA-87 share the second value. +pub(crate) const GAMMA2_Q_MINUS_1_OVER_88: i32 = MLDSA44Params::gamma2; +/// See [`GAMMA2_Q_MINUS_1_OVER_88`]. +pub(crate) const GAMMA2_Q_MINUS_1_OVER_32: i32 = MLDSA65Params::gamma2; + +/// The weaker of two security strengths. +/// +/// [`SecurityStrength`]'s discriminants are assigned in increasing order of strength, so comparing +/// them as integers orders them. A `const fn` because the strength of a HashML-DSA pairing is a +/// defaulted associated const. +const fn weaker_of(a: SecurityStrength, b: SecurityStrength) -> SecurityStrength { + if (a as u8) <= (b as u8) { a } else { b } +} + +/// A crate-private (aka "sealed") trait that prevents a new HashML-DSA pairing from being defined +/// outside this crate. +trait HashMLDSAParamsInternalTrait {} + +/// One HashML-DSA algorithm: an ML-DSA parameter set paired with a pre-hash function. +/// +/// FIPS 204, Algorithm 4 (HashML-DSA.Sign) leaves the choice of PH open, so an instantiation is a +/// pairing rather than a single parameter set. Everything that varies across the pairings lives +/// here, so [`crate::hash_mldsa::HashMLDSA`] takes one type rather than a parameter set plus a +/// hash function plus a digest length. +/// +/// Sealed via a private supertrait, so the six types below are the only implementations. +pub trait HashMLDSAParams: HashMLDSAParamsInternalTrait { + /// The ML-DSA parameter set underneath. + type MLDSA: MLDSAParams; + /// PH, the pre-hash function. + type PreHash: Hash + HashAlgParams + AlgorithmOID + Default; + + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + /// + /// Written out per pairing rather than derived: it is the two component names spliced + /// together, and `&'static str` cannot be concatenated in a const context. + const ALG_NAME: &'static str; + + /* Derived. Never written out per pairing. */ + + /// The length of the pre-hash `ph`, which is just PH's output length. + const PH_LEN: usize = ::OUTPUT_LEN; + + /// The strength claimed for the pairing, as reported by `Algorithm::MAX_SECURITY_STRENGTH`. + /// + /// A HashML-DSA signature is no stronger than either of its two components, so this is the + /// weaker of the two. That is what caps, for example, HashML-DSA-87_with_SHA256 at 128 bits. + const MAX_SECURITY_STRENGTH: SecurityStrength = weaker_of( + ::MAX_SECURITY_STRENGTH, + ::MAX_SECURITY_STRENGTH, + ); +} + +/// The HashML-DSA-44_with_SHA256 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA44_with_SHA256Params; +/// The HashML-DSA-65_with_SHA256 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA65_with_SHA256Params; +/// The HashML-DSA-87_with_SHA256 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA87_with_SHA256Params; +/// The HashML-DSA-44_with_SHA512 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA44_with_SHA512Params; +/// The HashML-DSA-65_with_SHA512 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA65_with_SHA512Params; +/// The HashML-DSA-87_with_SHA512 pairing. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HashMLDSA87_with_SHA512Params; + +impl HashMLDSAParamsInternalTrait for HashMLDSA44_with_SHA256Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA65_with_SHA256Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA87_with_SHA256Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA44_with_SHA512Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA65_with_SHA512Params {} +impl HashMLDSAParamsInternalTrait for HashMLDSA87_with_SHA512Params {} + +impl HashMLDSAParams for HashMLDSA44_with_SHA256Params { + type MLDSA = MLDSA44Params; + type PreHash = SHA256; + const ALG_NAME: &'static str = HASH_ML_DSA_44_with_SHA256_NAME; +} +impl HashMLDSAParams for HashMLDSA65_with_SHA256Params { + type MLDSA = MLDSA65Params; + type PreHash = SHA256; + const ALG_NAME: &'static str = HASH_ML_DSA_65_WITH_SHA256_NAME; +} +impl HashMLDSAParams for HashMLDSA87_with_SHA256Params { + type MLDSA = MLDSA87Params; + type PreHash = SHA256; + const ALG_NAME: &'static str = HASH_ML_DSA_87_with_SHA256_NAME; +} +impl HashMLDSAParams for HashMLDSA44_with_SHA512Params { + type MLDSA = MLDSA44Params; + type PreHash = SHA512; + const ALG_NAME: &'static str = HASH_ML_DSA_44_with_SHA512_NAME; +} +impl HashMLDSAParams for HashMLDSA65_with_SHA512Params { + type MLDSA = MLDSA65Params; + type PreHash = SHA512; + const ALG_NAME: &'static str = HASH_ML_DSA_65_WITH_SHA512_NAME; +} +impl HashMLDSAParams for HashMLDSA87_with_SHA512Params { + type MLDSA = MLDSA87Params; + type PreHash = SHA512; + const ALG_NAME: &'static str = HASH_ML_DSA_87_WITH_SHA512_NAME; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mldsa::d; + + /// FIPS 204, Table 1, transcribed column by column: the eight values each parameter set + /// assigns. `(tau, lambda, gamma1, gamma2, k, l, eta, omega)`. + const TABLE_1: [(i32, i32, i32, i32, usize, usize, usize, i32); 3] = [ + (39, 128, 131072, (q - 1) / 88, 4, 4, 2, 80), + (49, 192, 524288, (q - 1) / 32, 6, 5, 4, 55), + (60, 256, 524288, (q - 1) / 32, 8, 7, 2, 75), + ]; + + /// FIPS 204, Table 2, transcribed row by row: `(private key, public key, signature)` in bytes. + const TABLE_2: [(usize, usize, usize); 3] = + [(2560, 1312, 2420), (4032, 1952, 3309), (4896, 2592, 4627)]; + + /// FIPS 204, Table 1 also tabulates 𝛽, which it labels "𝛽 = 𝜏 ⋅ 𝜂". + const TABLE_1_BETA: [i32; 3] = [78, 196, 120]; + + fn check_table_1(i: usize) { + let (tau, lambda, gamma1, gamma2, k, l, eta, omega) = TABLE_1[i]; + assert_eq!(P::tau, tau, "{}: 𝜏", P::ALG_NAME); + assert_eq!(P::lambda, lambda, "{}: 𝜆", P::ALG_NAME); + assert_eq!(P::gamma1, gamma1, "{}: 𝛾1", P::ALG_NAME); + assert_eq!(P::gamma2, gamma2, "{}: 𝛾2", P::ALG_NAME); + assert_eq!(P::k, k, "{}: 𝑘", P::ALG_NAME); + assert_eq!(P::l, l, "{}: ℓ", P::ALG_NAME); + assert_eq!(P::eta, eta, "{}: 𝜂", P::ALG_NAME); + assert_eq!(P::omega, omega, "{}: 𝜔", P::ALG_NAME); + assert_eq!(P::beta, TABLE_1_BETA[i], "{}: 𝛽 = 𝜏 ⋅ 𝜂", P::ALG_NAME); + } + + fn check_table_2(i: usize) { + let (sk_len, pk_len, sig_len) = TABLE_2[i]; + assert_eq!(P::SK_LEN, sk_len, "{}: private key size", P::ALG_NAME); + assert_eq!(P::PK_LEN, pk_len, "{}: public key size", P::ALG_NAME); + assert_eq!(P::SIG_LEN, sig_len, "{}: signature size", P::ALG_NAME); + } + + /// Each of the three sizes of Table 2 also has a formula in FIPS 204, and the two must agree. + /// Table 2 is what is written down above; this is what re-derives it. + fn check_table_2_formulas() { + // Algorithm 22 (pkEncode): 𝑝𝑘 ∈ 𝔹^(32+32𝑘(bitlen (𝑞−1)−𝑑)). + let pk_len = 32 + 32 * P::k * (bitlen((q - 1) as u32) - d as usize); + assert_eq!(P::PK_LEN, pk_len, "{}: Algorithm 22 output size", P::ALG_NAME); + + // Algorithm 24 (skEncode): 𝑠𝑘 ∈ 𝔹^(32+32+64+32⋅((𝑘+ℓ)⋅bitlen (2𝜂)+𝑑𝑘)). + let sk_len = + 32 + 32 + 64 + 32 * ((P::k + P::l) * bitlen(2 * P::eta as u32) + d as usize * P::k); + assert_eq!(P::SK_LEN, sk_len, "{}: Algorithm 24 output size", P::ALG_NAME); + + // Algorithm 26 (sigEncode): 𝜎 ∈ 𝔹^(𝜆/4+ℓ⋅32⋅(1+bitlen (𝛾1−1))+𝜔+𝑘). + let sig_len = P::lambda as usize / 4 + + P::l * 32 * (1 + bitlen(P::gamma1 as u32 - 1)) + + P::omega as usize + + P::k; + assert_eq!(P::SIG_LEN, sig_len, "{}: Algorithm 26 output size", P::ALG_NAME); + } + + /// The associated types must be exactly as long as the consts that describe them; they are + /// written out by hand per parameter set, so this guards against a typo in one of them. + fn check_associated_type_sizes() { + assert_eq!( + size_of::(), + P::C_TILDE_LEN, + "{}: SigCTilde vs C_TILDE_LEN", + P::ALG_NAME + ); + assert_eq!( + size_of::(), + P::POLY_Z_PACKED_LEN, + "{}: PolyZPacked vs POLY_Z_PACKED_LEN", + P::ALG_NAME + ); + assert_eq!( + size_of::(), + P::POLY_W1_PACKED_LEN, + "{}: PolyW1Packed vs POLY_W1_PACKED_LEN", + P::ALG_NAME + ); + assert_eq!(::LEN, P::k, "{}: VecK vs 𝑘", P::ALG_NAME); + assert_eq!(::LEN, P::l, "{}: VecL vs ℓ", P::ALG_NAME); + } + + #[test] + fn test_parameter_sets_match_fips204_table_1() { + check_table_1::(0); + check_table_1::(1); + check_table_1::(2); + } + + #[test] + fn test_sizes_match_fips204_table_2() { + check_table_2::(0); + check_table_2::(1); + check_table_2::(2); + } + + #[test] + fn test_table_2_sizes_agree_with_the_encoding_formulas() { + check_table_2_formulas::(); + check_table_2_formulas::(); + check_table_2_formulas::(); + } + + #[test] + fn test_associated_types_are_the_length_their_consts_claim() { + check_associated_type_sizes::(); + check_associated_type_sizes::(); + check_associated_type_sizes::(); + } + + #[test] + fn test_bitlen_matches_its_definition() { + // FIPS 204 Section 2.3 defines bitlen 𝑥 as the length of the binary expansion of 𝑥. + assert_eq!(bitlen(0), 0); + assert_eq!(bitlen(1), 1); + assert_eq!(bitlen(2), 2); + assert_eq!(bitlen(3), 2); + assert_eq!(bitlen(4), 3); + // The two arguments the derivations above actually use, plus bitlen(𝑞 − 1) = 23. + assert_eq!(bitlen((1 << 17) - 1), 17); + assert_eq!(bitlen((1 << 19) - 1), 19); + assert_eq!(bitlen((q - 1) as u32), 23); + } + + #[test] + fn test_gamma_dispatch_constants_cover_every_parameter_set() { + // The packing routines dispatch on these; a parameter set whose 𝛾 is neither value would + // fall through to a panic at runtime rather than fail to compile, so pin them here. + for gamma1 in [MLDSA44Params::gamma1, MLDSA65Params::gamma1, MLDSA87Params::gamma1] { + assert!(gamma1 == GAMMA1_2_POW_17 || gamma1 == GAMMA1_2_POW_19); + } + for gamma2 in [MLDSA44Params::gamma2, MLDSA65Params::gamma2, MLDSA87Params::gamma2] { + assert!(gamma2 == GAMMA2_Q_MINUS_1_OVER_88 || gamma2 == GAMMA2_Q_MINUS_1_OVER_32); + } + assert_ne!(GAMMA1_2_POW_17, GAMMA1_2_POW_19); + assert_ne!(GAMMA2_Q_MINUS_1_OVER_88, GAMMA2_Q_MINUS_1_OVER_32); + } +} diff --git a/crypto/mldsa/src/polynomial.rs b/crypto/mldsa/src/polynomial.rs index 98d47125..73b33a0c 100644 --- a/crypto/mldsa/src/polynomial.rs +++ b/crypto/mldsa/src/polynomial.rs @@ -3,7 +3,9 @@ use crate::aux_functions::{ ZETAS, conditional_add_q, high_bits, low_bits, make_hint, montgomery_reduce, }; -use crate::mldsa::{MLDSA44_POLY_W1_PACKED_LEN, MLDSA65_POLY_W1_PACKED_LEN, N, q}; +use crate::mldsa::{N, d, q}; +use crate::params::{GAMMA2_Q_MINUS_1_OVER_32, GAMMA2_Q_MINUS_1_OVER_88, MLDSAParams}; +use bouncycastle_utils::secret::ZeroizablePrimitive; use core::ops::{Index, IndexMut}; /// A polynomial over the ML-DSA ring. @@ -12,8 +14,11 @@ use core::ops::{Index, IndexMut}; /// 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`. +/// +/// Public only because it appears in [`crate::VectorTrait`]'s signatures; its fields and +/// operations are crate-private, so from outside it is an opaque handle. #[derive(Clone, Copy)] -pub(crate) struct Polynomial { +pub struct Polynomial { pub(crate) coeffs: [i32; N], } @@ -34,7 +39,7 @@ impl IndexMut for Polynomial { impl Polynomial { /// Create a new polynomial with all coefficients set to zero. - pub const fn new() -> Self { + pub(crate) const fn new() -> Self { Self { coeffs: [0i32; N] } } @@ -65,25 +70,31 @@ impl Polynomial { } } - pub(crate) fn high_bits(&self) -> Self { + pub(crate) fn high_bits(&self) -> Self { let mut w = Self::new(); for i in 0..N { - w[i] = high_bits::(self[i]); + w[i] = high_bits::

(self[i]); } w } - pub(crate) fn low_bits(&self) -> Self { + pub(crate) fn low_bits(&self) -> Self { let mut w = Self::new(); for i in 0..N { - w[i] = low_bits::(self[i]); + w[i] = low_bits::

(self[i]); } w } - pub(crate) fn check_norm(&self) -> bool { + /// Tests whether any coefficient has absolute value at least `bound`. + /// + /// `bound` is a runtime argument rather than a const generic because every call site passes a + /// value derived from the parameter set (𝛾1 − 𝛽, 𝛾2 − 𝛽, or 𝛾2), and an associated const of a + /// type parameter cannot be used as a const generic argument. It is still a constant after + /// monomorphization, so this costs nothing. + pub(crate) fn check_norm(&self, bound: i32) -> bool { // It is acceptable that this function is not constant-time (returns true early) // The reason being because it is used in a rejection loop. // That is, the early quit here leads to rejection, dropping the secret values and @@ -95,33 +106,38 @@ impl Polynomial { // if bound > (q - 1) / 8 { // return true; // } - // but since BOUND is a constant here, a debug_assert is performed to make sure the value is what we expect. - debug_assert!(BOUND <= (q - 1) / 8); + // but since every caller passes a parameter-set constant, a debug_assert is performed + // instead to make sure the value is what we expect. + debug_assert!(bound <= (q - 1) / 8); let mut t: i32; for x in self.coeffs.iter() { t = *x >> 31; t = *x - (t & (2 * *x)); - if t >= BOUND { + if t >= bound { return true; } } false } - pub(crate) fn shift_left(&mut self) { + /// Multiplies every coefficient by 2^𝑑. + /// + /// 𝑑 is 13 for all three parameter sets (FIPS 204, Table 1), so it is read from the global + /// constant rather than being passed in. + pub(crate) fn shift_left_d(&mut self) { for x in self.coeffs.iter_mut() { *x <<= d; } } /// Creates the hint vector, and also returns its hamming weight (i.e. the number of 1's). - pub(crate) fn make_hint(&self, r: &Self) -> (Self, i32) { + pub(crate) fn make_hint(&self, r: &Self) -> (Self, i32) { let mut out = Polynomial::new(); let mut count = 0i32; for i in 0..N { - let x = make_hint::(self[i], r[i]); + let x = make_hint::

(self[i], r[i]); out[i] = x; // mutants note: this chains up to hint_hamming_weight > OMEGA and there is no test KAT that triggers this branch @@ -131,19 +147,24 @@ impl Polynomial { (out, count) } - pub(crate) fn w1_encode(&self) -> [u8; POLY_W1_PACKED_LEN] { - let mut r = [0u8; POLY_W1_PACKED_LEN]; + /// SimpleBitPack(𝐰1[𝑖], (𝑞 − 1)/(2𝛾2) − 1), the per-coordinate body of + /// FIPS 204, Algorithm 28 (w1Encode), line 3. + pub(crate) fn w1_encode(&self) -> P::PolyW1Packed { + let mut out = ::ZEROED; + let r = out.as_mut(); - match POLY_W1_PACKED_LEN { - MLDSA44_POLY_W1_PACKED_LEN => { + match P::gamma2 { + // ML-DSA-44: (𝑞 − 1)/(2𝛾2) − 1 = 43, so four 6-bit coefficients pack into three bytes. + GAMMA2_Q_MINUS_1_OVER_88 => { for i in 0..N / 4 { r[3 * i] = ((self[4 * i]) as u8) | ((self[4 * i + 1] << 6) as u8); r[3 * i + 1] = ((self[4 * i + 1] >> 2) as u8) | ((self[4 * i + 2] << 4) as u8); r[3 * i + 2] = ((self[4 * i + 2] >> 4) as u8) | ((self[4 * i + 3] << 2) as u8); } } - // ML-DSA65 and 87 share a POLY_W1_PACKED_LEN value - MLDSA65_POLY_W1_PACKED_LEN => { + // ML-DSA-65 and ML-DSA-87 share this 𝛾2: (𝑞 − 1)/(2𝛾2) − 1 = 15, so two 4-bit + // coefficients pack into one byte. + GAMMA2_Q_MINUS_1_OVER_32 => { for i in 0..N / 2 { r[i] = ((self[2 * i]) | (self[2 * i + 1] << 4)) as u8; } @@ -153,7 +174,7 @@ impl Polynomial { } } - r + out } /// Algorithm 41 NTT(𝑤) diff --git a/crypto/mldsa/tests/hash_mldsa_tests.rs b/crypto/mldsa/tests/hash_mldsa_tests.rs index ecdfb7c3..1ff7081e 100644 --- a/crypto/mldsa/tests/hash_mldsa_tests.rs +++ b/crypto/mldsa/tests/hash_mldsa_tests.rs @@ -3,7 +3,7 @@ mod hash_mldsa_tests { use bouncycastle_core::errors::SignatureError; use bouncycastle_core::key_material::{KeyMaterial256, KeyType}; use bouncycastle_core::traits::{ - Hash, PHSignatureVerifier, PHSigner, SignatureVerifier, Signer, + Hash, PHSignatureVerifier, PHSigner, SecurityStrength, SignatureVerifier, Signer, }; use bouncycastle_core_test_framework::signature::TestFrameworkSignature; use bouncycastle_hex as hex; @@ -365,4 +365,75 @@ mod hash_mldsa_tests { let pk_expanded = MLDSA44PublicKeyExpanded::from(&pk); HashMLDSA44_with_SHA256::verify_with_expanded_key(&pk_expanded, msg, None, &sig).unwrap(); } + + #[test] + fn algorithm_names_strengths_and_oids() { + use bouncycastle_core::traits::{Algorithm, AlgorithmOID}; + + // `Algorithm` is implemented once, generically over the pairing, so nothing else states + // these per algorithm. Pinned here so a wrong wiring of the blanket impl, or a typo in a + // pairing, is a test failure rather than a mislabelled algorithm. + assert_eq!(HashMLDSA44_with_SHA256::ALG_NAME, "HashML-DSA-44_with_SHA256"); + assert_eq!(HashMLDSA65_with_SHA256::ALG_NAME, "HashML-DSA-65_with_SHA256"); + assert_eq!(HashMLDSA87_with_SHA256::ALG_NAME, "HashML-DSA-87_with_SHA256"); + assert_eq!(HashMLDSA44_with_SHA512::ALG_NAME, "HashML-DSA-44_with_SHA512"); + assert_eq!(HashMLDSA65_with_SHA512::ALG_NAME, "HashML-DSA-65_with_SHA512"); + assert_eq!(HashMLDSA87_with_SHA512::ALG_NAME, "HashML-DSA-87_with_SHA512"); + + // The claimed strength is derived as the weaker of the two components, so these pin the + // derivation rather than six hand-written values. SHA-256 caps every pairing it appears in + // at 128 bits; with SHA-512 the ML-DSA parameter set is what binds. + assert_eq!(HashMLDSA44_with_SHA256::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(HashMLDSA65_with_SHA256::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(HashMLDSA87_with_SHA256::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(HashMLDSA44_with_SHA512::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(HashMLDSA65_with_SHA512::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!(HashMLDSA87_with_SHA512::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); + + // NIST's Computer Security Objects Register: id-hash-ml-dsa-44-with-sha512 { sigAlgs 32 }, + // -65- { sigAlgs 33 }, -87- { sigAlgs 34 }. The three SHA-256 pairings carry no OID in + // this implementation, which is why `AlgorithmOID` is still written out per alias rather + // than derived from the pairing like the name and strength above. + assert_eq!(HashMLDSA44_with_SHA512::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 32]); + assert_eq!(HashMLDSA65_with_SHA512::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 33]); + assert_eq!(HashMLDSA87_with_SHA512::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 34]); + + for (oid, der) in [ + (HashMLDSA44_with_SHA512::OID, HashMLDSA44_with_SHA512::OID_DER), + (HashMLDSA65_with_SHA512::OID, HashMLDSA65_with_SHA512::OID_DER), + (HashMLDSA87_with_SHA512::OID, HashMLDSA87_with_SHA512::OID_DER), + ] { + assert_eq!(der[0], 0x06, "DER tag must be OBJECT IDENTIFIER"); + assert_eq!(der[1] as usize, der.len() - 2, "DER length must match the content"); + assert_eq!( + &der[2..], + &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, *oid.last().unwrap() as u8] + ); + } + } + + #[test] + fn prehash_lengths_match_the_hash_functions() { + // `PH_LEN` is still a const generic on `HashMLDSA` -- `PHSigner` takes it as one -- but + // no alias hard-codes it: each passes `{ ...Params::PH_LEN }`, which is the pre-hash's own + // `HashAlgParams::OUTPUT_LEN`. So there is only one value, and nothing in the chain can + // disagree with itself. What is left to check is whether that value matches the digest + // the hash actually produces, which is what this test does. + assert_eq!(SHA256::new().hash(b"").len(), 32); + assert_eq!(SHA512::new().hash(b"").len(), 64); + + // A signature is produced from a `ph` of exactly that length, so a mismatch would fail + // here rather than silently truncating. + let msg = b"The quick brown fox"; + let ph256: [u8; 32] = SHA256::new().hash(msg).try_into().unwrap(); + let ph512: [u8; 64] = SHA512::new().hash(msg).try_into().unwrap(); + + let (pk, sk) = HashMLDSA65_with_SHA256::keygen().unwrap(); + let sig = HashMLDSA65_with_SHA256::sign_ph(&sk, &ph256, None).unwrap(); + HashMLDSA65_with_SHA256::verify_ph(&pk, &ph256, None, &sig).unwrap(); + + let (pk, sk) = HashMLDSA65_with_SHA512::keygen().unwrap(); + let sig = HashMLDSA65_with_SHA512::sign_ph(&sk, &ph512, None).unwrap(); + HashMLDSA65_with_SHA512::verify_ph(&pk, &ph512, None, &sig).unwrap(); + } } diff --git a/crypto/mldsa/tests/mldsa_tests.rs b/crypto/mldsa/tests/mldsa_tests.rs index 33aa8f9f..aebd3a06 100644 --- a/crypto/mldsa/tests/mldsa_tests.rs +++ b/crypto/mldsa/tests/mldsa_tests.rs @@ -1071,6 +1071,46 @@ mod mldsa_tests { _ => panic!("Expected an error when loading a SHAKE128 state into a MuBuilder"), } } + + #[test] + fn algorithm_names_and_oids() { + 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 + // blanket impls, or a typo in a parameter set, is a test failure rather than a silently + // mislabelled algorithm or an unparseable OID. + assert_eq!(MLDSA44::ALG_NAME, "ML-DSA-44"); + assert_eq!(MLDSA65::ALG_NAME, "ML-DSA-65"); + assert_eq!(MLDSA87::ALG_NAME, "ML-DSA-87"); + + assert_eq!(MLDSA44::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(MLDSA65::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!(MLDSA87::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); + + // NIST's Computer Security Objects Register: id-ml-dsa-44 { sigAlgs 17 }, + // id-ml-dsa-65 { sigAlgs 18 }, id-ml-dsa-87 { sigAlgs 19 }, under + // joint-iso-itu-t(2) country(16) us(840) organization(1) gov(101) csor(3) nistAlgorithm(4) + // sigAlgs(3). + assert_eq!(MLDSA44::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 17]); + assert_eq!(MLDSA65::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 18]); + assert_eq!(MLDSA87::OID, &[2, 16, 840, 1, 101, 3, 4, 3, 19]); + + // The DER encodings must be the OBJECT IDENTIFIER (tag 0x06) encodings of those arcs: + // 9 content bytes, the first being 40*2 + 16 = 0x60, then 840 as the two-byte 0x86 0x48. + for (oid, der) in [ + (MLDSA44::OID, MLDSA44::OID_DER), + (MLDSA65::OID, MLDSA65::OID_DER), + (MLDSA87::OID, MLDSA87::OID_DER), + ] { + assert_eq!(der[0], 0x06, "DER tag must be OBJECT IDENTIFIER"); + assert_eq!(der[1] as usize, der.len() - 2, "DER length must match the content"); + assert_eq!( + &der[2..], + &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, *oid.last().unwrap() as u8] + ); + } + } } struct Kat { diff --git a/crypto/mlkem-lowmemory/src/aux_functions.rs b/crypto/mlkem-lowmemory/src/aux_functions.rs index b548f440..406ef47a 100644 --- a/crypto/mlkem-lowmemory/src/aux_functions.rs +++ b/crypto/mlkem-lowmemory/src/aux_functions.rs @@ -146,7 +146,7 @@ pub(crate) fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { /// Takes a seed as input and outputs a pseudorandom sample from the distribution D𝜂(𝑅𝑞). /// Input: byte array 𝐵 ∈ 𝔹64𝜂 . /// Output: array 𝑓 ∈ ℤ256 ▷ the coefficients of the sampled polynomial -pub(crate) fn sample_poly_cbd(bytes: &[u8]) -> Polynomial { +pub(crate) fn sample_poly_cbd(bytes: &[u8], eta: i16) -> Polynomial { debug_assert_eq!(bytes.len(), 64 * eta as usize); let mut f = Polynomial::new(); @@ -193,7 +193,7 @@ pub(crate) fn sample_poly_cbd(bytes: &[u8]) -> Polynomial { /// SamplePolyCBD𝜂1(PRF𝜂1 (𝜎, 𝑁 )) /// Performs both the PRF and SamplePolyCBD steps -pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8) -> Polynomial { +pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { // Alg 13: 9: 𝐬[𝑖] ← SamplePolyCBD𝜂1(PRF𝜂1 (𝜎, 𝑁 )) // ▷ 𝐬[𝑖] ∈ ℤ256 sampled from CBD match eta { @@ -208,7 +208,7 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8) -> Polynomial buf }; - sample_poly_cbd::(&buf) + sample_poly_cbd(&buf, eta) } 3 => { let buf = { @@ -220,7 +220,7 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8) -> Polynomial buf }; - sample_poly_cbd::(&buf) + sample_poly_cbd(&buf, eta) } _ => unreachable!(), } diff --git a/crypto/mlkem-lowmemory/src/lib.rs b/crypto/mlkem-lowmemory/src/lib.rs index 250c9ea6..7a15b31e 100644 --- a/crypto/mlkem-lowmemory/src/lib.rs +++ b/crypto/mlkem-lowmemory/src/lib.rs @@ -244,6 +244,7 @@ mod aux_functions; mod low_memory_helpers; pub mod mlkem; mod mlkem_keys; +mod params; mod polynomial; /*** Exported types ***/ @@ -264,3 +265,5 @@ pub use mlkem::{MLKEM_RND_LEN, MLKEM_SEED_LEN, MLKEM_SS_LEN}; pub use mlkem::{MLKEM512_CT_LEN, MLKEM512_PK_LEN, MLKEM512_SK_LEN}; pub use mlkem::{MLKEM768_CT_LEN, MLKEM768_PK_LEN, MLKEM768_SK_LEN}; pub use mlkem::{MLKEM1024_CT_LEN, MLKEM1024_PK_LEN, MLKEM1024_SK_LEN}; + +/*** Parameter sets ***/ diff --git a/crypto/mlkem-lowmemory/src/low_memory_helpers.rs b/crypto/mlkem-lowmemory/src/low_memory_helpers.rs index f7773e54..8c16f34c 100644 --- a/crypto/mlkem-lowmemory/src/low_memory_helpers.rs +++ b/crypto/mlkem-lowmemory/src/low_memory_helpers.rs @@ -4,6 +4,7 @@ use crate::aux_functions::{byte_decode, byte_encode, sample_ntt, sample_poly_CBD}; use crate::mlkem::{N, POLY_BYTES, q}; +use crate::params::MLKEMParams; use crate::polynomial::Polynomial; /// Computes the element [i,j] of the A_hat public matrix @@ -13,23 +14,23 @@ pub(crate) fn expandA_elem(rho: &[u8; 32], i: usize, j: usize) -> Polynomial { /// Computes a single row of the core keygen operation /// Alg 13: line 18: 𝐀_hat ∘ 𝐬_hat -pub(crate) fn compute_A_hat_dot_s_hat( +pub(crate) fn compute_A_hat_dot_s_hat( rho: &[u8; 32], sigma: &[u8; 32], row: usize, ) -> Polynomial { let mut t_hat_i: Polynomial = { let mut A_i0 = expandA_elem(rho, row, 0); - let mut s_0 = sample_poly_CBD::(sigma, 0 as u8); + let mut s_0 = sample_poly_CBD(sigma, 0 as u8, P::eta1); s_0.ntt(); // now s_hat_0 A_i0.base_mult_montgomery(&s_0); A_i0 }; - for j in 1..k { + for j in 1..P::k { let mut A_ij = expandA_elem(rho, row, j); - let mut s_j = sample_poly_CBD::(sigma, j as u8); + let mut s_j = sample_poly_CBD(sigma, j as u8, P::eta1); s_j.ntt(); // now s_hat_j A_ij.base_mult_montgomery(&s_j); t_hat_i.add(&A_ij); @@ -43,7 +44,7 @@ pub(crate) fn compute_A_hat_dot_s_hat( /// Compute a single row of the core encaps operation /// Alg 14: line 19: NTT−1(𝐀_hat_T ∘ 𝐲_hat) -pub(crate) fn compute_A_hat_dot_y_hat( +pub(crate) fn compute_A_hat_dot_y_hat( rho: &[u8; 32], r: &[u8; 32], row: usize, @@ -52,7 +53,7 @@ pub(crate) fn compute_A_hat_dot_y_hat( // ▷ re-generate matrix 𝐀 ∈ (ℤ256_𝑞 )𝑘×𝑘 sampled in Alg. 13 // 9: for (𝑖 ← 0; 𝑖 < 𝑘; 𝑖++) - // ▷ generate 𝐲 ∈ (ℤ256_𝑞)k + // ▷ generate 𝐲 ∈ (ℤ256_𝑞)^𝑘 // 10: 𝐲[𝑖] ← SamplePolyCBD𝜂1(PRF𝜂1 (𝑟, 𝑁)) // ▷ 𝐲[𝑖] ∈ ℤ256 sampled from CBD // 11: 𝑁 ← 𝑁 + 1 @@ -61,16 +62,16 @@ pub(crate) fn compute_A_hat_dot_y_hat( let mut u_i: Polynomial = { let mut A_0i = expandA_elem(rho, 0, row); - let mut y_0 = sample_poly_CBD::(r, /*N*/ 0); + let mut y_0 = sample_poly_CBD(r, /*N*/ 0, P::eta1); y_0.ntt(); A_0i.base_mult_montgomery(&y_0); A_0i }; - for j in 1..k { + for j in 1..P::k { let mut A_ji = expandA_elem(&rho, j, row); - let mut y_j = sample_poly_CBD::(r, /*N*/ j as u8); + let mut y_j = sample_poly_CBD(r, /*N*/ j as u8, P::eta1); y_j.ntt(); A_ji.base_mult_montgomery(&y_j); u_i.add(&A_ji); @@ -82,12 +83,12 @@ pub(crate) fn compute_A_hat_dot_y_hat( /// Compute a term of the output polynomial v of the core encaps operation based on a single row of t_hat_i and y_hat. /// Alg 14: line 21: 𝑣 ← NTT−1(𝐭_hat_T ∘ 𝐲_hat) -pub(crate) fn compute_t_hat_dot_y_hat_row( +pub(crate) fn compute_t_hat_dot_y_hat_row( r: &[u8; 32], t_hat_i: &Polynomial, row: usize, ) -> Polynomial { - let mut y_i = sample_poly_CBD::(r, /*N*/ row as u8); + let mut y_i = sample_poly_CBD(r, /*N*/ row as u8, P::eta1); y_i.ntt(); y_i.base_mult_montgomery(&t_hat_i); y_i.inv_ntt(); @@ -95,32 +96,29 @@ pub(crate) fn compute_t_hat_dot_y_hat_row( y_i } -pub(crate) fn pack_t_hat_row( +pub(crate) fn pack_t_hat_row( t_hat_i: &Polynomial, row: usize, - t_hat_packed: &mut [u8; T_PACKED_LEN], + t_hat_packed: &mut P::TPacked, ) { byte_encode::<12, POLY_BYTES>( &t_hat_i, - t_hat_packed[row * POLY_BYTES..(row + 1) * POLY_BYTES].as_mut().try_into().unwrap(), + (&mut t_hat_packed.as_mut()[row * POLY_BYTES..(row + 1) * POLY_BYTES]).try_into().unwrap(), ); } -pub(crate) fn unpack_t_hat_row( - t_hat_packed: &[u8; T_PACKED_LEN], - row: usize, -) -> Polynomial { +pub(crate) fn unpack_t_hat_row(t_hat_packed: &[u8], row: usize) -> Polynomial { byte_decode::<12, POLY_BYTES>( t_hat_packed[row * POLY_BYTES..(row + 1) * POLY_BYTES].try_into().unwrap(), ) } -pub(crate) fn pack_s_hat_row( +pub(crate) fn pack_s_hat_row( s_hat_i: &Polynomial, row: usize, s_hat_packed: &mut [u8], ) { - debug_assert!(s_hat_packed.len() >= k * POLY_BYTES); + debug_assert!(s_hat_packed.len() >= P::k * POLY_BYTES); byte_encode::<12, POLY_BYTES>( s_hat_i, @@ -130,28 +128,28 @@ pub(crate) fn pack_s_hat_row( /// This is an optimized version of /// ByteEncode_𝑑𝑢( Compress_𝑑𝑢(𝐮) ) -/// which packs a single row of the polynomial vector u according to the packing coefficient dv +/// which packs a single row of the polynomial vector u according to the packing coefficient 𝑑𝑢 /// into the correct location within the ciphertext -pub(crate) fn compress_u_row( +pub(crate) fn compress_u_row( u_i: Polynomial, row: usize, ct: &mut [u8; CT_LEN], ) { - // make sure we have received a dv - assert!(du == 10 || du == 11); + // make sure we received a supported 𝑑𝑢 + assert!(P::du == 10 || P::du == 11); // bc-java has a conditional_sub_q() here, but I pass all unit tests without it, so I'm taking it out for performance. // let mut u_i = u_i.clone(); // u_i.conditional_sub_q(); // figure out where in the ct array we're going to write to - // each of the N i16's will take du bits, so a polynomial takes N * du bits, then we have k of them - let start: usize = row * (N * (du as usize) / 8); - let end: usize = (row + 1) * (N * (du as usize) / 8); + // each of the N i16's will take 𝑑𝑢 bits, so a polynomial takes N * 𝑑𝑢 bits, then we have 𝑘 of them + let start: usize = row * (N * (P::du as usize) / 8); + let end: usize = (row + 1) * (N * (P::du as usize) / 8); let out = &mut ct[start..end]; let mut idx = 0; - match du { + match P::du { 10 => { // MLKEM512 and MLKEM 768 let mut t = [0i16; 4]; @@ -196,24 +194,24 @@ pub(crate) fn compress_u_row( } } -pub(crate) fn unpack_ciphertext_u_row( +pub(crate) fn unpack_ciphertext_u_row( row: usize, ct: &[u8; CT_LEN], ) -> Polynomial { let mut u_i = Polynomial::new(); - // make sure to received a dv - assert!(du == 10 || du == 11); + // make sure we received a supported 𝑑𝑢 + assert!(P::du == 10 || P::du == 11); // figure out where in the ct array we're going to write to - // each of the N i16's will take du bits, so a polynomial takes N * du bits, then we have k of them - let start: usize = row * (N * (du as usize) / 8); - let end: usize = (row + 1) * (N * (du as usize) / 8); + // each of the N i16's will take 𝑑𝑢 bits, so a polynomial takes N * 𝑑𝑢 bits, then we have 𝑘 of them + let start: usize = row * (N * (P::du as usize) / 8); + let end: usize = (row + 1) * (N * (P::du as usize) / 8); let compressed_u_i = &ct[start..end]; let mut idx = 0; - match du { + match P::du { 10 => { // MLKEM512 and MLKEM768 let mut t = [0i16; 4]; @@ -269,18 +267,13 @@ pub(crate) fn unpack_ciphertext_u_row( u_i } -pub(crate) fn unpack_ciphertext_v< - const k: usize, - const CT_LEN: usize, - const du: i16, - const dv: i16, ->( +pub(crate) fn unpack_ciphertext_v( c: &[u8; CT_LEN], ) -> Polynomial { - // each of the N i16's will take du bits, so a polynomial takes N * du bits, then we have k of them - let lim: usize = k * (N * (du as usize) / 8); + // each of the N i16's will take 𝑑𝑢 bits, so a polynomial takes N * 𝑑𝑢 bits, then we have 𝑘 of them + let lim: usize = P::k * (N * (P::du as usize) / 8); - let v = Polynomial::decompress_poly::(&c[lim..]); + let v = Polynomial::decompress_poly::

(&c[lim..]); v } diff --git a/crypto/mlkem-lowmemory/src/mlkem.rs b/crypto/mlkem-lowmemory/src/mlkem.rs index dfb7ce51..da61c593 100644 --- a/crypto/mlkem-lowmemory/src/mlkem.rs +++ b/crypto/mlkem-lowmemory/src/mlkem.rs @@ -11,6 +11,7 @@ use crate::mlkem_keys::{ }; use crate::mlkem_keys::{MLKEMPrivateKeyInternalTrait, MLKEMPrivateKeyTrait}; 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::{ @@ -45,70 +46,44 @@ pub const MLKEM_SS_LEN: usize = 32; pub(crate) const N: usize = 256; pub(crate) const q: i16 = 3329; pub(crate) const q_inv: i32 = 62209; -pub(crate) const ETA2: i16 = 2; pub(crate) const POLY_BYTES: usize = 384; /* ML-KEM-512 params */ -/// Length of the \[u8] holding a ML-KEM-512 public key. -pub const MLKEM512_PK_LEN: usize = 800; -/// Length of the \[u8] holding a ML-KEM-512 seed-based private key. +/// Length of the \[u8] holding an ML-KEM-512 public key. +pub const MLKEM512_PK_LEN: usize = MLKEM512Params::PK_LEN; +/// Length of the \[u8] holding an ML-KEM-512 seed-based private key. pub const MLKEM512_SK_LEN: usize = MLKEM_SEED_LEN; /// Length of the \[u8] holding a full ML-KEM-512 private key in the NIST encoding. -pub const MLKEM512_FULL_SK_LEN: usize = 1632; -/// Length of the \[u8] holding a ML-KEM-512 ciphertext. -pub const MLKEM512_CT_LEN: usize = 768; -pub(crate) const MLKEM512_k: usize = 2; -pub(crate) const MLKEM512_ETA1: i16 = 3; -pub(crate) const MLKEM512_DU: i16 = 10; -pub(crate) const MLKEM512_DV: i16 = 4; -/// Maps to "required RBG strength (bits)" in FIPS 203 Table 2 -pub(crate) const MLKEM512_LAMBDA: i16 = 128; - -// internal derived values -pub(crate) const MLKEM512_T_PACKED_LEN: usize = 12 * MLKEM512_k * 32; +pub const MLKEM512_FULL_SK_LEN: usize = MLKEM512Params::FULL_SK_LEN; +/// Length of the \[u8] holding an ML-KEM-512 ciphertext. +pub const MLKEM512_CT_LEN: usize = MLKEM512Params::CT_LEN; + +/*** internal derived values ***/ /* ML-KEM-768 params */ -/// Length of the \[u8] holding a ML-KEM-768 public key. -pub const MLKEM768_PK_LEN: usize = 1184; -/// Length of the \[u8] holding a ML-KEM-768 seed-based private key. +/// Length of the \[u8] holding an ML-KEM-768 public key. +pub const MLKEM768_PK_LEN: usize = MLKEM768Params::PK_LEN; +/// Length of the \[u8] holding an ML-KEM-768 seed-based private key. pub const MLKEM768_SK_LEN: usize = MLKEM_SEED_LEN; /// Length of the \[u8] holding a full ML-KEM-768 private key in the NIST encoding. -pub const MLKEM768_FULL_SK_LEN: usize = 2400; -/// Length of the \[u8] holding a ML-KEM-768 ciphertext. -pub const MLKEM768_CT_LEN: usize = 1088; -pub(crate) const MLKEM768_k: usize = 3; -pub(crate) const MLKEM768_ETA1: i16 = 2; -pub(crate) const MLKEM768_DU: i16 = 10; -pub(crate) const MLKEM768_DV: i16 = 4; -/// Maps to "required RBG strength (bits)" in FIPS 203 Table 2 -pub(crate) const MLKEM768_LAMBDA: i16 = 192; - -// internal derived values -pub(crate) const MLKEM768_T_PACKED_LEN: usize = 12 * MLKEM768_k * 32; +pub const MLKEM768_FULL_SK_LEN: usize = MLKEM768Params::FULL_SK_LEN; +/// Length of the \[u8] holding an ML-KEM-768 ciphertext. +pub const MLKEM768_CT_LEN: usize = MLKEM768Params::CT_LEN; /* ML-KEM-1024 params */ -/// Length of the \[u8] holding a ML-KEM-1024 public key. -pub const MLKEM1024_PK_LEN: usize = 1568; -/// Length of the \[u8] holding a ML-KEM-512 seed-based private key. +/// Length of the \[u8] holding an ML-KEM-1024 public key. +pub const MLKEM1024_PK_LEN: usize = MLKEM1024Params::PK_LEN; +/// Length of the \[u8] holding an ML-KEM-1024 seed-based private key. pub const MLKEM1024_SK_LEN: usize = MLKEM_SEED_LEN; -/// Length of the \[u8] holding a full ML-KEM-512 private key in the NIST encoding. -pub const MLKEM1024_FULL_SK_LEN: usize = 3168; -/// Length of the \[u8] holding a ML-KEM-1024 ciphertext. -pub const MLKEM1024_CT_LEN: usize = 1568; -pub(crate) const MLKEM1024_k: usize = 4; -pub(crate) const MLKEM1024_ETA1: i16 = 2; -pub(crate) const MLKEM1024_DU: i16 = 11; -pub(crate) const MLKEM1024_DV: i16 = 5; -/// Maps to "required RBG strength (bits)" in FIPS 203 Table 2 -pub(crate) const MLKEM1024_LAMBDA: i16 = 256; - -// internal derived values -pub(crate) const MLKEM1024_T_PACKED_LEN: usize = 12 * MLKEM1024_k * 32; - -// Typedefs just to make the algorithms look more like the FIPS 204 sample code. +/// Length of the \[u8] holding a full ML-KEM-1024 private key in the NIST encoding. +pub const MLKEM1024_FULL_SK_LEN: usize = MLKEM1024Params::FULL_SK_LEN; +/// Length of the \[u8] holding an ML-KEM-1024 ciphertext. +pub const MLKEM1024_CT_LEN: usize = MLKEM1024Params::CT_LEN; + +/*** Typedefs just to make the algorithms look more like the FIPS 204 sample code. ***/ pub(crate) type G = SHA3_512; pub(crate) type H = SHA3_256; pub(crate) type J = SHAKE256; @@ -117,86 +92,73 @@ pub(crate) type J = SHAKE256; /// The ML-KEM-512 algorithm. pub type MLKEM512 = MLKEM< + MLKEM512Params, + MLKEM512PublicKey, + MLKEM512PrivateKey, MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM512_FULL_SK_LEN, MLKEM512_CT_LEN, MLKEM_SS_LEN, - MLKEM512PublicKey, - MLKEM512PrivateKey, - MLKEM512_k, - MLKEM512_ETA1, - MLKEM512_DU, - MLKEM512_DV, - MLKEM512_LAMBDA, - MLKEM512_T_PACKED_LEN, >; -impl Algorithm for MLKEM512 { - const ALG_NAME: &'static str = ML_KEM_512_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} -/// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-512 { kems 1 } -impl AlgorithmOID for MLKEM512 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 1]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x01]; -} - /// The ML-KEM-768 algorithm. pub type MLKEM768 = MLKEM< + MLKEM768Params, + MLKEM768PublicKey, + MLKEM768PrivateKey, MLKEM768_PK_LEN, MLKEM768_SK_LEN, MLKEM768_FULL_SK_LEN, MLKEM768_CT_LEN, MLKEM_SS_LEN, - MLKEM768PublicKey, - MLKEM768PrivateKey, - MLKEM768_k, - MLKEM768_ETA1, - MLKEM768_DU, - MLKEM768_DV, - MLKEM768_LAMBDA, - MLKEM768_T_PACKED_LEN, >; -impl Algorithm for MLKEM768 { - const ALG_NAME: &'static str = ML_KEM_768_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; -} -/// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-768 { kems 2 } -impl AlgorithmOID for MLKEM768 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 2]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x02]; -} - /// The ML-KEM-1024 algorithm. pub type MLKEM1024 = MLKEM< + MLKEM1024Params, + MLKEM1024PublicKey, + MLKEM1024PrivateKey, MLKEM1024_PK_LEN, MLKEM1024_SK_LEN, MLKEM1024_FULL_SK_LEN, MLKEM1024_CT_LEN, MLKEM_SS_LEN, - MLKEM1024PublicKey, - MLKEM1024PrivateKey, - MLKEM1024_k, - MLKEM1024_ETA1, - MLKEM1024_DU, - MLKEM1024_DV, - MLKEM1024_LAMBDA, - MLKEM1024_T_PACKED_LEN, >; -impl Algorithm for MLKEM1024 { - const ALG_NAME: &'static str = ML_KEM_1024_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +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, +> Algorithm for MLKEM +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -/// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-1024 { kems 3 } -impl AlgorithmOID for MLKEM1024 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 3]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x03]; + +/// The OIDs NIST assigned in the Computer Security Objects Register: id-alg-ml-kem-512 +/// { kems 1 }, id-alg-ml-kem-768 { kems 2 } and id-alg-ml-kem-1024 { kems 3 }. As with +/// [`Algorithm`], the values belong to the parameter set, so one impl covers all three. +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, +> AlgorithmOID for MLKEM +{ + const OID: &'static [u32] = P::OID; + const OID_DER: &'static [u8] = P::OID_DER; } /// The core internal implementation of the ML-KEM algorithm. @@ -204,57 +166,30 @@ impl AlgorithmOID for MLKEM1024 { /// but is shouldn't ever need to be used directly. /// Please use the named public types. pub struct MLKEM< + 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, - PK: MLKEMPublicKeyTrait - + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta1: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, - const T_PACKED_LEN: usize, > { - _phantom: PhantomData<(PK, SK)>, + _phantom: PhantomData<(P, PK, SK)>, } 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, - PK: MLKEMPublicKeyTrait - + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta1: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, - const T_PACKED_LEN: usize, -> - MLKEM< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - CT_LEN, - SS_LEN, - PK, - SK, - k, - eta1, - du, - dv, - LAMBDA, - T_PACKED_LEN, - > +> MLKEM { /// Performs the first step of key generation to transform the single provided seed into a set of internal intermediate seeds. /// @@ -278,7 +213,7 @@ impl< /// Input: randomness 𝑟 ∈ 𝔹32 . /// Output: ciphertext 𝑐 ∈ 𝔹32(𝑑𝑢𝑘+𝑑𝑣). fn pke_encrypt( - t_hat_packed: &[u8; T_PACKED_LEN], + t_hat_packed: &P::TPacked, rho: &[u8; 32], m: [u8; 32], r: &[u8; 32], @@ -297,14 +232,14 @@ impl< // Note: y_hat is needed twice: once here at line 19, and again at line 21. // Here it is generated each time it is needed in order to save memory. - for i in 0..k { - let mut u_i = compute_A_hat_dot_y_hat::(rho, &r, i); + for i in 0..P::k { + let mut u_i = compute_A_hat_dot_y_hat::

(rho, &r, i); - let e1_i = sample_poly_CBD::(&r, (k + i) as u8); + let e1_i = sample_poly_CBD(&r, (P::k + i) as u8, P::eta2); u_i.add(&e1_i); u_i.poly_reduce(); - compress_u_row::(u_i, i, &mut ct); + compress_u_row::(u_i, i, &mut ct); } // 17: 𝑒2 ← SamplePolyCBD_𝜂2(PRF𝜂2 (𝑟, 𝑁)) @@ -313,23 +248,23 @@ impl< // 23: 𝑐2 ← ByteEncode_𝑑𝑣(Compress_𝑑𝑣(𝑣)) { // compute v, which is a single polynomial, but requires iterating over the vectors t_hat and y_hat - let mut v = compute_t_hat_dot_y_hat_row::( + let mut v = compute_t_hat_dot_y_hat_row::

( &r, - &unpack_t_hat_row(t_hat_packed, 0), + &unpack_t_hat_row(t_hat_packed.as_ref(), 0), /*row*/ 0, ); - for i in 1..k { - let v_i = compute_t_hat_dot_y_hat_row::( + for i in 1..P::k { + let v_i = compute_t_hat_dot_y_hat_row::

( &r, - &unpack_t_hat_row(t_hat_packed, i), + &unpack_t_hat_row(t_hat_packed.as_ref(), i), /*row*/ i, ); v.add(&v_i); } // perform polynomial addition - let e2 = sample_poly_CBD::(&r, 2 * k as u8); + let e2 = sample_poly_CBD(&r, 2 * P::k as u8, P::eta2); v.add(&e2); let mu = Polynomial::from_msg(m); @@ -337,7 +272,7 @@ impl< v.poly_reduce(); - v.compress_poly::(&mut ct[CT_LEN - (N * (dv as usize) / 8)..]); + v.compress_poly::

(&mut ct[CT_LEN - (N * (P::dv as usize) / 8)..]); } ct @@ -367,8 +302,6 @@ impl< /// 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]) { - debug_assert_eq!(CT_LEN, 32 * ((du as usize) * k + (dv as usize))); - // 1: (𝐾, 𝑟) ← G(𝑚‖H(ek)) // ▷ derive shared secret key 𝐾 and randomness 𝑟 let K: [u8; MLKEM_SS_LEN]; @@ -411,7 +344,7 @@ impl< let mut v1 = { let mut s_hat_i = dk.compute_s_hat_row(0); { - let mut u_prime_i = unpack_ciphertext_u_row::(0, &ct); + let mut u_prime_i = unpack_ciphertext_u_row::(0, &ct); u_prime_i.ntt(); s_hat_i.base_mult_montgomery(&u_prime_i); } @@ -420,10 +353,10 @@ impl< s_hat_i }; - for i in 1..k { + for i in 1..P::k { let mut s_hat_i = dk.compute_s_hat_row(i); { - let mut u_prime_i = unpack_ciphertext_u_row::(i, &ct); + let mut u_prime_i = unpack_ciphertext_u_row::(i, &ct); u_prime_i.ntt(); s_hat_i.base_mult_montgomery(&u_prime_i); } @@ -439,7 +372,7 @@ impl< let w = { // second half of // 6: 𝑤 ← 𝑣′ − NTT−1(𝐬_hat^T ∘ NTT(𝐮′)) - let mut v_prime = unpack_ciphertext_v::(&ct); + let mut v_prime = unpack_ciphertext_v::(&ct); v_prime.sub(&v1); v_prime.poly_reduce(); @@ -532,52 +465,17 @@ impl< } 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, - PK: MLKEMPublicKeyTrait - + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta1: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, - const T_PACKED_LEN: usize, -> - MLKEMTrait< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - CT_LEN, - SS_LEN, - PK, - SK, - k, - eta1, - du, - dv, - LAMBDA, - T_PACKED_LEN, - > - for MLKEM< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - CT_LEN, - SS_LEN, - PK, - SK, - k, - eta1, - du, - dv, - LAMBDA, - T_PACKED_LEN, - > +> MLKEMTrait + for MLKEM { /// Imports a secret key from a seed. fn keygen_from_seed(seed: &KeyMaterial<64>) -> Result<(PK, SK), KEMError> { @@ -624,21 +522,15 @@ impl< /// Trait for all three of the ML-DSA algorithm variants. pub trait MLKEMTrait< + 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, - PK: MLKEMPublicKeyTrait - + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, - const T_PACKED_LEN: usize, >: Sized { /// Generates a fresh key pair. @@ -650,7 +542,7 @@ pub trait MLKEMTrait< // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), KEMError> { // Source the seed from the provided RNG - if rng.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if rng.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } let mut seed = KeyMaterial::<64>::new(); @@ -681,37 +573,17 @@ pub trait MLKEMTrait< } 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, - PK: MLKEMPublicKeyTrait - + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, - const T_PACKED_LEN: usize, > KEMEncapsulator - for MLKEM< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - CT_LEN, - SS_LEN, - PK, - SK, - k, - eta, - du, - dv, - LAMBDA, - T_PACKED_LEN, - > + for MLKEM { fn encaps(pk: &PK) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { let mut os_rng = HashDRBG_SHA512::new_from_os(); @@ -723,7 +595,7 @@ impl< rng: &mut dyn RNG, ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { // Source the random message m from the provided RNG - if rng.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if rng.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } let mut m = [0u8; 32]; @@ -734,7 +606,7 @@ impl< let mut ss_keymaterial = KeyMaterial::::from_bytes_as_type(&ss_bytes, KeyType::CryptographicRandom)?; do_hazardous_operations(&mut ss_keymaterial, |ss_keymaterial| { - ss_keymaterial.set_security_strength(SecurityStrength::from_bits(LAMBDA as usize)) + ss_keymaterial.set_security_strength(P::MAX_SECURITY_STRENGTH) })?; Ok((ss_keymaterial, ct)) @@ -742,37 +614,17 @@ impl< } 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, - PK: MLKEMPublicKeyTrait - + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, - const T_PACKED_LEN: usize, > KEMDecapsulator - for MLKEM< - PK_LEN, - SK_LEN, - FULL_SK_LEN, - CT_LEN, - SS_LEN, - PK, - SK, - k, - eta, - du, - dv, - LAMBDA, - T_PACKED_LEN, - > + for MLKEM { /// Performs a decapsulation of the given ciphertext. /// Returns the shared secret key. @@ -789,7 +641,7 @@ impl< let mut ss_keymaterial = KeyMaterial::::from_bytes_as_type(&ss_bytes, KeyType::CryptographicRandom)?; do_hazardous_operations(&mut ss_keymaterial, |ss_keymaterial| { - ss_keymaterial.set_security_strength(SecurityStrength::from_bits(LAMBDA as usize)) + ss_keymaterial.set_security_strength(P::MAX_SECURITY_STRENGTH) })?; Ok(ss_keymaterial) diff --git a/crypto/mlkem-lowmemory/src/mlkem_keys.rs b/crypto/mlkem-lowmemory/src/mlkem_keys.rs index df479a0b..c5f62e6e 100644 --- a/crypto/mlkem-lowmemory/src/mlkem_keys.rs +++ b/crypto/mlkem-lowmemory/src/mlkem_keys.rs @@ -3,27 +3,18 @@ use crate::low_memory_helpers::{ compute_A_hat_dot_s_hat, pack_s_hat_row, pack_t_hat_row, unpack_t_hat_row, }; use crate::mlkem::{G, H, POLY_BYTES, q}; -use crate::mlkem::{ - MLKEM512_ETA1, MLKEM512_FULL_SK_LEN, MLKEM512_LAMBDA, MLKEM512_PK_LEN, MLKEM512_SK_LEN, - MLKEM512_T_PACKED_LEN, MLKEM512_k, -}; -use crate::mlkem::{ - MLKEM768_ETA1, MLKEM768_FULL_SK_LEN, MLKEM768_LAMBDA, MLKEM768_PK_LEN, MLKEM768_SK_LEN, - MLKEM768_T_PACKED_LEN, MLKEM768_k, -}; -use crate::mlkem::{ - MLKEM1024_ETA1, MLKEM1024_FULL_SK_LEN, MLKEM1024_LAMBDA, MLKEM1024_PK_LEN, MLKEM1024_SK_LEN, - MLKEM1024_T_PACKED_LEN, MLKEM1024_k, -}; +use crate::mlkem::{MLKEM512_FULL_SK_LEN, MLKEM512_PK_LEN, MLKEM512_SK_LEN}; +use crate::mlkem::{MLKEM768_FULL_SK_LEN, MLKEM768_PK_LEN, MLKEM768_SK_LEN}; +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 crate::{ML_KEM_512_NAME, ML_KEM_768_NAME, ML_KEM_1024_NAME}; 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_sha3::SHA3_256; -use bouncycastle_utils::secret::Secret; +use bouncycastle_utils::secret::{Secret, ZeroizablePrimitive}; use core::fmt; use core::fmt::{Debug, Display, Formatter}; // imports just for docs @@ -31,53 +22,37 @@ use core::fmt::{Debug, Display, Formatter}; /* Pub Types */ /// ML-KEM-512 Public Key -pub type MLKEM512PublicKey = MLKEMPublicKey; +pub type MLKEM512PublicKey = MLKEMPublicKey; /// ML-KEM-512 Private Key -pub type MLKEM512PrivateKey = MLKEMSeedPrivateKey< - MLKEM512_k, - MLKEM512_ETA1, - MLKEM512_LAMBDA, - MLKEM512_SK_LEN, - MLKEM512_FULL_SK_LEN, - MLKEM512_PK_LEN, - MLKEM512_T_PACKED_LEN, ->; +pub type MLKEM512PrivateKey = + MLKEMSeedPrivateKey; /// ML-KEM-768 Public Key -pub type MLKEM768PublicKey = MLKEMPublicKey; +pub type MLKEM768PublicKey = MLKEMPublicKey; /// ML-KEM-768 Private Key -pub type MLKEM768PrivateKey = MLKEMSeedPrivateKey< - MLKEM768_k, - MLKEM768_ETA1, - MLKEM768_LAMBDA, - MLKEM768_SK_LEN, - MLKEM768_FULL_SK_LEN, - MLKEM768_PK_LEN, - MLKEM768_T_PACKED_LEN, ->; +pub type MLKEM768PrivateKey = + MLKEMSeedPrivateKey; /// ML-KEM-1024 Public Key -pub type MLKEM1024PublicKey = MLKEMPublicKey; +pub type MLKEM1024PublicKey = MLKEMPublicKey; /// ML-KEM-1024 Private Key -pub type MLKEM1024PrivateKey = MLKEMSeedPrivateKey< - MLKEM1024_k, - MLKEM1024_ETA1, - MLKEM1024_LAMBDA, - MLKEM1024_SK_LEN, - MLKEM1024_FULL_SK_LEN, - MLKEM1024_PK_LEN, - MLKEM1024_T_PACKED_LEN, ->; +pub type MLKEM1024PrivateKey = + MLKEMSeedPrivateKey; /// An ML-KEM public key. -#[derive(Clone)] -pub struct MLKEMPublicKey { - pub(crate) t_hat_packed: [u8; T_PACKED_LEN], +pub struct MLKEMPublicKey { + pub(crate) t_hat_packed: P::TPacked, pub(crate) rho: [u8; 32], } +// Written out rather than derived: `#[derive(Clone)]` would demand `P: Clone`, and `P` is a +// marker for the parameter set that is never stored, only used to name the field types. +impl Clone for MLKEMPublicKey { + fn clone(&self) -> Self { + Self { t_hat_packed: self.t_hat_packed, rho: self.rho } + } +} + /// General trait for all ML-KEM public keys types. -pub trait MLKEMPublicKeyTrait: - KEMPublicKey -{ +pub trait MLKEMPublicKeyTrait: KEMPublicKey { /// Algorithm 23 pkDecode(𝑝𝑘) /// Reverses the procedure pkEncode. /// Input: Public key 𝑝𝑘 ∈ 𝔹32+32𝑘(bitlen (𝑞−1)−𝑑). @@ -86,7 +61,7 @@ pub trait MLKEMPublicKeyTrait Result; /// Get a ref to t_hat_packed byte array - fn t_hat_packed(&self) -> &[u8; T_PACKED_LEN]; + fn t_hat_packed(&self) -> &P::TPacked; /// Get a ref to rho fn rho(&self) -> &[u8; 32]; @@ -95,29 +70,30 @@ pub trait MLKEMPublicKeyTrait [u8; 32]; } -pub(crate) trait MLKEMPublicKeyInternalTrait< - const k: usize, - const T_PACKED_LEN: usize, - const PK_LEN: usize, ->: MLKEMPublicKeyTrait +pub(crate) trait MLKEMPublicKeyInternalTrait: + MLKEMPublicKeyTrait { /// Not exposing a constructor publicly because you should have to get an instance either by /// running a keygen, or by decoding an existing key. - fn new(t_hat: [u8; T_PACKED_LEN], rho: [u8; 32]) -> Self; + fn new(t_hat: P::TPacked, rho: [u8; 32]) -> Self; } -impl - MLKEMPublicKeyTrait for MLKEMPublicKey +impl MLKEMPublicKeyTrait + for MLKEMPublicKey { fn pk_decode(pk: &[u8; PK_LEN]) -> Result { let pk = Self::new( - pk[..T_PACKED_LEN].try_into().unwrap(), - pk[T_PACKED_LEN..].try_into().unwrap(), + { + let mut t = ::ZEROED; + t.as_mut().copy_from_slice(&pk[..P::T_PACKED_LEN]); + t + }, + pk[P::T_PACKED_LEN..].try_into().unwrap(), ); // check that all entries are in range - for i in 0..k { - let p = unpack_t_hat_row(&pk.t_hat_packed, i); + for i in 0..P::k { + let p = unpack_t_hat_row(pk.t_hat_packed.as_ref(), i); for w in p.coeffs.iter() { if *w >= q { return Err(KEMError::DecodingError("Invalid public key")); @@ -128,7 +104,7 @@ impl Ok(pk) } - fn t_hat_packed(&self) -> &[u8; T_PACKED_LEN] { + fn t_hat_packed(&self) -> &P::TPacked { &self.t_hat_packed } @@ -141,7 +117,7 @@ impl let mut out = [0u8; 32]; let mut h = H::default(); - h.do_update(&self.t_hat_packed); + h.do_update(self.t_hat_packed.as_ref()); h.do_update(&self.rho); let bytes_written = h.do_final_out(&mut out); debug_assert_eq!(bytes_written, 32); @@ -149,24 +125,20 @@ impl } } -impl - MLKEMPublicKeyInternalTrait - for MLKEMPublicKey +impl MLKEMPublicKeyInternalTrait + for MLKEMPublicKey { - fn new(t_hat_packed: [u8; T_PACKED_LEN], rho: [u8; 32]) -> Self { + fn new(t_hat_packed: P::TPacked, rho: [u8; 32]) -> Self { Self { rho, t_hat_packed } } } -impl KEMPublicKey - for MLKEMPublicKey -{ +impl KEMPublicKey for MLKEMPublicKey { /// Algorithm 22 pkEncode(𝜌, 𝐭1) /// Encodes a public key for ML-DSA into a byte string. /// Input:𝜌 ∈ 𝔹32, 𝐭1 ∈ 𝑅𝑘 with coefficients in [0, 2bitlen (𝑞−1)−𝑑 − 1]. /// Output: Public key 𝑝𝑘 ∈ 𝔹32+32𝑘(bitlen (𝑞−1)−𝑑). fn encode(&self) -> [u8; PK_LEN] { - debug_assert_eq!(PK_LEN, 32 + 12 * k * 32); let mut pk = [0u8; PK_LEN]; self.encode_out(&mut pk); @@ -174,13 +146,14 @@ impl KEMPublicKe } fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { - debug_assert_eq!(self.t_hat_packed.len(), T_PACKED_LEN); + // Check length + debug_assert_eq!(self.t_hat_packed.as_ref().len(), P::T_PACKED_LEN); out.fill(0); - out[..T_PACKED_LEN].copy_from_slice(&self.t_hat_packed); - debug_assert_eq!(out[T_PACKED_LEN..].len(), 32); - out[T_PACKED_LEN..].copy_from_slice(&self.rho); + out[..P::T_PACKED_LEN].copy_from_slice(self.t_hat_packed.as_ref()); + debug_assert_eq!(out[P::T_PACKED_LEN..].len(), 32); + out[P::T_PACKED_LEN..].copy_from_slice(&self.rho); PK_LEN } @@ -194,60 +167,36 @@ impl KEMPublicKe } } -impl Eq - for MLKEMPublicKey -{ -} +impl Eq for MLKEMPublicKey {} -impl PartialEq - for MLKEMPublicKey -{ +impl PartialEq for MLKEMPublicKey { fn eq(&self, other: &Self) -> bool { bouncycastle_utils::ct::ct_eq_bytes(&self.encode(), &other.encode()) } } -impl Debug - for MLKEMPublicKey -{ +impl Debug for MLKEMPublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; let hash = SHA3_256::new().hash(&self.encode()); - write!(f, "MLKEMPublicKey {{ alg: {}, pub_key_hash: {:x?} }}", alg, hash) + write!(f, "MLKEMPublicKey {{ alg: {}, pub_key_hash: {:x?} }}", P::ALG_NAME, hash) } } -impl Display - for MLKEMPublicKey -{ +impl Display for MLKEMPublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; let hash = SHA3_256::new().hash(&self.encode()); - write!(f, "MLKEMPublicKey {{ alg: {}, pub_key_hash: {:x?} }}", alg, hash) + write!(f, "MLKEMPublicKey {{ alg: {}, pub_key_hash: {:x?} }}", P::ALG_NAME, hash) } } /// An ML-KEM private key. -#[derive(Clone)] pub struct MLKEMSeedPrivateKey< - const k: usize, - const eta1: i16, - const LAMBDA: i16, + P: MLKEMParams, const SK_LEN: usize, const FULL_SK_LEN: usize, const PK_LEN: usize, - const T_PACKED_LEN: usize, > { + _phantom: core::marker::PhantomData

, rho: [u8; 32], sigma: Secret<[u8; 32]>, pk_hash: Option<[u8; 32]>, @@ -255,15 +204,24 @@ pub struct MLKEMSeedPrivateKey< seed_d: Secret<[u8; 32]>, } -impl< - const k: usize, - const eta1: i16, - const LAMBDA: i16, - const SK_LEN: usize, - const FULL_SK_LEN: usize, - const PK_LEN: usize, - const T_PACKED_LEN: usize, -> MLKEMSeedPrivateKey +/// See the note on [`MLKEMPublicKey`]'s `Clone` for why this is not derived. +impl Clone + for MLKEMSeedPrivateKey +{ + fn clone(&self) -> Self { + Self { + _phantom: core::marker::PhantomData, + rho: self.rho, + sigma: self.sigma.clone(), + pk_hash: self.pk_hash, + z: self.z.clone(), + seed_d: self.seed_d.clone(), + } + } +} + +impl + MLKEMSeedPrivateKey { /// Create a new MLKEMSeedPrivateKey from a 64-byte KeyMaterial. /// Seed SecurityStrength must match algorithm security strength: 128-bit (ML-KEM-512), 192-bit (ML-KEM-768), or 256-bit (ML-KEM-1024). @@ -276,7 +234,7 @@ impl< )); } - if seed.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if seed.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(KEMError::KeyGenError("SecurityStrength")); } @@ -291,7 +249,7 @@ impl< // Deviation from the FIPS: The implementation does not persist the hash of the public key H(ek) in the // in-memory representation because it can be re-computed as needed. - Ok(Self { rho, sigma, pk_hash: None, z, seed_d }) + Ok(Self { _phantom: core::marker::PhantomData, rho, sigma, pk_hash: None, z, seed_d }) } /// Algorithm 13 K-PKE.KeyGen(𝑑) /// 1: (𝜌, 𝜎) ← G(𝑑‖𝑘) @@ -305,7 +263,7 @@ impl< let mut g = G::new(); g.do_update(seed_d); - g.do_update(&[k as u8]); + g.do_update(&[P::k as u8]); let bytes_written = g.do_final_out(buf.as_mut()); debug_assert_eq!(bytes_written, 64); @@ -318,11 +276,10 @@ impl< /// General trait for all ML-KEM private keys types. pub trait MLKEMPrivateKeyTrait< - const k: usize, + P: MLKEMParams, const SK_LEN: usize, const FULL_SK_LEN: usize, const PK_LEN: usize, - const T_PACKED_LEN: usize, >: KEMPrivateKey { /// New from KeyMaterial. Can throw a KEMError if the KeyMaterial does not contain sufficient entropy. @@ -332,7 +289,7 @@ pub trait MLKEMPrivateKeyTrait< fn seed(&self) -> Option>; /// Runs essentially a full keygen according to Algorithm 13. // Dev note: This is a partial implementation of keygen_internal(), and probably not allowed in FIPS mode. - fn pk(&self) -> MLKEMPublicKey; + fn pk(&self) -> MLKEMPublicKey; /// Get a ref to the stored public key hash. /// Since in this implementation, this requires running the full keygen, this is a lazy evaluation and /// will only be computationally heavy the first time it is called for a given key. @@ -362,10 +319,9 @@ pub trait MLKEMPrivateKeyTrait< } pub(crate) trait MLKEMPrivateKeyInternalTrait< - const k: usize, + P: MLKEMParams, const SK_LEN: usize, const PK_LEN: usize, - const T_PACKED_LEN: usize, > { fn z(&self) -> &[u8; 32]; @@ -375,19 +331,12 @@ pub(crate) trait MLKEMPrivateKeyInternalTrait< fn rho(&self) -> &[u8; 32]; /// Note: this one is not a ref because the data does not exist in the private key. - fn t_hat_packed(&self) -> [u8; T_PACKED_LEN]; + fn t_hat_packed(&self) -> P::TPacked; } -impl< - const k: usize, - const eta1: i16, - const LAMBDA: i16, - const SK_LEN: usize, - const FULL_SK_LEN: usize, - const PK_LEN: usize, - const T_PACKED_LEN: usize, -> MLKEMPrivateKeyTrait - for MLKEMSeedPrivateKey +impl + MLKEMPrivateKeyTrait + for MLKEMSeedPrivateKey { fn from_keymaterial(seed: &KeyMaterial<64>) -> Result { Self::new(seed) @@ -398,19 +347,14 @@ impl< tmp[32..].as_mut().copy_from_slice(&*self.z); let mut seed = KeyMaterial::<64>::from_bytes_as_type(&*tmp, KeyType::Seed).unwrap(); do_hazardous_operations(&mut seed, |seed| { - seed.set_security_strength(match k { - 2 => SecurityStrength::_128bit, - 3 => SecurityStrength::_192bit, - 4 => SecurityStrength::_256bit, - _ => unreachable!("Invalid mlkem param set"), - }) + seed.set_security_strength(P::MAX_SECURITY_STRENGTH) }) .unwrap(); Some(seed) } - fn pk(&self) -> MLKEMPublicKey { - MLKEMPublicKey::::new(self.t_hat_packed(), self.rho) + fn pk(&self) -> MLKEMPublicKey { + MLKEMPublicKey::::new(self.t_hat_packed(), self.rho) } fn pk_hash(&mut self) -> &[u8; 32] { if self.pk_hash.is_none() { @@ -447,10 +391,10 @@ impl< /* dk_pke */ // Alg 13; line 20: dkPKE ← ByteEncode12(𝐬) - for i in 0..k { - pack_s_hat_row::(&self.compute_s_hat_row(i), i, out); + for i in 0..P::k { + pack_s_hat_row::

(&self.compute_s_hat_row(i), i, out); } - pos += k * POLY_BYTES; + pos += P::k * POLY_BYTES; /* ek */ // Alg 13; line 19: ekPKE ← ByteEncode12(𝐭)‖𝜌 @@ -468,37 +412,29 @@ impl< FULL_SK_LEN } fn sk_decode(sk: &[u8; SK_LEN]) -> Self { - debug_assert_eq!(SK_LEN, /* seed*/ 64); Self::from_bytes(sk).unwrap() } } -impl< - const k: usize, - const eta1: i16, - const LAMBDA: i16, - const SK_LEN: usize, - const FULL_SK_LEN: usize, - const PK_LEN: usize, - const T_PACKED_LEN: usize, -> MLKEMPrivateKeyInternalTrait - for MLKEMSeedPrivateKey +impl + MLKEMPrivateKeyInternalTrait + for MLKEMSeedPrivateKey { fn z(&self) -> &[u8; 32] { &self.z } fn compute_s_hat_row(&self, idx: usize) -> Polynomial { - debug_assert!(idx < k); + debug_assert!(idx < P::k); // We're doing just one row of this: // 8: for (𝑖 ← 0; 𝑖 < 𝑘; 𝑖++) - // ▷ generate 𝐬 ∈ (ℤ256)^k + // ▷ generate 𝐬 ∈ (ℤ256)^P::k // 9: 𝐬[𝑖] ← SamplePolyCBD𝜂1(PRF𝜂1 (𝜎, 𝑁 )) // ▷ 𝐬[𝑖] ∈ ℤ256 sampled from CBD // 10: 𝑁 ← 𝑁 + 1 // Note: here n = 0 - let mut s_i = sample_poly_CBD::(&self.sigma, idx as u8); + let mut s_i = sample_poly_CBD(&self.sigma, idx as u8, P::eta1); // 16: 𝐬_hat ← NTT(𝐬)̂ s_i.ntt(); @@ -510,47 +446,39 @@ impl< } /// Runs essentially a full keygen according to Algorithm 13 /// Outputs t_hat in the packed encoding specified in FIPS 203 - fn t_hat_packed(&self) -> [u8; T_PACKED_LEN] { - let mut t_hat_packed = [0u8; T_PACKED_LEN]; + fn t_hat_packed(&self) -> P::TPacked { + let mut t_hat_packed = ::ZEROED; - for i in 0..k { + for i in 0..P::k { // first half of // 18: 𝐭_hat ← 𝐀_hat ∘ 𝐬_hat + 𝐞_hat - let mut t_hat_i = compute_A_hat_dot_s_hat::(&self.rho, &self.sigma, i); + let mut t_hat_i = compute_A_hat_dot_s_hat::

(&self.rho, &self.sigma, i); // second half of // 18: 𝐭_hat ← 𝐀_hat ∘ 𝐬_hat + 𝐞_hat { // 12: for (𝑖 ← 0; 𝑖 < 𝑘; 𝑖++) - // ▷ generate 𝐞 ∈ (ℤ256)^k + // ▷ generate 𝐞 ∈ (ℤ256)^P::k // 13: 𝐞[𝑖] ← SamplePolyCBD𝜂1(PRF𝜂1 (𝜎, 𝑁)) // ▷ 𝐞[𝑖] ∈ ℤ256 sampled from CBD // 14: 𝑁 ← 𝑁 + 1 - // Note: here n = k - let mut e_i = sample_poly_CBD::(&self.sigma, (k + i) as u8); + // Note: here n = P::k + let mut e_i = sample_poly_CBD(&self.sigma, (P::k + i) as u8, P::eta1); e_i.ntt(); // technically now e_hat_i t_hat_i.add(&e_i); } t_hat_i.poly_reduce(); - pack_t_hat_row::(&t_hat_i, i, &mut t_hat_packed); + pack_t_hat_row::

(&t_hat_i, i, &mut t_hat_packed); } t_hat_packed } } -impl< - const k: usize, - const eta1: i16, - const LAMBDA: i16, - const SK_LEN: usize, - const FULL_SK_LEN: usize, - const PK_LEN: usize, - const T_PACKED_LEN: usize, -> KEMPrivateKey - for MLKEMSeedPrivateKey +impl + KEMPrivateKey for MLKEMSeedPrivateKey { /// Encode the private key as a 64-byte seed (d || z) fn encode(&self) -> [u8; SK_LEN] { @@ -561,8 +489,6 @@ impl< } fn encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { - debug_assert_eq!(SK_LEN, 64); - out.fill(0); out[..32].copy_from_slice(&*self.seed_d); @@ -585,27 +511,13 @@ impl< } } -impl< - const k: usize, - const eta1: i16, - const LAMBDA: i16, - const SK_LEN: usize, - const FULL_SK_LEN: usize, - const PK_LEN: usize, - const T_PACKED_LEN: usize, -> Eq for MLKEMSeedPrivateKey +impl Eq + for MLKEMSeedPrivateKey { } -impl< - const k: usize, - const eta1: i16, - const LAMBDA: i16, - const SK_LEN: usize, - const FULL_SK_LEN: usize, - const PK_LEN: usize, - const T_PACKED_LEN: usize, -> PartialEq for MLKEMSeedPrivateKey +impl PartialEq + for MLKEMSeedPrivateKey { fn eq(&self, other: &Self) -> bool { let self_encoded = self.encode(); @@ -615,47 +527,21 @@ impl< } /// Debug impl mainly to prevent the secret key from being printed in logs. -impl< - const k: usize, - const eta1: i16, - const LAMBDA: i16, - const SK_LEN: usize, - const FULL_SK_LEN: usize, - const PK_LEN: usize, - const T_PACKED_LEN: usize, -> fmt::Debug for MLKEMSeedPrivateKey +impl fmt::Debug + for MLKEMSeedPrivateKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; let pk_hash = self.pk().compute_hash(); - write!(f, "MLKEMSeedPrivateKey {{ alg: {}, pub_key_hash: {:x?} }}", alg, &pk_hash,) + write!(f, "MLKEMSeedPrivateKey {{ alg: {}, pub_key_hash: {:x?} }}", P::ALG_NAME, &pk_hash,) } } /// Display impl mainly to prevent the secret key from being printed in logs. -impl< - const k: usize, - const eta1: i16, - const LAMBDA: i16, - const SK_LEN: usize, - const FULL_SK_LEN: usize, - const PK_LEN: usize, - const T_PACKED_LEN: usize, -> Display for MLKEMSeedPrivateKey +impl Display + for MLKEMSeedPrivateKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; let pk_hash = self.pk().compute_hash(); - write!(f, "MLKEMSeedPrivateKey {{ alg: {}, pub_key_hash: {:x?} }}", alg, &pk_hash,) + write!(f, "MLKEMSeedPrivateKey {{ alg: {}, pub_key_hash: {:x?} }}", P::ALG_NAME, &pk_hash,) } } diff --git a/crypto/mlkem-lowmemory/src/params.rs b/crypto/mlkem-lowmemory/src/params.rs new file mode 100644 index 00000000..447fb533 --- /dev/null +++ b/crypto/mlkem-lowmemory/src/params.rs @@ -0,0 +1,234 @@ +//! The three ML-KEM parameter sets of FIPS 203, Section 8, as a sealed trait with one type per set. +//! +//! This mirrors `bouncycastle_mlkem::params`, minus the vector and matrix types: this crate never +//! materializes 𝐀̂ or a whole polynomial vector, so the only parameter-sized type it needs is a +//! byte buffer for the packed 𝐭̂. +//! +//! # Derived parameters +//! +//! FIPS 203, Table 2 assigns five values per set (𝑘, 𝜂1, 𝜂2, 𝑑𝑢, 𝑑𝑣); its last column, the +//! required RBG strength, is carried by `MAX_SECURITY_STRENGTH`. +//! The sizes of Table 3 are each a function of those, so they are written once as defaulted +//! associated consts rather than three times as a hand-computed number. `params::tests` checks every +//! derivation against the values tabulated in FIPS 203. + +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_utils::secret::ZeroizablePrimitive; + +/// A fixed-size byte buffer whose length depends on the parameter set. +/// +/// [`ZeroizablePrimitive`] rather than [`Default`] supplies the all-zero value, because `Default` +/// for arrays stops at 32 elements and every buffer here is longer than that. +trait ByteBuffer: ZeroizablePrimitive + AsRef<[u8]> + AsMut<[u8]> {} +impl ByteBuffer for [u8; N] {} + +/// A crate-private (aka "sealed") trait that prevents a new ML-KEM parameter set from being defined +/// outside this crate. +trait MLKEMParamsInternalTrait {} + +/// One ML-KEM parameter set: the values of FIPS 203, Table 2 and Table 3, and the types whose size +/// they determine. +/// +/// Sealed via a private supertrait, so [`MLKEM512Params`], [`MLKEM768Params`] and +/// [`MLKEM1024Params`] are the only implementations. +pub trait MLKEMParams: MLKEMParamsInternalTrait { + /* FIPS 203, Table 2: the values assigned by each parameter set. */ + + /// 𝑘, the rank of the module. + const k: usize; + /// 𝜂1, the CBD parameter used for the secret vector 𝐬 and the keygen error vector 𝐞. + const eta1: i16; + /// 𝜂2, the CBD parameter used for the encaps error terms 𝐞1 and 𝑒2. + /// + /// FIPS 203, Table 2 lists this per parameter set even though all three assign it 2. + const eta2: i16; + /// 𝑑𝑢, the compression parameter for 𝐮. + const du: i16; + /// 𝑑𝑣, the compression parameter for 𝑣. + const dv: i16; + + /* Algorithm meta-data */ + + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + const ALG_NAME: &'static str; + /// The strength claimed for this parameter set, as reported by `Algorithm::MAX_SECURITY_STRENGTH`. + const MAX_SECURITY_STRENGTH: SecurityStrength; + /// The OID in component form, as reported by `AlgorithmOID::OID`. + const OID: &'static [u32]; + /// The DER encoding of [`MLKEMParams::OID`], as reported by `AlgorithmOID::OID_DER`. + const OID_DER: &'static [u8]; + + /* Derived. Never written out per parameter set -- see the module docs. */ + + /// The length of an encapsulation key: FIPS 203, Algorithm 16 (ML-KEM.KeyGen_internal) gives + /// ek ∈ 𝔹^(384𝑘+32). + const PK_LEN: usize = 384 * Self::k + 32; + + /// The length of the FIPS 203 encoding of a decapsulation key: Algorithm 16 gives + /// dk ∈ 𝔹^(768𝑘+96). + /// + /// Named `FULL_SK_LEN` rather than `SK_LEN` because this crate's private keys are held as the + /// 64-byte seed and expanded on demand; see [`MLKEMParams::SK_LEN`]. + const FULL_SK_LEN: usize = 768 * Self::k + 96; + + /// The length of a ciphertext: FIPS 203, Algorithm 17 (ML-KEM.Encaps_internal) gives + /// 𝑐 ∈ 𝔹^(32(𝑑𝑢𝑘+𝑑𝑣)). + const CT_LEN: usize = 32 * (Self::du as usize * Self::k + Self::dv as usize); + + /// The length of a private key as this crate stores it: the 64-byte seed (𝑑, 𝑧), for every + /// parameter set. + const SK_LEN: usize = MLKEM_SEED_LEN; + + /// The length of a shared secret. 32 bytes for every parameter set (FIPS 203, Table 3). + const SS_LEN: usize = MLKEM_SS_LEN; + + /// The packed length of 𝐭̂: 𝑘 polynomials of 12-bit coefficients, i.e. 384𝑘 bytes. This is the + /// encapsulation key without its trailing 32-byte 𝜌. + const T_PACKED_LEN: usize = 12 * Self::k * 32; + + /* Types whose size depends on the parameter set. */ + + /// The packed 𝐭̂, of [`MLKEMParams::T_PACKED_LEN`] bytes. + type TPacked: ByteBuffer; +} + +/// The ML-KEM-512 parameter set (FIPS 203, Table 2). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLKEM512Params; +/// The ML-KEM-768 parameter set (FIPS 203, Table 2). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLKEM768Params; +/// The ML-KEM-1024 parameter set (FIPS 203, Table 2). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLKEM1024Params; + +impl MLKEMParamsInternalTrait for MLKEM512Params {} +impl MLKEMParamsInternalTrait for MLKEM768Params {} +impl MLKEMParamsInternalTrait for MLKEM1024Params {} + +impl MLKEMParams for MLKEM512Params { + const k: usize = 2; + const eta1: i16 = 3; + const eta2: i16 = 2; + const du: i16 = 10; + const dv: i16 = 4; + + const ALG_NAME: &'static str = ML_KEM_512_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + /// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-512 { kems 1 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 1]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x01]; + + type TPacked = [u8; 768]; // 384 * 2 +} + +impl MLKEMParams for MLKEM768Params { + const k: usize = 3; + const eta1: i16 = 2; + const eta2: i16 = 2; + const du: i16 = 10; + const dv: i16 = 4; + + const ALG_NAME: &'static str = ML_KEM_768_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; + /// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-768 { kems 2 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 2]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x02]; + + type TPacked = [u8; 1152]; // 384 * 3 +} + +impl MLKEMParams for MLKEM1024Params { + const k: usize = 4; + const eta1: i16 = 2; + const eta2: i16 = 2; + const du: i16 = 11; + const dv: i16 = 5; + + const ALG_NAME: &'static str = ML_KEM_1024_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; + /// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-1024 { kems 3 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 3]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x03]; + + type TPacked = [u8; 1536]; // 384 * 4 +} + +#[cfg(test)] +mod tests { + use super::*; + + /// FIPS 203, Table 2, transcribed row by row: the five values each parameter set + /// assigns, plus its last column. `(k, eta1, eta2, du, dv, rbg_strength)`. + const TABLE_2: [(usize, i16, i16, i16, i16, i16); 3] = + [(2, 3, 2, 10, 4, 128), (3, 2, 2, 10, 4, 192), (4, 2, 2, 11, 5, 256)]; + + /// FIPS 203, Table 3, transcribed row by row, in bytes: + /// `(encapsulation key, decapsulation key, ciphertext, shared secret key)`. The decapsulation + /// key column is the full FIPS encoding, which this crate calls `FULL_SK_LEN`. + const TABLE_3: [(usize, usize, usize, usize); 3] = + [(800, 1632, 768, 32), (1184, 2400, 1088, 32), (1568, 3168, 1568, 32)]; + + fn check_table_2(i: usize) { + let (k, eta1, eta2, du, dv, rbg_strength) = TABLE_2[i]; + assert_eq!(P::k, k, "{}: 𝑘", P::ALG_NAME); + assert_eq!(P::eta1, eta1, "{}: 𝜂1", P::ALG_NAME); + assert_eq!(P::eta2, eta2, "{}: 𝜂2", P::ALG_NAME); + assert_eq!(P::du, du, "{}: 𝑑𝑢", P::ALG_NAME); + assert_eq!(P::dv, dv, "{}: 𝑑𝑣", P::ALG_NAME); + assert_eq!( + P::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bits(rbg_strength as usize), + "{}: required RBG strength", + P::ALG_NAME + ); + } + + fn check_table_3(i: usize) { + let (pk_len, full_sk_len, ct_len, ss_len) = TABLE_3[i]; + assert_eq!(P::PK_LEN, pk_len, "{}: encapsulation key size", P::ALG_NAME); + assert_eq!(P::FULL_SK_LEN, full_sk_len, "{}: decapsulation key size", P::ALG_NAME); + assert_eq!(P::CT_LEN, ct_len, "{}: ciphertext size", P::ALG_NAME); + assert_eq!(P::SS_LEN, ss_len, "{}: shared secret size", P::ALG_NAME); + // This crate stores the seed, not the expanded key, for every parameter set. + assert_eq!(P::SK_LEN, 64, "{}: stored private key size", P::ALG_NAME); + } + + fn check_associated_type_sizes() { + assert_eq!( + size_of::(), + P::T_PACKED_LEN, + "{}: TPacked vs T_PACKED_LEN", + P::ALG_NAME + ); + // The encapsulation key is the packed 𝐭̂ followed by the 32-byte 𝜌. + assert_eq!(P::T_PACKED_LEN + 32, P::PK_LEN, "{}: 384𝑘 + 32 = PK_LEN", P::ALG_NAME); + } + + #[test] + fn test_parameter_sets_match_fips203_table_2() { + check_table_2::(0); + check_table_2::(1); + check_table_2::(2); + } + + #[test] + fn test_sizes_match_fips203_table_3() { + check_table_3::(0); + check_table_3::(1); + check_table_3::(2); + } + + #[test] + fn test_associated_types_are_the_length_their_consts_claim() { + check_associated_type_sizes::(); + check_associated_type_sizes::(); + check_associated_type_sizes::(); + } +} diff --git a/crypto/mlkem-lowmemory/src/polynomial.rs b/crypto/mlkem-lowmemory/src/polynomial.rs index 20684c02..cc0480f1 100644 --- a/crypto/mlkem-lowmemory/src/polynomial.rs +++ b/crypto/mlkem-lowmemory/src/polynomial.rs @@ -4,6 +4,7 @@ use crate::aux_functions::{ ZETAS, ZETAS_INV, barrett_reduce, montgomery_reduce, mul_mont, ntt_base_mult, }; use crate::mlkem::{N, q}; +use crate::params::MLKEMParams; use core::ops::{Index, IndexMut}; /// A polynomial over the ML-KEM ring. @@ -168,13 +169,13 @@ impl Polynomial { /// This is an optimized version of /// ByteEncode_𝑑𝑣( Compress_𝑑𝑣(𝑣) ) /// which packs a single polynomial according to the packing coefficient dv - pub(crate) fn compress_poly(&self, out: &mut [u8]) { - // make sure to received a dv - debug_assert!(dv == 4 || dv == 5); + pub(crate) fn compress_poly(&self, out: &mut [u8]) { + // make sure to received a P::dv + debug_assert!(P::dv == 4 || P::dv == 5); // make sure the right size output buffer is given - // each of the N i16's will take dv bits - debug_assert_eq!(out.len(), N * (dv as usize) / 8); + // each of the N i16's will take P::dv bits + debug_assert_eq!(out.len(), N * (P::dv as usize) / 8); let mut t = [0u8; 8]; let mut idx = 0; @@ -186,7 +187,7 @@ impl Polynomial { // let mut s = self.clone(); // s.cond_sub_q(); - match dv { + match P::dv { 4 => { // MLKEM512 and MLKEM768 for i in 0..N / 8 { @@ -227,20 +228,20 @@ impl Polynomial { /// This is an optimized version of /// Decompress_𝑑𝑣( ByteDecode_𝑑𝑣(𝑐2) ) /// which unpacks a single polynomial according to the packing coefficient dv - pub(crate) fn decompress_poly(compressed_v: &[u8]) -> Polynomial { - // make sure we have received a dv - debug_assert!(dv == 4 || dv == 5); + pub(crate) fn decompress_poly(compressed_v: &[u8]) -> Polynomial { + // make sure we have received a P::dv + debug_assert!(P::dv == 4 || P::dv == 5); // make sure we were given the right size output buffer - // each of the N i16's will take dv bits - debug_assert_eq!(compressed_v.len(), N * (dv as usize) / 8); + // each of the N i16's will take P::dv bits + debug_assert_eq!(compressed_v.len(), N * (P::dv as usize) / 8); let mut v = Polynomial::new(); let mut idx = 0usize; // if self.m_engine.poly_compressed_bytes() == 128 { - match dv { + match P::dv { 4 => { // MLKEM512 and MLKEM768 for i in 0..N / 2 { diff --git a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs index 858bd200..74cd7c17 100644 --- a/crypto/mlkem-lowmemory/tests/mlkem_tests.rs +++ b/crypto/mlkem-lowmemory/tests/mlkem_tests.rs @@ -722,6 +722,42 @@ mod mlkem_tests { fake_rng.set_security_strength(SecurityStrength::_256bit); _ = MLKEM1024::encaps_rng(&pk1024, &mut fake_rng).unwrap(); } + + #[test] + fn algorithm_names_and_oids() { + use bouncycastle_core::traits::{Algorithm, AlgorithmOID, SecurityStrength}; + + // `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 + // blanket impls, or a typo in a parameter set, is a test failure rather than a silently + // mislabelled algorithm or an unparseable OID. + assert_eq!(MLKEM512::ALG_NAME, "ML-KEM-512"); + assert_eq!(MLKEM768::ALG_NAME, "ML-KEM-768"); + assert_eq!(MLKEM1024::ALG_NAME, "ML-KEM-1024"); + + assert_eq!(MLKEM512::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(MLKEM768::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!(MLKEM1024::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); + + // NIST's Computer Security Objects Register: id-alg-ml-kem-512 { kems 1 }, + // id-alg-ml-kem-768 { kems 2 }, id-alg-ml-kem-1024 { kems 3 }. + assert_eq!(MLKEM512::OID, &[2, 16, 840, 1, 101, 3, 4, 4, 1]); + assert_eq!(MLKEM768::OID, &[2, 16, 840, 1, 101, 3, 4, 4, 2]); + assert_eq!(MLKEM1024::OID, &[2, 16, 840, 1, 101, 3, 4, 4, 3]); + + for (oid, der) in [ + (MLKEM512::OID, MLKEM512::OID_DER), + (MLKEM768::OID, MLKEM768::OID_DER), + (MLKEM1024::OID, MLKEM1024::OID_DER), + ] { + assert_eq!(der[0], 0x06, "DER tag must be OBJECT IDENTIFIER"); + assert_eq!(der[1] as usize, der.len() - 2, "DER length must match the content"); + assert_eq!( + &der[2..], + &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, *oid.last().unwrap() as u8] + ); + } + } } // struct Kat { diff --git a/crypto/mlkem/src/aux_functions.rs b/crypto/mlkem/src/aux_functions.rs index dbd71e0f..3dbb8683 100644 --- a/crypto/mlkem/src/aux_functions.rs +++ b/crypto/mlkem/src/aux_functions.rs @@ -1,19 +1,20 @@ //! Implements auxiliary functions for ML-DSA as defined in Section 7 of FIPS 204. -use crate::matrix::{Matrix, Vector}; +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_sha3::{SHAKE128, SHAKE256}; -pub(crate) fn expandA(rho: &[u8; 32]) -> Matrix { - let mut A_hat = Matrix::::new(); - for i in 0..k { +pub(crate) fn expandA(rho: &[u8; 32]) -> P::MatrixA { + let mut A_hat = P::MatrixA::new(); + for i in 0..P::k { // 5: for (𝑗 ← 0; 𝑗 < 𝑘; 𝑗++) - for j in 0..k { + for j in 0..P::k { // 6: 𝐀[𝑖, 𝑗] ← SampleNTT(𝜌‖𝑗‖𝑖) // ▷ 𝑗 and 𝑖 are bytes 33 and 34 of the input - A_hat.elems[i][j] = sample_ntt(rho, &[j as u8, i as u8]); + A_hat.set_elem(i, j, sample_ntt(rho, &[j as u8, i as u8])); } } @@ -24,7 +25,6 @@ pub(crate) fn expandA(rho: &[u8; 32]) -> Matrix { /// Encodes an array of 𝑑-bit integers into a byte array for 1 ≤ 𝑑 ≤ 12. /// Input: integer array 𝐹 ∈ ℤ_M^256, where 𝑚 = 2^𝑑 if 𝑑 < 12, and 𝑚 = 𝑞 if 𝑑 = 12. /// Output: byte array 𝐵 ∈ 𝔹32𝑑. -/// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code. pub fn byte_encode(F: &Polynomial) -> [u8; PACK_LEN] { debug_assert_eq!(PACK_LEN, 32 * d); @@ -66,7 +66,6 @@ pub fn byte_encode(F: &Polynomial) -> [u8 /// Decodes a byte array into an array of 𝑑-bit integers for 1 ≤ 𝑑 ≤ 12. /// Input: byte array 𝐵 ∈ 𝔹32𝑑 . /// Output: integer array 𝐹 ∈ ℤ256 , where 𝑚 = 2𝑑 if 𝑑 < 12 and 𝑚 = 𝑞 if 𝑑 = 12. -/// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code. pub fn byte_decode(B: &[u8; PACK_LEN]) -> Polynomial { debug_assert_eq!(PACK_LEN, 32 * d); @@ -87,7 +86,6 @@ pub fn byte_decode(B: &[u8; PACK_LEN]) -> /// Takes a 32-byte seed and two indices as input and outputs a pseudorandom element of 𝑇𝑞. /// Input: byte array 𝐵 ∈ 𝔹34 . ▷ a 32-byte seed along with two indices /// Output: array 𝑎_hat ∈ ℤ256 ▷ the coefficients of the NTT of a polynomial -/// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code. pub fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { let mut a_hat = Polynomial::new(); @@ -157,8 +155,7 @@ pub fn sample_ntt(rho: &[u8; 32], nonce: &[u8; 2]) -> Polynomial { /// Takes a seed as input and outputs a pseudorandom sample from the distribution D𝜂(𝑅𝑞). /// Input: byte array 𝐵 ∈ 𝔹64𝜂 . /// Output: array 𝑓 ∈ ℤ256 ▷ the coefficients of the sampled polynomial -/// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code. -pub fn sample_poly_cbd(bytes: &[u8]) -> Polynomial { +pub(crate) fn sample_poly_cbd(bytes: &[u8], eta: i16) -> Polynomial { debug_assert_eq!(bytes.len(), 64 * eta as usize); let mut f = Polynomial::new(); @@ -205,7 +202,7 @@ pub fn sample_poly_cbd(bytes: &[u8]) -> Polynomial { /// SamplePolyCBD𝜂1(PRF𝜂1 (𝜎, 𝑁 )) /// Performs both the PRF and SamplePolyCBD steps -pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8) -> Polynomial { +pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8, eta: i16) -> Polynomial { // Alg 13: 9: 𝐬[𝑖] ← SamplePolyCBD𝜂1(PRF𝜂1 (𝜎, 𝑁 )) // ▷ 𝐬[𝑖] ∈ ℤ256 sampled from CBD match eta { @@ -220,7 +217,7 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8) -> Polynomial buf }; - sample_poly_cbd::(&buf) + sample_poly_cbd(&buf, eta) } 3 => { let buf = { @@ -232,21 +229,18 @@ pub(crate) fn sample_poly_CBD(b: &[u8; 32], n: u8) -> Polynomial buf }; - sample_poly_cbd::(&buf) + sample_poly_cbd(&buf, eta) } _ => unreachable!(), } } /// Internal helper for keygen since both s_hat and e_hat have identical sampling code -pub(crate) fn sample_vector_CBD( - b: &[u8; 32], - mut n: u8, -) -> Vector { - let mut v = Vector::::new(); +pub(crate) fn sample_vector_CBD(b: &[u8; 32], mut n: u8, eta: i16) -> P::VecK { + let mut v = P::VecK::new(); - for i in 0..k { - v[i] = sample_poly_CBD::(b, n); + for i in 0..P::k { + v[i] = sample_poly_CBD(b, n, eta); // Alg 13: 10: 𝑁 ← 𝑁 + 1 n += 1; @@ -333,48 +327,36 @@ pub(crate) fn ntt_base_mult( r[off + 1] = out_val1; } -pub(crate) fn pack_ciphertext( - u: &Vector, +pub(crate) fn pack_ciphertext( + u: &P::VecK, v: &Polynomial, ) -> [u8; CT_LEN] { let mut out = [0u8; CT_LEN]; // each of the N i16's will take du bits, so a polynomial takes N * du bits, then we have k of them - let lim: usize = k * (N * (du as usize) / 8); + let lim: usize = P::k * (N * (P::du as usize) / 8); - u.compress_pol_vec::(&mut out[..lim]); - v.compress_poly::(&mut out[lim..]); + u.compress_pol_vec::

(&mut out[..lim]); + v.compress_poly::

(&mut out[lim..]); out } -pub(crate) fn unpack_ciphertext_u< - const k: usize, - const CT_LEN: usize, - const du: i16, - const dv: i16, ->( +pub(crate) fn unpack_ciphertext_u( c: &[u8; CT_LEN], -) -> Vector { +) -> P::VecK { // each of the N i16's will take du bits, so a polynomial takes N * du bits, then we have k of them - let lim: usize = k * (N * (du as usize) / 8); + let lim: usize = P::k * (N * (P::du as usize) / 8); - let u = Vector::::decompress_pol_vec::(&c[..lim]); - - u + P::VecK::decompress_pol_vec::

(&c[..lim]) } -pub(crate) fn unpack_ciphertext_v< - const k: usize, - const CT_LEN: usize, - const du: i16, - const dv: i16, ->( +pub(crate) fn unpack_ciphertext_v( c: &[u8; CT_LEN], ) -> Polynomial { // each of the N i16's will take du bits, so a polynomial takes N * du bits, then we have k of them - let lim: usize = k * (N * (du as usize) / 8); + let lim: usize = P::k * (N * (P::du as usize) / 8); - let v = Polynomial::decompress_poly::(&c[lim..]); + let v = Polynomial::decompress_poly::

(&c[lim..]); v } diff --git a/crypto/mlkem/src/lib.rs b/crypto/mlkem/src/lib.rs index cfd91c3f..5c48828b 100644 --- a/crypto/mlkem/src/lib.rs +++ b/crypto/mlkem/src/lib.rs @@ -157,6 +157,7 @@ mod aux_functions; mod matrix; pub mod mlkem; mod mlkem_keys; +mod params; mod polynomial; /*** Exported types ***/ @@ -181,9 +182,6 @@ pub use mlkem::ML_KEM_768_NAME; pub use mlkem::ML_KEM_1024_NAME; pub use mlkem::{MLKEM_RND_LEN, MLKEM_SEED_LEN, MLKEM_SS_LEN}; - pub use mlkem::{MLKEM512_CT_LEN, MLKEM512_PK_LEN, MLKEM512_SK_LEN}; pub use mlkem::{MLKEM768_CT_LEN, MLKEM768_PK_LEN, MLKEM768_SK_LEN}; pub use mlkem::{MLKEM1024_CT_LEN, MLKEM1024_PK_LEN, MLKEM1024_SK_LEN}; - -pub use matrix::Matrix; diff --git a/crypto/mlkem/src/matrix.rs b/crypto/mlkem/src/matrix.rs index 93356585..2b04eb23 100644 --- a/crypto/mlkem/src/matrix.rs +++ b/crypto/mlkem/src/matrix.rs @@ -4,10 +4,59 @@ use core::ops::{Index, IndexMut}; use crate::mlkem::{N, q}; +use crate::params::MLKEMParams; use crate::polynomial; use crate::polynomial::Polynomial; use bouncycastle_utils::secret::ZeroizablePrimitive; +/// The operations this crate performs on a vector of polynomials, i.e. on an element of 𝑅^LEN. +/// +/// [`Vector`] is the only implementation; the trait exists so that code generic over a parameter +/// set can operate on `MLKEMParams::VecK` without knowing its length. +pub trait VectorTrait: + Sized + Copy + ZeroizablePrimitive + Index + IndexMut +{ + /// A vector with every coefficient set to zero. + fn new() -> Self; + + /// The coordinates, for iteration and chunking. + fn elems(&self) -> &[Polynomial]; + /// The coordinates, for iteration and chunking. + fn elems_mut(&mut self) -> &mut [Polynomial]; + + /// Adds another vector to this one, coordinatewise, in the NTT domain. + fn add_vector_ntt(&mut self, s: &Self); + /// The dot product of two vectors in the NTT domain. + fn dot_product(&self, v: &Self) -> Polynomial; + /// Barrett-reduces every coefficient. + fn reduce(&mut self); + /// Applies the NTT to every coordinate. + fn ntt(&mut self); + /// Applies the inverse NTT to every coordinate. + fn inv_ntt(&mut self); + /// Converts every coefficient into the Montgomery domain. + fn convert_to_mont(&mut self); + /// FIPS 203, Algorithm 5 (ByteEncode) applied to the compressed vector. + fn compress_pol_vec(&self, out: &mut [u8]); + /// The inverse of [`VectorTrait::compress_pol_vec`]. + fn decompress_pol_vec(compressed_u: &[u8]) -> Self; +} + +/// The operations this crate performs on the public matrix 𝐀̂. +/// +/// [`Matrix`] is the only implementation; see [`VectorTrait`] for why the trait exists. +pub trait MatrixTrait: Sized + Clone { + /// The vector this matrix maps between: an element of 𝑅^𝑘. + type Vec: VectorTrait; + + /// A matrix with every coefficient set to zero. + fn new() -> Self; + /// Overwrites the polynomial at `elems[row][col]`. + fn set_elem(&mut self, row: usize, col: usize, p: Polynomial); + /// Computes 𝐀̂ ∘ 𝐯̂, transposing 𝐀̂ first when `transpose` is set. + fn matrix_vector_ntt(&self, v: &Self::Vec) -> Self::Vec; +} + #[derive(Clone)] /// A matrix over the ML-KEM ring. pub struct Matrix { @@ -64,8 +113,30 @@ impl Matrix { } } +/// ML-KEM's 𝐀̂ is always 𝑘 × 𝑘, so the trait is implemented only for the square case; that is what +/// lets [`MatrixTrait::Vec`] be one vector type rather than an input and an output type. +impl MatrixTrait for Matrix { + type Vec = Vector; + + fn new() -> Self { + Matrix::new() + } + + fn set_elem(&mut self, row: usize, col: usize, p: Polynomial) { + self.elems[row][col] = p; + } + + fn matrix_vector_ntt(&self, v: &Vector) -> Vector { + Matrix::matrix_vector_ntt::(self, v) + } +} + #[derive(Clone, Copy)] -pub(crate) struct Vector { +/// A vector of `k` polynomials, i.e. an element of 𝑅^𝑘. +/// +/// Public only because it is the value of `MLKEMParams::VecK`; its fields and operations are +/// crate-private, so from outside it is an opaque handle. Reach it through [`VectorTrait`]. +pub struct Vector { pub(crate) elems: [Polynomial; k], } @@ -92,20 +163,28 @@ impl Vector { pub(crate) const fn new() -> Self { Self { elems: [Polynomial::new(); k] } } +} - /// Algorithm 46 AddVectorNTT(𝐯, 𝐰)̂ - /// Computes the sum 𝐯_hat + 𝐰_hat of two vectors 𝐯_hat, 𝐰_hat over 𝑇𝑞. - /// Input: ℓ ∈ ℕ, v_hat ∈ T^ℓ, w_hat ∈ 𝑇^ℓ - /// Output: u_hat ∈ T^ℓ_𝑞. - /// Add another vector to this vector - pub(crate) fn add_vector_ntt(&mut self, s: &Self) { +impl VectorTrait for Vector { + fn new() -> Self { + Vector::new() + } + + fn elems(&self) -> &[Polynomial] { + &self.elems + } + + fn elems_mut(&mut self) -> &mut [Polynomial] { + &mut self.elems + } + fn add_vector_ntt(&mut self, s: &Self) { for i in 0..k { // perform Montgomery addition of each polynomial in the vector self[i].add(&s[i]); } } - pub(crate) fn dot_product(&self, v: &Self) -> Polynomial { + fn dot_product(&self, v: &Self) -> Polynomial { // split out the 0 case to skip a no-op add_ntt() let mut w = polynomial::base_mult_montgomery(&self[0], &v[0]); @@ -120,25 +199,25 @@ impl Vector { w } - pub(crate) fn reduce(&mut self) { + fn reduce(&mut self) { for i in 0..k { self[i].poly_reduce(); } } - pub(crate) fn ntt(&mut self) { + fn ntt(&mut self) { for i in 0..k { self[i].ntt(); } } - pub(crate) fn inv_ntt(&mut self) { + fn inv_ntt(&mut self) { for i in 0..k { self[i].inv_ntt(); } } - pub(crate) fn convert_to_mont(&mut self) { + fn convert_to_mont(&mut self) { for i in 0..k { self[i].convert_to_mont(); } @@ -147,13 +226,13 @@ impl Vector { /// This is an optimized version of /// ByteEncode_𝑑𝑢( Compress_𝑑𝑢(𝐮) ) /// which packs a polynomial vector according to the packing coefficient dv - pub(crate) fn compress_pol_vec(&self, out: &mut [u8]) { + fn compress_pol_vec(&self, out: &mut [u8]) { // make sure we have received a dv - assert!(du == 10 || du == 11); + assert!(P::du == 10 || P::du == 11); // make sure we were given the right size output buffer // each of the N i16's will take dv bits - debug_assert_eq!(out.len(), k * (N * (du as usize) / 8)); + debug_assert_eq!(out.len(), k * (N * (P::du as usize) / 8)); // No conditional_sub_q needed (as done in bc-java): callers must reduce() first, // so coefficients are in [0, q) (barrett_reduce, floor variant). The Compress mask `& (2^du - 1)` folds @@ -165,7 +244,7 @@ impl Vector { // s.conditional_sub_q(); let mut idx = 0; - match du { + match P::du { 10 => { // MLKEM512 and MLKEM 768 let mut t = [0i16; 4]; @@ -218,19 +297,19 @@ impl Vector { } } - pub(crate) fn decompress_pol_vec(compressed_u: &[u8]) -> Vector { + fn decompress_pol_vec(compressed_u: &[u8]) -> Self { let mut u = Vector::::new(); // make sure we have received a dv - assert!(du == 10 || du == 11); + assert!(P::du == 10 || P::du == 11); // make sure we were given the right size output buffer // each of the N i16's will take dv bits - debug_assert_eq!(compressed_u.len(), k * (N * (du as usize) / 8)); + debug_assert_eq!(compressed_u.len(), k * (N * (P::du as usize) / 8)); let mut idx = 0; - match du { + match P::du { 10 => { // MLKEM512 and MLKEM768 let mut t = [0i16; 4]; diff --git a/crypto/mlkem/src/mlkem.rs b/crypto/mlkem/src/mlkem.rs index b0979b69..6490a521 100644 --- a/crypto/mlkem/src/mlkem.rs +++ b/crypto/mlkem/src/mlkem.rs @@ -90,6 +90,7 @@ //! private key encoding (which is often called the "semi-expanded format" since the in-memory representation //! is still larger). //! Contact us if you need such a thing implemented. +//! //! ## Deterministic encapsulation //! //! This section pertains to [`MLKEM::encaps_internal`] which allows to pass in the encapsulation randomness @@ -132,7 +133,7 @@ use crate::aux_functions::{ expandA, pack_ciphertext, sample_poly_CBD, sample_vector_CBD, unpack_ciphertext_u, unpack_ciphertext_v, }; -use crate::matrix::{Matrix, Vector}; +use crate::matrix::{MatrixTrait, VectorTrait}; use crate::mlkem_keys::{ MLKEM512PrivateKey, MLKEM512PublicKey, MLKEM768PrivateKey, MLKEM768PublicKey, MLKEM1024PrivateKey, MLKEM1024PublicKey, @@ -141,6 +142,7 @@ use crate::mlkem_keys::{ MLKEMPrivateKeyExpanded, MLKEMPublicKeyInternalTrait, MLKEMPublicKeyTrait, }; use crate::mlkem_keys::{MLKEMPrivateKeyInternalTrait, MLKEMPrivateKeyTrait}; +use crate::params::{MLKEM512Params, MLKEM768Params, MLKEM1024Params, MLKEMParams}; use crate::polynomial::Polynomial; use bouncycastle_core::errors::KEMError; use bouncycastle_core::errors::RNGError; @@ -175,53 +177,34 @@ pub const MLKEM_SS_LEN: usize = 32; pub(crate) const N: usize = 256; pub(crate) const q: i16 = 3329; pub(crate) const q_inv: i32 = 62209; -pub(crate) const ETA2: i16 = 2; pub(crate) const POLY_BYTES: usize = 384; -/* ML-KEM-512 params */ - -/// Length of the \[u8] holding a ML-KEM-512 public key. -pub const MLKEM512_PK_LEN: usize = 800; -/// Length of the \[u8] holding a ML-KEM-512 private key. -pub const MLKEM512_SK_LEN: usize = 1632; -/// Length of the \[u8] holding a ML-KEM-512 ciphertext. -pub const MLKEM512_CT_LEN: usize = 768; -pub(crate) const MLKEM512_k: usize = 2; -pub(crate) const MLKEM512_ETA1: i16 = 3; -pub(crate) const MLKEM512_DU: i16 = 10; -pub(crate) const MLKEM512_DV: i16 = 4; -/// Maps to "required RBG strength (bits)" in FIPS 203 Table 2 -pub(crate) const MLKEM512_LAMBDA: i16 = 128; - -/* ML-KEM-768 params */ - -/// Length of the \[u8] holding a ML-KEM-768 public key. -pub const MLKEM768_PK_LEN: usize = 1184; -/// Length of the \[u8] holding a ML-KEM-768 private key. -pub const MLKEM768_SK_LEN: usize = 2400; -/// Length of the \[u8] holding a ML-KEM-768 ciphertext. -pub const MLKEM768_CT_LEN: usize = 1088; -pub(crate) const MLKEM768_k: usize = 3; -pub(crate) const MLKEM768_ETA1: i16 = 2; -pub(crate) const MLKEM768_DU: i16 = 10; -pub(crate) const MLKEM768_DV: i16 = 4; -/// Maps to "required RBG strength (bits)" in FIPS 203 Table 2 -pub(crate) const MLKEM768_LAMBDA: i16 = 192; - -/* ML-KEM-1024 params */ - -/// Length of the \[u8] holding a ML-KEM-1024 public key. -pub const MLKEM1024_PK_LEN: usize = 1568; -/// Length of the \[u8] holding a ML-KEM-1024 private key. -pub const MLKEM1024_SK_LEN: usize = 3168; -/// Length of the \[u8] holding a ML-KEM-1024 ciphertext. -pub const MLKEM1024_CT_LEN: usize = 1568; -pub(crate) const MLKEM1024_k: usize = 4; -pub(crate) const MLKEM1024_ETA1: i16 = 2; -pub(crate) const MLKEM1024_DU: i16 = 11; -pub(crate) const MLKEM1024_DV: i16 = 5; -/// Maps to "required RBG strength (bits)" in FIPS 203 Table 2 -pub(crate) const MLKEM1024_LAMBDA: i16 = 256; +/* ML-KEM-512 sizes (FIPS 203, Table 3) */ + +/// Length of the \[u8] holding an ML-KEM-512 public key. +pub const MLKEM512_PK_LEN: usize = MLKEM512Params::PK_LEN; +/// Length of the \[u8] holding an ML-KEM-512 private key. +pub const MLKEM512_SK_LEN: usize = MLKEM512Params::SK_LEN; +/// Length of the \[u8] holding an ML-KEM-512 ciphertext. +pub const MLKEM512_CT_LEN: usize = MLKEM512Params::CT_LEN; + +/* ML-KEM-768 sizes (FIPS 203, Table 3) */ + +/// Length of the \[u8] holding an ML-KEM-768 public key. +pub const MLKEM768_PK_LEN: usize = MLKEM768Params::PK_LEN; +/// Length of the \[u8] holding an ML-KEM-768 private key. +pub const MLKEM768_SK_LEN: usize = MLKEM768Params::SK_LEN; +/// Length of the \[u8] holding an ML-KEM-768 ciphertext. +pub const MLKEM768_CT_LEN: usize = MLKEM768Params::CT_LEN; + +/* ML-KEM-1024 sizes (FIPS 203, Table 3) */ + +/// Length of the \[u8] holding an ML-KEM-1024 public key. +pub const MLKEM1024_PK_LEN: usize = MLKEM1024Params::PK_LEN; +/// Length of the \[u8] holding an ML-KEM-1024 private key. +pub const MLKEM1024_SK_LEN: usize = MLKEM1024Params::SK_LEN; +/// Length of the \[u8] holding an ML-KEM-1024 ciphertext. +pub const MLKEM1024_CT_LEN: usize = MLKEM1024Params::CT_LEN; // Typedefs just to make the algorithms look more like the FIPS 204 sample code. pub(crate) type G = SHA3_512; @@ -232,116 +215,96 @@ pub(crate) type J = SHAKE256; /// The ML-KEM-512 algorithm. pub type MLKEM512 = MLKEM< + MLKEM512Params, + MLKEM512PublicKey, + MLKEM512PrivateKey, MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM512_CT_LEN, MLKEM_SS_LEN, - MLKEM512PublicKey, - MLKEM512PrivateKey, - MLKEM512_k, - MLKEM512_ETA1, - MLKEM512_DU, - MLKEM512_DV, - MLKEM512_LAMBDA, >; -impl Algorithm for MLKEM512 { - const ALG_NAME: &'static str = ML_KEM_512_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; -} -/// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-512 { kems 1 } -impl AlgorithmOID for MLKEM512 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 1]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x01]; -} - /// The ML-KEM-768 algorithm. pub type MLKEM768 = MLKEM< + MLKEM768Params, + MLKEM768PublicKey, + MLKEM768PrivateKey, MLKEM768_PK_LEN, MLKEM768_SK_LEN, MLKEM768_CT_LEN, MLKEM_SS_LEN, - MLKEM768PublicKey, - MLKEM768PrivateKey, - MLKEM768_k, - MLKEM768_ETA1, - MLKEM768_DU, - MLKEM768_DV, - MLKEM768_LAMBDA, >; -impl Algorithm for MLKEM768 { - const ALG_NAME: &'static str = ML_KEM_768_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; -} -/// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-768 { kems 2 } -impl AlgorithmOID for MLKEM768 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 2]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x02]; -} - /// The ML-KEM-1024 algorithm. pub type MLKEM1024 = MLKEM< + MLKEM1024Params, + MLKEM1024PublicKey, + MLKEM1024PrivateKey, MLKEM1024_PK_LEN, MLKEM1024_SK_LEN, MLKEM1024_CT_LEN, MLKEM_SS_LEN, - MLKEM1024PublicKey, - MLKEM1024PrivateKey, - MLKEM1024_k, - MLKEM1024_ETA1, - MLKEM1024_DU, - MLKEM1024_DV, - MLKEM1024_LAMBDA, >; -impl Algorithm for MLKEM1024 { - const ALG_NAME: &'static str = ML_KEM_1024_NAME; - const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; +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, +> Algorithm for MLKEM +{ + const ALG_NAME: &'static str = P::ALG_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = P::MAX_SECURITY_STRENGTH; } -/// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-1024 { kems 3 } -impl AlgorithmOID for MLKEM1024 { - const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 3]; - const OID_DER: &'static [u8] = - &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x03]; + +/// The OIDs NIST assigned in the Computer Security Objects Register: id-alg-ml-kem-512 +/// { kems 1 }, id-alg-ml-kem-768 { kems 2 } and id-alg-ml-kem-1024 { kems 3 }. As with +/// [`Algorithm`], the values belong to the parameter set, so one impl covers all three. +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, +> AlgorithmOID for MLKEM +{ + const OID: &'static [u32] = P::OID; + const OID_DER: &'static [u8] = P::OID_DER; } /// The core internal implementation of the ML-KEM algorithm. /// This needs to be public for the compiler to be able to find it, but you shouldn't ever /// need to use this directly. Please use the named public types. pub struct MLKEM< + P: MLKEMParams, + PK: MLKEMPublicKeyTrait + MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const CT_LEN: usize, const SS_LEN: usize, - PK: MLKEMPublicKeyTrait + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, > { - _phantom: PhantomData<(PK, SK)>, + _phantom: PhantomData<(P, PK, SK)>, } 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, - PK: MLKEMPublicKeyTrait + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta1: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, -> MLKEM +> MLKEM { /// Algorithm 16 ML-KEM.KeyGen_internal(𝑑, 𝑧) /// Uses randomness to generate an encapsulation key and a corresponding decapsulation key. @@ -358,7 +321,7 @@ impl< )); } - if seed.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if seed.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(KEMError::KeyGenError( "Seed SecurityStrength must match algorithm security strength", )); @@ -385,7 +348,7 @@ impl< /// Input: randomness 𝑑 ∈ 𝔹32 . /// Output: encryption key ek_PKE ∈ 𝔹384𝑘+32. /// Output: decryption key dk_PKE ∈ 𝔹384𝑘. - fn pke_keygen(d: &[u8; 32]) -> (PK, Secret>) { + fn pke_keygen(d: &[u8; 32]) -> (PK, Secret) { // 1: (𝜌, 𝜎) ← G(𝑑‖𝑘) // ▷ expand 32+1 bytes to two pseudorandom 32-byte seeds1 // rho: public seed @@ -393,7 +356,7 @@ impl< let (rho, mut sigma) = { let mut g = G::new(); g.do_update(d); - g.do_update(&[k as u8]); + g.do_update(&[P::k as u8]); let mut buf = [0u8; 64]; let bytes_written = g.do_final_out(&mut buf); debug_assert_eq!(bytes_written, 64); @@ -412,9 +375,9 @@ impl< // ▷ 𝐬[𝑖] ∈ ℤ256 sampled from CBD // 10: 𝑁 ← 𝑁 + 1 // Note: here n = 0 - let s_hat: Secret> = { - let mut s: Secret> = Secret::new(); - *s = sample_vector_CBD::(&sigma, 0); + let s_hat: Secret = { + let mut s: Secret = Secret::new(); + *s = sample_vector_CBD::

(&sigma, 0, P::eta1); // 16: 𝐬_hat ← NTT(𝐬)̂ s.ntt(); @@ -427,7 +390,7 @@ impl< let mut t_hat = { // 3: for (𝑖 ← 0; 𝑖 < 𝑘; 𝑖++) // ▷ generate matrix A_hat ∈ (ℤ256)^k x k - let A_hat = expandA(&rho); + let A_hat = expandA::

(&rho); A_hat.matrix_vector_ntt::(&s_hat) }; @@ -441,7 +404,7 @@ impl< // ▷ 𝐞[𝑖] ∈ ℤ256 sampled from CBD // 14: 𝑁 ← 𝑁 + 1 // Note: here n = k - let mut e = sample_vector_CBD::(&sigma, k as u8); + let mut e = sample_vector_CBD::

(&sigma, P::k as u8, P::eta1); e.ntt(); // technically now e_hat e.reduce(); @@ -464,7 +427,7 @@ impl< /// Input: message 𝑚 ∈ 𝔹32 . /// Input: randomness 𝑟 ∈ 𝔹32 . /// Output: ciphertext 𝑐 ∈ 𝔹32(𝑑𝑢𝑘+𝑑𝑣). - fn pke_encrypt(ek: &PK, A_hat: &Matrix, m: [u8; 32], r: &[u8; 32]) -> [u8; CT_LEN] { + fn pke_encrypt(ek: &PK, A_hat: &P::MatrixA, m: [u8; 32], r: &[u8; 32]) -> [u8; CT_LEN] { // 1: 𝑁 ← 0 // since the number of loops here is static, the N values can be hard-coded rather than using a counter @@ -484,7 +447,7 @@ impl< // 11: 𝑁 ← 𝑁 + 1 // Note: here n = 0 let y_hat = { - let mut y = sample_vector_CBD::(&r, 0); + let mut y = sample_vector_CBD::

(&r, 0, P::eta1); // 18: 𝐲_hat ← NTT(𝐲) y.ntt(); @@ -502,7 +465,7 @@ impl< // ▷ 𝐞[𝑖] ∈ ℤ256 sampled from CBD𝑞 // 14: 𝑁 ← 𝑁 + 1 // note: here n = k - let e1 = sample_vector_CBD::(&r, k as u8); + let e1 = sample_vector_CBD::

(&r, P::k as u8, P::eta2); u.add_vector_ntt(&e1); } @@ -517,7 +480,7 @@ impl< // 17: 𝑒2 ← SamplePolyCBD𝜂2(PRF𝜂2 (𝑟, 𝑁)) // ▷ sample 𝑒2 ∈ ℤ256 from CBD // note: here n = 2k - let e2 = sample_poly_CBD::(&r, 2 * k as u8); + let e2 = sample_poly_CBD(&r, 2 * P::k as u8, P::eta2); v.add(&e2); let mu = Polynomial::from_msg(m); @@ -525,7 +488,7 @@ impl< v.poly_reduce(); - pack_ciphertext::(&u, &v) + pack_ciphertext::(&u, &v) } /// Algorithm 17 ML-KEM.Encaps_internal(ek, 𝑚) @@ -562,11 +525,9 @@ impl< /// Please don't do it. pub fn encaps_internal( ek: &PK, - A_hat: Option<&Matrix>, + A_hat: Option<&P::MatrixA>, m: [u8; 32], ) -> ([u8; 32], [u8; CT_LEN]) { - debug_assert_eq!(CT_LEN, 32 * ((du as usize) * k + (dv as usize))); - // 1: (𝐾, 𝑟) ← G(𝑚‖H(ek)) // ▷ derive shared secret key 𝐾 and randomness 𝑟 let K: [u8; MLKEM_SS_LEN]; @@ -606,7 +567,7 @@ impl< // 3: 𝐮′ ← Decompress_𝑑𝑢(ByteDecode_𝑑𝑢(𝑐1)) // 4: 𝑣′ ← Decompress_𝑑𝑣(ByteDecode_𝑑𝑣(𝑐2)) let v1 = { - let mut u_prime = unpack_ciphertext_u::(&ct); + let mut u_prime = unpack_ciphertext_u::(&ct); // 5: 𝐬_hat ← ByteDecode12(dkPKE) // Unnecessary here because dk is already decoded @@ -620,7 +581,7 @@ impl< }; let w = { - let mut v_prime = unpack_ciphertext_v::(&ct); + let mut v_prime = unpack_ciphertext_v::(&ct); v_prime.sub(&v1); v_prime.poly_reduce(); @@ -637,11 +598,7 @@ impl< /// Input: decapsulation key dk ∈ 𝔹768𝑘+96 . /// Input: ciphertext 𝑐 ∈ 𝔹32(𝑑𝑢𝑘+𝑑𝑣). /// Output: shared secret key 𝐾 ∈ 𝔹32 . - fn decaps_internal( - dk: &SK, - A_hat: Option<&Matrix>, - c: [u8; CT_LEN], - ) -> [u8; MLKEM_SS_LEN] { + fn decaps_internal(dk: &SK, A_hat: Option<&P::MatrixA>, c: [u8; CT_LEN]) -> [u8; MLKEM_SS_LEN] { // Structured to mirror the FIPS as closely as possible, with unnamed scopes // used to limit the number of live stack variables at any given time. @@ -720,20 +677,16 @@ impl< } 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, - PK: MLKEMPublicKeyTrait + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta1: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, -> MLKEMTrait - for MLKEM +> MLKEMTrait + for MLKEM { /// Imports a secret key from a seed. fn keygen_from_seed(seed: &KeyMaterial<64>) -> Result<(PK, SK), KEMError> { @@ -778,18 +731,18 @@ impl< } fn encaps_for_expanded_key( - pk: &MLKEMPublicKeyExpanded, + pk: &MLKEMPublicKeyExpanded, ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { let mut os_rng = HashDRBG_SHA512::new_from_os(); Self::encaps_for_expanded_key_rng(pk, &mut os_rng) } fn encaps_for_expanded_key_rng( - pk: &MLKEMPublicKeyExpanded, + pk: &MLKEMPublicKeyExpanded, rng: &mut dyn RNG, ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { // Source the random message m from the provided RNG - if rng.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if rng.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } let mut m = [0u8; 32]; @@ -799,20 +752,19 @@ impl< let mut key = KeyMaterial::::from_bytes_as_type(&ss, KeyType::CryptographicRandom)?; do_hazardous_operations(&mut key, |key| { - key.set_security_strength(SecurityStrength::from_bits(LAMBDA as usize)) + key.set_security_strength(P::MAX_SECURITY_STRENGTH) })?; Ok((key, ct)) } fn decaps_with_expanded_key( - sk: &MLKEMPrivateKeyExpanded, + sk: &MLKEMPrivateKeyExpanded, ct: &[u8], ) -> Result, KEMError> { /* decapsulation inputs checks described on FIPS 203 section 7.3 */ // 1. (Ciphertext type check) If 𝑐 is not a byte array of length 32(𝑑𝑢 𝑘 + 𝑑𝑣) for the values of 𝑑𝑢, // 𝑑𝑣, and 𝑘 specified by the relevant parameter set, then input checking has failed. - debug_assert_eq!(CT_LEN, 32 * ((du as usize) * k + (dv as usize))); if ct.len() != CT_LEN { return Err(KEMError::LengthError("Ciphertext has the incorrect length")); @@ -830,7 +782,7 @@ impl< let mut key = KeyMaterial::::from_bytes_as_type(&K, KeyType::CryptographicRandom)?; do_hazardous_operations(&mut key, |key| { - key.set_security_strength(SecurityStrength::from_bits(LAMBDA as usize)) + key.set_security_strength(P::MAX_SECURITY_STRENGTH) })?; Ok(key) @@ -839,18 +791,14 @@ impl< /// Trait for all three of the ML-DSA algorithm variants. pub trait MLKEMTrait< + P: MLKEMParams, + PK: MLKEMPublicKeyTrait + MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, const PK_LEN: usize, const SK_LEN: usize, const CT_LEN: usize, const SS_LEN: usize, - PK: MLKEMPublicKeyTrait + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, >: Sized { /// Generates a fresh key pair. @@ -862,7 +810,7 @@ pub trait MLKEMTrait< // Should still be ok in FIPS mode, provided that you're using the FIPS-approved RNG. fn keygen_from_rng(rng: &mut dyn RNG) -> Result<(PK, SK), KEMError> { // Source the seed from the provided RNG - if rng.security_strength() < SecurityStrength::from_bits(LAMBDA as usize) { + if rng.security_strength() < P::MAX_SECURITY_STRENGTH { return Err(RNGError::SecurityStrengthInsufficientForAlgorithm)?; } let mut seed = KeyMaterial::<64>::new(); @@ -893,37 +841,32 @@ pub trait MLKEMTrait< /// Same as [`KEMEncapsulator::encaps`], but acts on an [`MLKEMPublicKeyExpanded`]. fn encaps_for_expanded_key( - pk: &MLKEMPublicKeyExpanded, + pk: &MLKEMPublicKeyExpanded, ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>; /// Same as [`KEMEncapsulator::encaps`], but acts on an [`MLKEMPublicKeyExpanded`] and uses a provided RNG. fn encaps_for_expanded_key_rng( - pk: &MLKEMPublicKeyExpanded, + pk: &MLKEMPublicKeyExpanded, rng: &mut dyn RNG, ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError>; /// Same as [`KEMDecapsulator::decaps`], but acts on an [`MLKEMPrivateKeyExpanded`]. fn decaps_with_expanded_key( - sk: &MLKEMPrivateKeyExpanded, + sk: &MLKEMPrivateKeyExpanded, ct: &[u8], ) -> Result, KEMError>; } 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, - PK: MLKEMPublicKeyTrait + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, -> KEMEncapsulator - for MLKEM +> KEMEncapsulator for MLKEM { /// Performs an encapsulation against the given public key, using the library's default internal RNG. /// Returns (shared_secret_key, ciphertext) @@ -944,25 +887,20 @@ impl< pk: &PK, rng: &mut dyn RNG, ) -> Result<(KeyMaterial, [u8; CT_LEN]), KEMError> { - Self::encaps_for_expanded_key_rng(&MLKEMPublicKeyExpanded::::from(pk), rng) + Self::encaps_for_expanded_key_rng(&MLKEMPublicKeyExpanded::::from(pk), rng) } } 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, - PK: MLKEMPublicKeyTrait + MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, - const k: usize, - const eta: i16, - const du: i16, - const dv: i16, - const LAMBDA: i16, -> KEMDecapsulator - for MLKEM +> KEMDecapsulator for MLKEM { /// Performs a decapsulation of the given ciphertext. /// Returns the shared secret key. @@ -971,7 +909,7 @@ impl< /// As ML-KEM is an implicitly-rejecting KEM, this returns an error only if the ciphertext is invalid (ie the wrong length).. fn decaps(sk: &SK, ct: &[u8]) -> Result, KEMError> { Self::decaps_with_expanded_key( - &MLKEMPrivateKeyExpanded::::from(sk), + &MLKEMPrivateKeyExpanded::::from(sk), ct, ) } diff --git a/crypto/mlkem/src/mlkem_keys.rs b/crypto/mlkem/src/mlkem_keys.rs index 8fd2bb8a..2df7f861 100644 --- a/crypto/mlkem/src/mlkem_keys.rs +++ b/crypto/mlkem/src/mlkem_keys.rs @@ -1,14 +1,14 @@ use crate::aux_functions::{byte_decode, byte_encode, expandA}; -use crate::matrix::{Matrix, Vector}; +use crate::matrix::VectorTrait; use crate::mlkem::{H, POLY_BYTES, q}; -use crate::mlkem::{MLKEM512_PK_LEN, MLKEM512_SK_LEN, MLKEM512_k}; -use crate::mlkem::{MLKEM768_PK_LEN, MLKEM768_SK_LEN, MLKEM768_k}; -use crate::mlkem::{MLKEM1024_PK_LEN, MLKEM1024_SK_LEN, MLKEM1024_k}; -use crate::{ML_KEM_512_NAME, ML_KEM_768_NAME, ML_KEM_1024_NAME}; +use crate::mlkem::{MLKEM512_PK_LEN, MLKEM512_SK_LEN}; +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::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; -use bouncycastle_core::traits::{Hash, KEMPrivateKey, KEMPublicKey, SecurityStrength}; +use bouncycastle_core::traits::{Hash, KEMPrivateKey, KEMPublicKey}; use bouncycastle_sha3::SHA3_256; use bouncycastle_utils::secret::Secret; use core::fmt; @@ -23,29 +23,29 @@ use crate::polynomial::Polynomial; /* Pub Types */ /// ML-KEM-512 Public Key -pub type MLKEM512PublicKey = MLKEMPublicKey; +pub type MLKEM512PublicKey = MLKEMPublicKey; /// ML-KEM-512 Private Key pub type MLKEM512PrivateKey = - MLKEMPrivateKey; + MLKEMPrivateKey; /// ML-KEM-768 Public Key -pub type MLKEM768PublicKey = MLKEMPublicKey; +pub type MLKEM768PublicKey = MLKEMPublicKey; /// ML-KEM-768 Private Key pub type MLKEM768PrivateKey = - MLKEMPrivateKey; + MLKEMPrivateKey; /// ML-KEM-1024 Public Key -pub type MLKEM1024PublicKey = MLKEMPublicKey; +pub type MLKEM1024PublicKey = MLKEMPublicKey; /// ML-KEM-1024 Private Key pub type MLKEM1024PrivateKey = - MLKEMPrivateKey; + MLKEMPrivateKey; /* Pre-expanded keys for repeated operations */ /// ML-KEM-512 Public Key with a pre-expanded public matrix A for repeated encaps operations. pub type MLKEM512PublicKeyExpanded = - MLKEMPublicKeyExpanded; + MLKEMPublicKeyExpanded; /// ML-KEM-512 Private Key with a pre-expanded public matrix A for repeated decaps operations. pub type MLKEM512PrivateKeyExpanded = MLKEMPrivateKeyExpanded< - MLKEM512_k, + MLKEM512Params, MLKEM512PublicKey, MLKEM512PrivateKey, MLKEM512_SK_LEN, @@ -53,10 +53,10 @@ pub type MLKEM512PrivateKeyExpanded = MLKEMPrivateKeyExpanded< >; /// ML-KEM-768 Public Key with a pre-expanded public matrix A for repeated encaps operations. pub type MLKEM768PublicKeyExpanded = - MLKEMPublicKeyExpanded; + MLKEMPublicKeyExpanded; /// ML-KEM-768 Private Key with a pre-expanded public matrix A for repeated decaps operations. pub type MLKEM768PrivateKeyExpanded = MLKEMPrivateKeyExpanded< - MLKEM768_k, + MLKEM768Params, MLKEM768PublicKey, MLKEM768PrivateKey, MLKEM768_SK_LEN, @@ -64,10 +64,10 @@ pub type MLKEM768PrivateKeyExpanded = MLKEMPrivateKeyExpanded< >; /// ML-KEM-1024 Public Key with a pre-expanded public matrix A for repeated encaps operations. pub type MLKEM1024PublicKeyExpanded = - MLKEMPublicKeyExpanded; + MLKEMPublicKeyExpanded; /// ML-KEM-1024 Private Key with a pre-expanded public matrix A for repeated decaps operations. pub type MLKEM1024PrivateKeyExpanded = MLKEMPrivateKeyExpanded< - MLKEM1024_k, + MLKEM1024Params, MLKEM1024PublicKey, MLKEM1024PrivateKey, MLKEM1024_SK_LEN, @@ -75,50 +75,57 @@ pub type MLKEM1024PrivateKeyExpanded = MLKEMPrivateKeyExpanded< >; /// An ML-KEM public key. -#[derive(Clone)] -pub struct MLKEMPublicKey { - t_hat: Vector, +pub struct MLKEMPublicKey { + t_hat: P::VecK, rho: [u8; 32], } +// Written out rather than derived: `#[derive(Clone)]` would demand `P: Clone`, and `P` is a +// marker for the parameter set that is never stored, only used to name the field types. +impl Clone for MLKEMPublicKey { + fn clone(&self) -> Self { + Self { t_hat: self.t_hat, rho: self.rho } + } +} + /// General trait for all ML-KEM public keys types. -pub trait MLKEMPublicKeyTrait: KEMPublicKey { +pub trait MLKEMPublicKeyTrait: KEMPublicKey { /// Algorithm 23 pkDecode(𝑝𝑘) /// Reverses the procedure pkEncode. /// Input: Public key 𝑝𝑘 ∈ 𝔹32+32𝑘(bitlen (𝑞−1)−𝑑). /// Output: 𝜌 ∈ 𝔹32, 𝐭1 ∈ 𝑅𝑘 with coefficients in [0, 2bitlen (𝑞−1)−𝑑 − 1]. fn pk_decode(pk: &[u8; PK_LEN]) -> Result; /// Get a copy of the expanded public matrix A_hat - fn A_hat(&self) -> Matrix; + fn A_hat(&self) -> P::MatrixA; /// Get the hash of the public key fn compute_hash(&self) -> [u8; 32]; } -pub(crate) trait MLKEMPublicKeyInternalTrait: - MLKEMPublicKeyTrait +pub(crate) trait MLKEMPublicKeyInternalTrait: + MLKEMPublicKeyTrait { /// Not exposing a constructor publicly because you should have to get an instance either by /// running a keygen, or by decoding an existing key. - fn new(t_hat: Vector, rho: [u8; 32]) -> Self; + fn new(t_hat: P::VecK, rho: [u8; 32]) -> Self; /// Get a ref to t1 - fn t_hat(&self) -> &Vector; + fn t_hat(&self) -> &P::VecK; } -impl MLKEMPublicKeyTrait - for MLKEMPublicKey +impl MLKEMPublicKeyTrait + for MLKEMPublicKey { fn pk_decode(pk: &[u8; PK_LEN]) -> Result { let (pk_chunks, last_chunk) = pk.as_chunks::(); // that should divide evenly the remainder of the array, leaving space for rho at the end - debug_assert_eq!(pk_chunks.len(), k); + debug_assert_eq!(pk_chunks.len(), P::k); debug_assert_eq!(last_chunk.len(), 32); let t_hat = { - let mut t_hat = Vector::::new(); + let mut t_hat = P::VecK::new(); - for (t_i, pk_chunk) in t_hat.elems.iter_mut().zip(pk_chunks) { + for (t_i, pk_chunk) in t_hat.elems_mut().iter_mut().zip(pk_chunks) { t_i.coeffs.copy_from_slice(&byte_decode::<12, POLY_BYTES>(pk_chunk).coeffs); // FIPS 203 says: @@ -141,8 +148,8 @@ impl MLKEMPublicKeyTrait Ok(Self::new(t_hat, rho)) } - fn A_hat(&self) -> Matrix { - expandA(&self.rho) + fn A_hat(&self) -> P::MatrixA { + expandA::

(&self.rho) } fn compute_hash(&self) -> [u8; 32] { @@ -153,19 +160,19 @@ impl MLKEMPublicKeyTrait } } -impl MLKEMPublicKeyInternalTrait - for MLKEMPublicKey +impl MLKEMPublicKeyInternalTrait + for MLKEMPublicKey { - fn new(t_hat: Vector, rho: [u8; 32]) -> Self { + fn new(t_hat: P::VecK, rho: [u8; 32]) -> Self { Self { rho, t_hat } } - fn t_hat(&self) -> &Vector { + fn t_hat(&self) -> &P::VecK { &self.t_hat } } -impl KEMPublicKey for MLKEMPublicKey { +impl KEMPublicKey for MLKEMPublicKey { /// Encodes the public key as per FIPS 203 Algorithm 13 /// 19: ekPKE ← ByteEncode12(𝐭)‖𝜌 fn encode(&self) -> [u8; PK_LEN] { @@ -177,7 +184,6 @@ impl KEMPublicKey for MLKEMPublicKe /// Encodes the public key as per FIPS 203 Algorithm 13 /// 19: ekPKE ← ByteEncode12(𝐭)‖𝜌 fn encode_out(&self, out: &mut [u8; PK_LEN]) -> usize { - debug_assert_eq!(PK_LEN, 12 * k * 32 + 32); debug_assert_eq!(POLY_BYTES, 12 * 32); out.fill(0); @@ -185,10 +191,10 @@ impl KEMPublicKey for MLKEMPublicKe let (pk_chunks, last_chunk) = out.as_chunks_mut::(); // that should divide evenly the remainder of the array, leaving space for rho at the end - debug_assert_eq!(pk_chunks.len(), k); + debug_assert_eq!(pk_chunks.len(), P::k); debug_assert_eq!(last_chunk.len(), 32); - for (pk_chunk, t_i) in pk_chunks.into_iter().zip(&self.t_hat.elems) { + for (pk_chunk, t_i) in pk_chunks.into_iter().zip(self.t_hat.elems()) { pk_chunk.copy_from_slice(&byte_encode::<12, POLY_BYTES>(t_i)); } last_chunk.copy_from_slice(&self.rho); @@ -205,70 +211,66 @@ impl KEMPublicKey for MLKEMPublicKe } } -impl Eq for MLKEMPublicKey {} +impl Eq for MLKEMPublicKey {} -impl PartialEq for MLKEMPublicKey { +impl PartialEq for MLKEMPublicKey { fn eq(&self, other: &Self) -> bool { bouncycastle_utils::ct::ct_eq_bytes(&self.encode(), &other.encode()) } } -impl Debug for MLKEMPublicKey { +impl Debug for MLKEMPublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; let hash = SHA3_256::new().hash(&self.encode()); - write!(f, "MLKEMPublicKey {{ alg: {}, pub_key_hash: {:x?} }}", alg, hash) + write!(f, "MLKEMPublicKey {{ alg: {}, pub_key_hash: {:x?} }}", P::ALG_NAME, hash) } } -impl Display for MLKEMPublicKey { +impl Display for MLKEMPublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; let hash = SHA3_256::new().hash(&self.encode()); - write!(f, "MLKEMPublicKey {{ alg: {}, pub_key_hash: {:x?} }}", alg, hash) + write!(f, "MLKEMPublicKey {{ alg: {}, pub_key_hash: {:x?} }}", P::ALG_NAME, hash) } } /// A fully expanded ML-KEM public key that includes the intermediate values needed for performing multiple encaps operations /// against the same public key, which causes the MLKEMPublicKey struct to take up more memory, but results /// in more efficient repeated encaps() operations. -#[derive(Clone)] pub struct MLKEMPublicKeyExpanded< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const PK_LEN: usize, > { pub(crate) ek: PK, - pub(crate) A_hat: Matrix, + pub(crate) A_hat: P::MatrixA, } -impl, const PK_LEN: usize> - MLKEMPublicKeyInternalTrait for MLKEMPublicKeyExpanded +/// See the note on [`MLKEMPublicKey`]'s `Clone` for why this is not derived. +impl, const PK_LEN: usize> Clone + for MLKEMPublicKeyExpanded { - fn new(t_hat: Vector, rho: [u8; 32]) -> Self { + fn clone(&self) -> Self { + Self { ek: self.ek.clone(), A_hat: self.A_hat.clone() } + } +} + +impl, const PK_LEN: usize> + MLKEMPublicKeyInternalTrait for MLKEMPublicKeyExpanded +{ + fn new(t_hat: P::VecK, rho: [u8; 32]) -> Self { let ek = PK::new(t_hat, rho); let A_hat = ek.A_hat(); Self { ek, A_hat } } - fn t_hat(&self) -> &Vector { + fn t_hat(&self) -> &P::VecK { self.ek.t_hat() } } -impl, const PK_LEN: usize> - KEMPublicKey for MLKEMPublicKeyExpanded +impl, const PK_LEN: usize> + KEMPublicKey for MLKEMPublicKeyExpanded { fn encode(&self) -> [u8; PK_LEN] { let mut pk = [0u8; PK_LEN]; @@ -292,51 +294,39 @@ impl, const PK_LEN: u } } -impl, const PK_LEN: usize> PartialEq - for MLKEMPublicKeyExpanded +impl, const PK_LEN: usize> PartialEq + for MLKEMPublicKeyExpanded { fn eq(&self, other: &Self) -> bool { self.encode() == other.encode() } } -impl, const PK_LEN: usize> Eq - for MLKEMPublicKeyExpanded +impl, const PK_LEN: usize> Eq + for MLKEMPublicKeyExpanded { } -impl, const PK_LEN: usize> Debug - for MLKEMPublicKeyExpanded +impl, const PK_LEN: usize> Debug + for MLKEMPublicKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; let hash = SHA3_256::new().hash(&self.encode()); - write!(f, "MLKEMPublicKeyExpanded {{ alg: {}, pub_key_hash: {:x?} }}", alg, hash) + write!(f, "MLKEMPublicKeyExpanded {{ alg: {}, pub_key_hash: {:x?} }}", P::ALG_NAME, hash) } } -impl, const PK_LEN: usize> Display - for MLKEMPublicKeyExpanded +impl, const PK_LEN: usize> Display + for MLKEMPublicKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; let hash = SHA3_256::new().hash(&self.encode()); - write!(f, "MLKEMPublicKeyExpanded {{ alg: {}, pub_key_hash: {:x?} }}", alg, hash) + write!(f, "MLKEMPublicKeyExpanded {{ alg: {}, pub_key_hash: {:x?} }}", P::ALG_NAME, hash) } } -impl, const PK_LEN: usize> - MLKEMPublicKeyTrait for MLKEMPublicKeyExpanded +impl, const PK_LEN: usize> + MLKEMPublicKeyTrait for MLKEMPublicKeyExpanded { fn pk_decode(pk: &[u8; PK_LEN]) -> Result { let ek = PK::pk_decode(pk)?; @@ -344,7 +334,7 @@ impl, const PK_LEN: u Ok(Self { ek, A_hat }) } - fn A_hat(&self) -> Matrix { + fn A_hat(&self) -> P::MatrixA { self.A_hat.clone() } @@ -353,8 +343,8 @@ impl, const PK_LEN: u } } -impl, const PK_LEN: usize> From<&PK> - for MLKEMPublicKeyExpanded +impl, const PK_LEN: usize> From<&PK> + for MLKEMPublicKeyExpanded { /// Fully expands the intermediate values needed for performing multiple encaps operations /// against the same public key, which causes the MLKEMPublicKey struct to take up @@ -368,43 +358,64 @@ impl, const PK_LEN: u /// An ML-KEM private key. /// // Dev note: This will automatically inherit the [`Secret`] protections because [`Polynomial`] wraps the underlying data with [`Secret`]. -#[derive(Clone)] pub struct MLKEMPrivateKey< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, > { - s_hat: Secret>, + s_hat: Secret, ek: PK, pk_hash: [u8; 32], z: Secret<[u8; 32]>, seed_d: Option>, } +/// See the note on [`MLKEMPublicKey`]'s `Clone` for why this is not derived. +impl< + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, + const SK_LEN: usize, + const PK_LEN: usize, +> Clone for MLKEMPrivateKey +{ + fn clone(&self) -> Self { + Self { + s_hat: self.s_hat.clone(), + ek: self.ek.clone(), + pk_hash: self.pk_hash, + z: self.z.clone(), + seed_d: self.seed_d.clone(), + } + } +} + impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> MLKEMPrivateKey +> MLKEMPrivateKey { /// As described on Algorithm 16 line /// 3: dk ← (dkPKE ‖ ek ‖ H(ek) ‖ 𝑧) fn sk_encode_out(&self, out: &mut [u8; SK_LEN]) -> usize { out.fill(0); - debug_assert_eq!(SK_LEN, /* dk_pke*/ 12*k*32 + /*ek*/PK_LEN + /*H(ek)*/32 + /*z*/32); + debug_assert_eq!( + SK_LEN, + /* dk_pke*/ 12*P::k*32 + /*ek*/PK_LEN + /*H(ek)*/32 + /*z*/32 + ); let mut pos = 0usize; /* dk_pke */ // Alg 13; line 20: dkPKE ← ByteEncode12(𝐬) - for i in 0..k { + for i in 0..P::k { out[i * POLY_BYTES..(i + 1) * POLY_BYTES] .copy_from_slice(&byte_encode::<12, POLY_BYTES>(&self.s_hat[i])); } - pos += k * POLY_BYTES; + pos += P::k * POLY_BYTES; /* ek */ // Alg 13; line 19: ekPKE ← ByteEncode12(𝐭)‖𝜌 @@ -426,8 +437,8 @@ impl< /// General trait for all ML-KEM private keys types. pub trait MLKEMPrivateKeyTrait< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, >: KEMPrivateKey @@ -444,8 +455,8 @@ pub trait MLKEMPrivateKeyTrait< } pub(crate) trait MLKEMPrivateKeyInternalTrait< - const k: usize, - PK: MLKEMPublicKeyTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyTrait, const SK_LEN: usize, const PK_LEN: usize, > @@ -453,7 +464,7 @@ pub(crate) trait MLKEMPrivateKeyInternalTrait< /// Not exposing a constructor publicly because you should have to get an instance either by /// running a keygen, or by decoding an existing key. fn new( - s_hat: Secret>, + s_hat: Secret, ek: PK, h: [u8; 32], z: Secret<[u8; 32]>, @@ -461,17 +472,17 @@ pub(crate) trait MLKEMPrivateKeyInternalTrait< ) -> Self; /// Get a ref to s_hat - fn s_hat(&self) -> &Vector; + fn s_hat(&self) -> &P::VecK; fn z(&self) -> &Secret<[u8; 32]>; } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> MLKEMPrivateKeyTrait for MLKEMPrivateKey +> MLKEMPrivateKeyTrait for MLKEMPrivateKey { fn seed(&self) -> Option> { if self.seed_d.is_none() { @@ -483,12 +494,7 @@ impl< let mut seed = KeyMaterial::<64>::from_bytes_as_type(&*tmp, KeyType::Seed).unwrap(); key_material::do_hazardous_operations(&mut seed, |seed| { - seed.set_security_strength(match k { - 2 => SecurityStrength::_128bit, - 3 => SecurityStrength::_192bit, - 4 => SecurityStrength::_256bit, - _ => unreachable!("Invalid mlkem param set"), - }) + seed.set_security_strength(P::MAX_SECURITY_STRENGTH) }) .unwrap(); @@ -505,14 +511,17 @@ impl< } fn sk_decode(sk: &[u8; SK_LEN]) -> Result { - debug_assert_eq!(SK_LEN, /* dk_pke*/ 12*k*32 + /*ek*/PK_LEN + /*H(ek)*/32 + /*z*/32); + debug_assert_eq!( + SK_LEN, + /* dk_pke*/ 12*P::k*32 + /*ek*/PK_LEN + /*H(ek)*/32 + /*z*/32 + ); let mut pos = 0usize; /* dk_pke */ - let mut s_hat: Secret> = Secret::new(); + let mut s_hat: Secret = Secret::new(); // for (s_i, sk_chunk) in s_hat.0.iter_mut().zip(sk_chunks) { - for i in 0..k { + for i in 0..P::k { s_hat[i] = byte_decode::<12, POLY_BYTES>( sk[i * POLY_BYTES..(i + 1) * POLY_BYTES].try_into().unwrap(), ); @@ -529,7 +538,7 @@ impl< } } } - pos += k * POLY_BYTES; + pos += P::k * POLY_BYTES; /* ek */ let ek = PK::pk_decode(sk[pos..pos + PK_LEN].try_into().unwrap())?; @@ -557,15 +566,15 @@ impl< } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> MLKEMPrivateKeyInternalTrait for MLKEMPrivateKey +> MLKEMPrivateKeyInternalTrait for MLKEMPrivateKey { /// Note to future maintainers: FIPS 203 section 7.3 requires that ek be hashed and compared to pk_hash. fn new( - s_hat: Secret>, + s_hat: Secret, ek: PK, pk_hash: [u8; 32], z: Secret<[u8; 32]>, @@ -574,7 +583,7 @@ impl< Self { s_hat, ek, pk_hash, z, seed_d: seed_d.clone() } } - fn s_hat(&self) -> &Vector { + fn s_hat(&self) -> &P::VecK { &self.s_hat } @@ -584,11 +593,11 @@ impl< } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> KEMPrivateKey for MLKEMPrivateKey +> KEMPrivateKey for MLKEMPrivateKey { fn encode(&self) -> [u8; SK_LEN] { let mut out = [0u8; SK_LEN]; @@ -617,20 +626,20 @@ impl< } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Eq for MLKEMPrivateKey +> Eq for MLKEMPrivateKey { } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> PartialEq for MLKEMPrivateKey +> PartialEq for MLKEMPrivateKey { fn eq(&self, other: &Self) -> bool { let self_encoded = self.encode(); @@ -641,23 +650,17 @@ impl< /// Debug impl mainly to prevent the secret key from being printed in logs. impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> fmt::Debug for MLKEMPrivateKey +> fmt::Debug for MLKEMPrivateKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLKEMPrivateKey {{ alg: {}, pub_key_hash: {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.pk_hash, self.seed_d.is_some(), ) @@ -666,23 +669,17 @@ impl< /// Display impl mainly to prevent the secret key from being printed in logs. impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Display for MLKEMPrivateKey +> Display for MLKEMPrivateKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLKEMPrivateKey {{ alg: {}, pub_key_hash: {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.pk_hash, self.seed_d.is_some(), ) @@ -692,28 +689,42 @@ impl< /// A fully expanded ML-KEM private key that includes the intermediate values needed for performing /// multiple decaps operations with the same private key, which causes the private key struct to /// take up more memory, but results in more efficient repeated decaps() operations. -#[derive(Clone)] pub struct MLKEMPrivateKeyExpanded< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, > { _phantom: core::marker::PhantomData, pub(crate) dk: SK, - pub(crate) A_hat: Matrix, + pub(crate) A_hat: P::MatrixA, } +/// See the note on [`MLKEMPublicKey`]'s `Clone` for why this is not derived. impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> From<&SK> for MLKEMPrivateKeyExpanded +> Clone for MLKEMPrivateKeyExpanded +{ + fn clone(&self) -> Self { + Self { _phantom: core::marker::PhantomData, dk: self.dk.clone(), A_hat: self.A_hat.clone() } + } +} + +impl< + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, + const SK_LEN: usize, + const PK_LEN: usize, +> From<&SK> for MLKEMPrivateKeyExpanded { /// Fully expands the intermediate values needed for performing multiple encaps operations /// against the same public key, which causes the MLKEMPublicKey struct to take up @@ -725,13 +736,13 @@ impl< } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> KEMPrivateKey for MLKEMPrivateKeyExpanded +> KEMPrivateKey for MLKEMPrivateKeyExpanded { fn encode(&self) -> [u8; SK_LEN] { self.dk.encode() @@ -749,13 +760,13 @@ impl< } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> PartialEq for MLKEMPrivateKeyExpanded +> PartialEq for MLKEMPrivateKeyExpanded { fn eq(&self, other: &Self) -> bool { self.dk.eq(&other.dk) @@ -763,36 +774,30 @@ impl< } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Eq for MLKEMPrivateKeyExpanded +> Eq for MLKEMPrivateKeyExpanded { } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Debug for MLKEMPrivateKeyExpanded +> Debug for MLKEMPrivateKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLKEMPrivateKeyExpanded {{ alg: {}, pub_key_hash: {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.dk.pk().compute_hash(), self.dk.seed().is_some(), ) @@ -800,25 +805,19 @@ impl< } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> Display for MLKEMPrivateKeyExpanded +> Display for MLKEMPrivateKeyExpanded { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let alg = match k { - 2 => ML_KEM_512_NAME, - 3 => ML_KEM_768_NAME, - 4 => ML_KEM_1024_NAME, - _ => panic!("Unsupported key length"), - }; write!( f, "MLKEMPrivateKeyExpanded {{ alg: {}, pub_key_hash: {:x?}, has_seed: {} }}", - alg, + P::ALG_NAME, self.dk.pk().compute_hash(), self.dk.seed().is_some(), ) @@ -826,14 +825,14 @@ impl< } impl< - const k: usize, - PK: MLKEMPublicKeyInternalTrait, - SK: MLKEMPrivateKeyTrait - + MLKEMPrivateKeyInternalTrait, + P: MLKEMParams, + PK: MLKEMPublicKeyInternalTrait, + SK: MLKEMPrivateKeyTrait + + MLKEMPrivateKeyInternalTrait, const SK_LEN: usize, const PK_LEN: usize, -> MLKEMPrivateKeyTrait - for MLKEMPrivateKeyExpanded +> MLKEMPrivateKeyTrait + for MLKEMPrivateKeyExpanded { fn seed(&self) -> Option> { self.dk.seed() diff --git a/crypto/mlkem/src/params.rs b/crypto/mlkem/src/params.rs new file mode 100644 index 00000000..2e0df283 --- /dev/null +++ b/crypto/mlkem/src/params.rs @@ -0,0 +1,216 @@ +//! The three ML-KEM parameter sets of FIPS 203, Section 8, as a sealed trait with one type per set. +//! +//! # Derived parameters +//! +//! FIPS 203, Table 2 assigns five values per set (𝑘, 𝜂1, 𝜂2, 𝑑𝑢, 𝑑𝑣); its last column, the +//! required RBG strength, is carried by `MAX_SECURITY_STRENGTH`. +//! The three sizes of Table 3 are each a function of those, so they are written once as defaulted +//! associated consts rather than three times as a hand-computed number. `params::tests` checks every +//! derivation against the values tabulated in FIPS 203. + +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; + +/// A crate-private (aka "sealed") trait that prevents a new ML-KEM parameter set from being defined +/// outside this crate. +trait MLKEMParamsInternalTrait {} + +/// One ML-KEM parameter set: the values of FIPS 203, Table 2 and Table 3, and the types whose size +/// they determine. +/// +/// Sealed via a private supertrait, so [`MLKEM512Params`], [`MLKEM768Params`] and +/// [`MLKEM1024Params`] are the only implementations. +pub trait MLKEMParams: MLKEMParamsInternalTrait { + /* FIPS 203, Table 2: the values assigned by each parameter set. */ + + /// 𝑘, the rank of the module. + const k: usize; + /// 𝜂1, the CBD parameter used for the secret vector 𝐬 and the keygen error vector 𝐞. + const eta1: i16; + /// 𝜂2, the CBD parameter used for the encaps error terms 𝐞1 and 𝑒2. + /// FIPS 203, Table 2 lists this per parameter set even though all three assign it 2. + const eta2: i16; + /// 𝑑𝑢, the compression parameter for 𝐮. + const du: i16; + /// 𝑑𝑣, the compression parameter for 𝑣. + const dv: i16; + + /* Algorithm meta-data */ + + /// The algorithm name, as reported by `Algorithm::ALG_NAME`. + const ALG_NAME: &'static str; + /// The strength claimed for this parameter set, as reported by `Algorithm::MAX_SECURITY_STRENGTH`. + const MAX_SECURITY_STRENGTH: SecurityStrength; + /// The OID in component form, as reported by `AlgorithmOID::OID`. + const OID: &'static [u32]; + /// The DER encoding of [`MLKEMParams::OID`], as reported by `AlgorithmOID::OID_DER`. + const OID_DER: &'static [u8]; + + /* Derived. Never written out per parameter set -- see the module docs. */ + + /// The length of an encapsulation key: FIPS 203, Algorithm 16 (ML-KEM.KeyGen_internal) gives + /// ek ∈ 𝔹^(384𝑘+32). + const PK_LEN: usize = 384 * Self::k + 32; + + /// The length of a decapsulation key: FIPS 203, Algorithm 16 (ML-KEM.KeyGen_internal) gives + /// dk ∈ 𝔹^(768𝑘+96). + const SK_LEN: usize = 768 * Self::k + 96; + + /// The length of a ciphertext: FIPS 203, Algorithm 17 (ML-KEM.Encaps_internal) gives + /// 𝑐 ∈ 𝔹^(32(𝑑𝑢𝑘+𝑑𝑣)). + const CT_LEN: usize = 32 * (Self::du as usize * Self::k + Self::dv as usize); + + /// The length of a shared secret. 32 bytes for every parameter set (FIPS 203, Table 3). + const SS_LEN: usize = MLKEM_SS_LEN; + + /* Types whose size depends on the parameter set. */ + + /// A vector of 𝑘 polynomials, i.e. an element of 𝑅^𝑘. + type VecK: VectorTrait; + /// The 𝑘 × 𝑘 public matrix 𝐀̂. + type MatrixA: MatrixTrait; +} + +/// The ML-KEM-512 parameter set (FIPS 203, Table 2). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLKEM512Params; +/// The ML-KEM-768 parameter set (FIPS 203, Table 2). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLKEM768Params; +/// The ML-KEM-1024 parameter set (FIPS 203, Table 2). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MLKEM1024Params; + +impl MLKEMParamsInternalTrait for MLKEM512Params {} +impl MLKEMParamsInternalTrait for MLKEM768Params {} +impl MLKEMParamsInternalTrait for MLKEM1024Params {} + +impl MLKEMParams for MLKEM512Params { + const k: usize = 2; + const eta1: i16 = 3; + const eta2: i16 = 2; + const du: i16 = 10; + const dv: i16 = 4; + + const ALG_NAME: &'static str = ML_KEM_512_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; + /// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-512 { kems 1 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 1]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x01]; + + type VecK = Vector<2>; + type MatrixA = Matrix<2, 2>; +} + +impl MLKEMParams for MLKEM768Params { + const k: usize = 3; + const eta1: i16 = 2; + const eta2: i16 = 2; + const du: i16 = 10; + const dv: i16 = 4; + + const ALG_NAME: &'static str = ML_KEM_768_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_192bit; + /// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-768 { kems 2 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 2]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x02]; + + type VecK = Vector<3>; + type MatrixA = Matrix<3, 3>; +} + +impl MLKEMParams for MLKEM1024Params { + const k: usize = 4; + const eta1: i16 = 2; + const eta2: i16 = 2; + const du: i16 = 11; + const dv: i16 = 5; + + const ALG_NAME: &'static str = ML_KEM_1024_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_256bit; + /// Assigned by NIST in the Computer Security Objects Register: id-alg-ml-kem-1024 { kems 3 } + const OID: &'static [u32] = &[2, 16, 840, 1, 101, 3, 4, 4, 3]; + const OID_DER: &'static [u8] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x03]; + + type VecK = Vector<4>; + type MatrixA = Matrix<4, 4>; +} + +#[cfg(test)] +mod tests { + use super::*; + + /// FIPS 203, Table 2, transcribed row by row: the five values each parameter set + /// assigns, plus its last column. `(k, eta1, eta2, du, dv, rbg_strength)`. + const TABLE_2: [(usize, i16, i16, i16, i16, i16); 3] = + [(2, 3, 2, 10, 4, 128), (3, 2, 2, 10, 4, 192), (4, 2, 2, 11, 5, 256)]; + + /// FIPS 203, Table 3, transcribed row by row, in bytes: + /// `(encapsulation key, decapsulation key, ciphertext, shared secret key)`. + const TABLE_3: [(usize, usize, usize, usize); 3] = + [(800, 1632, 768, 32), (1184, 2400, 1088, 32), (1568, 3168, 1568, 32)]; + + fn check_table_2(i: usize) { + let (k, eta1, eta2, du, dv, rbg_strength) = TABLE_2[i]; + assert_eq!(P::k, k, "{}: 𝑘", P::ALG_NAME); + assert_eq!(P::eta1, eta1, "{}: 𝜂1", P::ALG_NAME); + assert_eq!(P::eta2, eta2, "{}: 𝜂2", P::ALG_NAME); + assert_eq!(P::du, du, "{}: 𝑑𝑢", P::ALG_NAME); + assert_eq!(P::dv, dv, "{}: 𝑑𝑣", P::ALG_NAME); + assert_eq!( + P::MAX_SECURITY_STRENGTH, + SecurityStrength::from_bits(rbg_strength as usize), + "{}: required RBG strength", + P::ALG_NAME + ); + } + + fn check_table_3(i: usize) { + let (pk_len, sk_len, ct_len, ss_len) = TABLE_3[i]; + assert_eq!(P::PK_LEN, pk_len, "{}: encapsulation key size", P::ALG_NAME); + assert_eq!(P::SK_LEN, sk_len, "{}: decapsulation key size", P::ALG_NAME); + assert_eq!(P::CT_LEN, ct_len, "{}: ciphertext size", P::ALG_NAME); + assert_eq!(P::SS_LEN, ss_len, "{}: shared secret size", P::ALG_NAME); + } + + /// The associated types must be as long as `k` says; they are written out by hand per + /// parameter set, so this guards against a typo in one of them. + fn check_associated_type_sizes() { + // Measured rather than read off a const: `MatrixTrait` already ties the + // matrix to the vector at the type level, so the only thing left to check is that both + // are 𝑘 polynomials long. + let poly = size_of::(); + assert_eq!(size_of::(), P::k * poly, "{}: VecK vs 𝑘", P::ALG_NAME); + assert_eq!( + size_of::(), + P::k * P::k * poly, + "{}: MatrixA vs 𝑘 × 𝑘", + P::ALG_NAME + ); + } + + #[test] + fn test_parameter_sets_match_fips203_table_2() { + check_table_2::(0); + check_table_2::(1); + check_table_2::(2); + } + + #[test] + fn test_sizes_match_fips203_table_3() { + check_table_3::(0); + check_table_3::(1); + check_table_3::(2); + } + + #[test] + fn test_associated_types_are_the_length_their_consts_claim() { + check_associated_type_sizes::(); + check_associated_type_sizes::(); + check_associated_type_sizes::(); + } +} diff --git a/crypto/mlkem/src/polynomial.rs b/crypto/mlkem/src/polynomial.rs index 9d913db0..5fdd6672 100644 --- a/crypto/mlkem/src/polynomial.rs +++ b/crypto/mlkem/src/polynomial.rs @@ -6,6 +6,7 @@ use crate::aux_functions::{ ZETAS, ZETAS_INV, barrett_reduce, montgomery_reduce, mul_mont, ntt_base_mult, }; use crate::mlkem::{N, q}; +use crate::params::MLKEMParams; /// A polynomial over the ML-KEM ring. /// @@ -14,7 +15,10 @@ use crate::mlkem::{N, q}; /// and sometimes private keys. /// It is the responsibility of the caller to wrap sensitive instances in `Secret`. #[derive(Clone, Copy)] -pub(crate) struct Polynomial { +/// +/// Public only because it appears in [`crate::VectorTrait`]'s signatures; its fields and +/// operations are crate-private, so from outside it is an opaque handle. +pub struct Polynomial { pub(crate) coeffs: [i16; N], } @@ -136,13 +140,13 @@ impl Polynomial { /// This is an optimized version of /// ByteEncode_𝑑𝑣( Compress_𝑑𝑣(𝑣) ) /// which packs a single polynomial according to the packing coefficient dv - pub(crate) fn compress_poly(&self, out: &mut [u8]) { - // make sure we have received a dv - debug_assert!(dv == 4 || dv == 5); + pub(crate) fn compress_poly(&self, out: &mut [u8]) { + // make sure we have received a P::dv + debug_assert!(P::dv == 4 || P::dv == 5); // make sure the right size output buffer is given - // each of the N i16's will take dv bits - debug_assert_eq!(out.len(), N * (dv as usize) / 8); + // each of the N i16's will take P::dv bits + debug_assert_eq!(out.len(), N * (P::dv as usize) / 8); let mut t = [0u8; 8]; let mut idx = 0; @@ -154,7 +158,7 @@ impl Polynomial { // let mut s = self.clone(); // s.cond_sub_q(); - match dv { + match P::dv { 4 => { // MLKEM512 and MLKEM768 for i in 0..N / 8 { @@ -195,22 +199,18 @@ impl Polynomial { /// This is an optimized version of /// Decompress_𝑑𝑣( ByteDecode_𝑑𝑣(𝑐2) ) /// which unpacks a single polynomial according to the packing coefficient dv - pub(crate) fn decompress_poly(compressed_v: &[u8]) -> Polynomial { - // make sure to received a dv - debug_assert!(dv == 4 || dv == 5); - + pub(crate) fn decompress_poly(compressed_v: &[u8]) -> Polynomial { // make sure the right size output buffer is given - // each of the N i16's will take dv bits - debug_assert_eq!(compressed_v.len(), N * (dv as usize) / 8); + // each of the N i16's will take P::dv bits + debug_assert_eq!(compressed_v.len(), N * (P::dv as usize) / 8); let mut v = Polynomial::new(); let mut idx = 0usize; - // if self.m_engine.poly_compressed_bytes() == 128 { - match dv { + match P::dv { + // MLKEM512 and MLKEM768 4 => { - // MLKEM512 and MLKEM768 for i in 0..N / 2 { v[2 * i] = (((((compressed_v[idx] & 15) as i16) as i32 * (q as i32)) + 8) >> 4) as i16; @@ -219,8 +219,8 @@ impl Polynomial { idx += 1; } } + // MLKEM1024 5 => { - // MLKEM1024 let mut t = [0u8; 8]; for i in 0..N / 8 { t[0] = compressed_v[idx]; @@ -320,7 +320,6 @@ impl Polynomial { /// /// Borrowed from: /// -/// Note: this is exposed publicly only for testing purposes and there is no good reason to use it in production code. pub(crate) fn base_mult_montgomery(a: &Polynomial, b: &Polynomial) -> Polynomial { let mut r = Polynomial::new(); diff --git a/crypto/mlkem/tests/mlkem_tests.rs b/crypto/mlkem/tests/mlkem_tests.rs index 9ade7bcf..4faf8498 100644 --- a/crypto/mlkem/tests/mlkem_tests.rs +++ b/crypto/mlkem/tests/mlkem_tests.rs @@ -813,6 +813,42 @@ mod mlkem_tests { fake_rng.set_security_strength(SecurityStrength::_256bit); _ = MLKEM1024::encaps_rng(&pk1024, &mut fake_rng).unwrap(); } + + #[test] + fn algorithm_names_and_oids() { + use bouncycastle_core::traits::{Algorithm, AlgorithmOID, SecurityStrength}; + + // `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 + // blanket impls, or a typo in a parameter set, is a test failure rather than a silently + // mislabelled algorithm or an unparseable OID. + assert_eq!(MLKEM512::ALG_NAME, "ML-KEM-512"); + assert_eq!(MLKEM768::ALG_NAME, "ML-KEM-768"); + assert_eq!(MLKEM1024::ALG_NAME, "ML-KEM-1024"); + + assert_eq!(MLKEM512::MAX_SECURITY_STRENGTH, SecurityStrength::_128bit); + assert_eq!(MLKEM768::MAX_SECURITY_STRENGTH, SecurityStrength::_192bit); + assert_eq!(MLKEM1024::MAX_SECURITY_STRENGTH, SecurityStrength::_256bit); + + // NIST's Computer Security Objects Register: id-alg-ml-kem-512 { kems 1 }, + // id-alg-ml-kem-768 { kems 2 }, id-alg-ml-kem-1024 { kems 3 }. + assert_eq!(MLKEM512::OID, &[2, 16, 840, 1, 101, 3, 4, 4, 1]); + assert_eq!(MLKEM768::OID, &[2, 16, 840, 1, 101, 3, 4, 4, 2]); + assert_eq!(MLKEM1024::OID, &[2, 16, 840, 1, 101, 3, 4, 4, 3]); + + for (oid, der) in [ + (MLKEM512::OID, MLKEM512::OID_DER), + (MLKEM768::OID, MLKEM768::OID_DER), + (MLKEM1024::OID, MLKEM1024::OID_DER), + ] { + assert_eq!(der[0], 0x06, "DER tag must be OBJECT IDENTIFIER"); + assert_eq!(der[1] as usize, der.len() - 2, "DER length must match the content"); + assert_eq!( + &der[2..], + &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, *oid.last().unwrap() as u8] + ); + } + } } // struct Kat { diff --git a/crypto/modes/Cargo.toml b/crypto/modes/Cargo.toml new file mode 100644 index 00000000..bc020c7f --- /dev/null +++ b/crypto/modes/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "bouncycastle-modes" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +# Only for the default OS-backed DRBG that generates the IV in `do_encrypt_init`. +bouncycastle-rng.workspace = true + +[dev-dependencies] +bouncycastle-aes-lowmemory.workspace = true +bouncycastle-core-test-framework.workspace = true +bouncycastle-hex.workspace = true +# Only to prove the modes compose with the padding layer for arbitrary-length data; no runtime dep. +bouncycastle-padding.workspace = true +criterion.workspace = true +serde_json = "1.0" + +[[bench]] +name = "modes_benches" +harness = false diff --git a/crypto/modes/benches/modes_benches.rs b/crypto/modes/benches/modes_benches.rs new file mode 100644 index 00000000..1df8b1ac --- /dev/null +++ b/crypto/modes/benches/modes_benches.rs @@ -0,0 +1,606 @@ +//! 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 -- +//! eights first, then pairs, then the remainder singly: for CBC that is `decrypt_blocks8` / +//! `decrypt_blocks2`, for CFB it is `encrypt_blocks8` / `encrypt_blocks2`, since CFB uses the +//! forward function in both directions. AES overrides only the pair form, so its eights are four +//! 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. +//! +//! 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::Aes128` and `modes::cfb::Aes128` 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_lowmemory::{Aes128, Aes256}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, SecurityStrength, +}; +use bouncycastle_modes::{Cbc, Cfb, Decrypting, Ecb, Encrypting}; +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 Aes128Ecb = Ecb; + +/// AES-128 with the pair methods **not** overridden, so they fall back to the trait defaults of +/// two single-block calls. +/// +/// 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_blocks2` 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(Aes128); + +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) + } + // encrypt_blocks2 / decrypt_blocks2 deliberately left as the trait defaults. +} + +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::Aes128"); + 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(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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // ---- decryption: parallel, uses decrypt_blocks2 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(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(block).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // N=2 is one pair and N=8 one eight (four pairs, for AES), so every block goes through + // decrypt_blocks2. + 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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB decrypt -- N=8 (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(8) { + let arr: &mut [u8; 8 * BLOCK_LEN] = + chunk.as_flattened_mut().try_into().unwrap(); + dec.do_decrypt(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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // The controlled comparison: identical N, identical cipher, pair methods overridden vs not. + // This pair of numbers -- and only this pair -- measures what `decrypt_blocks2` buys. + group.bench_function("16KiB decrypt -- N=8, pair path (blocks2 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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB decrypt -- N=8, no pair path (trait default)", |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(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::Aes256"); + 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(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(chunk).unwrap(); + } + + group.bench_function("16KiB decrypt -- N=8 (all pairs)", |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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +fn bench_cfb_aes128(c: &mut Criterion) { + let k = key::<16>(); + let blocks = data(); + + let mut group = c.benchmark_group("modes::cfb::Aes128"); + group.throughput(Throughput::Bytes(DATA_LEN as u64)); + + // ---- encryption: serial. Oj+1 = CIPH_K(Cj), and Cj is the previous call's output ---- + group.bench_function("16KiB encrypt -- N=1", |b| { + b.iter_batched( + || blocks.clone(), + |mut scratch| { + let (mut enc, _) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); + for block in scratch.iter_mut() { + enc.do_encrypt(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, _) = Aes128Cfb::::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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // ---- decryption: parallel, and uses `encrypt_blocks2` -- the FORWARD pair method ---- + let (mut enc, iv) = Aes128Cfb::::do_encrypt_init(&k).unwrap(); + let mut ciphertext = blocks.clone(); + for chunk in ciphertext.chunks_exact_mut(8) { + let arr: &mut [[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); + enc.do_encrypt_blocks(arr).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 = Aes128Cfb::::do_decrypt_init(&k, &iv).unwrap(); + for block in scratch.iter_mut() { + dec.do_decrypt(block).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // N=2 and N=8 are all pairs, so every block goes through encrypt_blocks2. + group.bench_function("16KiB decrypt -- N=2 (all pairs)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cfb::::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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB decrypt -- N=8 (all pairs)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cfb::::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(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 = Aes128Cfb::::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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // The controlled comparison: identical N, identical cipher, pair methods overridden vs not. + // This pair of numbers -- and only this pair -- measures what `encrypt_blocks2` buys CFB. + group.bench_function("16KiB decrypt -- N=8, pair path (blocks2 overridden)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes128Cfb::::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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB decrypt -- N=8, no pair path (trait default)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = UnpairedAes128Cfb::::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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.finish(); +} + +fn bench_cfb_aes256(c: &mut Criterion) { + let k = key::<32>(); + let blocks = data(); + + let mut group = c.benchmark_group("modes::cfb::Aes256"); + 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, _) = Aes256Cfb::::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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + let (mut enc, iv) = Aes256Cfb::::do_encrypt_init(&k).unwrap(); + let mut ciphertext = blocks.clone(); + for chunk in ciphertext.chunks_exact_mut(8) { + let arr: &mut [[u8; BLOCK_LEN]; 8] = chunk.try_into().unwrap(); + enc.do_encrypt_blocks(arr).unwrap(); + } + + group.bench_function("16KiB decrypt -- N=8 (all pairs)", |b| { + b.iter_batched( + || ciphertext.clone(), + |mut scratch| { + let mut dec = Aes256Cfb::::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(arr).unwrap(); + } + 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::Aes128"); + 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(block).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB encrypt -- N=8 (eights)", |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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + group.bench_function("16KiB decrypt -- N=8 (eights)", |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(arr).unwrap(); + } + black_box(&scratch); + }, + BatchSize::LargeInput, + ) + }); + + // The controlled comparison: identical N, identical cipher, batch methods overridden vs not. + group.bench_function("16KiB encrypt -- N=8, no pair path (trait default)", |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(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("Aes128 do_encrypt_init (key schedule + IV)", |b| { + b.iter(|| black_box(Aes128Cbc::::do_encrypt_init(black_box(&k128)).unwrap().1)) + }); + group.bench_function("Aes128 do_decrypt_init (key schedule only)", |b| { + b.iter(|| { + black_box(Aes128Cbc::::do_decrypt_init(black_box(&k128), &iv).unwrap()) + }) + }); + group.bench_function("Aes256 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("Aes128 do_encrypt_init, CFB (key schedule + IV)", |b| { + b.iter(|| black_box(Aes128Cfb::::do_encrypt_init(black_box(&k128)).unwrap().1)) + }); + group.bench_function("Aes128 do_decrypt_init, CFB (key schedule only)", |b| { + b.iter(|| { + black_box(Aes128Cfb::::do_decrypt_init(black_box(&k128), &iv).unwrap()) + }) + }); + + group.finish(); +} + +criterion_group!( + benches, bench_aes128, bench_aes256, bench_cfb_aes128, bench_cfb_aes256, bench_ecb_aes128, + bench_init +); +criterion_main!(benches); diff --git a/crypto/modes/src/cbc.rs b/crypto/modes/src/cbc.rs new file mode 100644 index 00000000..a5ea5ce1 --- /dev/null +++ b/crypto/modes/src/cbc.rs @@ -0,0 +1,237 @@ +//! The Cipher Block Chaining mode of operation (NIST SP 800-38A Sec 6.2). +//! +//! # The specification +//! +//! SP 800-38A Sec 6.2 defines the mode as, quoting verbatim: +//! +//! ```text +//! CBC Encryption: C1 = CIPH_K(P1 XOR IV); +//! Cj = CIPH_K(Pj XOR Cj-1) for j = 2 ... n. +//! +//! CBC Decryption: P1 = CIPH^-1_K(C1) XOR IV; +//! Pj = CIPH^-1_K(Cj) XOR Cj-1 for j = 2 ... n. +//! ``` +//! +//! The `j = 1` and `j >= 2` cases differ only in that the first one uses the IV where the others +//! use the previous ciphertext block. So this implementation keeps a single `chain` field holding +//! "whatever gets XORed next", initialised to the IV and replaced by each ciphertext block as it +//! is produced or consumed. That is the equivalence being used, and it is why there is no special +//! case for the first block anywhere below. +//! +//! # 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 eight blocks at a time through +//! [`ElectronicCodeBook::decrypt_blocks8`], then any remaining pair through +//! [`ElectronicCodeBook::decrypt_blocks2`], then the last block singly. A bit-sliced engine +//! computes a pair (AES) or eight blocks (SM4) for barely more than the cost of one. Encryption +//! cannot, and does not. + +use crate::iv::random_iv; +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::traits::{ + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, RNG, + SecurityStrength, +}; +use bouncycastle_rng::HashDRBG_SHA512; +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`. +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, +{ + /// `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_blocks2`] 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_blocks2(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 eight consecutive blocks with one [`ElectronicCodeBook::decrypt_blocks8`] call. + /// + /// The same argument as [`Self::decrypt_pair`], eight wide: `Pj+k = CIPH^-1_K(Cj+k) XOR Cj+k-1` + /// for `k = 0..8`, with `Cj-1` the incoming chaining value. No inverse cipher depends on + /// another's output, so all eight 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+7`. + #[inline] + fn decrypt_eight(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 8]) { + let cts = *blocks; + self.perm.decrypt_blocks8(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` 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( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result<(), SymmetricCipherError> { + for block in blocks.iter_mut() { + self.encrypt_one(block); + } + Ok(()) + } +} + +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` is provided over it). + /// + /// Walks the input in eights through `decrypt_blocks8`, then pairs through `decrypt_blocks2`, + /// 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( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result<(), SymmetricCipherError> { + let (eights, rest) = blocks.as_chunks_mut::<8>(); + for eight in eights.iter_mut() { + self.decrypt_eight(eight); + } + 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(()) + } +} diff --git a/crypto/modes/src/cfb.rs b/crypto/modes/src/cfb.rs new file mode 100644 index 00000000..07efe189 --- /dev/null +++ b/crypto/modes/src/cfb.rs @@ -0,0 +1,301 @@ +//! The Cipher Feedback mode of operation (NIST SP 800-38A Sec 6.3), full-block segment only. +//! +//! # The specification +//! +//! Sec 6.3 defines CFB against a segment size `s` with `1 <= s <= b`, where `b` is the block size. +//! Quoting the equations verbatim: +//! +//! ```text +//! CFB Encryption: I1 = IV; +//! Ij = LSB_{b-s}(I_{j-1}) | C#_{j-1} for j = 2 ... n; +//! Oj = CIPH_K(Ij) for j = 1, 2 ... n; +//! C#_j = P#_j XOR MSB_s(Oj) for j = 1, 2 ... n. +//! +//! CFB Decryption: I1 = IV; +//! Ij = LSB_{b-s}(I_{j-1}) | C#_{j-1} for j = 2 ... n; +//! Oj = CIPH_K(Ij) for j = 1, 2 ... n; +//! P#_j = C#_j XOR MSB_s(Oj) for j = 1, 2 ... n. +//! ``` +//! +//! # This type is the `s = b` specialisation +//! +//! [`Cfb`] implements **only** `s = b`, the variant Sec 6.3 says is "sometimes incorporated into +//! the name of the mode", i.e. CFB128 for a 128-bit block. That is the only segment size which is +//! block-aligned, and so the only one that fits [`BlockCipherEncryptor`] / +//! [`BlockCipherDecryptor`]. Substituting `s = b` collapses the equations exactly: +//! +//! * `LSB_{b-s}(I_{j-1})` becomes `LSB_0(I_{j-1})`, the empty bit string, so the concatenation +//! leaves `Ij = C_{j-1}`. Sec 6.3's alternative description agrees: the previous input block +//! "circularly shift[s] s positions to the left, and then the ciphertext segment replaces the s +//! least significant bits of the result" -- shifting a whole block by its own width and replacing +//! every bit of it is just assignment. +//! * `MSB_s(Oj)` becomes `MSB_b(Oj)`, which is `Oj`. No part of the output block is discarded, so +//! there are no wasted cipher calls: one forward cipher per block, the same as CBC. +//! +//! leaving +//! +//! ```text +//! I1 = IV; Ij = C_{j-1} (j >= 2); Oj = CIPH_K(Ij); Cj = Pj XOR Oj / Pj = Cj XOR Oj +//! ``` +//! +//! As in `Cbc`, the `j = 1` and `j >= 2` cases differ only in what gets fed to the cipher, so a +//! single `chain` field holds `Ij` -- the IV to start with, then each ciphertext block as it is +//! produced or consumed. That is why no code below special-cases the first block. +//! +//! CFB1 and CFB8 (the `s = 1` and `s = 8` variants, which SP 800-38A Appendix F.3 also gives +//! vectors for) are deliberately **not** here: they are not block-aligned, so they belong to a +//! `StreamCipher`-shaped API rather than this one. +//! +//! # Decryption uses the *forward* cipher function +//! +//! This is the thing about CFB that surprises a reader used to CBC: both directions apply +//! `CIPH_K`. Sec 6.3 is explicit -- "In CFB decryption, the IV is the first input block, and each +//! successive input block is formed as in CFB encryption [...] The *forward cipher* function is +//! applied to each input block to produce the output blocks." +//! +//! So [`Cfb`](Cfb) never calls [`ElectronicCodeBook::decrypt_block`] or +//! [`ElectronicCodeBook::decrypt_blocks2`]. A permutation could implement only the forward direction +//! and still work here; `cfb_tests.rs` pins that with a toy whose inverse panics. The mode XORs a +//! keystream in both directions, and the two directions differ only in which of the two buffers +//! becomes the next chaining value. +//! +//! # 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." +//! +//! Constructing them "in series" is trivial here: with `s = b` the input blocks *are* the IV +//! followed by the ciphertext blocks, already in hand. Decryption therefore walks the ciphertext in +//! pairs through [`ElectronicCodeBook::encrypt_blocks2`], which a bit-sliced engine computes for +//! barely more than the cost of one block. Encryption cannot, and does not. + +use crate::iv::random_iv; +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::traits::{ + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, RNG, + SecurityStrength, +}; +use bouncycastle_rng::HashDRBG_SHA512; +use core::marker::PhantomData; + +/// CFB mode over any [`ElectronicCodeBook`], 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. +/// +/// `Dir` is [`Encrypting`] or [`Decrypting`]. [`BlockCipherEncryptor`] is implemented only for the +/// former and [`BlockCipherDecryptor`] 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`. +/// +/// # State +/// +/// The same two fields as `Cbc`, and the same size: the permutation (which owns the key schedule, +/// and is responsible for keeping it in a zeroize-on-drop wrapper) and one block holding `Ij`. `Ij` +/// is an IV or a ciphertext block, 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 `chain` on each call and +/// lives only in a local, so no keystream outlives the call that used it. +pub struct Cfb +where + P: ElectronicCodeBook, +{ + perm: P, + /// `Ij`: the IV, then `C_{j-1}`. See the module docs on why there is only one field for both. + chain: [u8; BLOCK_LEN], + _dir: PhantomData, +} + +impl Cfb +where + P: ElectronicCodeBook, +{ + /// `Oj = CIPH_K(Ij)`, the keystream block for the current position. + /// + /// The forward cipher function, in both directions -- see the module docs. + #[inline] + fn keystream(&self) -> [u8; BLOCK_LEN] { + let mut o = self.chain; + self.perm.encrypt_block(&mut o); + o + } + + /// `Cj = Pj XOR Oj` in place, then `Cj` becomes the next input block. + #[inline] + fn encrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { + let o = self.keystream(); + for (b, o) in block.iter_mut().zip(o.iter()) { + *b ^= *o; + } + // I_{j+1} = Cj. Serial: this is the input to the next cipher call. + self.chain = *block; + } + + /// `Pj = Cj XOR Oj` in place, then `Cj` -- the *ciphertext*, not the recovered plaintext -- + /// becomes the next input block. `Cj` is overwritten by `Pj`, so it is copied first. + #[inline] + fn decrypt_one(&mut self, block: &mut [u8; BLOCK_LEN]) { + // `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 cj = *block; + let o = self.keystream(); + for (b, o) in block.iter_mut().zip(o.iter()) { + *b ^= *o; + } + self.chain = cj; + } + + /// Decrypts two consecutive blocks with one [`ElectronicCodeBook::encrypt_blocks2`] call. + /// + /// Writing the pair as `Cj, Cj+1` with `Ij` the incoming chaining value, the `s = b` equations + /// give + /// + /// ```text + /// Ij = chain 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 chaining value. + /// Decrypts eight consecutive blocks with one [`ElectronicCodeBook::encrypt_blocks8`] call. + /// + /// The same construction as [`Self::decrypt_pair`] widened to eight: the input blocks are the + /// incoming chaining value followed by the first seven ciphertext blocks, all known before any + /// cipher call, so the eight forward ciphers are independent (Sec 6.3's parallel decryption). + /// `I_{j+8} = Cj+7` is read before the XOR turns it into `Pj+7`. + #[inline] + fn decrypt_eight(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 8]) { + let mut o = [ + self.chain, blocks[0], blocks[1], blocks[2], blocks[3], blocks[4], blocks[5], blocks[6], + ]; + self.perm.encrypt_blocks8(&mut o); + self.chain = blocks[7]; + for (block, o) in blocks.iter_mut().zip(o.iter()) { + for (b, o) in block.iter_mut().zip(o.iter()) { + *b ^= *o; + } + } + } + + #[inline] + fn decrypt_pair(&mut self, blocks: &mut [[u8; BLOCK_LEN]; 2]) { + // The two input blocks, constructed in series: Ij (already held) and Ij+1 (= Cj). + let mut o = [self.chain, blocks[0]]; + self.perm.encrypt_blocks2(&mut o); + + // I_{j+2} = Cj+1, read before the XOR below turns it into Pj+1. + self.chain = blocks[1]; + + for (block, o) in blocks.iter_mut().zip(o.iter()) { + for (b, o) in block.iter_mut().zip(o.iter()) { + *b ^= *o; + } + } + } +} + +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 + BlockCipherEncryptor 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 [`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)?; + // `I1 = IV`. + let iv = random_iv::(rng)?; + Ok((Self { perm, chain: iv, _dir: PhantomData }, iv)) + } + + /// The implementor hook (the flat `do_encrypt` is provided over it). + /// + /// Strictly serial: `Oj+1 = CIPH_K(Cj)` and `Cj` is the *output* of the previous cipher call, so + /// there is no pair path here. See the module docs. Never fails: CFB has no per-IV data limit. + fn do_encrypt_blocks( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result<(), SymmetricCipherError> { + for block in blocks.iter_mut() { + self.encrypt_one(block); + } + Ok(()) + } +} + +impl + BlockCipherDecryptor for Cfb +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)?; + // `I1 = IV`, exactly as on the encrypt side. + Ok(Self { perm, chain: *init_data, _dir: PhantomData }) + } + + /// The implementor hook (the flat `do_decrypt` is provided over it). + /// + /// Walks the input in eights through the permutation's *forward* eight-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. Never + /// fails: CFB has no per-IV data limit. + fn do_decrypt_blocks( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result<(), SymmetricCipherError> { + let (eights, rest) = blocks.as_chunks_mut::<8>(); + for eight in eights.iter_mut() { + self.decrypt_eight(eight); + } + 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(()) + } +} diff --git a/crypto/modes/src/ecb.rs b/crypto/modes/src/ecb.rs new file mode 100644 index 00000000..2f69c362 --- /dev/null +++ b/crypto/modes/src/ecb.rs @@ -0,0 +1,185 @@ +//! The Electronic Codebook mode of operation (NIST SP 800-38A Sec 6.1). +//! +//! # The specification +//! +//! Sec 6.1 defines the mode in one equation each way, quoted verbatim: +//! +//! ```text +//! ECB Encryption: Cj = CIPH_K(Pj) for j = 1 ... n. +//! ECB Decryption: Pj = CIPH^-1_K(Cj) for j = 1 ... n. +//! ``` +//! +//! "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." +//! +//! # A mode with no state +//! +//! There is no IV and no chaining: the mode *is* the keyed permutation applied block by block, +//! which is why the permutation trait itself is named [`ElectronicCodeBook`]. What this type adds is +//! the [`BlockCipherEncryptor`] / [`BlockCipherDecryptor`] shape shared with `Cbc` and `Cfb` -- +//! the direction in the type, the streaming and one-shot methods with their compile-time length +//! checks, and the batching -- so ECB can stand wherever the other modes can, including under the +//! padding layer and behind the CLI. Its `INIT_DATA_LEN` is 0: [`BlockCipherEncryptor::do_encrypt_init`] +//! returns an empty array and draws nothing from the RNG, and +//! [`BlockCipherDecryptor::do_decrypt_init`] takes an empty one. +//! +//! # Why it is here at all +//! +//! Sec 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." It is undesirable in nearly every application -- equal plaintext blocks +//! give equal ciphertext blocks, so the structure of the plaintext shows through the ciphertext, and +//! blocks can be reordered, repeated or removed without anything to detect it. ECB is provided for +//! interoperability with systems and specifications that use it, and for driving test vectors; it is +//! not a way to encrypt data. See the crate docs, "Security Considerations". +//! +//! # 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." Unlike CBC and CFB, whose encryption is serial, +//! both directions here batch through the permutation's eight-block and pair methods +//! ([`ElectronicCodeBook::encrypt_blocks8`] / [`ElectronicCodeBook::encrypt_blocks2`] and their +//! inverses), then finish the remaining block singly. + +use crate::{Decrypting, Encrypting}; +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::traits::{ + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, RNG, + SecurityStrength, +}; +use core::marker::PhantomData; + +/// ECB mode over any [`ElectronicCodeBook`], with the direction encoded in the type. +/// +/// **Not a confidentiality mode for data**: see the module docs and the crate'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. +/// +/// There is no initialization data, so `INIT_DATA_LEN == 0`. +/// +/// # 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::

()`. +pub struct Ecb +where + P: ElectronicCodeBook, +{ + perm: P, + _dir: PhantomData

, +} + +impl Ecb +where + P: ElectronicCodeBook, +{ + /// 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)?, [])) + } + + /// As [`BlockCipherEncryptor::do_encrypt_init`]. Nothing is drawn from `rng`: there is no IV to + /// generate, so this exists only to satisfy the trait and is identical to the plain constructor. + fn do_encrypt_init_rng( + key: &KeyMaterial, + _rng: &mut dyn RNG, + ) -> Result<(Self, [u8; 0]), SymmetricCipherError> { + Self::do_encrypt_init(key) + } + + /// The implementor hook (the flat `do_encrypt` 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 eights, 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( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result<(), SymmetricCipherError> { + let (eights, rest) = blocks.as_chunks_mut::<8>(); + for eight in eights.iter_mut() { + self.perm.encrypt_blocks8(eight); + } + let (pairs, tail) = rest.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.perm.encrypt_blocks2(pair); + } + for block in tail.iter_mut() { + self.perm.encrypt_block(block); + } + Ok(()) + } +} + +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` is provided over it): `Pj = CIPH^-1_K(Cj)` for + /// every block, in place -- eights, then pairs, then the remaining block, as on the encrypt + /// side. Never fails. + fn do_decrypt_blocks( + &mut self, + blocks: &mut [[u8; BLOCK_LEN]], + ) -> Result<(), SymmetricCipherError> { + let (eights, rest) = blocks.as_chunks_mut::<8>(); + for eight in eights.iter_mut() { + self.perm.decrypt_blocks8(eight); + } + let (pairs, tail) = rest.as_chunks_mut::<2>(); + for pair in pairs.iter_mut() { + self.perm.decrypt_blocks2(pair); + } + for block in tail.iter_mut() { + self.perm.decrypt_block(block); + } + Ok(()) + } +} diff --git a/crypto/modes/src/iv.rs b/crypto/modes/src/iv.rs new file mode 100644 index 00000000..d2b60c02 --- /dev/null +++ b/crypto/modes/src/iv.rs @@ -0,0 +1,26 @@ +//! 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 on Appendix D. +pub(crate) fn random_iv( + rng: &mut dyn RNG, +) -> Result<[u8; N], SymmetricCipherError> { + let mut iv = [0u8; N]; + // `RNGError` converts into `SymmetricCipherError` via the `From` impl in core::errors. + rng.next_bytes_out(&mut iv)?; + Ok(iv) +} diff --git a/crypto/modes/src/lib.rs b/crypto/modes/src/lib.rs new file mode 100644 index 00000000..1a1a86b3 --- /dev/null +++ b/crypto/modes/src/lib.rs @@ -0,0 +1,393 @@ +//! Block cipher modes of operation (NIST SP 800-38A). +//! +//! A mode turns a keyed block permutation -- `bouncycastle-aes-lowmemory`'s `Aes128` and friends, +//! or anything else implementing [`ElectronicCodeBook`] -- into something that can encrypt more than +//! one block. This crate provides: +//! +//! | Mode | Type | Spec | Notes | +//! |---|---|---|---| +//! | ECB | [`Ecb`] | SP 800-38A Sec 6.1 | Electronic Codebook. **Not confidential for data**; interoperability and test vectors only | +//! | CBC | [`Cbc`] | SP 800-38A Sec 6.2 | Cipher Block Chaining | +//! | CFB | [`Cfb`] | SP 800-38A Sec 6.3 | Cipher Feedback, full-block segment (`s = b`) only | +//! +//! All three are strictly block-aligned. CBC and CFB generate their own IV and differ only in how +//! the block permutation is wired up; the two types have identical APIs and identical size. ECB has +//! no IV at all (`INIT_DATA_LEN = 0`), is one block smaller, and is the raw permutation applied +//! block by block -- see +//! [ECB is not a confidentiality mode for data](#ecb-is-not-a-confidentiality-mode-for-data) and +//! [Choosing between CBC and CFB](#choosing-between-cbc-and-cfb). +//! +//! The crate is deliberately cipher-agnostic: it depends on no concrete block cipher, only on the +//! trait. Define a one-line alias for the combination you use -- or use the ready-made +//! `AES_CBC_128` / `AES_CFB_128` / `AES_ECB_128` and friends from `bouncycastle-aes-lowmemory`: +//! +//! ``` +//! use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; +//! use bouncycastle_modes::{Cbc, Cfb, Ecb}; +//! +//! type Aes128Cbc = Cbc; +//! type Aes192Cbc = Cbc; +//! type Aes256Cbc = Cbc; +//! +//! type Aes128Cfb = Cfb; +//! type Aes192Cfb = Cfb; +//! type Aes256Cfb = Cfb; +//! +//! type Aes128Ecb = Ecb; +//! ``` +//! +//! # Usage Examples +//! +//! The direction is part of the type: [`Cbc`](Cbc) implements +//! [`BlockCipherEncryptor`] and nothing else, and [`Cbc`](Cbc) implements +//! [`BlockCipherDecryptor`] and nothing else. [`Cfb`] is the same. The IV is generated for you and +//! returned; there is no API for supplying your own (see +//! [Security Considerations](#security-considerations)). +//! +//! ``` +//! use bouncycastle_aes_lowmemory::Aes128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +//! use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; +//! +//! type Aes128Cbc = 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 = Aes128Cbc::::encrypt(&key, &mut data).expect("encryption"); +//! assert_ne!(data, plaintext); +//! +//! Aes128Cbc::::decrypt(&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_aes_lowmemory::Aes256; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +//! use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; +//! +//! type Aes256Cbc = Cbc; +//! +//! let key = KeyMaterial::<32>::from_bytes_as_type(&[0x07; 32], KeyType::SymmetricCipherKey) +//! .expect("a 32-byte symmetric cipher key"); +//! +//! let (mut encryptor, iv) = +//! Aes256Cbc::::do_encrypt_init(&key).expect("encrypt init"); +//! let mut first = [0xAAu8; 16]; +//! let mut rest = [0xBBu8; 32]; +//! encryptor.do_encrypt(&mut first).expect("block 1"); +//! encryptor.do_encrypt(&mut rest).expect("blocks 2-3"); +//! +//! let mut decryptor = Aes256Cbc::::do_decrypt_init(&key, &iv).expect("decrypt init"); +//! decryptor.do_decrypt(&mut first).unwrap(); +//! decryptor.do_decrypt(&mut rest).unwrap(); +//! assert_eq!(first, [0xAAu8; 16]); +//! assert_eq!(rest, [0xBBu8; 32]); +//! ``` +//! +//! CFB is a drop-in swap for CBC -- same methods, same IV convention, same block alignment. The +//! only visible difference is the ciphertext: +//! +//! ``` +//! use bouncycastle_aes_lowmemory::Aes128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +//! use bouncycastle_modes::{Cbc, Cfb, Decrypting, Encrypting}; +//! +//! type Aes128Cbc = Cbc; +//! type Aes128Cfb = Cfb; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let plaintext = [0x5Au8; 32]; +//! +//! let mut ciphertext = plaintext; +//! let iv = Aes128Cfb::::encrypt(&key, &mut ciphertext).expect("encryption"); +//! let mut recovered = ciphertext; +//! Aes128Cfb::::decrypt(&key, &iv, &mut recovered).expect("decryption"); +//! assert_eq!(recovered, plaintext); +//! +//! // The modes are not interchangeable: a ciphertext must be decrypted with the mode that +//! // produced it, and nothing at the type level stops you getting that wrong. +//! let mut as_if_cbc = ciphertext; +//! Aes128Cbc::::decrypt(&key, &iv, &mut as_if_cbc).expect("decryption"); +//! assert_ne!(as_if_cbc, plaintext); +//! ``` +//! +//! 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_aes_lowmemory::Aes128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +//! use bouncycastle_modes::{Decrypting, Ecb, Encrypting}; +//! +//! type Aes128Ecb = Ecb; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! let plaintext = [0x5Au8; 32]; // two equal blocks +//! +//! let mut data = plaintext; +//! let no_iv: [u8; 0] = Aes128Ecb::::encrypt(&key, &mut data).expect("encryption"); +//! assert_eq!(data[..16], data[16..], "equal plaintext blocks give equal ciphertext blocks"); +//! +//! Aes128Ecb::::decrypt(&key, &no_iv, &mut data).expect("decryption"); +//! assert_eq!(data, plaintext); +//! ``` +//! +//! Using the wrong direction does not compile: +//! +//! ```compile_fail +//! use bouncycastle_aes_lowmemory::Aes128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::BlockCipherDecryptor; +//! use bouncycastle_modes::{Cbc, Encrypting}; +//! +//! type Aes128Cbc = Cbc; +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); +//! +//! // `Encrypting` does not implement `BlockCipherDecryptor`. +//! let _ = Aes128Cbc::::do_decrypt_init(&key, &[0u8; 16]); +//! ``` +//! +//! # Choosing between CBC and CFB +//! +//! Neither is authenticated, so the honest answer for new designs is "neither -- use an AEAD". ECB +//! is not a candidate for data at all (below). Between the two: +//! +//! * **Error propagation differs**, and it is the sharpest practical difference. SP 800-38A +//! Appendix D, Table D.2: a bit error in `Cj` gives CBC a *randomised* `Pj` plus the **same bit** +//! flipped in `Pj+1`, and gives CFB the **same bit** flipped in `Pj` plus a randomised `Pj+1`. +//! So under CFB an attacker who can flip a ciphertext bit flips the corresponding plaintext bit +//! directly, in the block they targeted. Both are malleable; authenticate the ciphertext. +//! * **CFB needs only the forward cipher function**, in both directions (Sec 6.3). That halves what +//! a permutation has to provide, and where the inverse costs more than the forward direction it +//! makes CFB decryption faster: with `bouncycastle-aes-lowmemory` this crate's benches measure CFB +//! decryption at about 1.37x CBC decryption (AES-128, 16 KiB, `N = 8`). Encryption is the same +//! speed in both, since both are serial and both use only the forward function. +//! * **"CFB" alone is ambiguous.** SP 800-38A's `s = 8` and `s = 1` variants are also called CFB and +//! are *not* interoperable with [`Cfb`], which is `s = b`. If you are matching an existing system, +//! check which segment size it means before assuming this one. CBC has no such ambiguity. +//! * Both encrypt serially and decrypt in parallel, so their scaling with `N` matches. +//! +//! # Block alignment +//! +//! These types are **strictly block-aligned**: whole blocks in, whole blocks out, no finalization +//! step. SP 800-38A Sec 5.2 requires exactly that of ECB and CBC ("For the ECB and CBC modes, the +//! total number of bits in the plaintext must be a multiple of the block size"); for CFB it requires the total to be a multiple +//! of the segment size `s`, and this crate fixes `s = b`, so the requirement is the same. Appendix +//! A puts the formatting of non-aligned data outside the scope of the recommendation. +//! +//! Arbitrary-length data therefore needs a padding layer on top. That layer is *not* in this crate: +//! it is `bouncycastle-padding`, whose `PaddedEncryptor` / `PaddedDecryptor` wrap any +//! [`BlockCipherEncryptor`] / [`BlockCipherDecryptor`] pair, so the modes get arbitrary-length +//! support by being wrapped rather than by growing padding logic of their own. The same adapters +//! over `bouncycastle-padding`'s `NoPadding` give the opposite guarantee -- an unaligned message is +//! an error at `do_final` rather than something padded -- for formats defined on whole blocks. +//! +//! ``` +//! use bouncycastle_aes_lowmemory::Aes128; +//! use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +//! use bouncycastle_core::traits::{SymmetricCipherDecryptor, SymmetricCipherEncryptor}; +//! use bouncycastle_modes::{Cfb, Decrypting, Encrypting}; +//! use bouncycastle_padding::{PKCS7, PaddedDecryptor, PaddedEncryptor}; +//! +//! type Enc = PaddedEncryptor, PKCS7, 16, 16, 16>; +//! type Dec = PaddedDecryptor, PKCS7, 16, 16, 16>; +//! +//! let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) +//! .expect("a 16-byte symmetric cipher key"); +//! +//! // 5 bytes: not a whole block, which the bare mode would refuse to compile. +//! let message = b"hello"; +//! let mut ciphertext = [0u8; 16]; +//! let (iv, written) = Enc::encrypt_out(&key, message, &mut ciphertext).expect("encryption"); +//! assert_eq!(written, 16); +//! +//! let mut plaintext = [0u8; 16]; +//! let n = Dec::decrypt_out(&key, &iv, &ciphertext, &mut plaintext).expect("decryption"); +//! assert_eq!(&plaintext[..n], message); +//! ``` +//! +//! # Memory Usage +//! +//! No heap allocation, and no lookup tables of its own. A CBC or CFB value is the permutation plus +//! one block of chaining value; an ECB value is just the permutation, since nothing chains: +//! +//! ```text +//! size_of::>() == size_of::

() + BLOCK_LEN +//! size_of::>() == size_of::

() + BLOCK_LEN +//! size_of::>() == size_of::

() +//! ``` +//! +//! | Combination | Permutation | Chain | Total | +//! |---|---|---|---| +//! | AES-128 CBC or CFB | 176 B | 16 B | 192 B | +//! | AES-192 CBC or CFB | 208 B | 16 B | 224 B | +//! | AES-256 CBC or CFB | 240 B | 16 B | 256 B | +//! | AES-128 ECB | 176 B | 0 B | 176 B | +//! | AES-192 ECB | 208 B | 0 B | 208 B | +//! | AES-256 ECB | 240 B | 0 B | 240 B | +//! +//! CFB is the same size as CBC because it stores the same thing: one block of input to the next +//! cipher call. Its keystream block `Oj` is recomputed per call and lives only in a local, so it +//! costs `BLOCK_LEN` of transient stack and nothing persistent. +//! +//! The data methods work in place. The pair path in either mode's decryptor adds one +//! `[[u8; BLOCK_LEN]; 2]` copy of the ciphertext it needs for the chaining value. [`Encrypting`] and [`Decrypting`] are zero-sized and held in a +//! `PhantomData`, so encoding the direction in the type is free. The table is pinned by +//! `sizes_match_the_documented_memory_table` in `tests/cbc_tests.rs`, `tests/cfb_tests.rs` and +//! `tests/ecb_tests.rs`. +//! +//! # Security Considerations +//! +//! ## ECB is not a confidentiality mode for data +//! +//! SP 800-38A Sec 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." It is undesirable for data: equal plaintext +//! blocks give equal ciphertext blocks, so patterns in the plaintext show through the ciphertext; +//! the same message encrypts to the same ciphertext every time, so an observer learns when a message +//! repeats; and with nothing tying blocks together, ciphertext blocks can be reordered, duplicated or +//! deleted, or spliced in from another message under the same key, and the result decrypts to +//! plaintext that looks valid block by block. +//! +//! [`Ecb`] is in this crate because ECB is what some specifications and existing systems require -- +//! a raw permutation exposed through the same mode API as the others, so that a key-wrapping scheme, +//! a legacy protocol or a test-vector harness can use it -- and because it is the natural way to +//! drive an [`ElectronicCodeBook`] implementation's known-answer tests. Do not use it to encrypt +//! data. If you find yourself reaching for it because it needs no IV, that is the problem the IV +//! solves. +//! +//! ## None of the modes is authenticated +//! +//! All three provide, at best, confidentiality only. None detects tampering, and each is malleable +//! in specific, exploitable ways -- SP 800-38A Appendix D, Table D.2: +//! +//! * **ECB:** flipping a bit of `Cj` randomises the decryption of `Cj` and nothing else, and whole +//! blocks can be reordered, repeated or dropped undetectably (above). +//! * **CBC:** flipping a bit of `Cj` flips the same bit of the decryption of `Cj+1`, and randomises +//! the decryption of `Cj` itself. +//! * **CFB:** flipping a bit of `Cj` flips the same bit of the decryption of `Cj` -- the block the +//! attacker aimed at -- and randomises the decryption of `Cj+1`. So the controlled flip lands in +//! the targeted block rather than the next one. +//! +//! **Authenticate the ciphertext.** Prefer an AEAD; if you must use either of these, MAC the +//! ciphertext *and* the IV, and verify before decrypting. +//! +//! Combining decryption with a padding check is the classic padding-oracle setup, for either mode. +//! Do not report padding failures distinguishably, and do not decrypt unauthenticated ciphertext. +//! `bouncycastle-padding`'s `unpad` is constant-time for exactly this reason, but constant-time +//! unpadding is not a substitute for authentication. +//! +//! ## The IV must be unpredictable, and this crate generates it +//! +//! (ECB has no IV; Table D.2 lists its IV column as "Not applicable". This section is about CBC and +//! CFB.) +//! +//! SP 800-38A Sec 5.3 requires that "for the CBC and CFB modes, the IV for any particular execution +//! of the encryption process must be unpredictable" -- not merely unique. Appendix C spells out +//! that "for any given plaintext, it must not be possible to predict the IV that will be associated +//! to the plaintext in advance of the generation of the IV". +//! +//! Rather than accept an IV and hope, [`BlockCipherEncryptor::do_encrypt_init`] generates one from +//! the library's default OS-backed DRBG and returns it. There is deliberately **no** API for +//! supplying your own. Known-answer tests drive [`BlockCipherEncryptor::do_encrypt_init_rng`] with +//! a fixed-output test RNG instead. +//! +//! ## IV integrity +//! +//! 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 `P1`. +//! +//! CFB damages `P1` too, but unpredictably rather than controllably: the IV is the first thing fed +//! to the cipher, so Table D.2 gives *random* bit errors in the decryption of `C1` -- and, because +//! this crate fixes `s = b`, in `C1` only (Appendix D's "the first `i/s` (rounding up) ciphertext +//! segments" is one segment when `s = b`). Later blocks are unaffected in both modes. +//! +//! Either way the IV need not be secret, but it must be authenticated along with the ciphertext. +//! +//! ## Key and IV reuse +//! +//! Nothing here stops one key being used for many messages, which is fine for either mode provided +//! each gets a fresh unpredictable IV. It is the IV, not the key, that must not repeat. +//! +//! Repeating one matters more for CFB. CFB XORs a keystream, so two messages encrypted under the +//! same key *and* IV satisfy `C1 XOR C1' == P1 XOR P1'` -- the plaintext XOR leaks directly, the +//! classic two-time-pad failure, and it continues into later blocks for as long as the two +//! ciphertexts agree. CBC under a repeated IV leaks only whether the blocks were equal, not their +//! XOR. Since [`BlockCipherEncryptor::do_encrypt_init`] draws every IV from the DRBG, neither case +//! arises through this API; it is a reason not to add an IV-accepting one. +//! +//! # Not yet implemented +//! +//! * **The CFB segment sizes below the block size** (`s = 1` and `s = 8`, for which SP 800-38A +//! Appendix F.3 also gives vectors). They are not block-aligned, so they need a +//! `StreamCipher`-shaped API rather than [`BlockCipherEncryptor`]. +//! * **OFB and CTR**, the remaining two modes of the recommendation. Both are keystream modes and, +//! like CFB1/8, do not require block alignment. +//! +//! # Command line +//! +//! The `bc-rust` CLI exposes all three modes for all three AES key lengths: `aes128-cbc`, +//! `aes192-cbc`, `aes256-cbc`, `aes128-cfb`, `aes192-cfb`, `aes256-cfb`, `aes128-ecb`, +//! `aes192-ecb` and `aes256-ecb`, each taking `encrypt` or `decrypt` and streaming stdin to +//! stdout. For CBC and CFB there is no API for a caller-supplied IV, so `encrypt` writes the +//! generated IV as the first block of its output and `decrypt` reads it back from the first block +//! of its input, so the two compose; the `-ecb` commands have no IV and write and read none: +//! +//! ```text +//! bc-rust aes256-cbc encrypt --key-file k.bin < plain.bin > cipher.bin +//! bc-rust aes256-cbc decrypt --key-file k.bin < cipher.bin | cmp - plain.bin +//! +//! bc-rust aes256-cfb encrypt --key-file k.bin < plain.bin > cipher.bin +//! bc-rust aes256-cfb decrypt --key-file k.bin < cipher.bin | cmp - plain.bin +//! +//! bc-rust aes128-ecb encrypt --key-file k.bin < plain.bin > cipher.bin # same length out as in +//! ``` +//! +//! The `-cfb` commands are CFB128, matching [`Cfb`]. Input must be block-aligned for every command, +//! for the reason given above. + +#![no_std] +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod cbc; +mod cfb; +mod ecb; +mod iv; + +pub use cbc::Cbc; +pub use cfb::Cfb; +pub use ecb::Ecb; + +// Imports needed for docs +#[allow(unused_imports)] +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook}; +// end of imports needed for docs + +/// Direction marker for a mode that encrypts. See [`Cbc`], [`Cfb`] and [`Ecb`]. +/// +/// Zero-sized: encoding the direction in the type costs no memory. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Encrypting; + +/// Direction marker for a mode that decrypts. See [`Cbc`], [`Cfb`] and [`Ecb`]. +/// +/// Zero-sized: encoding the direction in the type costs no memory. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Decrypting; diff --git a/crypto/modes/tests/acvp_cfb_tests.rs b/crypto/modes/tests/acvp_cfb_tests.rs new file mode 100644 index 00000000..97e84bff --- /dev/null +++ b/crypto/modes/tests/acvp_cfb_tests.rs @@ -0,0 +1,327 @@ +//! 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-lowmemory` and AES-CBC suites -- +//! `cargo test` must stay green for someone who has only cloned this repository. +//! +//! This is the CFB counterpart to `acvp_tests.rs` (AES-CBC) and to +//! `crypto/aes-lowmemory/tests/acvp_tests.rs` (AES-ECB, the raw permutation). The `CFB128` file is +//! the one that matches [`Cfb`]: `ACVP-AES-CFB8` and `ACVP-AES-CFB1` are the sub-block segment +//! sizes this crate does not implement, and are 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 **three times**: block by +//! block, in pairs with a one-block remainder for odd lengths, and as one hook call over the whole +//! payload. The second and third passes are what put the multi-block cases through the pair and +//! eight-block paths -- which for CFB are [`ElectronicCodeBook::encrypt_blocks2`] and +//! [`ElectronicCodeBook::encrypt_blocks8`], the *forward* function, even on the decrypt side -- so +//! they are exercised against real vectors and not only against the toys in `cfb_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_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, SecurityStrength, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Cfb, Decrypting, Encrypting}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +const BLOCK_LEN: usize = 16; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = [ + "../../../bc-test-data/crypto/aes_tdes_vectors/AES", + "../bc-test-data/crypto/aes_tdes_vectors/AES", +]; + +const REQUEST_FILE: &str = "ACVP-AES-CFB128.4014530.req.json"; +const RESPONSE_FILE: &str = "ACVP-AES-CFB128.4014530.rsp.json"; + +fn test_data_dir() -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate); + if path.join(REQUEST_FILE).exists() && path.join(RESPONSE_FILE).exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); \ + ACVP AES-CFB128 tests will be skipped" + ); + None +} + +/// Builds a `KeyMaterial` from raw ACVP key bytes, including the all-zero keys. +/// +/// The ACVP set deliberately includes 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. +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 +} + +/// 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, + /// The whole payload in one hook call: eights, then pairs, then the remaining block. The cases + /// spanning 8 to 10 blocks are the ones that reach `encrypt_blocks8`. + Whole, +} + +/// 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; 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) = 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"); + + match grouping { + Grouping::Single => { + for block in input { + let mut c = *block; + enc.do_encrypt(&mut c).unwrap(); + out.push(c); + } + } + Grouping::Whole => { + let mut all = input.to_vec(); + enc.do_encrypt_blocks(&mut all).unwrap(); + out.extend_from_slice(&all); + } + Grouping::Pairs => { + let (pairs, tail) = input.as_chunks::<2>(); + for pair in pairs { + let mut c = *pair; + enc.do_encrypt_blocks(&mut c).unwrap(); + out.extend_from_slice(&c); + } + for block in tail { + let mut c = *block; + enc.do_encrypt(&mut c).unwrap(); + out.push(c); + } + } + } + } else { + let mut dec = + Cfb::::do_decrypt_init(&key, &iv).expect("dec init"); + + match grouping { + Grouping::Single => { + for block in input { + let mut p = *block; + dec.do_decrypt(&mut p).unwrap(); + out.push(p); + } + } + Grouping::Whole => { + let mut all = input.to_vec(); + dec.do_decrypt_blocks(&mut all).unwrap(); + out.extend_from_slice(&all); + } + Grouping::Pairs => { + let (pairs, tail) = input.as_chunks::<2>(); + for pair in pairs { + let mut p = *pair; + dec.do_decrypt_blocks(&mut p).unwrap(); + out.extend_from_slice(&p); + } + for block in tail { + let mut p = *block; + dec.do_decrypt(&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 CFB128 payloads are block-aligned"); + bytes.chunks(BLOCK_LEN).map(|c| c.try_into().unwrap()).collect() +} + +fn decode(value: &Value, field: &str, tc_id: u64) -> Vec { + let s = value + .get(field) + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("tcId {tc_id}: missing field {field}")); + hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc_id}: bad hex in {field}")) +} + +#[test] +fn acvp_aes_cfb128_known_answer_tests() { + let Some(dir) = test_data_dir() else { return }; + + let req: Value = serde_json::from_str( + &fs::read_to_string(dir.join(REQUEST_FILE)).expect("readable request file"), + ) + .expect("valid ACVP request JSON"); + let rsp: Value = serde_json::from_str( + &fs::read_to_string(dir.join(RESPONSE_FILE)).expect("readable response file"), + ) + .expect("valid ACVP response JSON"); + + // The response file carries only the answer, against a tcId. Index it. + let mut answers: BTreeMap = BTreeMap::new(); + for group in rsp + .get(1) + .and_then(|s| s.get("testGroups")) + .and_then(Value::as_array) + .expect("response testGroups") + { + for test in group.get("tests").and_then(Value::as_array).expect("response tests") { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + answers.insert(tc_id, test.clone()); + } + } + + let groups = req + .get(1) + .and_then(|s| s.get("testGroups")) + .and_then(Value::as_array) + .expect("request testGroups"); + + let mut 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 = decode(test, "key", tc_id); + let iv: [u8; BLOCK_LEN] = decode(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(&decode(test, input_field, tc_id)); + let expected = to_blocks(&decode(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, Grouping::Whole] { + 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() + ); + } + + *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 three 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/modes/tests/acvp_ecb_tests.rs b/crypto/modes/tests/acvp_ecb_tests.rs new file mode 100644 index 00000000..e33d0593 --- /dev/null +++ b/crypto/modes/tests/acvp_ecb_tests.rs @@ -0,0 +1,221 @@ +//! Known-answer tests against the NIST ACVP `ACVP-AES-ECB` vectors from the `bc-test-data` repo, +//! driven through [`Ecb`] -- the mode API -- rather than the raw permutation. +//! +//! 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. +//! +//! `crypto/aes-lowmemory/tests/acvp_tests.rs` runs the same file against the permutation's block +//! methods; this file is what pins that the mode adds nothing and loses nothing on the way: every +//! case is run through the `BlockCipherEncryptor` / `BlockCipherDecryptor` API in three groupings +//! -- block by block, in pairs with a remainder, and the whole payload in one hook call (which for +//! the 8-to-10-block cases reaches the eight-block path) -- in both directions. +//! +//! Unlike the CBC and CFB response files, the ECB one records `key`, `pt` and `ct` for every case, +//! so it is read alone and each case is checked in both directions regardless of its group's +//! declared direction. The MCT (Monte Carlo) groups carry a `resultsArray` defined by the ACVP AES +//! specification rather than SP 800-38A and are skipped, with the count reported. + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, SecurityStrength, +}; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Decrypting, Ecb, Encrypting}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +const BLOCK_LEN: usize = 16; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = [ + "../../../bc-test-data/crypto/aes_tdes_vectors/AES", + "../bc-test-data/crypto/aes_tdes_vectors/AES", +]; + +const RESPONSE_FILE: &str = "ACVP-AES-ECB.4014527.rsp.json"; + +fn test_data_dir() -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate); + if path.join(RESPONSE_FILE).exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); \ + ACVP AES-ECB mode tests will be skipped" + ); + None +} + +/// Builds a `KeyMaterial` from raw ACVP key bytes, including the all-zero keys the set contains. +fn cipher_key(bytes: &[u8]) -> KeyMaterial { + assert_eq!(bytes.len(), N, "key length should match the 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 +} + +/// How to walk the blocks of one case. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +enum Grouping { + /// One block per call. + Single, + /// Two blocks per call, with a one-block remainder for odd lengths. + Pairs, + /// The whole payload in one hook call: eights, then pairs, then the remainder. + Whole, +} + +fn run_case( + key_bytes: &[u8], + input: &[[u8; BLOCK_LEN]], + encrypt: bool, + grouping: Grouping, +) -> Vec<[u8; BLOCK_LEN]> +where + P: ElectronicCodeBook, +{ + let key = cipher_key::(key_bytes); + let mut out = input.to_vec(); + + // Both directions have the same shape; `step` applies the right one to a slice of blocks. + let mut enc = encrypt + .then(|| Ecb::::do_encrypt_init(&key).expect("init").0); + let mut dec = (!encrypt).then(|| { + Ecb::::do_decrypt_init(&key, &[]).expect("init") + }); + let mut step = |blocks: &mut [[u8; BLOCK_LEN]]| { + if let Some(e) = enc.as_mut() { + e.do_encrypt_blocks(blocks).unwrap(); + } else { + dec.as_mut().unwrap().do_decrypt_blocks(blocks).unwrap(); + } + }; + + match grouping { + Grouping::Single => { + for block in out.iter_mut() { + step(core::slice::from_mut(block)); + } + } + Grouping::Pairs => { + let (pairs, tail) = out.as_chunks_mut::<2>(); + for pair in pairs { + step(pair); + } + step(tail); + } + Grouping::Whole => step(&mut out), + } + out +} + +fn run_case_for_key_len( + key_bytes: &[u8], + input: &[[u8; BLOCK_LEN]], + encrypt: bool, + grouping: Grouping, +) -> Vec<[u8; BLOCK_LEN]> { + match key_bytes.len() { + 16 => run_case::(key_bytes, input, encrypt, grouping), + 24 => run_case::(key_bytes, input, encrypt, grouping), + 32 => run_case::(key_bytes, 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 ECB payloads are block-aligned"); + bytes.chunks(BLOCK_LEN).map(|c| c.try_into().unwrap()).collect() +} + +#[test] +fn acvp_aes_ecb_through_the_mode_api() { + let Some(dir) = test_data_dir() else { return }; + + let parsed: Value = serde_json::from_str( + &fs::read_to_string(dir.join(RESPONSE_FILE)).expect("readable response file"), + ) + .expect("valid ACVP JSON"); + 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 multi_block = 0usize; + let mut eight_or_more = 0usize; + let mut skipped_mct = 0usize; + let mut per_key_len: BTreeMap = BTreeMap::new(); + + for group in groups { + for test in group.get("tests").and_then(Value::as_array).expect("tests array") { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + 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 = to_blocks(&get("pt")); + let ct = to_blocks(&get("ct")); + assert_eq!(pt.len(), ct.len(), "tcId {tc_id}: pt and ct differ in length"); + multi_block += usize::from(pt.len() > 1); + eight_or_more += usize::from(pt.len() >= 8); + + for grouping in [Grouping::Single, Grouping::Pairs, Grouping::Whole] { + assert_eq!( + run_case_for_key_len(&key, &pt, true, grouping), + ct, + "tcId {tc_id}: AES-{} ECB encrypt, {} blocks, {grouping:?}", + key.len() * 8, + pt.len() + ); + assert_eq!( + run_case_for_key_len(&key, &ct, false, grouping), + pt, + "tcId {tc_id}: AES-{} ECB decrypt, {} blocks, {grouping:?}", + key.len() * 8, + pt.len() + ); + } + *per_key_len.entry(key.len() * 8).or_default() += 1; + checked += 1; + } + } + + for (bits, n) in &per_key_len { + println!("ACVP AES-ECB via Ecb, AES-{bits}: {n} cases, both directions"); + } + println!( + "ACVP AES-ECB via Ecb: {checked} AFT cases checked in three groupings each \ + ({multi_block} multi-block, {eight_or_more} of eight or more blocks); {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!(eight_or_more > 0, "expected cases that reach the eight-block path"); + assert_eq!(per_key_len.len(), 3, "expected all three key lengths"); +} diff --git a/crypto/modes/tests/acvp_tests.rs b/crypto/modes/tests/acvp_tests.rs new file mode 100644 index 00000000..37b48d96 --- /dev/null +++ b/crypto/modes/tests/acvp_tests.rs @@ -0,0 +1,311 @@ +//! 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-lowmemory` suites -- `cargo test` +//! must stay green for someone who has only cloned this repository. +//! +//! These are the counterpart to `crypto/aes-lowmemory/tests/acvp_tests.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_blocks2`, 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_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{ + KeyMaterial, KeyMaterialTrait, KeyType, do_hazardous_operations, +}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, SecurityStrength, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +const BLOCK_LEN: usize = 16; + +/// Candidate locations, covering `cargo test` run from the crate root or from the repo root. +const TEST_DATA_PATHS: [&str; 2] = [ + "../../../bc-test-data/crypto/aes_tdes_vectors/AES", + "../bc-test-data/crypto/aes_tdes_vectors/AES", +]; + +const REQUEST_FILE: &str = "ACVP-AES-CBC.4014528.req.json"; +const RESPONSE_FILE: &str = "ACVP-AES-CBC.4014528.rsp.json"; + +fn test_data_dir() -> Option { + for candidate in TEST_DATA_PATHS { + let path = Path::new(candidate); + if path.join(REQUEST_FILE).exists() && path.join(RESPONSE_FILE).exists() { + return Some(path.to_path_buf()); + } + } + println!( + "WARNING: bc-test-data not found (looked in {TEST_DATA_PATHS:?}); \ + ACVP AES-CBC tests will be skipped" + ); + None +} + +/// Builds a `KeyMaterial` from raw ACVP key bytes, including the all-zero keys. +/// +/// The ACVP set deliberately includes 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. +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 +} + +/// 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(&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(&mut c).unwrap(); + out.extend_from_slice(&c); + } + for block in tail { + let mut c = *block; + enc.do_encrypt(&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(&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(&mut p).unwrap(); + out.extend_from_slice(&p); + } + for block in tail { + let mut p = *block; + dec.do_decrypt(&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() +} + +fn decode(value: &Value, field: &str, tc_id: u64) -> Vec { + let s = value + .get(field) + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("tcId {tc_id}: missing field {field}")); + hex::decode(s).unwrap_or_else(|_| panic!("tcId {tc_id}: bad hex in {field}")) +} + +#[test] +fn acvp_aes_cbc_known_answer_tests() { + let Some(dir) = test_data_dir() else { return }; + + let req: Value = serde_json::from_str( + &fs::read_to_string(dir.join(REQUEST_FILE)).expect("readable request file"), + ) + .expect("valid ACVP request JSON"); + let rsp: Value = serde_json::from_str( + &fs::read_to_string(dir.join(RESPONSE_FILE)).expect("readable response file"), + ) + .expect("valid ACVP response JSON"); + + // The response file carries only the answer, against a tcId. Index it. + let mut answers: BTreeMap = BTreeMap::new(); + for group in rsp + .get(1) + .and_then(|s| s.get("testGroups")) + .and_then(Value::as_array) + .expect("response testGroups") + { + for test in group.get("tests").and_then(Value::as_array).expect("response tests") { + let tc_id = test.get("tcId").and_then(Value::as_u64).expect("tcId"); + answers.insert(tc_id, test.clone()); + } + } + + let groups = req + .get(1) + .and_then(|s| s.get("testGroups")) + .and_then(Value::as_array) + .expect("request testGroups"); + + let mut 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 = decode(test, "key", tc_id); + let iv: [u8; BLOCK_LEN] = decode(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(&decode(test, input_field, tc_id)); + let expected = to_blocks(&decode(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/modes/tests/cbc_tests.rs b/crypto/modes/tests/cbc_tests.rs new file mode 100644 index 00000000..28185e83 --- /dev/null +++ b/crypto/modes/tests/cbc_tests.rs @@ -0,0 +1,435 @@ +//! 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 `sp800_38a_tests.rs`. + +mod common; + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor}; +use bouncycastle_core_test_framework::electronic_code_book::TestFrameworkElectronicCodeBook; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkBlockCipher; +use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; +use common::{SwappedEightToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; + +type ToyCbc

= Cbc; +type SwappedCbc = Cbc; +type SwappedEightCbc = Cbc; + +/// The implementor hook `do_encrypt_blocks`, 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(&mut blocks).unwrap(); + blocks +} + +/// The implementor hook `do_decrypt_blocks`, 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(&mut blocks).unwrap(); + blocks +} + +/// The flat streaming method `do_encrypt`, by value. +fn enc_flat( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *plaintext; + enc.do_encrypt(&mut data).unwrap(); + data +} + +/// The flat streaming method `do_decrypt`, by value. +fn dec_flat( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *ciphertext; + dec.do_decrypt(&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. + 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` 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_blocks2` 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_blocks2" + ); + + // 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 eight-block path in `do_decrypt_blocks` must actually be taken, and only for full eights. +/// +/// [`SwappedEightToy`] returns its eight results rotated while its pair and single-block methods +/// are correct. So a CBC decryptor that uses `decrypt_blocks8` gives the wrong answer for eight +/// blocks handed over together, and the right answer for the same eight blocks handed over as +/// two fours (pairs) or one at a time. Nine blocks are wrong too: eight, then one. +#[test] +fn the_eight_block_path_is_really_used() { + let key = toy_key(); + let plaintext: [[u8; TOY_LEN]; 9] = core::array::from_fn(|i| [0x10 * i as u8 + 1; TOY_LEN]); + + // The correct toy round-trips nine 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-eight toy encrypts identically (encryption is serial and never batches)... + let (mut enc, iv) = SwappedEightCbc::::do_encrypt_init(&key).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + + // ...but decrypting nine together must be wrong, because the first eight take the eight path. + let mut dec = SwappedEightCbc::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!( + dec_blocks(&mut dec, &ct), + plaintext, + "eight blocks must go through decrypt_blocks8" + ); + + // Exactly eight together is wrong for the same reason. + let eight: [[u8; TOY_LEN]; 8] = ct[..8].try_into().unwrap(); + let mut dec = SwappedEightCbc::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!(&dec_blocks(&mut dec, &eight)[..], &plaintext[..8]); + + // Two fours go through the pair path and are correct; so is the ninth block on its own. + let mut dec = SwappedEightCbc::::do_decrypt_init(&key, &iv).unwrap(); + let first: [[u8; TOY_LEN]; 4] = ct[..4].try_into().unwrap(); + let second: [[u8; TOY_LEN]; 4] = ct[4..8].try_into().unwrap(); + assert_eq!( + &dec_blocks(&mut dec, &first)[..], + &plaintext[..4], + "fewer than eight must not batch" + ); + assert_eq!(&dec_blocks(&mut dec, &second)[..], &plaintext[4..8]); + assert_eq!(dec_flat(&mut dec, &ct[8]), plaintext[8]); +} + +/// 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(&key, &mut first).unwrap(); + let mut second = plaintext; + ToyCbc::::encrypt(&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()); +} + +// ---- memory ------------------------------------------------------------------------------ + +/// Pins the "Memory Usage" table in the crate docs. +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::size_of; + + assert_eq!(size_of::>(), 176 + 16); + assert_eq!(size_of::>(), 208 + 16); + assert_eq!(size_of::>(), 240 + 16); + + // The direction marker is free, and does not change the layout. + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!(size_of::(), 0); + assert_eq!(size_of::(), 0); + + // ...and the general rule the docs state. + assert_eq!(size_of::>(), size_of::() + 16); +} + +/// 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(&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(&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(&key, &mut pinned_rng(), &mut buf).unwrap(); + assert_eq!(buf, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); + ToyCbc::::decrypt(&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(&key, &mut buf).unwrap(); + assert_ne!(buf, flat3); + ToyCbc::::decrypt(&key, &iv_fresh, &mut buf).unwrap(); + assert_eq!(buf, flat3); +} diff --git a/crypto/modes/tests/cfb_tests.rs b/crypto/modes/tests/cfb_tests.rs new file mode 100644 index 00000000..6042c0f3 --- /dev/null +++ b/crypto/modes/tests/cfb_tests.rs @@ -0,0 +1,678 @@ +//! Structural tests for CFB, driven by a toy permutation. +//! +//! These check the properties of the *mode* -- the keystream construction, chaining, call +//! sequencing, the pair/remainder split, 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 `sp800_38a_cfb_tests.rs`, +//! and the ACVP CFB128 set is in `acvp_cfb_tests.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. + +mod common; + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkBlockCipher; +use bouncycastle_modes::{Cbc, Cfb, Decrypting, Encrypting}; +use bouncycastle_padding::{PKCS7, PaddedDecryptor, PaddedEncryptor}; +use common::{ForwardOnlyToy, SwappedEightToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; + +type ToyCfb = Cfb; +type SwappedCfb = Cfb; +type ForwardOnlyCfb = Cfb; +type SwappedEightCfb = Cfb; + +/// The implementor hook `do_encrypt_blocks`, 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(&mut blocks).unwrap(); + blocks +} + +/// The implementor hook `do_decrypt_blocks`, 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(&mut blocks).unwrap(); + blocks +} + +/// The flat streaming method `do_encrypt`, by value. +fn enc_flat( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *plaintext; + enc.do_encrypt(&mut data).unwrap(); + data +} + +/// The flat streaming method `do_decrypt`, by value. +fn dec_flat( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *ciphertext; + dec.do_decrypt(&mut data).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) +} + +// ---- the mode against the shared framework ------------------------------------------------ + +#[test] +fn cfb_conforms_to_the_block_cipher_framework() { + TestFrameworkBlockCipher::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 +/// ``` +/// +/// 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; TOY_LEN]], + encrypt: bool, +) -> Vec<[u8; TOY_LEN]> { + let mut chain = iv; // I1 = IV + let mut out = Vec::with_capacity(input.len()); + for block in input { + let mut o = chain; + perm.encrypt_block(&mut o); // Oj = CIPH_K(Ij) + let result: [u8; TOY_LEN] = core::array::from_fn(|k| block[k] ^ o[k]); + // I_{j+1} is always the *ciphertext* block, whichever direction we are going. + chain = if encrypt { result } else { *block }; + out.push(result); + } + out +} + +/// The mode must reproduce the Sec 6.3 equations exactly, in both directions. +/// +/// 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(); + 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, got_iv) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(got_iv, iv, "the pinned RNG should reproduce the IV"); + let ct = enc_blocks(&mut enc, &plaintext); + + assert_eq!( + ct.to_vec(), + reference_cfb(&perm, iv, &plaintext, true), + "encryption must match the Sec 6.3 equations" + ); + + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let recovered = dec_blocks(&mut dec, &ct); + assert_eq!(recovered, plaintext, "round trip"); + assert_eq!( + recovered.to_vec(), + reference_cfb(&perm, iv, &ct, false), + "decryption must match the Sec 6.3 equations" + ); + + // Anchor 1: `O1 = CIPH_K(IV)` and `C1 = P1 XOR O1`. + let mut o1 = iv; + perm.encrypt_block(&mut o1); + let expected_c1: [u8; TOY_LEN] = core::array::from_fn(|k| plaintext[0][k] ^ o1[k]); + assert_eq!(ct[0], 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. + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(enc_flat(&mut enc, &[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(); + assert_ne!(enc_flat(&mut cbc, &plaintext[0]), ct[0], "CFB must not agree with CBC"); +} + +// ---- 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 both `decrypt_block` and `decrypt_blocks2`, so this test fails +/// loudly if either direction of the mode ever reaches the inverse cipher. Both the pair path (even +/// `N`) and the single-block path are exercised, 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: [[u8; TOY_LEN]; 4] = + core::array::from_fn(|i| core::array::from_fn(|j| (i * 17 + j) as u8)); + + let (mut enc, _) = + ForwardOnlyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + + // The pair path: N = 4 is two pairs, so `encrypt_blocks2` is used and `decrypt_blocks2` is not. + let mut dec = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &ct), plaintext, "pair path, forward cipher only"); + + // The single-block path. + let mut dec = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); + for (c, p) in ct.iter().zip(plaintext.iter()) { + assert_eq!(&dec_flat(&mut dec, c), p, "single-block path, forward cipher only"); + } + + // N = 3 leaves a remainder after the pair loop, so both paths run in one call. + let mut dec = ForwardOnlyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let three = dec_blocks(&mut dec, &[ct[0], ct[1], ct[2]]); + assert_eq!(three, [plaintext[0], plaintext[1], plaintext[2]], "pairs + remainder"); + + // The forward-only toy must agree with the real one, or the above proves nothing. + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(enc_blocks(&mut enc, &plaintext), ct, "the two toys must agree going forward"); +} + +/// The decryptor must feed the **ciphertext** block 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. +#[test] +fn the_decryptor_chains_on_ciphertext_not_plaintext() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = [[0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; + + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + assert_ne!(ct[0], plaintext[0], "the two feedback choices must actually differ here"); + + let (mut wrong, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let out = enc_blocks(&mut wrong, &ct); + + assert_eq!(out[0], plaintext[0], "block 1 cannot tell the two apart"); + assert_ne!(out[1], plaintext[1], "block 2 must, so the feedback source is pinned"); +} + +// ---- 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 CFB it is entirely about `Ij` 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 iv = pinned_iv(); + let plaintext: [[u8; TOY_LEN]; 8] = + core::array::from_fn(|i| core::array::from_fn(|j| (i * TOY_LEN + j) as u8)); + + // Reference: all eight blocks in one call. + let (mut enc, got_iv) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).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, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).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 = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &ct), plaintext); + + for grouping in [1usize, 2, 4] { + let mut dec = ToyCfb::::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 = ToyCfb::::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` must actually be taken. +/// +/// [`SwappedPairToy`] returns its two pair results in the wrong order while its single-block methods +/// are correct. CFB decryption pairs through `encrypt_blocks2`, so with this permutation a pair +/// comes out wrong and a lone block comes out right. 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 iv = pinned_iv(); + let plaintext = [[0xA5u8; TOY_LEN], [0x5Au8; TOY_LEN]]; + + // The correct toy round-trips. + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &ct), plaintext); + + // The swapped-pair toy encrypts identically -- CFB encryption is serial and never pairs, so its + // `encrypt_blocks2` override is not reached from the encryptor at all. + let (mut enc, _) = + SwappedCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let swapped_ct = enc_blocks(&mut enc, &plaintext); + assert_eq!(swapped_ct, 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 dec = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!( + dec_blocks(&mut dec, &swapped_ct), + plaintext, + "decrypting a pair must go through encrypt_blocks2" + ); + + // Decrypting one block at a time avoids the pair path, so it is correct even for this toy. + let mut dec = SwappedCfb::::do_decrypt_init(&key, &iv).unwrap(); + let p0 = dec_flat(&mut dec, &swapped_ct[0]); + let p1 = dec_flat(&mut dec, &swapped_ct[1]); + assert_eq!([p0, p1], plaintext, "the single-block path must not pair"); +} + +/// The eight-block path in `do_decrypt_blocks` must actually be taken, and only for full eights. +/// +/// [`SwappedEightToy`] returns its eight `encrypt_blocks8` results rotated while its pair and +/// single-block methods are correct. CFB decryption batches eights through the *forward* +/// `encrypt_blocks8`, so with this permutation nine blocks handed over together decrypt wrongly +/// (eight rotated, then one), while the same blocks handed over as two fours (pairs) or one at a +/// time decrypt correctly. Encryption is serial and never batches, so it is unaffected. +#[test] +fn the_eight_block_path_is_really_used() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext: [[u8; TOY_LEN]; 9] = core::array::from_fn(|i| [0x10 * i as u8 + 1; TOY_LEN]); + + // The correct toy round-trips nine blocks. + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &ct), plaintext); + + // The rotated-eight toy encrypts identically: CFB encryption is serial and never batches. + let (mut enc, _) = + SwappedEightCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + assert_eq!(enc_blocks(&mut enc, &plaintext), ct, "CFB encryption must not use the eight path"); + + // ...but nine blocks together must now be wrong, because the first eight go through + // encrypt_blocks8. + let mut dec = SwappedEightCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_ne!(dec_blocks(&mut dec, &ct), plaintext, "nine blocks must go through encrypt_blocks8"); + + // Two fours use the pair path only, so they are correct even for this toy... + let mut dec = SwappedEightCfb::::do_decrypt_init(&key, &iv).unwrap(); + let first = dec_blocks(&mut dec, &[ct[0], ct[1], ct[2], ct[3]]); + let second = dec_blocks(&mut dec, &[ct[4], ct[5], ct[6], ct[7]]); + assert_eq!( + [first, second].as_flattened(), + &plaintext[..8], + "fours must not use the eight path" + ); + + // ...and so is one block at a time. + let mut dec = SwappedEightCfb::::do_decrypt_init(&key, &iv).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"); + } +} + +/// 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 iv = pinned_iv(); + 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, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let flat_ct = enc_flat(&mut enc, &flat_plaintext); + + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + 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 = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_blocks(&mut dec, &block_ct), plaintext); + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + assert_eq!(dec_flat(&mut dec, &flat_ct), flat_plaintext); +} + +/// 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 = pinned_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, got) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + (got, enc_blocks(&mut enc, &blocks3)) + }; + let mut buf = flat3; + let iv_b = ToyCfb::::encrypt_rng(&key, &mut pinned_rng(iv), &mut buf).unwrap(); + assert_eq!(iv_a, iv_b); + assert_eq!(buf, *ct_blocks.as_flattened(), "3 blocks: one-shot must equal streaming"); + ToyCfb::::decrypt(&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, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + enc_blocks(&mut enc, &blocks4) + }; + let mut buf = flat4; + ToyCfb::::encrypt_rng(&key, &mut pinned_rng(iv), &mut buf).unwrap(); + assert_eq!(buf, *ct_blocks.as_flattened(), "4 blocks: one-shot must equal streaming"); + ToyCfb::::decrypt(&key, &iv, &mut buf).unwrap(); + assert_eq!(buf, flat4); + + // The OS-RNG variant round-trips too. + let mut buf = flat3; + let iv_fresh = ToyCfb::::encrypt(&key, &mut buf).unwrap(); + assert_ne!(buf, flat3); + ToyCfb::::decrypt(&key, &iv_fresh, &mut buf).unwrap(); + assert_eq!(buf, flat3); +} + +// ---- 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`). +#[test] +fn a_ciphertext_bit_error_flips_exactly_that_bit_of_its_own_block() { + let key = toy_key(); + let iv = pinned_iv(); + let plaintext = [[0x00u8; TOY_LEN], [0x11u8; TOY_LEN], [0x22u8; TOY_LEN], [0x33u8; TOY_LEN]]; + + let (mut enc, _) = + ToyCfb::::do_encrypt_init_rng(&key, &mut pinned_rng(iv)).unwrap(); + let ct = enc_blocks(&mut enc, &plaintext); + + // Every bit of C2, so the SBE claim is checked exhaustively rather than at one position. + for byte in 0..TOY_LEN { + for bit in 0..8 { + let mut corrupt = ct; + corrupt[1][byte] ^= 1 << bit; + + let mut dec = ToyCfb::::do_decrypt_init(&key, &iv).unwrap(); + let got = dec_blocks(&mut dec, &corrupt); + + assert_eq!(got[0], plaintext[0], "P1 depends only on the IV and C1"); + + let mut expected_p2 = plaintext[1]; + expected_p2[byte] ^= 1 << bit; + assert_eq!( + got[1], expected_p2, + "C2 byte {byte} bit {bit}: exactly that bit of P2 should change" + ); + + assert_ne!(got[2], plaintext[2], "P3 comes from CIPH_K of the corrupted C2"); + assert_eq!(got[3], plaintext[3], "P4 is unaffected: b/s = 1, so damage stops at P3"); + } + } +} + +/// The parts of Appendix D that need a real cipher's diffusion, checked with AES-128. +/// +/// Table D.2 for CFB says the *other* affected block gets "RBE" -- random bit errors, "bit errors +/// occur independently in any bit position with an expected probability of 1/2". That is a property +/// of the block cipher, not of the mode, so the toy (whose rounds are byte-local) cannot show it. +/// +/// The point worth pinning is that CFB and CBC differ here, and in which direction: under CBC a +/// corrupted IV 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`), whereas under CFB +/// the IV goes through the cipher first, so `P1` is randomised instead. Confusing the two would be a +/// real bug and this is what catches it. +#[test] +fn an_iv_bit_error_randomises_only_the_first_block() { + type Aes128Cfb = Cfb; + const LEN: usize = 16; + + let key = KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey) + .expect("a valid AES-128 key"); + let iv: [u8; LEN] = core::array::from_fn(|i| 0x0F ^ (i as u8)); + let plaintext = [[0x00u8; LEN], [0x11u8; LEN], [0x22u8; LEN]]; + + let (mut enc, got_iv) = + Aes128Cfb::::do_encrypt_init_rng(&key, &mut FixedSeedRNG::::new(iv)) + .unwrap(); + assert_eq!(got_iv, iv); + let mut ct = plaintext; + enc.do_encrypt_blocks(&mut ct).unwrap(); + + let mut first_blocks = std::collections::BTreeSet::new(); + + for byte in 0..LEN { + for bit in 0..8 { + let mut corrupt_iv = iv; + corrupt_iv[byte] ^= 1 << bit; + + let mut dec = Aes128Cfb::::do_decrypt_init(&key, &corrupt_iv).unwrap(); + let mut got = ct; + dec.do_decrypt_blocks(&mut got).unwrap(); + + // Only P1 is affected: with s = b, Appendix D's "first i/s (rounding up) ciphertext + // segments" is one segment for every bit position i. + 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 it is randomised, not flipped in place. The CBC behaviour would be a + // single-bit difference in exactly the position that was corrupted. + let differing_bits: u32 = + got[0].iter().zip(plaintext[0].iter()).map(|(a, b)| (a ^ b).count_ones()).sum(); + assert!( + differing_bits > 1, + "IV byte {byte} bit {bit}: P1 should be randomised, not flipped in place \ + ({differing_bits} bit(s) differ)" + ); + + let mut cbc_style = plaintext[0]; + cbc_style[byte] ^= 1 << bit; + 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(&key, &mut first).unwrap(); + let mut second = plaintext; + ToyCfb::::encrypt(&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()); +} + +// ---- composition with the padding layer -------------------------------------------------- + +/// CFB is block-aligned by contract, so arbitrary-length data goes through `bouncycastle-padding`. +/// Nothing in either crate knows about the other, so this is the test that they actually compose -- +/// across every length from empty to just past three blocks, which covers an exact multiple of the +/// block size (where PKCS7 appends a whole extra block) and every partial block. +#[test] +fn the_padding_layer_round_trips_every_length() { + type Enc = PaddedEncryptor, PKCS7, TOY_LEN, TOY_LEN, TOY_LEN>; + type Dec = PaddedDecryptor, PKCS7, TOY_LEN, TOY_LEN, 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 (iv, written) = + Enc::encrypt_out(&toy_key(), &plaintext, &mut ciphertext).expect("padded encryption"); + assert_eq!(written, ciphertext.len(), "len {len}: one whole number of blocks out"); + assert!(written > len, "len {len}: PKCS7 always adds at least one byte"); + + let mut recovered = vec![0u8; Dec::decrypt_out_max_len(written)]; + let n = Dec::decrypt_out(&toy_key(), &iv, &ciphertext, &mut recovered) + .expect("padded decryption"); + assert_eq!(&recovered[..n], &plaintext[..], "len {len}: round trip through PKCS7"); + } +} + +// ---- memory ------------------------------------------------------------------------------ + +/// Pins the "Memory Usage" table in the crate docs, and the claim that CFB costs exactly what CBC +/// costs. +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::size_of; + + assert_eq!(size_of::>(), 176 + 16); + assert_eq!(size_of::>(), 208 + 16); + assert_eq!(size_of::>(), 240 + 16); + + // The direction marker is free, and does not change the layout. + assert_eq!( + size_of::>(), + size_of::>() + ); + + // ...and the general rule the docs state. + assert_eq!(size_of::>(), size_of::() + 16); + + // The docs say CFB is the same size as CBC, because it stores the same thing. + assert_eq!( + size_of::>(), + size_of::>() + ); + assert_eq!( + size_of::>(), + size_of::>() + ); +} diff --git a/crypto/modes/tests/common/mod.rs b/crypto/modes/tests/common/mod.rs new file mode 100644 index 00000000..306b3052 --- /dev/null +++ b/crypto/modes/tests/common/mod.rs @@ -0,0 +1,222 @@ +//! 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 `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::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{Algorithm, ElectronicCodeBook, SecurityStrength}; + +/// 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); + } + } +} + +/// 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_blocks2` 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_blocks2(&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_blocks2(&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); + } +} + +/// 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_blocks2` or `decrypt_blocks8`. 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 eight-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_blocks2(&self, blocks: &mut [[u8; TOY_LEN]; 2]) { + self.inner.encrypt_blocks2(blocks); + } + + fn decrypt_blocks2(&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_blocks8(&self, blocks: &mut [[u8; TOY_LEN]; 8]) { + self.inner.encrypt_blocks8(blocks); + } + + fn decrypt_blocks8(&self, _blocks: &mut [[u8; TOY_LEN]; 8]) { + panic!("CFB must never call the inverse cipher eight-block function (SP 800-38A Sec 6.3)"); + } +} + +/// A [`Toy`] whose `encrypt_blocks8` / `decrypt_blocks8` return their eight results rotated by one +/// slot, while every other method -- single block and pair -- is correct. +/// +/// The eight-block analogue of [`SwappedPairToy`]: a CBC decryptor that uses `decrypt_blocks8` +/// must produce something other than the correct plaintext for eight or more blocks, while fewer +/// than eight, which go through the pair and single paths, still round-trip. +pub struct SwappedEightToy { + inner: Toy, +} + +impl Algorithm for SwappedEightToy { + const ALG_NAME: &'static str = "SwappedEightToy"; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} + +impl ElectronicCodeBook for SwappedEightToy { + 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_blocks8(&self, blocks: &mut [[u8; TOY_LEN]; 8]) { + for block in blocks.iter_mut() { + self.inner.encrypt_block(block); + } + blocks.rotate_left(1); + } + + fn decrypt_blocks8(&self, blocks: &mut [[u8; TOY_LEN]; 8]) { + 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/modes/tests/ecb_tests.rs b/crypto/modes/tests/ecb_tests.rs new file mode 100644 index 00000000..db1f2c66 --- /dev/null +++ b/crypto/modes/tests/ecb_tests.rs @@ -0,0 +1,429 @@ +//! 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 eight-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 `sp800_38a_ecb_tests.rs`, and the ACVP +//! set is in `acvp_ecb_tests.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. + +mod common; + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{ + BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook, SymmetricCipherDecryptor, + SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::symmetric_ciphers::TestFrameworkBlockCipher; +use bouncycastle_modes::{Cbc, Decrypting, Ecb, Encrypting}; +use bouncycastle_padding::{PKCS7, PaddedDecryptor, PaddedEncryptor}; +use common::{SwappedEightToy, SwappedPairToy, TOY_LEN, Toy, toy_key}; + +type ToyEcb = Ecb; +type SwappedEcb = Ecb; +type SwappedEightEcb = Ecb; + +/// The implementor hook `do_encrypt_blocks`, 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(&mut blocks).unwrap(); + blocks +} + +/// The implementor hook `do_decrypt_blocks`, 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(&mut blocks).unwrap(); + blocks +} + +/// The flat streaming method `do_encrypt`, by value. +fn enc_flat( + enc: &mut impl BlockCipherEncryptor, + plaintext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *plaintext; + enc.do_encrypt(&mut data).unwrap(); + data +} + +/// The flat streaming method `do_decrypt`, by value. +fn dec_flat( + dec: &mut impl BlockCipherDecryptor, + ciphertext: &[u8; LEN], +) -> [u8; LEN] { + let mut data = *ciphertext; + dec.do_decrypt(&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, [], "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(&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-shots see the same thing: `encrypt` returns the empty init data and is repeatable. + let flat: [u8; 4 * TOY_LEN] = plaintext.as_flattened().try_into().unwrap(); + let mut once = flat; + let init_a: [u8; 0] = ToyEcb::::encrypt(&key, &mut once).unwrap(); + let mut twice = flat; + let init_b = ToyEcb::::encrypt_rng( + &key, + &mut FixedSeedRNG::::new([0xAB; TOY_LEN]), + &mut twice, + ) + .unwrap(); + assert_eq!(init_a, init_b); + assert_eq!(once, twice, "the RNG variant draws nothing, so it changes nothing"); + assert_eq!(once, *ct_a.as_flattened()); +} + +/// The RNG-taking constructor must not consume from the RNG: there is no IV to generate. A +/// fixed-seed RNG of the wrong width would panic on its first draw, so this is observable. +#[test] +fn the_rng_constructor_draws_nothing() { + let key = toy_key(); + let mut rng = FixedSeedRNG::<0>::new([]); + let (mut enc, init) = ToyEcb::::do_encrypt_init_rng(&key, &mut rng).unwrap(); + assert_eq!(init, []); + let mut block = [0x42u8; TOY_LEN]; + enc.do_encrypt(&mut block).unwrap(); + assert_eq!(block, enc_flat(&mut encryptor(), &[0x42u8; TOY_LEN])); +} + +// ---- batching: pairs and eights, 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_blocks2, 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_blocks2"); + + // ...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_blocks2" + ); + 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 eight-block path must be taken, and only for full eights, in both directions. +/// [`SwappedEightToy`] rotates its eight results while its pair and single-block methods are +/// correct, so nine blocks handed over together are wrong (eight rotated, then one right) and the +/// same blocks as two fours or singly are right. +#[test] +fn the_eight_block_path_is_used_in_both_directions() { + let key = toy_key(); + let plaintext: [[u8; TOY_LEN]; 9] = 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, _) = SwappedEightEcb::::do_encrypt_init(&key).unwrap(); + let rotated = enc_blocks(&mut enc, &plaintext); + assert_ne!(rotated, ct, "nine blocks must go through encrypt_blocks8"); + assert_eq!(rotated[8], ct[8], "the ninth block goes through the single path and is right"); + assert_eq!( + &rotated[..8], + &[ct[1], ct[2], ct[3], ct[4], ct[5], ct[6], ct[7], ct[0]], + "eight rotated" + ); + + let (mut enc, _) = SwappedEightEcb::::do_encrypt_init(&key).unwrap(); + let a = enc_blocks(&mut enc, &[plaintext[0], plaintext[1], plaintext[2], plaintext[3]]); + let b = enc_blocks(&mut enc, &[plaintext[4], plaintext[5], plaintext[6], plaintext[7]]); + assert_eq!([a, b].as_flattened(), &ct[..8], "fours use the pair path only"); + + let mut dec = SwappedEightEcb::::do_decrypt_init(&key, &[]).unwrap(); + assert_ne!(dec_blocks(&mut dec, &ct), plaintext, "nine blocks must go through decrypt_blocks8"); + let mut dec = SwappedEightEcb::::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, 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(&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(&key, &mut buf).unwrap(); + assert_eq!(buf, *block_ct.as_flattened(), "one-shot must equal streaming"); + ToyEcb::::decrypt(&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 toy is byte-local, so it can show only the "no other +/// block" half exactly; the randomisation is checked with real AES below. +#[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 mut corrupt = ct; + corrupt[1][byte] ^= 1 << bit; + let got = dec_blocks(&mut decryptor(), &corrupt); + assert_eq!(got[0], plaintext[0]); + assert_ne!(got[1], plaintext[1], "C2 byte {byte} bit {bit}: P2 must change"); + assert_eq!(got[2], plaintext[2], "P3 is unaffected: nothing chains"); + assert_eq!(got[3], plaintext[3]); + } + } +} + +/// The randomisation half of Table D.2, with AES-128: every one of the 128 bit positions of `C2` +/// must randomise `P2` (more than one bit differs) and leave `P1` and `P3` untouched. +#[test] +fn with_aes_a_ciphertext_bit_error_randomises_its_block() { + type Aes128Ecb = Ecb; + let key = + KeyMaterial::<16>::from_bytes_as_type(&[0x42; 16], KeyType::SymmetricCipherKey).unwrap(); + let plaintext = [[0x00u8; 16], [0x11u8; 16], [0x22u8; 16]]; + let mut ct = plaintext; + let flat: &mut [u8; 48] = ct.as_flattened_mut().try_into().unwrap(); + Aes128Ecb::::encrypt(&key, flat).unwrap(); + + for byte in 0..16 { + for bit in 0..8 { + let mut corrupt = ct; + corrupt[1][byte] ^= 1 << bit; + let flat: &mut [u8; 48] = corrupt.as_flattened_mut().try_into().unwrap(); + Aes128Ecb::::decrypt(&key, &[], flat).unwrap(); + assert_eq!(corrupt[0], plaintext[0], "C2 byte {byte} bit {bit}: P1 unaffected"); + assert_eq!(corrupt[2], plaintext[2], "C2 byte {byte} bit {bit}: P3 unaffected"); + let differing: u32 = + corrupt[1].iter().zip(plaintext[1].iter()).map(|(a, b)| (a ^ b).count_ones()).sum(); + assert!( + differing > 1, + "C2 byte {byte} bit {bit}: P2 should be randomised ({differing} bit(s) differ)" + ); + } + } +} + +// ---- 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-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 = PaddedEncryptor, PKCS7, TOY_LEN, 0, TOY_LEN>; + type Dec = PaddedDecryptor, 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, []); + assert_eq!(written, ciphertext.len(), "len {len}"); + let mut recovered = vec![0u8; Dec::decrypt_out_max_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 "Memory Usage" table in the crate docs: an ECB value is exactly the permutation. +#[test] +fn sizes_match_the_documented_memory_table() { + use core::mem::size_of; + assert_eq!(size_of::>(), 176); + assert_eq!(size_of::>(), 208); + assert_eq!(size_of::>(), 240); + 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::>() + 16, + size_of::>() + ); +} diff --git a/crypto/modes/tests/sp800_38a_cfb_tests.rs b/crypto/modes/tests/sp800_38a_cfb_tests.rs new file mode 100644 index 00000000..fbc90f5e --- /dev/null +++ b/crypto/modes/tests/sp800_38a_cfb_tests.rs @@ -0,0 +1,364 @@ +//! 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. +//! +//! 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 [`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_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Cfb, Decrypting, Encrypting}; + +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") +} + +/// Runs one Appendix F.3 encrypt subsection. +/// +/// Checks the whole message in one call, then again one segment 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 segments in one call. + let (mut 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"); + let mut data = flat(&PLAINTEXTS); + enc.do_encrypt(&mut data).unwrap(); + assert_eq!(data, flat(expected), "{section}: four segments in one call"); + + // One segment at a time. + let (mut enc, _) = Cfb::::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(&mut got).unwrap(); + assert_eq!(&got, c, "{section}: segment #{}", i + 1); + } + + // Through the implementor hook, `do_*_blocks`. + let (mut enc, _) = Cfb::::do_encrypt_init_rng( + &key, + &mut FixedSeedRNG::::new(iv), + ) + .unwrap(); + let mut blocks = pt; + enc.do_encrypt_blocks(&mut blocks).unwrap(); + assert_eq!(blocks, ct, "{section}: implementor hook"); +} + +/// Runs one Appendix F.3 decrypt subsection. +/// +/// Checks one call, one segment 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`. +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(&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(&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(&mut three).unwrap(); + let mut one = ct[3]; + dec.do_decrypt(&mut one).unwrap(); + assert_eq!(&three[..], pt[..3].as_flattened(), "{section}: segments 1-3"); + assert_eq!(one, pt[3], "{section}: segment 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(&mut blocks).unwrap(); + assert_eq!(blocks, pt, "{section}: implementor hook"); +} + +#[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 take flat arrays and work in place, so the four ciphertext segments are presented +/// as 64 contiguous bytes and become the four plaintext blocks. +#[test] +fn the_one_shot_api_matches_the_vectors() { + let iv = block(IV); + let pt = flat(&PLAINTEXTS); + + let mut data = flat(&CIPHERTEXTS_128); + Cfb::::decrypt(&key_material::<16>(KEY_128), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); + + let mut data = flat(&CIPHERTEXTS_192); + Cfb::::decrypt(&key_material::<24>(KEY_192), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); + + let mut data = flat(&CIPHERTEXTS_256); + Cfb::::decrypt(&key_material::<32>(KEY_256), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); +} + +/// 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(&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(&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/modes/tests/sp800_38a_ecb_tests.rs b/crypto/modes/tests/sp800_38a_ecb_tests.rs new file mode 100644 index 00000000..ea9a7539 --- /dev/null +++ b/crypto/modes/tests/sp800_38a_ecb_tests.rs @@ -0,0 +1,219 @@ +//! Known-answer tests from NIST SP 800-38A Appendix F.1, "ECB Example Vectors". +//! +//! Sections **F.1.1 through F.1.6**: ECB-AES128, ECB-AES192 and ECB-AES256, Encrypt and Decrypt. +//! All six use the same four plaintext blocks (Appendix F preamble) and the same three keys as F.2 +//! (CBC) and F.3 (CFB), so these vectors also re-check each AES key expansion through the plainest +//! possible construction. Transcribed from the published SP 800-38A PDF (2001 edition). +//! +//! # No IV to drive +//! +//! ECB has no initialization data, so -- unlike the CBC and CFB suites -- `encrypt` can be checked +//! against the published ciphertext directly, through the one-shot as well as the streaming API. +//! +//! # The mode is the permutation +//! +//! Sec 6.1 gives `Cj = CIPH_K(Pj)`, so each tabulated ciphertext block must equal the raw +//! permutation applied to the corresponding plaintext block. `each_block_is_the_raw_permutation` +//! checks that, which ties the mode to [`ElectronicCodeBook`] and confirms the transcription: a +//! typo in either column would break the equality. + +use bouncycastle_aes_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook}; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Decrypting, Ecb, Encrypting}; + +const BLOCK_LEN: usize = 16; + +/// The four plaintext blocks shared by every Appendix F subsection (Appendix F preamble). +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 ciphertext 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 ciphertext 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 ciphertext blocks. +const CIPHERTEXTS_256: [&str; 4] = [ + "f3eed1bdb5d2a03c064b5a7e3db181f8", + "591ccb10d410ed26dc5ba74a31362870", + "b6ed21b99ca6f4f9f153e7b1beafed1d", + "23304b7a39f9f3ff067d8d8f9e24ecc7", +]; + +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.1 encrypt subsection: the whole message in one call (two pairs), one block +/// at a time, the `3 + 1` grouping that leaves a remainder after the pair loop, the implementor +/// hook, and the one-shot. +fn check_encrypt(section: &str, key_hex: &str, expected: &[&str; 4]) +where + P: ElectronicCodeBook, +{ + type Enc = Ecb; + let key = key_material::(key_hex); + let pt = blocks(&PLAINTEXTS); + let ct = blocks(expected); + + let (mut enc, init) = Enc::::do_encrypt_init(&key).unwrap(); + assert_eq!(init, [], "{section}: ECB has no init data"); + let mut data = flat(&PLAINTEXTS); + enc.do_encrypt(&mut data).unwrap(); + assert_eq!(data, flat(expected), "{section}: four blocks in one call"); + + let (mut enc, _) = Enc::::do_encrypt_init(&key).unwrap(); + for (i, (p, c)) in pt.iter().zip(ct.iter()).enumerate() { + let mut got = *p; + enc.do_encrypt(&mut got).unwrap(); + assert_eq!(&got, c, "{section}: block #{}", i + 1); + } + + let (mut enc, _) = Enc::::do_encrypt_init(&key).unwrap(); + let mut three: [u8; 3 * BLOCK_LEN] = pt[..3].as_flattened().try_into().unwrap(); + enc.do_encrypt(&mut three).unwrap(); + let mut one = pt[3]; + enc.do_encrypt(&mut one).unwrap(); + assert_eq!(&three[..], ct[..3].as_flattened(), "{section}: blocks 1-3"); + assert_eq!(one, ct[3], "{section}: block 4"); + + let (mut enc, _) = Enc::::do_encrypt_init(&key).unwrap(); + let mut hook = pt; + enc.do_encrypt_blocks(&mut hook).unwrap(); + assert_eq!(hook, ct, "{section}: implementor hook"); + + let mut data = flat(&PLAINTEXTS); + let init = Enc::::encrypt(&key, &mut data).unwrap(); + assert_eq!(init, []); + assert_eq!(data, flat(expected), "{section}: one-shot"); +} + +/// Runs one Appendix F.1 decrypt subsection, in the same five groupings. +fn check_decrypt(section: &str, key_hex: &str, ciphertext: &[&str; 4]) +where + P: ElectronicCodeBook, +{ + type Dec = Ecb; + let key = key_material::(key_hex); + let pt = blocks(&PLAINTEXTS); + let ct = blocks(ciphertext); + + let mut dec = Dec::::do_decrypt_init(&key, &[]).unwrap(); + let mut data = flat(ciphertext); + dec.do_decrypt(&mut data).unwrap(); + assert_eq!(data, flat(&PLAINTEXTS), "{section}: four blocks in one call"); + + let mut dec = Dec::::do_decrypt_init(&key, &[]).unwrap(); + for (i, (c, p)) in ct.iter().zip(pt.iter()).enumerate() { + let mut got = *c; + dec.do_decrypt(&mut got).unwrap(); + assert_eq!(&got, p, "{section}: block #{}", i + 1); + } + + let mut dec = Dec::::do_decrypt_init(&key, &[]).unwrap(); + let mut three: [u8; 3 * BLOCK_LEN] = ct[..3].as_flattened().try_into().unwrap(); + dec.do_decrypt(&mut three).unwrap(); + let mut one = ct[3]; + dec.do_decrypt(&mut one).unwrap(); + assert_eq!(&three[..], pt[..3].as_flattened(), "{section}: blocks 1-3"); + assert_eq!(one, pt[3], "{section}: block 4"); + + let mut dec = Dec::::do_decrypt_init(&key, &[]).unwrap(); + let mut hook = ct; + dec.do_decrypt_blocks(&mut hook).unwrap(); + assert_eq!(hook, pt, "{section}: implementor hook"); + + let mut data = flat(ciphertext); + Dec::::decrypt(&key, &[], &mut data).unwrap(); + assert_eq!(data, flat(&PLAINTEXTS), "{section}: one-shot"); +} + +#[test] +fn f_1_1_ecb_aes128_encrypt() { + check_encrypt::("F.1.1", KEY_128, &CIPHERTEXTS_128); +} + +#[test] +fn f_1_2_ecb_aes128_decrypt() { + check_decrypt::("F.1.2", KEY_128, &CIPHERTEXTS_128); +} + +#[test] +fn f_1_3_ecb_aes192_encrypt() { + check_encrypt::("F.1.3", KEY_192, &CIPHERTEXTS_192); +} + +#[test] +fn f_1_4_ecb_aes192_decrypt() { + check_decrypt::("F.1.4", KEY_192, &CIPHERTEXTS_192); +} + +#[test] +fn f_1_5_ecb_aes256_encrypt() { + check_encrypt::("F.1.5", KEY_256, &CIPHERTEXTS_256); +} + +#[test] +fn f_1_6_ecb_aes256_decrypt() { + check_decrypt::("F.1.6", KEY_256, &CIPHERTEXTS_256); +} + +/// Sec 6.1: `Cj = CIPH_K(Pj)`. Every tabulated ciphertext block is the raw permutation of the +/// corresponding plaintext block, for all three key lengths. +fn check_raw(section: &str, key_hex: &str, ciphertexts: &[&str; 4]) +where + P: ElectronicCodeBook, +{ + let perm = P::new(&key_material::(key_hex)).expect("a valid key"); + for (j, (p, c)) in PLAINTEXTS.iter().zip(ciphertexts.iter()).enumerate() { + let mut computed = block(p); + perm.encrypt_block(&mut computed); + assert_eq!(computed, block(c), "{section}: block #{} should be CIPH_K(P{})", j + 1, j + 1); + } +} + +#[test] +fn each_block_is_the_raw_permutation() { + check_raw::("F.1.1", KEY_128, &CIPHERTEXTS_128); + check_raw::("F.1.3", KEY_192, &CIPHERTEXTS_192); + check_raw::("F.1.5", KEY_256, &CIPHERTEXTS_256); +} diff --git a/crypto/modes/tests/sp800_38a_tests.rs b/crypto/modes/tests/sp800_38a_tests.rs new file mode 100644 index 00000000..1dee9ac7 --- /dev/null +++ b/crypto/modes/tests/sp800_38a_tests.rs @@ -0,0 +1,262 @@ +//! 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_lowmemory::{Aes128, Aes192, Aes256}; +use bouncycastle_core::key_material::{KeyMaterial, KeyType}; +use bouncycastle_core::traits::{BlockCipherDecryptor, BlockCipherEncryptor, ElectronicCodeBook}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_hex as hex; +use bouncycastle_modes::{Cbc, Decrypting, Encrypting}; + +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(&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(&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(&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`. +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(&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(&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(&mut three).unwrap(); + let mut one = ct[3]; + dec.do_decrypt(&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(&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. +#[test] +fn the_one_shot_api_matches_the_vectors() { + let iv = block(IV); + let pt = flat(&PLAINTEXTS); + + let mut data = flat(&CIPHERTEXTS_128); + Cbc::::decrypt(&key_material::<16>(KEY_128), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); + + let mut data = flat(&CIPHERTEXTS_192); + Cbc::::decrypt(&key_material::<24>(KEY_192), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); + + let mut data = flat(&CIPHERTEXTS_256); + Cbc::::decrypt(&key_material::<32>(KEY_256), &iv, &mut data) + .unwrap(); + assert_eq!(data, pt); +} + +/// 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(&mut cbc).unwrap(); + assert_eq!(cbc, block(CIPHERTEXTS_128[0]), "F.2.1 block #1"); + assert_ne!(cbc, ecb); +} diff --git a/crypto/padding/Cargo.toml b/crypto/padding/Cargo.toml new file mode 100644 index 00000000..315ce973 --- /dev/null +++ b/crypto/padding/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "bouncycastle-padding" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.workspace = true +bouncycastle-utils.workspace = true + +[dev-dependencies] +bouncycastle-core-test-framework.workspace = true +bouncycastle-rng.workspace = true +criterion.workspace = true + +[[bench]] +name = "padding_benches" +harness = false diff --git a/crypto/padding/benches/padding_benches.rs b/crypto/padding/benches/padding_benches.rs new file mode 100644 index 00000000..1e096af1 --- /dev/null +++ b/crypto/padding/benches/padding_benches.rs @@ -0,0 +1,27 @@ +use bouncycastle_core::traits::Padding; +use bouncycastle_padding::PKCS7; +use criterion::{Criterion, criterion_group, criterion_main}; +use std::hint::black_box; + +fn bench_pkcs7(c: &mut Criterion) { + let mut group = c.benchmark_group("padding::PKCS7"); + group.bench_function("pad/16", |b| { + let mut block = [0u8; 16]; + b.iter(|| { + >::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/padding/src/lib.rs b/crypto/padding/src/lib.rs new file mode 100644 index 00000000..cdd5b8bf --- /dev/null +++ b/crypto/padding/src/lib.rs @@ -0,0 +1,176 @@ +//! Block padding schemes implementing [`bouncycastle_core::traits::Padding`]. +//! +//! * [`PKCS7`] — the padding scheme of RFC 5652 §6.3. +//! * [`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. +//! * [`PaddedEncryptor`] / [`PaddedDecryptor`] — 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_final`. +//! +//! # Usage Examples +//! +//! ``` +//! use bouncycastle_core::traits::Padding; +//! use bouncycastle_padding::PKCS7; +//! +//! // 5 data bytes in a 16-byte block: pad with 11 bytes of value 0x0b. +//! let mut block = [0u8; 16]; +//! block[..5].copy_from_slice(b"hello"); +//! >::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::Padding; +//! use bouncycastle_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)); +//! ``` +//! +//! # 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 | +//! | `PaddedEncryptor` | one `BLOCK_LEN` buffer (in a `Secret`) + a length | +//! | `PaddedDecryptor` | two `BLOCK_LEN` buffers + a length | +//! +//! # Security Considerations +//! +//! `unpad` is the classic padding-oracle site: if timing or the error depends on *which* byte was +//! malformed, an attacker who can submit ciphertexts can decrypt them byte by byte. [`PKCS7::unpad`] +//! inspects every byte with constant-time masks and returns a single undifferentiated +//! [`PaddingError::InvalidPadding`]. This does not make unauthenticated encryption safe: still +//! authenticate the ciphertext (MAC or AEAD) so the error is never reachable by an attacker. +//! +//! [`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. + +#![forbid(unsafe_code)] +#![forbid(missing_docs)] +#![no_std] + +mod padded; +pub use padded::{PaddedDecryptor, PaddedEncryptor}; + +use bouncycastle_core::errors::PaddingError; +use bouncycastle_core::traits::Padding; +use bouncycastle_utils::ct::Condition; + +/// RFC 5652 §6.3 padding (the CMS successor to PKCS #7): "the input shall be padded at the trailing +/// end with `k-(lth mod k)` octets all having value `k-(lth mod k)`". Defined only for block lengths +/// `0 < k < 256`, enforced at compile time. +pub struct PKCS7; + +impl Padding 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 [`Padding`] 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`](Padding::ALWAYS_PADS) is `false`, a [`PaddedEncryptor`] +/// over it emits no final block for an aligned message and fails at `do_final` for an unaligned one, +/// and a [`PaddedDecryptor`] 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 the crate's "Security Considerations". +pub struct NoPadding; + +impl Padding 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/padding/src/padded.rs b/crypto/padding/src/padded.rs new file mode 100644 index 00000000..ee22d21e --- /dev/null +++ b/crypto/padding/src/padded.rs @@ -0,0 +1,332 @@ +//! [`PaddedEncryptor`] / [`PaddedDecryptor`]: adapt a block-aligned [`BlockCipherEncryptor`] / +//! [`BlockCipherDecryptor`] to arbitrary-length data using a [`Padding`] 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 +//! [`Padding::ALWAYS_PADS`] `false` (`NoPadding`) and an aligned message, nothing at all, in which +//! case `do_final` reports 0 of the `FINAL_LEN` bytes as output. + +use bouncycastle_core::errors::SymmetricCipherError; +use bouncycastle_core::key_material::KeyMaterial; +use bouncycastle_core::traits::{ + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, Padding, RNG, SecurityStrength, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_utils::secret::Secret; +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_update_out`] then [`SymmetricCipherEncryptor::do_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_final`). The buffered partial plaintext block is held in a [`Secret`]. +pub struct PaddedEncryptor< + E, + P, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +> where + E: BlockCipherEncryptor, + P: Padding, +{ + inner: E, + /// Partial plaintext block; `buf_len < BLOCK_LEN` between calls. + buf: Secret<[u8; BLOCK_LEN]>, + buf_len: usize, + _padding: PhantomData

, +} + +impl + PaddedEncryptor +where + E: BlockCipherEncryptor, + P: Padding, +{ + fn wrap(inner: E) -> Self { + Self { inner, buf: Secret::new(), buf_len: 0, _padding: PhantomData } + } +} + +impl Algorithm + for PaddedEncryptor +where + E: BlockCipherEncryptor, + P: Padding, +{ + /// 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 PaddedEncryptor +where + E: BlockCipherEncryptor, + P: Padding, +{ + fn do_encrypt_init( + key: &KeyMaterial, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (inner, init_data) = E::do_encrypt_init(key)?; + Ok((Self::wrap(inner), init_data)) + } + + fn do_encrypt_init_rng( + key: &KeyMaterial, + rng: &mut dyn RNG, + ) -> Result<(Self, [u8; INIT_DATA_LEN]), SymmetricCipherError> { + let (inner, init_data) = E::do_encrypt_init_rng(key, rng)?; + Ok((Self::wrap(inner), init_data)) + } + + /// Whole blocks among the buffered bytes plus `input_len`. + fn update_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_update_out( + &mut self, + plaintext: &[u8], + ciphertext: &mut [u8], + ) -> Result { + let out_len = self.update_out_len(plaintext.len()); + if ciphertext.len() < out_len { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("ciphertext", 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::(); + 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.inner.do_encrypt_blocks(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.inner.do_encrypt_blocks(group)?; + } + for block in out_tail.iter_mut() { + self.inner.do_encrypt_blocks(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_final(self) -> Result<([u8; BLOCK_LEN], usize), SymmetricCipherError> { + let Self { 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(&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_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 [`PaddedEncryptor`] with the matching cipher and padding. +/// +/// Only the last block carries padding, so [`do_update_out`](Self::do_update_out) always withholds +/// the most recent complete block and [`do_final`](Self::do_final) unpads it. One-shot: +/// [`decrypt_out`](Self::decrypt_out). +pub struct PaddedDecryptor< + D, + P, + const KEY_LEN: usize, + const INIT_DATA_LEN: usize, + const BLOCK_LEN: usize, +> where + D: BlockCipherDecryptor, + P: Padding, +{ + inner: D, + /// 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]>, + _padding: PhantomData

, +} + +impl Algorithm + for PaddedDecryptor +where + D: BlockCipherDecryptor, + P: Padding, +{ + /// 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 PaddedDecryptor +where + D: BlockCipherDecryptor, + P: Padding, +{ + fn do_decrypt_init( + key: &KeyMaterial, + init_data: &[u8; INIT_DATA_LEN], + ) -> Result { + Ok(Self { + inner: D::do_decrypt_init(key, init_data)?, + buf: [0u8; BLOCK_LEN], + buf_len: 0, + held: None, + _padding: PhantomData, + }) + } + + /// All complete blocks but the most recent one are released. + fn update_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_update_out( + &mut self, + ciphertext: &[u8], + plaintext: &mut [u8], + ) -> Result { + let out_len = self.update_out_len(ciphertext.len()); + if plaintext.len() < out_len { + return Err(SymmetricCipherError::IncorrectOutputBufferLength("plaintext", 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.inner.do_decrypt_blocks(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.inner.do_decrypt_blocks(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.inner.do_decrypt_blocks(group)?; + } + for block in out_tail.iter_mut() { + self.inner.do_decrypt_blocks(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_final(self) -> Result<([u8; BLOCK_LEN], usize), SymmetricCipherError> { + let Self { 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(&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_max_len(ciphertext_len: usize) -> usize { + if P::ALWAYS_PADS { ciphertext_len.saturating_sub(1) } else { ciphertext_len } + } +} diff --git a/crypto/padding/tests/nopadding_tests.rs b/crypto/padding/tests/nopadding_tests.rs new file mode 100644 index 00000000..148ea93f --- /dev/null +++ b/crypto/padding/tests/nopadding_tests.rs @@ -0,0 +1,55 @@ +//! Tests for `NoPadding`: a `Padding` 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_core::errors::PaddingError; +use bouncycastle_core::traits::Padding; +use bouncycastle_padding::{NoPadding, PKCS7}; + +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/padding/tests/padded_tests.rs b/crypto/padding/tests/padded_tests.rs new file mode 100644 index 00000000..42f7cb60 --- /dev/null +++ b/crypto/padding/tests/padded_tests.rs @@ -0,0 +1,403 @@ +//! Tests for PaddedEncryptor / PaddedDecryptor. +//! +//! 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_core::errors::{KeyMaterialError, PaddingError, SymmetricCipherError}; +use bouncycastle_core::key_material::{KeyMaterial, KeyMaterialTrait, KeyType}; +use bouncycastle_core::traits::{ + Algorithm, BlockCipherDecryptor, BlockCipherEncryptor, RNG, SecurityStrength, + SymmetricCipherDecryptor, SymmetricCipherEncryptor, +}; +use bouncycastle_core_test_framework::FixedSeedRNG; +use bouncycastle_core_test_framework::symmetric_ciphers::{ + TestFrameworkBlockCipher, TestFrameworkSymmetricCipher, +}; +use bouncycastle_padding::{NoPadding, PKCS7, PaddedDecryptor, PaddedEncryptor}; +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(&mut self, blocks: &mut [[u8; B]]) -> Result<(), SymmetricCipherError> { + 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(()) + } +} + +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(&mut self, blocks: &mut [[u8; B]]) -> Result<(), SymmetricCipherError> { + 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(()) + } +} + +type Enc = PaddedEncryptor; +type Dec = PaddedDecryptor; +/// The same adapters over `NoPadding`: an alignment check rather than a padding scheme. +type EncNP = PaddedEncryptor; +type DecNP = PaddedDecryptor; + +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_max_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.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = enc.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact"); + ct.extend_from_slice(&buf[..n]); + } + let (last, last_len) = enc.do_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_max_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.update_out_len(piece.len()); + let mut buf = vec![0u8; expect]; + let n = dec.do_update_out(piece, &mut buf).unwrap(); + assert_eq!(n, expect, "update_out_len must be exact (decrypt)"); + rec.extend_from_slice(&buf[..n]); + } + let (block, data_len) = dec.do_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.update_out_len(B), 0); + assert_eq!(dec.do_update_out(&ct[..B], &mut out).unwrap(), 0); + // second block: releases the first + assert_eq!(dec.update_out_len(B), B); + assert_eq!(dec.do_update_out(&ct[B..2 * B], &mut out).unwrap(), B); + // third block: releases the second + assert_eq!(dec.do_update_out(&ct[2 * B..], &mut out[B..]).unwrap(), B); + let (last, n) = dec.do_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_update_out(&msg(B + 2), &mut ct).unwrap(); + assert_eq!(n, B); + let mut last = [0u8; B]; + assert_eq!(enc.do_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_update_out(&ct, &mut out).unwrap(), B); + let mut last_pt = [0u8; B]; + let data_len = dec.do_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_update_out(&[0u8; B + 3], &mut out).unwrap(); + assert!(matches!(dec.do_final(), Err(SymmetricCipherError::DecryptionFailed))); + // streaming: nothing fed at all + let dec = Dec::do_decrypt_init(&key, &iv).unwrap(); + assert!(matches!(dec.do_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::IncorrectOutputBufferLength(_, 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_update_out(&pt, &mut tiny) { + Err(SymmetricCipherError::IncorrectOutputBufferLength(_, 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::IncorrectOutputBufferLength(_, 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_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_max_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(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_final reports zero output bytes. + let (mut enc, _) = EncNP::do_encrypt_init(&key).unwrap(); + let mut buf = vec![0u8; enc.update_out_len(len)]; + assert_eq!(enc.do_update_out(&pt, &mut buf).unwrap(), len); + let (_, last_len) = enc.do_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_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_update_out(&pt, &mut buf).unwrap(), whole, "whole blocks still stream"); + assert!( + matches!( + enc.do_final(), + Err(SymmetricCipherError::PaddingError(PaddingError::PaddingNotPermitted)) + ), + "len {len}: do_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_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/padding/tests/pkcs7_tests.rs b/crypto/padding/tests/pkcs7_tests.rs new file mode 100644 index 00000000..d68de485 --- /dev/null +++ b/crypto/padding/tests/pkcs7_tests.rs @@ -0,0 +1,121 @@ +//! 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_core::errors::PaddingError; +use bouncycastle_core::traits::Padding; +use bouncycastle_padding::PKCS7; + +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; + + >::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]; + >::pad(&mut b, K - 1).unwrap(); + assert_eq!(b[K - 1], 0x01); + + let mut b = [0xFFu8; K]; + >::pad(&mut b, K - 2).unwrap(); + assert_eq!(&b[K - 2..], &[0x02, 0x02]); + + let mut b = [0xFFu8; K]; + >::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 + >::pad(&mut b, 13).unwrap(); + assert_eq!(b, [0x03u8; K]); + assert_eq!(>::unpad(&b), Ok(13)); +} diff --git a/crypto/rng/src/hash_drbg80090a.rs b/crypto/rng/src/hash_drbg80090a.rs index be70cb8d..a52a3950 100644 --- a/crypto/rng/src/hash_drbg80090a.rs +++ b/crypto/rng/src/hash_drbg80090a.rs @@ -13,7 +13,7 @@ use bouncycastle_core::traits::{Hash, HashAlgParams, RNG, SecurityStrength}; 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 +90,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) } } diff --git a/crypto/sha2/Cargo.toml b/crypto/sha2/Cargo.toml index 7ff2e037..558da22a 100644 --- a/crypto/sha2/Cargo.toml +++ b/crypto/sha2/Cargo.toml @@ -11,6 +11,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/lib.rs b/crypto/sha2/src/lib.rs index 6906e0c6..60f2d341 100644 --- a/crypto/sha2/src/lib.rs +++ b/crypto/sha2/src/lib.rs @@ -3,7 +3,8 @@ //! # 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 [`SHA224`], [`SHA256`], [`SHA384`], [`SHA512`], [`SHA512_224`] and +//! [`SHA512_256`]. //! //! The simplest usage is via the static functions. //! ``` @@ -14,7 +15,7 @@ //! let output: Vec = sha2::SHA256::new().hash(data); //! ``` //! -//! More advanced usage will require creating a SHA3 or SHAKE object to hold state between successive calls, +//! 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: //! //! ``` @@ -34,6 +35,82 @@ //! let output: Vec = sha2.do_final(); //! ``` //! +//! 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 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_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"); +//! ``` +//! +//! # SHA-512/t +//! +//! FIPS 180-4 s. 5.3.6 defines SHA-512/t, a family of hash functions that run SHA-512 with a +//! t-specific initial hash value and truncate the result to t bits. The family is exposed as the +//! generic [`SHA512t`]; its initial hash value is derived at compile time by the spec's "SHA-512/t +//! IV Generation Function". Only the two truncations that FIPS 180-4 approves, `t = 224` and +//! `t = 256`, are instantiable, as [`SHA512_224`] and [`SHA512_256`]; any other `t` fails to +//! compile. +//! +//! ``` +//! use bouncycastle_core::traits::Hash; +//! use bouncycastle_sha2 as sha2; +//! +//! let output: Vec = sha2::SHA512_256::new().hash(b"Hello, world!"); +//! assert_eq!(output.len(), 32); +//! +//! // `SHA512_256` is an alias for `SHA512t<256>`. +//! let same: Vec = sha2::SHA512t::<256>::new().hash(b"Hello, world!"); +//! assert_eq!(output, same); +//! ``` +//! +//! A truncation that FIPS 180-4 does not approve is rejected by the compiler: +//! +//! ```compile_fail +//! use bouncycastle_core::traits::Hash; +//! use bouncycastle_sha2 as sha2; +//! +//! let output: Vec = sha2::SHA512t::<200>::new().hash(b"Hello, world!"); +//! ``` +//! +//! # 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`, `SHA512_224`, `SHA512_256` | 208 | +//! | Suspended `SHA224`/`SHA256` state | 108 | +//! | Suspended `SHA384`/`SHA512`/`SHA512_224`/`SHA512_256` state | 204 | +//! +//! 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-512/224 and SHA-512/256 offer 112 and 128 bits. +//! * 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). +//! //! # Suspending and resuming execution //! //! When hashing a large message, it can be advantageous to be able to suspend the operation @@ -73,22 +150,28 @@ mod sha256; mod sha512; pub use self::sha256::SHA256Internal; +use self::sha256::{SHA224_H0, SHA256_H0}; pub use self::sha512::SHA512Internal; +use self::sha512::{SHA384_H0, SHA512_H0, sha512t_h0}; use bouncycastle_core::traits::{Algorithm, AlgorithmOID, HashAlgParams, SecurityStrength}; /*** Imports needed for docs ***/ #[allow(unused_imports)] -use bouncycastle_core::traits::Suspendable; +use bouncycastle_core::traits::{Hash, Suspendable}; /*** String constants ***/ -/// +/// Algorithm name string for SHA224, as used by the factories and CLI. pub const SHA224_NAME: &str = "SHA224"; -/// +/// Algorithm name string for SHA256, as used by the factories and CLI. pub const SHA256_NAME: &str = "SHA256"; -/// +/// Algorithm name string for SHA384, as used by the factories and CLI. pub const SHA384_NAME: &str = "SHA384"; -/// +/// Algorithm name string for SHA512, as used by the factories and CLI. pub const SHA512_NAME: &str = "SHA512"; +/// Algorithm name string for SHA512/224, as used by the factories and CLI. +pub const SHA512_224_NAME: &str = "SHA512/224"; +/// Algorithm name string for SHA512/256, as used by the factories and CLI. +pub const SHA512_256_NAME: &str = "SHA512/256"; /*** pub types ***/ /// Public type for SHA224. @@ -99,16 +182,47 @@ 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. Only the NIST-approved truncations `T = 224` and `T = 256` +/// can be instantiated; see [`SHA512_224`] and [`SHA512_256`]. +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 {} -/*** SHA224 ***/ -impl HashAlgParams for SHA224 { - const OUTPUT_LEN: usize = 28; - const BLOCK_LEN: usize = 64; +/// 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. +/// Private for the same reason as [`SHA2Params`]. +trait Sha256Family: SHA2Params { + /// The initial hash value H(0), FIPS 180-4 s. 5.3.2 / 5.3.3. + const H0: [u32; 8]; +} + +/// 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. +/// Private for the same reason as [`SHA2Params`]. +trait Sha512Family: SHA2Params { + /// 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; @@ -127,12 +241,12 @@ impl AlgorithmOID for SHA224 { &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04]; } impl SHA2Params for SHA224Params {} +impl Sha256Family 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; @@ -151,12 +265,12 @@ impl HashAlgParams for SHA256Params { const BLOCK_LEN: usize = 64; } impl SHA2Params for SHA256Params {} +impl Sha256Family 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; @@ -175,15 +289,15 @@ impl HashAlgParams for SHA384Params { const BLOCK_LEN: usize = 128; } impl SHA2Params for SHA384Params {} +impl Sha512Family 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; @@ -199,6 +313,123 @@ impl AlgorithmOID for SHA512 { &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03]; } impl SHA2Params for SHA512Params {} +impl Sha512Family 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. +/// +/// The parameter traits are implemented only for the NIST-approved truncations `T = 224` and +/// `T = 256` ("Other SHA-512/t hash algorithms with different t values may be specified in +/// [SP 800-107] in the future as the need arises"), so any other `T` is a compile-time error. +#[derive(Clone)] +pub struct SHA512tParams; + +/// 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, +]; + +/// `const`-evaluable `a == b` for the H(0) arrays (array `PartialEq` is not `const`). +const fn h0_eq(a: &[u64; 8], b: &[u64; 8]) -> bool { + let mut i = 0; + while i < 8 { + if a[i] != b[i] { + return false; + } + i += 1; + } + true +} + +// The IV Generation Function (s. 5.3.6) must reproduce the words listed in s. 5.3.6.1 and +// s. 5.3.6.2. Checked at compile time, so a wrong H(0) can never reach a build. +const _: () = assert!(h0_eq(&sha512t_h0(224), &SHA512_224_H0), "FIPS 180-4 s. 5.3.6.1"); +const _: () = assert!(h0_eq(&sha512t_h0(256), &SHA512_256_H0), "FIPS 180-4 s. 5.3.6.2"); + +/*** SHA512/224 ***/ +impl Algorithm for SHA512tParams<224> { + const ALG_NAME: &'static str = SHA512_224_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_112bit; +} +impl HashAlgParams for SHA512tParams<224> { + const OUTPUT_LEN: usize = 28; // FIPS 180-4 s. 6.6 exception 2: truncated to the left-most 224 bits + const BLOCK_LEN: usize = 128; // FIPS 180-4 Figure 1: block size 1024 bits +} +/// 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]; +} +impl SHA2Params for SHA512tParams<224> {} +impl Sha512Family for SHA512tParams<224> { + // FIPS 180-4 s. 6.6 exception 1: H(0) as specified in s. 5.3.6.1 (checked against it above). + const H0: [u64; 8] = sha512t_h0(224); +} + +/*** SHA512/256 ***/ +impl Algorithm for SHA512tParams<256> { + const ALG_NAME: &'static str = SHA512_256_NAME; + const MAX_SECURITY_STRENGTH: SecurityStrength = SecurityStrength::_128bit; +} +impl HashAlgParams for SHA512tParams<256> { + const OUTPUT_LEN: usize = 32; // FIPS 180-4 s. 6.7 exception 2: truncated to the left-most 256 bits + const BLOCK_LEN: usize = 128; // FIPS 180-4 Figure 1: block size 1024 bits +} +/// 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]; +} +impl SHA2Params for SHA512tParams<256> {} +impl Sha512Family for SHA512tParams<256> { + // FIPS 180-4 s. 6.7 exception 1: H(0) as specified in s. 5.3.6.2 (checked against it above). + const H0: [u64; 8] = sha512t_h0(256); +} + +/// `h0_eq` and `sha512t_h0` are otherwise only evaluated inside `const` assertions, which +/// `cargo mutants` cannot see fail (a mutant that makes `h0_eq` always true just makes the assertions +/// vacuous), so they are exercised at runtime here as well. +#[cfg(test)] +mod const_helper_tests { + use super::*; + + #[test] + fn h0_eq_detects_a_difference_in_any_word() { + assert!(h0_eq(&SHA512_224_H0, &SHA512_224_H0)); + assert!(!h0_eq(&SHA512_224_H0, &SHA512_256_H0)); + for i in 0..8 { + let mut h = SHA512_256_H0; + h[i] ^= 1; + assert!(!h0_eq(&h, &SHA512_256_H0), "word {i}"); + } + } + + /// FIPS 180-4 s. 5.3.6.1 / s. 5.3.6.2: the IV Generation Function reproduces the listed words. + #[test] + fn sha512t_h0_matches_the_listed_words() { + assert_eq!(sha512t_h0(224), SHA512_224_H0); + assert_eq!(sha512t_h0(256), SHA512_256_H0); + assert_eq!( as Sha512Family>::H0, SHA512_224_H0); + assert_eq!( as Sha512Family>::H0, SHA512_256_H0); + // FIPS 180-4 s. 5.3.6: the two-digit and one-digit t paths of the message formatting. + assert_ne!(sha512t_h0(8), sha512t_h0(80)); + assert_ne!(sha512t_h0(80), sha512t_h0(224)); + } +} 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..9080c64a 100644 --- a/crypto/sha2/src/sha256.rs +++ b/crypto/sha2/src/sha256.rs @@ -1,10 +1,11 @@ -use crate::SHA2Params; +use crate::Sha256Family; 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_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 +17,146 @@ const SHA256_K: [u32; 64] = [ 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2, ]; +/// 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 +165,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 +186,81 @@ 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 finalize(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 +286,8 @@ 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. + // byte_count is a u64 byte counter, so this supports messages up to 2^64 bytes (2^67 bits). + // Exceeding it is infeasible in practice; in debug builds the add panics, in release it wraps. self.byte_count += len as u64; let available = 64 - self.x_buf_off; @@ -225,6 +307,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 +323,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.finalize(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.finalize(partial_byte, num_partial_bits, output)) } fn max_security_strength(&self) -> SecurityStrength { @@ -307,7 +362,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..c6676a8e 100644 --- a/crypto/sha2/src/sha512.rs +++ b/crypto/sha2/src/sha512.rs @@ -1,10 +1,12 @@ -use crate::SHA2Params; +use crate::Sha512Family; 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_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,127 +30,217 @@ 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, +]; + +/// 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`). +/// +/// This is a `const fn` so that the IV is computed at compile time; the results for t = 224 and +/// t = 256 are checked at compile time against the words listed in s. 5.3.6.1 and s. 5.3.6.2 (see +/// `lib.rs`). The message is at most 11 bytes, so the SHA-512 computation is always exactly one +/// padded block (s. 5.1.2). +pub(crate) const fn sha512t_h0(t: usize) -> [u64; 8] { + // FIPS 180-4 s. 5.3.6: "t is any positive integer without a leading zero such that t < 512, and t is not 384". + assert!(t > 0 && t < 512 && t != 384, "FIPS 180-4 s. 5.3.6: 0 < t < 512 and t != 384"); + + // FIPS 180-4 s. 5.3.6: H(0)' = the SHA-512 initial hash value (s. 5.3.5), 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 < 512, so at most three digits). + 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); } } } @@ -157,20 +249,20 @@ impl Sha512State { /// 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 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 +271,83 @@ 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 finalize(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 +373,8 @@ 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. + // byte_count is a u64 byte counter, so this supports messages up to 2^64 bytes (2^67 bits). + // Exceeding it is infeasible in practice; in debug builds the add panics, in release it wraps. self.byte_count += len as u64; let available = 128 - self.x_buf_off; @@ -236,6 +393,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 +409,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.finalize(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.finalize(partial_byte, num_partial_bits, output)) } fn max_security_strength(&self) -> SecurityStrength { @@ -316,10 +445,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/cavp_tests.rs b/crypto/sha2/tests/cavp_tests.rs new file mode 100644 index 00000000..bd83d4c5 --- /dev/null +++ b/crypto/sha2/tests/cavp_tests.rs @@ -0,0 +1,211 @@ +//! 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_hex as hex; +use bouncycastle_sha2::{SHA224, SHA256, SHA384, SHA512, SHA512_224, SHA512_256}; +use std::fs; +use std::path::Path; +use std::sync::Once; + +const TEST_DATA_PATH_RELATIVE: &str = "../../../bc-test-data/crypto/sha2"; +const TEST_DATA_PATH: &str = "../bc-test-data/crypto/sha2"; + +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"), + ) +} + +/// 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) = 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 { + 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) = get_test_data(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..d738b54b 100644 --- a/crypto/sha2/tests/sha2_tests.rs +++ b/crypto/sha2/tests/sha2_tests.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod sha2_tests { - use bouncycastle_core::errors::SuspendableError; + use bouncycastle_core::errors::{HashError, SuspendableError}; use bouncycastle_core::traits::{Algorithm, Hash, HashAlgParams, SecurityStrength}; use bouncycastle_core_test_framework::hash::TestFrameworkHash; use bouncycastle_sha2::*; @@ -12,8 +12,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 +23,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 +33,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 +43,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 +212,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 +240,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 +252,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 +298,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 +326,22 @@ 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); } } diff --git a/crypto/sha3/src/keccak.rs b/crypto/sha3/src/keccak.rs index 6188f826..de32fa97 100644 --- a/crypto/sha3/src/keccak.rs +++ b/crypto/sha3/src/keccak.rs @@ -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/lib.rs b/crypto/sha3/src/lib.rs index 841451c0..4e26061b 100644 --- a/crypto/sha3/src/lib.rs +++ b/crypto/sha3/src/lib.rs @@ -34,8 +34,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; diff --git a/crypto/sha3/src/sha3.rs b/crypto/sha3/src/sha3.rs index 4a5bad02..39ff6989 100644 --- a/crypto/sha3/src/sha3.rs +++ b/crypto/sha3/src/sha3.rs @@ -47,8 +47,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 +66,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. diff --git a/crypto/sha3/src/shake.rs b/crypto/sha3/src/shake.rs index 4d1a87a1..263cb0cc 100644 --- a/crypto/sha3/src/shake.rs +++ b/crypto/sha3/src/shake.rs @@ -319,8 +319,12 @@ impl XOF for SHAKEInternal { } // 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); + // The public convention puts the message bits in the most significant bits of partial_byte, + // leading bit first (ASN.1 BIT STRING order, X.690 s. 8.6.2.1). Keccak absorbs a byte + // LSB-first: FIPS 202 Algorithm 10 (h2b) step 3 sets message bit T[8i + j] = b_ij, the bit + // of weight 2^j in byte i. So reverse the bit order and keep the low num_partial_bits bits. + let message_bits = (partial_byte.reverse_bits() as u16) & ((1 << num_partial_bits) - 1); + let mut final_input: u16 = message_bits | (0x0F << num_partial_bits); let mut final_bits = num_partial_bits + 4; if final_bits >= 8 { @@ -376,7 +380,12 @@ impl XOF for SHAKEInternal { let mut buf = [0u8; 1]; self.squeeze_out(&mut buf); - *output = buf[0] & ((1u8 << num_bits) - 1); + // Keccak emits the bits of an output byte LSB-first (FIPS 202 Algorithm 11, b2h: output bit + // T[8i + j] has weight 2^j), and the public convention returns them as the final octet of an + // ASN.1 BIT STRING (X.690 s. 8.6.2.1): first bit in the MSB, unused low bits zero. So reverse + // the bit order and keep the top num_bits bits. The mask is built in u16 so that num_bits == 0 + // cannot overflow (0xFF00 >> 0 truncates to 0x00). + *output = buf[0].reverse_bits() & ((0xFF00u16 >> num_bits) as u8); Ok(()) } diff --git a/crypto/sha3/tests/cavp_tests.rs b/crypto/sha3/tests/cavp_tests.rs index 54069c88..334bb6f9 100644 --- a/crypto/sha3/tests/cavp_tests.rs +++ b/crypto/sha3/tests/cavp_tests.rs @@ -5,12 +5,13 @@ //! under `crypto/sha3/{bit-oriented,byte-oriented}/`. 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. +//! 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`). //! //! Test types exercised (SHA3VS s. 6): //! @@ -96,7 +97,8 @@ 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). +/// 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; @@ -106,7 +108,8 @@ fn sha3_bits(msg: &[u8], len_bits: usize) -> Vec { } 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") + h.do_final_partial_bits(msg[whole_bytes].reverse_bits(), partial_bits) + .expect("partial_bits is in 1..=7") } } @@ -160,18 +163,24 @@ fn run_sha3_monte_file(orientation: &str, filename: &str) { // --------------------------------------------------------------------------------------------- /// 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.absorb_last_partial_byte(msg[whole].reverse_bits(), partial) + .expect("partial is in 1..=7"); } let (out_whole, out_partial) = (out_bits / 8, out_bits % 8); let mut out = x.squeeze(out_whole); if out_partial != 0 { - out.push(x.squeeze_partial_byte_final(out_partial).expect("out_partial is in 1..=7")); + out.push( + x.squeeze_partial_byte_final(out_partial) + .expect("out_partial is in 1..=7") + .reverse_bits(), + ); } out } diff --git a/crypto/sha3/tests/sha3_tests.rs b/crypto/sha3/tests/sha3_tests.rs index 0a3c686f..9a49ba3d 100644 --- a/crypto/sha3/tests/sha3_tests.rs +++ b/crypto/sha3/tests/sha3_tests.rs @@ -621,15 +621,21 @@ pub(crate) mod sha3_test_helpers { 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 { - block[(full_bytes * 8)..].reverse(); - result[full_bytes] = parse_binary(&block[(full_bytes * 8)..]); + let partial_bits = bits - full_bytes * 8; + result[full_bytes] = parse_binary(&block[(full_bytes * 8)..]) << (8 - partial_bits); } result diff --git a/crypto/sha3/tests/shake_tests.rs b/crypto/sha3/tests/shake_tests.rs index e10e5c85..3d2f5fba 100644 --- a/crypto/sha3/tests/shake_tests.rs +++ b/crypto/sha3/tests/shake_tests.rs @@ -49,8 +49,9 @@ mod shake_tests { 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); + // byte [3] of the stream is 0xFF, so its first `i` bits, returned MSB-first, are the top + // `i` set bits. + assert_eq!(out, (0xFF00u16 >> i) as u8); } // success case -- output slice version @@ -59,12 +60,13 @@ mod shake_tests { _ = shake.squeeze(3); let mut out = 0u8; shake.squeeze_partial_byte_final_out(1, &mut out).expect("Squeeze failed"); - assert_eq!(out, 0x01); + assert_eq!(out, 0x80); } /// Regression: squeeze_partial_byte_final() as the *first* squeeze must apply the SHAKE "1111" /// domain suffix (previously it bypassed it and returned raw Keccak output), and must return the - /// low `num_bits` bits of the next output byte (FIPS 202 B.1 bit ordering), zero-extended. + /// first `num_bits` bits of the next output byte (its low bits, FIPS 202 B.1 bit ordering) in the + /// top `num_bits` bits of the result (ASN.1 BIT STRING order), with the unused low bits zero. #[test] fn partial_bit_output_as_first_squeeze_matches_full_output() { let msg = b"abc"; @@ -85,19 +87,25 @@ mod shake_tests { _ = 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"); + assert_eq!( + got, + full.reverse_bits() & ((0xFF00u16 >> n) as u8), + "skip={skip} n={n}" + ); + assert_eq!(got & (0xFFu8 >> n), 0, "unused low bits must be zero"); } } } /// Regression: when the 4 trailing message bits plus the SHAKE "1111" suffix exactly fill a byte, /// the sponge must still switch to squeezing, otherwise the first squeeze appended a second suffix. - /// Vector: NIST CAVP SHA3VS SHAKE128ShortMsg (bit-oriented), Len = 4, Msg = 08. + /// 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(); + shake.absorb_last_partial_byte(0x10, 4).unwrap(); assert_eq!( shake.squeeze(16), bouncycastle_hex::decode("d40238024b040a954d9c2c89daf480e5").unwrap(), @@ -129,7 +137,7 @@ mod shake_tests { // 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(); + b.absorb_last_partial_byte(0xFE, 7).unwrap(); assert_ne!(b.squeeze(32), SHAKE128::new().hash_xof(b"abc", 32)); } @@ -571,15 +579,21 @@ pub(crate) mod shake_test_helpers { 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 { - block[(full_bytes * 8)..].reverse(); - result[full_bytes] = parse_binary(&block[(full_bytes * 8)..]); + let partial_bits = bits - full_bytes * 8; + result[full_bytes] = parse_binary(&block[(full_bytes * 8)..]) << (8 - partial_bits); } result diff --git a/crypto/sm3/Cargo.toml b/crypto/sm3/Cargo.toml new file mode 100644 index 00000000..e2765b0c --- /dev/null +++ b/crypto/sm3/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "bouncycastle-sm3" +version.workspace = true +edition.workspace = true + +[dependencies] +bouncycastle-core.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 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/lib.rs b/crypto/sm3/src/lib.rs new file mode 100644 index 00000000..102e4cc9 --- /dev/null +++ b/crypto/sm3/src/lib.rs @@ -0,0 +1,133 @@ +//! 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 its own message expansion, round 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); +//! assert_eq!(output[..4], [0x66, 0xc7, 0xf0, 0xf4]); +//! ``` +//! +//! More advanced usage will require creating an SM3 object to hold state between successive calls, +//! for example if input is received in chunks and not all available at the same time: +//! +//! ``` +//! 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"); +//! ``` +//! +//! # 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 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(); +//! ``` + +#![forbid(unsafe_code)] +#![forbid(missing_docs)] + +mod sm3; + +pub use self::sm3::{SM3, SUSPENDED_SM3_STATE_LEN}; +use bouncycastle_core::traits::{Algorithm, AlgorithmOID, HashAlgParams, SecurityStrength}; + +/*** 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..d23c011c --- /dev/null +++ b/crypto/sm3/src/sm3.rs @@ -0,0 +1,364 @@ +use bouncycastle_core::errors::{HashError, SuspendableError}; +use bouncycastle_core::suspendable_state::{add_lib_ver, check_lib_ver}; +use bouncycastle_core::traits::{Hash, SecurityStrength, Suspendable}; +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. 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 finalize(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(); + + // byte_count is a u64 byte counter, so this supports messages up to 2^64 bytes. + // Exceeding it is infeasible in practice; in debug builds the add panics, in release it wraps. + 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.finalize(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.finalize(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..ead260c5 --- /dev/null +++ b/crypto/sm3/tests/sm3_tests.rs @@ -0,0 +1,240 @@ +#[cfg(test)] +mod sm3_tests { + use bouncycastle_core::errors::{HashError, SuspendableError}; + use bouncycastle_core::traits::{ + Algorithm, AlgorithmOID, Hash, HashAlgParams, SecurityStrength, + }; + 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/mem_usage_benches/Cargo.toml b/mem_usage_benches/Cargo.toml index a3623aac..5d3e1aed 100644 --- a/mem_usage_benches/Cargo.toml +++ b/mem_usage_benches/Cargo.toml @@ -18,3 +18,7 @@ path = "bench_mlkem_mem_usage.rs" [[bin]] name = "bench_sha3_mem_usage" path = "bench_sha3_mem_usage.rs" + +[[bin]] +name = "bench_aes_mem_usage" +path = "bench_aes_mem_usage.rs" diff --git a/mem_usage_benches/bench_aes_mem_usage.rs b/mem_usage_benches/bench_aes_mem_usage.rs new file mode 100644 index 00000000..00d0acd3 --- /dev/null +++ b/mem_usage_benches/bench_aes_mem_usage.rs @@ -0,0 +1,131 @@ +//! 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_aes_mem_usage > /dev/null +//! +//! ms_print massif.out.835000 +//! +//! or, shoved all into one line: +//! +//! 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 +//! +//! Unlike ML-KEM and ML-DSA, AES has no interesting stack profile: there is no polynomial +//! arithmetic and no sampling, so peak usage is a small constant plus the key schedule. The +//! numbers worth recording in the crate docs are the ones `print_struct_sizes` prints -- the +//! persistent size of each engine -- and the confirmation that per-block work is a fixed, small +//! amount of stack independent of key length. +//! +//! 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_lowmemory::{Aes128, Aes192, Aes256}; +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 compressed, so bit-slicing adds nothing to these. + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); + println!("size_of: {}", size_of::()); +} + +fn key() -> KeyMaterial { + // A fixed non-zero key: an all-zero buffer would be tagged KeyType::Zeroized and rejected. + let mut bytes = [0u8; N]; + for (i, b) in bytes.iter_mut().enumerate() { + *b = (i as u8).wrapping_mul(7).wrapping_add(1); + } + KeyMaterial::::from_bytes_as_type(&bytes, KeyType::SymmetricCipherKey).unwrap() +} + +fn bench_aes128_key_expansion() { + eprintln!("Aes128::new (key expansion)"); + + let aes = Aes128::new(&key::<16>()).unwrap(); + print!("{aes:?}"); +} + +fn bench_aes192_key_expansion() { + eprintln!("Aes192::new (key expansion)"); + + let aes = Aes192::new(&key::<24>()).unwrap(); + print!("{aes:?}"); +} + +fn bench_aes256_key_expansion() { + eprintln!("Aes256::new (key expansion)"); + + let aes = Aes256::new(&key::<32>()).unwrap(); + print!("{aes:?}"); +} + +fn bench_aes128_encrypt_block() { + eprintln!("Aes128::encrypt_block"); + + let aes = Aes128::new(&key::<16>()).unwrap(); + let mut block = [0x11u8; 16]; + aes.encrypt_block(&mut block); + print!("{block:x?}"); +} + +fn bench_aes256_encrypt_block() { + eprintln!("Aes256::encrypt_block"); + + let aes = Aes256::new(&key::<32>()).unwrap(); + let mut block = [0x11u8; 16]; + aes.encrypt_block(&mut block); + print!("{block:x?}"); +} + +fn bench_aes256_decrypt_block() { + eprintln!("Aes256::decrypt_block"); + + let aes = Aes256::new(&key::<32>()).unwrap(); + let mut block = [0x11u8; 16]; + aes.decrypt_block(&mut block); + print!("{block:x?}"); +} + +fn bench_aes256_encrypt_blocks2() { + eprintln!("Aes256::encrypt_blocks2"); + + let aes = Aes256::new(&key::<32>()).unwrap(); + let mut blocks = [[0x11u8; 16], [0x22u8; 16]]; + aes.encrypt_blocks2(&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_aes256_encrypt_block() + // bench_aes256_decrypt_block() + // bench_aes256_encrypt_blocks2() +} diff --git a/mem_usage_benches/lib.rs b/mem_usage_benches/lib.rs index a281a8b2..0445bb89 100644 --- a/mem_usage_benches/lib.rs +++ b/mem_usage_benches/lib.rs @@ -1,3 +1,4 @@ +mod bench_aes_mem_usage; mod bench_mldsa_mem_usage; mod bench_mlkem_mem_usage; mod bench_sha3_mem_usage; diff --git a/src/lib.rs b/src/lib.rs index b46df8cd..777ff298 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +pub use bouncycastle_aes_lowmemory as aes_lowmemory; +pub use bouncycastle_ascon as ascon; pub use bouncycastle_base64 as base64; pub use bouncycastle_core as core; pub use bouncycastle_factory as factory; @@ -8,6 +10,9 @@ pub use bouncycastle_mldsa as mldsa; pub use bouncycastle_mldsa_lowmemory as mldsa_lowmemory; pub use bouncycastle_mlkem as mlkem; pub use bouncycastle_mlkem_lowmemory as mlkem_lowmemory; +pub use bouncycastle_modes as modes; +pub use bouncycastle_padding as padding; pub use bouncycastle_rng as rng; pub use bouncycastle_sha2 as sha2; pub use bouncycastle_sha3 as sha3; +pub use bouncycastle_sm3 as sm3;