diff --git a/Makefile.am b/Makefile.am index 15bc7644..4a0c4f18 100644 --- a/Makefile.am +++ b/Makefile.am @@ -251,6 +251,17 @@ iceberg_example_LDFLAGS += -lbcrypt endif TESTS += iceberg_example endif +if ENABLE_MODULE_FROST_ENROLLMENT +noinst_PROGRAMS += frost_enrollment_example +frost_enrollment_example_SOURCES = examples/frost_enrollment.c +frost_enrollment_example_CPPFLAGS = -I$(top_srcdir)/include -DSECP256K1_STATIC +frost_enrollment_example_LDADD = libsecp256k1.la +frost_enrollment_example_LDFLAGS = -static +if BUILD_WINDOWS +frost_enrollment_example_LDFLAGS += -lbcrypt +endif +TESTS += frost_enrollment_example +endif endif ### Precomputed tables diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d04e6645..648bda1d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -43,3 +43,7 @@ endif() if(SECP256K1_ENABLE_MODULE_ICEBERG) add_example(iceberg) endif() + +if(SECP256K1_ENABLE_MODULE_FROST_ENROLLMENT) + add_example(frost_enrollment) +endif() diff --git a/examples/frost_enrollment.c b/examples/frost_enrollment.c new file mode 100644 index 00000000..7edac885 --- /dev/null +++ b/examples/frost_enrollment.c @@ -0,0 +1,423 @@ +/************************************************************************* + * To the extent possible under law, the author(s) have dedicated all * + * copyright and related and neighboring rights to the software in this * + * file to the public domain worldwide. This software is distributed * + * without any warranty. For the CC0 Public Domain Dedication, see * + * EXAMPLES_COPYING or https://creativecommons.org/publicdomain/zero/1.0 * + *************************************************************************/ + +/** This file demonstrates how to use the FROST enrollment module: first to + * grow a 2-of-3 group into a 2-of-4 one without re-running key generation, + * then to repair a participant's lost share. Additionally, see the + * documentation in include/secp256k1_frost_enrollment.h and + * src/modules/frost_enrollment/frost_enrollment.md. + * + * The example runs all roles (trusted dealer, helpers, the party receiving + * the share, coordinator) in a single process. In a real deployment these + * roles are performed by different parties, and the enrollment shares and + * the aggregated values they produce MUST travel over confidential and + * authenticated channels: they are additive shares of real secret shares. + * + * Two things this example demonstrates that are easy to get wrong: + * + * 1. The verification flow is only non-circular if the threshold public key + * comes from somewhere the helpers do not control. Here the enrollee gets + * it straight from the dealer step, which stands in for real + * authentication, and validates the helpers' public shares against it + * before deriving anything from them. + * 2. Nothing in the protocol decides WHO may be enrolled. That is a + * caller-side precondition; see the note before enroll() below. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "examples_util.h" + +/* Number of participants n before the enrollment */ +#define N_PARTICIPANTS 3 +/* Number of participants after the enrollment */ +#define N_PARTICIPANTS_AFTER (N_PARTICIPANTS + 1) +/* Threshold t */ +#define THRESHOLD 2 +/* Number of helpers u taking part in the enrollment run. Any u with + * THRESHOLD <= u <= N_PARTICIPANTS works; the result does not depend on the + * choice. */ +#define N_HELPERS 2 +/* The identifier the new participant will hold. In enrollment mode this must + * be exactly the current participant count. */ +#define NEW_ID N_PARTICIPANTS +/* The signers of the demonstration signing session: participant 2 and the + * newly enrolled participant. */ +#define N_SIGNERS 2 + +/* What one helper holds during a run. */ +struct helper { + uint32_t id; + /* The helper's own secret share of the group key. Never leaves the + * helper. */ + unsigned char secshare[32]; + /* Round 1.1 output, aligned with the helper id array: entry j goes to the + * helper at position j. The entry at this helper's own position is kept + * locally. These are secret. */ + unsigned char shares[N_HELPERS * 32]; + /* The parameters hash this helper computed, sent alongside every share it + * distributes and to the enrollee. Public. */ + unsigned char params_hash[32]; + /* Round 1.2 output, sent to the enrollee. Secret. */ + unsigned char sigma[32]; +}; + +/* Run the trusted dealer key generation for the initial 2-of-3 group. + * + * WARNING: The trusted dealer knows the threshold secret key and all secret + * shares, and must erase them after distributing the shares. A distributed key + * generation protocol avoids a trusted dealer entirely; see the chilldkg + * module. */ +static int trusted_dealer_keygen(const secp256k1_context* ctx, unsigned char *threshold_seckey, unsigned char *secshares, secp256k1_pubkey *thresh_pk, secp256k1_pubkey *pubshares) { + if (!fill_random(threshold_seckey, 32)) { + printf("Failed to generate randomness\n"); + return 0; + } + if (!secp256k1_frost_trusted_dealer_keygen(ctx, secshares, thresh_pk, pubshares, N_PARTICIPANTS, THRESHOLD, threshold_seckey)) { + return 0; + } + return secp256k1_frost_threshold_info_validate(ctx, thresh_pk, pubshares, N_PARTICIPANTS, THRESHOLD); +} + +/* Run one enrollment or repair, all three rounds. + * + * `helper_pubshares` and `helpers` are aligned with `ids`. On success the new + * secret share is written to `new_secshare` and its public counterpart to + * `new_pubshare`. + * + * PRECONDITION THE LIBRARY CANNOT ENFORCE: the helpers must already have + * agreed, out of band, that this party is entitled to a share at `new_id`. + * The protocol has no authorization step of its own: anyone who convinces t + * helpers to run it walks away with a valid share, and in repair mode that is + * an existing participant's actual share. Authenticated channels establish who + * is speaking, not that the group approved the request. */ +static int enroll(const secp256k1_context* ctx, unsigned char *new_secshare, secp256k1_pubkey *new_pubshare, struct helper *helpers, const uint32_t *ids, const secp256k1_pubkey *helper_pubshares, const secp256k1_pubkey *thresh_pk, uint32_t new_id) { + unsigned char sigmas[N_HELPERS * 32]; + unsigned char enrollee_params_hash[32]; + int i, j; + + /* --- Round 1.1 ------------------------------------------------------ + * Every helper splits its Lagrange-weighted share into one additive share + * per helper, keeps its own and sends the rest out. */ + for (i = 0; i < N_HELPERS; i++) { + unsigned char session_secrand[32]; + + /* Fresh randomness for every run. Reusing it across runs leaks share + * information. shares_gen wipes it before returning. */ + if (!fill_random(session_secrand, sizeof(session_secrand))) { + printf("Failed to generate randomness\n"); + return 0; + } + if (!secp256k1_frost_enrollment_shares_gen(ctx, helpers[i].shares, helpers[i].params_hash, session_secrand, helpers[i].secshare, thresh_pk, ids, N_HELPERS, helpers[i].id, new_id, N_PARTICIPANTS, THRESHOLD)) { + return 0; + } + secure_erase(session_secrand, sizeof(session_secrand)); + } + + /* --- Round 1.2 ------------------------------------------------------ + * Helper j receives entry j of every other helper's buffer, over a + * confidential and authenticated channel, together with that helper's + * parameters hash. It recomputes the hash itself and compares. */ + for (j = 0; j < N_HELPERS; j++) { + unsigned char all_shares[N_HELPERS * 32]; + unsigned char received_hashes[N_HELPERS * 32]; + uint32_t mismatch_id; + + /* The two buffers take deliberately opposite conventions at this + * helper's own position: the share kept locally IS read, while the + * hash slot is never read and stays zero. The own hash is + * recomputed, never taken from a buffer -- which is what makes this + * a recomputation check rather than a comparison between two strings + * the caller supplied. */ + memset(received_hashes, 0, sizeof(received_hashes)); + for (i = 0; i < N_HELPERS; i++) { + memcpy(&all_shares[32 * i], &helpers[i].shares[32 * j], 32); + if (i != j) { + memcpy(&received_hashes[32 * i], helpers[i].params_hash, 32); + } + } + if (!secp256k1_frost_enrollment_share_agg(ctx, helpers[j].sigma, &mismatch_id, all_shares, received_hashes, thresh_pk, ids, N_HELPERS, helpers[j].id, new_id, N_PARTICIPANTS, THRESHOLD)) { + if (mismatch_id != UINT32_MAX) { + printf("\nHelper %u disagrees about the enrollment parameters\n", mismatch_id); + } + return 0; + } + secure_erase(all_shares, sizeof(all_shares)); + } + + /* --- Round 2 -------------------------------------------------------- + * The enrollee receives one value from each helper, again confidentially + * and authenticated, plus the parameters hash. + * + * The expected public share is derived from the helpers' public shares -- + * which the caller has already validated against an independently + * authenticated threshold public key (see main). Without that step both + * checks below would be circular: t colluding helpers could present a + * consistent but fabricated polynomial and every check would pass on a + * worthless share. */ + for (i = 0; i < N_HELPERS; i++) { + memcpy(&sigmas[32 * i], helpers[i].sigma, 32); + } + memcpy(enrollee_params_hash, helpers[0].params_hash, 32); + + if (!secp256k1_frost_enrollment_pubshare_derive(ctx, new_pubshare, helper_pubshares, ids, N_HELPERS, new_id, N_PARTICIPANTS, THRESHOLD)) { + return 0; + } + if (!secp256k1_frost_enrollment_secshare_gen(ctx, new_secshare, sigmas, thresh_pk, ids, N_HELPERS, new_id, N_PARTICIPANTS, THRESHOLD, enrollee_params_hash, new_pubshare)) { + return 0; + } + secure_erase(sigmas, sizeof(sigmas)); + for (i = 0; i < N_HELPERS; i++) { + secure_erase(helpers[i].shares, sizeof(helpers[i].shares)); + secure_erase(helpers[i].sigma, sizeof(helpers[i].sigma)); + } + return 1; +} + +/* Produce a BIP340 signature with the given signer set and verify it against + * the (untweaked) threshold public key. `secshares` and `pubshares` are + * aligned with `ids`. `n_participants` is the group size AFTER the enrollment, + * which every participant must have updated consistently. */ +static int sign_and_verify(const secp256k1_context* ctx, const uint32_t *ids, unsigned char (*secshares)[32], const secp256k1_pubkey *pubshares, const secp256k1_pubkey *thresh_pk, size_t n_participants, const unsigned char *msg, size_t msglen) { + secp256k1_frost_tweak_cache cache; + secp256k1_frost_secnonce secnonces[N_SIGNERS]; + secp256k1_frost_pubnonce pubnonces[N_SIGNERS]; + const secp256k1_frost_pubnonce *pubnonce_ptrs[N_SIGNERS]; + secp256k1_frost_partial_sig partial_sigs[N_SIGNERS]; + const secp256k1_frost_partial_sig *partial_sig_ptrs[N_SIGNERS]; + secp256k1_frost_aggnonce aggnonce; + secp256k1_frost_session session; + secp256k1_xonly_pubkey tweaked_pk; + unsigned char tweaked_pk32[32]; + unsigned char sig[64]; + int i; + int ret = 0; + + if (!secp256k1_frost_tweak_cache_init(ctx, &cache, thresh_pk)) { + return 0; + } + /* No tweaks are applied here, so the "tweaked" key is the threshold public + * key itself in its x-only encoding. */ + if (!secp256k1_frost_tweaked_pubkey_get(ctx, &tweaked_pk, &cache) + || !secp256k1_xonly_pubkey_serialize(ctx, tweaked_pk32, &tweaked_pk)) { + return 0; + } + + for (i = 0; i < N_SIGNERS; i++) { + unsigned char session_secrand[32]; + if (!fill_random(session_secrand, sizeof(session_secrand))) { + printf("Failed to generate randomness\n"); + return 0; + } + if (!secp256k1_frost_nonce_gen(ctx, &secnonces[i], &pubnonces[i], session_secrand, secshares[i], &pubshares[i], tweaked_pk32, msg, msglen, NULL, 0)) { + return 0; + } + secure_erase(session_secrand, sizeof(session_secrand)); + pubnonce_ptrs[i] = &pubnonces[i]; + partial_sig_ptrs[i] = &partial_sigs[i]; + } + if (!secp256k1_frost_nonce_agg(ctx, &aggnonce, NULL, pubnonce_ptrs, N_SIGNERS)) { + return 0; + } + if (!secp256k1_frost_session_init(ctx, &session, &aggnonce, ids, pubshares, N_SIGNERS, n_participants, THRESHOLD, &cache, msg, msglen)) { + return 0; + } + for (i = 0; i < N_SIGNERS; i++) { + if (!secp256k1_frost_sign(ctx, &partial_sigs[i], &secnonces[i], secshares[i], &session, ids, pubshares, N_SIGNERS, ids[i])) { + goto cleanup; + } + if (!secp256k1_frost_partial_sig_verify(ctx, &partial_sigs[i], &pubnonces[i], &pubshares[i], &session, ids, N_SIGNERS, (size_t)i)) { + goto cleanup; + } + } + if (!secp256k1_frost_partial_sig_agg(ctx, sig, NULL, &session, partial_sig_ptrs, N_SIGNERS)) { + goto cleanup; + } + ret = secp256k1_schnorrsig_verify(ctx, sig, msg, msglen, &tweaked_pk); + +cleanup: + for (i = 0; i < N_SIGNERS; i++) { + secure_erase(&secnonces[i], sizeof(secnonces[i])); + } + return ret; +} + +int main(void) { + secp256k1_context* ctx; + /* Key material of the initial 2-of-3 group. The dealer erases all of it + * at the end of this function. */ + unsigned char threshold_seckey[32]; + unsigned char secshares[N_PARTICIPANTS * 32]; + secp256k1_pubkey thresh_pk; + secp256k1_pubkey pubshares[N_PARTICIPANTS_AFTER]; + + /* The enrollment run for the new participant 3. */ + struct helper helpers[N_HELPERS]; + uint32_t helper_ids[N_HELPERS] = { 0, 1 }; + secp256k1_pubkey helper_pubshares[N_HELPERS]; + unsigned char new_secshare[32]; + secp256k1_pubkey new_pubshare; + + /* The repair run for participant 1. */ + struct helper repair_helpers[N_HELPERS]; + uint32_t repair_ids[N_HELPERS] = { 0, 2 }; + secp256k1_pubkey repair_pubshares[N_HELPERS]; + unsigned char repaired_secshare[32]; + secp256k1_pubkey repaired_pubshare; + + /* The signing session that proves the enrolled participant works. */ + uint32_t signer_ids[N_SIGNERS] = { 2, NEW_ID }; + unsigned char signer_secshares[N_SIGNERS][32]; + secp256k1_pubkey signer_pubshares[N_SIGNERS]; + unsigned char msg[32] = "this_could_be_the_hash_of_a_msg"; + + unsigned char buf[33]; + size_t outputlen; + int i; + + ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + printf("Generating threshold key material for a %d-of-%d group...", THRESHOLD, N_PARTICIPANTS); + fflush(stdout); + if (!trusted_dealer_keygen(ctx, threshold_seckey, secshares, &thresh_pk, pubshares)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + outputlen = sizeof(buf); + if (!secp256k1_ec_pubkey_serialize(ctx, buf, &outputlen, &thresh_pk, SECP256K1_EC_COMPRESSED)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("Threshold public key: "); + print_hex(buf, outputlen); + fflush(stdout); + + /* The new participant obtains the threshold public key from a source it + * authenticates INDEPENDENTLY OF THE HELPERS. In this single-process + * example that is the dealer step above; in a real deployment it might be + * a signed group descriptor, an on-chain commitment, or whatever the group + * already trusts to say what it is. + * + * It then validates the helpers' public shares against that key. This is + * the step that makes the round 2 checks worth anything: without it, the + * enrollee would be checking the helpers' arithmetic against numbers the + * helpers also chose, and t colluding helpers could hand it a consistent + * fake polynomial. */ + printf("Validating the group's public shares against the authenticated key..."); + fflush(stdout); + if (!secp256k1_frost_threshold_info_validate(ctx, &thresh_pk, pubshares, N_PARTICIPANTS, THRESHOLD)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + + /* Enrollment: helpers 0 and 1 give participant 3 a share. */ + for (i = 0; i < N_HELPERS; i++) { + helpers[i].id = helper_ids[i]; + memcpy(helpers[i].secshare, &secshares[32 * helper_ids[i]], 32); + helper_pubshares[i] = pubshares[helper_ids[i]]; + } + printf("Enrolling participant %d with %d helpers...", NEW_ID, N_HELPERS); + fflush(stdout); + if (!enroll(ctx, new_secshare, &new_pubshare, helpers, helper_ids, helper_pubshares, &thresh_pk, NEW_ID)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + + /* n -> n+1 bookkeeping. Every participant must extend its table of public + * shares and update its record of n; participants that disagree about n + * compute different Lagrange coefficients and cannot sign together. */ + pubshares[NEW_ID] = new_pubshare; + printf("Validating the extended %d-participant key material...", N_PARTICIPANTS_AFTER); + fflush(stdout); + if (!secp256k1_frost_threshold_info_validate(ctx, &thresh_pk, pubshares, N_PARTICIPANTS_AFTER, THRESHOLD)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + outputlen = sizeof(buf); + if (!secp256k1_ec_pubkey_serialize(ctx, buf, &outputlen, &new_pubshare, SECP256K1_EC_COMPRESSED)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("New participant's public share: "); + print_hex(buf, outputlen); + fflush(stdout); + + /* Sign with participant 2 and the newly enrolled participant 3. The + * signature verifies against the group's original threshold public key: + * enrollment does not change the group key or any existing share. */ + memcpy(signer_secshares[0], &secshares[32 * signer_ids[0]], 32); + memcpy(signer_secshares[1], new_secshare, 32); + signer_pubshares[0] = pubshares[signer_ids[0]]; + signer_pubshares[1] = pubshares[signer_ids[1]]; + printf("Signing with participants %u and %u...", signer_ids[0], signer_ids[1]); + fflush(stdout); + if (!sign_and_verify(ctx, signer_ids, signer_secshares, signer_pubshares, &thresh_pk, N_PARTICIPANTS_AFTER, msg, sizeof(msg))) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + + /* Repair: participant 1 lost its share. The same three rounds at + * new_id = 1 reproduce it exactly -- the share is a fixed value, f(x_1), + * not a fresh random one. Note that n does NOT change here, and that this + * is the mode where the missing authorization step bites hardest: whoever + * convinces the helpers to run it receives participant 1's actual share. + */ + for (i = 0; i < N_HELPERS; i++) { + repair_helpers[i].id = repair_ids[i]; + memcpy(repair_helpers[i].secshare, &secshares[32 * repair_ids[i]], 32); + repair_pubshares[i] = pubshares[repair_ids[i]]; + } + printf("Repairing participant 1's lost share..."); + fflush(stdout); + if (!enroll(ctx, repaired_secshare, &repaired_pubshare, repair_helpers, repair_ids, repair_pubshares, &thresh_pk, 1)) { + printf("FAILED\n"); + return EXIT_FAILURE; + } + if (memcmp(repaired_secshare, &secshares[32], 32) != 0) { + printf("FAILED (the repaired share differs from the original)\n"); + return EXIT_FAILURE; + } + if (memcmp(&repaired_pubshare, &pubshares[1], sizeof(repaired_pubshare)) != 0) { + printf("FAILED (the repaired public share differs from the original)\n"); + return EXIT_FAILURE; + } + printf("ok\n"); + + /* It's best practice to try to clear secrets from memory after using them. + * This is done because some bugs can allow an attacker to leak memory, for + * example through "out of bounds" array access (see Heartbleed), or the OS + * swapping them to disk. Hence, we overwrite secret key material with + * zeros. + * + * The session randomness was already wiped by shares_gen, and the + * secnonces by frost_sign. */ + secure_erase(threshold_seckey, sizeof(threshold_seckey)); + secure_erase(secshares, sizeof(secshares)); + secure_erase(new_secshare, sizeof(new_secshare)); + secure_erase(repaired_secshare, sizeof(repaired_secshare)); + secure_erase(signer_secshares, sizeof(signer_secshares)); + for (i = 0; i < N_HELPERS; i++) { + secure_erase(&helpers[i], sizeof(helpers[i])); + secure_erase(&repair_helpers[i], sizeof(repair_helpers[i])); + } + secp256k1_context_destroy(ctx); + return EXIT_SUCCESS; +}