frost_enrollment: implement the three rounds
Third of six commits. Replaces the Phase 1 stubs with the real
arithmetic, adds a smoke test that a 2-of-3 group really does grow into
a working 2-of-4 one, and wires the entry points into ctime_tests.
The Lagrange machinery is frost's, called in place. pubshare_derive is a
skin over secp256k1_frost_derive_pubshare_at
(src/modules/frost/keygen_impl.h:150) evaluated at identifier new_id,
and the id canonicalization is secp256k1_frost_sort_ids, reached through
the declaration the previous commit added.
The one piece frost could not supply is the scalar Lagrange coefficient
at an arbitrary point. frost's secp256k1_frost_derive_interpolating_value
evaluates at x-coordinate 0, which is what reconstructing the group
secret needs; enrollment needs the basis polynomial at the TARGET
x-coordinate. secp256k1_frost_enrollment_lagrange_at is that, and it is
deliberately the same product derive_pubshare_at applies to each
pubshare, in the same identifier space -- so the scalar path and the
point path agree by construction rather than by coincidence. Working in
identifier space is what makes the id-to-x-coordinate +1 cancel: an
x-coordinate difference x_j - x_i is the identifier difference
id_j - id_i.
Round 1.1 computes v = lambda * secshare and splits it. Every share but
the one kept locally is masking randomness derived as
Scalar.from_bytes_wrapping(
TH("FROST enrollment/share_split",
rand32 || params_hash32 || ser32(my_id) || ser32(recipient_id)))
with rand32 = TH(same tag, session_secrand32) XOR secshare32; the kept
share absorbs the remainder so the set sums to v. Three details:
- The reduction wraps rather than rejects, chilldkg's
from_bytes_wrapping (src/modules/chilldkg/util_impl.h:394). A 256-bit
hash mod the group order is about 2^-128 from uniform; rejection
sampling would buy that back in exchange for a variable-time loop.
- Masking with the secret share is the secp256k1_frost_nonce_gen pattern
(session_impl.h:340), so a broken RNG alone does not reveal the split.
- The derivation is indexed by the recipient's IDENTIFIER, not by its
position in the caller's ids array. The plan called for a counter;
identifiers are unique, so they are one, and using them makes the
split independent of the order a caller lists the helper set in. What
the binding buys is DOMAIN SEPARATION only: params_hash32 carries the
group key and the whole parameter tuple, so two runs sharing a seed
but differing in either cannot produce the same deltas. It cannot
detect a disagreement between helpers, because nothing cross-checks
per-helper private randomness. That is the params hash's job.
session_secrand32 is wiped whether the call succeeds or fails, so a
caller cannot retry a failed run on the same randomness.
Round 1.2 recomputes its own params hash from the group key and the
tuple, compares every received hash against it, then sums. The slot at
the caller's own position in received_params_hashes32 is skipped, while
the same position in all_shares32 is read -- the asymmetry the header
documents, and the thing that makes this a recomputation rather than a
string comparison. The mode and bounds are re-validated here rather than
trusted from the round 1.1 call site, since the full tuple is present.
An out-of-range share is reported through mismatch_id the way
secp256k1_frost_partial_sig_agg reports an unparseable partial
signature.
Round 2 compares the params hash against its own recomputation over the
authenticated group key, sums, rejects a zero share, and checks
secshare*G against the expected public share.
Three deviations from the plan, all to match what the tree already does:
- Value ranges return 0; only pointers get ARG_CHECK. The plan called
for an ARG_CHECK on the n_ids bound, but the frost module's split is
the one used here (secp256k1_frost_trusted_dealer_keygen,
keygen_impl.h:227), and the header already documents these as
return-0 conditions. The bound is still enforced in production builds
-- params_are_valid requires 2 <= threshold <= n_ids <= n_participants
<= 128 -- so it does not ride on the VERIFY_CHECK inside
secp256k1_frost_sort_ids, which is what the plan was guarding against.
- The public-share check declassifies the derived point and compares
with secp256k1_ge_eq_var, rather than comparing 33 serialized bytes in
constant time. There is no constant-time memcmp in this tree, and
secshare*G is a public key: secp256k1_frost_sign declassifies exactly
this quantity before exactly this comparison
(src/modules/frost/session_impl.h:770, :789). Inventing a primitive to
avoid following that precedent would be the worse trade.
- params_hash's public entry point delegates to the same internal
routine every gate uses, so the encoding has exactly one
implementation to keep in step with the vectors.
One real bug found by the tooling rather than by reading. Accumulators
were initialized with secp256k1_scalar_clear, and
secp256k1_memclear_explicit marks its target UNDEFINED in VERIFY builds
(src/util.h:295) precisely so that reading cleared memory is caught. It
was: valgrind reported 143752 errors in share_agg's summation loop.
Accumulators now start at secp256k1_scalar_set_int(x, 0); scalar_clear
is used only where it means "done with this secret". Worth stating
plainly because the failure mode is invisible in a production build,
where memclear_explicit only zeroes.
ctime_tests gains a 2-of-3-enrolls-a-fourth block covering all three
rounds, following prefractal's b66c757b. The threshold key, the secret
shares, the session randomness and every delta and sigma on the wire are
marked secret; the identifiers, public shares, group key, parameters
hashes and derived public share are not. Under valgrind: 0 errors from 0
contexts, so no branch or memory access in the new code depends on
secret data.
Verification: ./tests, ./noverify_tests and ./exhaustive_tests exit 0;
the frost_enrollment module runs clean under valgrind (0 errors); a
separate CPPFLAGS='-DVERIFY' build compiles without warnings and passes;
the module builds warning-free alongside frost, chilldkg, iceberg and
prefractal.
The smoke test is the substantive check: after a 2-of-3 group enrolls
participant 3, every pair {i, 3} for i in 0..2 reconstructs the original
threshold secret and matches the threshold public key, and the untouched
pair {0, 1} still does too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user