/*********************************************************************** * 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); } } /* Re-points an already-dealt run at a different helper set, so that two runs * can be compared over identical key material without copying the (large) * run struct or hand-editing its fields. */ static void frost_enrollment_test_use_helpers(frost_enrollment_test_run *r, const uint32_t *ids, size_t u) { CHECK(u > 0 && u <= SECP256K1_FROST_MAX_PARTICIPANTS); memcpy(r->ids, ids, u * sizeof(*ids)); r->u = u; } /* 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 r; unsigned char share_u2[32]; secp256k1_pubkey pubshare_u2; static const uint32_t helpers_u2[2] = { 0, 1 }; static const uint32_t helpers_u3[3] = { 0, 1, 2 }; /* Enroll id 3 into a 2-of-3 group with two helpers, then with all three, * over the same dealt key material. */ frost_enrollment_test_deal(&r, 3, 2, 2, 3); frost_enrollment_test_use_helpers(&r, helpers_u2, 2); frost_enrollment_test_round1_gen(&r); frost_enrollment_test_round1_agg(&r); frost_enrollment_test_round2(&r); memcpy(share_u2, r.new_secshare, 32); pubshare_u2 = r.new_pubshare; frost_enrollment_test_use_helpers(&r, helpers_u3, 3); frost_enrollment_test_round1_gen(&r); frost_enrollment_test_round1_agg(&r); frost_enrollment_test_round2(&r); CHECK(secp256k1_memcmp_var(share_u2, r.new_secshare, 32) == 0); /* The derived public share does not depend on the helper set either. */ CHECK(secp256k1_memcmp_var(&pubshare_u2, &r.new_pubshare, sizeof(pubshare_u2)) == 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) { /* Static rather than automatic: this struct is ~540 KB and this test * needs several of them live at once. */ static 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 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 believing the target is 3 while helper 1 * believes it is 4 -- the disagreement enters where it would in reality, * at the round 1.1 call, rather than being patched in afterwards. * 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, r.shares[0], r.params_hashes[0], secrand, r.secshares[r.ids[0]], &r.thresh_pk, r.ids, r.u, r.ids[0], 3, r.n, (uint32_t)r.t) == 1); testrand256(secrand); CHECK(secp256k1_frost_enrollment_shares_gen(CTX, r.shares[1], r.params_hashes[1], secrand, r.secshares[r.ids[1]], &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 1); CHECK(secp256k1_memcmp_var(r.params_hashes[0], good_hash, 32) != 0); CHECK(secp256k1_memcmp_var(r.params_hashes[1], good_hash, 32) == 0); 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. */ { static 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, and nothing else wrong: new_id = 4 is a * valid enrollment target for n = 4, and 5 is neither a valid id nor * equal to new_id, so the id range check is the only condition that * fails. */ memcpy(ids, r.ids, 3 * sizeof(ids[0])); ids[2] = 5; CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, ids, 3, 4, 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) { frost_enrollment_test_run r; secp256k1_pubkey pubshare; unsigned char buf32[32]; unsigned char secshare32[32]; unsigned char secrand32[32]; unsigned char hash32[32]; /* (n = 2, t = 2, new_id = 2) is a valid enrollment tuple, so n_ids = 0 is * the ONLY failing condition here. The earlier version used (n = 1, * t = 2), which fails on threshold > n_participants whatever n_ids is, * and would have stayed green with the n_ids handling removed. */ frost_enrollment_test_deal(&r, 2, 2, 2, 2); memcpy(secshare32, r.secshares[0], 32); memset(&pubshare, 0xff, sizeof(pubshare)); testrand256(secrand32); memset(buf32, 0xff, sizeof(buf32)); CHECK(secp256k1_frost_enrollment_params_hash(CTX, buf32, &r.thresh_pk, r.ids, 0, 2, 2, 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, secshare32, &r.thresh_pk, r.ids, 0, 0, 2, 2, 2) == 0); CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32))); CHECK(secp256k1_is_zero_array(secrand32, sizeof(secrand32))); memset(buf32, 0xff, sizeof(buf32)); CHECK(secp256k1_frost_enrollment_share_agg(CTX, buf32, NULL, r.sigmas, hash32, &r.thresh_pk, r.ids, 0, 0, 2, 2, 2) == 0); CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32))); CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, r.pubshares, r.ids, 0, 2, 2, 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, r.sigmas, &r.thresh_pk, r.ids, 0, 2, 2, 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, j, 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. The helper set is the other three. */ for (i = 0; i < 4; i++) { k = 0; for (j = 0; j < 4; j++) { if (j != i) { ids[k] = (uint32_t)j; aligned[k] = r.pubshares[j]; k++; } } CHECK(k == 3); 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. */ for (k = 0; k < 3; k++) { ids[k] = (uint32_t)k; aligned[k] = r.pubshares[k]; CHECK(secp256k1_pubkey_load(CTX, &points[k], &r.pubshares[k]) == 1); } CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &derived, aligned, ids, 3, 4, 4, 3) == 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); /* The other end of the polynomial. The public API cannot ask for * x-coordinate 0 -- that is identifier -1, and new_id is a uint32_t * bounded by n_participants -- so the identifier convention there is * pinned by evaluating frost's own derive_thresh_pubkey over the same * points and requiring it to reproduce the group key. Together with the * check above, this fixes both ends of the interpolation this module * relies on. */ CHECK(secp256k1_frost_derive_thresh_pubkey(&resultj, ids, points, 3) == 1); secp256k1_ge_set_gej_var(&got, &resultj); CHECK(secp256k1_pubkey_load(CTX, &expected, &r.thresh_pk) == 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; /* The loop bound gives sub_ids[t-2] = i+t-2 <= n-1 < n = new_id, * so the subset identifiers are always distinct and ascending. */ CHECK(sub_ids[t - 2] < new_id); 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))); } } /* NULL-argument handling for all five entry points: every pointer the header * marks ARG_NONNULL must fire the illegal-argument callback, and outputs must * still be left unusable. This mirrors the frost module's api test. */ static void run_frost_enrollment_api_test(void) { frost_enrollment_test_run r; secp256k1_pubkey pubshare; secp256k1_pubkey helper_pubshares[2]; unsigned char hash32[32]; unsigned char shares[2 * 32]; unsigned char received[2 * 32]; unsigned char secrand[32]; unsigned char out[32]; frost_enrollment_test_full_run(&r, 3, 2, 2, 3); frost_enrollment_test_helper_pubshares(&r, helper_pubshares); memset(received, 0, sizeof(received)); memcpy(received, r.params_hashes[1], 32); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_params_hash(CTX, NULL, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_params_hash(CTX, hash32, NULL, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, NULL, r.u, r.new_id, r.n, (uint32_t)r.t)); testrand256(secrand); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, NULL, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, NULL, secrand, r.secshares[0], &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, NULL, r.secshares[0], &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, NULL, &r.thresh_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], NULL, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], &r.thresh_pk, NULL, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); /* None of those consumed the seed: they never reached the body. */ CHECK(!secp256k1_is_zero_array(secrand, sizeof(secrand))); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, NULL, NULL, r.shares[0], received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, out, NULL, NULL, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, out, NULL, r.shares[0], NULL, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, out, NULL, r.shares[0], received, NULL, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, out, NULL, r.shares[0], received, &r.thresh_pk, NULL, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_pubshare_derive(CTX, NULL, helper_pubshares, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, NULL, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, helper_pubshares, NULL, r.u, r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, NULL, r.sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, out, NULL, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, NULL, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, &r.thresh_pk, NULL, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL)); /* An unusable pubkey object is API misuse everywhere it is accepted, not * just in params_hash. */ { secp256k1_pubkey zero_pk; memset(&zero_pk, 0, sizeof(zero_pk)); testrand256(secrand); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], &zero_pk, r.ids, r.u, r.ids[0], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_share_agg(CTX, out, NULL, r.shares[0], received, &zero_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_pubshare_derive(CTX, &pubshare, &zero_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t)); CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, &zero_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, NULL, NULL)); CHECK_ILLEGAL(CTX, 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, &zero_pk)); } } /* pubshare_derive must reject an input set whose interpolation lands on the * point at infinity. With u = 2 and new_id = 2 the coefficients are -1 and 2, * so P_0 = 2*P_1 makes the sum vanish. */ static void run_frost_enrollment_infinity_test(void) { secp256k1_pubkey pubshares[2], out; unsigned char sk1[32], sk0[32]; uint32_t ids[2] = { 0, 1 }; do { testrand256(sk1); } while (!secp256k1_ec_seckey_verify(CTX, sk1)); memcpy(sk0, sk1, 32); /* sk0 = 2 * sk1 */ CHECK(secp256k1_ec_seckey_tweak_add(CTX, sk0, sk1) == 1); CHECK(secp256k1_ec_pubkey_create(CTX, &pubshares[0], sk0) == 1); CHECK(secp256k1_ec_pubkey_create(CTX, &pubshares[1], sk1) == 1); memset(&out, 0xff, sizeof(out)); CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &out, pubshares, ids, 2, 2, 3, 2) == 0); CHECK(secp256k1_is_zero_array((unsigned char *)&out, sizeof(out))); /* The same points at a different target do not vanish, so the rejection * above is the infinity check and not a parameter problem. */ CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &out, pubshares, ids, 2, 3, 4, 2) == 1); } /* Enrollment leaves every existing participant's key material untouched -- * the C analogue of the reference implementation's test_participant_not_in_dkg, * which checks that the new participant holds only its aggregate share and the * group key. */ static void run_frost_enrollment_no_side_effects_test(void) { frost_enrollment_test_run r; unsigned char secshares_before[3][32]; secp256k1_pubkey pubshares_before[3]; secp256k1_pubkey thresh_pk_before; size_t i; frost_enrollment_test_deal(&r, 3, 2, 2, 3); memcpy(secshares_before, r.secshares, sizeof(secshares_before)); memcpy(pubshares_before, r.pubshares, sizeof(pubshares_before)); thresh_pk_before = r.thresh_pk; frost_enrollment_test_round1_gen(&r); frost_enrollment_test_round1_agg(&r); frost_enrollment_test_round2(&r); for (i = 0; i < 3; i++) { CHECK(secp256k1_memcmp_var(r.secshares[i], secshares_before[i], 32) == 0); CHECK(secp256k1_memcmp_var(&r.pubshares[i], &pubshares_before[i], sizeof(r.pubshares[i])) == 0); } CHECK(secp256k1_memcmp_var(&r.thresh_pk, &thresh_pk_before, sizeof(r.thresh_pk)) == 0); /* And the new share is genuinely new. */ for (i = 0; i < 3; i++) { CHECK(secp256k1_memcmp_var(r.new_secshare, r.secshares[i], 32) != 0); } } /* Full protocol runs at the largest sizes the API admits. u is capped at 127 * in both modes -- enrollment needs n < 128 and repair excludes the target * from the helper set -- so these exercise the fixed-size arrays * (points[128] in pubshare_derive, sorted_ids[128] in the hash) one entry * below their bound, which is as far as a valid tuple reaches. */ static void run_frost_enrollment_max_size_test(void) { /* ~540 KB per run; static so that this does not sit on the stack. */ static frost_enrollment_test_run r; const size_t max = SECP256K1_FROST_MAX_PARTICIPANTS; /* Enrollment at the largest group that can still grow. */ frost_enrollment_test_full_run(&r, max - 1, 2, max - 1, (uint32_t)(max - 1)); { static secp256k1_pubkey extended[SECP256K1_FROST_MAX_PARTICIPANTS]; size_t i; for (i = 0; i < max - 1; i++) { extended[i] = r.pubshares[i]; } extended[max - 1] = r.new_pubshare; CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, extended, max, 2) == 1); } /* Repair in a full group, which enrollment mode refuses. */ frost_enrollment_test_full_run(&r, max, 2, max - 1, 0); CHECK(secp256k1_memcmp_var(r.new_secshare, r.secshares[0], 32) == 0); } 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_infinity_test), CASE1(run_frost_enrollment_no_side_effects_test), CASE1(run_frost_enrollment_max_size_test), CASE1(run_frost_enrollment_api_test), CASE1(run_frost_enrollment_random_test), }; #endif /* SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H */