Files
secp256k1-zkp/src/modules/frost_enrollment
Kgothatso Ngako 266c6a7c4f frost_enrollment: fix API contract issues found in review
Five review findings, all non-blocking, all in the contract between the
module and its callers rather than in the cryptography. Each fix comes
with a regression test that fails without it.

1. shares_gen zeroed shares32_out before validating n_ids.

   shares32_out is the only output in this module whose size is
   caller-supplied. A caller that takes the helper count from a
   negotiated protocol message, passes a fixed buffer, and relies on
   this API's "invalid ranges return 0" convention would have memory
   past that buffer zeroed before the call reported failure -- turning a
   recoverable length-confusion bug into memory corruption. The frost
   module validates counts first for exactly this reason
   (trusted_dealer_keygen, keygen_impl.h:228).

   Validation now happens before the memset. The early return still
   wipes session_secrand32, because "a failed call cannot be retried on
   the same randomness" is a security property and an exception to it
   would be worse than the tidier control flow. The header's zeroing
   promise is scoped accordingly: the buffer is zeroed on failure except
   when n_ids itself is out of range, where it is not written at all.

2. mismatch_id had an undocumented second cause.

   The header said mismatch_id names the helper whose PARAMETERS HASH
   disagrees and is UINT32_MAX "when the failure has another cause", but
   share_agg also sets it when a helper's share is not a valid scalar.
   The example baked the wrong reading in, printing "Helper %u disagrees
   about the enrollment parameters" for what may be a corrupted
   transmission.

   Documented rather than removed: the attribution is genuinely useful
   for both causes, and this is API- and vector-compatible. The header
   now names both, says they are not distinguished so a caller must not
   report one specifically, and calls out that the second can name the
   CALLER'S OWN identifier, since the kept share is summed with the
   rest. The example's message is corrected in a following commit.

3. params_hash's doc claimed it returns 0 on an "unparseable thresh_pk".

   It does not, and cannot: secp256k1_pubkey_load (secp256k1.c:280) only
   ARG_CHECKs that x is nonzero, so a zeroed pubkey fires the
   illegal-argument callback and any other 64-byte content is accepted
   without curve validation. A caller writing input screening around the
   documented return 0 would abort on the first malformed input. The doc
   now states that an unusable pubkey object is API misuse, matching the
   pointer/value split the impl already follows.

4. params_hash's doc listed three of its ten validity conditions.

   It is the natural pre-validation entry point -- it enforces exactly
   what the other four enforce -- but the doc mentioned only duplicate
   ids and the two n_ids bounds, so the threshold >= 2 divergence and
   the mode-specific n bounds were discoverable only from the .md or the
   source. The parameter list now carries the same constraint lines as
   shares_gen.

5. secshare_gen required a signing context even when it would not sign.

   The ecmult_gen check was unconditional, but ecmult_gen is used only
   inside the expected_pubshare != NULL branch. A caller on a
   verification-only context passing NULL -- explicitly permitted -- hit
   the illegal-argument callback for a generator multiplication that
   would never happen.

   The check is now conditional on expected_pubshare being non-NULL, and
   stays at the top of the function rather than moving into the branch:
   ARG_CHECK returns directly, and from inside the branch that would
   skip the cleanup that wipes secshare and term. Documented in the
   header.

Also in this commit, three comment/dead-code fixes the review noted:
the redundant set_int of `term` in both aggregation loops (always
written by set_b32 before it is read), the Lagrange denominator comment
crediting new_id for something only id distinctness provides, and the
comment that described the memset-before-validation ordering rather than
justifying it -- now moot.

The new run_frost_enrollment_contract_test also closes review coverage
gaps 1, 2 and 8, which overlap these findings: malformed wire scalars
into share_agg and secshare_gen, sigmas summing to zero mod the order,
an invalid secshare32 into shares_gen (the only path exercising its
declassify branch), mismatch_id asserted on a NON-CONTIGUOUS helper set
{0, 2} so an implementation returning the array index would now be
caught, mismatch_id at the caller's own slot, and successful runs with
each optional secshare_gen check skipped and with both skipped.

Both fixes were verified to be load-bearing by reverting them
individually: the F1 test fails on `guarded[i] == 0xa5` and the F5 test
fires the illegal-argument callback. ./tests, ./noverify_tests and
ctime_tests pass; the module is clean under valgrind (0 errors from 0
contexts).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 10:15:26 +02:00
..