Skip to content

pykyber V2 - #44

Merged
Zikithezikit merged 72 commits into
devfrom
dev-v2
Oct 31, 2026
Merged

Zikithezikit merged 72 commits into
devfrom
dev-v2

Conversation

@Zikithezikit

Copy link
Copy Markdown
Owner

No description provided.

Zikithezikit and others added 30 commits October 29, 2026 08:54
Adds PLAN-2.0.md (work packages for issue #3), AGENTS.md (rules for
coding agents), CLAUDE.md and opencode.json so both Claude Code and
opencode load the same project instructions.

All 2.0 work targets dev-v2 and is rebase-merged; every commit must
pass on its own, and review fixes are folded into the commit they fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2.0 development happens on dev-v2 (issue #4); merges into it must be
built for every platform before the 2.0.0 release. pull_request already
covers PRs into any branch, so only the push trigger was missing.
Setup, build/test commands, the 2.0 branch/commit rules
(dev-v2, wp<N>-* branches, Conventional Commits, fixup folding,
rebase-merge so every commit passes on its own), and the security
reporting pointer (issue #4).
PR template carries the constant-time/security checklist from AGENTS.md
so it is in front of every PR, not only agent-written ones. CODEOWNERS
routes src/ and SECURITY.md to the maintainer for review (issue #4).
Per WP-0 (issue #4). blank_issues_enabled: false plus a contact link
routing security reports to GitHub private vulnerability reporting, since
a public security issue template would defeat the point of private
advisories; bug/feature templates carry the same pointer.
Root security policy per WP-0 (issue #4): reports go through GitHub
private vulnerability reporting (enabled), never public issues; scope,
disclosure, and response expectations stated. Migrates the six resolved
audit items from .opencode/SECURITY.md (gitignored, so invisible to
contributors) verbatim, with a provenance note. The original stays in
place as the internal log.
Agent-written PR bodies reached GitHub with broken characters this
week (mojibake from non-ASCII) and one read like generated output, so
the rules follow: prose for the reviewer with the facts and evidence,
a 7-bit ASCII byte check before anything is posted, and the REST API
fallback plus body read-back for the GraphQL failures the gh CLI hits
in this repo.
test_kyber_1024_params asserted the Kyber512/768 compression parameters
(du=10, dv=4) for Kyber1024, which compresses with du=11, dv=5 per the
Kyber round-3 specification (2021-08-04), Sec. 1.4, Tables 1-2, and FIPS
203 final, Sec. 8, Tables 2-3 (identical sizes: pk 1568, sk 3168,
ct 1568). PolyVec-compressed bytes are 4*352 = 1408 and the ciphertext
is 4*352 + 160 = 1568 bytes.

The constants in src/kyber/params.rs were already correct: only the
test assertions were wrong (both the first failing one,
POLYVECCOMPRESSEDBYTES = 4*320, and CIPHERTEXTBYTES = 4*320+128).
No runtime behavior changes; Kyber v1.0.5 output is unaffected.

Fixes #5
Freeze the byte output of pykyber v1.0.5 (round-3 Kyber) for seeded keygen, seeded encapsulation via the Rust crypto_kem path at tag v1.0.5, and decapsulation. Captured from the pinned PyPI wheel (sha256 recorded in the file meta) plus a cargo --release run in a git worktree at tag v1.0.5; both layers cross-checked equal by tests/vectors/generate.py. The vector file is append-only: never regenerate or edit it to make a test pass.
tests/legacy_vectors.rs replays seeded keygen, seeded encapsulation and decapsulation from the append-only vector file and asserts byte equality with v1.0.5; the rng guard panics if any seeded path consumes randomness. python/tests/test_legacy_vectors.py checks the same bytes through the Kyber512/768/1024 wrappers (seeded keygen, decapsulate, from_keys, and encapsulate/decapsulate roundtrip over v1.0.5 key material). serde and serde_json are dev-only dependencies; Cargo.lock is left untouched (its cleanup stays with the owed chore PR).
Freeze the byte output of pykyber v1.0.5 (round-3 Kyber) for seeded keygen, seeded encapsulation via the Rust crypto_kem path at tag v1.0.5, and decapsulation. Captured from the pinned PyPI wheel (sha256 recorded in the file meta) plus a cargo --release run in a git worktree at tag v1.0.5; both layers cross-checked equal by tests/vectors/generate.py. The vector file is append-only: never regenerate or edit it to make a test pass.
tests/legacy_vectors.rs replays seeded keygen, seeded encapsulation and decapsulation from the append-only vector file and asserts byte equality with v1.0.5; the rng guard panics if any seeded path consumes randomness. python/tests/test_legacy_vectors.py checks the same bytes through the Kyber512/768/1024 wrappers (seeded keygen, decapsulate, from_keys, and encapsulate/decapsulate roundtrip over v1.0.5 key material). serde and serde_json are dev-only dependencies; Cargo.lock is left untouched (its cleanup stays with the owed chore PR).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fixed where the change is plainly behavior-preserving:
- fips202: add Default for KeccakState, use u64::rotate_left, parenthesise
  shift amounts
- kem, indcpa: drop needless borrows
- poly: KYBER_Q.div_ceil(2) for the public rounding constant

Allowed with a comment instead of rewriting hot-path code:
- needless_range_loop, identity_op, explicit_counter_loop in poly, polyvec,
  indcpa (and one loop in ntt): the loops mirror the reference C layout
- manual_is_multiple_of in fips202: is_multiple_of needs Rust 1.89 and no
  MSRV is declared

Legacy output is unchanged: tests/legacy_vectors.rs and
python/tests/test_legacy_vectors.py pass untouched. A single commit
because each lint-kind commit alone would still fail clippy.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…eck.sh

requirements-dev.txt pins the ranges (pytest <9 keeps Python 3.8 usable).
scripts/check.sh is the fmt, clippy, test, maturin develop, pytest gate that
CI will run per commit. CONTRIBUTING.md now points at both and drops the
stale baseline-failures note.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Adds a test job (Linux, macOS, Windows; Python 3.8 and newest 3.x) and a
commits job that runs scripts/check.sh through git rebase --exec on each
commit of a PR, since PRs are rebase-merged. The wheel release now needs
the test job.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Literal transcriptions of Algorithms 5, 8, 9, 10, 11 and Compress_d /
Decompress_d, plus hashlib-derived SHA-3/SHAKE vectors, compared with the
legacy Keccak, NTT, CBD and poly encoding code. Also records that legacy
d=11 compression deviates from Compress_11 at two residues.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
G(m || H(ek)) and J(z || c) in ML-KEM hash secret inputs, and FIPS 203
section 3.3 requires intermediates to be destroyed. The shake paths already
zeroize on drop through KeccakState; these two one-shot hashes kept the
state in a plain array. Output is unchanged (legacy golden vectors pass).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ParamSet<K> derives every FIPS 203 Table 2 parameter and the Table 3 sizes
from k, with aliases for the three standard sets, so the new code does not
need another _512/_768/_1024 copy. PolyVec<K> wraps the legacy Poly NTT,
reduction and ByteEncode12 routines and is tested against the three legacy
polyvec types. from_bytes reduces mod q like ByteDecode12; the legacy
poly_frombytes does not. The module is crate-private and unused until WP-3.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Algorithm 7 over the shared SHAKE128 XOF. rej_uniform becomes pub(crate)
(no body change). The test compares against a line-by-line transcription of
the algorithm over 3000 seeds, including inputs that need more than the
initial three blocks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The legacy d=11 routine is off by one at the residues 1875 and 3125 and
cannot change without breaking the Kyber-1024 golden vectors (checked by
applying the fix temporarily: legacy_kyber_v1_0_5_outputs_are_frozen
fails). ML-KEM gets its own, using ((x << 11) + 1664) * 645084 >> 31, which
the unit test compares with FIPS 203 (4.7) for every residue and every
negative representative.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… jobs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
poly_compress_d11 becomes poly_compress(r, a, d) with a public width d,
so the du, dv and message paths of K-PKE can all use one exact,
division-free routine instead of the legacy per-level loops. d is a
runtime argument because stable Rust cannot pass ParamSet's associated
constants as const generics. The multiplier is checked against FIPS 203
(4.7) for every residue and every negative representative for d = 1, 4,
5, 10 and 11. The test that d = 11 differs from the legacy routine only
at 1875 and 3125 is kept. No legacy code changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
poly_decompress is ByteDecode_d followed by Decompress_d from the
(y * q + 2^(d-1)) >> d form, exact over all 2^d inputs for the widths
ML-KEM uses. poly_from_msg is Decompress_1(ByteDecode_1(m)) with the
all-ones or all-zeros mask routed through black_box, replacing the
legacy poly_frommsg pattern that CVE-2024-37880 hit. Neither touches
legacy code.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Zikithezikit and others added 27 commits October 30, 2026 15:55
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
All four file kinds (seed decapsulation, encapsulation, key generation
from a seed, semi-expanded decapsulation) for the three parameter sets,
pinned to a commit and a SHA-256 per file in the manifest. The result
rule of the schema is applied as written.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A dev-only differential test (ml-kem 0.3.3, FIPS 203 final) over a
fixed-seed generator: keygen from a seed, encaps with a given m, decaps
of valid, bit-flipped and random ciphertexts, decaps under a changed
dk_PKE or z, and the modulus and hash checks after random byte changes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The mask built from a message bit in the legacy poly_frommsg is the pattern
behind CVE-2024-37880 (Clangover), where a compiler turned it into a branch.
Route the bit and the mask through core::hint::black_box. Output is
unchanged; the golden vectors pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Route the accumulated difference, the flag and the select mask through
core::hint::black_box, the same idea as mlkem::ct, so the compiler cannot
prove the mask is 0 or 0xff and turn the select into a branch. The release
disassembly shows setne/cset and a vector select with no branch on the
compare result. Output is unchanged; the golden vectors pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Key generation and encapsulation in the Python bindings used thread_rng().
Use rand::rngs::OsRng, call try_fill_bytes in the legacy KEM functions, and
return KyberError::RandomBytesGeneration when the generator fails instead of
panicking. Wipe the half-built secret key when the second draw (z) fails and
hold the partly filled seed in Zeroizing. The Python wrappers keep the
RandomnessError subclass instead of re-wrapping it. Seeded paths and the
golden vectors are unchanged; the random-fill order and sizes are as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Hold m', the KDF input and the re-encrypted ciphertext of crypto_kem_dec*
in Zeroizing so they are wiped on every exit, unwinding included, and keep
the implicit-rejection flag in Zeroizing<u8>. The PRF output buffers in
poly_getnoise_* are the secret noise (the key in key generation, r in
encryption) and were never wiped; wipe them too. The release disassembly
shows the wipe of the flag and the buffers on both the normal and the
cleanup path. Add a test for the implicit-rejection output, derived from z
and H(c), for all three levels; it also passes on the commit before this
series. Output is unchanged and the golden vectors pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the secret key

Kyber512/768/1024.from_keys accepted any public key and secret key of the
right sizes, so a pair from two different key pairs built a keypair whose
decapsulate silently returned a different secret than encapsulate had
produced. The round-3 secret key embeds the public key (sk_pke || pk ||
H(pk) || z), so compare the two (both are public) and raise KyberError on a
mismatch. The offset comes from named constants. A wrong-size secret key
keeps its existing size error at use. Approved by the maintainer in the
discussion on issue #18; ML_KEM_*.from_keys is not changed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The legacy bindings returned secret keys and shared secrets as Vec<u8>, which
PyO3 turns into bytes and then frees with the secret still in the allocation.
Hand them over as a SecretBytes wrapper that builds the bytes object and then
wipes the Rust buffer, and wipe the fixed-size copy from the safe API. The
Python object itself cannot be wiped. Output is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The seeded derive path hands the KEM code a stand-in generator that is never
asked for bytes. Now that the legacy code draws randomness through
try_fill_bytes, make that method return an error, so a future change that
reaches it gets RandomBytesGeneration and not a panic. No path from Python
reaches it today.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ation functions

crypto_kem_keypair* and crypto_kem_enc* indexed caller-supplied pk, sk, ct,
ss and seed slices without checking their lengths, unlike decapsulation, so a
Rust caller with a short buffer panicked. Return InvalidInput instead. The
Python bindings already size every buffer, so no Python path reaches this.
Output is unchanged; the golden vectors pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Lockfile-only patch bump of an existing dependency. The advisory needs the
log feature, which pykyber does not enable, so it was not reachable here;
the legacy bindings no longer use thread_rng at all.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The old benchmark parsed the Kyber performance tests only and its figures came
from a debug build. benchmark.py now times ML_KEM_* and Kyber* directly (median
of 7 rounds of 500 calls), writes results.json, and draws two charts that replace
the four old ones. make benchmark builds the release extension first. The chart
dependency is in performance-tests/requirements.txt, not in requirements-dev.txt.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXWjkNs3Htog36snV9FcXb
Same facts, fewer words: install, quick start, a shorter class tour, sizes,
errors, the Kyber deprecation, security status and the new benchmark table and
charts. Every Python block in it was run, and the error message it shows is the
real one. The performance section now says what was measured and on what.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXWjkNs3Htog36snV9FcXb
Adds LICENSE-MIT and LICENSE-APACHE (the Apache text is the apache.org file,
11358 bytes, SHA-256 cfc7749b...523d30) and removes LICENSE. Sets license =
"MIT OR Apache-2.0" in Cargo.toml and pyproject.toml, with license-files, and
drops the Public Domain classifier. The built wheel's METADATA has
License-Expression: MIT OR Apache-2.0 and ships both files. Releases up to 1.0.5
stay under the Unlicense; the README, CONTRIBUTING.md, the changelog and the
agent notes say so.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CXWjkNs3Htog36snV9FcXb
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ents file

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Zikithezikit Zikithezikit self-assigned this Oct 31, 2026
@Zikithezikit
Zikithezikit merged commit 99d5498 into dev Oct 31, 2026
90 checks passed
@Zikithezikit
Zikithezikit deleted the dev-v2 branch October 31, 2026 09:36
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