Repository navigation
Conversation
hubot
force-pushed
the
feature/kem-rework
branch
from
November 7, 2026 08:37
9b72a72 to
cf178ae
Compare
hubot
force-pushed
the
feature/kem-rework
branch
from
November 7, 2026 13:31
4bf8c53 to
cf178ae
Compare
hubot
force-pushed
the
feature/kem-rework
branch
from
November 7, 2026 19:39
e859b14 to
cf178ae
Compare
The key-agreement traits of the ECC CDH plan (local/eccdh_plan.md, Phases 3 and 4), in the shape the KEM traits took on feature/kem-rework. core - `agreement_key`: the role trait `AgreementKey<Params, SS_LEN>`, implemented once per algorithm on a private key over the parameters a worker is made from beyond the key, `()` for the raw primitive and the caller-configured KDF for an algorithm that derives its output, so the parameter type selects the impl at the call and both parties name the same one; each impl names its algorithm as `type Algorithm: KeyAgreementAlgorithm` and its peer key as `type PublicKey`, and produces the worker as a GAT borrowing the key through `create_agreement(&'k self, params)`, with the one-shot `agree(params, &peer_pk)` provided over it. Two parties only; MQV and the multi-key schemes of SP 800-56Ar3 Section 6 are not expressed. - `KeyAgreement<PK, SS_LEN>` describes the worker: `AlgorithmInfo` supertrait, `agree(&self, &PK)` returning `Z` as `KeyMaterial<SS_LEN>` typed `KeyType::SharedSecret`, for a KDF and nothing else (Section 5.8); reusable against many peers; dyn-compatible. `KeyAgreementPrivateKey` / `KeyAgreementPublicKey` are the encodable key traits, on the KEM ones' shape, with the public key's decoder carrying the algorithm's validation. - `KeyAgreementError`: `AgreementFailed` for a primitive that produces no secret (Section 5.7.1.2 step 2, the point at infinity), with the usual decoding, encoding, key-generation, key-material, length and RNG variants and the `From` impls. core-test-framework - The key-agreement suite: both parties compute the same `Z`, typed and at the algorithm's strength; the worker gives the one-shot's value, carries the metadata, is reusable and works as a trait object; a third party gives a different secret. A keys suite round-trips the encodings and refuses the wrong lengths. An in-file mock Diffie-Hellman over a 61-bit prime, made from a borrowed parameter, exercises the suite and the parameter plumbing. - `dyn_compatible_tests` pins the worker trait. Mutants on core, scoped to agreement_key.rs and errors.rs with the core and framework tests: 22 (2 caught, 20 unviable). Assisted-by: Claude:claude-mythos-5-1
`examples/p256_key_agreement.rs`: ECC CDH over P-256 (SP 800-56A Rev. 3 Section 5.7.1.2) on `core::agreement_key`, built from the crate's hazmat arithmetic as the `ecdh` crate will be: key types on `KeyAgreementPrivateKey` / `KeyAgreementPublicKey` with validation in the SEC 1 decoder, FIPS 186-5 Appendix A.2.1 key generation, the role implemented over `()` with a worker borrowing `d` that computes `[d]Q`, rejects the identity, takes the x-coordinate as `Z` typed `SharedSecret` at 128 bits and scrubs the intermediates. Checked against RFC 5903 Section 8.1 from both sides, then on a fresh pair, with an off-curve peer key refused at decode. The same keys implement the role a second time over an HKDF salt and info, naming `ECCDH_P256_HKDF_SHA256`, whose worker runs the raw primitive and then HKDF-SHA256 over `Z` (the two-step key derivation of SP 800-56C, with `Z` never leaving the call), so that `sk.agree(params, &pk)` and `sk.agree((), &pk)` are two algorithms on one key selected by the parameter type. `core`, `rng` and `sha2` become dev-dependencies for it. Assisted-by: Claude:claude-mythos-5-1
… first The provided one-shot is `sk.agree(&peer_pk, params)`, the peer's key before the algorithm's parameters, as `pk.encaps(rng, params)` puts the parameters last; the framework and the P-256 example follow. No behaviour changes. Assisted-by: Claude:claude-mythos-5-1
…cycastle_hkdf::HkdfParams ECCDH_P256_HKDF_SHA256 in examples/p256_key_agreement.rs is now the role impl over `HkdfParams<'p, SHA256>`, the salt and info the ML-KEM-with-HKDF roles take, in place of the example's own salt-and-info struct: `HkdfParams::<SHA256>::new(&salt, info)` selects the algorithm on the same keys by the hash on its type, the worker reads the salt and info through the accessors, and a salt typed Unknown is refused at construction with the key-material error the ML-KEM roles give. The marker stays in the example until the ecdh crate of the plan's Phase 4 exists to hold it. bouncycastle-hkdf joins the crate's dev-dependencies for it. Assisted-by: Claude:claude-mythos-5-1
…type `test_key_agreement` pinned the secret's type to `SharedSecret`, which is the raw primitive's and not what a KDF-bearing algorithm over the same role returns (a derived key typed `CryptographicRandom`). It now checks what the KEM suite checks, typed full-entropy material at the algorithm's strength and never `Unknown`, and leaves the exact type to the implementor's own tests, so that an ECC CDH key under `HkdfParams` runs through the suite as the ML-KEM keys do. Assisted-by: Claude:claude-mythos-5-1
… P-521, secp256k1 and the three brainpool curves The new crate bouncycastle-eccdh holds the ECC CDH primitive on the key-agreement roles of core::agreement_key, built on bouncycastle-ec's arithmetic, SEC 1 codecs and constant-time multipliers: per curve a key pair of its own (ECCDHP256PrivateKey / ECCDHP256PublicKey and the six siblings, distinct from the ECDSA types because Section 5.6.3.2 item 5 does not let one pair serve both purposes), the marker (ECCDHP256, ECCDHP384, ECCDHP521, ECCDHSecp256K1, ECCDHBp256r1, ECCDHBp384r1, ECCDHBp512r1, named "ECC-CDH-P256" and so on), the worker a private key hands out over `()`, and keygen by extra random bits (Section 5.6.1.2.1, L = len(n) + 64 bits, the RBG checked against the curve's strength). A private key loads only in [1, n-1] (Section 5.6.2.1.2); a public key is fully validated at decode (Section 5.6.2.3.3 steps 1 to 3 through the ec decoders) with step 4 argued rather than computed, since every curve here has cofactor 1 (SP 800-186 Section 3.2.1, SEC 2 v2 Section 2.4.1, RFC 5639 Sections 3.4, 3.6 and 3.7), so a point on the curve other than the identity has order n. `agree` follows Section 5.7.1.2 line by line: [d]Q in constant time, the identity test kept on a condition no validated input reaches and acted on after the intermediates are zeroized, x_P through a branch-free affine conversion and Appendix C.2's conversion, and Z returned as KeyMaterial typed SharedSecret at the curve's strength (an all-zero Z comes back Zeroized, not an error). The P-256 keys implement the role a second time over `HkdfParams<'p, SHA256>` as ECCDHP256_HKDF_SHA256, Z through HKDF-SHA256 with the caller's salt and info, the shape the ML-KEM HKDF pairings have; the other curves return Z for the caller's KDF. `keys_common`'s DerivePublicKey recomputes Q = [d]G for the CLI. The umbrella crate re-exports it as `bouncycastle::eccdh`, and the 0.1.4 release notes gain the entry. Tests: the core framework's agreement and key suites per curve (and over HkdfParams on P-256), derive_pk against keygen, the [1, n-1] boundary (0, n, n + 1, all ones refused; n - 1 loads), an off-curve point refused at decode, compressed encodings, a weak RNG refused and one at exactly the curve's strength accepted, keygen's determinism given the DRBG output, Z feeding HKDF to a CryptographicRandom PRK, and the HKDF variant checked against HKDF run by hand over the raw Z. Known answers: the NIST CAVP ECC CDH primitive vectors (CAVS 14.1, 25 records each for P-256, P-384 and P-521, from KAS_ECC_CDH_PrimitiveTest.txt, which bc-test-data needs to gain under crypto/cavp; the suite fails rather than skips without it), RFC 7027 Appendix A.1 to A.3 for the brainpool curves from both sides, and Wycheproof's ecdh_*_test.json for all seven curves plus the ecpoint sets for the three NIST curves (10 files, 7667 vectors: 5608 valid, 1639 acceptable, 420 invalid, with the SPKI handled by a strict DER walk in the test and a mismatched AlgorithmIdentifier treated as a key rejected at decode). Benches: keygen and agree per curve. Not here: the Section 6 schemes and key confirmation, ECC MQV, SM2 key exchange, and the CLI and stack-usage harness, which follow in their own commits. Mutants per curve on its keys_*.rs and eccdh_*.rs, each run with the curve's own test binaries: P-256 55 (22 caught, 32 unviable, 1 missed), P-384 45 (19/25/1), P-521 45 (20/25/0), secp256k1 45 (19/25/1), brainpoolP256r1 45 (19/25/1), brainpoolP384r1 45 (19/25/1), brainpoolP512r1 45 (19/25/1). The miss on five curves was keygen's `<` on the RNG strength becoming `<=`, which the exact-strength test added here now kills; brainpoolP512r1's is `agree`'s guard around setting Z's strength, equivalent there because the curve's 256 bits is also the length-derived default of a 64-byte value. Assisted-by: Claude:claude-mythos-5-1
…512r1 One generic `eccdh_cmd` over the key traits and the `AgreementKey<()>` role, instantiated per curve as `ecdsa_cmd` is, with the actions keygen, pk-from-sk, check-consistency (SP 800-56A Rev. 3 Section 5.6.2.1.4 method (b), recompute dG) and agree, which takes the own private key from stdin or --skfile and the peer's public key from --pkfile and writes the raw shared secret Z, keying material for a KDF; the help says so and points at `hkdf-* --ikm`. Checked by hand and in cli/tests/test_eccdh.sh against the CAVP ECC CDH primitive vectors (P-256 and P-521 COUNT 0: pk-from-sk gives QIUT, check-consistency passes, agree gives ZIUT, through files, stdin and --hex-input), with round trips on all seven curves (lengths, both sides agree, a third pair differs, a mismatched pair refused), an off-curve peer key and d = 0 refused, a P-384 key refused by the P-256 command, and Z piped into hkdf-sha256 on both sides. Assisted-by: Claude:claude-mythos-5-1
`bench_eccdh_mem_usage` prints the fourteen key types' in-memory and on-disk sizes and runs one keygen or agree per curve under a non-inlined `measure` wrapper, with fixed keys for agree so keygen's cost is not folded in. The crate docs' table is the deepest frame chain below that wrapper, measured the way the cipher crates' tables are (massif's peak sits at the process's start-up baseline for these curves and says nothing): a release build with `-C remark=prologepilog -C remark=stack-frame-layout` for the frame sizes, the call graph from `objdump -d` with the GOT-indirect calls resolved through `readelf -rW` and `nm`, and a return address per hop over the `bouncycastle_*` symbols; rustc 1.97.0-nightly (d3cd04068), x86-64. agree: P-256 4,216, P-384 5,736, P-521 8,520, secp256k1 4,056, brainpoolP256r1 4,312, brainpoolP384r1 5,712, brainpoolP512r1 7,600 bytes, the variable-base multiplier's per-call table of sixteen Jacobian points being most of it; keygen: 2,600 / 2,232 / 5,352 / 2,424 / 2,712 / 2,528 / 4,672, excluding the caller's RNG, reached through `dyn RNG`. Assisted-by: Claude:claude-mythos-5-1
… types `crypto/eccdh/examples/p256_key_agreement.rs` is the short walk-through of the API: two parties' keys, `agree` from both sides, a static key's worker, and the same keys under `HkdfParams<SHA256>` as ECC-CDH-P256-HKDF-SHA256. The ec crate's example, the sketch of the roles over its hazmat arithmetic that preceded the crate, goes, with the core, hkdf, rng and sha2 dev-dependencies it alone needed. Assisted-by: Claude:claude-mythos-5-1
…A-256, SHA-384 and SHA-512 The new crate bouncycastle-x963kdf holds `X963KDF<H>`, SEC 1 v2 Section 3.6.1 line by line: K_i = Hash(Z || Counter || [SharedInfo]) with the 4-octet big-endian counter from 1, K the leftmost keydatalen octets, keydatalen checked below hashlen * (2^32 - 1) (step 2) and against the buffer, the buffer zeroized first, one output block held in a Secret and each block hashed by streaming Z, the counter and SharedInfo rather than concatenating them; the entropy accounting is HKDF's, the output CryptographicRandom once at least half a hash output of typed full-entropy material has gone in, at the least of Z's, the hash's and the length's strengths, else Unknown. `derive_out` is the function; the `KDF` trait runs it at one hash output (`derive_key`) or the buffer's capacity (`_out`), with SharedInfo as the additional input and the multi-key methods concatenating the keys as Z. The hash is bound by `X963KDFParams`, HashOneShot + HashAlgParams plus the instantiation's name, implemented for the four SHA-2 hashes of SEC 1 Section 3.5 with the aliases X963KDF_SHA224 to X963KDF_SHA512 named "X963KDF-SHA256" and so on. `X963KdfParams<'p, H>` carries the SharedInfo with the hash as the type's parameter, the `Params` a key-agreement role impl over the KDF is made from, as HkdfParams is for HKDF. The umbrella crate re-exports it as `bouncycastle::x963kdf`; the 0.1.4 release notes carry the entry. Tests: the core KDF framework's single- and multi-key suites on a CAVP vector; the counter and truncation against the hash of Z || Counter || SharedInfo computed by hand at lengths across block boundaries; both length bounds refused with the buffer zeroized; the entropy accounting (the strength caps by Z, by the hash and by the length, the half-output seeding threshold at 31/32 bytes for SHA-512, Unknown input credited nothing); SharedInfo selecting the key; names, strengths and output lengths; and the NIST CAVP "ANS X9.63-2001" component vectors, all 80 SHA-224, SHA-256, SHA-384 and SHA-512 records of ansx963_2001.rsp (SHA-1 skipped), which bc-test-data needs to gain under crypto/cavp; the suite fails rather than skips without it. A criterion bench at 32, 1024 and 64 bytes. Assisted-by: Claude:claude-mythos-5-1
The three instantiations join the KDF factory's enum, its `new` by name and every pass-through arm, beside the HKDF and SHA-3 ones; SHA-224 is left out as HKDF-SHA224 is. Assisted-by: Claude:claude-mythos-5-1
Keying data of `--len` bytes from a shared secret (`--ikm`/`--ikm-file`) and the optional SharedInfo (`--additional-input`/`--additional-input-file`), the hkdf-* commands' inputs minus the salt, with the same 16320-byte bound on the output. cli/tests/test_x963kdf.sh checks the CAVP ANS X9.63-2001 records (SHA-256 with and without SharedInfo, SHA-384, SHA-512, through hex and file inputs and binary output), the bound, and SEC 1 Section 6.1's ECDH scheme by hand: eccdh-p256's Z into x963kdf-sha256 on both sides. Assisted-by: Claude:claude-mythos-5-1
…X9.63 KDF over X963KdfParams Each curve's keys implement the agreement role a further time over `X963KdfParams<'p, H>`, the hash matched to the curve's strength (SHA-256 for P-256, secp256k1 and brainpoolP256r1, SHA-384 for P-384 and brainpoolP384r1, SHA-512 for P-521 and brainpoolP512r1), as the markers ECCDHP256_X963KDF_SHA256, ECCDHP384_X963KDF_SHA384, ECCDHP521_X963KDF_SHA512, ECCDHSecp256K1_X963KDF_SHA256, ECCDHBp256r1_X963KDF_SHA256, ECCDHBp384r1_X963KDF_SHA384 and ECCDHBp512r1_X963KDF_SHA512, named "ECC-CDH-P256-X963KDF-SHA256" and so on at the curve's strength: Z from the primitive, then KDF(Z, SharedInfo) with keydatalen one hash output, so `sk.agree(&peer_pk, X963KdfParams::<SHA256>::new(shared_info))` on a P-256 key is the ECDH scheme's keying data with no annotation, and the hash the curve is not paired with is a compile error. The worker holds the key and the SharedInfo; construction cannot fail, the KDF having no parameter it refuses. Tests per curve: the framework's agreement suite over the params, the derived key against X963KDF::derive_out over the raw Z, its length, type and strength, and the SharedInfo (including none) selecting the key. The crate docs' overview and status say which variants each curve carries. Assisted-by: Claude:claude-mythos-5-1
…'s bound Just below hashlen * (2^32 - 1) the step 2 check passes and the buffer check refuses the length; the test matched either error, so the bound's `*` becoming `+` survived mutation (x963kdf after the KDF reshaping: 36 mutants, 21 caught, 13 unviable, 2 missed, this and `Clone` for the stateless type replaced by `Default`, which is equivalent). It now matches the buffer check's message. Assisted-by: Claude:claude-mythos-5-1
Assisted-by: Claude:claude-mythos-5-1
…rivation is refused The accounting follows the `KDF` trait's rule after the PR #192 review: a full-entropy key whose strength is `None` (a `SharedSecret` whose producer stated nothing, which is now the default for one built from bytes) credits no bytes, so the output is `Unknown` rather than `CryptographicRandom` with no strength, which the framework's suite now checks; and `keydatalen = 0` is refused as every KDF refuses it. Assisted-by: Claude:claude-mythos-5-1
A `SharedSecret` built from bytes has no strength until its producer states one, so the example says so and sets it, as a key agreement does. Assisted-by: Claude:claude-mythos-5-1
…s the compressed point to The runner let any `acceptable` vector pass when nothing agreed, and printed the agreed count without asserting it, so a decoder that rejected every compressed point, or a DER walk that masked a vector, went unnoticed; 1,557 of the 1,609 acceptable vectors are `InvalidAsn` cases the strict walk disposes of before the library sees them. Each test now passes the number of vectors that must reach the library and agree (the valid ones and the one `CompressedPoint` acceptable vector per file: 331, 772, 633, 474, 518, 642 and 512), and an acceptable vector flagged `CompressedPoint` that is rejected fails the run. Found by the PR #193 review (H6). Assisted-by: Claude:claude-mythos-5-1
`X963KDF-SHA256`, `-SHA384` and `-SHA512` joined `KDFFactory` without a test, so routing one name to another instantiation, or dropping a name, survived the suite. Each is now checked for its name and strength and against the first record of its section in NIST CAVP `ansx963_2001.rsp` (192-bit shared secret, no SharedInfo, 128 bits of key data), through both `_out` forms. Found by the PR #193 review (H7). Assisted-by: Claude:claude-mythos-5-1
The `eccdh-p256` help and the module comment told the user to pass Z as `hkdf-sha256 --ikm`, the inline hex argument, which leaves Z in `ps` and the shell history, against the warning the `hkdf-*` and `x963kdf-*` help carry. Both now say to write Z to a file and give that to `--ikm-file`, and `test_eccdh.sh`'s KDF check does the same, as `test_x963kdf.sh` already did. Found by the PR #193 review (H8). Assisted-by: Claude:claude-mythos-5-1
…ent corrected `sm2_sec1.rs` carried the same comment as `p256_sec1.rs` did, that an out-of-range coordinate also fails the curve check, which `(x + p, y)` for a point `(x, y)` refutes. The comment now says what the previous commit's does, and `sm2_sec1_tests.rs` decodes the SM2 point with `x = 0` (b is a residue mod SM2's p) in its canonical encoding and refuses `x + p` in both forms, the vector computed in Python from the crate's constants as the seven others were. Assisted-by: Claude:claude-mythos-5-1
… documented
`KEMError` gained `KDFError(KDFError)` when the ML-KEM pairings took a KDF; the agreement
pairings flattened the same refusals to `GenericError("the KDF over Z failed")`.
`KeyAgreementError::KDFError(KDFError)` joins the enum (non-exhaustive, so no match breaks), and
`bouncycastle-eccdh`'s `kdf::derive` maps into it. The variants' empty doc comments, which
clippy counted against the PR, each get a sentence. The 0.1.4 release notes named a
`KeyAgreementAlgorithm::Params` that does not exist; they now name `AgreementKey<Params, SS_LEN>`
and what its parameters are. From the PR #193 review's N6, N9 and N10.
Assisted-by: Claude:claude-mythos-5-1
`TestFrameworkKeyAgreement` and `TestFrameworkKeyAgreementKeys` get the `Default` impl the other suites have and a sentence on `new`, which were clippy's `new_without_default` and `empty_docs` warnings on the new file (PR #193 review, N9). Assisted-by: Claude:claude-mythos-5-1
…ld at its edge, and the docs say what step 1 is From the PR #193 review's Notes on this crate: - A derivation from no keys is refused while a key of length zero is derived from, giving `Hash(Counter || SharedInfo)` typed `Unknown` with no strength (N3). That is the `KDF` trait's rule for `Zeroized` and empty material, which the framework's suite holds every KDF to, so the behaviour stays and is now stated in the Security Considerations and pinned by a test (the refusal, the empty key's output and type, and an empty key adding nothing beside a real one). - The Security Considerations' claim that SEC 1 Section 3.6.1 step 1's bound is beyond any slice was not true for SHA-224 and SHA-256 on a 64-bit target; the bullet now gives SEC 1 Section 3.5's hashmaxlen values (2^61 - 1 and 2^125 - 1 octets) and says the check is not made and why (N4). The multi-key doc no longer cites SP 800-56C Rev. 2 for a concatenation it does not specify; it names the convention as the KDF trait's, with no length framing (N4). - After the block loop the output is labelled; a failure there would have left key data in the buffer against the `_out` rule. The buffer is zeroized on that path, although no setter fails on a buffer that passed the length checks (N7). - The step 2 test holds the bound at exactly `hashlen * (2^32 - 1)` by the message step 2 gives, so `>=` to `>` no longer survives (N7), and the bound's arithmetic runs only where it fits a `usize`, so the tests build on a 32-bit target (N8). Mutants on apollo, the crate: 35 (25 caught, 9 unviable, 1 missed: `Clone` for the stateless type replaced by `Default`, equivalent, as before). Assisted-by: Claude:claude-mythos-5-1
…-bit usize The same two changes as the X9.63 KDF's, for the same reasons: a test pins that a derivation from no keys is refused while a key of length zero is derived from, giving `H(counter || FixedInfo)` typed `Unknown` (the `KDF` trait's rule) and adding nothing beside a real key, and the step 2 bound's arithmetic in the tests runs only where `32 * (2^32 - 1)` fits a `usize`, with `MAX_LEN`'s saturation checked where it does not. Mutants on apollo, lib.rs: 40 (29 caught, 8 unviable, 3 missed: the `Clone` equivalent, and `>` to `>=` or `==` in `MAX_LEN`'s guard against a bound that does not fit a usize, which a 64-bit target cannot trip, as before). Assisted-by: Claude:claude-mythos-5-1
…ugh, the prose made exact, and three tests that held from one side From the PR #193 review's Notes: - The X9.63 and concatenation pairings are `ECC-CDH-P256-X963KDF-SHA256` and `ECC-CDH-P256-ConcatKDF-SHA256` on every curve, the KDFs' own spelling and the HKDF pairing's, where the code had `SHA-256` and the doc comments a third form, `P-256` (N6). The tests pin the new names. - `kdf::derive` returns the KDF's refusal as `KeyAgreementError::KDFError` (N10). The pairing docs say how the derived key is typed, that a key under 14 bytes carries no strength although the algorithm's `MAX_SECURITY_STRENGTH` is the curve's (N10), and what an all-zero `Z` yields through each role: `Unknown` with no strength from the X9.63, concatenation and unsalted HKDF roles, `CryptographicRandom` from HKDF with a typed salt, none of it an error (N2). - The Security Considerations name the four decisions that depend on secret data (the identity test, the all-zero `Z` typing, the range check's one masked decision, `to_affine` at keygen) and limit the claim to optimised builds, where `bouncycastle_ec`'s debug assertions are out (N1). Key generation "draws at least len(n) + 64 bits, rounded up to a byte", and P-521's constant says 74 bytes, 592 bits, for the 585 the step names (N4). The Status says SP 800-56A Rev. 3 Appendix D approves the NIST curves and that secp256k1 and the brainpool curves, which SP 800-186 Appendix H allows, follow its procedures by analogy (N4). - Two `compile_fail` doctests on the `kdf` module hold the `KeyLen` bounds (zero, and one past HKDF-SHA256's 255 outputs), which the const assertion enforces in codegen and `cargo check` therefore never saw (N7). The weak-RNG test on every curve uses the strength one step below the curve's instead of `None`, so lowering a threshold no longer survives (N7). Mutants on apollo, kdf.rs with keys_p256.rs and eccdh_p256.rs: 74 (28 caught, 38 unviable, 8 missed, none new: seven in `x963_max_len`, whose value reaches only the const assertion that cargo mutants cannot see fail and the doctests now hold, and `||` to `&&` in `derive`'s check on the KDF's reported length, which no paired KDF can trip). Assisted-by: Claude:claude-mythos-5-1
… keygen reduction writes each step in place Round two of the PR #193 review measured, with a dead-stack probe on arm64, what the round-one Secrets left behind: the by-value returns and arguments between them. `Scalar::from_be_bytes` staged d in a return slot (P-384, brainpoolP384r1) or a by-value argument (P-521, brainpoolP512r1); `barrett::limbs_from_be_bytes` and `barrett::reduce` returned the DRBG output and d - 1 through slots the Secrets were then copied from, and `from_limbs(*d)` copied d out again (R2-H1, R2-H2). Each scalar gains `from_borrowed_limbs(&[u64; L])`, which reduces into the new Secret in place: the limbs are copied in, n subtracted there (`nat::sub_assign`), and the original moved back under the borrow's mask (`Condition::mov`), with P-521's wide fold done the same way over Secrets. `from_limbs` delegates to it and wipes its by-value parameter. `nat` gains the in-place `add_assign` and `sub_assign`, `barrett` the out-parameter `limbs_from_be_bytes_into` and `reduce_into`, `montgomery` `widening_mul_into`, each the by-value form's body with the by-value form a wrapper. `reduce_wide_bits_mod_n_minus_1` on all seven curves writes every step into its Secrets through them and builds the scalar from the borrowed result, so what remains is the returned scalar's own move, which the docs now say. Tests: the in-place nat forms against the by-value ones over carries and borrows; the Barrett and widening out-parameter forms against the by-value ones; `from_borrowed_limbs` against `from_limbs` on every curve at 0, 1, n - 1, n, n + 1 and all ones. Mutants on apollo, nat.rs, barrett.rs, montgomery.rs, the P-256, P-521 and brainpoolP512r1 scalars and the seven extra_bits files with the crate's tests: 476 (376 caught, 80 unviable, 17 missed, 3 timeouts). The misses are the known classes, none in the new logic: `|` to `^` on the two carry or borrow bits in `add`, `sub` and the new `add_assign` and `sub_assign` (never both set); `>>` to `<<` and `+=` to `-=` on the carry propagation in the widening multiplies, including `widening_mul_into`, which montgomery.rs's own comment documents as accepted; `|` to `^` in `widening_square` and in P-521's `hi[0]` assembly (disjoint bit ranges); and `%` to `+` in the shift amount of `limbs_from_be_bytes_into`, equivalent under the profile's masked shifts. The three timeouts are `+=` to `*=` in the same carry loops, which then never terminate. Assisted-by: Claude:claude-mythos-5-1
…written into the caller's buffer, and the claims say what is left From round two of the PR #193 review (R2-H1, R2-H3, R2-N3, R2-N10): - `from_bytes` builds the scalar with `from_borrowed_limbs` from the Secret limbs it already holds, and forms the difference for the range check in place (`nat::sub_assign` on a Secret copy), so no plain copy of d is staged in its frame; the comment that claimed the frame held none now says what is left, the returned key's own move, which a move does not wipe. - The primitive is `agree_out(sk, peer_pk, &mut ss)`: `z_from_product` zeroizes `ss` first and writes Z into it (`set_bytes_as_type`, with the all-zero Z's courtesy error accepted as the Zeroized typing it is), so Z no longer travels as a by-value `Result<KeyMaterial>` through two frames. The worker's `agree_out` is that call; the KDF worker holds Z in a local it owns, zeroized on drop, and derives from it. The per-curve unit test drives the identity path over a prefilled buffer and checks it is left zeroized, which pins the zeroize round one's third hand mutation deleted (R2-N3). - The Security Considerations say what is not reached: a one-shot's returned value is moved out of the frame that built it, so the `_out` forms into a buffer you own are the ones to use where that matters; the field element's conversion to limbs returns by value; and the arithmetic's intermediates in `bouncycastle_ec`, now including a limb of the scalar in the multiplier's frames. `to_affine`'s identity branch is named at key generation and in `derive_pk` (R2-N10). Mutants on apollo, keys_p256.rs, eccdh_p256.rs, keys_p521.rs, eccdh_p521.rs with the crate's tests: 102 (46 caught, 56 unviable, 0 missed). Assisted-by: Claude:claude-mythos-5-1
…roize match the X9.63 KDF's Round two of the PR #193 review (R2-N2, R2-N5, R2-N9): the Security Considerations' claim that no slice reaches the hash's message limit was the one round one corrected in x963kdf, and is corrected the same way (2^61 - 1 bytes for SHA-224 and SHA-256, 2^125 - 1 for the other SHA-2 hashes, none for SHA-3; not checked); the multi-key doc says the concatenation is bare, with no length framing, and is SP 800-56C Rev. 2 Section 2's hybrid Z || T only when the parties have agreed T's format and length; and the buffer is zeroized on the (unreachable) error path after the block loop, as x963kdf's is. Mutants on apollo, lib.rs: 40 (29 caught, 8 unviable, 3 missed, the same equivalents as before). Assisted-by: Claude:claude-mythos-5-1
Round two of the PR #193 review (R2-N1): the test at the bound matched any InvalidLength whose message contained "keydatalen", which the buffer check's message does too, so `>=` to `>` in step 2 survived. It now matches "below hashlen", step 2's message alone. Mutants on apollo, the crate: 35 (25 caught, 9 unviable, 1 missed, the `Clone` equivalent). Assisted-by: Claude:claude-mythos-5-1
…names concatkdf Round two of the PR #193 review (R2-H4, R2-N7): `concatkdf-sha256`, `-sha384`, `-sha3-256` and `-sha3-384` were held only by a test that they differ, so swapping two arms or running SHA-512 under `-sha384` passed. Each is now checked against SP 800-56C Rev. 2 Section 4.1 computed in Python with hashlib (counter || Z || FixedInfo per block, counter from 1, leftmost L bytes) at a length past one hash output; the ACVP sample has no group for these four hashes. The `eccdh-p256` help names `concatkdf-sha256 --ikm-file` beside the other two KDF commands. Assisted-by: Claude:claude-mythos-5-1
Round three of the PR #193 review (R3-H1): "What remains is the returned scalar's own move" read as a statement about key generation, which leaves two copies of d in its own frame (the moved-from local and the tuple temporary); the sentence is about this function's frame. Assisted-by: Claude:claude-mythos-5-1
…e hash's consumed state Round three of the PR #193 review (R3-H1, R3-H2): the list of what step 4's destruction does not reach told the reader to use the _out forms, which keygen and from_bytes do not have, so it now says that key generation and decoding leave d where the move staged it; and it names the copy of Z the KDF's hash leaves when Hash::do_final_out takes self, which the residue probe measured once per paired derivation on every curve, on the _out path too. The seven KDF workers' comments said Z never leaves the call; they now claim only this crate's frames. No code change. Assisted-by: Claude:claude-mythos-5-1
…r exits Round three of the PR #193 review (R3-N2): the doc says the caller's buffer is left zeroized on the error path, but the two exits after Z is written into it, set_bytes_as_type's error and the strength setter's, returned with Z in place. Neither is reachable (the buffer is SS_LEN bytes and SharedSecret takes a strength); each now zeroizes first, on all seven curves, as concatkdf's unreachable post-loop exit does. Assisted-by: Claude:claude-mythos-5-1
…nd BLAKE3) into feature/eccdh Two conflicts, both lists: the factory's dependency table (BLAKE2 and BLAKE3 beside concatkdf) and the CLI's use list (BLAKE2Variant beside ConcatKDFVariant and ECCDHAction), each keeping both sides. The factory, cli, eccdh, x963kdf and concatkdf suites, the eccdh, x963kdf, concatkdf, hkdf and blake CLI shell suites, the all-targets build and rustdoc pass on the result.
mwcw
approved these changes
Nov 11, 2026
mwcw
left a comment
There was a problem hiding this comment.
Multiple reviews by Mythos, Codex and Muse
dghgit
added a commit
that referenced
this pull request
Nov 11, 2026
…n the key objects and ECC CDH over seven curves with its KDFs: in core, AgreementKey<Params, SS_LEN> implemented once per algorithm on a private key over the parameters a worker is made from (() for the raw primitive, KdfParams<'p, K, LEN> naming a borrowed KDF instance and the fixed info for an algorithm that derives its output, so the parameter type selects the impl at the call and both parties name the same one), handing out a KeyAgreement<PK, SS_LEN> worker as a GAT borrowing the key, with agree_out(peer_pk, ss) the primitive and agree provided over it, Z returned as KeyMaterial typed KeyType::SharedSecret for a KDF and nothing else (SP 800-56A Rev. 3 Section 5.8), KeyAgreementPrivateKey / KeyAgreementPublicKey as the encodable key traits with validation in the public key's decoder, and KeyAgreementError with AgreementFailed for Section 5.7.1.2 step 2; in core-test-framework, the agreement suite (both parties agree, typed full-entropy material at the algorithm's strength, the worker reusable and dyn-compatible, a third party differs, prefilled buffers left zeroized on error) and the keys suite, run by a mock Diffie-Hellman over a 61-bit prime; bouncycastle-eccdh, the ECC CDH primitive of Section 5.7.1.2 over P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1 on bouncycastle-ec's arithmetic, SEC 1 codecs and constant-time multipliers, with per-curve key pairs distinct from the ECDSA types (Section 5.6.3.2 item 5), keygen by extra random bits (Section 5.6.1.2.1, L = len(n) + 64 bits reduced in constant time with every step written into a Secret, the RBG checked against the curve's strength), a private key loaded only in [1, n-1] through one masked decision with no comparison routine (checked in the optimised x86-64 and arm64 code), a public key fully validated at decode (Section 5.6.2.3.3 steps 1 to 3, step 4 argued from cofactor 1 on every curve), the agreement following Section 5.7.1.2 line by line ([d]Q in constant time, the identity test kept on a condition no validated input reaches and acted on after the intermediates are zeroized, x_P through a branch-free affine conversion, Z typed at the curve's strength with an all-zero Z coming back Zeroized rather than an error) through z_from_product(&mut P, ss), which leaves the caller's buffer zeroized on every error exit, and the crate docs stating what step 4's destruction reaches and what it does not (the by-value returns of a one-shot and of the limb conversion, the private key's missing _out form, the hash's consumed state, and ec's own intermediates); the agreement role implemented on every curve a further two times over KdfParams, through SEC 1 Section 6.1's X9.63 KDF and SP 800-56A's one-step derivation, each with the hash matched to the curve's strength (ECCDHP256_X963KDF_SHA256, ECCDHP384_ConcatKDF_SHA384 and the rest, named "ECC-CDH-P256-X963KDF-SHA256" and so on), and on P-256 a third time over HKDF-SHA256, so sk.agree(&peer_pk, KdfParams::new(&kdf, info)) needs no annotation and an unpaired KDF is a compile error, with the KDF worker holding Z in one local wiped when it drops; bouncycastle-x963kdf, X963KDF<H> as SEC 1 v2 Section 3.6.1 line by line (Hash(Z || Counter || [SharedInfo]) with the 4-octet counter from 1, keydatalen checked against step 2's bound and the buffer, each block streamed into the hash and held in a Secret, the entropy accounting HKDF's) over SHA-224 to SHA-512; bouncycastle-concatkdf, ConcatKDF<H> as SP 800-56C Rev. 2 Section 4.1 Option 1 line by line (L > 0, reps at most 2^32 - 1, H(counter || Z || FixedInfo) streamed, the leftmost L bytes into a buffer zeroized first) over the SHA-2 and SHA-3 hashes of its Table 1, the multi-key forms of both KDFs concatenating their keys bare as Z; in ec, multiply_point, the scalars' from_borrowed_limbs(&[u64; L]) reducing in place through nat::add_assign / sub_assign and Condition::mov with from_limbs delegating and wiping its parameter, barrett::limbs_from_be_bytes_into and reduce_into, montgomery::widening_mul_into, the extra-bits reductions writing every step into Secrets, and the SEC 1 tests decoding (x + p, y) on every curve; in factory, KDFFactory's X963KDF-SHA256 / -SHA384 / -SHA512 and ConcatKDF-SHA256 / -SHA384 / -SHA512 / -SHA3-256 / -SHA3-384 / -SHA3-512 arms, checked against CAVP known answers; in the CLI, eccdh-p256 .. eccdh-bp512r1 through one generic command (keygen, pk-from-sk, check-consistency by Section 5.6.2.1.4 method (b), and agree writing the raw Z to a file for hkdf-* / x963kdf-* / concatkdf-* --ikm-file, never the inline form that would leave Z in the shell history), x963kdf-sha256 / -sha384 / -sha512 and concatkdf-sha256 / -sha384 / -sha512 / -sha3-256 / -sha3-384 / -sha3-512, each with a known answer; the eccdh stack harness with the per-curve table in the crate docs; and the 0.1.4 release notes. Known answers: NIST CAVP ECC CDH primitive vectors for the three NIST curves, RFC 7027 Appendix A for the brainpool curves from both sides, Wycheproof ecdh and ecpoint sets for all seven curves (10 files, 7667 vectors: 5608 valid, 1609 acceptable, 450 invalid, the agreed count per file pinned), NIST CAVP ansx963_2001.rsp (80 records) and the ACVP KDA OneStep SP800-56Cr2 sample set (150 records), every vector suite failing rather than skipping without bc-test-data. Reviewed in three rounds by Megan's three-agent setup (mythos, codex, muse; 0 Bug throughout: eight Hardening and ten Notes, then four and eleven, then two and ten, every item fixed or declined with its reason, "ready to approve" at 29114c8 and again after the merge of the BLAKE squash at 55a3696, which GitHub carries the approval on) and by a final high-effort pass before this squash (no bug; its eight cleanup items, the dead KeyAgreementError variants, the unused From impls, the hand-rolled test-data loaders, the by-value twins of the in-place reductions, the duplicated KDF CLI command, the repeated cofactor citation and one dead import, go to the release branch with the reviewers' deferred items: an in-place hash finaliser, an into form for the SEC 1 limb conversion and for key generation, tests for the cleanup lines nothing holds, the all-zero Z typing with the hazardous window open, and the empty Z the three KDF commands accept). Mutants on apollo in the PR: ec (nat, barrett, montgomery, the P-256, P-521 and brainpoolP512r1 scalars, the seven extra-bits files) 476 (376 caught, 80 unviable, 17 missed, all equivalent carry and disjoint-bit forms, 3 timeouts); eccdh key and agreement files 102 (46/56/0) and kdf.rs 17 (3/6/8, const-only arithmetic and a defensive check); concatkdf 40 (29/8/3 equivalent); x963kdf 35 (25/9/1 equivalent); core agreement_key.rs 10 (4/6/0). Assisted-by: Claude:claude-mythos-5-1 Assisted-by: Claude:claude-opus-5-5
Contributor
Author
dghgit
added a commit
that referenced
this pull request
Nov 11, 2026
…e items the three review rounds of PR #193 and the final pass before its squash left for later: in core, KeyAgreementError loses EncodingError, KeyGenError and LengthError, which nothing constructed (a wrong-length key is DecodingError, whose doc now says the length is part of the form), and gains From<KDFError> beside its KeyMaterialError and RNGError conversions, as KEMError has; in ec, reduce_once on six scalars and P-521's reduce_wide become wrappers over the reduce_*_into forms instead of second bodies, the seven SEC 1 codecs gain limbs_from_be_bytes_into and be_bytes_from_limbs_into with the by-value forms over them, and the field elements gain to_limbs_into (a plain copy on the NIST curves and secp256k1; on the brainpool curves the REDC inside still returns by value and the doc says so), the from_borrowed_limbs tests check the reduced value itself rather than the function against from_limbs, which now delegates to it, widening_mul_into and to_limbs_into are each checked over a prefilled buffer, and the extra-bits comment no longer claims every step writes into a Secret, since barrett::reduce_into's working values are plain locals; in eccdh, from_bytes converts the key bytes into the Secret limbs through the codec's into form and z_from_product converts x_P and Z the same way, so no copy of the key or the secret is staged in a return slot on those curves, the RNG, key-material and KDF errors convert through From, the range-check comment names nat::sub_assign, the all-zero Z typing is stated as the buffer's decision (skipped while the caller's hazardous window is open) in the step 5 comment and the crate docs' list of secret-dependent decisions, the kdf docs credit HKDF's salt only when it is a MACKey, the cofactor citation repeated twelve times across the curve files points at the crate docs' one statement, a unit test per curve drives z_from_product on G and checks that P is zeroized afterwards, and the CAVP and Wycheproof suites read through the framework's manifest-relative loaders with the crate's own serde_json dependency gone; in x963kdf and concatkdf, derive_key_from_multiple_out says the concatenation is bare and unframed as derive_keys_out did, the import kept for a doc link that did not exist goes, and the CAVP and ACVP suites read through the framework's loaders, still failing rather than skipping; in the CLI, one kdf_cmd over the KDF trait serves the nine x963kdf-* and concatkdf-* arms that were one function written twice, and the hkdf-*, x963kdf-* and concatkdf-* commands refuse an empty --ikm or --ikm-file before any derivation, with a shell case each; and cli/tests/test_blake.sh drops its trailing newline with a computed count rather than GNU head's -c -1, which failed six of its cases on macOS. Left as disclosed, after the consistency and dyn-sweep documents were checked: the copy a by-value final leaves in its moved-from slot, which is every family's and is the question for the boxed-finals step, and an into form for key generation, which needs an out-parameter on the private key. Verified: the workspace suite (2939 tests), the all-targets build, rustdoc with warnings denied, nightly rustfmt, clippy adding nothing on the changed lines, and the eccdh, x963kdf, concatkdf, hkdf and blake CLI suites. Mutants on apollo, scoped to the changed files: ec field, scalar and SEC 1 files 1716 (1347 caught, 303 unviable, 9 timeouts, 57 missed: the carry-form, disjoint-bit and zeroize-with-() equivalents noted at their sites, and to_limbs_into replaced by (), closed by the field tests); eccdh key, agreement and kdf files 381 (164 caught, 209 unviable, 8 missed: x963_max_len's const-only arithmetic and derive's defensive check, as before). Assisted-by: Claude:claude-mythos-5-1
dghgit
added a commit
that referenced
this pull request
Nov 11, 2026
…ment and ECC CDH with the X9.63 and concatenation KDFs; PR #180 squash: BLAKE2b-512, BLAKE2s-256 and BLAKE3; the post-#193 cleanups) into feature/tdes Two keep-both conflicts: the mem_usage_benches manifest (the tdes stack bench beside the BLAKE2 and BLAKE3 ones) and the umbrella crate's re-exports (tdes beside x963kdf). One adaptation: the CLI's main thread stack goes from 8 MiB to 32 MiB. In a debug build the startup frame for the full subcommand set already sat close to 8 MiB on the release, and the eleven tdes-* / tdes2-* commands take it past, so the debug binary aborted on every invocation, --help included (the release-profile binary was unaffected). That bound is the release's to set once; it is raised here so the branch's shell suites run. The tdes, cipher, factory and cli suites, the tdes, blake, eccdh, concatkdf, x963kdf and hkdf CLI shell suites and the all-targets build pass.
dghgit
added a commit
that referenced
this pull request
Nov 11, 2026
…ment and ECC CDH with the X9.63 and concatenation KDFs; PR #180 squash: BLAKE2b-512, BLAKE2s-256 and BLAKE3; the post-#193 cleanups) into feature/rsa-sig No conflicts. One adaptation, as on feature/tdes: the CLI's main thread stack goes from 8 MiB to 32 MiB, since in a debug build the startup frame for the full subcommand set passes 8 MiB once the rsa commands join the release's and the debug binary aborted on every invocation (the release-profile binary was unaffected). That bound is the release's to set once. The rsa and cli suites, the blake and eccdh CLI shell suites and the all-targets build pass.
dghgit
added a commit
to ounsworth/bc-rust
that referenced
this pull request
Nov 11, 2026
… plus the MAC rework, the supplied nonce, the BLAKE hashes, two-party key agreement and ECC CDH with the X9.63 and concatenation KDFs, and the post-bcgit#193 cleanups Conflicts: core's traits take the release's agreement traits (AgreementKey on the private key, the KeyAgreement worker with agree_out, and the two key-encoding traits) in place of this branch's static KeyAgreement trait and its own key traits; core's errors keep the release's KeyAgreementError (this branch's twin goes; its InvalidPublicKey on KEMError stays, the DHKEM's refusal of a small-order recipient key); the test framework takes the release's agreement suite in place of this branch's; CLAUDE.md takes the release's trait list; and the stack-bench manifest keeps the X25519 bench beside the BLAKE2 and BLAKE3 ones. The ed crate, its tests and benches, the hpke crate and the x25519 CLI command still name the old trait at this commit and do not build; the next commits port them.
dghgit
added a commit
that referenced
this pull request
Nov 11, 2026
…of PR #194, the BLAKE hashes of PR #180, two-party key agreement and ECC CDH with the X9.63 and concatenation KDFs of PR #193, and the post-#193 cleanups) into feature/simple-ciphers-arm Two keep-both conflicts: the workspace dependency table (this branch's `default-features = false` on bouncycastle-cipher beside the release's concatkdf entry) and the imports of the AES CMAC key tests (this branch's AES*Params beside the release's Encrypting). The aes, cipher, asm and cli suites, the aes-gcm, aes-cbc and blake CLI shell suites pass, and the aarch64 library type-checks in the default, declared-feature (+aes,+neon) and asm-off configurations.
roy-basmacier
pushed a commit
to roy-basmacier/bc-rust
that referenced
this pull request
Nov 14, 2026
Brings in the eight release commits since the FIPS branch's base: PR bcgit#193 (feature/eccdh, squashed), its post-review cleanups, and the ec keygen, curve marker, Curve trait, generic eccdh and generic ecdsa refactors, plus the KDF description follow-ups. The FIPS branch carried feature/eccdh unsquashed (merged by sub-task 7 at d3318a1, eccdh tip 55a3696). Every file whose content on this branch was exactly the eccdh tip's, i.e. untouched by the FIPS work, takes the release's version: 135 files, including 50 of the 56 conflicts and the ec *_scalar.rs files the textual merge had silently given two copies of reduce_once_into / reduce_wide_into. The 51 per-curve modules the generic refactors replaced (eccdh_<curve>, keys_<curve>, ecdsa_<curve>, rfc6979_<curve>, their benches, and the CLI's concatkdf_cmd.rs / x963kdf_cmd.rs, now kdf_cmd.rs) are removed. Resolved by hand, keeping the FIPS side's additions on the release's content: - concatkdf, eccdh, x963kdf Cargo.toml: the release's manifests plus the `links` key; eccdh now depends on hkdf, already inside the boundary. - Cargo.toml: the workspace's `bouncycastle-ec` stays default-features = false (the approved-mode switch); the umbrella crate's new direct dependency on it asks for `sm2`, so the umbrella still offers everything. - core/src/errors.rs: the release's KeyAgreementError (no LengthError, EncodingError, KeyGenError; From<KDFError>) plus the fips SelfTestNotPassed. - cli/src/main.rs: the release's kdf_cmd dispatch plus the fips wiring. Every SM2 module in ec's hazmat, including the release's new sm2_point and sm2_wnaf, is behind the `sm2` feature. Against the release, the remaining diff outside crypto/fips is the FIPS work only. Assisted-by: Claude:claude-opus-5-5
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.
Two-party key agreement on the key objects, in the shape feature/kem-rework gives the KEM traits (this PR is stacked on it), and
bouncycastle-eccdh, the ECC CDH primitive of SP 800-56A Rev. 3 Section 5.7.1.2 over seven curves on those roles, with its CLI and stack harness: Phases 3 to 7 of the ECC CDH plan. One commit per crate group, each with its mutants figures.What changes
core, core-test-framework (the API)
core::agreement_key:AgreementKey<Params, SS_LEN>, implemented once per algorithm on a private key over the parameters a worker is made from,()for the raw primitive and the caller-configured KDF for an algorithm that derives its output, so the parameter type selects the impl at the call and both parties name the same one. Each impl namestype Algorithm: KeyAgreementAlgorithmandtype PublicKey, produces the worker as a GAT borrowing the key throughcreate_agreement(&'k self, params), and getsagree(&peer_pk, params)provided. Two parties only; MQV and the multi-key schemes of Section 6 are not expressed.core::traits::KeyAgreement<PK, SS_LEN>: the worker,AlgorithmInfosupertrait,agree(&self, &PK)returningZasKeyMaterial<SS_LEN>typedKeyType::SharedSecret(for a KDF and nothing else, Section 5.8), reusable, dyn-compatible.KeyAgreementPrivateKey/KeyAgreementPublicKeyare the encodable key traits, with the public key's decoder carrying the algorithm's validation.KeyAgreementErrorwithAgreementFailedfor Section 5.7.1.2 step 2.SharedSecretor a KDF-bearing algorithm's derived key; the implementor's tests pin which); the worker gives the one-shot's value, carries the metadata, is reusable and works as a trait object; a third party differs; a keys suite round-trips the encodings. An in-file mock Diffie-Hellman over a 61-bit prime, made from a borrowed parameter, runs it.eccdh (11d229a)
crypto/eccdh,bouncycastle::eccdh, over P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1, built onbouncycastle-ec's arithmetic, SEC 1 codecs and constant-time multipliers. Per curve: its own key pair (ECCDHP256PrivateKey/ECCDHP256PublicKeyand siblings; distinct from the ECDSA types, since Section 5.6.3.2 item 5 does not let one pair serve both purposes), the marker (ECCDHP256,ECCDHSecp256K1,ECCDHBp256r1, ..., named "ECC-CDH-P256" and so on), the worker a private key hands out over(), andkeygenby extra random bits (Section 5.6.1.2.1, L = len(n) + 64 bits, the RBG checked against the curve's strength).agreefollows Section 5.7.1.2 line by line:[d]Qin constant time, the identity test kept on a condition no validated input reaches and acted on after the intermediates are zeroized,x_Pthrough a branch-free affine conversion and Appendix C.2's conversion, andZtypedSharedSecretat the curve's strength (an all-zeroZcomes backZeroized, not an error).ECCDHP256_HKDF_SHA256,Zthrough HKDF-SHA256 with the caller's salt and info, the shape of feature/kem-rework's ML-KEM pairings (now overKdfParams<HKDF_SHA256>, below); every curve also carries the X9.63 and concatenation KDF pairings described below, and the rawZis there for any other KDF.keys_common::DerivePublicKeyrecomputesQ = [d]Gfor the CLI. A short example,examples/p256_key_agreement.rs, replaces the ec crate's earlier sketch of the roles over its hazmat API (deleted, with the dev-dependencies it alone needed).cli (fefce52)
eccdh-p256..eccdh-bp512r1: one genericeccdh_cmdover the key traits and the role, with keygen, pk-from-sk, check-consistency (Section 5.6.2.1.4 method (b)) and agree, which writes the rawZfor a KDF (hkdf-* --ikm-file,x963kdf-* --ikm-file,concatkdf-* --ikm-file; never the inline--ikm, which would leave Z in the command history).mem_usage_benches (8909d12)
bench_eccdh_mem_usage: the fourteen key types' sizes and one keygen or agree per curve under a non-inlinedmeasurewrapper. The crate docs' stack table is the deepest frame chain below it (frame-layout remarks plus the binary's call graph, the method of the cipher crates' tables): agree 4,216 / 5,736 / 8,520 / 4,056 / 4,312 / 5,712 / 7,600 bytes for P-256 / P-384 / P-521 / secp256k1 / bp256r1 / bp384r1 / bp512r1, keygen 2,600 / 2,232 / 5,352 / 2,424 / 2,712 / 2,528 / 4,672.x963kdf, factory, cli (9fdf234, 8278197, 0e196f1)
crypto/x963kdf,bouncycastle::x963kdf:X963KDF<H>, SEC 1 v2 Section 3.6.1 line by line, K_i = Hash(Z || Counter || [SharedInfo]) with the 4-octet counter from 1, keydatalen checked against step 2's bound and the buffer, each block hashed by streaming rather than concatenating, one block held in aSecret; the entropy accounting is HKDF's (full-entropy once half a hash output of typed material has gone in, at the least of Z's, the hash's and the length's strengths).derive_outis the function and theKDFtrait runs it, SharedInfo as the fixed info and the multi-key forms concatenating their keys as Z. Over the four SHA-2 hashes of Section 3.5 asX963KDF_SHA224..X963KDF_SHA512("X963KDF-SHA256" and so on). (X963KdfParamswas the agreement parameters untilKdfParams, below.)KDFFactorygainsX963KDF-SHA256,-SHA384and-SHA512(SHA-224 left out as HKDF-SHA224 is); CLIx963kdf-sha256,-sha384,-sha512, thehkdf-*inputs minus the salt.eccdh (0d48ad6): SEC 1 Section 6.1's ECDH scheme on every curve
ECCDHP256_X963KDF_SHA256,ECCDHP384_X963KDF_SHA384,ECCDHP521_X963KDF_SHA512,ECCDHSecp256K1_X963KDF_SHA256,ECCDHBp256r1_X963KDF_SHA256,ECCDHBp384r1_X963KDF_SHA384,ECCDHBp512r1_X963KDF_SHA512("ECC-CDH-P256-X963KDF-SHA256" and so on), Z then KDF(Z, SharedInfo) at one hash output, sosk.agree(&peer_pk, KdfParams::new(&x963kdf, shared_info))on a P-256 key needs no annotation and a KDF the curve is not paired with is a compile error.concatkdf, factory, cli, eccdh (475a58c, 64264b3, 603330c, d671502): SP 800-56A Rev. 3 Section 5.8's one-step derivation
crypto/concatkdf,bouncycastle::concatkdf:ConcatKDF<H>, the one-step KDF of SP 800-56C Rev. 2 Section 4.1, Option 1, withH(x) = hash(x), line by line: L > 0 (step 1), reps at most 2^32 - 1 (step 2, asMAX_LENin bytes), the 4-byte big-endian counter from 1, K(i) = H(counter || Z || FixedInfo) streamed into the hash, the leftmost L bits written into the caller's buffer, zeroized first. Over the Table 1 hashes the library has: SHA-224 to SHA-512/256 and SHA3-224 to SHA3-512, asConcatKDF_SHA256and so on ("ConcatKDF-SHA256", "ConcatKDF-SHA3-256"). The entropy accounting is the X9.63 KDF's.KDFFactorygains the six SHA-2 and SHA-3 arms (ConcatKDF-SHA256,-SHA384,-SHA512,-SHA3-256,-SHA3-384,-SHA3-512); CLIconcatkdf-sha256,-sha384,-sha512,-sha3-256,-sha3-384,-sha3-512, thex963kdf-*inputs.ECCDHP256_ConcatKDF_SHA256,ECCDHP384_ConcatKDF_SHA384,ECCDHP521_ConcatKDF_SHA512,ECCDHSecp256K1_ConcatKDF_SHA256,ECCDHBp256r1_ConcatKDF_SHA256,ECCDHBp384r1_ConcatKDF_SHA384,ECCDHBp512r1_ConcatKDF_SHA512("ECC-CDH-P256-ConcatKDF-SHA256" and so on), through the existingKdfParams<ConcatKDF_SHA256, LEN>role impl.crypto/acvp/KDA-OneStep-Sp800-56Cr2in bc-test-data, the suite fails rather than skips without it), the core KDF framework's suites, the counter and truncation by hand across block boundaries, one block of every instantiation against its hash, the bounds, the entropy accounting, FixedInfo selecting the key; per curve the framework's agreement suite over the params and the derived key againstConcatKDF::derive_outover the raw Z; the factory arms against the aliases;cli/tests/test_concatkdf.sh(the ACVP SHA2-512 case through hex, file and binary paths, the SHA3-512 case through hex,hashlib-computed answers for the other four commands, the bound, and eccdh-p256's Z into concatkdf-sha256 on both sides). Mutants (apollo): concatkdf 40 (29 caught, 8 unviable, 3 missed, equivalents), eccdh kdf.rs 17 (3/6/8, all inx963_max_len's const-only arithmetic andderive's defensive check).merge of feature/kem-rework (2c1c80a, c5cd567): takes the KDF trait reshaping (4edb0a7 there, with its test commits); the X9.63 crate and the factory's arms follow the
&self,&dynand explicit-length shape.Rebased onto release/0.1.4alpha at f3fdb8b, the squash of PR #192 (2026-11-08, tip e3e9239): the merge commits that had brought feature/kem-rework into this branch drop out, the 24 eccdh commits are replayed, and one commit holds the three adaptations those merges had carried (eccdh's direct hkdf dependency gone, the KDF factory's X9.63 arms on the reshaped trait without the SHA-3 ones, the X9.63 tests on the
&selfshape). The change from the previous tip to this one is exactly kem-rework's final seven commits, checked file by file. What the merges took, for the record (9bbb5f5, d7a1763, defd754, 0046464, c83f78c, 444000d in the old history): c83f78c took the 2026-11-08 re-review round of PR #192 (SHA-3 and SHAKE no longer aKDF, so the factory keeps the X9.63 arms and loses theirs; the empty-key-list refusal, which x963kdf follows in 8aab6ba; andKdfParams<'p, K, LEN>withKeyLen, which eccdh follows in 8c43584: the pairings generic over the length,Dbound by a sealedPairedKdfwithMAX_LEN, the derivation under a const assert, a P-256 length test). Earlier: the last two take the fixes from the 2026-11-08 review of PR #192, and x963kdf follows the KDF rule (db6072b, f88abb8: a key with no strength credits nothing, a zero-length derivation is refused, the docs' Z states its strength). d7a1763 takes theSuspendableKeyedremoval and the release's signature-worker dyn change, with core's dyn test keeping the key-agreement, KEM and signature checks. 9bbb5f5 takesKdfParams(e859b14 there). The ECC CDH keys implement the agreement role once per curve overKdfParams<'p, D>, bound on the sealedeccdh::kdf::KdfPairing<D, SS_LEN>a curve's marker implements once per KDF it is paired with (the X9.63 KDF over the matched hash on every curve, HKDF-SHA256 on P-256), with one worker type per curve (ECCDHP256KdfAgreementand siblings) and the derivation shared inkdf::derive; the per-KDF workers and impls go,X963KdfParamsgoes from x963kdf, and eccdh's hkdf dependency with it. A further pairing on a curve is one marker and oneimpl KdfPairing. The example runs HKDF-SHA256 and the X9.63 KDF on the same P-256 keys. 444000d took the round-3 fixes and the KEM_outforms, and the agreement roles followed:KeyAgreement::agree_out(peer_pk, ss)is the primitive withagreeprovided over it, on the worker and onAgreementKey, each_outzeroizing its buffer first and leaving it so on error;kdf::derivewrites into the caller's buffer; the framework's agreement suite drivesagree_outover prefilled buffers. Mutants (apollo, on the pre-rebase tip, same trees): coreagreement_key.rswith the eccdh tests 10 (4 caught, 6 unviable, 0 missed); eccdhkdf.rsand the P-256agree_outpaths with the P-256 tests 14 (7 caught, 6 unviable, 1 missed:||to&&on the two length checks inderive, equivalent for the sealed KDFs, as in mlkem-common).Verification
cargo test --workspace,cargo test --workspace --all-targets --no-run, rustdoc with no warnings, rustfmt (nightly), clippy adds nothing in the new code.Per curve: the framework's agreement and key suites (and over
HkdfParamson P-256),derive_pkagainstkeygen, the [1, n-1] boundary (0, n, n + 1, all ones refused; n - 1 loads), an off-curve point refused at decode, compressed encodings, a weak RNG refused and one at exactly the curve's strength accepted, keygen's determinism given the DRBG output,Zfeeding HKDF to aCryptographicRandomPRK, and the HKDF variant against HKDF run by hand over the rawZ.Known answers: NIST CAVP ECC CDH primitive vectors (CAVS 14.1, 25 records each for P-256, P-384 and P-521, from
crypto/cavp/KAS_ECC_CDH_PrimitiveTest.txtin bc-test-data; the suite fails rather than skips without it), RFC 7027 Appendix A.1 to A.3 for the brainpool curves from both sides, and Wycheproofecdh_*_test.jsonfor all seven curves plus theecpointsets for the NIST curves: 10 files, 7667 vectors (5608 valid, 1609 acceptable, 450 invalid), the SPKI handled by a strict DER walk in the test with a mismatched AlgorithmIdentifier treated as a key rejected at decode; the runner pins the number that reach the library and agree per file (the valid vectors and the one compressed-point acceptable vector) and fails if that compressed point is rejected.CLI:
cli/tests/test_eccdh.sh5 of 5 (CAVP P-256 and P-521 COUNT 0 through files, stdin and--hex-input; round trips on all seven curves; an off-curve peer key, d = 0 and a P-384 key under the P-256 command refused;Zthrough a file intohkdf-sha256 --ikm-fileon both sides).Mutants (apollo), per curve on its
keys_*.rsandeccdh_*.rswith the curve's own test binaries: P-256 55 (22 caught, 32 unviable, 1 missed), P-384 45 (19/25/1), P-521 45 (20/25/0), secp256k1 45 (19/25/1), brainpoolP256r1 45 (19/25/1), brainpoolP384r1 45 (19/25/1), brainpoolP512r1 45 (19/25/1). The miss on five curves was keygen's<on the RNG strength becoming<=, killed by the exact-strength test (confirmation run on the key files: 28 each, 15 caught, 13 unviable, 0 missed); brainpoolP512r1's isagree's guard around settingZ's strength (the all-zeroZarm), unreachable there becausebis a quadratic non-residue mod that curve'sp, sox_P = 0is not on the curve; on P-521, wherex_P = 0is on the curve, the same mutant is caught by Wycheproof. (An earlier version of this body gave the length-derived default strength as the reason, which stopped holding once aSharedSecrettook no strength of its own.) Coreagreement_key.rson the role trait: 6 (2 caught, 4 unviable, 0 missed).X9.63: the core KDF framework's suites on a CAVP vector, the counter and truncation against hand-computed Hash(Z || Counter || SharedInfo) across block boundaries, both length bounds, the entropy accounting (caps by Z, hash and length; the 31/32-byte threshold for SHA-512; Unknown credited nothing), and all 80 SHA-224/256/384/512 records of NIST CAVP
ansx963_2001.rspfrom bc-test-data'scrypto/cavp(the suite fails rather than skips without it).cli/tests/test_x963kdf.sh6 of 6 (CAVP records through hex, file and binary paths, the bound, and eccdh-p256's Z into x963kdf-sha256 on both sides). Per curve: the framework's agreement suite over the params, the derived key againstX963KDF::derive_outover the raw Z, and the SharedInfo selecting the key.Mutants for the X9.63 work (apollo, before the merge): x963kdf crate 42 (21 caught, 19 unviable, 2 missed:
Clonefor the stateless type replaced byDefault, equivalent, and a+in the last-block copy, killed since by a test deriving a partial last block into an exactly-sized buffer); eccdh per curve on itseccdh_*.rswith the curve's test binary: P-256 36 (10 caught, 26 unviable, 0 missed), P-384, secp256k1, brainpoolP256r1 and brainpoolP384r1 26 each (7/19/0), P-521 and brainpoolP512r1 26 each (6/19/1, theagreestrength guard: unreachable on brainpoolP512r1 as above, and since caught by Wycheproof on P-521). After the merge (x963kdf on the reshaped trait): 36 (21 caught, 13 unviable, 2 missed: theCloneequivalent and step 2's*to+, which the bound test now distinguishes from the buffer check, a6a87a8; 22/13/1 on re-run, the equivalent remaining).After KdfParams (9bbb5f5, 7cb95db):
cargo test --workspace, all targets built, rustdoc, rustfmt clean; the example and the eccdh, x963kdf and hkdf CLI suites pass. Mutants (apollo): eccdhkdf.rs6 (2 caught, 4 unviable, 0 missed); per curve on itseccdh_*.rswith the curve's test binary 26 each: P-256, P-384, secp256k1, brainpoolP256r1 and brainpoolP384r1 7/19/0, P-521 and brainpoolP512r1 6/19/1 (theagreestrength guard, as above); x963kdf crate 32 (22/9/1, theCloneequivalent).Review round (2026-11-10, Megan's three-agent review of ea8b62e: 0 Bug, 8 Hardening, 10 Notes)
All eight Hardening items and the ten Notes are in, as per-crate commits above the merge of #194:
from_byteson every curve borrows its input, holds the limbs and the differenced - ninSecret, and makes the[1, n-1]check one masked decision (!nat::is_zero & is_equal(borrow, 1)) with no array comparison;ec's extra-random-bits reductions hold the DRBG output, its reduction and d inSecrettoo. In the x86-64 release assembly of P-256'sfrom_bytes: no comparison routine, one barriered decision, bothSecrets cleared on the accept, reject and unwind paths.x_affinescrubs the square of the inverse, the limbs ofzlive in aSecret, and the crate docs now say which intermediates this crate destroys (P,z, the inverse and its square, the bytes) and thatbouncycastle_ec's own are not reached, in place of the claim that step 4 was done.z_from_product(&mut P)so an in-file unit test per curve drives the identity path; the framework's mock rejects a context labelled 0 and a test drivesagree_outover prefilled buffers through the worker, the role with the identity peer, and the role with the rejected context.ecSEC 1 test decodes(x + p, y)for a point on the curve after reduction (only the range check rejects it; the P-256 vector is the reviewers'), and the false comment inp256_sec1.rs(andsm2_sec1.rs) is corrected.ansx963_2001.rspknown answers. H8 The eccdh help, module comment and shell test send Z through--ikm-file.Zyields through each KDF role (N2); the X9.63 and concatenation KDFs' edge is stated and pinned: no keys is refused, a key of length zero is derived from and yieldsUnknownoutput, theKDFtrait's rule the framework suite holds every KDF to (N3); the step 1 wording gives SEC 1's hashmaxlen values, the multi-key doc no longer cites SP 800-56C, P-521's DRBG draw reads "74 bytes, 592 bits, at least the 585 asked", and the Status says secp256k1 and the brainpool curves are outside SP 800-56A's Appendix D and allowed by SP 800-186 Appendix H (N4); the Wycheproof split and the brainpoolP512r1 mutant's reason above are corrected (N5, N6); the pairing names areECC-CDH-P256-X963KDF-SHA256andECC-CDH-P256-ConcatKDF-SHA256throughout, the release notes nameAgreementKey<Params, SS_LEN>(N6); the X9.63 step 2 test holds the bound itself, the weak-RNG tests use the strength one step below each curve's, twocompile_faildoctests hold theKeyLenbounds, and the X9.63 KDF zeroizes on the (unreachable) error path after its block loop (N7); the bound tests no longer overflow a 32-bitusize(N8); theKeyAgreementErrorvariants are documented and the framework structs haveDefault(N9);KeyAgreementError::KDFErrorcarries the KDF's own error (N10). Not changed from N10:AgreementKey::PublicKeystays unbounded andagree/agree_outstay overridable, as the KEM roles are;#![no_std]stays nominal whilecoreis std.keys_p256.rs+eccdh_p256.rs57 (25 caught, 32 unviable, 0 missed);ecp256_sec1.rs+p256_extra_bits.rs+p521_extra_bits.rs52 (46 caught, 5 unviable, 1 missed:|to^on two non-overlapping bit ranges in P-521's fold, equivalent); coreagreement_key.rswith the framework's and eccdh's tests 10 (4 caught, 6 unviable). The three hand mutations the review listed (the role's zeroize, the identity check, the range check) each fail one test.Round two (same day, at 45422de: 0 Bug, 4 Hardening, 11 Notes, "ready to approve"): the five pre-merge items and the code behind R2-H1 to R2-H3, five commits (ec, eccdh, concatkdf, x963kdf, cli). Each
ecscalar gainsfrom_borrowed_limbs(&[u64; L]), reducing into itsSecretin place (nat::sub_assign,Condition::mov),from_limbsdelegates and wipes its parameter, andnat,barrettandmontgomerygain in-place and out-parameter forms the keygen reduction now writes every step through, so what the dead-stack probe found in the by-value returns is gone and what remains is the returned scalar's move (R2-H1, R2-H2). The eccdh primitive isagree_out(sk, peer_pk, &mut ss), writing Z into the caller's buffer, with the KDF worker holding Z in a local it owns (R2-H3); the unit test drives the identity path over a prefilled buffer and checks it is left zeroized (R2-N3). Thekeys_*.rscomment and the lib.rs sentence say what is left: a one-shot's returned value is moved out of the frame that built it, the field element's limb conversion returns by value, and ec's arithmetic including a limb of the scalar in the multiplier's frames is not reached. The X9.63 bound test matches step 2's own message (R2-N1); concatkdf's step 4 wording, post-loop zeroize and multi-key doc match x963kdf's (R2-N2, N5, N9); the four untestedconcatkdf-*commands have hashlib-computed known answers (R2-H4); the eccdh help namesconcatkdf-sha256 --ikm-fileand this body the concatkdf work (R2-N7). Mutants (apollo): ec (nat, barrett, montgomery, the P-256, P-521 and brainpoolP512r1 scalars, the seven extra_bits files) 476 (376 caught, 80 unviable, 17 missed, 3 timeouts): the misses are|/^on carry bits inadd,sub,add_assign,sub_assign; the documented carry-propagation shift and sign swaps in the widening multiplies,widening_mul_intoincluded;|/^on disjoint bit ranges inwidening_squareand P-521'shi[0]; and%/+onlimbs_from_be_bytes_into's shift amount, equivalent under masked shifts; the timeouts are+=/*=in the same carry loops. eccdh (the P-256 and P-521 key and agreement files) 102 (46 caught, 56 unviable, 0 missed);kdf.rs17 more (3 caught, 6 unviable, 8 missed:x963_max_len's const-only arithmetic andderive's defensive check, as before). concatkdf lib.rs 40 (29/8/3, the pre-existing equivalents). x963kdf 35 (25/9/1, theCloneequivalent).Round three (same day, at 10064f9: 0 Bug, 2 Hardening, 10 Notes, "ready to approve"): the pre-merge items, three commits (ec, eccdh, eccdh). Both Hardening items are what the dead-stack probe still measures end to end:
keygenleaves two copies of d in its own frame (the moved-from local and the tuple temporary) andfrom_bytesone on the four wide curves, and every paired derivation leaves one copy of Z because the KDF's hash finalises by value (Hash::do_final_out(self), in sha2 and sha3, unchanged by this PR). The crate docs now say that a private key has no_outform, so key generation and decoding leave d where the move staged it, and name the hash's consumed state among what is not reached; the seven KDF workers' comments claim only this crate's frames, and ec's extra_bits comment speaks for its own frame (R3-H1, R3-H2).z_from_productzeroizes the buffer before its two unreachable late error exits, as its doc says (R3-N2). This body's slips: the eccdh 102 did not includekdf.rs, the SHA3-512 CLI case goes through hex only, and a round-one mutants sentence trailed the round-two paragraph (R3-N6). Left for issues, on the reviewers' grading: an in-place hash finaliser, an into form for the SEC 1 limb conversion and for key generation, the untested cleanup lines (R3-N3), and the three KDF commands' acceptance of an empty Z (R2-N4).Not here
The Section 6 schemes and key confirmation, ECC MQV, SM2 key exchange, HKDF pairings on the other six curves (each is a marker and one
impl KdfPairingin the curve's file), the X9.63 KDF over SHA-1 (not implemented in this library).Assisted-by: Claude:claude-mythos-5-1
🤖 Generated with Claude Code