Skip to content

feat(kem)!: ML-KEM-512 and ML-KEM-1024 - #606

Queued
kixelated wants to merge 7 commits into
mainfrom
quest/m3/parity/ml-kem
Queued

kixelated wants to merge 7 commits into
mainfrom
quest/m3/parity/ml-kem

Conversation

@kixelated

@kixelated kixelated commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Completes quest/m3/parity/ml-kem.md and deletes it, removing every link to it.

Problem

rind::kem offered only ML-KEM-768. rustls's aws-lc-rs provider ships MLKEM1024, so rustls provider parity needs ML-KEM-1024. aws-lc-rs users of ML_KEM_512 and ML_KEM_1024 had no rind equivalent.

Approach

  • Generic construction. mid/mlkem.rs (formerly mlkem768.rs) is generic over a sealed Params trait. The trait carries the rank K (2, 3, 4), the compression widths du/dv (10/4, or 11/5 for 1024), and eta1 (3 for 512), plus associated array types for each size, since stable Rust has no generic_const_exprs.
  • Arithmetic. s2n-bignum's HOL Light-proven mlkem_basemul_k2 and k4 come in through admin/parse-asm for x86_64 and aarch64. mlkem-native's AVX2 poly_compress_d5 and d11 come in through admin/import-aws-lc. The portable and armv7 backends get a generic base multiplication and generic division-free compression, checked exhaustively against the definition. Decompression and eta=3 CBD sampling are Rust on every backend. The matrix-index computation uses no division, so the variable-time scan passes.
  • Tests.
    • Vectors for each set: Wycheproof (keygen, encaps, decaps, semi-expanded decaps), ACVP's 512 and 1024 groups (admin/sync-acvp widened), BoringSSL's ML-KEM-1024 files (admin/sync-vectors, about 5 MB), and CCTV's per-set modulus vectors.
    • Differential tests in both directions against aws-lc-rs.
    • ctgrind, BINSEC (ct_binsec_mlkem512/768/1024), qemu constant-time traces, zeroing and heap-scratch checks, and the fuzzer's key parsing, all per set.
  • Adapters. rind-aws-lc-rs gains ML_KEM_512 and ML_KEM_1024, which drops the omission from its README. rind-rustls moves to the new API and keeps only X25519MLKEM768. Its MLKEM1024 group belongs to the rustls parity quest.
  • Docs. ARCHITECTURE.md now says KEMs and signing key pairs take a type per parameter set. ALGORITHMS.md, MIGRATING.md, TESTING.md, and the README are updated.

Public API impact (breaking)

kem::ML_KEM_768 and kem::Algorithm as a value are gone. The sealed marker types MlKem512, MlKem768, and MlKem1024 parameterize DecapsulationKey<A>, EncapsulationKey<A>, Ciphertext<A>, and SharedSecret<A>, so a ciphertext of one set fails to compile against a key of another. kem::Algorithm is now the sealed trait they implement.

Migration:

Before After
DecapsulationKey::generate(&ML_KEM_768)? DecapsulationKey::<MlKem768>::generate()?
EncapsulationKey::new(&ML_KEM_768, bytes)? EncapsulationKey::<MlKem768>::new(bytes)?
key.decapsulate(bytes)? key.decapsulate(&Ciphertext::<MlKem768>::new(bytes)?). The length check moves to Ciphertext::new, and decapsulate cannot fail.
encapsulate() returning (Ciphertext, SharedSecret) the same, typed (Ciphertext<A>, SharedSecret<A>). The bytes stay available through AsRef<[u8]>.

rind-aws-lc-rs's API is unchanged apart from the two new algorithms.

Measurements

These numbers come from a local x86_64 host (AVX-512) that other agents share, so they are noisy. Criterion ran with 1 s warm-up and 3 s measurement, through quest gate. Times are medians.

Operation rind aws-lc-rs libcrux-ml-kem rind / aws-lc-rs
ML-KEM-512 keygen 3.75 µs 5.34 µs 9.97 µs 0.70
ML-KEM-512 encaps 4.29 µs 6.03 µs 8.62 µs 0.71
ML-KEM-512 decaps 3.58 µs 7.43 µs 9.22 µs 0.48
ML-KEM-768 keygen 5.03 µs 9.90 µs 13.22 µs 0.51
ML-KEM-768 encaps 4.92 µs 9.88 µs 10.91 µs 0.50
ML-KEM-768 decaps 4.24 µs 11.35 µs 11.59 µs 0.37
ML-KEM-1024 keygen 5.99 µs 13.92 µs 16.79 µs 0.43
ML-KEM-1024 encaps 6.36 µs 14.37 µs 14.70 µs 0.44
ML-KEM-1024 decaps 5.37 µs 16.47 µs 16.35 µs 0.33

Matrix cache: keeping it boxed beat re-sampling the matrix each call. Re-sampling took encaps from 4.29, 4.92, and 6.36 µs to 5.08, 6.39, and 8.42 µs, and decaps from 3.58, 4.24, and 5.37 µs to 4.42, 5.61, and 7.46 µs (512, 768, and 1024).

Verification

  • After merging main (head below): just check origin/main passes, admin/ctgrind unit --release -- kem and the adapters' KEM test pass under memcheck, and a targeted admin/mutants run over every survivor CI reported catches all of them.
  • The previous head's dart-ios hit the 30 minute limit with the simulator silent for 20 minutes after a slow Xcode build (210 s, against 70 s on passing runs), while the native macOS arm64 Dart job passed the same ML-KEM code. Read as a runner hang, not rerun; this push runs it again.
  • just check origin/main passes. This covers lint, the workspace build, rind's tests, the variable-time scan on every target, admin/testing, quest check, and the bench-report render.
  • just armv7 passes under qemu on all three CPU paths.
  • just portable passes up to heap_scratch's planted self-test detects_a_leak_in_key_generation, which fails the same way on main with portable plus debug assertions. moq-dev/rind#600 fixes it with black_box.
  • aarch64: cargo clippy -p rind --lib --target aarch64-unknown-linux-gnu -D warnings passes. The aarch64 tests, including the k2 and k4 basemul paths, were not run locally. CI's arm64 runners run them.
  • admin/regenerate's generators reproduce the existing files unchanged and add only the new routines.

Decisions made without asking

  • SharedSecret is generic over the set, as the quest's goal says ("one key, ciphertext, and secret type per parameter set"). The bytes are 32 in every set, so it could stay untyped. Recommend keeping it typed, for symmetry and so a future KEM with a different secret size fits.
  • decapsulate is infallible. Once Ciphertext::new has checked the length, FIPS 203 decapsulation has no failure (implicit rejection). Recommend keeping it.
  • The matrix cache stays, boxed (Option<Box<_>>, wiped in the decapsulation key's drop). It measured faster, as above.
  • Decompression is portable Rust at every width. mlkem-native's AVX2 decompression was not imported. Decompression only reads public ciphertext, and rind already beats aws-lc-rs at every set. Recommend importing it only if a profile shows it matters.
  • Python, Dart, and rind-timing stay ML-KEM-768-only. They have no consumer for the other sets. Recommend leaving them until one asks.
  • The bench target is renamed mlkem768 → mlkem (bench.yml and bench-report updated). Historic mlkem768-* series keep their names.
  • BoringSSL's ML-KEM-1024 vector files are vendored (about 5 MB). They add the "nist" and encap/decap KATs that Wycheproof lacks for 1024.
  • Mutant runs build with rind/wipe (.cargo/mutants.toml's features). Since moq-dev/rind#461 made wiping opt-in, rind/tests/zeroing.rs was compiled out of mutant runs, so every wiping Drop replaced with () survived; this PR's DecapKey and SharedSecret drops did. With the feature, zeroing.rs kills them, as the existing "whose own Drop wipes all of it" exclusions already assumed. moq-dev/rind#609, which turns wipe on by default, would make the line redundant but harmless.
  • rind-timing's case list joins the tool code mutation skips. Its cases only feed the timing harness, so no test observes them; recommend this over a test that only runs each case.
  • The matrix transpose walks the lower triangle (for j in 0..i), the same swaps, which leaves no equivalent (i + 1) bound mutant to record.

Follow-ups

Models

Size: M
Implementer: Claude Code, Opus 5.5 (claude-opus-5-5), medium effort
Implementer: Claude Code, claude-opus-5-5, medium effort (iterate, M)
Implementer: Claude Code, claude-opus-5-5, medium effort (iterate, XS)

(Written by Claude Opus 5.5 with medium)

🤖 Generated with Claude Code

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, medium effort
  • Session: 01a1ce10-ccc7-7122-bf17-b7d2efd07b46, round 1
  • Head: 46206f4
  • Base: origin/main at b3322d0
  • Score: 9/10, The parameter-set generalization has substantial vector, differential, constant-time, and zeroing coverage with no actionable defects found, though builds and tests were not run as instructed.
  • Findings: none

kixelated and others added 2 commits November 11, 2026 01:15
Quest-Claim: e11f844db1ac1a2d
rind::kem offers ML-KEM-512 and ML-KEM-1024 beside ML-KEM-768 on every
backend. Sealed marker types MlKem512, MlKem768, and MlKem1024 parameterize
DecapsulationKey, EncapsulationKey, Ciphertext, and SharedSecret, so a
ciphertext of one set fails to compile against a key of another. They
replace kem::ML_KEM_768; Ciphertext::new checks the length, and decapsulate
takes &Ciphertext and cannot fail.

The FIPS 203 construction is generic over the rank, the compression
widths, and eta1, using s2n-bignum's basemul_k2 and k4 and mlkem-native's
AVX2 compression at 5 and 11 bits. Tests cover Wycheproof, ACVP, BoringSSL,
and CCTV vectors for each set, differential tests against aws-lc-rs, and
ctgrind, BINSEC, and zeroing checks. rind-aws-lc-rs gains ML_KEM_512 and
ML_KEM_1024.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title ML-KEM-512 and ML-KEM-1024 feat(kem)!: ML-KEM-512 and ML-KEM-1024 Nov 11, 2026
@kixelated
kixelated marked this pull request as ready for review November 11, 2026 10:13
@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 55 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: eb5b35c9-65ec-4f55-8a5c-3a890bec3523
Commits

Reviewing files that changed from the base of the PR and between b3322d0 and 46206f4.

Files ignored due to path filters: 2

  • benchmarks/tiers-dark.svg is excluded by !**/*.svg
  • benchmarks/tiers.svg is excluded by !**/*.svg
Files selected for processing: 70

  • .cargo/mutants.toml
  • .github/workflows/bench.yml
  • ALGORITHMS.md
  • ARCHITECTURE.md
  • MIGRATING.md
  • README.md
  • TESTING.md
  • admin/bench-report
  • admin/import-aws-lc
  • admin/mutants
  • admin/parse-asm/driver.mlkem.py
  • admin/sync-acvp
  • admin/sync-vectors
  • fuzz/fuzzer/kem.rs
  • fuzz/fuzzer/keys.rs
  • quest/m2/api/README.md
  • quest/m3/parity/aws-lc-extras.md
  • quest/m3/parity/ml-kem.md
  • quest/m3/parity/rustls.md
  • quest/m4/pq/README.md
  • quest/m4/pq/hqc.md
  • quest/m4/pq/rustls-p384-mlkem1024.md
  • quest/m4/pq/x-wing.md
  • quest/m4/protocols/hpke-pq.md
  • rind-aws-lc-rs/README.md
  • rind-aws-lc-rs/src/kem.rs
  • rind-aws-lc-rs/tests/keys.rs
  • rind-bench/Cargo.toml
  • rind-bench/benches/mlkem.rs
  • rind-dart/src/lib.rs
  • rind-python/src/kem.rs
  • rind-rustls/src/kx_group/mlkem.rs
  • rind-timing/src/cases.rs
  • rind/src/high/kem.rs
  • rind/src/lib.rs
  • rind/src/low/aarch64/mlkem_basemul_k2.rs
  • rind/src/low/aarch64/mlkem_basemul_k4.rs
  • rind/src/low/aarch64/mod.rs
  • rind/src/low/generic/mlkem.rs
  • rind/src/low/generic/mlkem_poly.rs
  • rind/src/low/generic/mlkem_sample.rs
  • rind/src/low/mod.rs
  • rind/src/low/x86_64/aws_lc/mlkem_poly_compress_d11_avx2_asm.apple.s
  • rind/src/low/x86_64/aws_lc/mlkem_poly_compress_d11_avx2_asm.linux.s
  • rind/src/low/x86_64/aws_lc/mlkem_poly_compress_d5_avx2_asm.apple.s
  • rind/src/low/x86_64/aws_lc/mlkem_poly_compress_d5_avx2_asm.linux.s
  • rind/src/low/x86_64/mlkem.rs
  • rind/src/low/x86_64/mlkem_basemul_k2.rs
  • rind/src/low/x86_64/mlkem_basemul_k4.rs
  • rind/src/low/x86_64/mlkem_compress.rs
  • rind/src/low/x86_64/mlkem_sample_poly_ntt_8x.rs
  • rind/src/low/x86_64/mod.rs
  • rind/src/mid/mlkem.rs
  • rind/src/mid/mlkem/sample_ntt.rs
  • rind/src/mid/mlkem768.rs
  • rind/src/mid/mod.rs
  • rind/src/mid/sha3.rs
  • rind/src/test/binsec.rs
  • rind/tests/cpu.rs
  • rind/tests/crosschecks.rs
  • rind/tests/debug.rs
  • rind/tests/heap_scratch.rs
  • rind/tests/zeroing.rs
  • thirdparty/acvp/ML-KEM-encapDecap-FIPS203-tr1.json
  • thirdparty/acvp/ML-KEM-keyGen-FIPS203.json
  • thirdparty/vectors/boringssl/crypto/mlkem/mlkem1024_decap_tests.txt
  • thirdparty/vectors/boringssl/crypto/mlkem/mlkem1024_encap_tests.txt
  • thirdparty/vectors/boringssl/crypto/mlkem/mlkem1024_keygen_tests.txt
  • thirdparty/vectors/boringssl/crypto/mlkem/mlkem1024_nist_decap_tests.txt
  • thirdparty/vectors/boringssl/crypto/mlkem/mlkem1024_nist_keygen_tests.txt
Pre-merge checks: Inconclusive 2

Failed Failed checks: 2 inconclusive

Check name Status Explanation Resolution
Title check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
Description check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@kixelated

Copy link
Copy Markdown
Collaborator Author

Maintainer pick (2026-11-11, /quest-run): ✅ /quest-merge (rejected: skip, /quest-delete).

(Written by Claude Opus 5.5 with high)

kixelated and others added 3 commits November 11, 2026 10:10
# Conflicts:
#	rind/src/mid/mlkem768.rs
Mutant runs build with `rind/wipe`, so rind/tests/zeroing.rs runs and
kills a wiping `Drop` replaced with `()`. Since wiping became opt-in,
every such mutant survived. The transpose loop walks the lower triangle,
which leaves no equivalent `(i + 1)` bound mutant. The debug test covers
`Ciphertext`, a rind-dart test the unknown KEM id, and rind-timing's
case list joins the tool code mutation skips. Stale `mlkem768` entries
follow the rename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	.cargo/mutants.toml
kixelated added a commit that referenced this pull request Nov 11, 2026
Mutant runs now build with `rind/wipe`, as #606 does, so the
zeroing and heap scratch tests catch ML-DSA's wiping `Drop`s. New tests
check the portable `reduce` and `chknorm` against their contracts, which
aarch64's backend comparison could not, and that `MlDsaPrivateKey`'s
`encoded_len` matches its encoding. The rest are recorded as equivalent
or unreachable in .cargo/mutants.toml, each class with why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated disabled auto-merge November 12, 2026 04:42
@kixelated
kixelated added this pull request to the merge queue Nov 12, 2026
Any commits made after this event will not be merged.

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.

1 participant