Repository navigation
Key object rework prototype: AESKey<P, M> and HmacKey<H> producing their workers through role traits - #170
Closed
dghgit wants to merge 46 commits into
Closed
Key object rework prototype: AESKey<P, M> and HmacKey<H> producing their workers through role traits#170dghgit wants to merge 46 commits into
dghgit wants to merge 46 commits into
Conversation
1 of 4 tasks
…de traits (prototype) The key-object layer of the symmetric key rework, built beside the existing constructors. symmetric_key.rs holds FromKeyMaterial<KEY_LEN> (from_key_material plus a provided generate(rng) that tags the draw with the implementor's KEY_TYPE), FromAnyKeyMaterial for constructions that take a key of any length (the only trait with a weak-key form), the AeadEncryptionKey / AeadDecryptionKey pair with the one-shots provided over create_encryptor(rng) / create_decryptor(nonce), and the MacGenerationKey / MacVerificationKey pair with mac, mac_out and verify provided over create_generator() / create_verifier(). hazmat gains Mode<P> (KeyState and KEY_TYPE) and the AeadEncryptMode / AeadDecryptMode constructors the role impls forward to. No method instantiates a DRBG for the caller: everything that draws randomness takes &mut dyn RNG, and the create_* names carry no _rng suffix because they have no twin. KeyMaterial::from_rng accepts an unsized RNG so the provided generate can use it. Two departures from the issue draft, both forced by the tree as it stands: the role and mode traits carry KEY_LEN while the cipher traits still do, so that Encryptor and Decryptor can be bounded by them; and the MAC role pair bounds its workers on MAC rather than on a MACGenerator / MACVerifier split, because putting the same final names on two traits while MAC still has them makes every call site with both in scope ambiguous. Both resolve when the constructors leave the worker traits. Assisted-by: Claude:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…er a key state (prototype) GCM<TAG_LEN = 16> is the mode type a mode-bound key names, AES128Key<GCM>. GcmKeyState<P> is what such a key stores: the keyed permutation and the hash subkey H = CIPH_K(0^128) (SP 800-38D Algorithm 4 step 1), derived once per key rather than once per message. Gcm's setup is split so that the per-message half (J0, CIPH_K(J0), the counter start) runs over an existing state, and Gcm<P, Encrypting, ..>::from_key_state(state, rng) / Gcm<P, Decrypting, ..>::from_key_state(state, nonce) are the constructors a permutation crate's mode impls call; the decrypt side is infallible. do_encrypt_init_rng and do_decrypt_init are unchanged in behaviour and still expand per message. The previous prototype's GcmMode marker, GCM constant and default-RNG from_perm are gone: the mode is a type, not a value, and the key path offers no form that instantiates a DRBG. Assisted-by: Claude:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he packet bench (prototype) AESKey<P: AESParams, M: Mode<AESInternal<P>>> stores M::KeyState and nothing else; AES128Key<M>, AES192Key<M> and AES256Key<M> are the aliases, so AES128Key<GCM> and AES128Key<GCM<12>> are distinct key types over the same material and a key under a second mode is a second from_key_material call. FromKeyMaterial<16 | 24 | 32> is implemented per key length (the KeyMaterial<N> argument cannot be sized from P::KEY_LEN on stable), checks M::KEY_TYPE and then runs the existing AESInternal validation and expansion once. The AeadEncryptionKey / AeadDecryptionKey impls forward to whichever mode traits M implements, so the mode decides the role. No Debug, Clone written out so M need not be Clone. The Mode / AeadEncryptMode / AeadDecryptMode impls for GCM over the three AES permutations live here, one set per key length, because a generic impl in cipher over P: ElectronicCodeBook<KEY_LEN, 16> leaves KEY_LEN unconstrained on Mode<P> (E0207) and the per-P form needs a const expression; they collapse into cipher once KEY_LEN leaves the cipher traits. key_tests pins that create_encryptor(rng) produces the same nonce, ciphertext and tag as do_encrypt_init_rng from the same material and RNG, that the role-trait one-shots match the cipher traits' key-taking one-shots byte for byte and zeroize on a failed tag, interoperation of the two paths at every key length, the GCM<12> key type, generate (including refusal of an RNG below the key strength), the construction-time errors, and Clone; a compile_fail doctest shows a wrong-length nonce refused by the type. aes_gcm_packet_benches compares the two paths over 16 KiB packets, with the OS-DRBG-per-packet arms now explicit on both sides. Assisted-by: Claude:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… buffer (prototype)
HmacKey<H> is the key object: H after absorbing K XOR ipad and H after absorbing K XOR opad,
the precomputation RFC 2104 Section 4 describes ("can be precomputed only once at the time of
generation of the key K, or before its first use", saving "the application of the compression
function of H on two B-byte blocks" per message). It is built once from key material of any
length through FromAnyKeyMaterial (from_key_material, from_key_material_allow_weak, and
generate(rng), which draws the hash's output length), implements MacGenerationKey and
MacVerificationKey, and holds no key bytes. create_generator() and create_verifier() clone the
two states into a fresh HMAC<H>, so any number of workers can be in flight from one key.
HMAC<H> becomes { hasher, outer }: the KEY_BUF_LEN parameter, the key buffer and the key length
go, and the aliases in sha2, sha3 and sm3 are HMAC<SHA256> and so on. MAC::new and
new_allow_weak_key build an HmacKey and take one worker from it, so every existing caller and
vector suite runs through the new pads unchanged; do_final finishes the inner hash into the
stored outer state, and from_suspended rebuilds the outer state from the re-supplied key with
the state bytes unchanged. The padded key block is built in a Secret scratch rather than a vec,
which removes two of the three heap allocations per message the old path made.
Sizes (size_of, x86-64): HMAC<SHA256> and HmacKey<SHA256> 224 (worker was 184), the SHA-512
family 416 (was 344), the SHA-3 family 880 for all four (was 520 to 592); HKDF_SHA256 256 (was
296), HKDF_SHA384/512 448 (was 392). The memory tables in the alias modules and sha2::hkdf are
updated to these figures. hmac_key_tests pins key-against-constructor agreement for short,
block-length and over-length keys on SHA-256, SHA-512 and SHA3-256, independent workers from one
key, the one-shots, the key-type and strength policy, generate, suspend/resume and Clone.
Assisted-by: Claude:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Expand builds HmacKey<H> from the PRK once and calls create_generator() for each T(i), which is the many-messages-one-key case the pad cache is for; do_extract_init builds the salt key with from_key_material_allow_weak, as it used new_allow_weak_key before. Output bytes and the suspended-state layout are unchanged, and the HKDF vector suites pass as before. Assisted-by: Claude:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e keys (prototype) AeadEncryptMode and AeadDecryptMode gain encrypt_detached_in_place(state, rng, aad, buf) and decrypt_detached_in_place(state, nonce, aad, buf, tag), provided over encrypt_init / decrypt_init; AeadEncryptionKey and AeadDecryptionKey gain the same pair over create_encryptor / create_decryptor. The provided bodies (in_place.rs) copy the message through a 64-byte stack buffer and encrypt each chunk back into the bytes it came from, which needs no second buffer the size of the message because a streaming worker never emits more than it has consumed. The decryptor zeroizes the buffer on any error, as the _out forms do. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…ed (prototype) CCM<NONCE_LEN, TAG_LEN, AAD_LEN, DATA_LEN> is the M of a mode-bound CCM key; the frame is on the mode because the streaming worker a key produces is the fixed-frame CcmEncryptor / CcmDecryptor. CcmPacket::from_perm and random_nonce, and CcmEncryptor::from_perm / CcmDecryptor::from_perm, build from an already-expanded schedule; the existing do_*_init forms now go through the latter. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…cket (prototype) The CCM Mode / AeadEncryptMode / AeadDecryptMode impls for the three AES permutations, per key length as for GCM. The role impls on AESKey forward the in-place one-shots to the mode, and the CCM impls override them to run a CcmPacket committed to the message's own length, so a CCM key takes any length in place while its streaming worker and the provided _out one-shots over it stay fixed-frame. key_tests covers both paths at lengths around the chunk size, the fixed-frame refusal, agreement with the packet API, and zeroization on a bad tag. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…e mode traits (prototype) in_place.rs becomes provided.rs and gains encrypt_inline / encrypt_detached over an already-built worker; the role keys' two _out one-shots call them. No behaviour change: this is so the supplied-nonce forms that follow can share the bodies with the drawing forms. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…(prototype) AeadEncryptMode gains encrypt_init_nonce(state, nonce), infallible as decrypt_init is, and a provided encrypt_detached_in_place_nonce over it. AeadEncryptionKey gains create_encryptor_nonce and the _nonce twins of its one-shots (encrypt_with_aad_nonce[_out], encrypt_detached_nonce[_out], encrypt_detached_in_place_nonce), provided over the shared bodies; the trait docs say the nonce is drawn by default and what a caller of a _nonce form takes on. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…CCM encryptors (prototype) Gcm::from_key_state_nonce and CcmEncryptor::from_perm_nonce, the constructors a permutation crate's encrypt_init_nonce impl calls; the drawing from_perm now goes through the CCM one. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
… the in-place nonce form (prototype) AESKey forwards create_encryptor_nonce and encrypt_detached_in_place_nonce to the mode. The CCM impls run the in-place nonce form through CcmPacket at any A.1 nonce length, so a key such as AES128Key<CCM<8, 8, 0, 0>> is usable through the supplied-nonce forms while the drawing forms, which carry the 12-byte floor, are never instantiated for it. key_tests checks every _nonce form against the drawing form's bytes on GCM and CCM, and the short-nonce key against the packet API. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
The command builds an AES128Key<CCM<N, T, 0, 0>> (and the 192/256 forms) and runs encrypt_detached_in_place_nonce / decrypt_detached_in_place on the input it read, so it no longer names CcmPacket. A small CcmKeys trait with a generic associated type lets `run` stay generic over the key length while choosing the nonce and tag lengths at run time. The AAD is read whole now, since the one-shots take it as a slice; the declared-length streaming of a regular --aad-file goes, along with its help text. The q-limit message computes the limit itself. test_aes_ccm.sh passes as before. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…_LEN on the mode type (prototype) Mode<P> for CCM needs no KEY_LEN (the key state is the schedule itself), and the two AEAD mode traits carry it, so the impls live here once rather than per key length in each permutation crate. The in-place one-shot overrides move with them. CCM::MAX_PAYLOAD_LEN is the A.1 limit for the nonce length, for callers that no longer see the packet type. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…rototype) Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…fixed-frame stream (prototype) The structural tests no longer name CcmPacket: the one-shots are CCM::encrypt_detached_in_place_nonce / decrypt_detached_in_place over the toy permutation, and the chunked and frame-holding checks run through CcmEncryptor / CcmDecryptor at the message's length. The toys derive Clone, which the mode impls ask of a key state. The suspend tests are untouched for now. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
… the trait pair (prototype) sp800_38c_tests, ccm_bc-test-data, ccm_wycheproof, key_tests and the criterion benches no longer name CcmPacket. The Appendix C vectors go through CCM::encrypt_detached_in_place_nonce / decrypt_detached_in_place and the pair's any-length inline one-shots, with the chunking sweep on the pair's streams at each example's frame; the progressive-AAD check feeds the pair's do_update_aad in pieces, with C.4's 64 KiB AAD as its capacity. The test of new_with_lengths' declared-AAD accounting goes with that constructor, which has no public counterpart; the pair's own AAD and frame bounds are already pinned. The memory table keeps the pair's sizes. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
… one-shot (prototype) Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…/ CcmDecryptor (prototype) The packet type is now reachable only through the pair and the CCM mode's in-place one-shots, so its SuspendableKeyed impl moves to the pair: the adapter's state is the packet's plus the held-back AAD, its length and the phase flag, and the decryptor adds the held-back tag. A resumed state must owe no more than the frame, and one that says the header is not yet formatted must have absorbed nothing. new_with_lengths and do_update_aad on the packet had no caller left and go. The module docs and the modes overview are rewritten over the pair, the A.1 shape compile_fail cases move to CcmEncryptor, and the suspend tests run over the pair, with the AAD buffer, phase flag and tag bounds pinned alongside the packet's. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…de-bound key (prototype) The module docs describe one family, AES_CCM_128 and friends, with the supplied-nonce and in-place examples; the memory tables are re-measured (key schedule, pair sizes, massif peaks for the in-place, streaming and one-shot paths). The alg-name and suspend tests use the pair. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…ototype) Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
… lengths up front (prototype) AeadEncryptWorker / AeadDecryptWorker are the cipher traits' streaming methods without their constructors; the role and mode traits bound their workers on them, so a worker a key produces need not implement a constructor it cannot have. AeadPacketEncryptMode / AeadPacketDecryptMode and AeadPacketEncryptionKey / AeadPacketDecryptionKey are the forms whose constructors take the AAD and payload lengths, for a mode that must commit to them before its first byte; their one-shots have the lengths in hand and are provided over the shared bodies, which now include the decrypting _out forms and the inline-layout length check. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…/ CcmPacketDecryptor as its workers (prototype) A CCM key's constructors now take the AAD and payload lengths, so the frame leaves the mode type and the workers hold nothing back: the encryptor is the packet state alone and the decryptor adds only the inline tag it holds past the declared payload. The in-place one-shots run the packet directly. Gcm's workers forward to the cipher traits, named by path since the method names coincide. The declared-AAD update returns to the packet, crate-private. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…e (prototype) AES128Key<CCM<12, 16>> and friends implement AeadPacketEncryptionKey / AeadPacketDecryptionKey by forwarding to the mode. ccm_key_tests covers every form at any length against the pair, the streams holding to the declared lengths, the up-front refusals, zeroization and the short-nonce key, with the worker sizes the docs quote pinned. key_tests keeps GCM, with its generic helpers naming the worker traits by path. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…ype) Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…th of the caller's key, so a MAC keyed by a block cipher can pass it (prototype) Ported from feature/cmac-gmac 0d004a9e: the weak key takes the caller's key length when it fits the buffer, raised to the shortest length that can carry the lowered strength; keys that are empty or longer than 64 bytes keep the old per-strength lengths. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
… MAC role keys forward to (prototype) Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…AC_ALG_NAME on ModeNames (prototype) Ported from feature/cmac-gmac 572c4d09 and reshaped for the key objects: the keyed permutation and the two subkeys are a CmacKeyState, derived once per key, and a Cmac worker is built from it by from_key_state or, for one message, by MAC::new from the material as before. The mode reports ModeNames::CMAC_ALG_NAME like the others; the AES permutations' and the benches' ModeNames impls gain the constant here so that the tree builds at this commit. The known-answer suites move to the aes crate, since cipher tests only over toys. Assisted-by: Claude:claude-mythos-5-1 Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
dghgit
force-pushed
the
feature/key-rework
branch
from
October 24, 2026 10:19
69e24ba to
a615121
Compare
mwcw
approved these changes
Oct 31, 2026
…buffers, and the CLI's optional info and key-file precedence Squash of fix/hkdf-review. From local/hkdf_review_44682f23.md and local/hkdf_fixes_plan.md (phases 2-7, the parts a33d9d5 had not already done). hkdf: - A salt of any key type but Unknown keys the extract. do_extract_init handed the salt to HmacKey, which takes only MACKey or Zeroized material, so a CryptographicRandom or Seed salt failed with InvalidKeyType against docs that promised any KeyType. The salt and the PRK are now re-typed through one helper, mac_key_copy, sized by HMAC's LARGEST_HASHER_BLOCK_LEN; bytes longer than the hash block are hashed first, as HMAC does with such a key (RFC 2104 Section 3), so the output is unchanged. KeyType::Unknown stays refused: promoting bytes of unknown provenance is a hazardous operation. test_entropy_tracking asserted the old rejection of a random-typed salt and now asserts acceptance with an Unknown output. - derive_key_from_multiple_out ran a second entropy tracker and set the output strength unconditionally, which set_security_strength rejects on an Unknown key, so a partial-entropy input errored where derive_key returned an Unknown key. The keys go through do_extract_update_key and the object's own tracker types the PRK and output. - expand_out requires a PRK of at least HashLen (RFC 5869 Section 2.3) and accepts one of any length above it; 16- and 31-byte PRKs were accepted and 65-byte ones refused before. T(i) lives in a Secret, the HMAC key is built before any byte of okm is written, and a failure after that zeroizes the partial output. - Citations: "RFC5896" is RFC 5869; the default-salt note attributed "HashLen zeros" to SP 800-56Cr2, where Sections 4.1 and 5.1 say one hash input block of zeros and HashLen zeros is RFC 5869 Section 2.2, with the comment now saying why the empty salt keys HMAC the same as either; the seeding threshold is stated as library policy, with SP 800-133r2 Section 6.3 note (g) as the nearest standard text. hmac: - LARGEST_HASHER_BLOCK_LEN is public, for the copies above. - The inner digest is finished into a Secret array rather than a Vec: key-derived state no longer sits in freed heap memory, and the finalisation path makes no allocation. Output is unchanged; no test can observe the zeroization. cli: - hkdf-sha*: --additional-input is optional and defaults to empty (RFC 5869 Section 2.3), so Appendix A.3 runs as written; the salt bound is the 1024-byte buffer the command holds, not a 128-byte figure described as 1024 bits; the help names the RFC's steps. - hmac-*: the key comes from --key-file when both it and --key are given, as the help text says and the other loaders do. Tests: derive_key_from_multiple agrees with derive_key on a 16-byte key; extract gives one PRK for a salt typed MACKey, CryptographicRandom, Seed or SymmetricCipherKey, and a 100-byte salt gives the PRK its hash gives; PRKs of 31 and 63 bytes are refused and 32 and 64 accepted; a refused expand leaves a pre-filled buffer as it was; PRKs of 64 to 1000 bytes expand to T(1) || T(2) as computed with HmacKey directly; shell tests pin RFC 5869 A.3 and both file-over-hex precedences. cargo mutants on crypto/hkdf/src/lib.rs: 153 mutants, 107 caught, 45 unviable, 1 missed (a HkdfStates::try_from arm, untouched here). Assisted-by: Claude:claude-mythos-5-1
…nto a new HashOneShot: Hash + Default trait with provided bodies, called as SHA256::hash(msg) with no instance
Hash carried hash(self, data) and hash_out(self, data, output), one-shots that needed an
instance to call them -- SHA3_256::new().hash(&data) -- and that every one of the 19
implementors wrote out by hand (do_update then do_final, or an equivalent squeeze). As
by-value receivers they could never have been called through a trait object either.
HashOneShot: Hash + Default now holds them as receiver-less provided functions, so the
one-shot is SHA256::hash(msg) and an implementation is `impl HashOneShot for X {}`. It is
implemented where Default is the function itself: SHA-2 (incl. SHA512t<T>), SHA-3, SHAKE,
SM3, Ascon-Hash256 and Ascon-XOF128. The parameterised functions -- cSHAKE, KMAC, TupleHash,
ParallelHash (fixed and XOF forms) and Ascon-CXOF128 -- and the HashFactory / XOFFactory,
where an uncustomised default would be a different function under the same name, keep an
inherent instance method hash(self, data) / hash_out(self, data, out) with the streaming
body, so existing call sites on them are unchanged. Hash itself is left with only
receiver-taking methods, one step towards a dyn-compatible Hash.
Bounds that were `Hash + Default` because they called the one-shot on a fresh instance are
now `HashOneShot`: HMAC / HmacKey / HMACParams, HKDF, MLDSAParams::PreHash in both ML-DSA
crates, the hash_df / hashgen helpers of Hash_DRBG, and the test framework's test_hash and
the pre-hash signature tests. Call sites of the form X::new().hash(..) / X::default().hash(..)
become X::hash(..) throughout crypto/, the crate docs and INTRODUCTION.md. The framework's
output-buffer and XOF checks no longer exercise a one-shot (there is none on a parameterised
type); the ParallelHash sample-value tests take the inherent one-shots as closures so they
stay covered. No digest, suspended-state byte or CLI output changes.
Assisted-by: Claude:claude-mythos-5-1
…and a shared crate for what the two implementations have in common Squash of fix/mldsa-drop-sha256-pairings (29 commits). From local/mldsa_review_44682f23.md, local/mldsa-lowmemory_review_44682f23.md and local/mldsa_shared_crate_plan.md. The branch was based on 4576915, before HashOneShot (0e391c4); the squash merge conflicted only on the trait imports of the two params.rs and hash_mldsa.rs files, resolved to the HashOneShot bound on PreHash with the branch's new mldsa-common imports. Both crates: - HashML-DSA-65 and -87 with SHA-256 are gone. FIPS 204 Section 5.4 requires the pre-hash to give "at least lambda bits of classical security strength against both collision and second preimage attacks" (footnote 6: a digest of at least 2 lambda bits); SHA-256's 128 bits meet lambda = 128 for ML-DSA-44 only. The two pairings had no OID, no CLI subcommand and no known-answer vector, and weaker_of capped their strength at 128 bits, which hid the gap. The 44 pairing stays and says why it is the only SHA-256 one. - do_verify_final on a context built by do_sign_init returned GenericError("No public key set on streaming verifier.") from MLDSA in the main crate and panicked on an assert in the other three places (HashMLDSA in both crates, MLDSA in lowmemory); all four return the error. - Every signing _out zeroes its output before any check; the Option handling is if-let and let-else rather than is_some/unwrap. - decompose returned Algorithm 36's (r1, r0) under the names (r0, r1); the names match the algorithm, and the "by the powers of someone much more clever than me" lines are replaced by the equivalence to the reference implementation's rounding.c (pq-crystals/dilithium): the fixed-point rounding, the wrap of the top value to 0 as steps 3-4, the final subtraction of q as step 2's mod± and step 5's -1; use_hint is Algorithm 40 lines 3-4 with mod m as the wrap at the ends. make_hint takes the reference implementation's (a0, a1), now named (low, high), with the mapping to Algorithm 39 stated. coeff_from_half_byte's constant-time TODO is closed with the argument (the three-way range split is independent of the coefficient it produces; the rejection branch is the one every rejection sampler has). - HASH_ML_DSA_65/87_WITH_SHA512_NAME are _with_, as the 44 constant and the type names. - Docs: the algorithm-name constants are documented; the headings are "Usage Examples" and "Memory Usage"; "hedged" is the fresh-rnd variant of FIPS 204 Section 3.4, not the deterministic one; the Security Considerations say what is and is not zeroized (FIPS 204 Section 3.6.3); ML-KEM leftovers ("encaps", "MLKEMPublicKey") and the first-person and "my laptop" notes go; the lowmemory crate's stale NTT and inverse-NTT comments are corrected and the Dilithium Section 5.1 shortcut note says the code applies it. - Tests: the 568-line commented-out bc-java harness port leaves each mldsa_tests.rs; the extensive framework run is an #[ignore] test with the current framework signature; the vector suites take clippy's suggestions. Both crates are clippy clean in src, tests and benches. mldsa: - sign_ph_with_expanded_key discarded the result of its _out call and returned Ok, so a 256-byte ctx gave an all-zero signature as a success; the result is propagated. - The merged keygen-and-sign path (sign_mu_deterministic_from_seed_out, 295 lines of Algorithms 6 and 7 fused, recomputing NTT(s1), A_hat * NTT(s1), NTT(s2) and t0 on every rejection pass) is keygen_internal then sign_internal, and sign_init_from_seed keeps the expanded key rather than running key generation at init and again at final. Its claimed memory and 25% speed advantages were never measured and the sizes do not support them; Sign_from_seed bench before -> after: ML-DSA-44 308 -> 323 us, -65 448 -> 444 us, -87 645 -> 679 us. The module docs say signing from a seed costs a key generation. - sigEncode's inlined HintBitPack wrote y[omega + i] inside the inner loop, 256 times per row; once per row, as Algorithm 20 line 10 reads. mldsa-lowmemory: - keygen_from_seed_and_encoded took &[u8; SK_LEN], and SK_LEN is 32 here, so it compared the seed with itself. It takes the FULL_SK_LEN Algorithm 24 encoding and compares encode_full_sk() against it in constant time. - The seed key holds tr = H(pk, 64), computed once when the key is built (176 to 240 bytes, table updated), instead of deriving the public key on every tr() call: Signer::sign, do_sign_init, HashML-DSA's mu, encode_full_sk and Display each paid a key generation. Sign bench before -> after: ML-DSA-44 920 -> 844 us, -65 1.59 -> 1.47 ms, -87 2.72 -> 2.51 ms. - verify_mu computes NTT(c) once rather than once per row. Verify bench, 1000 signatures: ML-DSA-44 84.4 -> 81.7 ms, -65 147 -> 141 ms, -87 254 -> 245 ms. The per-row unpacking of z stays, with the comment saying why (holding z_hat is l KiB). - sign_mu_deterministic_from_seed called SK::from_keymaterial twice and dropped the first result; the non-_out form delegates to the _out form. - from_bytes documents that it tags any 32 bytes as a full-strength Seed and that keygen_from_seed on typed KeyMaterial is the checked path; sk_decode's unwrap is justified at the site. - A cross-implementation test (tests/cross_check_tests.rs, bouncycastle-mldsa as a dev-dependency) pins that one seed gives the same pkEncode and skEncode bytes in both crates, the same mu and rnd the same signature bytes, and that each verifies the other's. - The crate docs name what produced the memory tables and that the peak figures predate the tr cache, and say why this crate has no CLI subcommand. mldsa-common: - A new crate, bouncycastle-mldsa-common, for what the two crates carried as identical copies with no shared source. Everything in it is #[doc(hidden)] and it is not re-exported from the umbrella crate: the FIPS 204 constants and algorithm names; MLDSAParamsBase with the Table 1 and Table 2 values, OIDs, strength, derived constants and the three marker types, pinned by the Table 1/2 tests once; Polynomial with the NTT, inverse NTT, reductions, ZETAS and norm check; the Section 7 samplers, per-coordinate bit packing and unpacking, Power2Round, Decompose, HighBits, LowBits, MakeHint and UseHint; MuBuilder, plus compute_mu_ph for the HashML-DSA M' that each crate built inline in two places. - Each crate's MLDSAParams extends the base with only what it needs (the vector and matrix types; the packed-vector buffers and the 32-byte SK_LEN) and is sealed through its supertrait. Where the crates had one operation in two shapes, common has both, the in-place or caller-buffer form as the implementation and the value form wrapping it, with the value forms #[must_use]: high_bits / high_bits_inplace, low_bits / low_bits_inplace, bitpack_gamma1 / bitpack_gamma1_out, bit_unpack_eta / bit_unpack_eta_out, multiply_ntt as method and free function. coeff_from_three_bytes and coeff_from_half_byte return Option. The shared constants and MuBuilder reach each crate's public API and docs through #[doc(inline)] pub use, so no public item of either crate changes name, path or shape. - bouncycastle-mldsa keeps matrix.rs, the vector-level aux functions, its keys, MLDSA and HashMLDSA; bouncycastle-mldsa-lowmemory keeps low_memory_helpers.rs, the row-wise signature unpacking, its seed key, MLDSA and HashMLDSA. Net over the three crates: 1,718 lines added, 3,052 removed. A shared HashMLDSA and shared key traits wait for the key-object rework. Tests: both crates' ACVP, Wycheproof, framework and key suites, the cross-check test and the workspace doctests pass; rustdoc with warnings denied is clean for all three crates. cargo mutants on crypto/mldsa-common, tested by the three crates' suites: 1807 mutants, 1768 caught, 0 missed, 30 unviable, 9 timeouts. A first pass with the ACVP and Wycheproof suites skipped left 67 missed; the --iterate pass with the full suites caught all 67. The 9 timeouts are rejection-sampler mutants that never terminate (coeff_from_three_bytes / coeff_from_half_byte returning None or with a flipped comparison, += to *= on the fill index in rej_ntt_poly and rej_bounded_poly), so they are kills by hang rather than survivors. Assisted-by: Claude:claude-mythos-5-1
…thm's constants; Hash, KDF and MAC take it as a supertrait and lose their own max_security_strength(&self) Squash of feature/173-algorithm-info (2 commits). Hash, KDF and MAC each declared fn max_security_strength(&self), the same fact as Algorithm::MAX_SECURITY_STRENGTH written out again by hand in 24 implementations (SHA-2 and SHA-3 computed it from OUTPUT_LEN / 2, SHAKE from PARAMS::SIZE, KMAC stored it in a field, HMAC read it off the hash instance), with nothing tying any of them to the constant. AlgorithmInfo carries the two Algorithm constants as &self methods, alg_name() and max_security_strength(), with a blanket impl for every Algorithm (first piece of the dyn sweep for issue #173, local/dyn_sweep_plan_173.md step 2.1: a trait object can be asked what a constant cannot be). With it the method is its blanket impl, so the three declarations and all 24 bodies go, and the three traits take AlgorithmInfo as a supertrait (MAC keeps Sized for now; it goes with the rest of the dyn sweep). Callers write the same h.max_security_strength() with AlgorithmInfo in scope, and gain alg_name() beside it. Two things fell out of the supertrait: - Hash: AlgorithmInfo + Clone replaces Hash: Algorithm + Clone. HashFactory and XOFFactory implemented Algorithm only because Hash demanded it, with ALG_NAME = "TODO" and MAX_SECURITY_STRENGTH = None and a TODO comment asking for exactly this rework; the blanket impl would have reported those placeholders. Both now implement AlgorithmInfo directly, dispatching on the variant, as do KDFFactory and MACFactoryWorker, so a factory finally reports the name and strength of the algorithm it selected. Concrete hashes still implement Algorithm; generic code that reads H::ALG_NAME through a Hash bound did so via HashAlgParams, which is unchanged, so no bound outside hmac/hkdf moved. - HKDF had no Algorithm impl and so no name. HMACParams gains HKDF_ALG_NAME beside HMAC_ALG_NAME, set from the existing HKDF_SHA256/384/512_NAME constants and new ones for the other eight hashes (HKDF-SHA224, HKDF-SHA512/224, HKDF-SHA512/256, HKDF-SHA3-*, HKDF-SM3, following the HMAC spelling), and HKDF<H: HMACParams> implements Algorithm with that name and the hash's strength. HKDF's and HmacKey/HMAC's hash bound is therefore HMACParams rather than HashOneShot (+ HashAlgParams); every hash that satisfied the old bound already implements HMACParams. HMAC's weak-key check compares against HASH::MAX_SECURITY_STRENGTH directly. KMAC's strength field, read only by the deleted method, is removed from the key, the MAC and the XOF structs; the key-strength check at construction keeps computing it. Tests: a core test asserts the blanket impl through &dyn AlgorithmInfo; the factory tests assert alg_name() and max_security_strength() for every variant of all four factories; hkdf asserts HKDF's ALG_NAME and strength; the two-view asserts in the sha3 tests collapse to one. The sha2 and sm3 hmac modules carry the new name constants. Assisted-by: Claude:claude-mythos-5-1
hubot
force-pushed
the
feature/key-rework
branch
from
November 2, 2026 13:55
711a571 to
9aa1631
Compare
…a shared crate for what the two implementations have in common, and an expanded seed key Squash of fix/mlkem-review (10 commits). From local/mlkem_review_44682f23.md and local/mlkem-lowmemory_review_44682f23.md, both of which have an "Addressed" section. mlkem: - decaps and encaps no longer clone the key through the expanded path (decaps peak stack 27,112 -> 20,920 B on massif); seed() builds the KeyMaterial type-first inside the hazardous block rather than re-tagging afterwards. - The ML-DSA residue in docs and comments is corrected against FIPS 203 (aux_functions header, matrix.rs's "Algorithm 48" for the Section 2.4.7 product with MultiplyNTTs, pkDecode is Algorithm 14, pkEncode Algorithm 13); the commented-out reference code goes; clippy clean. The crate docs' peak-stack table is re-measured with massif: KeyGen 13,960 / 20,520 / 30,152, Encaps 16,424 / 27,256 / 38,840, Decaps 20,920 / 33,560 / 47,240 B. - 565 lines of commented-out ML-DSA test code ported from bc-java and a commented-out KAT generator leave mlkem_tests.rs; the full bit-flipping framework run is an #[ignore] test. mlkem-lowmemory: - The seed key stores H(ek) (160 bytes, was 161 with the seed on disk 64), so decapsulation no longer derives the public key hash: 19.0 -> 13.5 / 32.7 -> 22.9 / 52.4 -> 36.3 ms per 256 operations across the three sets, keygen 5-8% slower for the one extra hash; seed() no longer panics on an all-zero seed; keygen_from_seed_and_encoded takes the FULL_SK_LEN encoding it compares against; tests/cross_check_tests.rs pins byte-identical keys, ciphertexts and shared secrets against bouncycastle-mlkem. - MLKEMSeedPrivateKeyExpanded holds the packed t-hat a seed key would otherwise derive inside every decapsulation (a key generation's worth of sampling), with MLKEMTrait::decaps_with_expanded_key: 928 / 1312 / 1696 bytes against the seed key's 160, decaps 13.6 -> 8.5 / 23.0 -> 13.9 / 36.4 -> 21.5 ms per 256 operations. It implements the seed key's traits, encodes to the same 64-byte seed, compares by seed and prints only H(ek). - The same ML-DSA residue and clippy pass as the main crate; the crate docs' memory tables are re-measured (KeyGen 7.7 / 7.7 / 8.8, Encaps 10.8 / 12.6 / 14.8, Decaps 11.8 / 13.7 / 15.7 kB), which makes the headline "about half the peak memory" rather than a third, and the Security Considerations say that from_bytes takes any 64 bytes as a full-strength seed. mlkem-common (new crate, bouncycastle-mlkem-common, every item #[doc(hidden)], not re-exported from the umbrella): the FIPS 203 constants, length constants, the G/H/J aliases, the names and the parameter sets as MLKEMParamsBase with the three marker types, which each crate's MLKEMParams extends with what it alone needs; Polynomial with the NTT, inverse NTT, base-case multiplication, message encoding and single-polynomial compression; the reductions and twiddle tables; ByteEncode / ByteDecode; SampleNTT and SamplePolyCBD. Where the crates had one operation in two shapes the common crate has both, the in-place form as the implementation. ByteEncode keeps cond_sub_q after the floor-variant barrett_reduce, which returns q for a negative multiple of q: the lowmemory crate's unreduced NTT outputs reach it, and the ACVP keyGen suite caught the version that had dropped it. No public item of either crate changes name, path or shape; net over the three crates the two implementations lose 1,650 lines and the common crate is 980. Tests: both crates' ACVP, Wycheproof, framework and key suites, the cross-check and the workspace doctests pass; rustdoc with warnings denied is clean for all three crates. cargo mutants with the full suites: mlkem-common 538 mutants, 510 caught, 24 missed, 3 unviable, 1 timeout (inv_ntt += to *=, a loop that never ends); mlkem 585, 407 caught, 27 missed, 151 unviable; mlkem-lowmemory 547, 395 caught, 31 missed, 121 unviable. The survivors that were not | to ^ on disjoint bit fields got tests: Decompress_d's rounding (every rounding mutant passed the ACVP decapsulation vectors because decryption tolerates a coefficient off by a little) is checked against FIPS 203 eq. (4.7) and (4.8) for d = 4, 5, 10 and 11; a decapsulation key whose first coefficient is q is refused by the range check itself; the expanded seed key's encode, from_bytes, equality and display are exercised. A scoped re-run over those functions leaves only the disjoint-bit-field mutants. Assisted-by: Claude:claude-mythos-5-1
…s and benches From local/sha2_review_44682f23.md (review of the crate at 44682f2): - `from_suspended` accepted a state whose byte count and buffer offset disagreed modulo the block length, one that describes no message, and resumed it into a digest. Both families now refuse it with `InvalidData`, as `do_update` keeps the two congruent. The hand-written state in `sha512_length_field_carries_the_high_word` sets the offset to match, and a new test feeds each family a disagreeing state. - The stale TODO about `<` and `<=` in SHA-256's `do_update` and a commented-out line in SHA-512's are gone; `block_bitlen` returns `BLOCK_LEN * 8` instead of a second copy of the number. - The two parameter traits are public and sealed (`SHA256InitValue`, `SHA512InitValue`: `sealed::Sealed`), which lets the crate-wide `allow(private_bounds)` go. - Docs: the `Usage Examples` heading, the message-length sentence (2^61 - 1 bytes for SHA-224/256, the standard's limit, and 2^64 - 1 bytes for the SHA-512 family), the memory table row, the HMAC module's citations and typos, and the nine empty `///` on the `*_NAME` constants. The review's `HMAC<SHA512t<8>>` panic no longer exists as such: since the `HMACParams` bound, HMAC is instantiable only over the two approved truncations, which the `SHA512tParams` docs now say, pinned by a `compile_fail,E0277` doctest. - Benches: the hash bench covers all six members; the HMAC bench labels lose a wrong `::hashes()`; the HKDF key-fed extract benches say what they measure. A `bench_sha2_mem_usage` stack harness joins the SHA-3 one and reproduces the documented 112/208, 108/204 and 224/416. `max_security_strength()` had already gone with `AlgorithmInfo`. Assisted-by: Claude:claude-mythos-5-1
… ParallelHash block size, FIPS 202 citations, sealed parameter traits, SP 800-185 benches From local/sha3_review_44682f23.md (review of the crate at 44682f2): - KMAC accepted any output length, including zero. SP 800-185 Sec 8.4.2 says applications "shall not select an output length L that is less than 32 bits", so `new_with_params` refuses anything under the new `KMAC_MIN_OUTPUT_LEN` (4 bytes) with `InvalidLength`, and a suspended state naming a shorter length is `InvalidData`. The keyed cSHAKE both constructors start from is one private function, so KMACXOF no longer gets its sponge by asking KMAC for a zero-length instance. `do_final_out` zeroizes the buffer before the length check, as HMAC does, and `mac_out` no longer zero-fills a second time. - ParallelHash's block size is a `NonZeroUsize`: Sec 6.2's `0 < B` is on the type, the constructor's assert and the panic test go, resume refuses a zero through the same type, and the CLI's check produces the value. - `keccak.rs` cites FIPS 202 at each step: the round constants (Algorithm 6 steps 2-3 via Algorithm 5), the lane mapping (Sec 3.1.2, lane (x, y) at `buf[x + 5y]`), theta, the fused rho/pi, chi and iota (Algorithms 1 to 6, with the register offsets the unrolling introduces), and the permutation as KECCAK-p[1600, 24]. `KeccakSize::_288` is gone and `KeccakSize::rate_bits()` replaces the eight copies of `1600 - 2c`. - `SHA3Params` and `SHAKEParams` are public and sealed, which lets the crate-wide `allow(private_bounds)` go; `KeccakSize` is re-exported with them. `CSHAKESqueezer`'s constant and tag are named after the type (`SUSPENDED_CSHAKE_SQUEEZER_STATE_LEN`, `CSHAKE_SQUEEZER_STATE_TAG`), as are the comments that still said `LengthBoundSqueezer`. - SHAKE: `mix_key_internal` cannot fail, so it returns nothing; the derive-key truncation uses the nominal length instead of a match with an `unreachable!` arm; the "citation needed" thresholds in SHAKE's and SHA-3's KDF paths are stated as this library's own rule; the struct docs say that SHAKE does implement `Hash` and why FIPS 202 Sec 7 does not call it one. A no-op `set_key_len` and two stale TODOs in `sha3.rs` are gone. - Docs: the crate's cSHAKE paragraph presents it as a usable XOF whose `N` is NIST's (Sec 3.2), the ParallelHash module no longer opens with the Recommendation's Sec 6.1 pasted in, the TupleHash example names the right function, and the four `HMAC_SHA3_*_NAME` constants have one-line docs. - The SP 800-185 functions had no criterion bench; cSHAKE128, KMAC128, TupleHash128 and ParallelHash128 each get one over the 16 KiB input the SHA-3 benches use. The clippy warnings in `src/` (`right_encode`'s "unused until" note and `dead_code` allow, two index loops, a `mem::replace` that is `mem::take`, a test module before an impl) and in the test files are fixed; the crate's `--all-targets` clippy run is clean. Assisted-by: Claude:claude-mythos-5-1
…its are dyn-compatible Step 1 of the dyn sweep for issue #173 (local/dyn_sweep_plan_173.md, Sec 2.2 to 2.4), after `AlgorithmInfo` and the Hash/KDF/MAC half of it landed in 711a571. On this branch the worker traits still carry their constructors and one-shots, so the fence list is longer than the plan's. - `SymmetricCipherEncryptor`, `SymmetricCipherDecryptor`, `BlockCipherEncryptor`, `BlockCipherDecryptor` and `hazmat::KeyStream` are bounded on `AlgorithmInfo` instead of `Algorithm + Sized`, and `MAC` loses its `Sized`. The AEAD and stream traits inherit the change. - Every method a trait object cannot carry gets `where Self: Sized`: the receiver-less constructors (`do_*_init`, `do_*_init_rng`, `do_encrypt_init_nonce`, `KeyStream::new`, `MAC::new`, `MAC::new_allow_weak_key`), the receiver-less one-shots (`encrypt*`, `decrypt*`, `encrypt_detached*`, `decrypt_detached*`, `encrypt_with_aad*`, `decrypt_with_aad*`, `*_inplace`), the four `*_out_len` functions, and the four provided `final` conveniences that take `self` by value (`do_*_final_out`, `do_*_final_detached`). What remains dispatchable is the streaming core: `do_update_aad`, `do_*_out_len(&self, ..)`, `do_*_out`, the required by-value finals, `alg_name()` and `max_security_strength()`. - `PaddedBlockCipherEncryptor`/`Decryptor` and `StreamCipher` forward their inner worker's `ALG_NAME` and `MAX_SECURITY_STRENGTH`, so their impls bound the inner type on `Algorithm` as well; the test framework's key-strength checks read the constant the same way and take the same bound. Every mode and keystream in the workspace satisfies it, and the static names on the AES aliases are unchanged. - `crypto/core/tests/dyn_compatible_tests.rs` takes each worker trait as `&mut dyn` and compiles; `crypto/aes/tests/dyn_worker_tests.rs` streams AES-CBC-PKCS7 and AES-GCM through `&mut dyn SymmetricCipherEncryptor` / `&mut dyn AEADCipherEncryptor` (AAD included), reads `alg_name()` and `max_security_strength()` through the object, finishes on the concrete value and decrypts with the one-shot. `BlockPermutation` keeps `Algorithm + Sized`: it is the primitive the modes are generic over, not a worker, and its constants are read through that bound. Assisted-by: Claude:claude-mythos-5-1
…nto the types `Hash: AlgorithmInfo + Clone` became `Hash: AlgorithmInfo`. `Clone` implies `Sized`, so the supertrait was the one thing keeping `Hash`, and `XOF` with it, from being a trait object; every hash type in the library derives `Clone` and goes on doing so, and forking a computation mid-stream works exactly as before. The code that forks a hash says so where it needs it: `HMACParams` is `HashOneShot + HashAlgParams + Clone`, and the test framework's `test_hash` and `test_xof`, which check that a clone and its original finish alike and diverge once fed different input, are bounded on `+ Clone`. No other generic code cloned a hash. The trait docs' "Forking is part of this trait" section becomes "Forking is on the types, not the trait", and QUALITY_AND_STYLE's Architecture section states the rule: `Clone` on the types, not on the `core` traits, and for any trait meant to be held as `dyn` no `Clone`, `Default` or `Sized` supertrait, no associated constant, and `where Self: Sized` on receiver-less or provided by-value methods. `crypto/core/tests/dyn_compatible_tests.rs` now also takes `&mut dyn Hash` and `&mut dyn XOF<Squeezer = S>`. A `dyn Hash` can be fed and queried but not finished, since the finals take `self` by value; that is the same gap the planned boxed finals close for the cipher workers. Assisted-by: Claude:claude-mythos-5-1
Mutation runs on the ML-DSA crates were taking a minute or more per mutant: under `dev` the low-memory suite runs for 160 s because unoptimised signing re-expands everything per step, and under `release` each mutant pays a full rebuild plus the thin-LTO link of six test binaries, about 80 s. `[profile.mutants]` inherits `release`, turns LTO off and incremental codegen on (16 codegen units); `.cargo/mutants.toml` selects it. Measured on `bouncycastle-mldsa-lowmemory` after a real one-line change: 0.8 s rebuild, 3.8 s test run, so a mutant costs about 5 s instead of 60 to 100. CLAUDE.md's mutation-testing notes say so. Assisted-by: Claude:claude-mythos-5-1
Closed
… cli: signing and verifying move onto the keys The signer/verifier init-removal plan (local/signer_verifier_init_removal_plan.md), decisions D1 to D7, squashed from feature/signature-keys. The cipher and MAC traits already had construction on the key objects; the signature traits now do too. core - `signature_key`: the role traits `SigningKey<A>`, `DeterministicSigningKey<A>`, `VerifyingKey<A>` and their pre-hash forms, generic over the pre-hash `H` at the method (`create_ph_signer::<SHA512>`), with the worker as a GAT borrowing the key (`type Signer<'k>`) and the RNG drawn at `create_signer(rng, ctx)`. The algorithm is the trait parameter, so one key encoding serves ML-DSA and HashML-DSA. - `Signer` becomes `SignatureSigner` and, with `SignatureVerifier`, keeps only the streaming half; the workers' methods take the MAC worker's names (`do_update`, `do_final`, `do_verify_final`). The static `do_sign_init` / `do_verify_init` constructors and the static one-shots are gone. - `core-test-framework`: the signature suite runs over the key role traits, with deterministic and pre-hash sub-suites. rng - `FixedSeedRNG` moves from the test framework to `bouncycastle-rng`, generic over what holds its bytes (`FixedSeedRNG<B: AsRef<[u8]>>`, `new(bytes)`), still cycling. KATs feed `rnd` through it and the ordinary `create_signer`; the `_rnd` methods are deleted. mldsa, mldsa-lowmemory, mldsa-common - `MLDSA<P>` and `HashMLDSA*_with_*` are algorithm markers; the keys do the work. The key types carry only their parameter set (`MLDSAPublicKey<P>`); the encoded lengths are reached through sealed per-length traits (`PublicKeyLen`, `PrivateKeyLen`, `SignatureLen`, `FullPrivateKeyLen`), one impl per parameter set, so `sk.sign(&mut rng, msg, ctx)` needs no annotation; `P::PkEncoded` / `P::SkEncoded` supply whole-encoding buffers where no const is in scope. - The external-mu API is on the key traits: `pk.create_mu_builder(ctx)`, `pk.verify_mu(&mu, &sig)`, `sk.sign_mu(&mu, &mut rng)`, `sk.sign_mu_deterministic(&mu)`; `MuBuilder::from_tr` replaces `do_init` and `compute_mu_ph` is a free function in mldsa-common. The deterministic variant (rnd = 0) is reachable only through core's `DeterministicSigningKey`. - The expanded keys borrow the key they expand (`MLDSAPublicKeyExpanded<'k, P>`, `key()`), so they hold the matrix and a reference rather than a second copy of the key: 16392 / 30728 / 57352 bytes for both directions, down from 20512 / 36896 / 65568 (public) and 28848 / 48304 / 81072 (private). `A_hat`, `pk_decode` and `sk_decode` leave the public key traits. - The streaming pre-hash verifier is tested on a bad signature, message and context; the low-memory seed-key docs no longer promise a merged decode-and-sign. cli, mem_usage_benches - The ML-DSA subcommands and the stack harness use the key API. docs - QUALITY_AND_STYLE and CLAUDE.md record the lessons (lengths the parameter set fixes do not go on the key type; algorithm-level operations go on the key trait; same-named methods from two traits are ambiguous; a cycling test RNG defeats a "two signatures differ" check); release notes updated. Mutation testing (apollo, mutants profile): bouncycastle-mldsa 612 mutants, 265 caught, 4 missed, 7 timeouts, 336 unviable; bouncycastle-mldsa-lowmemory 624 mutants, 279 caught, 6 missed, 2 timeouts, 337 unviable. The unviable ones are `Ok(Default::default())` key constructions that do not compile. The misses are the `kappa > 1000 * k` iteration cap in `sign_internal`, unreachable with a valid key, and the `&` to `|` mutations of `MLDSASeedPrivateKey::eq`, whose fields all derive from the seed; the timeouts are `kappa +=` mutations that spin the rejection loop, which are kills. Assisted-by: Claude:claude-mythos-5-1
… caller's RNG `MLDSATrait::keygen()` and `MLKEMTrait::keygen()` built an OS-seeded `HashDRBG_SHA512` inside the call and delegated to `keygen_from_rng`; the no-argument form goes and `keygen(rng: &mut dyn RNG)` is the one key-generation entry point besides the seed forms, as the ECDSA and SM2 crates and everything else that draws randomness now have. The strength check on the RNG is unchanged. The CLI's `keygen` actions pass `HashDRBG_SHA512::new_from_os()`; tests, benches, the stack harnesses and the doc examples pass an RNG, and the shared suites' `fn() -> ..` keygen parameter is met with a closure. ML-KEM's `encaps()` convenience, which also instantiates the default DRBG, is left for its own change. Assisted-by: Claude:claude-mythos-5-1
…han one draw The randomized signature suite seeds its FixedSeedRNG with a 64-byte counting pattern. FixedSeedRNG emits its seed cyclically, so an algorithm that draws 64 bytes per signature, RSASSA-PSS with SHA-512's 64-byte salt, gets the same bytes twice and the "two signatures of one message must differ" check fails when the RSA crate is run through the suite. The seed is now 256 bytes, longer than any one draw, and the lengths helper takes `&mut dyn RNG` rather than naming the seed array's size. CLAUDE.md's note on FixedSeedRNG says the pattern must outlast a draw. Assisted-by: Claude:claude-mythos-5-1
dghgit
added a commit
that referenced
this pull request
Nov 3, 2026
…work: mode-bound symmetric keys (AESKey<P, M>, HmacKey<H>) producing their workers through role traits, GCM/CCM/CMAC/GMAC as mode types over a key state, FromKeyMaterial and the AEAD, packet and MAC role pairs in core; the signing and verifying key traits (SigningKey, DeterministicSigningKey, VerifyingKey and the pre-hash forms) with ML-DSA and HashML-DSA moved onto them; the ML-DSA and ML-KEM review rounds with the shared mldsa-common and mlkem-common crates and the expanded ML-KEM seed key; the SHA-2 and SHA-3 review rounds (KMAC output-length floor, ParallelHash block size, FIPS 202 citations, sealed parameter traits, suspended-state checks); HKDF review fixes; HashOneShot and AlgorithmInfo, Clone off the Hash trait, dyn-compatible cipher and MAC worker traits; keygen taking the caller's RNG; FixedSeedRNG in bouncycastle-rng; the mutants cargo profile. Created by David Hook. Assisted-by: Claude:claude-mythos-5-1
Contributor
Author
|
Merged by hand as squash commit. |
dghgit
added a commit
that referenced
this pull request
Nov 3, 2026
…f the key object rework: the symmetric cipher and MAC traits lose their constructors and key-taking one-shots (do_encrypt_init, do_decrypt_init, the worker one-shots), construction is a key's create_* and the one-shots are provided on the role traits; every mode gets a key path (CBC, CFB, CFB8, CTR, ECB, CCM, CCM_Packet, GCM, GMAC, CMAC as mode types), with AsconAEAD128Key and KMAC128Key/KMAC256Key beside AESKey and HmacKey, and the MAC factory as a key enum handing out a worker enum; workers borrow the key through a GAT Encryptor<'k>, so creating a worker copies no schedule; BlockPermutation splits from ElectronicCodeBook; resume takes the key object; keys compare in constant time; the CLI, the test framework's key runners and the memory benches on keys; the fixes from the 2026-10-30 and 2026-10-31 reviews of PRs #170 and #172. Created by David Hook. Assisted-by: Claude:claude-mythos-5-1
dghgit
added a commit
that referenced
this pull request
Nov 3, 2026
The notes left out the symmetric key-object rework (PRs #170 and #172), the SP 800-185 functions, AES-CMAC and AES-GMAC, the generic HMAC/HKDF with their new aliases (#122), the ML-DSA-87 reduction fix (#137), the removed ML-DSA-65/87 SHA-256 pairings, the sealed parameter traits (#117), keygen taking the caller's RNG, the non_exhaustive errors (#81), the unsigned Condition (#63), the CLI, the contributor docs and CI, and the people and issues behind them. The signature-keys entry was a 25-line method listing. Rewritten with one entry per change, grouped as the 0.1.2 notes are, with issue numbers and thanks, and the ML-KEM low-memory decaps line fixed. Assisted-by: Claude:claude-fable-5-1
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.
Issue Link
No issue filed yet. This is the prototype behind the key object rework document circulated for review (
AES128Key<GCM>, "Death to init()"), opened so the design can be discussed against working code alongside #167.Summary
Prototype of mode-bound symmetric key objects:
AESKey<P, M>andHmacKey<H>are built once fromKeyMaterialand produce their encryptors, decryptors, generators and verifiers through role traits incore.Description
coregainsFromKeyMaterial<KEY_LEN>/FromAnyKeyMaterial, the AEAD and MAC role pairs (AeadEncryptionKey/AeadDecryptionKey, the packet pair,MacGenerationKey/MacVerificationKey) and the hazmatMode<P>traits a mode implements over a permutation.ciphergets GCM, CCM, CMAC and GMAC as mode types over a key state.aesgets oneAESKey<P, M>withAES128Key<M>/AES192Key<M>/AES256Key<M>aliases, the forwarding role impls, and a check at construction that the material'sKeyTypeis the one the mode requires (SymmetricCipherKeyfor the cipher modes,MACKeyfor CMAC and GMAC), ahead of the permutation's own length and strength checks. There is no algorithm-name binding:KeyMaterialcarries no such field, so oneKeyMaterial<16>keysAES128Key<GCM>,AES128Key<GCM<12>>andAES128Key<CCM<N, T>>alike, and oneMACKeyboth<CMAC>and<GMAC>.hmacgetsHmacKey<H>holding the two pad states, andhkdfoneHmacKeyper PRK. Theaes*-ccmCLI commands and the CCM memory bench run through the key path.KeyMaterialwith the constructors kept on the traits (the shape of SymmetricCipherKey: dedicated key types per algorithm #167). Each is discussed in the document.do_*_initconstructors, which stay in place so both routes coexist on this branch.crypto/aes/tests, CCM and GCM one-shot and packet tests incipher, HMAC key tests covering keys shorter than, equal to and longer than the block, and the key-type refusals. Every commit builds and passescargo test --workspace; 1187 tests at the tip.Scope and Risk
core,cipher,aes,hmac,hkdf,sha2,sha3,sm3,core-test-framework,cli,mem_usage_benches. 59 files, +7333 / -1630.HMAC<H>is two hash states.quality_stats.shagainst the base, unwraps in core code 805 → 818 andErr()458 → 481: the new fallible one-shots and mode types incipherandcore.Validation
Up to date with
feature/simple-ciphers: its tip (the CCMdo_*_inplacerename, 7084adb) is merged in as 69e24ba, with the rename carried onto the reworked packet type here, and 1185 tests passing on the result.AI Usage Statement
Did you use AI in creating this pull request:
Assisted-by: Claude:claude-mythos-5-1