diff --git a/src/ctime_tests.c b/src/ctime_tests.c index 8ce2d8eb..30bb2e01 100644 --- a/src/ctime_tests.c +++ b/src/ctime_tests.c @@ -65,6 +65,10 @@ #include "../include/secp256k1_prefractal.h" #endif +#ifdef ENABLE_MODULE_FROST_ENROLLMENT +#include "../include/secp256k1_frost_enrollment.h" +#endif + #ifdef ENABLE_MODULE_ICEBERG #include "../include/secp256k1_iceberg.h" #include "../include/secp256k1_iceberg_dealer.h" @@ -691,6 +695,85 @@ static void run_tests(secp256k1_context *ctx, unsigned char *key) { } #endif +#ifdef ENABLE_MODULE_FROST_ENROLLMENT + { + /* A 2-of-3 group enrolling a fourth participant, taken through all + * three rounds. Secret here is the threshold key, the secret shares + * derived from it, the session randomness the split comes from, and + * every delta and sigma value on the wire -- those are additive shares + * of real secret shares. Not secret: the identifiers, the public + * shares, the threshold public key, the parameters hashes, and the + * public share derived for the new participant. */ + unsigned char thresh_seckey[32]; + unsigned char secshares[3 * 32]; + unsigned char session_secrand[2][32]; + unsigned char shares[2][2 * 32]; + unsigned char all_shares[2 * 32]; + unsigned char received_hashes[2 * 32]; + unsigned char params_hashes[2][32]; + unsigned char sigmas[2 * 32]; + unsigned char new_secshare[32]; + unsigned char direct_hash[32]; + secp256k1_pubkey thresh_pk, pubshares[3], new_pubshare; + uint32_t fe_ids[2] = { 0, 1 }; + uint32_t mismatch_id; + int j; + + SECP256K1_CHECKMEM_DEFINE(key, 32); + memcpy(thresh_seckey, key, sizeof(thresh_seckey)); + thresh_seckey[0] = thresh_seckey[0] + 9; + memcpy(session_secrand[0], key, 32); + session_secrand[0][0] = session_secrand[0][0] + 10; + memcpy(session_secrand[1], key, 32); + session_secrand[1][0] = session_secrand[1][0] + 11; + + SECP256K1_CHECKMEM_UNDEFINE(thresh_seckey, sizeof(thresh_seckey)); + ret = secp256k1_frost_trusted_dealer_keygen(ctx, secshares, &thresh_pk, pubshares, 3, 2, thresh_seckey); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + SECP256K1_CHECKMEM_DEFINE(&thresh_pk, sizeof(thresh_pk)); + SECP256K1_CHECKMEM_DEFINE(pubshares, sizeof(pubshares)); + + /* The parameters hash and the public share at the target identifier + * are functions of public data alone. */ + CHECK(secp256k1_frost_enrollment_params_hash(ctx, direct_hash, &thresh_pk, fe_ids, 2, 3, 3, 2) == 1); + CHECK(secp256k1_frost_enrollment_pubshare_derive(ctx, &new_pubshare, pubshares, fe_ids, 2, 3, 3, 2) == 1); + + /* Round 1.1. The seed and the secret share are secret; the parameters + * hash is public, the delta values are not. */ + for (i = 0; i < 2; i++) { + SECP256K1_CHECKMEM_UNDEFINE(session_secrand[i], 32); + SECP256K1_CHECKMEM_UNDEFINE(&secshares[32 * i], 32); + ret = secp256k1_frost_enrollment_shares_gen(ctx, shares[i], params_hashes[i], session_secrand[i], &secshares[32 * i], &thresh_pk, fe_ids, 2, fe_ids[i], 3, 3, 2); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + SECP256K1_CHECKMEM_DEFINE(params_hashes[i], 32); + } + + /* Round 1.2, for both helpers. The delta values stay secret through + * the sum; only the return value is examined. */ + for (j = 0; j < 2; j++) { + memset(received_hashes, 0, sizeof(received_hashes)); + for (i = 0; i < 2; i++) { + memcpy(&all_shares[32 * i], &shares[i][32 * j], 32); + if (i != j) { + memcpy(&received_hashes[32 * i], params_hashes[i], 32); + } + } + ret = secp256k1_frost_enrollment_share_agg(ctx, &sigmas[32 * j], &mismatch_id, all_shares, received_hashes, &thresh_pk, fe_ids, 2, fe_ids[j], 3, 3, 2); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + } + + /* Round 2, with both optional checks on. The resulting share is + * secret; the verification against the expected public share is the + * one place a secret-derived point is deliberately declassified. */ + ret = secp256k1_frost_enrollment_secshare_gen(ctx, new_secshare, sigmas, &thresh_pk, fe_ids, 2, 3, 3, 2, params_hashes[0], &new_pubshare); + SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret)); + CHECK(ret == 1); + } +#endif + #ifdef ENABLE_MODULE_ICEBERG { /* A 3-of-5 group, dealt from `key` and taken as far as one signature diff --git a/src/modules/frost_enrollment/enrollment_impl.h b/src/modules/frost_enrollment/enrollment_impl.h index 5d082173..edf77f2a 100644 --- a/src/modules/frost_enrollment/enrollment_impl.h +++ b/src/modules/frost_enrollment/enrollment_impl.h @@ -13,31 +13,274 @@ /* This module is compiled into the same translation unit as frost and is * included after it, so it may use frost's static internals. It adds nothing - * to them: the Lagrange machinery, the identifier conventions and the id + * to them: the identifier conventions, the polynomial evaluation and the id * canonicalization all come from there. */ #include "../frost/keygen.h" #include "../frost/session.h" #include "../../eckey.h" +#include "../../ecmult_gen.h" #include "../../group.h" #include "../../hash.h" #include "../../scalar.h" #include "../../util.h" +/* Initializes SHA256 with the "FROST enrollment/params_hash" tag. */ +static void secp256k1_frost_enrollment_sha256_tagged_params_hash(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) { + secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"FROST enrollment/params_hash", sizeof("FROST enrollment/params_hash") - 1); +} + +/* Initializes SHA256 with the "FROST enrollment/share_split" tag. */ +static void secp256k1_frost_enrollment_sha256_tagged_share_split(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) { + secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"FROST enrollment/share_split", sizeof("FROST enrollment/share_split") - 1); +} + +/* Checks the parameter tuple shared by all five entry points. Ids must be + * unique, in range and distinct from new_id; the helper count must lie between + * the threshold and the participant count; and new_id must select exactly one + * of the two modes: + * + * enrollment (new_id == n_participants): the resulting group has n+1 + * participants, so n must be strictly below the maximum. + * repair (new_id < n_participants): n does not change, so the full range is + * allowed. + * + * threshold >= 2 is a deliberate divergence from the frost module, which + * accepts threshold >= 1. Because this API permits any threshold <= n_ids, + * t = 1 would permit u = 1, and at u = 1 the additive split of shares_gen + * degenerates to a single share -- the lone helper would send the unsplit + * v_1, which at t = 1 is the whole group secret. Requiring t >= 2 forces + * u >= 2, which is what makes the split non-degenerate. + * + * Operates on public data only. Returns 1 if the parameters are valid, 0 + * otherwise. */ +static int secp256k1_frost_enrollment_params_are_valid(const uint32_t *ids, size_t n_ids, uint32_t new_id, size_t n_participants, uint32_t threshold) { + size_t i; + + if (n_participants < 1 || n_participants > SECP256K1_FROST_MAX_PARTICIPANTS) { + return 0; + } + if (threshold < 2 || (size_t)threshold > n_participants) { + return 0; + } + if (n_ids < (size_t)threshold || n_ids > n_participants) { + return 0; + } + if ((size_t)new_id > n_participants) { + return 0; + } + if ((size_t)new_id == n_participants && n_participants >= SECP256K1_FROST_MAX_PARTICIPANTS) { + /* Enrollment mode would produce a group of n+1 > 128 participants, + * which the frost module rejects everywhere. */ + return 0; + } + for (i = 0; i < n_ids; i++) { + if ((size_t)ids[i] >= n_participants) { + return 0; + } + if (ids[i] == new_id) { + /* A helper cannot be the target of its own run. */ + return 0; + } + } + if (!secp256k1_frost_ids_are_valid(ids, n_ids)) { + return 0; + } + return 1; +} + +/* Returns the position of my_id in ids, or n_ids if it does not occur. */ +static size_t secp256k1_frost_enrollment_id_pos(const uint32_t *ids, size_t n_ids, uint32_t my_id) { + size_t i; + + for (i = 0; i < n_ids; i++) { + if (ids[i] == my_id) { + return i; + } + } + return n_ids; +} + +/* Computes the parameters hash + * + * out32 = TH("FROST enrollment/params_hash", + * cbytes(thresh_pk) || ser32(n_participants) || ser32(threshold) + * || ser32(new_id) || ser32(n_ids) || ser32(sorted_ids[0]) || + * ... || ser32(sorted_ids[n_ids-1])) + * + * over the already-validated parameters and the already-loaded group key. + * + * The fixed-width u32be fields make the concatenation unambiguous, following + * chilldkg's params_hash (src/modules/chilldkg/util_impl.h:399); binding + * cbytes(thresh_pk) follows the same precedent's commitment to key material + * rather than to integers alone, and is what makes the digest name a group + * rather than a tuple of numbers. + * + * Ids are sorted into a local copy first, so that helpers holding the same + * set in different orders agree. Every other array in this API stays aligned + * with the caller's own ids order. + * + * Public data only; `thresh_pk` is normalized in place by the serialization, + * which is why it is not const. */ +static void secp256k1_frost_enrollment_params_hash_internal(const secp256k1_hash_ctx *hash_ctx, unsigned char *out32, secp256k1_ge *thresh_pk, const uint32_t *ids, size_t n_ids, uint32_t new_id, size_t n_participants, uint32_t threshold) { + uint32_t sorted_ids[SECP256K1_FROST_MAX_PARTICIPANTS]; + unsigned char thresh_pk33[33]; + unsigned char buf[4]; + secp256k1_sha256 sha; + size_t i; + + VERIFY_CHECK(n_ids <= SECP256K1_FROST_MAX_PARTICIPANTS); + + secp256k1_eckey_pubkey_serialize33(thresh_pk, thresh_pk33); + secp256k1_frost_enrollment_sha256_tagged_params_hash(hash_ctx, &sha); + secp256k1_sha256_write(hash_ctx, &sha, thresh_pk33, sizeof(thresh_pk33)); + secp256k1_write_be32(buf, (uint32_t)n_participants); + secp256k1_sha256_write(hash_ctx, &sha, buf, sizeof(buf)); + secp256k1_write_be32(buf, threshold); + secp256k1_sha256_write(hash_ctx, &sha, buf, sizeof(buf)); + secp256k1_write_be32(buf, new_id); + secp256k1_sha256_write(hash_ctx, &sha, buf, sizeof(buf)); + secp256k1_write_be32(buf, (uint32_t)n_ids); + secp256k1_sha256_write(hash_ctx, &sha, buf, sizeof(buf)); + secp256k1_frost_sort_ids(sorted_ids, ids, n_ids); + for (i = 0; i < n_ids; i++) { + secp256k1_write_be32(buf, sorted_ids[i]); + secp256k1_sha256_write(hash_ctx, &sha, buf, sizeof(buf)); + } + secp256k1_sha256_finalize(hash_ctx, &sha, out32); + secp256k1_sha256_clear(&sha); +} + +/* Validates the parameters, loads thresh_pk and computes the parameters hash. + * Returns 1 on success and 0 if either step fails, in which case out32 is left + * as the caller zeroed it. The single place the encoding is produced: every + * gate in this module goes through here, so there is exactly one + * implementation to keep in step with the vectors. + * + * The validation also bounds n_ids for everything downstream: it requires + * threshold <= n_ids <= n_participants <= SECP256K1_FROST_MAX_PARTICIPANTS + * with threshold >= 2, so the fixed-size arrays in this module are always in + * range. That bound must not ride on the VERIFY_CHECK inside + * secp256k1_frost_sort_ids (src/modules/frost/session_impl.h:520), which + * compiles out in production builds. */ +static int secp256k1_frost_enrollment_params_hash_checked(const secp256k1_context *ctx, unsigned char *out32, const secp256k1_pubkey *thresh_pk, const uint32_t *ids, size_t n_ids, uint32_t new_id, size_t n_participants, uint32_t threshold) { + secp256k1_ge pk; + + if (!secp256k1_frost_enrollment_params_are_valid(ids, n_ids, new_id, n_participants, threshold)) { + return 0; + } + if (!secp256k1_pubkey_load(ctx, &pk, thresh_pk)) { + return 0; + } + secp256k1_frost_enrollment_params_hash_internal(secp256k1_get_hash_context(ctx), out32, &pk, ids, n_ids, new_id, n_participants, threshold); + return 1; +} + +/* Computes the Lagrange basis polynomial of my_id over the helper set, + * evaluated at the target identifier: + * + * lambda = product_{j != i} (new_id - id_j) / (id_i - id_j) (mod n) + * + * This is the scalar counterpart of the coefficient + * secp256k1_frost_derive_pubshare_at (src/modules/frost/keygen_impl.h:150) + * applies to each pubshare, and it works in the same identifier space: the + * x-coordinate of identifier id is id + 1, so an x-coordinate difference + * x_j - x_i equals the identifier difference id_j - id_i and the +1 cancels. + * frost's own secp256k1_frost_derive_interpolating_value cannot be reused + * here because it evaluates at x-coordinate 0, not at an arbitrary point. + * + * All inputs are public, so the variable-time inverse is fine -- it is the + * same one both frost functions use. Requires ids to contain my_id and no + * duplicates, which the caller has already checked. */ +static void secp256k1_frost_enrollment_lagrange_at(secp256k1_scalar *out, const uint32_t *ids, size_t n_ids, uint32_t my_id, uint32_t new_id) { + secp256k1_scalar num, deno, id_i, x; + size_t j; + + secp256k1_scalar_set_int(&id_i, my_id); + secp256k1_scalar_set_int(&x, new_id); + secp256k1_scalar_set_int(&num, 1); + secp256k1_scalar_set_int(&deno, 1); + for (j = 0; j < n_ids; j++) { + secp256k1_scalar id_j, term; + if (ids[j] == my_id) { + continue; + } + secp256k1_scalar_set_int(&id_j, ids[j]); + secp256k1_scalar_negate(&term, &id_j); + /* num *= new_id - id_j */ + secp256k1_scalar_add(&term, &term, &x); + secp256k1_scalar_mul(&num, &num, &term); + /* deno *= my_id - id_j */ + secp256k1_scalar_negate(&term, &id_j); + secp256k1_scalar_add(&term, &term, &id_i); + secp256k1_scalar_mul(&deno, &deno, &term); + } + /* deno != 0 because the ids are distinct and new_id is not among them */ + VERIFY_CHECK(!secp256k1_scalar_is_zero(&deno)); + secp256k1_scalar_inverse_var(&deno, &deno); + secp256k1_scalar_mul(out, &num, &deno); +} + +/* Derives one masking share as + * + * Scalar.from_bytes_wrapping( + * TH("FROST enrollment/share_split", + * rand32 || params_hash32 || ser32(my_id) || ser32(recipient_id))) + * + * where rand32 is the session randomness masked with the secret share. + * + * The reduction wraps rather than rejects, following chilldkg's + * from_bytes_wrapping (src/modules/chilldkg/util_impl.h:394): the statistical + * distance of a 256-bit hash taken mod the group order is about 2^-128, and + * rejection sampling would buy nothing in exchange for a variable-time loop. + * + * The recipient's IDENTIFIER indexes the derivation, not its position in the + * caller's ids array, so the split does not depend on the order a caller + * happens to list the helper set in. Identifiers are unique, so this is + * injective, which is all a counter had to be. */ +static void secp256k1_frost_enrollment_derive_mask(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *out, const unsigned char *rand32, const unsigned char *params_hash32, uint32_t my_id, uint32_t recipient_id) { + unsigned char buf[4]; + unsigned char hash32[32]; + secp256k1_sha256 sha; + + secp256k1_frost_enrollment_sha256_tagged_share_split(hash_ctx, &sha); + secp256k1_sha256_write(hash_ctx, &sha, rand32, 32); + secp256k1_sha256_write(hash_ctx, &sha, params_hash32, 32); + secp256k1_write_be32(buf, my_id); + secp256k1_sha256_write(hash_ctx, &sha, buf, sizeof(buf)); + secp256k1_write_be32(buf, recipient_id); + secp256k1_sha256_write(hash_ctx, &sha, buf, sizeof(buf)); + secp256k1_sha256_finalize(hash_ctx, &sha, hash32); + + secp256k1_scalar_set_b32(out, hash32, NULL); + secp256k1_memclear_explicit(hash32, sizeof(hash32)); + secp256k1_sha256_clear(&sha); +} + int secp256k1_frost_enrollment_params_hash(const secp256k1_context *ctx, unsigned char *out32, const secp256k1_pubkey *thresh_pk, const uint32_t *ids, size_t n_ids, uint32_t new_id, size_t n_participants, uint32_t threshold) { VERIFY_CHECK(ctx != NULL); ARG_CHECK(out32 != NULL); memset(out32, 0, 32); ARG_CHECK(thresh_pk != NULL); ARG_CHECK(ids != NULL); - (void)n_ids; - (void)new_id; - (void)n_participants; - (void)threshold; - return 0; + + /* Value ranges return 0 rather than firing the illegal callback, which is + * the frost module's split (pointers are ARG_CHECKed, ranges are not; see + * secp256k1_frost_trusted_dealer_keygen, keygen_impl.h:227). The int + * return matters here: ARG_CHECK_VOID would leave out32 unwritten under a + * non-aborting illegal callback, and a caller would then compare a buffer + * that was never computed. */ + return secp256k1_frost_enrollment_params_hash_checked(ctx, out32, thresh_pk, ids, n_ids, new_id, n_participants, threshold); } int secp256k1_frost_enrollment_shares_gen(const secp256k1_context *ctx, unsigned char *shares32_out, unsigned char *params_hash32_out, unsigned char *session_secrand32, const unsigned char *secshare32, const secp256k1_pubkey *thresh_pk, const uint32_t *ids, size_t n_ids, uint32_t my_id, uint32_t new_id, size_t n_participants, uint32_t threshold) { + const secp256k1_hash_ctx *hash_ctx; + secp256k1_scalar secshare, lambda, v, mask; + secp256k1_sha256 sha; + unsigned char rand32[32]; + size_t my_pos, i; + int ret = 0; + VERIFY_CHECK(ctx != NULL); ARG_CHECK(shares32_out != NULL); ARG_CHECK(params_hash32_out != NULL); @@ -46,15 +289,83 @@ int secp256k1_frost_enrollment_shares_gen(const secp256k1_context *ctx, unsigned ARG_CHECK(secshare32 != NULL); ARG_CHECK(thresh_pk != NULL); ARG_CHECK(ids != NULL); - (void)n_ids; - (void)my_id; - (void)new_id; - (void)n_participants; - (void)threshold; - return 0; + /* By contract shares32_out holds n_ids entries, whatever n_ids is; the + * value itself is bounded by params_are_valid below. */ + memset(shares32_out, 0, n_ids * 32); + + hash_ctx = secp256k1_get_hash_context(ctx); + memset(rand32, 0, sizeof(rand32)); + + if (!secp256k1_frost_enrollment_params_hash_checked(ctx, params_hash32_out, thresh_pk, ids, n_ids, new_id, n_participants, threshold)) { + goto cleanup; + } + my_pos = secp256k1_frost_enrollment_id_pos(ids, n_ids, my_id); + if (my_pos == n_ids) { + goto cleanup; + } + { + /* Branching on validity only leaks whether the provided share is a + * valid secret key, which is not secret. */ + int valid = secp256k1_scalar_set_b32_seckey(&secshare, secshare32); + secp256k1_declassify(ctx, &valid, sizeof(valid)); + if (!valid) { + goto cleanup; + } + } + + /* v = lambda_my_id(x_new) * secshare */ + secp256k1_frost_enrollment_lagrange_at(&lambda, ids, n_ids, my_id, new_id); + secp256k1_scalar_mul(&v, &lambda, &secshare); + + /* rand32 = TH("FROST enrollment/share_split", session_secrand32) XOR + * secshare32. Masking the derived randomness with the secret share is the + * secp256k1_frost_nonce_gen pattern (session_impl.h:340): it means a + * broken random number generator alone does not reveal the split. */ + secp256k1_frost_enrollment_sha256_tagged_share_split(hash_ctx, &sha); + secp256k1_sha256_write(hash_ctx, &sha, session_secrand32, 32); + secp256k1_sha256_finalize(hash_ctx, &sha, rand32); + secp256k1_sha256_clear(&sha); + for (i = 0; i < 32; i++) { + rand32[i] ^= secshare32[i]; + } + + /* Split v into n_ids additive shares. Every share but the one kept at + * my_pos is masking randomness; the kept one absorbs the remainder, so the + * whole set sums to v. */ + for (i = 0; i < n_ids; i++) { + if (i == my_pos) { + continue; + } + secp256k1_frost_enrollment_derive_mask(hash_ctx, &mask, rand32, params_hash32_out, my_id, ids[i]); + secp256k1_scalar_get_b32(&shares32_out[32 * i], &mask); + secp256k1_scalar_negate(&mask, &mask); + secp256k1_scalar_add(&v, &v, &mask); + } + secp256k1_scalar_get_b32(&shares32_out[32 * my_pos], &v); + ret = 1; + +cleanup: + if (!ret) { + secp256k1_memzero_explicit(shares32_out, n_ids * 32); + memset(params_hash32_out, 0, 32); + } + /* The seed is consumed either way: a caller must not be able to retry a + * failed call with the same randomness. */ + secp256k1_memzero_explicit(session_secrand32, 32); + secp256k1_memclear_explicit(rand32, sizeof(rand32)); + secp256k1_scalar_clear(&secshare); + secp256k1_scalar_clear(&lambda); + secp256k1_scalar_clear(&v); + secp256k1_scalar_clear(&mask); + return ret; } int secp256k1_frost_enrollment_share_agg(const secp256k1_context *ctx, unsigned char *sigma32_out, uint32_t *mismatch_id, const unsigned char *all_shares32, const unsigned char *received_params_hashes32, const secp256k1_pubkey *thresh_pk, const uint32_t *ids, size_t n_ids, uint32_t my_id, uint32_t new_id, size_t n_participants, uint32_t threshold) { + secp256k1_scalar sigma, term; + unsigned char params_hash32[32]; + size_t my_pos, i; + int ret = 0; + VERIFY_CHECK(ctx != NULL); ARG_CHECK(sigma32_out != NULL); memset(sigma32_out, 0, 32); @@ -65,41 +376,192 @@ int secp256k1_frost_enrollment_share_agg(const secp256k1_context *ctx, unsigned ARG_CHECK(received_params_hashes32 != NULL); ARG_CHECK(thresh_pk != NULL); ARG_CHECK(ids != NULL); - (void)n_ids; - (void)my_id; - (void)new_id; - (void)n_participants; - (void)threshold; - return 0; + + /* set_int, not scalar_clear: this accumulator is READ on the first add, + * and secp256k1_memclear_explicit marks its target undefined in VERIFY + * builds (src/util.h:295) precisely to catch reads of cleared memory. */ + secp256k1_scalar_set_int(&sigma, 0); + secp256k1_scalar_set_int(&term, 0); + memset(params_hash32, 0, sizeof(params_hash32)); + + /* The full parameter tuple is present here, so the mode and bounds are + * re-validated rather than trusted from the round 1.1 call site. */ + if (!secp256k1_frost_enrollment_params_hash_checked(ctx, params_hash32, thresh_pk, ids, n_ids, new_id, n_participants, threshold)) { + goto cleanup; + } + my_pos = secp256k1_frost_enrollment_id_pos(ids, n_ids, my_id); + if (my_pos == n_ids) { + goto cleanup; + } + + /* Compare every received hash against the one just recomputed. The slot at + * my own position is never read: the own hash comes from the line above, + * not from a buffer, so a caller cannot copy a received hash into that + * slot and launder a mismatch into a pass. Public data, so the + * variable-time compare and the early exit are fine. */ + for (i = 0; i < n_ids; i++) { + if (i == my_pos) { + continue; + } + if (secp256k1_memcmp_var(&received_params_hashes32[32 * i], params_hash32, 32) != 0) { + if (mismatch_id != NULL) { + *mismatch_id = ids[i]; + } + goto cleanup; + } + } + + /* sigma = sum over all u entries, including the share kept at my own + * position. Out-of-range entries are rejected the same way + * secp256k1_frost_partial_sig_agg rejects an unparseable partial + * signature. */ + for (i = 0; i < n_ids; i++) { + int overflow; + secp256k1_scalar_set_b32(&term, &all_shares32[32 * i], &overflow); + secp256k1_declassify(ctx, &overflow, sizeof(overflow)); + if (overflow) { + if (mismatch_id != NULL) { + *mismatch_id = ids[i]; + } + goto cleanup; + } + secp256k1_scalar_add(&sigma, &sigma, &term); + } + secp256k1_scalar_get_b32(sigma32_out, &sigma); + ret = 1; + +cleanup: + if (!ret) { + secp256k1_memzero_explicit(sigma32_out, 32); + } + secp256k1_memclear_explicit(params_hash32, sizeof(params_hash32)); + secp256k1_scalar_clear(&sigma); + secp256k1_scalar_clear(&term); + return ret; } int secp256k1_frost_enrollment_pubshare_derive(const secp256k1_context *ctx, secp256k1_pubkey *new_pubshare_out, const secp256k1_pubkey *pubshares, const uint32_t *ids, size_t n_ids, uint32_t new_id, size_t n_participants, uint32_t threshold) { + secp256k1_ge points[SECP256K1_FROST_MAX_PARTICIPANTS]; + secp256k1_ge result; + secp256k1_gej resultj; + secp256k1_scalar x; + size_t i; + VERIFY_CHECK(ctx != NULL); ARG_CHECK(new_pubshare_out != NULL); memset(new_pubshare_out, 0, sizeof(*new_pubshare_out)); ARG_CHECK(pubshares != NULL); ARG_CHECK(ids != NULL); - (void)n_ids; - (void)new_id; - (void)n_participants; - (void)threshold; - return 0; + + if (!secp256k1_frost_enrollment_params_are_valid(ids, n_ids, new_id, n_participants, threshold)) { + return 0; + } + for (i = 0; i < n_ids; i++) { + if (!secp256k1_pubkey_load(ctx, &points[i], &pubshares[i])) { + return 0; + } + } + /* The target x-coordinate in identifier space; derive_pubshare_at applies + * the same +1 convention this module's Lagrange helper does. */ + secp256k1_scalar_set_int(&x, new_id); + if (!secp256k1_frost_derive_pubshare_at(&resultj, ids, points, n_ids, &x)) { + return 0; + } + if (secp256k1_gej_is_infinity(&resultj)) { + return 0; + } + secp256k1_ge_set_gej_var(&result, &resultj); + secp256k1_pubkey_save(new_pubshare_out, &result); + return 1; } int secp256k1_frost_enrollment_secshare_gen(const secp256k1_context *ctx, unsigned char *secshare32_out, const unsigned char *sigmas32, const secp256k1_pubkey *thresh_pk, const uint32_t *ids, size_t n_ids, uint32_t new_id, size_t n_participants, uint32_t threshold, const unsigned char *expected_params_hash32, const secp256k1_pubkey *expected_pubshare) { + secp256k1_scalar secshare, term; + unsigned char params_hash32[32]; + size_t i; + int ret = 0; + VERIFY_CHECK(ctx != NULL); ARG_CHECK(secshare32_out != NULL); memset(secshare32_out, 0, 32); ARG_CHECK(sigmas32 != NULL); ARG_CHECK(thresh_pk != NULL); ARG_CHECK(ids != NULL); - (void)n_ids; - (void)new_id; - (void)n_participants; - (void)threshold; - (void)expected_params_hash32; - (void)expected_pubshare; - return 0; + ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx)); + + /* set_int for the same reason as in share_agg above. */ + secp256k1_scalar_set_int(&secshare, 0); + secp256k1_scalar_set_int(&term, 0); + memset(params_hash32, 0, sizeof(params_hash32)); + + /* Recomputed from the parameters the target believes and the group key it + * authenticated, then compared against the helpers'. This is the only + * check that covers a disagreement between the helpers as a body and the + * target -- the round 1.2 gate is helper against helper only. */ + if (!secp256k1_frost_enrollment_params_hash_checked(ctx, params_hash32, thresh_pk, ids, n_ids, new_id, n_participants, threshold)) { + goto cleanup; + } + if (expected_params_hash32 != NULL + && secp256k1_memcmp_var(expected_params_hash32, params_hash32, 32) != 0) { + goto cleanup; + } + + for (i = 0; i < n_ids; i++) { + int overflow; + secp256k1_scalar_set_b32(&term, &sigmas32[32 * i], &overflow); + secp256k1_declassify(ctx, &overflow, sizeof(overflow)); + if (overflow) { + goto cleanup; + } + secp256k1_scalar_add(&secshare, &secshare, &term); + } + { + /* A zero share is not a usable secret key. Whether the sum is zero is + * a property of values the helpers chose, and it can only happen with + * negligible probability for honest ones. */ + int is_zero = secp256k1_scalar_is_zero(&secshare); + secp256k1_declassify(ctx, &is_zero, sizeof(is_zero)); + if (is_zero) { + goto cleanup; + } + } + + if (expected_pubshare != NULL) { + secp256k1_ge expected, derived; + secp256k1_gej derivedj; + int eq; + + if (!secp256k1_pubkey_load(ctx, &expected, expected_pubshare)) { + goto cleanup; + } + secp256k1_ecmult_gen_gej(&ctx->ecmult_gen_ctx, &derivedj, &secshare); + secp256k1_ge_set_gej(&derived, &derivedj); + /* secshare is nonzero, so the result is never the point at infinity. + * secshare*G is the participant's public share: a public value, and + * the one the caller is about to publish. Declassifying it before the + * comparison is exactly what secp256k1_frost_sign does with the same + * quantity (src/modules/frost/session_impl.h:770, :789). */ + VERIFY_CHECK(!secp256k1_ge_is_infinity(&derived)); + secp256k1_declassify(ctx, &derived, sizeof(derived)); + eq = secp256k1_ge_eq_var(&derived, &expected); + secp256k1_ge_clear(&derived); + secp256k1_gej_clear(&derivedj); + if (!eq) { + goto cleanup; + } + } + + secp256k1_scalar_get_b32(secshare32_out, &secshare); + ret = 1; + +cleanup: + if (!ret) { + secp256k1_memzero_explicit(secshare32_out, 32); + } + secp256k1_memclear_explicit(params_hash32, sizeof(params_hash32)); + secp256k1_scalar_clear(&secshare); + secp256k1_scalar_clear(&term); + return ret; } #endif diff --git a/src/modules/frost_enrollment/tests_impl.h b/src/modules/frost_enrollment/tests_impl.h index bf880ba3..159f44d1 100644 --- a/src/modules/frost_enrollment/tests_impl.h +++ b/src/modules/frost_enrollment/tests_impl.h @@ -8,10 +8,149 @@ #include "../../../include/secp256k1_frost_enrollment.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][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); + CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand))); + } +} + +/* Runs round 1.2 for every helper, transposing the round 1.1 outputs on the + * way: helper j aggregates entry j of every helper's share buffer. */ +static void frost_enrollment_test_round1_agg(frost_enrollment_test_run *r) { + size_t i, 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; + + 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); + } + } + 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); + } +} + +/* Runs round 2, with both optional checks enabled. */ +static void frost_enrollment_test_round2(frost_enrollment_test_run *r) { + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r->new_pubshare, r->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); +} + +/* A 2-of-3 group grows to 2-of-4, and the new participant's share sits on the + * same polynomial as the old ones: every threshold-sized subset that contains + * it reconstructs the original threshold secret. */ +static void run_frost_enrollment_smoke_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); + + /* The pair that ran the protocol, and every other pair including the new + * participant. */ + 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); + } + /* And the old group still reconstructs, unchanged. */ + 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); +} + /* Every entry point must reject an empty helper set and leave its output - * zeroed. This holds for the stubs this commit adds and for the finished - * implementation alike, so it doubles as the check that all five symbols are - * reachable from the test binary. */ + * 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; @@ -46,6 +185,7 @@ static void run_frost_enrollment_rejects_empty_set_test(void) { } static const struct tf_test_entry tests_frost_enrollment[] = { + CASE1(run_frost_enrollment_smoke_test), CASE1(run_frost_enrollment_rejects_empty_set_test), };