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>
971 lines
46 KiB
C
971 lines
46 KiB
C
/***********************************************************************
|
|
* Distributed under the MIT software license, see the accompanying *
|
|
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
|
|
***********************************************************************/
|
|
|
|
#ifndef SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H
|
|
#define SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H
|
|
|
|
#include "../../../include/secp256k1_frost_enrollment.h"
|
|
#include "../../../include/secp256k1_schnorrsig.h"
|
|
|
|
#include "vectors.h"
|
|
|
|
/* Everything one enrollment run needs, so a test can set one up in a line and
|
|
* then poke at individual pieces. */
|
|
typedef struct {
|
|
size_t n, t, u;
|
|
uint32_t new_id;
|
|
unsigned char thresh_sk[32];
|
|
unsigned char secshares[SECP256K1_FROST_MAX_PARTICIPANTS][32];
|
|
secp256k1_pubkey pubshares[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
secp256k1_pubkey thresh_pk;
|
|
uint32_t ids[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
|
|
/* shares[i] is helper ids[i]'s round 1.1 output buffer, aligned with ids:
|
|
* entry j is what helper ids[i] produced for helper ids[j]. */
|
|
unsigned char shares[SECP256K1_FROST_MAX_PARTICIPANTS][SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
unsigned char params_hashes[SECP256K1_FROST_MAX_PARTICIPANTS][32];
|
|
unsigned char sigmas[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
|
|
secp256k1_pubkey new_pubshare;
|
|
unsigned char new_secshare[32];
|
|
} frost_enrollment_test_run;
|
|
|
|
/* Deals a fresh (t, n) group and fills in the helper set: the first u
|
|
* identifiers that are not new_id, in ascending order. */
|
|
static void frost_enrollment_test_deal(frost_enrollment_test_run *r, size_t n, size_t t, size_t u, uint32_t new_id) {
|
|
size_t i, k;
|
|
|
|
r->n = n;
|
|
r->t = t;
|
|
r->u = u;
|
|
r->new_id = new_id;
|
|
testrand256(r->thresh_sk);
|
|
CHECK(secp256k1_frost_trusted_dealer_keygen(CTX, r->secshares[0], &r->thresh_pk, r->pubshares, n, (uint32_t)t, r->thresh_sk) == 1);
|
|
k = 0;
|
|
for (i = 0; i < n && k < u; i++) {
|
|
if ((uint32_t)i == new_id) {
|
|
continue;
|
|
}
|
|
r->ids[k] = (uint32_t)i;
|
|
k++;
|
|
}
|
|
CHECK(k == u);
|
|
}
|
|
|
|
/* Runs round 1.1 for every helper. */
|
|
static void frost_enrollment_test_round1_gen(frost_enrollment_test_run *r) {
|
|
size_t i;
|
|
|
|
for (i = 0; i < r->u; i++) {
|
|
unsigned char secrand[32];
|
|
testrand256(secrand);
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, r->shares[i], r->params_hashes[i], secrand, r->secshares[r->ids[i]], &r->thresh_pk, r->ids, r->u, r->ids[i], r->new_id, r->n, (uint32_t)r->t) == 1);
|
|
/* The seed is consumed by the call. */
|
|
CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand)));
|
|
}
|
|
}
|
|
|
|
/* Assembles helper j's round 1.2 inputs out of the round 1.1 outputs: the
|
|
* share kept at its own position, the shares received at the others', and the
|
|
* received parameters hashes with its own slot left zero. */
|
|
static void frost_enrollment_test_collect(const frost_enrollment_test_run *r, size_t j, unsigned char *all_shares, unsigned char *received) {
|
|
size_t i;
|
|
|
|
memset(received, 0, r->u * 32);
|
|
for (i = 0; i < r->u; i++) {
|
|
memcpy(&all_shares[32 * i], &r->shares[i][32 * j], 32);
|
|
if (i != j) {
|
|
memcpy(&received[32 * i], r->params_hashes[i], 32);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Runs round 1.2 for every helper. */
|
|
static void frost_enrollment_test_round1_agg(frost_enrollment_test_run *r) {
|
|
size_t j;
|
|
|
|
for (j = 0; j < r->u; j++) {
|
|
unsigned char all_shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
unsigned char received[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
uint32_t mismatch_id = 0;
|
|
|
|
frost_enrollment_test_collect(r, j, all_shares, received);
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, &r->sigmas[32 * j], &mismatch_id, all_shares, received, &r->thresh_pk, r->ids, r->u, r->ids[j], r->new_id, r->n, (uint32_t)r->t) == 1);
|
|
CHECK(mismatch_id == UINT32_MAX);
|
|
}
|
|
}
|
|
|
|
/* Gathers the helpers' public shares into an array aligned with ids. The
|
|
* run's own table is indexed by participant id, which only coincides with the
|
|
* ids alignment when the helper set happens to be 0..u-1 -- exactly the
|
|
* confusion the API documentation warns about. */
|
|
static void frost_enrollment_test_helper_pubshares(const frost_enrollment_test_run *r, secp256k1_pubkey *out) {
|
|
size_t i;
|
|
|
|
for (i = 0; i < r->u; i++) {
|
|
out[i] = r->pubshares[r->ids[i]];
|
|
}
|
|
}
|
|
|
|
/* Runs round 2, with both optional checks enabled. */
|
|
static void frost_enrollment_test_round2(frost_enrollment_test_run *r) {
|
|
secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
|
|
frost_enrollment_test_helper_pubshares(r, helper_pubshares);
|
|
CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r->new_pubshare, helper_pubshares, r->ids, r->u, r->new_id, r->n, (uint32_t)r->t) == 1);
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, r->new_secshare, r->sigmas, &r->thresh_pk, r->ids, r->u, r->new_id, r->n, (uint32_t)r->t, r->params_hashes[0], &r->new_pubshare) == 1);
|
|
}
|
|
|
|
static void frost_enrollment_test_full_run(frost_enrollment_test_run *r, size_t n, size_t t, size_t u, uint32_t new_id) {
|
|
frost_enrollment_test_deal(r, n, t, u, new_id);
|
|
frost_enrollment_test_round1_gen(r);
|
|
frost_enrollment_test_round1_agg(r);
|
|
frost_enrollment_test_round2(r);
|
|
}
|
|
|
|
/* Reconstructs the threshold secret from the shares of the given identifiers
|
|
* and checks it against the threshold public key. shares[k] must be the share
|
|
* of ids[k]. */
|
|
static void frost_enrollment_test_check_reconstruction(const uint32_t *ids, const unsigned char *const *shares, size_t n_ids, const secp256k1_pubkey *thresh_pk) {
|
|
secp256k1_scalar secret, share, lambda;
|
|
secp256k1_ge pk, expected;
|
|
secp256k1_gej pkj;
|
|
size_t i;
|
|
|
|
secp256k1_scalar_set_int(&secret, 0);
|
|
for (i = 0; i < n_ids; i++) {
|
|
CHECK(secp256k1_frost_derive_interpolating_value(&lambda, ids, n_ids, ids[i]) == 1);
|
|
CHECK(secp256k1_scalar_set_b32_seckey(&share, shares[i]) == 1);
|
|
secp256k1_scalar_mul(&share, &share, &lambda);
|
|
secp256k1_scalar_add(&secret, &secret, &share);
|
|
}
|
|
CHECK(!secp256k1_scalar_is_zero(&secret));
|
|
secp256k1_ecmult_gen_gej(&CTX->ecmult_gen_ctx, &pkj, &secret);
|
|
secp256k1_ge_set_gej(&pk, &pkj);
|
|
CHECK(secp256k1_pubkey_load(CTX, &expected, thresh_pk) == 1);
|
|
CHECK(secp256k1_ge_eq_var(&pk, &expected) == 1);
|
|
}
|
|
|
|
/* Produces and verifies a BIP340 signature with the given signer set. shares
|
|
* and pubshares must be aligned with ids. */
|
|
static void frost_enrollment_test_sign(const uint32_t *ids, const unsigned char *const *shares, const secp256k1_pubkey *pubshares, size_t n_signers, size_t n_participants, size_t threshold, const secp256k1_pubkey *thresh_pk) {
|
|
secp256k1_frost_tweak_cache cache;
|
|
secp256k1_frost_secnonce secnonces[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
secp256k1_frost_pubnonce pubnonces[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
const secp256k1_frost_pubnonce *pubnonce_ptrs[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
secp256k1_frost_partial_sig partial_sigs[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
const secp256k1_frost_partial_sig *partial_sig_ptrs[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
secp256k1_frost_aggnonce aggnonce;
|
|
secp256k1_frost_session session;
|
|
secp256k1_xonly_pubkey tweaked_pk;
|
|
unsigned char tweaked_pk32[32];
|
|
unsigned char msg[32];
|
|
unsigned char sig64[64];
|
|
size_t i;
|
|
|
|
testrand256(msg);
|
|
CHECK(secp256k1_frost_tweak_cache_init(CTX, &cache, thresh_pk) == 1);
|
|
CHECK(secp256k1_frost_tweaked_pubkey_get(CTX, &tweaked_pk, &cache) == 1);
|
|
CHECK(secp256k1_xonly_pubkey_serialize(CTX, tweaked_pk32, &tweaked_pk) == 1);
|
|
|
|
for (i = 0; i < n_signers; i++) {
|
|
unsigned char secrand[32];
|
|
testrand256(secrand);
|
|
CHECK(secp256k1_frost_nonce_gen(CTX, &secnonces[i], &pubnonces[i], secrand, shares[i], &pubshares[i], tweaked_pk32, msg, sizeof(msg), NULL, 0) == 1);
|
|
pubnonce_ptrs[i] = &pubnonces[i];
|
|
}
|
|
CHECK(secp256k1_frost_nonce_agg(CTX, &aggnonce, NULL, pubnonce_ptrs, n_signers) == 1);
|
|
CHECK(secp256k1_frost_session_init(CTX, &session, &aggnonce, ids, pubshares, n_signers, n_participants, (uint32_t)threshold, &cache, msg, sizeof(msg)) == 1);
|
|
for (i = 0; i < n_signers; i++) {
|
|
CHECK(secp256k1_frost_sign(CTX, &partial_sigs[i], &secnonces[i], shares[i], &session, ids, pubshares, n_signers, ids[i]) == 1);
|
|
CHECK(secp256k1_frost_partial_sig_verify(CTX, &partial_sigs[i], &pubnonces[i], &pubshares[i], &session, ids, n_signers, i) == 1);
|
|
partial_sig_ptrs[i] = &partial_sigs[i];
|
|
}
|
|
CHECK(secp256k1_frost_partial_sig_agg(CTX, sig64, NULL, &session, partial_sig_ptrs, n_signers) == 1);
|
|
CHECK(secp256k1_schnorrsig_verify(CTX, sig64, msg, sizeof(msg), &tweaked_pk) == 1);
|
|
}
|
|
|
|
/* PoC test_generate_frost_share: a 2-of-3 group grows to 2-of-4, and the new
|
|
* share sits on the same polynomial as the old ones. Every threshold-sized
|
|
* subset containing the new participant reconstructs the original threshold
|
|
* secret, and so does the untouched original pair. */
|
|
static void run_frost_enrollment_reconstruction_test(void) {
|
|
frost_enrollment_test_run r;
|
|
const unsigned char *shares[2];
|
|
uint32_t ids[2];
|
|
size_t i;
|
|
|
|
frost_enrollment_test_full_run(&r, 3, 2, 2, 3);
|
|
|
|
for (i = 0; i < 3; i++) {
|
|
ids[0] = (uint32_t)i;
|
|
ids[1] = 3;
|
|
shares[0] = r.secshares[i];
|
|
shares[1] = r.new_secshare;
|
|
frost_enrollment_test_check_reconstruction(ids, shares, 2, &r.thresh_pk);
|
|
}
|
|
ids[0] = 0;
|
|
ids[1] = 1;
|
|
shares[0] = r.secshares[0];
|
|
shares[1] = r.secshares[1];
|
|
frost_enrollment_test_check_reconstruction(ids, shares, 2, &r.thresh_pk);
|
|
}
|
|
|
|
/* PoC test_sign: a real BIP340 signature from a signer set that includes the
|
|
* enrolled participant, over the unchanged threshold public key. Also the
|
|
* n -> n+1 bookkeeping: the extended public share table must still satisfy
|
|
* secp256k1_frost_threshold_info_validate at n+1. */
|
|
static void run_frost_enrollment_signing_test(void) {
|
|
frost_enrollment_test_run r;
|
|
secp256k1_pubkey pubshares[4];
|
|
const unsigned char *shares[2];
|
|
uint32_t ids[2];
|
|
size_t i;
|
|
|
|
frost_enrollment_test_full_run(&r, 3, 2, 2, 3);
|
|
|
|
/* The extended table: the three original public shares plus the derived
|
|
* one at the new identifier. */
|
|
for (i = 0; i < 3; i++) {
|
|
pubshares[i] = r.pubshares[i];
|
|
}
|
|
pubshares[3] = r.new_pubshare;
|
|
CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, pubshares, 4, 2) == 1);
|
|
|
|
/* Signer set {2, 3}: one original participant and the new one. */
|
|
ids[0] = 2;
|
|
ids[1] = 3;
|
|
shares[0] = r.secshares[2];
|
|
shares[1] = r.new_secshare;
|
|
{
|
|
secp256k1_pubkey signer_pubshares[2];
|
|
signer_pubshares[0] = pubshares[2];
|
|
signer_pubshares[1] = pubshares[3];
|
|
frost_enrollment_test_sign(ids, shares, signer_pubshares, 2, 4, 2, &r.thresh_pk);
|
|
}
|
|
}
|
|
|
|
/* Repair mode: participant 1 "loses" its share and the same protocol run at
|
|
* new_id = 1 reproduces it, byte for byte. The share is f(x_1), a fixed value,
|
|
* not a fresh random one, so anything short of exact equality is a bug. */
|
|
static void run_frost_enrollment_repair_test(void) {
|
|
frost_enrollment_test_run r;
|
|
|
|
frost_enrollment_test_full_run(&r, 3, 2, 2, 1);
|
|
/* The helper set is {0, 2}: deal() skips the target identifier. */
|
|
CHECK(r.ids[0] == 0);
|
|
CHECK(r.ids[1] == 2);
|
|
CHECK(secp256k1_memcmp_var(r.new_secshare, r.secshares[1], 32) == 0);
|
|
/* And the repaired participant keeps its old public share. */
|
|
CHECK(secp256k1_memcmp_var(&r.new_pubshare, &r.pubshares[1], sizeof(r.new_pubshare)) == 0);
|
|
}
|
|
|
|
/* An oversized helper set produces the same share: Lagrange interpolation at
|
|
* the target is exact for any u >= t points on a degree-(t-1) polynomial. */
|
|
static void run_frost_enrollment_oversized_set_test(void) {
|
|
frost_enrollment_test_run r2, r3;
|
|
unsigned char share_u2[32];
|
|
|
|
/* Enroll id 3 into a 2-of-3 group with two helpers, then with all three,
|
|
* from the same dealt key material. */
|
|
frost_enrollment_test_full_run(&r2, 3, 2, 2, 3);
|
|
memcpy(share_u2, r2.new_secshare, 32);
|
|
|
|
r3 = r2;
|
|
r3.u = 3;
|
|
r3.ids[2] = 2;
|
|
frost_enrollment_test_round1_gen(&r3);
|
|
frost_enrollment_test_round1_agg(&r3);
|
|
frost_enrollment_test_round2(&r3);
|
|
CHECK(secp256k1_memcmp_var(share_u2, r3.new_secshare, 32) == 0);
|
|
/* The derived public share does not depend on the helper set either. */
|
|
CHECK(secp256k1_memcmp_var(&r2.new_pubshare, &r3.new_pubshare, sizeof(r2.new_pubshare)) == 0);
|
|
}
|
|
|
|
/* A corrupted sigma value must be caught by the public-share check, and the
|
|
* output must be wiped rather than left holding a wrong share. Tampered
|
|
* pubshares are caught earlier, by the validation step the recommended flow
|
|
* runs before the protocol starts. */
|
|
static void run_frost_enrollment_fault_injection_test(void) {
|
|
frost_enrollment_test_run r;
|
|
unsigned char sigmas[2 * 32];
|
|
unsigned char out[32];
|
|
secp256k1_pubkey tampered[3];
|
|
size_t i;
|
|
|
|
/* A clean run first, so that r.new_secshare holds the share the corrupted
|
|
* runs below must fail to reproduce. */
|
|
frost_enrollment_test_full_run(&r, 3, 2, 2, 3);
|
|
|
|
for (i = 0; i < 2; i++) {
|
|
memcpy(sigmas, r.sigmas, sizeof(sigmas));
|
|
sigmas[32 * i] ^= 1;
|
|
memset(out, 0xff, sizeof(out));
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, r.params_hashes[0], &r.new_pubshare) == 0);
|
|
CHECK(secp256k1_is_zero_array(out, sizeof(out)));
|
|
/* Without the public-share check nothing notices: the sum is a
|
|
* perfectly well-formed scalar, just the wrong one. This is what
|
|
* makes expected_pubshare load-bearing rather than optional. */
|
|
memset(out, 0xff, sizeof(out));
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, r.params_hashes[0], NULL) == 1);
|
|
CHECK(secp256k1_memcmp_var(out, r.new_secshare, 32) != 0);
|
|
}
|
|
|
|
/* The recommended flow, not just the module: a tampered public share is
|
|
* rejected by secp256k1_frost_threshold_info_validate against the
|
|
* independently authenticated threshold public key, before enrollment
|
|
* begins. */
|
|
for (i = 0; i < 3; i++) {
|
|
unsigned char ser[33];
|
|
size_t len = sizeof(ser);
|
|
memcpy(tampered, r.pubshares, sizeof(tampered));
|
|
CHECK(secp256k1_ec_pubkey_serialize(CTX, ser, &len, &tampered[i], SECP256K1_EC_COMPRESSED) == 1);
|
|
/* Flip to the other point of the same x-coordinate: still a valid
|
|
* pubkey, but no longer on the group's polynomial. */
|
|
ser[0] ^= 1;
|
|
CHECK(secp256k1_ec_pubkey_parse(CTX, &tampered[i], ser, len) == 1);
|
|
CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, tampered, 3, 2) == 0);
|
|
}
|
|
}
|
|
|
|
/* Parameter and group agreement, from four angles. */
|
|
static void run_frost_enrollment_mismatch_test(void) {
|
|
frost_enrollment_test_run r, other;
|
|
unsigned char all_shares[2 * 32];
|
|
unsigned char received[2 * 32];
|
|
unsigned char sigma[32];
|
|
unsigned char out[32];
|
|
unsigned char bad_shares[2 * 32];
|
|
unsigned char bad_hash[32];
|
|
unsigned char good_hash[32];
|
|
unsigned char secrand[32];
|
|
uint32_t mismatch_id;
|
|
size_t j;
|
|
|
|
frost_enrollment_test_deal(&r, 4, 2, 2, 4);
|
|
|
|
/* (a) Helper 0 runs round 1.1 for a different target. Helper 1's round
|
|
* 1.2 must abort and name helper 0 by IDENTIFIER. */
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, good_hash, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t) == 1);
|
|
testrand256(secrand);
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, bad_shares, bad_hash, secrand, r.secshares[0], &r.thresh_pk, r.ids, r.u, r.ids[0], 3, r.n, (uint32_t)r.t) == 1);
|
|
CHECK(secp256k1_memcmp_var(bad_hash, good_hash, 32) != 0);
|
|
frost_enrollment_test_round1_gen(&r);
|
|
CHECK(secp256k1_memcmp_var(r.params_hashes[0], good_hash, 32) == 0);
|
|
memcpy(r.params_hashes[0], bad_hash, 32);
|
|
memcpy(r.shares[0], bad_shares, sizeof(bad_shares));
|
|
|
|
frost_enrollment_test_collect(&r, 1, all_shares, received);
|
|
mismatch_id = 0;
|
|
memset(sigma, 0xff, sizeof(sigma));
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma, &mismatch_id, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 0);
|
|
CHECK(mismatch_id == r.ids[0]);
|
|
CHECK(secp256k1_is_zero_array(sigma, sizeof(sigma)));
|
|
|
|
/* (b) A caller that ignores the abort and finishes round 1.2 anyway still
|
|
* does not end up with a usable share: the public-share check catches the
|
|
* inconsistent sum. Defence in depth, rather than a test of the test's
|
|
* own control flow. */
|
|
for (j = 0; j < r.u; j++) {
|
|
size_t k;
|
|
frost_enrollment_test_collect(&r, j, all_shares, received);
|
|
/* Simulate the gate having passed: every helper is handed the hash it
|
|
* expects, while helper 0's mismatched delta values stay in place. */
|
|
for (k = 0; k < r.u; k++) {
|
|
if (k != j) {
|
|
memcpy(&received[32 * k], good_hash, 32);
|
|
}
|
|
}
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, &r.sigmas[32 * j], NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[j], r.new_id, r.n, (uint32_t)r.t) == 1);
|
|
}
|
|
{
|
|
secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
frost_enrollment_test_helper_pubshares(&r, helper_pubshares);
|
|
CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r.new_pubshare, helper_pubshares, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t) == 1);
|
|
}
|
|
memset(out, 0xff, sizeof(out));
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, good_hash, &r.new_pubshare) == 0);
|
|
CHECK(secp256k1_is_zero_array(out, sizeof(out)));
|
|
|
|
/* (c) The helpers agree with each other but not with the target: a clean
|
|
* run for new_id = 3, handed to a target that believes it is 4. Round 1.2
|
|
* passed everywhere; round 2's own recomputation is what catches it. */
|
|
frost_enrollment_test_full_run(&other, 4, 2, 2, 3);
|
|
memset(out, 0xff, sizeof(out));
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, other.sigmas, &other.thresh_pk, other.ids, other.u, 4, other.n, (uint32_t)other.t, other.params_hashes[0], NULL) == 0);
|
|
CHECK(secp256k1_is_zero_array(out, sizeof(out)));
|
|
|
|
/* (d) Group binding. Two groups with identical (t, n, ids, new_id) get
|
|
* different parameters hashes, because the hash commits to the threshold
|
|
* public key -- and a hash from one group fails round 1.2 in the other. */
|
|
{
|
|
frost_enrollment_test_run a, b;
|
|
unsigned char hash_a[32], hash_b[32];
|
|
|
|
frost_enrollment_test_deal(&a, 3, 2, 2, 3);
|
|
frost_enrollment_test_deal(&b, 3, 2, 2, 3);
|
|
CHECK(secp256k1_memcmp_var(&a.thresh_pk, &b.thresh_pk, sizeof(a.thresh_pk)) != 0);
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash_a, &a.thresh_pk, a.ids, a.u, a.new_id, a.n, (uint32_t)a.t) == 1);
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash_b, &b.thresh_pk, b.ids, b.u, b.new_id, b.n, (uint32_t)b.t) == 1);
|
|
CHECK(secp256k1_memcmp_var(hash_a, hash_b, 32) != 0);
|
|
|
|
frost_enrollment_test_round1_gen(&a);
|
|
frost_enrollment_test_collect(&a, 1, all_shares, received);
|
|
memcpy(&received[0], hash_b, 32);
|
|
mismatch_id = 0;
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma, &mismatch_id, all_shares, received, &a.thresh_pk, a.ids, a.u, a.ids[1], a.new_id, a.n, (uint32_t)a.t) == 0);
|
|
CHECK(mismatch_id == a.ids[0]);
|
|
}
|
|
}
|
|
|
|
/* The own slot of received_params_hashes32 is never read, so a caller cannot
|
|
* fill it with a received hash and launder a mismatch into a pass. */
|
|
static void run_frost_enrollment_own_slot_test(void) {
|
|
frost_enrollment_test_run r;
|
|
unsigned char all_shares[2 * 32];
|
|
unsigned char received[2 * 32];
|
|
unsigned char sigma_zero[32], sigma_garbage[32];
|
|
|
|
frost_enrollment_test_deal(&r, 3, 2, 2, 3);
|
|
frost_enrollment_test_round1_gen(&r);
|
|
|
|
/* Helper 1 aggregates with its own slot zero, as documented. */
|
|
frost_enrollment_test_collect(&r, 1, all_shares, received);
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma_zero, NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 1);
|
|
|
|
/* And again with garbage in that slot. Same result: it is not read. */
|
|
memset(&received[32], 0xa5, 32);
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma_garbage, NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 1);
|
|
CHECK(secp256k1_memcmp_var(sigma_zero, sigma_garbage, 32) == 0);
|
|
|
|
/* But a wrong hash in a slot that IS read still aborts, even if the same
|
|
* wrong hash sits in the own slot -- the own hash is recomputed, so there
|
|
* is nothing to agree with. */
|
|
memset(&received[0], 0xa5, 32);
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma_garbage, NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 0);
|
|
}
|
|
|
|
/* Invalid parameter tuples, including the two deliberate divergences from the
|
|
* frost module (threshold >= 2, and enrollment refused at n = 128). */
|
|
static void run_frost_enrollment_invalid_params_test(void) {
|
|
frost_enrollment_test_run r;
|
|
unsigned char hash32[32];
|
|
unsigned char shares[4 * 32];
|
|
unsigned char secrand[32];
|
|
uint32_t ids[4];
|
|
|
|
frost_enrollment_test_deal(&r, 4, 2, 3, 4);
|
|
|
|
/* The valid baseline. */
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 2) == 1);
|
|
|
|
/* Duplicate ids. */
|
|
memcpy(ids, r.ids, 3 * sizeof(ids[0]));
|
|
ids[2] = ids[0];
|
|
memset(hash32, 0xff, sizeof(hash32));
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, ids, 3, 4, 4, 2) == 0);
|
|
CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32)));
|
|
|
|
/* new_id among the helpers. */
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, r.ids[1], 4, 2) == 0);
|
|
|
|
/* new_id past the end: neither enrollment (== n) nor repair (< n). */
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 5, 4, 2) == 0);
|
|
|
|
/* Too few helpers, and more helpers than participants. */
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 1, 4, 4, 2) == 0);
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 2, 2) == 0);
|
|
|
|
/* threshold = 1 is refused, unlike in the frost module. */
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 1) == 0);
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 0) == 0);
|
|
/* threshold above the participant count. */
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 5) == 0);
|
|
|
|
/* A helper id outside 0..n-1. */
|
|
memcpy(ids, r.ids, 3 * sizeof(ids[0]));
|
|
ids[2] = 4;
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, ids, 3, 5, 4, 2) == 0);
|
|
|
|
/* n above the maximum, and n_ids above the maximum. Both must be caught
|
|
* in production builds; neither may ride on the VERIFY_CHECK inside
|
|
* secp256k1_frost_sort_ids. */
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, SECP256K1_FROST_MAX_PARTICIPANTS + 1, 2) == 0);
|
|
memset(hash32, 0xff, sizeof(hash32));
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, SECP256K1_FROST_MAX_PARTICIPANTS + 1, 4, 4, 2) == 0);
|
|
CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32)));
|
|
|
|
/* Mode-specific bounds at the maximum: enrollment would produce a
|
|
* 129-participant group and is refused; repair leaves n alone and is
|
|
* accepted. Both use a helper set of exactly t, so no oversized array is
|
|
* involved either way. */
|
|
{
|
|
uint32_t big_ids[2];
|
|
big_ids[0] = 0;
|
|
big_ids[1] = 1;
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, big_ids, 2, SECP256K1_FROST_MAX_PARTICIPANTS, SECP256K1_FROST_MAX_PARTICIPANTS, 2) == 0);
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, big_ids, 2, SECP256K1_FROST_MAX_PARTICIPANTS - 1, SECP256K1_FROST_MAX_PARTICIPANTS, 2) == 1);
|
|
/* One below the maximum, enrollment is fine again. */
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, big_ids, 2, SECP256K1_FROST_MAX_PARTICIPANTS - 1, SECP256K1_FROST_MAX_PARTICIPANTS - 1, 2) == 1);
|
|
}
|
|
|
|
/* An uninitialized threshold public key: secp256k1_pubkey_load treats
|
|
* that as an API misuse and fires the illegal-argument callback, as it
|
|
* does everywhere else in the library. */
|
|
{
|
|
secp256k1_pubkey zero_pk;
|
|
memset(&zero_pk, 0, sizeof(zero_pk));
|
|
CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_params_hash(CTX, hash32, &zero_pk, r.ids, 3, 4, 4, 2));
|
|
CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32)));
|
|
}
|
|
|
|
/* The other entry points reject the same tuples. */
|
|
testrand256(secrand);
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, 3, r.ids[0], 4, 4, 1) == 0);
|
|
CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand)));
|
|
/* my_id must be one of the helpers. */
|
|
testrand256(secrand);
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, 3, 3, 4, 4, 2) == 0);
|
|
{
|
|
secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
frost_enrollment_test_helper_pubshares(&r, helper_pubshares);
|
|
CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r.new_pubshare, helper_pubshares, r.ids, 3, 4, 4, 1) == 0);
|
|
}
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, shares, r.sigmas, &r.thresh_pk, r.ids, 3, 4, 4, 1, NULL, NULL) == 0);
|
|
}
|
|
|
|
/* Every entry point must reject an empty helper set and leave its output
|
|
* zeroed. This also checks that all five symbols are reachable from the test
|
|
* binary. */
|
|
static void run_frost_enrollment_rejects_empty_set_test(void) {
|
|
secp256k1_pubkey pk;
|
|
secp256k1_pubkey pubshare;
|
|
unsigned char buf32[32];
|
|
unsigned char secrand32[32];
|
|
unsigned char hash32[32];
|
|
uint32_t ids[1] = { 0 };
|
|
|
|
memset(&pk, 0, sizeof(pk));
|
|
memset(&pubshare, 0xff, sizeof(pubshare));
|
|
memset(secrand32, 0x11, sizeof(secrand32));
|
|
|
|
memset(buf32, 0xff, sizeof(buf32));
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, buf32, &pk, ids, 0, 0, 1, 2) == 0);
|
|
CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32)));
|
|
|
|
memset(buf32, 0xff, sizeof(buf32));
|
|
memset(hash32, 0xff, sizeof(hash32));
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, buf32, hash32, secrand32, buf32, &pk, ids, 0, 0, 1, 1, 2) == 0);
|
|
CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32)));
|
|
|
|
memset(buf32, 0xff, sizeof(buf32));
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, buf32, NULL, buf32, hash32, &pk, ids, 0, 0, 1, 1, 2) == 0);
|
|
CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32)));
|
|
|
|
CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, &pk, ids, 0, 0, 1, 2) == 0);
|
|
CHECK(secp256k1_is_zero_array((unsigned char *)&pubshare, sizeof(pubshare)));
|
|
|
|
memset(buf32, 0xff, sizeof(buf32));
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, buf32, buf32, &pk, ids, 0, 0, 1, 2, NULL, NULL) == 0);
|
|
CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32)));
|
|
}
|
|
|
|
/* pubshare_derive is a wrapper over frost's derive_pubshare_at. This pins its
|
|
* argument plumbing: at an existing participant's identifier it must return
|
|
* exactly that participant's public share, and it must agree with a direct
|
|
* call to the function it wraps. */
|
|
static void run_frost_enrollment_pubshare_derive_test(void) {
|
|
frost_enrollment_test_run r;
|
|
secp256k1_pubkey derived, aligned[3];
|
|
secp256k1_ge points[3], expected, got;
|
|
secp256k1_gej resultj;
|
|
secp256k1_scalar x;
|
|
uint32_t ids[3];
|
|
size_t i, k;
|
|
|
|
frost_enrollment_test_deal(&r, 4, 3, 3, 4);
|
|
|
|
/* Repair mode at every existing identifier: the derived public share is
|
|
* the one the dealer produced. */
|
|
for (i = 0; i < 4; i++) {
|
|
k = 0;
|
|
{
|
|
size_t m;
|
|
for (m = 0; m < 4 && k < 3; m++) {
|
|
if (m != i) {
|
|
ids[k] = (uint32_t)m;
|
|
k++;
|
|
}
|
|
}
|
|
}
|
|
for (k = 0; k < 3; k++) {
|
|
aligned[k] = r.pubshares[ids[k]];
|
|
}
|
|
CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &derived, aligned, ids, 3, (uint32_t)i, 4, 3) == 1);
|
|
CHECK(secp256k1_pubkey_load(CTX, &expected, &r.pubshares[i]) == 1);
|
|
CHECK(secp256k1_pubkey_load(CTX, &got, &derived) == 1);
|
|
CHECK(secp256k1_ge_eq_var(&expected, &got) == 1);
|
|
}
|
|
|
|
/* And the wrapper passes the target identifier through unchanged. */
|
|
ids[0] = 0;
|
|
ids[1] = 1;
|
|
ids[2] = 2;
|
|
for (k = 0; k < 3; k++) {
|
|
aligned[k] = r.pubshares[ids[k]];
|
|
}
|
|
CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &derived, aligned, ids, 3, 4, 4, 3) == 1);
|
|
for (i = 0; i < 3; i++) {
|
|
CHECK(secp256k1_pubkey_load(CTX, &points[i], &r.pubshares[i]) == 1);
|
|
}
|
|
secp256k1_scalar_set_int(&x, 4);
|
|
CHECK(secp256k1_frost_derive_pubshare_at(&resultj, ids, points, 3, &x) == 1);
|
|
secp256k1_ge_set_gej_var(&expected, &resultj);
|
|
CHECK(secp256k1_pubkey_load(CTX, &got, &derived) == 1);
|
|
CHECK(secp256k1_ge_eq_var(&expected, &got) == 1);
|
|
}
|
|
|
|
/* One random (t, n, u) round trip. Each helper is given the identifier set in
|
|
* its own random order, which must not change the parameters hash -- while the
|
|
* delta buffers stay aligned with whatever order that helper used. */
|
|
static void frost_enrollment_random_iteration(void) {
|
|
frost_enrollment_test_run r;
|
|
uint32_t perm[SECP256K1_FROST_MAX_PARTICIPANTS][SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
unsigned char hash32[32];
|
|
size_t n, t, u, i, j;
|
|
uint32_t new_id;
|
|
|
|
/* 2 <= t <= u <= n <= 7 */
|
|
t = 2 + testrand_int(3);
|
|
u = t + testrand_int(4);
|
|
n = u + testrand_int(8 - (unsigned int)u);
|
|
if (n > 7) {
|
|
n = 7;
|
|
}
|
|
if (u > n) {
|
|
u = n;
|
|
}
|
|
/* Enrollment half the time, repair the other half. */
|
|
new_id = testrand_bits(1) ? (uint32_t)n : (uint32_t)testrand_int((unsigned int)n);
|
|
if ((size_t)new_id < n && u > n - 1) {
|
|
u = n - 1;
|
|
}
|
|
if (u < t) {
|
|
return;
|
|
}
|
|
|
|
frost_enrollment_test_deal(&r, n, t, u, new_id);
|
|
|
|
/* Give every helper its own shuffled view of the identifier set. */
|
|
for (i = 0; i < u; i++) {
|
|
memcpy(perm[i], r.ids, u * sizeof(r.ids[0]));
|
|
for (j = u; j > 1; j--) {
|
|
size_t k = testrand_int((unsigned int)j);
|
|
uint32_t tmp = perm[i][j - 1];
|
|
perm[i][j - 1] = perm[i][k];
|
|
perm[i][k] = tmp;
|
|
}
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, perm[i], u, new_id, n, (uint32_t)t) == 1);
|
|
}
|
|
|
|
/* Round 1.1 in each helper's own order. */
|
|
for (i = 0; i < u; i++) {
|
|
unsigned char secrand[32];
|
|
testrand256(secrand);
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, r.shares[i], r.params_hashes[i], secrand, r.secshares[r.ids[i]], &r.thresh_pk, perm[i], u, r.ids[i], new_id, n, (uint32_t)t) == 1);
|
|
/* Order-independent: same digest as the canonical order. */
|
|
CHECK(secp256k1_memcmp_var(r.params_hashes[i], hash32, 32) == 0);
|
|
}
|
|
|
|
/* Round 1.2, translating each helper's alignment into the canonical one.
|
|
* all_shares[k] must be what helper ids[k] produced for helper ids[j],
|
|
* which sits at helper k's own position for ids[j]. */
|
|
for (j = 0; j < u; j++) {
|
|
unsigned char all_shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
unsigned char received[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
|
|
memset(received, 0, u * 32);
|
|
for (i = 0; i < u; i++) {
|
|
size_t pos;
|
|
for (pos = 0; pos < u; pos++) {
|
|
if (perm[i][pos] == r.ids[j]) {
|
|
break;
|
|
}
|
|
}
|
|
CHECK(pos < u);
|
|
memcpy(&all_shares[32 * i], &r.shares[i][32 * pos], 32);
|
|
if (i != j) {
|
|
memcpy(&received[32 * i], r.params_hashes[i], 32);
|
|
}
|
|
}
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, &r.sigmas[32 * j], NULL, all_shares, received, &r.thresh_pk, r.ids, u, r.ids[j], new_id, n, (uint32_t)t) == 1);
|
|
}
|
|
|
|
{
|
|
secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
frost_enrollment_test_helper_pubshares(&r, helper_pubshares);
|
|
CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r.new_pubshare, helper_pubshares, r.ids, u, new_id, n, (uint32_t)t) == 1);
|
|
}
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, r.new_secshare, r.sigmas, &r.thresh_pk, r.ids, u, new_id, n, (uint32_t)t, hash32, &r.new_pubshare) == 1);
|
|
|
|
if ((size_t)new_id < n) {
|
|
/* Repair reproduces the lost share exactly. */
|
|
CHECK(secp256k1_memcmp_var(r.new_secshare, r.secshares[new_id], 32) == 0);
|
|
} else {
|
|
/* Enrollment: every t-subset of the extended group that contains the
|
|
* new participant reconstructs the same threshold secret, and the
|
|
* extended public share table still validates at n+1. */
|
|
secp256k1_pubkey extended[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
uint32_t sub_ids[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
const unsigned char *sub_shares[SECP256K1_FROST_MAX_PARTICIPANTS];
|
|
|
|
for (i = 0; i < n; i++) {
|
|
extended[i] = r.pubshares[i];
|
|
}
|
|
extended[n] = r.new_pubshare;
|
|
CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, extended, n + 1, (uint32_t)t) == 1);
|
|
|
|
for (i = 0; i + t <= n + 1; i++) {
|
|
for (j = 0; j + 1 < t; j++) {
|
|
sub_ids[j] = (uint32_t)(i + j);
|
|
sub_shares[j] = r.secshares[i + j];
|
|
}
|
|
sub_ids[t - 1] = new_id;
|
|
sub_shares[t - 1] = r.new_secshare;
|
|
if (sub_ids[t - 2] >= new_id) {
|
|
continue;
|
|
}
|
|
frost_enrollment_test_check_reconstruction(sub_ids, sub_shares, t, &r.thresh_pk);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* The frozen regression vectors. They pin the two tag strings, the parameters
|
|
* hash encoding and the share-splitting derivation: any change to those is a
|
|
* vector-breaking change, and this is where it shows up. */
|
|
static void run_frost_enrollment_vectors_test(void) {
|
|
size_t c;
|
|
|
|
for (c = 0; c < sizeof(frost_enrollment_vec_cases) / sizeof(frost_enrollment_vec_cases[0]); c++) {
|
|
const struct frost_enrollment_vec_case *v = &frost_enrollment_vec_cases[c];
|
|
secp256k1_pubkey thresh_pk, pubshares[SECP256K1_FROST_MAX_PARTICIPANTS], new_pubshare;
|
|
unsigned char hash32[32];
|
|
unsigned char shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
unsigned char all_shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
unsigned char received[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
unsigned char sigmas[SECP256K1_FROST_MAX_PARTICIPANTS * 32];
|
|
unsigned char secshare[32];
|
|
unsigned char secrand[32];
|
|
unsigned char ser[33];
|
|
size_t len;
|
|
size_t i, j;
|
|
|
|
CHECK(secp256k1_ec_pubkey_parse(CTX, &thresh_pk, v->thresh_pk33, 33) == 1);
|
|
for (i = 0; i < v->n_ids; i++) {
|
|
CHECK(secp256k1_ec_pubkey_parse(CTX, &pubshares[i], v->pubshares33[i], 33) == 1);
|
|
}
|
|
|
|
/* The parameters hash. */
|
|
CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &thresh_pk, v->ids, v->n_ids, v->new_id, v->n_participants, v->threshold) == 1);
|
|
CHECK(secp256k1_memcmp_var(hash32, v->params_hash32, 32) == 0);
|
|
|
|
/* Round 1.1 for every helper, from the frozen seeds. */
|
|
for (i = 0; i < v->n_ids; i++) {
|
|
memcpy(secrand, v->session_secrand32[i], 32);
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, v->secshares32[i], &thresh_pk, v->ids, v->n_ids, v->ids[i], v->new_id, v->n_participants, v->threshold) == 1);
|
|
CHECK(secp256k1_memcmp_var(hash32, v->params_hash32, 32) == 0);
|
|
CHECK(secp256k1_memcmp_var(shares, v->shares32[i], v->n_ids * 32) == 0);
|
|
}
|
|
|
|
/* Round 1.2 for every helper. */
|
|
for (j = 0; j < v->n_ids; j++) {
|
|
memset(received, 0, v->n_ids * 32);
|
|
for (i = 0; i < v->n_ids; i++) {
|
|
memcpy(&all_shares[32 * i], &v->shares32[i][32 * j], 32);
|
|
if (i != j) {
|
|
memcpy(&received[32 * i], v->params_hash32, 32);
|
|
}
|
|
}
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, &sigmas[32 * j], NULL, all_shares, received, &thresh_pk, v->ids, v->n_ids, v->ids[j], v->new_id, v->n_participants, v->threshold) == 1);
|
|
}
|
|
CHECK(secp256k1_memcmp_var(sigmas, v->sigmas32, v->n_ids * 32) == 0);
|
|
|
|
/* The derived public share and round 2. */
|
|
CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &new_pubshare, pubshares, v->ids, v->n_ids, v->new_id, v->n_participants, v->threshold) == 1);
|
|
len = sizeof(ser);
|
|
CHECK(secp256k1_ec_pubkey_serialize(CTX, ser, &len, &new_pubshare, SECP256K1_EC_COMPRESSED) == 1);
|
|
CHECK(len == 33);
|
|
CHECK(secp256k1_memcmp_var(ser, v->new_pubshare33, 33) == 0);
|
|
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, secshare, sigmas, &thresh_pk, v->ids, v->n_ids, v->new_id, v->n_participants, v->threshold, v->params_hash32, &new_pubshare) == 1);
|
|
CHECK(secp256k1_memcmp_var(secshare, v->new_secshare32, 32) == 0);
|
|
}
|
|
}
|
|
|
|
/* COUNT iterations of the above, so that -i scales the fuzzing the way it does
|
|
* for the iceberg module's randomized loops. */
|
|
static void run_frost_enrollment_random_test(void) {
|
|
int i;
|
|
|
|
for (i = 0; i < COUNT; i++) {
|
|
frost_enrollment_random_iteration();
|
|
}
|
|
}
|
|
|
|
/* Contract details that are easy to regress and that a caller can reasonably
|
|
* depend on. */
|
|
static void run_frost_enrollment_contract_test(void) {
|
|
frost_enrollment_test_run r;
|
|
unsigned char guarded[4 * 32];
|
|
unsigned char hash32[32];
|
|
unsigned char secrand[32];
|
|
unsigned char sigma[32];
|
|
unsigned char out[32];
|
|
unsigned char all_shares[2 * 32];
|
|
unsigned char received[2 * 32];
|
|
uint32_t mismatch_id;
|
|
size_t i;
|
|
|
|
/* An out-of-range n_ids must be rejected WITHOUT writing shares32_out,
|
|
* whose size is only u*32 by contract: a caller passing a fixed buffer
|
|
* and a bad count would otherwise have memory past it zeroed. The seed is
|
|
* still consumed, so a failed call cannot be retried on it. */
|
|
frost_enrollment_test_deal(&r, 3, 2, 2, 3);
|
|
memset(guarded, 0xa5, sizeof(guarded));
|
|
testrand256(secrand);
|
|
memset(hash32, 0xff, sizeof(hash32));
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, guarded, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, SECP256K1_FROST_MAX_PARTICIPANTS + 1, r.ids[0], r.new_id, r.n, (uint32_t)r.t) == 0);
|
|
for (i = 0; i < sizeof(guarded); i++) {
|
|
CHECK(guarded[i] == 0xa5);
|
|
}
|
|
CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32)));
|
|
CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand)));
|
|
|
|
/* The same holds for an n_ids that is merely inconsistent with the rest
|
|
* of the tuple rather than out of the absolute range. */
|
|
memset(guarded, 0xa5, sizeof(guarded));
|
|
testrand256(secrand);
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, guarded, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, 1, r.ids[0], r.new_id, r.n, (uint32_t)r.t) == 0);
|
|
for (i = 0; i < sizeof(guarded); i++) {
|
|
CHECK(guarded[i] == 0xa5);
|
|
}
|
|
CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand)));
|
|
|
|
/* A valid call does zero the buffer it is allowed to write, and only
|
|
* that part of it. */
|
|
memset(guarded, 0xa5, sizeof(guarded));
|
|
testrand256(secrand);
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, guarded, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t) == 1);
|
|
for (i = r.u * 32; i < sizeof(guarded); i++) {
|
|
CHECK(guarded[i] == 0xa5);
|
|
}
|
|
|
|
/* secshare_gen needs a signing-capable context only when it is going to
|
|
* check the public share. With expected_pubshare == NULL it must work on
|
|
* the static context; with a public share it is API misuse there. */
|
|
frost_enrollment_test_full_run(&r, 3, 2, 2, 3);
|
|
memset(out, 0xff, sizeof(out));
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(STATIC_CTX, out, r.sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, r.params_hashes[0], NULL) == 1);
|
|
CHECK(secp256k1_memcmp_var(out, r.new_secshare, 32) == 0);
|
|
CHECK_ILLEGAL(STATIC_CTX, secp256k1_frost_enrollment_secshare_gen(STATIC_CTX, out, r.sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, r.params_hashes[0], &r.new_pubshare));
|
|
|
|
/* Both optional checks are genuinely optional: skipping either, or both,
|
|
* still produces the same share on an honest run. */
|
|
memset(out, 0xff, sizeof(out));
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, &r.new_pubshare) == 1);
|
|
CHECK(secp256k1_memcmp_var(out, r.new_secshare, 32) == 0);
|
|
memset(out, 0xff, sizeof(out));
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL) == 1);
|
|
CHECK(secp256k1_memcmp_var(out, r.new_secshare, 32) == 0);
|
|
|
|
/* share_agg reports a share that is not a valid scalar the same way it
|
|
* reports a parameters disagreement: by naming the responsible helper.
|
|
* The helper set here is {0, 2}, so an implementation returning the array
|
|
* index rather than the identifier would be caught. */
|
|
frost_enrollment_test_deal(&r, 3, 2, 2, 1);
|
|
CHECK(r.ids[0] == 0 && r.ids[1] == 2);
|
|
frost_enrollment_test_round1_gen(&r);
|
|
frost_enrollment_test_collect(&r, 1, all_shares, received);
|
|
/* All-ones is larger than the group order. */
|
|
memset(&all_shares[0], 0xff, 32);
|
|
mismatch_id = 0;
|
|
memset(sigma, 0xff, sizeof(sigma));
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma, &mismatch_id, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 0);
|
|
CHECK(mismatch_id == r.ids[0]);
|
|
CHECK(mismatch_id == 0);
|
|
CHECK(secp256k1_is_zero_array(sigma, sizeof(sigma)));
|
|
|
|
/* And at the caller's OWN slot, which the header calls out: the kept
|
|
* share is summed along with the rest, so the caller can be named. */
|
|
frost_enrollment_test_collect(&r, 1, all_shares, received);
|
|
memset(&all_shares[32], 0xff, 32);
|
|
mismatch_id = 0;
|
|
CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma, &mismatch_id, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 0);
|
|
CHECK(mismatch_id == r.ids[1]);
|
|
CHECK(mismatch_id == 2);
|
|
|
|
/* An out-of-range sigma is rejected by secshare_gen too, which has no
|
|
* attribution to offer. */
|
|
frost_enrollment_test_full_run(&r, 3, 2, 2, 3);
|
|
{
|
|
unsigned char sigmas[2 * 32];
|
|
memcpy(sigmas, r.sigmas, sizeof(sigmas));
|
|
memset(&sigmas[32], 0xff, 32);
|
|
memset(out, 0xff, sizeof(out));
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, r.params_hashes[0], &r.new_pubshare) == 0);
|
|
CHECK(secp256k1_is_zero_array(out, sizeof(out)));
|
|
}
|
|
|
|
/* Sigmas summing to zero mod the group order are rejected: a zero share
|
|
* is not a usable secret key. secshare = 1 + (order - 1). */
|
|
{
|
|
unsigned char sigmas[2 * 32];
|
|
static const unsigned char order_minus_one[32] = {
|
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
|
|
0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B,
|
|
0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x40
|
|
};
|
|
memset(sigmas, 0, sizeof(sigmas));
|
|
sigmas[31] = 1;
|
|
memcpy(&sigmas[32], order_minus_one, 32);
|
|
memset(out, 0xff, sizeof(out));
|
|
CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL) == 0);
|
|
CHECK(secp256k1_is_zero_array(out, sizeof(out)));
|
|
}
|
|
|
|
/* shares_gen rejects a secret share that is not a valid secret key. */
|
|
{
|
|
unsigned char bad_secshare[32];
|
|
memset(bad_secshare, 0, sizeof(bad_secshare));
|
|
testrand256(secrand);
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, guarded, hash32, secrand, bad_secshare, &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t) == 0);
|
|
CHECK(secp256k1_is_zero_array(guarded, r.u * 32));
|
|
CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand)));
|
|
memset(bad_secshare, 0xff, sizeof(bad_secshare));
|
|
testrand256(secrand);
|
|
CHECK(secp256k1_frost_enrollment_shares_gen(CTX, guarded, hash32, secrand, bad_secshare, &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t) == 0);
|
|
CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand)));
|
|
}
|
|
}
|
|
|
|
static const struct tf_test_entry tests_frost_enrollment[] = {
|
|
CASE1(run_frost_enrollment_vectors_test),
|
|
CASE1(run_frost_enrollment_reconstruction_test),
|
|
CASE1(run_frost_enrollment_signing_test),
|
|
CASE1(run_frost_enrollment_repair_test),
|
|
CASE1(run_frost_enrollment_oversized_set_test),
|
|
CASE1(run_frost_enrollment_fault_injection_test),
|
|
CASE1(run_frost_enrollment_mismatch_test),
|
|
CASE1(run_frost_enrollment_own_slot_test),
|
|
CASE1(run_frost_enrollment_contract_test),
|
|
CASE1(run_frost_enrollment_invalid_params_test),
|
|
CASE1(run_frost_enrollment_rejects_empty_set_test),
|
|
CASE1(run_frost_enrollment_pubshare_derive_test),
|
|
CASE1(run_frost_enrollment_random_test),
|
|
};
|
|
|
|
#endif /* SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H */
|