Skip to content

Two-party key agreement on the key objects, and bouncycastle-eccdh: ECC CDH over seven curves with its CLI and stack harness - #193

Closed
dghgit wants to merge 53 commits into
release/0.1.4alphafrom
feature/eccdh
Closed

dghgit wants to merge 53 commits into
release/0.1.4alphafrom
feature/eccdh

Conversation

@dghgit

@dghgit dghgit commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

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 names type Algorithm: KeyAgreementAlgorithm and type PublicKey, produces the worker as a GAT borrowing the key through create_agreement(&'k self, params), and gets agree(&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, AlgorithmInfo supertrait, agree(&self, &PK) returning Z as KeyMaterial<SS_LEN> typed KeyType::SharedSecret (for a KDF and nothing else, Section 5.8), reusable, dyn-compatible. KeyAgreementPrivateKey / KeyAgreementPublicKey are the encodable key traits, with the public key's decoder carrying the algorithm's validation. KeyAgreementError with AgreementFailed for Section 5.7.1.2 step 2.
  • Framework suite: both parties compute the same secret, typed full-entropy material at the algorithm's strength (the raw primitive's SharedSecret or 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)

  • The new crate crypto/eccdh, bouncycastle::eccdh, over P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1 and brainpoolP512r1, built on bouncycastle-ec's arithmetic, SEC 1 codecs and constant-time multipliers. Per curve: its own key pair (ECCDHP256PrivateKey / ECCDHP256PublicKey and 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 (), 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); step 4 is argued, not computed, because 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 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 as ECCDHP256_HKDF_SHA256, Z through HKDF-SHA256 with the caller's salt and info, the shape of feature/kem-rework's ML-KEM pairings (now over KdfParams<HKDF_SHA256>, below); every curve also carries the X9.63 and concatenation KDF pairings described below, and the raw Z is there for any other KDF. keys_common::DerivePublicKey recomputes Q = [d]G for 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).
  • The 0.1.4 release notes carry the entry.

cli (fefce52)

  • eccdh-p256 .. eccdh-bp512r1: one generic eccdh_cmd over 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 raw Z for 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-inlined measure wrapper. 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)

  • The new crate 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 a Secret; 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_out is the function and the KDF trait 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 as X963KDF_SHA224 .. X963KDF_SHA512 ("X963KDF-SHA256" and so on). (X963KdfParams was the agreement parameters until KdfParams, below.)
  • KDFFactory gains X963KDF-SHA256, -SHA384 and -SHA512 (SHA-224 left out as HKDF-SHA224 is); CLI x963kdf-sha256, -sha384, -sha512, the hkdf-* inputs minus the salt.

eccdh (0d48ad6): SEC 1 Section 6.1's ECDH scheme on every curve

  • Each curve's keys implement the role a further time over the X9.63 KDF, the hash matched to the curve's strength: 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, so sk.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

  • The new crate crypto/concatkdf, bouncycastle::concatkdf: ConcatKDF<H>, the one-step KDF of SP 800-56C Rev. 2 Section 4.1, Option 1, with H(x) = hash(x), line by line: L > 0 (step 1), reps at most 2^32 - 1 (step 2, as MAX_LEN in 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, as ConcatKDF_SHA256 and so on ("ConcatKDF-SHA256", "ConcatKDF-SHA3-256"). The entropy accounting is the X9.63 KDF's.
  • KDFFactory gains the six SHA-2 and SHA-3 arms (ConcatKDF-SHA256, -SHA384, -SHA512, -SHA3-256, -SHA3-384, -SHA3-512); CLI concatkdf-sha256, -sha384, -sha512, -sha3-256, -sha3-384, -sha3-512, the x963kdf-* inputs.
  • Each curve's keys implement the agreement role a further time over the concatenation KDF matched to the curve's hash: 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 existing KdfParams<ConcatKDF_SHA256, LEN> role impl.
  • Tests: the NIST ACVP KDA OneStep SP800-56Cr2 sample set (150 records: SHA2-224, SHA2-512 and SHA3-512, AFT and VAL, FixedInfo as the ACVP KDA specification builds it; crypto/acvp/KDA-OneStep-Sp800-56Cr2 in 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 against ConcatKDF::derive_out over 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 in x963_max_len's const-only arithmetic and derive'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, &dyn and 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 &self shape). 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 a KDF, so the factory keeps the X9.63 arms and loses theirs; the empty-key-list refusal, which x963kdf follows in 8aab6ba; and KdfParams<'p, K, LEN> with KeyLen, which eccdh follows in 8c43584: the pairings generic over the length, D bound by a sealed PairedKdf with MAX_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 the SuspendableKeyed removal and the release's signature-worker dyn change, with core's dyn test keeping the key-agreement, KEM and signature checks. 9bbb5f5 takes KdfParams (e859b14 there). The ECC CDH keys implement the agreement role once per curve over KdfParams<'p, D>, bound on the sealed eccdh::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 (ECCDHP256KdfAgreement and siblings) and the derivation shared in kdf::derive; the per-KDF workers and impls go, X963KdfParams goes from x963kdf, and eccdh's hkdf dependency with it. A further pairing on a curve is one marker and one impl 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 _out forms, and the agreement roles followed: KeyAgreement::agree_out(peer_pk, ss) is the primitive with agree provided over it, on the worker and on AgreementKey, each _out zeroizing its buffer first and leaving it so on error; kdf::derive writes into the caller's buffer; the framework's agreement suite drives agree_out over prefilled buffers. Mutants (apollo, on the pre-rebase tip, same trees): core agreement_key.rs with the eccdh tests 10 (4 caught, 6 unviable, 0 missed); eccdh kdf.rs and the P-256 agree_out paths with the P-256 tests 14 (7 caught, 6 unviable, 1 missed: || to && on the two length checks in derive, 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 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 against HKDF run by hand over the raw Z.

  • 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.txt in 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 Wycheproof ecdh_*_test.json for all seven curves plus the ecpoint sets 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.sh 5 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; Z through a file into hkdf-sha256 --ikm-file on both sides).

  • Mutants (apollo), per curve on its keys_*.rs and eccdh_*.rs 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 <=, killed by the exact-strength test (confirmation run on the key files: 28 each, 15 caught, 13 unviable, 0 missed); brainpoolP512r1's is agree's guard around setting Z's strength (the all-zero Z arm), unreachable there because b is a quadratic non-residue mod that curve's p, so x_P = 0 is not on the curve; on P-521, where x_P = 0 is 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 a SharedSecret took no strength of its own.) Core agreement_key.rs on 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.rsp from bc-test-data's crypto/cavp (the suite fails rather than skips without it). cli/tests/test_x963kdf.sh 6 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 against X963KDF::derive_out over 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: Clone for the stateless type replaced by Default, 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 its eccdh_*.rs with 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, the agree strength 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: the Clone equivalent 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): eccdh kdf.rs 6 (2 caught, 4 unviable, 0 missed); per curve on its eccdh_*.rs with 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 (the agree strength guard, as above); x963kdf crate 32 (22/9/1, the Clone equivalent).

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:

  • H1, H3 from_bytes on every curve borrows its input, holds the limbs and the difference d - n in Secret, 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 in Secret too. In the x86-64 release assembly of P-256's from_bytes: no comparison routine, one barriered decision, both Secrets cleared on the accept, reject and unwind paths.
  • H2 x_affine scrubs the square of the inverse, the limbs of z live in a Secret, and the crate docs now say which intermediates this crate destroys (P, z, the inverse and its square, the bytes) and that bouncycastle_ec's own are not reached, in place of the claim that step 4 was done.
  • H4 Steps 2 to 5 moved into 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 drives agree_out over prefilled buffers through the worker, the role with the identity peer, and the role with the rejected context.
  • H5 Each curve's ec SEC 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 in p256_sec1.rs (and sm2_sec1.rs) is corrected.
  • H6 The Wycheproof runner pins the agreed count per file and requires the compressed point to agree. H7 The three X9.63 factory arms are checked against CAVP ansx963_2001.rsp known answers. H8 The eccdh help, module comment and shell test send Z through --ikm-file.
  • Notes: the constant-time prose names the four secret-dependent decisions and is limited to optimised builds (N1); the pairing docs say what an all-zero Z yields 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 yields Unknown output, the KDF trait'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 are ECC-CDH-P256-X963KDF-SHA256 and ECC-CDH-P256-ConcatKDF-SHA256 throughout, the release notes name AgreementKey<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, two compile_fail doctests hold the KeyLen bounds, and the X9.63 KDF zeroizes on the (unreachable) error path after its block loop (N7); the bound tests no longer overflow a 32-bit usize (N8); the KeyAgreementError variants are documented and the framework structs have Default (N9); KeyAgreementError::KDFError carries the KDF's own error (N10). Not changed from N10: AgreementKey::PublicKey stays unbounded and agree / agree_out stay overridable, as the KEM roles are; #![no_std] stays nominal while core is std.
  • Mutants (apollo): eccdh keys_p256.rs + eccdh_p256.rs 57 (25 caught, 32 unviable, 0 missed); ec p256_sec1.rs + p256_extra_bits.rs + p521_extra_bits.rs 52 (46 caught, 5 unviable, 1 missed: | to ^ on two non-overlapping bit ranges in P-521's fold, equivalent); core agreement_key.rs with 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 ec scalar gains from_borrowed_limbs(&[u64; L]), reducing into its Secret in place (nat::sub_assign, Condition::mov), from_limbs delegates and wipes its parameter, and nat, barrett and montgomery gain 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 is agree_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). The keys_*.rs comment 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 untested concatkdf-* commands have hashlib-computed known answers (R2-H4); the eccdh help names concatkdf-sha256 --ikm-file and 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 in add, sub, add_assign, sub_assign; the documented carry-propagation shift and sign swaps in the widening multiplies, widening_mul_into included; |/^ on disjoint bit ranges in widening_square and P-521's hi[0]; and %/+ on limbs_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.rs 17 more (3 caught, 6 unviable, 8 missed: x963_max_len's const-only arithmetic and derive's defensive check, as before). concatkdf lib.rs 40 (29/8/3, the pre-existing equivalents). x963kdf 35 (25/9/1, the Clone equivalent).

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: keygen leaves two copies of d in its own frame (the moved-from local and the tuple temporary) and from_bytes one 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 _out form, 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_product zeroizes the buffer before its two unreachable late error exits, as its doc says (R3-N2). This body's slips: the eccdh 102 did not include kdf.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 KdfPairing in 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

@dghgit dghgit changed the title Two-party key agreement on the key objects (AgreementKey / KeyAgreement, P-256 ECC CDH example) Two-party key agreement on the key objects, and bouncycastle-eccdh: ECC CDH over seven curves with its CLI and stack harness Nov 7, 2026
@hubot
hubot force-pushed the feature/kem-rework branch from 9b72a72 to cf178ae Compare November 7, 2026 08:37
@hubot
hubot force-pushed the feature/kem-rework branch from 4bf8c53 to cf178ae Compare November 7, 2026 13:31
@hubot
hubot force-pushed the feature/kem-rework branch from e859b14 to cf178ae Compare November 7, 2026 19:39
@hubot
hubot deleted the feature/eccdh branch November 7, 2026 20:03
@hubot
hubot deleted the branch feature/kem-rework November 7, 2026 20:03
@dghgit
dghgit restored the feature/eccdh branch November 7, 2026 20:26
dghgit added 17 commits November 8, 2026 15:16
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
@dghgit
dghgit requested a review from mwcw November 10, 2026 03:46
… 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 mwcw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@dghgit

dghgit commented Nov 11, 2026

Copy link
Copy Markdown
Contributor Author

Squash-merged onto release/0.1.4alpha as f60bb06 (tree identical to 55a3696). Closing by hand, as the merge happens on the private upstream. The deferred items from the review rounds and the final pass are taken on the release branch.

@dghgit dghgit closed this Nov 11, 2026
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 dghgit mentioned this pull request Nov 11, 2026
1 task
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants