Skip to content

sm4: add bouncycastle-sm4, a constant-time SM4 block cipher - #140

Open
dghgit wants to merge 22 commits into
feature/simple-ciphersfrom
feature/sm4
Open

dghgit wants to merge 22 commits into
feature/simple-ciphersfrom
feature/sm4

Conversation

@dghgit

@dghgit dghgit commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds bouncycastle-sm4: a constant-time, table-free, low-memory SM4 block cipher (GB/T 32907-2016), ported from Bouncy Castle Java's SM4Engine.

Description

  • S-box is a 127-gate Boolean circuit (found by exhaustive search, reusing the Boyar-Peralta AES non-linear section via a field isomorphism), not a lookup table.
  • Bit-sliced over 16-bit planes, four blocks at a time (encrypt_4blocks/decrypt_4blocks).
  • SM4_CBC<Dir> alias plus a sm4-cbc CLI subcommand.

Scope and Risk

New crate only, plus the shared <Dir, Pad>/PaddedMode refactor (moves PaddedMode from bouncycastle-aes into bouncycastle-padding so every block cipher can share it) -- additive, covered by existing AES suites.

Validation

  • Draft draft-ribose-cfrg-sm4-10 Appendix A.1 vectors (all 6 examples, every round key/output, two 1,000,000-fold iterations); Appendix A.2.1/A.2.2 ECB/CBC vectors; BC Java's own SM4Test vectors.
  • cargo test -p bouncycastle-sm4: all passing.
  • cargo mutants -p bouncycastle-sm4: 374 mutants, 361 caught, 8 unviable, 4 missed (XOR/OR equivalences), 1 timeout.

AI Usage Statement

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

Assisted-by: Claude:claude-sonnet-5

🤖 Generated with Claude Code

@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
dghgit and others added 14 commits September 28, 2026 20:28
…om bc-java

New crate `crypto/sm4` (`bouncycastle::sm4`): the SM4 block cipher
(GB/T 32907-2016, read via draft-ribose-cfrg-sm4-10) as a raw keyed
permutation implementing ElectronicCodeBook, ported from BC Java's SM4Engine.

The S-box is a 127-gate Boolean circuit over eight u32 bit-planes rather
than the Java engine's 256-byte table, so there is no secret-indexed memory
access in the cipher or the key schedule. The SM4 S-box is GF(2^8) inversion
between two affine maps -- a decomposition found by exhaustive search against
the spec's table -- so a field isomorphism lets the non-linear section of the
Boyar-Peralta AES circuit (copied from aes) do the inversion, with
generated affine top and bottom layers. A round substitutes four bytes per
block, so the engine works on eight blocks per circuit pass:
encrypt_8blocks/decrypt_8blocks are the full-rate path, encrypt_2blocks is
overridden to use two lanes, and single-block calls do eight blocks' work.
One stored schedule (128 B, in a Secret) serves both directions.

Also: SM4_CBC<Dir> alias over bouncycastle-modes; `sm4-cbc` CLI subcommand,
with aes_cbc_cmd.rs generalised into cbc_cmd.rs so the four CBC commands
share one implementation; criterion bench; mem_usage_benches harness;
umbrella re-export; release notes.

Verified against every value in the draft's Appendix A.1 (Examples 1-6,
including all 32 round keys and all 32 per-round outputs of Examples 1 and
4, and both 1,000,000-fold iterated ciphertexts, the latter in release
builds only), the SM4-ECB and SM4-CBC vectors of Appendix A.2, the vectors
of BC Java's core and provider SM4Test including test1000000(), an
exhaustive 256-input S-box test, and a table-driven transcription of the
Java engine kept in the tests, which the engine must agree with on
thousands of keys and blocks in every lane and both directions.
cargo mutants: 364 mutants, 353 caught, 8 unviable, 3 missed -- all three
proven XOR/OR equivalences, commented at the site.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The S-box circuit moves from eight u32 bit-planes to eight u16 planes,
matching the four-lane shape of bouncycastle-aes and bouncycastle-camellia. A round substitutes four bytes per
block, so the u16 planes hold the T argument of four blocks -- each split into
two 16-bit halves -- and encrypt_4blocks/decrypt_4blocks become the natural
unit; the trait's blocks8 is two passes, blocks2 uses two lanes, and a
single-block call does four blocks' work. The per-call working state drops from
about 160 B (128 B of block state, 32 B of planes) to about 100 B (64 B of
block state, 16 B of T arguments, 16 B of planes), at half the throughput on a
32/64-bit machine -- the trade this design makes. The 127-gate
circuit itself is unchanged, and the transpose is the same BearSSL routine on
u16 masks; the key schedule's T' now fills four lanes instead of eight.

Also: the ElectronicCodeBook::encrypt_8blocks doc in core now names both
four-lane crates, and the release notes and the CLI's import follow.

Verified as before: every value of draft-ribose-cfrg-sm4-10 Appendix A.1
(round keys and per-round outputs of Examples 1 and 4, now pinned in every lane
of the four-block path and every slot of the eight-block path, and both
1,000,000-fold iterated ciphertexts in release builds), the A.2 ECB and CBC
vectors, BC Java's SM4Test including test1000000(), the exhaustive 256-input
S-box test (sixteen passes of sixteen positions), and the table-driven
transcription of the Java engine on thousands of keys and blocks in every lane
and slot, both directions; the sm4-cbc CLI tests.
cargo mutants: 376 mutants, 364 caught, 8 unviable, 4 missed -- all four proven
|/^ equivalences on disjoint bits (the transpose, the half-word join in tau)
or the documented Boyar-Peralta t37 gate, commented at the site.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…N as a parameter, so every block cipher crate can write its <Dir, Pad> CBC and ECB aliases over the one projection
…ode projection, and the block-aligned vectors move onto modes::Cbc
Doc comments scattered per-file citations to BC Java's SM4Engine
(its table-driven S-box, its F0..F3 round structure, its per-direction
key expansion, its CK/FK tables, its algorithm name) throughout
lib.rs, sm4.rs and schedule.rs. The crate's Provenance section already
names BC Java's SM4Engine as the source implementation this crate is
ported from and describes what it reproduces; the scattered
comparisons are replaced with self-contained descriptions of the same
points (a table-driven S-box's cache-timing exposure, a
direction-aware engine's per-direction expansion) that don't depend on
the reader having BC Java's source open. Test files that cross-check
against BC Java's own vectors (tests/bc_java_tests.rs, tests/common/mod.rs)
are untouched, since BC Java is literally their subject.

No behavioural change; cargo fmt --check is clean; cargo test -p
bouncycastle-sm4 passes (14 passed, 2 ignored long-running cases).

Assisted-by: Claude:claude-sonnet-5

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
…/ sm4-cfb8 / sm4-ctr CLI commands, so SM4 covers the same four modes AES does rather than CBC alone -- `bouncycastle-modes` is cipher-agnostic and its Cfb, Cfb8 and Ctr already work over any ElectronicCodeBook, so each alias is three lines of type plus the const parameters SM4 fills in (KEY_LEN = BLOCK_LEN = 16, and a 12-byte CTR nonce leaving the 4-byte counter the mode caps at, the same split bouncycastle-aes chooses for its 16-byte block). SM4_CFB is draft-ribose-cfrg-sm4-10's SM4-CFB-128 and SM4_CFB8 its SM4-CFB-8, the two variants Sec 8.5.1 names; SM4_CTR is Sec 8.7, whose counter sequence the draft leaves to the caller ("any sequence that does not repeat within the block size"), so nonce || counter is an admissible choice rather than a deviation. Test coverage follows what the draft actually publishes: Appendix A.2.4's two SM4-CFB examples are straight known-answer tests through SM4_CFB, driven under the vector's own IV via a FixedSeedRNG exactly as the existing A.2.2 CBC tests are; Appendix A.2.5's SM4-CTR examples make the whole 16-byte IV the first counter block and increment it as a unit, which the nonce/counter split cannot produce, so those vectors pin a reference CTR written from the Sec 8.7.1 equations over the permutation and SM4_CTR is then required to agree with that reference on the counter blocks it does produce; SM4-CFB-8 has no published example at all, so it is pinned the same way against the Sec 8.5.2 equations at s = 8. stream_mode_alias_tests.rs covers the wiring the vectors do not -- that each alias names the mode it claims to, round-trips at any length with no padding, gives the same answer whatever the chunking, draws fresh init data per encryption, and that CFB128, CFB8 and CTR are mutually distinct. The three CLI commands are thin dispatchers over the existing stream_mode_cmd plumbing, so they inherit the IV-in-the-ciphertext convention, the 1 KiB streaming chunk and the key loader unchanged; their tests pin the A.2.4 vectors end to end through the pipe for sm4-cfb and hold sm4-cfb8 and sm4-ctr to the library for the two modes without vectors. No new mutants: `cargo mutants -p bouncycastle-sm4 --list` reports the same 374 before and after, because the three new source files are type aliases and documentation with no executable code of their own. quality_stats.sh's "unwraps in core code" for sm4 goes 3 -> 29, all of it `.unwrap()` inside the new doctests, which the script counts from src/ without distinguishing doc comments; the doctests are written in the same style as crypto/aes/src/{cfb,cfb8,ctr}.rs, which is why aes reports 58.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pe on the error paths, which is what failed the Rust Tests workflow on fc35da9 -- `a_key_of_the_wrong_length_is_rejected` in sm4_cfb8_cli_tests.rs writes its stdin payload from the test thread and `.expect()`s the result, but the command it invokes rejects the key and `exit`s before reading a byte, so the write races the child's exit and gets EPIPE. The assertion the test actually makes -- non-zero status, and the algorithm named in stderr -- is unaffected, because `wait_with_output` still returns both, so the fix is to match on the write result and ignore `ErrorKind::BrokenPipe` while still panicking on every other write error. That is the same treatment, and the same reasoning, that aes_ctr_cli_tests.rs already documents for its threaded harness; the harness is copied per file, so all three new suites (cfb, cfb8, ctr) get it. The race is timing-dependent -- it passed locally and failed on the loaded CI runner -- so sm4_cfb8_cli_tests.rs also gains a deterministic guard: a 4 MiB payload to a command that exits on a rejected key cannot fit in a pipe buffer, so the write is certain to get EPIPE rather than merely likely to. Verified by reverting the harness under that guard, which reproduces the CI panic exactly, and re-applying it, which does not. Also drops an unused `tohex` helper that sm4_cfb8_cli_tests.rs never called, which was the one warning `cargo test --all` still emitted. No behaviour outside the test harness changes.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eam-mode suites got in 703ca45, closing the last copy of that race in this crate's CLI tests -- `a_key_of_the_wrong_length_is_rejected` writes its stdin payload from the test thread and `.expect()`s the result, but `sm4-cbc` rejects the key and `exit`s before reading a byte, so the write races the child's exit and gets EPIPE. This file predates the stream modes and has never failed CI, but the defect is identical and was demonstrated here rather than assumed: dropping a 4 MiB error-path payload into the unpatched harness reproduces exactly the panic that failed the Rust Tests workflow on the cfb8 suite. The write result is now matched on, `ErrorKind::BrokenPipe` ignored and every other write error still a panic, which is what the five aes_*_cli_tests.rs suites have always done; an audit of cli/tests confirms no suite in the tree still `.expect()`s that write. The new `a_large_payload_on_an_error_path_does_not_break_the_harness` is the deterministic guard for it -- a payload that cannot fit in a pipe buffer makes EPIPE certain rather than merely likely, so the harness cannot silently regress to the flaky form. Also reflows the over-width comment lines this series left in sm4_cfb8_cli_tests.rs: rustfmt does not wrap comments, so they passed `cargo fmt --check` while sitting past the 100-column max_width every other comment in the tree respects. Nothing the existing tests assert on changes: `wait_with_output` still returns the exit status and the stderr they match against, and no non-test code is touched.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ibed a repo that no longer exists -- this file's own preamble asks for stale lines to be reported and fixed, and these two were found the hard way, by a PR failing a workflow the file says does not exist. CI: it claimed `publish_doc_benches_to_ghpages.yaml` was the only workflow and that "there is no separate CI test/lint job -- local `cargo test --workspace` is the gate". There are five workflows, four of them real PR gates (`rust-build.yml`, `rust-test.yml`, `rust-docs.yml`, `rust-style.yml`), and `Rust Tests` fails PRs; the section is now a table of what each one runs, so the local commands that mirror the gate are in one place. Three further details are recorded because each has already cost time: the pages workflow only publishes on a push to `main` (its last two jobs are gated on `github.ref`), it does not run benchmarks at all despite the file name (the `run_benches` job is commented out, "the benches run crazy slow on the github agent") so the old claim that it "publishes docs, code stats, and benchmark results" was wrong on the third, and its `concurrency: group: "pages"` is global rather than per branch, so pushing several branches at once leaves all but the last reporting "cancelled" -- a result that looks like a failure and is not. Toolchain: it claimed the workspace uses nightly pinned in `rust-toolchain.toml` because `core/src/lib.rs` enables `#![feature(adt_const_params)]`. No `rust-toolchain.toml` exists anywhere in the tree, `core` has no feature gate, and the only `adt_const_params` line in the workspace is commented out in `crypto/mldsa/src/lib.rs` beside a commented `unsized_const_params`; CI builds and tests on stable (1.98.1 as installed by `dtolnay/rust-toolchain@stable`) and passes, so the workspace is stable-clean and the note now says so, and warns that a nightly default toolchain locally will hide nightly-only code until CI catches it. The one place nightly is genuinely required is `rust-style.yml`, which does `rustup override set nightly` before `cargo fmt --all --check` while every other job stays on stable; that is now stated rather than implied. Documentation only -- no code, tests or build files are touched.

Assisted-by: Claude:claude-opus-5

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hich no longer exist since 334cd2b made the methods required

Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bsorbed through its merges (SymmetricCipher/BlockCipherPadding renames, the ElectronicCodeBook batch methods, AES*Internal, core::security_strength, PaddedBlockCipher* adapters and the rest), restored in one commit after the linear rebase dropped those merges; the tree is identical to merging c5f639d with 2d4d038

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hubot
hubot deleted the feature/sm4 branch September 28, 2026 15:21
@hubot
hubot deleted the branch feature/simple-ciphers September 28, 2026 15:21
@hubot
hubot restored the feature/sm4 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
dghgit and others added 2 commits September 29, 2026 10:25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tests, stream-mode tests, the CLI CFB8/CTR tests, the raw-Cbc draft test and the crate's doc examples take the renamed in-place one-shots (encrypt_in_place/decrypt_in_place), renamed only where the call is in place (the padded SM4_CBC Vec one-shots are unchanged), and import the SymmetricCipher* supertraits their do_*_init calls now come from

Assisted-by: Claude:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dghgit and others added 5 commits October 1, 2026 15:08
# Conflicts:
#	alpha_0.1.3_release_notes.md
#	cli/src/main.rs
#	crypto/aes/src/cbc.rs
#	crypto/aes/src/ecb.rs
#	crypto/aes/src/lib.rs
#	crypto/padding/src/padded_mode.rs
SM4 moves to bouncycastle_sm4::hazmat with BLOCK_LEN, KEY_LEN and LANES defined at the crate root;
everything else is the path in use lines and doc links. No logic change, no mutation run owed.

Assisted-by: Claude Code:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts:
#	crypto/aes/src/cbc.rs
#	crypto/aes/src/hazmat/ecb.rs
#	crypto/padding/src/padded_mode.rs
Follows aes 160ac18; the padding crate's PaddedMode copy goes with it. Type alias only, no
behaviour change, no mutation run owed.

Assisted-by: Claude Code:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Brings in 8dff255 (bouncycastle-modes and bouncycastle-padding folded into the new
bouncycastle-cipher crate) and a27d9e6 (StreamCipher and the direction markers moved out of
core), so the SM4 crate, its CLI commands and tests are rewritten onto the new paths:
bouncycastle_cipher::modes::*, bouncycastle_cipher::padding::*, and
bouncycastle_cipher::{Direction, Encrypting, Decrypting}. Import rewrite only, no behaviour
change.

Assisted-by: Claude Code:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants