diff --git a/include/secp256k1_frost_enrollment.h b/include/secp256k1_frost_enrollment.h new file mode 100644 index 00000000..3d203b72 --- /dev/null +++ b/include/secp256k1_frost_enrollment.h @@ -0,0 +1,372 @@ +#ifndef SECP256K1_FROST_ENROLLMENT_H +#define SECP256K1_FROST_ENROLLMENT_H + +#include "secp256k1.h" +#include "secp256k1_frost.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/** This module implements FROST enrollment, the protocol of Section 4.1.1 of + * the FROST paper (https://eprint.iacr.org/2017/1155.pdf). It converts a + * (t, n) FROST group into a (t, n+1) group without re-running key generation + * and without any participant revealing its secret share. The same mechanism + * repairs a lost share: running it with the target identifier of an existing + * participant reproduces exactly that participant's share. + * + * This code is currently a work in progress. It's not secure nor stable. + * IT IS EXTREMELY DANGEROUS AND RECKLESS TO USE THIS MODULE IN PRODUCTION! + * + * Unlike the frost module, this protocol has no BIP. It is specified only by + * the paper and by the reference proof of concept at + * https://github.com/siv2r/frost-enrollment. The tagged hash strings and the + * parameter serialization used here are this module's own, frozen by the + * regression vectors in src/modules/frost_enrollment/vectors.h. + * + * Identifiers follow the frost module: participants are identified by uint32 + * values 0..n-1, and participant id i sits at polynomial x-coordinate i+1. + * The party receiving a share is identified by `new_id`, which selects the + * mode: + * + * - enrollment: new_id == n_participants. The group grows to n+1 + * participants, so n_participants must be strictly smaller than + * SECP256K1_FROST_MAX_PARTICIPANTS. + * - repair: new_id < n_participants. The share of an existing participant + * is reproduced; n_participants may be SECP256K1_FROST_MAX_PARTICIPANTS. + * + * In both modes new_id must not appear in the helper id set. + * + * A run involves u helpers (threshold <= u <= n_participants), all of which + * must be existing participants, plus the target participant: + * + * 1. Round 1.1: every helper runs + * `secp256k1_frost_enrollment_shares_gen`, keeps the output share at + * its own position and sends each other output share, together with the + * returned parameters hash, to the helper it is aligned with. + * 2. Round 1.2: every helper runs + * `secp256k1_frost_enrollment_share_agg` over the share it kept and the + * shares it received. The function recomputes the parameters hash and + * compares every received hash against it, then sums the shares into a + * single value that is sent to the target participant along with the + * hash. + * 3. Round 2: the target participant runs + * `secp256k1_frost_enrollment_secshare_gen` over the u received values. + * It recomputes the parameters hash from the group key and parameters it + * believes, sums the values into its secret share, and verifies the + * result against the expected public share obtained from + * `secp256k1_frost_enrollment_pubshare_derive`. + * + * Afterwards, in enrollment mode, all participants must consistently update + * their record of n from n to n+1, and extend their table of public shares + * with the output of `secp256k1_frost_enrollment_pubshare_derive`. + * + * SECURITY: the values exchanged in rounds 1 and 2 are additive shares of + * real secret shares. They MUST be transmitted over confidential and + * authenticated channels. Like the chilldkg module, this module handles bytes + * only; transport is the caller's responsibility. Furthermore, the protocol + * contains no authorization step: any party that convinces t helpers to run + * it at a given identifier receives a valid share for that identifier. In + * repair mode that is an existing participant's actual share. Deciding who + * may be enrolled is a caller-side precondition. + * + * It is recommended to read the documentation in this include file carefully. + * Further notes on API usage can be found in + * src/modules/frost_enrollment/frost_enrollment.md. + */ + +/** Compute the enrollment parameters hash. + * + * The hash is + * + * out32 = tagged_hash("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])) + * + * where cbytes is the 33-byte compressed serialization, ser32 is the 32-bit + * big-endian encoding and sorted_ids is `ids` in ascending order. Sorting + * makes the hash independent of the order in which a caller lists the helper + * set; the alignment of every other array in this API follows the caller's + * own `ids` order. + * + * Binding the threshold public key is what makes the hash identify a GROUP + * rather than a tuple of numbers: two unrelated groups that happen to share + * (t, n, ids, new_id) produce different hashes. + * + * Every party computes this value itself. The hash checks performed by + * `secp256k1_frost_enrollment_share_agg` and + * `secp256k1_frost_enrollment_secshare_gen` compare a received hash against a + * freshly recomputed one; they are never an equality test between two + * caller-supplied strings. + * + * This function operates on public data only. + * + * Returns: 0 if the arguments are invalid (duplicate ids, n_ids == 0, n_ids + * greater than SECP256K1_FROST_MAX_PARTICIPANTS, unparseable + * thresh_pk), 1 otherwise + * Args: ctx: pointer to a context object + * Out: out32: pointer to a 32-byte array for the hash. Set to zero + * if this function returns 0. + * In: thresh_pk: pointer to the threshold public key of the group + * ids: array of the u helper identifiers. Every id must be + * unique; the order is irrelevant. + * n_ids: number of helpers u + * new_id: identifier of the participant receiving the share + * n_participants: total number of participants n + * threshold: threshold t + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Round 1.1: generate a helper's enrollment shares. + * + * Computes v = lambda_my_id(x_new) * secshare, where lambda is the Lagrange + * basis polynomial of my_id over the helper set evaluated at the target + * x-coordinate, and splits v into u additive shares that sum to v. + * + * `shares32_out` is aligned with `ids`: entry j is destined for the helper + * with identifier ids[j] and must be sent to it over a confidential, + * authenticated channel, together with `params_hash32_out`. The entry at + * my_id's own position is not sent anywhere; it is passed back into + * `secp256k1_frost_enrollment_share_agg` in round 1.2. + * + * The masking shares are derived from `session_secrand32` by tagged hashing, + * masked with the secret share as defense in depth against a broken random + * number generator (as `secp256k1_frost_nonce_gen` does). The derivation + * binds the parameters hash, and therefore the group key and the whole + * parameter tuple, for DOMAIN SEPARATION: two runs that share a seed but + * differ in group or parameters must not produce the same shares, because + * differencing the round-1.2 outputs would then leak share information. This + * binding cannot detect a parameter disagreement between helpers — these + * values are per-helper private randomness that nothing cross-checks — which + * is what the parameters hash comparison in round 1.2 is for. + * + * `session_secrand32` must be fresh uniformly random 32 bytes for every run. + * It is wiped before this function returns. `secshare32` is left unmodified. + * + * Returns: 0 if the arguments are invalid, 1 otherwise + * Args: ctx: pointer to a context object + * Out: shares32_out: pointer to an array of u*32 bytes for the enrollment + * shares, aligned with `ids`. Set to zero if this + * function returns 0. + * params_hash32_out: pointer to a 32-byte array for the parameters hash, + * identical to what + * `secp256k1_frost_enrollment_params_hash` returns for + * the same arguments. Set to zero if this function + * returns 0. + * In/Out: + * session_secrand32: pointer to a 32-byte array of fresh randomness. Must + * not be reused across runs. Wiped by this function. + * In: secshare32: pointer to the 32-byte secret share of my_id + * thresh_pk: pointer to the threshold public key of the group + * ids: array of the u helper identifiers. Every id must be + * unique, smaller than n_participants and different + * from new_id; the order is irrelevant but fixes the + * alignment of `shares32_out`. + * n_ids: number of helpers u. Must be between threshold and + * n_participants. + * my_id: own identifier. Must appear in `ids`. + * new_id: identifier of the participant receiving the share. + * Must equal n_participants (enrollment) or be smaller + * than it (repair). + * n_participants: total number of participants n. Must be at most + * SECP256K1_FROST_MAX_PARTICIPANTS, and strictly + * smaller in enrollment mode. + * threshold: threshold t. Must be at least 2 (see + * frost_enrollment.md) and at most n_participants. + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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 +) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7); + +/** Round 1.2: check parameter agreement and aggregate a helper's enrollment + * shares. + * + * The function recomputes its own parameters hash from `thresh_pk` and the + * parameter tuple it is given, and compares every entry of + * `received_params_hashes32` against it. On the first disagreement it returns + * 0 and, if `mismatch_id` is not NULL, stores the IDENTIFIER of the + * disagreeing helper there (not an array index, which would be ambiguous + * because identifiers need not be 0..u-1). `mismatch_id` is set to + * UINT32_MAX when the failure has another cause. + * + * Note the deliberately OPPOSITE own-slot conventions of the two u*32 input + * buffers, both of which are aligned with `ids`: + * + * - `all_shares32`: the entry at my_id's position IS read. It is the share + * `secp256k1_frost_enrollment_shares_gen` kept locally. + * - `received_params_hashes32`: the entry at my_id's position is NEVER + * read, and may be left zero. The own hash is recomputed, never taken + * from a buffer. + * + * The asymmetry is the point: it is what makes this a recomputation check + * rather than an equality test among caller-supplied strings, so a caller + * cannot launder a mismatch into a pass by filling its own slot with a + * received value. + * + * `sigma32_out` must be sent to the target participant over a confidential, + * authenticated channel, together with the parameters hash. + * + * Returns: 0 if the arguments are invalid or a parameters hash disagrees, + * 1 otherwise + * Args: ctx: pointer to a context object + * Out: sigma32_out: pointer to a 32-byte array for the aggregated share. + * Set to zero if this function returns 0. + * mismatch_id: pointer to an identifier to store the first helper + * whose parameters hash disagrees, or NULL + * In: all_shares32: pointer to an array of u*32 bytes, aligned with + * `ids`: the share kept locally at my_id's position and + * the shares received from the other helpers at theirs + * received_params_hashes32: pointer to an array of u*32 bytes, aligned + * with `ids`, holding the parameters hash received from + * each other helper. The entry at my_id's position is + * ignored. + * thresh_pk: pointer to the threshold public key of the group + * ids: array of the u helper identifiers, in the same order + * as in round 1.1 + * n_ids: number of helpers u + * my_id: own identifier. Must appear in `ids`. + * new_id: identifier of the participant receiving the share + * n_participants: total number of participants n + * threshold: threshold t + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7); + +/** Derive the public share at the target identifier. + * + * Computes sum_i lambda_i(x_new) * pubshares[i], the value of the group's + * public-share polynomial at the target participant's x-coordinate. This is + * the public counterpart of what the protocol produces, and it is used both + * to verify the new secret share in round 2 and to extend the group's table + * of public shares from n to n+1 entries after an enrollment. + * + * This function operates on public data only. + * + * Returns: 0 if the arguments are invalid or the result is the point at + * infinity, 1 otherwise + * Args: ctx: pointer to a context object + * Out: new_pubshare_out: pointer to a pubkey object for the derived public + * share. Set to zero if this function returns 0. + * In: pubshares: array of u pubkeys, aligned with `ids`, holding the + * public share of each helper + * ids: array of the u helper identifiers + * n_ids: number of helpers u + * new_id: identifier of the participant receiving the share + * n_participants: total number of participants n + * threshold: threshold t + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4); + +/** Round 2: derive the target participant's secret share. + * + * In order: + * + * 1. If `expected_params_hash32` is not NULL, the parameters hash is + * recomputed from `thresh_pk` and the parameter tuple the target + * participant believes, and compared against it. The round-1.2 check + * covers helper against helper only; without this check, all helpers + * could agree on parameters that differ from the ones the target + * expects, or belong to a different group entirely, and the discrepancy + * would surface only as an unexplained failure of the public-share + * check. + * 2. The u values are summed into `secshare32_out`. + * 3. If `expected_pubshare` is not NULL, secshare*G is compared against it. + * + * `expected_pubshare` is load-bearing: it is the only check that a helper + * contributed a correct value. Pass NULL only if the resulting share is + * validated by other means. + * + * PRECONDITION, documented but not enforced: `thresh_pk` must come from a + * source the target participant authenticates independently of the helpers, + * and `expected_pubshare` must be derived from public shares validated + * against it with `secp256k1_frost_threshold_info_validate`. Otherwise both + * checks are circular: t colluding helpers can present a consistent but + * fabricated polynomial, and every check in this function passes on a + * worthless share. See frost_enrollment.md and examples/frost_enrollment.c. + * + * Returns: 0 if the arguments are invalid or a check fails, 1 otherwise + * Args: ctx: pointer to a context object + * Out: secshare32_out: pointer to a 32-byte array for the secret share. Set + * to zero if this function returns 0. + * In: sigmas32: pointer to an array of u*32 bytes, aligned with + * `ids`, holding the value received from each helper + * thresh_pk: pointer to the INDEPENDENTLY AUTHENTICATED threshold + * public key of the group + * ids: array of the u helper identifiers, in the same order + * as `sigmas32` + * n_ids: number of helpers u + * new_id: own identifier, the one the share is being derived + * for + * n_participants: total number of participants n + * threshold: threshold t + * expected_params_hash32: pointer to the 32-byte parameters hash received + * from the helpers, or NULL to skip the comparison + * expected_pubshare: pointer to the expected public share, from + * `secp256k1_frost_enrollment_pubshare_derive`, or NULL + * to skip the verification (not recommended) + */ +SECP256K1_API SECP256K1_WARN_UNUSED_RESULT 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_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5); + +#ifdef __cplusplus +} +#endif + +#endif /* SECP256K1_FROST_ENROLLMENT_H */ diff --git a/src/modules/frost_enrollment/frost_enrollment.md b/src/modules/frost_enrollment/frost_enrollment.md new file mode 100644 index 00000000..335948b9 --- /dev/null +++ b/src/modules/frost_enrollment/frost_enrollment.md @@ -0,0 +1,207 @@ +Notes on the frost_enrollment module API +======================================== + +This module implements FROST enrollment, the protocol of Section 4.1.1 of the +FROST paper (https://eprint.iacr.org/2017/1155.pdf). It turns a (t, n) FROST +group into a (t, n+1) group without re-running key generation and without any +participant revealing its secret share, and it repairs a lost share by running +the same protocol at the identifier of an existing participant. + +Unlike the frost module, which tracks BIP 445, this protocol has no BIP. It is +specified by the paper and by the reference proof of concept at +https://github.com/siv2r/frost-enrollment. The tagged hash strings and the +parameter serialization used here are this module's own. + +The following sections contain additional notes on the API of the +frost_enrollment module (`include/secp256k1_frost_enrollment.h`). A usage +example can be found in `examples/frost_enrollment.c`. + +**This module is experimental.** Do not use it in production. The API should +not be considered stable. + +The protocol +------------ + +Write `f` for the group's secret sharing polynomial of degree t-1, `s_i = +f(x_i)` for the secret share of participant `i` at x-coordinate `x_i = i + 1`, +and `L_i(x)` for the Lagrange basis polynomial of participant `i` over the set +of the u participating helpers. For any u >= t helpers, + + f(x_new) = sum_i L_i(x_new) * s_i + +so the target share is a fixed public linear combination of the helpers' +shares. The protocol computes that sum without any helper learning another's +term. + +1. **Round 1.1** (`secp256k1_frost_enrollment_shares_gen`, each helper). Helper + `i` computes `v_i = L_i(x_new) * s_i` and splits it into u additive shares + `d_{j,i}` with `sum_j d_{j,i} = v_i`, one per helper. It keeps `d_{i,i}` and + sends `d_{j,i}` to helper `j`. + + The random split is the security step. Without it, helper `i` would send + `v_i` itself to the target, who could multiply by `L_i(x_new)^-1` and + recover `s_i`. + +2. **Round 1.2** (`secp256k1_frost_enrollment_share_agg`, each helper). Helper + `j` sums the share it kept and the u-1 shares it received: + `sigma_j = sum_i d_{j,i}`, and sends `sigma_j` to the target. + +3. **Round 2** (`secp256k1_frost_enrollment_secshare_gen`, target). The target + sums the u values it received: + + sum_j sigma_j = sum_j sum_i d_{j,i} = sum_i v_i = f(x_new) + + which is a valid share at `x_new`. + +No individual helper ever sees more than one additive share of any `v_i`, and +the target sees only `u` sums, each of which is masked by every helper's +randomness. + +Enrollment mode and repair mode +------------------------------- + +`new_id` selects the mode, and the bounds differ: + +| mode | condition | bound on n | +|------------|----------------------------|-----------------------------------------------| +| enrollment | `new_id == n_participants` | `n < SECP256K1_FROST_MAX_PARTICIPANTS` | +| repair | `new_id < n_participants` | `n <= SECP256K1_FROST_MAX_PARTICIPANTS` | + +Enrollment needs the stricter bound because the resulting group has n+1 +participants, and a 129-participant group is rejected by the frost module +everywhere. Repair does not change n, so the full range is allowed. + +In both modes `new_id` must not be one of the helper identifiers: a helper +cannot be the target of its own run. + +After a successful enrollment, every participant must update its record of n to +n+1 and append the output of `secp256k1_frost_enrollment_pubshare_derive` to +its table of public shares. Nothing in the library enforces this bookkeeping; +participants that disagree about n will compute different Lagrange +coefficients in later signing sessions. + +Repair mode reproduces the lost share exactly, byte for byte, because the +target x-coordinate determines the value: the share is `f(x_j)`, not a fresh +random value. A repaired participant therefore keeps its old public share. + +Transport +--------- + +The `d` values of round 1.1 and the `sigma` values of round 1.2 are additive +shares of real secret shares. They MUST be transmitted over confidential and +authenticated channels, both helper to helper and helper to target. Like the +chilldkg module, this module handles bytes only; the caller owns transport. + +Authorization +------------- + +The protocol has no authorization step. Any party that convinces t helpers to +run it at `x_new` walks away with a valid share at `x_new`. In repair mode this +is acute: an attacker who impersonates participant `j` and reaches t helpers +obtains participant `j`'s actual share. + +Authenticated channels are necessary but not sufficient — they establish who is +speaking, not that the helpers agreed this party is entitled to a share. The +library cannot enforce the missing step; it is a caller-side precondition, to +be met by the group's existing approval policy out of band. This is the same +gap that motivated ChillDKG's certification round. + +Verification, and what it is worth +---------------------------------- + +`secp256k1_frost_enrollment_secshare_gen` checks the derived share against the +expected public share `sum_i L_i(x_new) * P_i`, where `P_i` is helper `i`'s +public share. This catches any helper that contributed a wrong value, using +public data only. + +The check is only as strong as the provenance of its inputs. If the target +obtains both the helpers' public shares and the threshold public key from those +same helpers, then t colluding helpers can hand it a consistent fabricated +polynomial, and the check passes on a worthless share. The required flow is: + +1. Obtain `thresh_pk` from a source authenticated independently of the helpers. +2. Validate the helpers' public shares against it with + `secp256k1_frost_threshold_info_validate`. +3. Derive `expected_pubshare` from the validated public shares with + `secp256k1_frost_enrollment_pubshare_derive`. +4. Only then run `secp256k1_frost_enrollment_secshare_gen`, passing the same + authenticated `thresh_pk`. + +`examples/frost_enrollment.c` demonstrates exactly this sequence. + +Parameter agreement and domain separation +----------------------------------------- + +Three distinct mechanisms, which should not be conflated: + +1. *Helper against helper.* All u helpers must use byte-identical + `(thresh_pk, ids, new_id, n_participants, threshold)`. The `d` values cannot + cross-check this because they are per-helper private randomness, so + `secp256k1_frost_enrollment_share_agg` recomputes its own parameters hash + from the group key and the tuple, and compares every received hash against + the recomputed value. It never compares two caller-supplied strings, and it + never reads a hash out of its own slot — otherwise a caller could copy a + received hash into that slot and launder a mismatch into a pass. A + disagreement aborts round 1 and reports the disagreeing helper's identifier. + +2. *Helper against target.* The round-1.2 check says nothing about the target's + view. `secp256k1_frost_enrollment_secshare_gen` therefore recomputes the + hash from the authenticated `thresh_pk` and the tuple the target believes, + and compares it against the helpers' hash. This catches "every helper agreed + on new_id = 7 while the target expected 5", and it catches a target being + fed a run from an entirely different group, before any secret arithmetic + happens. + +3. *Domain separation.* The share-splitting randomness binds the parameters + hash, and therefore the group key and the whole tuple. Two runs that share a + `session_secrand32` but differ in group or parameters would otherwise produce + identical `d` sets, and differencing the `sigma` values would leak share + information. + +Because the hash commits to `thresh_pk` and not only to the integers, agreement +covers group identity: two unrelated groups with the same `(t, n, ids, new_id)` +produce different hashes. + +What none of this covers: once the parameter gates pass, a *corrupted* `sigma` +value is still only detectable, by the public-share check in round 2, and not +attributable to a particular helper. + +Threshold must be at least 2 +---------------------------- + +The frost module accepts `threshold >= 1`. This module refuses `threshold < 2`. + +The reason is not that t = 1 is a weak threshold — a lone member of a 1-of-n +group can already sign anything. It is that this API permits any +`threshold <= n_ids`, so t = 1 would permit u = 1, and at u = 1 the additive +split degenerates to a single share: the lone helper sends the unsplit `v_1`, +which at t = 1 is the entire group secret. The protocol's security property +would be vacuous rather than merely weak. Requiring `threshold >= 2` forces +`n_ids >= 2`, which is what actually guarantees the split is non-degenerate. + +Randomness +---------- + +`secp256k1_frost_enrollment_shares_gen` requires fresh uniform randomness for +every run. Reusing `session_secrand32` across runs leaks share information; the +domain-separation binding above mitigates this for runs that differ in their +parameters, but does not excuse it. The function wipes the seed before +returning and masks the derived randomness with the secret share, so that a +weak random number generator alone does not reveal the split. + +No key refresh +-------------- + +Enrollment does not change the polynomial and does not change any existing +share. The group's long-term security assumptions are unchanged: an adversary +who had collected shares before an enrollment still holds valid shares +afterwards. This protocol adds a participant; it is not a proactive refresh. + +Constant time +------------- + +All arithmetic on secret data uses the constant-time `secp256k1_scalar` +operations. Every branch driven by identifiers, counts or hash comparisons +operates on public values. Secret intermediates are cleansed with +`secp256k1_memclear_explicit` before the functions return, and outputs are +zeroed on every failure path.