Skip to content

tdes: add bouncycastle-tdes, a constant-time table-free TDEA engine - #139

Closed
dghgit wants to merge 55 commits into
release/0.1.4alphafrom
feature/tdes
Closed

dghgit wants to merge 55 commits into
release/0.1.4alphafrom
feature/tdes

Conversation

@dghgit

@dghgit dghgit commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds bouncycastle-tdes: a constant-time, table-free Triple DES (TDEA) engine per SP 800-67r2, on the block cipher key objects: TDES3Key<M> (three-key) and the decryption-only two-key TDES2Key<M>, over the engines TDESInternal and TDES2Internal, with the modes, TKW and CMAC over them. Base: release/0.1.4alpha, currently merged up to f213c9e (the EC Curve trait, generic ECDSA and ECC CDH, and the KDF description).

Description

  • The S-box layer is a compile-time-derived ANF Boolean circuit (no lookup tables, no secret-indexed memory access). The round function's assembly on Cortex-M0 and RISC-V 32I holds no multiply (spread goes through ct::value_barrier off x86-64 and aarch64, as AES's round-key widening does); dev_scripts/check_aes_ct_asm.sh and the aes-ct-asm CI job include it, with a plain-shift spread as the positive control.
  • TDESKey<P, M> mirrors AESKey: the key holds the mode's state, its workers borrow it, and which role pair a key carries is decided by its mode (CBC<Blocks>, CBC<PKCS7>, ECB<..>, CFB, CFB8, CTR<6, 8>, TKW). The aliases TDES_CBC<'k, Dir, Pad>, TDES_CFB, TDES_CFB8, TDES_CTR, TDES_ECB, TDES_TKW<'k, Dir> and the TDES2_* decryptors and unwrapper name what the keys hand out. Keys and engines implement PartialEq / Eq in constant time over the expanded schedule.
  • TDESInternal::new rejects non-distinct component keys and exactly the 64 weak, semi-weak and possibly-weak DES keys of SP 800-67r2 Sec 3.3.2 (a structural check, not a table) as KeyMaterialError::WeakKey; TDES2Internal::new rejects Key1 = Key2 and either component being weak.
  • Two-key TDEA is decryption-only, at compile time. TDES2Internal sets BlockPermutation::DECRYPT_ONLY (this library's policy for two-key alone; three-key encryption stays for the OpenSSL-provider interop, said so in the crate's Security Considerations), and every route that would make an encrypting worker over a decrypt-only permutation asserts it in an inline const: the modes' encrypt_init and encrypt_init_nonce, StreamCipher::from_keystream (crate-private, through the sealed KeyStreamPolicy), Tkw's wrapping constructor and CMAC::generate_init. A two-key key's create_encryptor, create_encryptor_nonce, create_wrapper and create_generator do not compile; its decryptors, unwrapper and verifier work. compile_fail doctests on TDES2Internal pin each route. The two-key docs say what the gate does not stop: CFB and CTR decryption are the forward cipher, so a deployment that must rule two-key encryption out altogether exposes only the CBC and ECB decryptors.
  • TKW (SP 800-38F Sec 7): cipher::modes::TKW / Tkw<'k, P, Dir> is Kw at half the width (4-byte semiblocks, 32-bit counter, ICV3), generic over any 64-bit block permutation; TDES3Key<TKW> wraps and unwraps through the KeyWrappingKey / KeyUnwrappingKey roles, TDES2Key<TKW> unwraps only. Alongside, Kw and Kwp carry their direction in the type (Kw<'k, P, Dir>), so the AES_KW_*, AES_KWP_*, ARIA_KW_*, ARIA_KWP_*, Camellia_KW_*, SM4_KW and SM4_KWP aliases gain the Dir parameter: an API change to those crates, not an addition, listed in the 0.1.4 release notes.
  • CMAC over TDEA (SP 800-38B Sec 5.3) is reached through the generic BlockCipherKey<TDESInternal, CMAC<8>> (TDESKey carries no MAC role); the two-key form verifies only (SP 800-131A Rev 2 Table 9). The engines name it TDES_CMAC / TDES2_CMAC.
  • The data limit (SP 800-67r2 Sec 3.4, 2^20 blocks per key bundle) is stated per mode in the crate docs as the blocks TDEA processes: 8 MiB for ECB, CBC, CFB64 and CTR, 1 MiB for CFB8, 699,048 bytes for TKW. The library cannot count across the uses of a key, so the limit is the key manager's; the tdes-* CLI encrypt commands refuse input past it in one invocation, through an InputLimit on the shared block- and stream-mode loops.
  • CLI subcommands tdes-* / tdes2-* for every mode and tdes-tkw / tdes2-tkw, through the key objects, with the aes*-* contract: tdes-cbc and tdes2-cbc are CBC<PKCS7> at any length, every command with an IV or nonce takes --iv/--iv-file (--nonce as its alias), every command takes -d/--direction, and the tdes2-* commands accept only decrypt there. The CLI's block-mode plumbing takes the block length as a const parameter so the shared loops serve 64-bit blocks.

Changes outside the new crate

core: BlockPermutation::DECRYPT_ONLY, Direction::IS_ENCRYPTING, KeyMaterialError::WeakKey, and the DECRYPT_ONLY contract naming MAC generators. cipher: the const asserts at every encrypting construction (modes, keystream wrapper, Tkw, CMAC), StreamCipher::from_keystream crate-private behind the sealed KeyStreamPolicy, the TKW mode, and Kw / Kwp with Dir in the type (the alias change above). aes, aria, camellia, sm4: their key-wrap aliases take Dir. cli: generic block length, DecryptOnlyAction, InputLimit, aes_kw_cmd::run_with. dev_scripts: the TDES round function in the constant-time assembly check. cli also raises the main-thread stack from 8 MiB to 32 MiB (67ae72d), because the debug binary overflowed the 8 MiB on --help once this branch's subcommands joined the release's. The 0.1.4 release notes carry all of it.

Validation

  • NIST CAVP T{E,C}{CB,BC,FB64,FB8}MMT{2,3} vectors, both key sizes, both directions: CBC and CFB through the keys, ECB through the engines. NIST CAVP TKW_AE / TKW_AD (five plaintext lengths, 100 trials each, 20 forgeries per section) through TDES3Key<TKW> and TDES2Key<TKW>, plus RFC 3394-style reference tests over the raw permutation. NIST's TDES_CMAC samples (three-key generate and verify, two-key verify and refuse) and the ACVP CMAC-TDES.4014545 / .4014546 sets (three-key: 72 gen and 180 ver, truncated tags on their prefix; two-key: the 60 full-tag ver cases, the 120 truncated-tag ones counted and skipped since two-key cannot compute a tag here). All read through the test framework's bc_test_data loader. The TKW files are a recent addition to bc-test-data (crypto/aes_tdes_vectors/TDES/TKW_A{E,D}.txt, 2026-11-06; the crypto/aes_tdes_vectors/CMAC/CMAC-TDES.*.json sets have been there since August). The loader skips only when the bc-test-data directory is absent; when it is present but a file is missing it panics, so a checkout older than the TKW files fails tkw_ae, tkw_ad and the TKW CLI tests rather than passing vacuously. Update it alongside this branch.
  • The weak-key predicate is asked about every rotation-invariant (C0, D0) pair, all 256, and must refuse exactly the 64 the spec lists; random keys with C0 = D0 and no period are accepted, which an OR-to-XOR mutant in the predicate would not do.
  • Shared frameworks over every mode key: TestFrameworkFromKeyMaterial, TestFrameworkBlockCipher, TestFrameworkSymmetricCipher, TestFrameworkStreamCipher, TestFrameworkElectronicCodeBook, and the key-wrap and MAC-key suites.
  • cli/tests/tdes_cli_tests.rs and tdes_tkw_cli_tests.rs pin COUNT = 0 of the MMT3 files and TKW through the binary in both directions, and the two-key [DECRYPT] vectors through tdes2-*; cli/tests/test_tdes.sh checks the per-mode data limit end to end.
  • cargo test --workspace, cargo test --workspace --all-targets --no-run, rustdoc, cargo +nightly fmt --check, clippy adds nothing in the new code.
  • cargo mutants -p bouncycastle-tdes after the review fixes: 500 mutants, 422 caught, 41 unviable, 33 missed (31 bit-permutation XOR/OR equivalences in spread, permute_p, lookup, rotl28, pc1 and expand; a scoped re-run over schedule.rs and des.rs at fd55fc5 is 333 mutants, 303 caught, 30 missed, every miss a |-to-^ on disjoint bits, and two loop-bound mutants in the compile-time anf_constants), 4 timeouts (the compile-time ANF constant derivation).

Review rounds

  • Round 1 (2026-11-04): is_weak matched 256 key classes, not the spec's 64; the gate covered create_encryptor only, and a two-key decryptor's keystream could be wrapped as an encryptor; spread folded back into a muls on Cortex-M0; the CAVP suites passed vacuously when a file was missing; the NIST withdrawal notice quoted verbatim; doc and help nits. All fixed.
  • Round 2 (2026-11-05): SuspendableKeyed removed from every cipher worker (Security: Prevent keystream reuse when cloning or resuming stateful cipher workers #173, by merge of release 1bee29d): the resume route and its doctest go with it.
  • Round 3 (2026-11-06): TKW with its direction in the type, so a two-key unwrapper cannot wrap; the tdes-tkw and tdes-cfb8 limits count the blocks TDEA processes; the wrap docs cite Table 6; nits across cipher, core, cli and dev_scripts.
  • Round 4 (2026-11-10): the MAC rework (The MAC worker splits into MACGenerator and MACVerifier, with the tag length in the type #194) taken by merge; CMAC's generating constructor refuses a decrypt-only permutation (generate_init alone, the verifier works); the DECRYPT_ONLY contract names MAC generators and Table 9; the data limit stated per mode; release-notes nits.
  • Round 5 (2026-11-10, Francis, at 9249570): (1) the modes' encrypt_init_nonce bodies from the fix/supplied-nonce merge carried no DECRYPT_ONLY assert, so eight supplied-IV routes encrypted under two-key TDEA (confirmed against OpenSSL): the five bodies now carry it and a doctest pins create_encryptor_nonce. (2) The CMAC test round 4 cited was never added to the tree: committed, with the ACVP sets beside it. (3) CMAC's empty name: TDES_CMAC / TDES2_CMAC. Plus this description rewritten and the bc-test-data note above. The round-5 changes are const asserts, tests and name constants; mutants were not re-run for them.

AI Usage Statement

  • Yes, non-trivial code changes were generated by AI

Assisted-by: Claude:claude-sonnet-5
Assisted-by: Claude:claude-mythos-5-1

🤖 Generated with Claude Code

dghgit added a commit that referenced this pull request Sep 19, 2026
mem_usage_benches/src/lib.rs makes every bench source a module of a lib
target, so its //! header is rustdoc'd and an indented block compiles
as a Rust doctest by default; the valgrind/ms_print shell one-liners
here weren't fenced, so `cargo test --workspace` failed to compile
them as Rust (CI on PR #139). Fenced as ```text like the other bench
binaries, per CLAUDE.md's note on this exact pitfall.

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dghgit added a commit that referenced this pull request Sep 19, 2026
Same issue as PR #139 (tdes): mem_usage_benches/src/lib.rs makes every
bench source a module of a lib target, so its //! header is rustdoc'd
and an indented block compiles as a Rust doctest by default. Fenced
as ```text like the other bench binaries.

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@hubot
hubot deleted the feature/tdes branch September 19, 2026 01:02
@dghgit
dghgit restored the feature/tdes branch September 19, 2026 04:26
@ounsworth

ounsworth commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

We had agreed not to add more ciphers until we get through reviewing and locking in the overall architecture via the AES work. Closing this PR for now.

@ounsworth ounsworth closed this Sep 19, 2026
@dghgit dghgit reopened this Sep 19, 2026
@npajkovsky

Copy link
Copy Markdown
Collaborator

Why on earth would anybody want 3DES?

NIST deprecated 3DES for new use via SP 800-131A Rev. 2 (disallowed after December 31, 2023) and withdrew SP 800-67 Rev. 2 effective January 1, 2024.

@dghgit

dghgit commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Fair question, even though, as much as I hate to say it, I could point you at people still using DES, some of who should definitely know better... We still support it in BC-FIPS as well, as far as I can tell it's mainly used for dealing with legacy encrypted data these days. In this case this one is simply here as it's on the OpenSSL provider list, so it's one of the algorithms they'll want to invoke (at least for a while).

@hubot
hubot deleted the feature/tdes branch September 28, 2026 15:21
@hubot
hubot deleted the branch release/0.1.4alpha September 28, 2026 15:21
@hubot
hubot restored the feature/tdes branch September 28, 2026 15:45
@dghgit
dghgit changed the base branch from feature/xof-cshake to feature/simple-ciphers September 28, 2026 15:49
roy-basmacier pushed a commit to roy-basmacier/bc-rust that referenced this pull request Sep 30, 2026
Implements the KeyWrapper / KeyUnwrapper traits from the previous commit
for the two 128-bit-block key-wrap algorithms of NIST SP 800-38F, KW
(Sec 6.2; RFC 3394) and KWP (Sec 6.3; RFC 5649), generically over any
ElectronicCodeBook permutation, and publishes the AES instantiations.

modes:
- kw.rs: Kw<P, KEK_LEN>. The wrapping function W and its inverse
  (Algorithms 1 and 2) run in place on the caller's buffer, visiting the
  registers in the order the spec's shift register would present them,
  which RFC 3394 Sec 2.2.1 gives as the equivalent index-based form; the
  one block of scratch is a Secret. KW-AE/AD are Algorithms 3 and 4.
- kwp.rs: Kwp<P, KEK_LEN>. KWP-AE/AD are Algorithms 5 and 6, including
  the single-block path for plaintexts of at most one semiblock. The ICV,
  length-field and padding checks of Algorithm 6 steps 4, 7 and 8 are all
  made, combined bitwise, and reported as the one DecryptionFailed.
- The fixed-length API rejects invalid (KEY_LEN, CT_LEN) pairs at compile
  time via const assertions on the Table 1 limits; the run-time API
  returns InvalidInputLength outside them. Output buffers are zeroized on
  every failure. TKW (TDEA) is not included: it needs a 64-bit-block
  engine (PR bcgit#139).

aes:
- kw.rs: AES_KW_128/192/256 and AES_KWP_128/192/256 aliases, with the
  RFC 3394 Sec 4.1 and RFC 5649 7-octet vectors as doctests. No OIDs yet,
  in line with the other AES mode aliases.

tests:
- modes/tests/kw_tests.rs: all six RFC 3394 Sec 4 vectors and both
  RFC 5649 Sec 6 examples via TestFrameworkKeyWrap::test_kat, the shared
  framework for every AES key length at 26 payload lengths (every padding
  length of the single-block case included), and pins for the properties
  that distinguish KW, KWP and a raw block encryption.
- modes/tests/acvp_kw_tests.rs: the NIST ACVP-AES-KW and ACVP-AES-KWP
  sets from bc-test-data, 1800 cases each, through both APIs, including
  the 171 + 178 forged ciphertexts that must be rejected.
- aes/tests/kw_alias_tests.rs: the aliases name the right permutation and
  pass the framework.

benches: KW/KWP wrap and unwrap of a 256-bit key, and KWP over 4 KiB.

cli: aes{128,192,256}-kw and -kwp with wrap|unwrap, reading all of stdin
(key wrap is one-shot), sharing key loading with the other AES commands;
cli/tests/aes_kw_cli_tests.rs replays the RFC vectors in both directions
and pins the length rules, the fail-closed unwrap and the help text.

Assisted-by: Claude:claude-fable-5-1
dghgit added a commit that referenced this pull request Oct 24, 2026
mem_usage_benches/src/lib.rs makes every bench source a module of a lib
target, so its //! header is rustdoc'd and an indented block compiles
as a Rust doctest by default; the valgrind/ms_print shell one-liners
here weren't fenced, so `cargo test --workspace` failed to compile
them as Rust (CI on PR #139). Fenced as ```text like the other bench
binaries, per CLAUDE.md's note on this exact pitfall.

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@dghgit
dghgit changed the base branch from feature/simple-ciphers to feature/key-rework-memory October 24, 2026 11:03
dghgit added a commit that referenced this pull request Oct 24, 2026
Same issue as PR #139 (tdes): mem_usage_benches/src/lib.rs makes every
bench source a module of a lib target, so its //! header is rustdoc'd
and an indented block compiles as a Rust doctest by default. Fenced
as ```text like the other bench binaries.

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hubot pushed a commit that referenced this pull request Oct 24, 2026
Same issue as PR #139 (tdes): mem_usage_benches/src/lib.rs makes every
bench source a module of a lib target, so its //! header is rustdoc'd
and an indented block compiles as a Rust doctest by default. Fenced
as ```text like the other bench binaries.

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dghgit added a commit that referenced this pull request Oct 24, 2026
…```text

Same issue as PR #139 (tdes): mem_usage_benches/src/lib.rs makes every
bench source a module of a lib target, so its //! header is rustdoc'd
and an indented block compiles as a Rust doctest by default. Fenced
as ```text like the other bench binaries.

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hubot pushed a commit that referenced this pull request Oct 24, 2026
…```text

Same issue as PR #139 (tdes): mem_usage_benches/src/lib.rs makes every
bench source a module of a lib target, so its //! header is rustdoc'd
and an indented block compiles as a Rust doctest by default. Fenced
as ```text like the other bench binaries.

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dghgit added a commit that referenced this pull request Oct 25, 2026
Same issue as PR #139 (tdes): mem_usage_benches/src/lib.rs makes every
bench source a module of a lib target, so its //! header is rustdoc'd
and an indented block compiles as a Rust doctest by default. Fenced
as ```text like the other bench binaries.

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hubot pushed a commit that referenced this pull request Oct 25, 2026
Same issue as PR #139 (tdes): mem_usage_benches/src/lib.rs makes every
bench source a module of a lib target, so its //! header is rustdoc'd
and an indented block compiles as a Rust doctest by default. Fenced
as ```text like the other bench binaries.

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dghgit pushed a commit to roy-basmacier/bc-rust that referenced this pull request Oct 26, 2026
Implements the KeyWrapper / KeyUnwrapper traits from the previous commit
for the two 128-bit-block key-wrap algorithms of NIST SP 800-38F, KW
(Sec 6.2; RFC 3394) and KWP (Sec 6.3; RFC 5649), generically over any
ElectronicCodeBook permutation, and publishes the AES instantiations.

modes:
- kw.rs: Kw<P, KEK_LEN>. The wrapping function W and its inverse
  (Algorithms 1 and 2) run in place on the caller's buffer, visiting the
  registers in the order the spec's shift register would present them,
  which RFC 3394 Sec 2.2.1 gives as the equivalent index-based form; the
  one block of scratch is a Secret. KW-AE/AD are Algorithms 3 and 4.
- kwp.rs: Kwp<P, KEK_LEN>. KWP-AE/AD are Algorithms 5 and 6, including
  the single-block path for plaintexts of at most one semiblock. The ICV,
  length-field and padding checks of Algorithm 6 steps 4, 7 and 8 are all
  made, combined bitwise, and reported as the one DecryptionFailed.
- The fixed-length API rejects invalid (KEY_LEN, CT_LEN) pairs at compile
  time via const assertions on the Table 1 limits; the run-time API
  returns InvalidInputLength outside them. Output buffers are zeroized on
  every failure. TKW (TDEA) is not included: it needs a 64-bit-block
  engine (PR bcgit#139).

aes:
- kw.rs: AES_KW_128/192/256 and AES_KWP_128/192/256 aliases, with the
  RFC 3394 Sec 4.1 and RFC 5649 7-octet vectors as doctests. No OIDs yet,
  in line with the other AES mode aliases.

tests:
- modes/tests/kw_tests.rs: all six RFC 3394 Sec 4 vectors and both
  RFC 5649 Sec 6 examples via TestFrameworkKeyWrap::test_kat, the shared
  framework for every AES key length at 26 payload lengths (every padding
  length of the single-block case included), and pins for the properties
  that distinguish KW, KWP and a raw block encryption.
- modes/tests/acvp_kw_tests.rs: the NIST ACVP-AES-KW and ACVP-AES-KWP
  sets from bc-test-data, 1800 cases each, through both APIs, including
  the 171 + 178 forged ciphertexts that must be rejected.
- aes/tests/kw_alias_tests.rs: the aliases name the right permutation and
  pass the framework.

benches: KW/KWP wrap and unwrap of a 256-bit key, and KWP over 4 KiB.

cli: aes{128,192,256}-kw and -kwp with wrap|unwrap, reading all of stdin
(key wrap is one-shot), sharing key loading with the other AES commands;
cli/tests/aes_kw_cli_tests.rs replays the RFC vectors in both directions
and pins the length rules, the fail-closed unwrap and the help text.

Assisted-by: Claude:claude-fable-5-1
@dghgit

dghgit commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

The review round of PR #140 applied here, in 5f367891 and e7b7e68e (branch head e7b7e68e):

  1. Key roles. The keys never had the MAC-key path, but ran a check_key_type ahead of the engine's own validation, which is the same check for every mode that has a role; it is gone. A scoped cargo mutants on key.rs is 0 missed. Wycheproof carries no TDEA vectors, so there is no suite to add.
  2. CLI. tdes-cbc and tdes2-cbc are CBC<PKCS7> at any length; every command with an IV or nonce takes --iv/--iv-file; every command takes -d/--direction, the tdes2-* ones with decrypt as the only value the parser accepts; ECB stays unpadded with no IV, as aes*-ecb is. The commands had no CLI tests: cli/tests/tdes_cli_tests.rs pins COUNT = 0 of TCBCMMT3 (plus the PKCS7 block, computed from the engine), TCFB64MMT3, TCFB8MMT3 and TECBMMT3 through the binary in both directions, encryption under --iv, and the two-key [DECRYPT] vectors through the tdes2-* commands, read from bc-test-data at test time; plus the round trips, padding against alignment, a supplied CTR nonce held to the library, the parser's refusal of tdes2-* -d encrypt, and the bundle rejections.
  3. Heading is # 🚨 Security Considerations 🚨, anchor updated.
  4. Memory Usage tabulates measured per-entry-point stack for both engines (new 800 B / 544 B, one-block 48 B, two-block 80 B, four-block 112 B; the two-key engine's decrypting entry points only), by the same method and toolchain as the SM4 and Camellia tables.
  5. The public-behaviour unit tests in both engines (sizes, names and strengths, the ENCRYPTION_APPROVED gate) moved to alg_name_tests.rs; the split/join, parity and schedule pins stay.
    6–13. Manifest comment, the reference-style link in key.rs, and every doc line within 100 columns.
  6. cargo mutants -p bouncycastle-tdes after the changes: 500 mutants, 422 caught, 41 unviable, 33 missed (the bit-permutation XOR/OR equivalences and two loop bounds in the compile-time ANF derivation), 4 timeouts (that derivation). PR description updated.

cargo test --workspace, --all-targets --no-run, rustdoc with warnings denied and rustfmt are clean.

@dghgit

dghgit commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Roy's two follow-ups from PR #140 applied here too, head c659ff62:

  1. Stack table. The table reported each entry point's measure closure frame alone. The disassembly of the remark build shows the key-expansion closures calling the engine's new out of line, which calls the schedule's expand, and the one-block closures calling encrypt_block / decrypt_block; the two- and four-block entry points are inlined into their closures, so those rows stand. Summed as the deepest chain with return addresses (rustc 1.97 nightly, x86-64): new 1304 B for the three-key engine and 920 B for the two-key one, one-block calls 104 B, two- and four-block calls 80 B and 112 B as before. The bench's method note says to sum the chain and lists each frame; its [ENCRYPT] is no longer read as a rustdoc link (74b718ee).
  2. MAC-mode key type. The check against Mode::KEY_TYPE is back ahead of the engine's own on both bundle lengths, as AESKey has it; under CMAC<8> cipher material is refused for its type and MAC material by the engine, so nothing passes and generate fails the same way. mode_key_tests pins the constant, both materials and generate for the three-key and two-key forms. Scoped mutants on key.rs: 27 tested, none missed (c659ff62).

@dghgit dghgit added this to the 0.1.4alpha milestone Oct 31, 2026
@dghgit
dghgit force-pushed the feature/key-rework-memory branch from 3c170c9 to 81cef74 Compare November 3, 2026 02:36
dghgit added a commit that referenced this pull request Nov 3, 2026
mem_usage_benches/src/lib.rs makes every bench source a module of a lib
target, so its //! header is rustdoc'd and an indented block compiles
as a Rust doctest by default; the valgrind/ms_print shell one-liners
here weren't fenced, so `cargo test --workspace` failed to compile
them as Rust (CI on PR #139). Fenced as ```text like the other bench
binaries, per CLAUDE.md's note on this exact pitfall.

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread alpha_0.1.4_release_notes.md Resolved
@officialfrancismendoza

Copy link
Copy Markdown
Contributor

@dghgit Fourth-round review of 9e40efe5. Every round-3 item is fixed and checks out. The TKW direction is in the type: all five unwrapper routes are now type errors, the gated ones are still E0080, and the two new compile_fail doctests fail for the reason they state. tdes-tkw and tdes-cfb8 stop exactly at 699,048 bytes and 2^20 bytes, with nothing written past the bound, and decryption is unlimited. The asm check now fails when a checked body is missing (I renamed tdes_rounds in a scratch copy to confirm). TKW still matches all CAVP cases and my independent implementation, and 22,921 adversarial checks fail closed. Round 2's mode-gate probes give the same results after all seven merges. One new item comes from the fix/block-cipher-key merge: the base's generic BlockCipherKey over TDES2Internal generates two-key CMAC tags (TDES4-001, Low, needs a hazmat type name). Two doc nits remain.

…ncryptor_nonce and the _nonce one-shots on SymmetricEncryptionKey and BlockEncryptionKey, encrypt_init_nonce on the modes, CLI --iv through them with --nonce as its alias) into feature/tdes: conflicts in cli/src/cbc_cmd.rs and ecb_cmd.rs (the three-argument start_encryptor with this branch's BLOCK_LEN const generics); TDESKey gains create_encryptor_nonce forwarding to the modes, tdes_cbc_cmd and tdes_ecb_cmd take the new start_encryptor, the eight TDES --iv / --iv-file args take the --nonce / --nonce-file aliases, and ChaCha20Key's create_encryptor_nonce goes through this branch's stream_cipher_over
…t split into MACGenerator / MACVerifier with the tag length in the type, the direction markers in bouncycastle_core::direction) into feature/tdes: conflict in cipher/src/lib.rs, resolved by taking core's markers through cipher's re-export, keeping a crate-local sealed trait for KeyStreamPolicy, and carrying this branch's Direction::IS_ENCRYPTING (the DECRYPT_ONLY check in StreamCipher) onto core's Direction
PR #139 round four, TDES4-001: the generic BlockCipherKey over TDES2Internal could generate two-key TDEA CMAC tags, which SP 800-131A Rev 2 Table 9 lists as "Disallowed" (verification "Legacy use"), because CMAC carried no DECRYPT_ONLY check. With the MAC worker split from PR #194 the generator and the verifier have separate constructors, so MacMode::generate_init for CMAC gets the same inline const assert the encrypting cipher constructors and the key wrappers carry, and verify_init does not: a two-key key under CMAC verifies and cannot produce a tag, and three-key is unchanged.

Assisted-by: Claude:claude-mythos-5-1
PR #139 round four: the doc says a MAC generator is among the constructors the gate covers, a verifier is not, and cites SP 800-131A Rev 2 Table 9 beside Tables 1 and 6.

Assisted-by: Claude:claude-mythos-5-1
…mode

PR #139 round four. The two-key section says two-key TDEA verifies CMAC tags and cannot generate them, that TDESKey carries no MAC role, and that CMAC over either bundle is reached through bouncycastle_cipher::BlockCipherKey; the policy paragraph keeps three-key CMAC generation on the same terms as three-key encryption and wrapping (Table 9: "Deprecated through 2023, Disallowed after 2023"). TDES2Internal gains a compile_fail doctest for create_generator on BlockCipherKey<TDES2Internal, CMAC<8>>, and cmac_generic_key_tests checks all four NIST CMAC-TDES samples (csrc.nist.gov TDES_CMAC.pdf): three-key generated and verified, two-key verified streamed and one-shot with a changed tag refused. The data-limit section gives the figure per mode as the blocks TDEA processes: 8 MiB for ECB, CBC, CFB64 and CTR, 1 MiB for CFB8, 699,048 bytes of TKW plaintext.

Assisted-by: Claude:claude-mythos-5-1
…a and SM4 key-wrap aliases gain Dir

PR #139 round four nits.

Assisted-by: Claude:claude-mythos-5-1
@dghgit

dghgit commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Round four is in at 9249570, after the branch took release/0.1.4alpha 6144224 (the PR #194 MAC rework squash) in 7cb618d. TDES4-001 is fixed on the generating constructor: the MAC split gives CMAC separate generate_init / verify_init, so the DECRYPT_ONLY assert sits on generate_init alone and two-key TDEA verifies CMAC tags (Table 9 "Legacy use") without being able to produce them (Table 9 "Disallowed"); three-key CMAC generation through BlockCipherKey is kept on the same terms as three-key encryption and TKW wrapping and the docs say so. Pinned by a compile_fail doctest on TDES2Internal and by cmac_generic_key_tests against NIST's CMAC-TDES examples for both bundles. The two nits are done: the TDEA limit section gives the per-mode figures, and the release notes list the ARIA, Camellia and SM4 key-wrap aliases and state the two-key CMAC position. The merge of the rework also adapted this branch's Direction::IS_ENCRYPTING onto core::direction and kept a crate-local sealed trait for KeyStreamPolicy. Checks on the tip: workspace build with all targets, the tdes, cipher, aes, aria, camellia, sm4 and core suites with doctests, rustdoc with warnings denied, nightly fmt, test_tdes.sh 10/10 and test_aes_mac.sh 6/6.

Comment thread crypto/tdes/src/key.rs Resolved
Comment thread crypto/tdes/src/lib.rs Resolved
Comment thread crypto/tdes/src/hazmat/tdes.rs Resolved
Comment thread crypto/tdes/src/hazmat/tdes.rs Resolved
@officialfrancismendoza

officialfrancismendoza commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

@dghgit With #194's split constructors, the gate is on CMAC::generate_init alone, so every two-key generating route I tried is E0080, the verifier works, and the new doctest fails for the reason it states. Previous round 4 fixes 2 and 3 are done. The MAC-rework merge resolution is clean: Direction is sealed with the right IS_ENCRYPTING, and KeyStreamPolicy is still sealed. One new problem comes from the fix/supplied-nonce merge: the modes' encrypt_init_nonce bodies carry no DECRYPT_ONLY assert, so TDES2Key::<CBC<PKCS7>>::encrypt_nonce(&iv, msg) and seven other routes produce two-key TDEA ciphertext, which I confirmed against OpenSSL (TDES5-001, Medium). Five copies of the existing assert fix it; I checked that in a scratch copy. The CMAC test the round-4 reply cites is not in the PR (round 5 issue 2), and CMAC over TDEA reports an empty name (round 5 issue 3).

Two other additional items (neither merge-blocking, but worth mentioning):

  1. Outdated PR description. The description still omits TKW, the direction-typed key-wrap API changes, CMAC support, and the updated mode-specific TDEA limits. It also describes some changes as additive even though they alter existing APIs.
  2. Test-data dependency. The TKW tests require sufficiently recent bc-test-data files. An older checkout can fail because the required vectors are missing. A short note in the PR description would help future contributors.

…e too

Review round five of PR #139 (issue 1): the const assertion on BlockPermutation::DECRYPT_ONLY
sat in the modes' encrypt_init bodies alone, and the encrypt_init_nonce twins that the
fix/supplied-nonce merge brought in had none, so a two-key key's create_encryptor_nonce,
encrypt_nonce, encrypt_nonce_out and encrypt_inplace_nonce compiled and encrypted under CBC
(both paddings), CFB, CFB8 and padded ECB; CTR was already refused through the stream cipher's
keystream policy. The five bodies now carry the same assertion, so every encrypting
constructor over the two-key engine is a compile error, pinned by a compile_fail doctest on
create_encryptor_nonce beside the create_encryptor one; the three-key twins build as before.

Assisted-by: Claude:claude-mythos-5-1
Review round five of PR #139 (issue 2): cmac_generic_key_tests.rs, which 0bf4d98's message
cites, was never added to the tree, so nothing exercised CMAC over a 64-bit block (the cipher
crate's suite runs a 16-byte toy). It is committed now: NIST's TDES_CMAC samples, the three-key
bundle generating and verifying and the two-key one verifying and refusing a changed tag.
cmac_bc-test-data.rs runs the ACVP sets already in bc-test-data: CMAC-TDES.4014545 (three-key:
72 gen and 180 ver, truncated tags compared on their prefix, full tags through verify) and
CMAC-TDES.4014546 (two-key: the 60 full-tag ver cases through verify; the 120 truncated-tag
cases are counted and skipped, since two-key TDEA cannot compute a tag here).

Assisted-by: Claude:claude-mythos-5-1
Review round five of PR #139 (issue 3): CMAC_ALG_NAME was empty on both engines, under a
comment saying no MAC mode is offered, while the crate documents CMAC through the generic
BlockCipherKey, and ModeNames reserves the empty name for a mode the permutation does not
implement. The names are TDES_CMAC and TDES2_CMAC, as TKW's are, with alg_name_tests
checking both directions of the former and the verifying direction of the latter.

Assisted-by: Claude:claude-mythos-5-1
@dghgit

dghgit commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, Francis. Round five is on the branch at 3c7fa45, three commits over 9249570: 3c8c1f1 (cipher, tdes) puts the DECRYPT_ONLY assert in the five encrypt_init_nonce bodies and pins create_encryptor_nonce with a compile_fail doctest; e14b150 (tdes) commits the CMAC test that 0bf4d98 cited but never added, plus a runner for the ACVP CMAC-TDES sets in bc-test-data; 3c7fa45 (tdes) names CMAC TDES_CMAC / TDES2_CMAC. Replies are on each thread. The PR description is rewritten: TKW, the Dir parameter on the key-wrap aliases (called out as an API change, not an addition), CMAC, the per-mode limits, the review rounds, and a note that the TKW and CMAC suites need a recent bc-test-data checkout. Verified: the tdes, cipher, aes, aria, camellia, sm4, chacha20, core and cli suites, the tdes doctests, fmt and clippy on the changed lines; mutants not re-run for asserts, tests and constants.

…ment and ECC CDH with the X9.63 and concatenation KDFs; PR #180 squash: BLAKE2b-512, BLAKE2s-256 and BLAKE3; the post-#193 cleanups) into feature/tdes

Two keep-both conflicts: the mem_usage_benches manifest (the tdes stack bench beside the BLAKE2 and
BLAKE3 ones) and the umbrella crate's re-exports (tdes beside x963kdf). One adaptation: the CLI's
main thread stack goes from 8 MiB to 32 MiB. In a debug build the startup frame for the full
subcommand set already sat close to 8 MiB on the release, and the eleven tdes-* / tdes2-*
commands take it past, so the debug binary aborted on every invocation, --help included (the
release-profile binary was unaffected). That bound is the release's to set once; it is raised
here so the branch's shell suites run. The tdes, cipher, factory and cli suites, the tdes, blake,
eccdh, concatkdf, x963kdf and hkdf CLI shell suites and the all-targets build pass.
…generator per curve in bouncycastle-ec's hazmat, which the ecdsa and eccdh keygens and derive_pks now wrap) into feature/tdes
…trait, fix/eccdh-generic, fix/ecdsa-generic and fix/kdf-description-followups squashes: curve markers and the Curve trait in bouncycastle-ec, ECC CDH and ECDSA written once over Curve and the hash, KdfParams over an HSM-readable KdfDescription, and the KDF description's follow-ups) into feature/tdes

No conflicts and no adaptation: the five squashes touch ec, ecdsa, eccdh and the KDF crates, which the TDES crate, its CLI commands and its stack harness do not reach; cli/src/main.rs and src/lib.rs auto-merged on neighbouring lines.

Assisted-by: Claude:claude-mythos-5-1
@officialfrancismendoza

Copy link
Copy Markdown
Contributor

@dghgit Sixth-round review of 7d60a21e. All three round-5 items are fixed, and I checked each independently.

  1. The DECRYPT_ONLY assert is now in all five encrypt_init_nonce bodies. 13 supplied-IV routes over TDES2Internal (every padding, TDES2Key, BlockCipherKey, the one-shots, &dyn, a generic caller, the hazmat mode trait) are E0080, each pointing inside the new assert. The drawn-IV forms, CTR, TKW wrapping and CMAC generation stay refused, the three-key twins build and match OpenSSL, and I found no other public constructor of an encrypting worker.
  2. The CMAC tests are in. Changing R_64's 0x1B to 0x1A in a scratch copy now fails both NIST-sample tests and both ACVP tests, and the NIST sample tags match OpenSSL's CMAC.
  3. The workers report TDES_CMAC / TDES2_CMAC. The three release merges touch nothing in the cipher, tdes or core-hazmat paths. The workspace suite passes apart from three missing local vector files in crates this PR does not touch.

I also suggest the following edits to the description:

  1. The test-data note says an older checkout makes the TKW and CMAC suites "pass without checking anything". The loader only skips when the bc-test-data directory is absent; when the directory exists but a file is missing, it panics ("bc-test-data is present but … is unreadable"). So an older checkout fails tkw_ae, tkw_ad and the TKW CLI tests (and the ACVP CMAC tests, on a checkout old enough to lack those files), which is the safer behaviour, and the note should say so.
  2. The CMAC-TDES files are not recent (they are in a September checkout); only TKW_AE.txt / TKW_AD.txt are.
  3. "merged up to 6144224" is now f213c9e4.
  4. "Changes outside the new crate" could mention the CLI main-thread stack going from 8 MiB to 32 MiB in 67ae72d2, for the debug build.

@dghgit

dghgit commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the sixth round. No code change this time; the description is updated on all four points:

  1. The bc-test-data note now says what the loader does: it skips only when the directory is absent, and panics on a missing file when it is present, so an old checkout fails the TKW suites and CLI tests rather than passing vacuously.
  2. Only the TKW files are recent (2026-11-06); the CMAC-TDES sets have been in bc-test-data since August.
  3. The base line reads merged up to f213c9e.
  4. "Changes outside the new crate" names the CLI main-thread stack going from 8 MiB to 32 MiB in 67ae72d, and why.

Assisted-by: Claude:claude-mythos-5-1

@officialfrancismendoza officialfrancismendoza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dghgit looks good. LGTM!

dghgit added a commit that referenced this pull request Nov 14, 2026
…ant-time table-free Triple DES (TDEA) engine per SP 800-67r2 on the block cipher key objects: TDES3Key<M> (three-key) and the decryption-only two-key TDES2Key<M> over the engines TDESInternal and TDES2Internal, with the modes, TKW and CMAC over them. The S-box layer is a compile-time-derived ANF Boolean circuit with no lookup tables and no secret-indexed memory access; the round function's assembly on Cortex-M0 and RISC-V 32I holds no multiply (spread goes through ct::value_barrier off x86-64 and aarch64, as AES's round-key widening does), and dev_scripts/check_aes_ct_asm.sh and the aes-ct-asm CI job include it with a plain-shift spread as the positive control. TDESKey<P, M> mirrors AESKey: the key holds the mode's state, its workers borrow it, and which role pair a key carries is decided by its mode (CBC<Blocks>, CBC<PKCS7>, ECB<..>, CFB, CFB8, CTR<6, 8>, TKW), with the aliases TDES_CBC<'k, Dir, Pad>, TDES_CFB, TDES_CFB8, TDES_CTR, TDES_ECB, TDES_TKW<'k, Dir> and the TDES2_* decryptors and unwrapper naming what the keys hand out; keys and engines implement PartialEq and Eq in constant time over the expanded schedule. TDESInternal::new rejects non-distinct component keys and exactly the 64 weak, semi-weak and possibly-weak DES keys of SP 800-67r2 Sec 3.3.2 by a structural check as KeyMaterialError::WeakKey, and TDES2Internal::new rejects Key1 = Key2 and either component being weak. Two-key TDEA is decryption-only at compile time: TDES2Internal sets BlockPermutation::DECRYPT_ONLY (this library's policy for two-key alone; three-key encryption stays for the OpenSSL-provider interop, said so in the crate's Security Considerations), and every route that would make an encrypting worker over a decrypt-only permutation asserts it in an inline const (the modes' encrypt_init and encrypt_init_nonce, StreamCipher::from_keystream, crate-private through the sealed KeyStreamPolicy, Tkw's wrapping constructor and CMAC::generate_init), so a two-key key's create_encryptor, create_encryptor_nonce, create_wrapper and create_generator do not compile while its decryptors, unwrapper and verifier work, each route pinned by a compile_fail doctest on TDES2Internal; the two-key docs say what the gate does not stop, CFB and CTR decryption being the forward cipher. TKW (SP 800-38F Sec 7) is cipher::modes::TKW / Tkw<'k, P, Dir>, Kw at half the width (4-byte semiblocks, 32-bit counter, ICV3) generic over any 64-bit block permutation, TDES3Key<TKW> wrapping and unwrapping through the KeyWrappingKey and KeyUnwrappingKey roles and TDES2Key<TKW> unwrapping only; alongside it Kw and Kwp carry their direction in the type (Kw<'k, P, Dir>), so the AES_KW_*, AES_KWP_*, ARIA_KW_*, ARIA_KWP_*, Camellia_KW_*, SM4_KW and SM4_KWP aliases gain the Dir parameter, an API change to those crates listed in the 0.1.4 release notes. CMAC over TDEA (SP 800-38B Sec 5.3) is reached through the generic BlockCipherKey<TDESInternal, CMAC<8>> (TDESKey carries no MAC role), the two-key form verifying only (SP 800-131A Rev 2 Table 9), named TDES_CMAC and TDES2_CMAC. The data limit of SP 800-67r2 Sec 3.4 (2^20 blocks per key bundle) is stated per mode in the crate docs as the blocks TDEA processes (8 MiB for ECB, CBC, CFB64 and CTR, 1 MiB for CFB8, 699,048 bytes for TKW); the library cannot count across the uses of a key, so the limit is the key manager's, and the tdes-* CLI encrypt commands refuse input past it in one invocation through an InputLimit on the shared block- and stream-mode loops. The CLI gains tdes-* and tdes2-* subcommands for every mode and tdes-tkw and tdes2-tkw through the key objects with the aes*-* contract (tdes-cbc and tdes2-cbc are CBC<PKCS7> at any length, --iv/--iv-file with --nonce as its alias, -d/--direction everywhere, the tdes2-* commands accepting only decrypt), its block-mode plumbing taking the block length as a const parameter so the shared loops serve 64-bit blocks, and its main-thread stack raised from 8 MiB to 32 MiB because the debug binary overflowed the 8 MiB on --help once this branch's subcommands joined the release's. Outside the new crate: core gains BlockPermutation::DECRYPT_ONLY, Direction::IS_ENCRYPTING, KeyMaterialError::WeakKey and the DECRYPT_ONLY contract naming MAC generators; cipher the const asserts at every encrypting construction, StreamCipher::from_keystream crate-private behind the sealed KeyStreamPolicy, the TKW mode and Kw / Kwp with Dir in the type; aes, aria, camellia and sm4 their key-wrap aliases taking Dir; cli the generic block length, DecryptOnlyAction, InputLimit and aes_kw_cmd::run_with; dev_scripts the TDES round function in the constant-time assembly check; mem_usage_benches the TDES stack harness; and the 0.1.4 release notes carry all of it. Tests: the NIST CAVP T{E,C}{CB,BC,FB64,FB8}MMT{2,3} vectors at both key sizes in both directions (CBC and CFB through the keys, ECB through the engines), the CAVP TKW_AE / TKW_AD sets (five plaintext lengths, 100 trials each, 20 forgeries per section) through TDES3Key<TKW> and TDES2Key<TKW> plus RFC 3394-style reference tests over the raw permutation, NIST's TDES_CMAC samples (three-key generate and verify, two-key verify and refuse) and the ACVP CMAC-TDES.4014545 / .4014546 sets (three-key 72 gen and 180 ver, two-key the 60 full-tag ver cases, the 120 truncated-tag ones counted and skipped), all through the test framework's bc_test_data loader, which skips only when the bc-test-data directory is absent and panics on a missing file (the TKW files are a 2026-11-06 addition to bc-test-data, the CMAC sets there since August); the weak-key predicate asked about all 256 rotation-invariant (C0, D0) pairs and refusing exactly the spec's 64; the shared frameworks over every mode key (TestFrameworkFromKeyMaterial, TestFrameworkBlockCipher, TestFrameworkSymmetricCipher, TestFrameworkStreamCipher, TestFrameworkElectronicCodeBook, the key-wrap and MAC-key suites); cli/tests/tdes_cli_tests.rs and tdes_tkw_cli_tests.rs pinning COUNT = 0 of the MMT3 files and TKW through the binary in both directions and the two-key [DECRYPT] vectors through tdes2-*; and cli/tests/test_tdes.sh checking the per-mode data limit end to end. Reviewed by Francis in six rounds (2026-11-04 to 2026-11-13: is_weak matching 256 classes rather than 64, the gate covering create_encryptor only with the keystream and resume routes open, spread folding to a multiply on Cortex-M0, the CAVP suites passing vacuously on a missing file, the SuspendableKeyed resume route removed with #173, TKW and the key-wrap modes taking their direction in the type so a two-key unwrapper cannot wrap, the limits counting the blocks TDEA processes, CMAC's generating constructor refusing a decrypt-only permutation after #194, the five encrypt_init_nonce bodies from the supplied-nonce merge carrying no DECRYPT_ONLY assert so eight supplied-IV routes encrypted under two-key TDEA, the CMAC test that was never added, and the empty CMAC name; all fixed, approved 2026-11-13). Mutants on apollo: bouncycastle-tdes 500 (422 caught, 41 unviable, 33 missed, 31 of them bit-permutation XOR/OR equivalences in spread, permute_p, lookup, rotl28, pc1 and expand and two loop-bound mutants in the compile-time anf_constants, 4 timeouts in the compile-time ANF derivation), a scoped re-run over schedule.rs and des.rs after the round-one fixes 333 (303 caught, 30 missed, every miss a |-to-^ on disjoint bits), and the key-wrap modes kw/kwp/tkw with the cipher, aes and tdes tests 217 (159 caught, 58 unviable, 0 missed).

Assisted-by: Claude:claude-mythos-5-1
Assisted-by: Claude:claude-sonnet-5
Assisted-by: Claude:claude-fable-5-1
Assisted-by: Claude:claude-opus-5-5
@dghgit

dghgit commented Nov 14, 2026

Copy link
Copy Markdown
Contributor Author

Squashed onto release/0.1.4alpha as fa7789f (tree identical to 7d60a21), pushed to origin and GitHub.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants