/*********************************************************************** * 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(); } } 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_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 */