Skip to content

Feat: AES LightEngine CCM Mode - #126

Draft
officialfrancismendoza wants to merge 28 commits into
bcgit:feature/xof-cshakefrom
officialfrancismendoza:feature/officialfrancismendoza/125-AES-lightengine-CCM-mode
Draft

officialfrancismendoza wants to merge 28 commits into
bcgit:feature/xof-cshakefrom
officialfrancismendoza:feature/officialfrancismendoza/125-AES-lightengine-CCM-mode

Conversation

@officialfrancismendoza

@officialfrancismendoza officialfrancismendoza commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Builds off AEAD Cipher split PR (#120) to add CCM mode for AES LightEngine (#125)

@officialfrancismendoza officialfrancismendoza added the enhancement New feature or request label Sep 10, 2026
@officialfrancismendoza
officialfrancismendoza changed the base branch from feature/symmetric-cipher to release/0.1.3alpha September 14, 2026 05:35
@officialfrancismendoza
officialfrancismendoza changed the base branch from release/0.1.3alpha to feature/symmetric-cipher September 14, 2026 05:40
@officialfrancismendoza
officialfrancismendoza force-pushed the feature/officialfrancismendoza/125-AES-lightengine-CCM-mode branch from 1d9bf20 to 63b9df6 Compare September 14, 2026 15:38

@dghgit dghgit 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.

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
officialfrancismendoza force-pushed the feature/officialfrancismendoza/125-AES-lightengine-CCM-mode branch from fcb9b75 to 63b9df6 Compare September 15, 2026 17:43
officialfrancismendoza added a commit to officialfrancismendoza/bc-rust that referenced this pull request Sep 15, 2026
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.
officialfrancismendoza added a commit to officialfrancismendoza/bc-rust that referenced this pull request Sep 15, 2026
…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.
officialfrancismendoza added a commit to officialfrancismendoza/bc-rust that referenced this pull request Sep 15, 2026
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.
officialfrancismendoza added a commit to officialfrancismendoza/bc-rust that referenced this pull request Sep 15, 2026
… 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.
officialfrancismendoza added a commit to officialfrancismendoza/bc-rust that referenced this pull request Sep 15, 2026
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).
officialfrancismendoza added a commit to officialfrancismendoza/bc-rust that referenced this pull request Sep 15, 2026
… 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.
officialfrancismendoza added a commit to officialfrancismendoza/bc-rust that referenced this pull request Sep 15, 2026
…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.
@ounsworth
ounsworth marked this pull request as draft September 16, 2026 02:56
…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
…OF/XOFSqueezer API and updated factory/CLI/tests/benches to compile against the new API (bcgit#119)
…/officialfrancismendoza/119-core-aead-cipher
…CLI thread, and fix a broken intra-doc link (bcgit#119)

Review follow-ups on the head of bcgit#120; no behaviour changes.

- cli/src/main.rs, cli/src/ascon_cmd.rs: the ascon-aead128 command's help and module docs still
  described the pre-nonce-prefix format ("output = ciphertext||tag") after the command started
  generating a nonce and writing it as the first 16 bytes of the stream. They now spell the
  convention out in both directions, the way aes128-ctr's help does for its own nonce, and say what
  --nonce/--nonce-file turn off -- the part a user gets wrong, since feeding a prefixed ciphertext
  to "--decrypt --nonce ..." decrypts garbage and only then fails the tag check. The two encrypt
  paths each gain a line saying which API they drive and why the explicit-nonce one cannot use the
  AEADCipherEncryptor pair (do_encrypt_init generates the nonce by construction).
- cli/src/main.rs: fn main's 8 MiB thread gains a comment for the constraint it exists for. It is
  load-bearing: with it removed and `ulimit -s 1024`, every subcommand -- sha3-256 as much as
  ascon-aead128 -- overflows during argument parsing in a debug build, before any algorithm runs.
- crypto/ascon/src/lib.rs: [`ascon_aead128::AsconAead128Decryptor::do_decrypt_final`] does not
  resolve, because do_decrypt_final is an AEADCipherDecryptor method rather than an inherent one,
  so `cargo doc` warned and published a dead link. Points at the trait method instead.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#119)

The entry carried the pre-remediation run, flagged as such ("20 missed before the XOF/CXOF
boundary-test additions"). Re-measured on this head with `cargo mutants -p bouncycastle-ascon
--test-package bouncycastle-ascon --jobs 3 --timeout 120`, with bc-test-data reachable from the
copied tree and a config whose examine_globs block is removed: 735 mutants, 618 caught, 111
unviable, 6 missed. The six are the known equivalences already commented at their sites -- the
sponge absorb/squeeze boundaries and the two disjoint-bit `|` -> `^` in set_state_byte -- so the
14 real survivors that run found in the XOF/CXOF Hash view are dead.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eded by the AEADCipherEncryptor/AEADCipherDecryptor split (bcgit#119)

AEADCipher was the single-type AEAD trait this issue exists to split. It had no implementor on the
base branch and its conformance suite had nothing to run against; this PR was about to give it its
first and only implementor, on AsconAead128, in the same change that introduces the pair meant to
replace it. That would have left the library with two parallel AEAD abstractions and Ascon-AEAD128
with four public one-shot encrypt surfaces. Deleted instead:

- crypto/core/src/traits.rs: the trait itself (encrypt/encrypt_out/decrypt/decrypt_out, the
  aead_* pair, do_aead_encrypt_final/do_aead_decrypt_final). The AEADCipherEncryptor doc that
  contrasted its tag placement with this trait's now just points at tagged_aead.
- crypto/core-test-framework/src/symmetric_ciphers.rs: TestFrameworkAEADCipher::test and
  ::test_plain_one_shots, the suites for it. The struct keeps test_encryptor_decryptor and
  test_buffering_toy, which exercise the pair.
- crypto/ascon/src/ascon_aead128.rs: the impl, and the module-doc sentence that justified the
  newtype pair by pointing at it.

Test coverage is kept where it was about Ascon rather than about the trait: the chunk-boundary
sweep and the wrong-tag rejection now drive the inherent do_encrypt_final/do_decrypt_final (they
only used the trait for its finalizers), and the undersized-buffer suite is rewritten against the
inherent one-shots, whose own length checks -- including the 16-byte-ciphertext and oversized-buffer
boundaries that must NOT be rejected -- were previously reached only through the trait. The three
tests that were about the deleted code (the std Vec wrappers, the plain view's
DecryptionFailed remapping, the AEADCipher framework conformance call) go with it.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ion figures (bcgit#119)

Deleting the trait and its suites takes bouncycastle-ascon from 735 mutants to 655: 558 caught, 91
unviable, 6 missed, the same six known equivalences as before, so the tests ported onto the
inherent one-shots hold the coverage the deleted trait's tests had.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ag layout on the AEAD traits, and address the remaining API-shape review points (bcgit#119)

The `tagged_aead` adapter pair is gone; what it did belongs to the traits themselves.

- crypto/core/src/traits.rs: AEADCipherEncryptor gains `tagged_encrypt` (one-shot into
  `ciphertext || tag`), `tagged_do_aead_encrypt_final` (streaming: flush, then append the tag) and
  `tagged_encrypt_out_len`; AEADCipherDecryptor gains `tagged_decrypt`,
  `tagged_do_aead_decrypt_final` (streaming: the tail is leftover ciphertext followed by the tag)
  and `tagged_decrypt_out_max_len`. All are defaults over the existing methods, so every
  implementor gets both layouts and neither has to be bolted on by a wrapper type that cannot
  express a buffering cipher's lengths (the `FINAL_LEN = 0` restriction TaggedEncryptor and
  TaggedDecryptor carried).
- crypto/core/src/tagged_aead.rs is deleted, with its module declaration and every use of it.
  crypto/core/tests/aead_tagged_tests.rs keeps the toy AEAD the deleted module's in-`src` tests
  used and points it at the new methods: round trip at every length crossing `TAG_LEN`, every
  chunking, tampering, a stream that ends before a whole tag, and every undersized buffer.
- crypto/core-test-framework: the AEAD suite now checks the inline layout for every implementor
  (one-shot against streaming, and a too-short tail as DecryptionFailed), and the buffering toy
  checks it where FINAL_LEN > 0, which is where `tagged_do_aead_encrypt_final` has to flush and
  append in one call. Its short-buffer probe on the decryptor now feeds the decryptor its own
  ciphertext rather than the plaintext, and uses the ciphertext's length.
- crypto/ascon: `AsconAead128::new`'s `for_encryption: bool` is no longer public API --
  `new_encrypting` / `new_decrypting` name the direction, and the bool constructor they share is
  private. The crate docs gain a `tagged_*` example.
- cli/src/ascon_cmd.rs: both directions drive the trait pair, holding the tag back by hand on the
  way in, which is what the adapter did for it. A failed `do_encrypt_init`/`do_decrypt_init` --
  the RNG or the key material -- now prints an error and exits rather than panicking, as
  block_mode_cmd.rs does for the same call, and the remaining unwraps carry their `infallible:`
  notes.
- crypto/core/src/traits.rs also: the allocating one-shot's three-part return is now the named
  `AEADEncrypted<NONCE_LEN, TAG_LEN>` (clippy `type_complexity`), and `decrypt_out` /
  `encrypt_out_rng` get the same "an implementor with FINAL_LEN > 0 must override this" note
  `encrypt_out` already had.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nges (bcgit#119)

661 mutants, 558 caught, 97 unviable, 6 missed -- the same six known equivalences (the sponge
absorb/squeeze boundaries and the two disjoint-bit `|` -> `^` in set_state_byte). The count moves
from 655 with the new_encrypting/new_decrypting constructors.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… in the tagged AEAD defaults (bcgit#119)

`cargo mutants -p bouncycastle-core -f crypto/core/src/traits.rs --re
'AEADCipherEncryptor|AEADCipherDecryptor' --test-package bouncycastle-core --test-package
bouncycastle-ascon` reported 116 mutants, 91 caught, 19 unviable, 6 missed. Four of the six were
real: the buffer guards could be weakened without a test noticing, because a too-short buffer is
rejected either by the guard or by the `do_update_out` behind it, and both report
IncorrectOutputBufferLength with the same length -- so the probes could not tell which had fired.

- crypto/core/tests/aead_tagged_tests.rs: `tagged_do_aead_decrypt_final` with a buffer of exactly
  `needed` must succeed. Kills `plaintext.len() < needed` -> `<=` and -> `==`.
- crypto/core-test-framework: the buffering toy now finishes from a tail that still holds
  ciphertext (TAG_LEN + 4 bytes) into an exactly-sized buffer, which is what makes
  `update_out_len(..) + FINAL_LEN` observable -- with a generous buffer any arithmetic there would
  do. Kills `+ FINAL_LEN` -> `* FINAL_LEN`. The AEAD suite also feeds `encrypt_out_rng` a buffer
  with room to spare, so its own guard cannot be flipped to `>` unnoticed.

The re-run is 116 mutants, 95 caught, 19 unviable, 2 missed; the two are `written + final_len` ->
`written - final_len` in `encrypt_out_rng`, equivalent while every implementor has FINAL_LEN = 0.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…defaults (bcgit#119)

The ascon crate's numbers were already there; the pair's own defaults in core were only in
f376c14's commit message. 116 mutants, 95 caught, 19 unviable, 2 missed.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dghgit and others added 2 commits September 21, 2026 18:13
…/119-core-aead-cipher

Two conflicts, both from the base branch's renames. In crypto/core/src/traits.rs the
AEADCipher trait this PR deletes collided textually with the AEADCipherDecryptor docs
that replaced it, resolved in favour of the deletion. In core-test-framework's
symmetric_ciphers.rs the two import lists were unioned, minus the deleted AEADCipher.

The PR's own code follows the base branch's API renames: SimpleCipherEncryptor /
SimpleCipherDecryptor are SymmetricCipherEncryptor / SymmetricCipherDecryptor,
TestFrameworkSimpleCipher is TestFrameworkSymmetricCipher, and
SymmetricCipherError::IncorrectOutputBufferLength(&'static str, usize) is
OutputBufferTooSmall(usize) -- the dropped name field took a sentence in ascon's two
one-shots, which is no loss since the error identifies the buffer by the call it came from.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assisted-by: Claude:claude-sonnet-5
@officialfrancismendoza
officialfrancismendoza requested review from dghgit and removed request for dghgit September 21, 2026 15:36
…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.
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.
@officialfrancismendoza
officialfrancismendoza force-pushed the feature/officialfrancismendoza/125-AES-lightengine-CCM-mode branch from c52f994 to 26dda13 Compare September 21, 2026 20:14
@officialfrancismendoza
officialfrancismendoza changed the base branch from feature/symmetric-cipher to feature/xof-cshake September 21, 2026 20:16
@officialfrancismendoza

Copy link
Copy Markdown
Contributor Author

Rebased CCM branch off new updates for (#120 / #149). Diff from pre-rebase includes:

  • Rebase conflict in crypto/modes/src/lib.rs on docs/imports around CCM and AEAD core trait wording
  • API drift post-rebase, where updated to use new OutputBufferTooSmall(usize)
  • David's changes on Feat: Core AEAD Cipher Split #120 which deleted TaggedDecryptor/TaggedEncryptor to instead use additional do_final and one shot on AEADCipherEncryptor/AEADCipherDecryptor

This branch has not been deployed

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants