Feat: AES LightEngine CCM Mode - #126
Draft
officialfrancismendoza wants to merge 13 commits into
Draft
officialfrancismendoza wants to merge 13 commits into
officialfrancismendoza wants to merge 13 commits into
Conversation
officialfrancismendoza
changed the base branch from
feature/symmetric-cipher
to
release/0.1.3alpha
September 14, 2026 05:35
officialfrancismendoza
changed the base branch from
release/0.1.3alpha
to
feature/symmetric-cipher
September 14, 2026 05:40
…d multi-call-squeeze checks
…in update_out_len and a FINAL_LEN final buffer so a buffering cipher or an inline ciphertext||tag layout can be expressed; TaggedEncryptor/TaggedDecryptor adapt any FINAL_LEN=0 pair to the SimpleCipherEncryptor/SimpleCipherDecryptor ciphertext||tag shape; the block, simple-cipher and AEAD strength sweeps assert they are not vacuous, and the AEAD streaming suite gains a genuinely-buffering toy plus undersized-buffer and std-one-shot coverage
…XOF128/CXOF128) implementing AEADCipherEncryptor/AEADCipherDecryptor via AsconAead128Encryptor/AsconAead128Decryptor, with HashFactory/XOFFactory registration and CLI wiring including a TaggedDecryptor-based decrypt stream
…aped for CCM CCM was implemented in part to test whether the AEAD streaming traits could support a packet cipher; it confirmed they cannot without buffering, since SP 800-38C needs the total AAD and payload length before it can authenticate anything, and the trait's do_encrypt_init/do_update_aad/do_update_out are open-ended by design for the common case (Ascon-AEAD128, and GCM once it exists) that never needs a total up front. Record the finding and the chosen resolution -- buffer internally or ship a dedicated non-buffering API, not a length parameter on the shared trait -- at the trait definition itself, cross referenced from CcmEncryptor, so a future implementor doesn't have to re-derive it.
officialfrancismendoza
force-pushed
the
feature/officialfrancismendoza/125-AES-lightengine-CCM-mode
branch
from
September 14, 2026 15:38
1d9bf20 to
63b9df6
Compare
dghgit
requested changes
Sep 14, 2026
dghgit
left a comment
Contributor
There was a problem hiding this comment.
Looks like it's getting there. I've sent the report.
Make sure the update to .gitignore is removed - it's wrong! Delete the commit or revert it.
officialfrancismendoza
force-pushed
the
feature/officialfrancismendoza/125-AES-lightengine-CCM-mode
branch
from
September 15, 2026 17:43
fcb9b75 to
63b9df6
Compare
read_from_file's hex-or-raw heuristic is fine for a key, where a wrong guess only produces a mismatch, but for a CCM nonce it can turn two distinct binary nonce files into the same nonce value if both happen to be valid hex text for it -- and a repeated nonce under one key breaks CCM's authentication (SP 800-38C Appendix B). Add read_from_file_raw and use it for --nonce-file specifically; --nonce (hex on the command line) is unaffected. PR bcgit#126 review, finding F1.
…payload limit a compile error Ccm::y held Yr (the raw tag before the S0 mask) and every intermediate CBC-MAC chaining value in a plain array, unlike the keystream beside it, which is a Secret for the same reason; wrap it and finish_mac's local S0 the same way. Separately, CcmEncryptor/CcmDecryptor's BUFFER_LEN could exceed the payload limit NONCE_LEN implies (A.1's 2^8q - 1) and only fail at do_*_final, after buffering the whole message for nothing; assert the relationship at construction instead, which also makes MAX_PAYLOAD_LEN pub and lets do_*_final's # Errors sections state the guarantee precisely. Document the same capacity error as a general possibility on the trait's do_update_aad/do_update_out. PR bcgit#126 review, findings F3 and F4.
apply_keystream generated one counter block per encrypt_block call, even though A.3's Ctrj depends only on j and the counter blocks are exactly as independent as CTR's -- only the CBC-MAC half is genuinely serial (Sec 6.1 step 3). Restructure it like Ctr::apply: finish any open keystream block byte-wise, batch aligned whole blocks through encrypt_4blocks/encrypt_2blocks, then finish the tail byte-wise. Measured ~35-38% throughput gain (26->36 MiB/s for AES-128, no AAD; matches the buffering pair too), all 480 ACVP cases and 4 Appendix C vectors still pass. That made three doc passages actively wrong, since they said this was inherent: modes/src/lib.rs's mode comparison, modes_benches.rs's CCM doc comment (both rewritten with the new ratios against CTR), and lib.rs's "CCM takes no direction"/"there is no direction parameter" claims, which were already false against the code (Dir is very much a parameter) and predate this session. Also: fixed lib.rs's "264 B" vs the documented and now-tested 256 B, added size_of assertions pinning Ccm/CcmEncryptor's sizes against the memory table (previously undocumented by a test), and added the CCM aliases to the AES crate's "Modes of operation" section, which listed every other mode but this one. PR bcgit#126 review, findings F5 and F7.
… doesn't have The Encrypt/Decrypt value help (rendered by clap under --help for every mode subcommand, including the three CCM ones) said a fresh IV or nonce is generated and written to the output. CCM's nonce is supplied via --nonce and never written, so bc-rust aes128-ccm --help printed instructions that produce "authentication failed" if followed. Trim the shared enum's help to direction only and point at each subcommand's own --help, which already documents its mode's exact framing (CBC/CFB/CFB8/CTR already do; CCM's own help already explains the nonce is supplied, not generated). PR bcgit#126 review, finding F6.
go() called the *_detached one-shots, each of which needs a fresh ciphertext/plaintext buffer the size of the input on top of the input buffer already read from stdin. Use Ccm::new plus do_*_update/do_*_final directly on the buffer already in hand: input.len() is exactly the declared payload length and is supplied in one call, so the two do_*_update/do_*_final calls this replaces cannot fail, which the .expect()s explain. Also: decrypt's tag split now goes through split_last_chunk_mut, matching Ccm::decrypt's own reasoning for admitting Clen == Tlen instead of restating the spec's stricter Clen <= Tlen and then testing < anyway; and the payload-limit error message reads Ccm::MAX_PAYLOAD_LEN (now pub) instead of re-deriving it. Documented the packet-AEAD exception to CLAUDE.md's CLI-streams rule this relies on. PR bcgit#126 review, finding F8 (buffer only; the pre-existing duplicated nonce-range check is deliberate and stays, per its own comment).
… the redundant key check CcmEncryptor and CcmDecryptor carried seven identical fields and byte-for-byte identical do_update_aad, differing only in one error string in do_update_out and in which Ccm direction do_*_final builds; the "set data_started before the length check" comment was on the encryptor's copy only. Factor the buffering itself into a private CcmBuffer that both now wrap as newtypes (the same pattern bouncycastle-ascon uses for AsconAead128Encryptor/Decryptor), so the shared behavior has one body. Also: Ccm::checked_perm re-checked KeyType::SymmetricCipherKey, which P::new (AES_128::new and friends) already checks per ElectronicCodeBook::new's own documented contract -- confirmed no other mode in this crate duplicates it, so it bought nothing but a second, differently-worded error message for the same bad key. Removed, and Ccm::new/CcmEncryptor/CcmDecryptor now call P::new(key) directly like every other mode. CcmEncryptor's nonce draw now calls crate::iv::random_iv, the same OS-backed draw Cbc/Cfb/Ctr already share, instead of a CCM-specific copy of the same three lines. No behavior or memory-layout change: CcmEncryptor/CcmDecryptor are still 8400 B at BUFFER_LEN=4096, all 480 ACVP cases and 4 Appendix C vectors still pass. PR bcgit#126 review, finding F10.
…used no private API crypto/modes/tests/wycheproof_ccm_tests.rs drives bc-test-data's vendored aes_ccm_test.json (552 tests) through Ccm::encrypt_detached/decrypt_detached, following the file/skip-with-warning convention acvp_ccm_tests.rs already uses. Unlike the ACVP set (one nonce length, no malformed inputs), this one is deliberately adversarial: every nonce length from 8 to 2144 bits, tag sizes A.1 forbids, truncated and bit-flipped tags. Ccm's NONCE_LEN/TAG_LEN are const generics restricted to A.1's sets, so a case whose sizes fall outside them has no instantiation to dispatch to at all -- not a runtime failure, a compile-time non-option -- and those are counted as skipped rather than silently dropped. Locally: 486 of 552 cases run (405 valid, 81 invalid), 66 skipped across 63 out-of-range groups, all passing. bc-test-data/crypto/wycheproof/ already vendors sm4_ccm_test.json for this exact purpose; aes_ccm_test.json needs adding there too (copied from https://github.com/C2SP/wycheproof, testvectors_v1) for this suite to run anywhere but here -- that's a separate repository this PR cannot touch. Also, per QUALITY_AND_STYLE.md's unit-vs-integration-test rule (a unit test only where the behaviour cannot be reached from outside): moved payload_longer_than_the_q_limit_is_refused and a_short_or_long_payload_is_refused out of ccm.rs's #[cfg(test)] block into sp800_38c_tests.rs (converted from the toy Identity permutation to AES_128, matching that file's convention), since both exercise only Ccm::new/do_encrypt_update/do_encrypt_final. Deleted both_directions_mac_the_plaintext outright: it was byte-for-byte the same check as sp800_38c_tests.rs's each_direction_has_its_own_methods, just against Identity instead of AES_128. What remains in ccm.rs's own test module is exactly what its module doc says it should be: the private formatting helpers (format_b0, encode_aad_len, put_q_field) that no public API exposes directly. PR bcgit#126 review, finding F9.
…ry changes left Scoped cargo-mutants (apply_keystream, counter_block, CcmBuffer) found 7 survivors after the F5/F7/F10 commits: 3 on apply_keystream's head_len comparison/subtraction, 2 more on the same expression, and 2 on CcmBuffer::do_update_aad/do_update_out's `end > BUFFER_LEN` checks. The two BUFFER_LEN checks were genuinely untested at the exact boundary (end == BUFFER_LEN, which must be accepted, not refused) -- added the_buffering_pair_accepts_a_message_that_exactly_fills_its_buffer. apply_keystream's gap needed an actual bug, caught it, then a second attempt to test it: no existing test ever calls it with `ks_pos` genuinely strictly between 0 and BLOCK_LEN followed by a chunk large enough to reach the batched fours/pairs path -- every chunking sp800_38c_tests.rs sweeps is uniform, and Appendix C.4's 32-byte payload (Plen = 256 *bits*, not bytes) is too short regardless. Added resuming_a_part_way_open_block_agrees_with_a_one_shot, a dedicated 123-byte case; verified by hand-applying each surviving mutation and confirming it now fails before restoring the correct code. One mutant remains and is provably equivalent (`<` vs `<=` on `ks_pos < BLOCK_LEN`, since `ks_pos` never exceeds `BLOCK_LEN` and both arms agree at that boundary) -- same class as format_b0's documented `|`/`^` equivalence, now commented the same way. Re-run: 34 caught, 116 unviable, 1 equivalent, 0 missed.
ounsworth
marked this pull request as draft
September 16, 2026 02:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds off AEAD Cipher split PR (#120) to add CCM mode for AES LightEngine (#125)