diff --git a/src/modules/frost_enrollment/Makefile.am.include b/src/modules/frost_enrollment/Makefile.am.include index f8f13831..3232405f 100644 --- a/src/modules/frost_enrollment/Makefile.am.include +++ b/src/modules/frost_enrollment/Makefile.am.include @@ -2,3 +2,4 @@ include_HEADERS += include/secp256k1_frost_enrollment.h noinst_HEADERS += src/modules/frost_enrollment/main_impl.h noinst_HEADERS += src/modules/frost_enrollment/enrollment_impl.h noinst_HEADERS += src/modules/frost_enrollment/tests_impl.h +noinst_HEADERS += src/modules/frost_enrollment/vectors.h diff --git a/src/modules/frost_enrollment/tests_impl.h b/src/modules/frost_enrollment/tests_impl.h index 159f44d1..a92d6d83 100644 --- a/src/modules/frost_enrollment/tests_impl.h +++ b/src/modules/frost_enrollment/tests_impl.h @@ -7,6 +7,9 @@ #define SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H #include "../../../include/secp256k1_frost_enrollment.h" +#include "../../../include/secp256k1_schnorrsig.h" + +#include "vectors.h" /* Everything one enrollment run needs, so a test can set one up in a line and * then poke at individual pieces. */ @@ -19,7 +22,8 @@ typedef struct { secp256k1_pubkey thresh_pk; uint32_t ids[SECP256K1_FROST_MAX_PARTICIPANTS]; - /* shares[i][j] is what helper ids[i] produced for helper ids[j]. */ + /* shares[i] is helper ids[i]'s round 1.1 output buffer, aligned with ids: + * entry j is what helper ids[i] produced for helper ids[j]. */ unsigned char shares[SECP256K1_FROST_MAX_PARTICIPANTS][SECP256K1_FROST_MAX_PARTICIPANTS * 32]; unsigned char params_hashes[SECP256K1_FROST_MAX_PARTICIPANTS][32]; unsigned char sigmas[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; @@ -58,35 +62,59 @@ static void frost_enrollment_test_round1_gen(frost_enrollment_test_run *r) { unsigned char secrand[32]; testrand256(secrand); CHECK(secp256k1_frost_enrollment_shares_gen(CTX, r->shares[i], r->params_hashes[i], secrand, r->secshares[r->ids[i]], &r->thresh_pk, r->ids, r->u, r->ids[i], r->new_id, r->n, (uint32_t)r->t) == 1); + /* The seed is consumed by the call. */ CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand))); } } -/* 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. */ +/* Assembles helper j's round 1.2 inputs out of the round 1.1 outputs: the + * share kept at its own position, the shares received at the others', and the + * received parameters hashes with its own slot left zero. */ +static void frost_enrollment_test_collect(const frost_enrollment_test_run *r, size_t j, unsigned char *all_shares, unsigned char *received) { + size_t i; + + memset(received, 0, r->u * 32); + for (i = 0; i < r->u; i++) { + memcpy(&all_shares[32 * i], &r->shares[i][32 * j], 32); + if (i != j) { + memcpy(&received[32 * i], r->params_hashes[i], 32); + } + } +} + +/* Runs round 1.2 for every helper. */ static void frost_enrollment_test_round1_agg(frost_enrollment_test_run *r) { - size_t i, j; + size_t j; for (j = 0; j < r->u; j++) { unsigned char all_shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; unsigned char received[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; uint32_t mismatch_id = 0; - 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); - } - } + frost_enrollment_test_collect(r, j, all_shares, received); CHECK(secp256k1_frost_enrollment_share_agg(CTX, &r->sigmas[32 * j], &mismatch_id, all_shares, received, &r->thresh_pk, r->ids, r->u, r->ids[j], r->new_id, r->n, (uint32_t)r->t) == 1); CHECK(mismatch_id == UINT32_MAX); } } +/* Gathers the helpers' public shares into an array aligned with ids. The + * run's own table is indexed by participant id, which only coincides with the + * ids alignment when the helper set happens to be 0..u-1 -- exactly the + * confusion the API documentation warns about. */ +static void frost_enrollment_test_helper_pubshares(const frost_enrollment_test_run *r, secp256k1_pubkey *out) { + size_t i; + + for (i = 0; i < r->u; i++) { + out[i] = r->pubshares[r->ids[i]]; + } +} + /* Runs round 2, with both optional checks enabled. */ static void frost_enrollment_test_round2(frost_enrollment_test_run *r) { - 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); + secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS]; + + frost_enrollment_test_helper_pubshares(r, helper_pubshares); + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r->new_pubshare, helper_pubshares, r->ids, r->u, r->new_id, r->n, (uint32_t)r->t) == 1); CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, r->new_secshare, r->sigmas, &r->thresh_pk, r->ids, r->u, r->new_id, r->n, (uint32_t)r->t, r->params_hashes[0], &r->new_pubshare) == 1); } @@ -120,10 +148,50 @@ static void frost_enrollment_test_check_reconstruction(const uint32_t *ids, cons 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) { +/* Produces and verifies a BIP340 signature with the given signer set. shares + * and pubshares must be aligned with ids. */ +static void frost_enrollment_test_sign(const uint32_t *ids, const unsigned char *const *shares, const secp256k1_pubkey *pubshares, size_t n_signers, size_t n_participants, size_t threshold, const secp256k1_pubkey *thresh_pk) { + secp256k1_frost_tweak_cache cache; + secp256k1_frost_secnonce secnonces[SECP256K1_FROST_MAX_PARTICIPANTS]; + secp256k1_frost_pubnonce pubnonces[SECP256K1_FROST_MAX_PARTICIPANTS]; + const secp256k1_frost_pubnonce *pubnonce_ptrs[SECP256K1_FROST_MAX_PARTICIPANTS]; + secp256k1_frost_partial_sig partial_sigs[SECP256K1_FROST_MAX_PARTICIPANTS]; + const secp256k1_frost_partial_sig *partial_sig_ptrs[SECP256K1_FROST_MAX_PARTICIPANTS]; + secp256k1_frost_aggnonce aggnonce; + secp256k1_frost_session session; + secp256k1_xonly_pubkey tweaked_pk; + unsigned char tweaked_pk32[32]; + unsigned char msg[32]; + unsigned char sig64[64]; + size_t i; + + testrand256(msg); + CHECK(secp256k1_frost_tweak_cache_init(CTX, &cache, thresh_pk) == 1); + CHECK(secp256k1_frost_tweaked_pubkey_get(CTX, &tweaked_pk, &cache) == 1); + CHECK(secp256k1_xonly_pubkey_serialize(CTX, tweaked_pk32, &tweaked_pk) == 1); + + for (i = 0; i < n_signers; i++) { + unsigned char secrand[32]; + testrand256(secrand); + CHECK(secp256k1_frost_nonce_gen(CTX, &secnonces[i], &pubnonces[i], secrand, shares[i], &pubshares[i], tweaked_pk32, msg, sizeof(msg), NULL, 0) == 1); + pubnonce_ptrs[i] = &pubnonces[i]; + } + CHECK(secp256k1_frost_nonce_agg(CTX, &aggnonce, NULL, pubnonce_ptrs, n_signers) == 1); + CHECK(secp256k1_frost_session_init(CTX, &session, &aggnonce, ids, pubshares, n_signers, n_participants, (uint32_t)threshold, &cache, msg, sizeof(msg)) == 1); + for (i = 0; i < n_signers; i++) { + CHECK(secp256k1_frost_sign(CTX, &partial_sigs[i], &secnonces[i], shares[i], &session, ids, pubshares, n_signers, ids[i]) == 1); + CHECK(secp256k1_frost_partial_sig_verify(CTX, &partial_sigs[i], &pubnonces[i], &pubshares[i], &session, ids, n_signers, i) == 1); + partial_sig_ptrs[i] = &partial_sigs[i]; + } + CHECK(secp256k1_frost_partial_sig_agg(CTX, sig64, NULL, &session, partial_sig_ptrs, n_signers) == 1); + CHECK(secp256k1_schnorrsig_verify(CTX, sig64, msg, sizeof(msg), &tweaked_pk) == 1); +} + +/* PoC test_generate_frost_share: a 2-of-3 group grows to 2-of-4, and the new + * share sits on the same polynomial as the old ones. Every threshold-sized + * subset containing the new participant reconstructs the original threshold + * secret, and so does the untouched original pair. */ +static void run_frost_enrollment_reconstruction_test(void) { frost_enrollment_test_run r; const unsigned char *shares[2]; uint32_t ids[2]; @@ -131,8 +199,6 @@ static void run_frost_enrollment_smoke_test(void) { 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; @@ -140,7 +206,6 @@ static void run_frost_enrollment_smoke_test(void) { 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]; @@ -148,6 +213,329 @@ static void run_frost_enrollment_smoke_test(void) { frost_enrollment_test_check_reconstruction(ids, shares, 2, &r.thresh_pk); } +/* PoC test_sign: a real BIP340 signature from a signer set that includes the + * enrolled participant, over the unchanged threshold public key. Also the + * n -> n+1 bookkeeping: the extended public share table must still satisfy + * secp256k1_frost_threshold_info_validate at n+1. */ +static void run_frost_enrollment_signing_test(void) { + frost_enrollment_test_run r; + secp256k1_pubkey pubshares[4]; + const unsigned char *shares[2]; + uint32_t ids[2]; + size_t i; + + frost_enrollment_test_full_run(&r, 3, 2, 2, 3); + + /* The extended table: the three original public shares plus the derived + * one at the new identifier. */ + for (i = 0; i < 3; i++) { + pubshares[i] = r.pubshares[i]; + } + pubshares[3] = r.new_pubshare; + CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, pubshares, 4, 2) == 1); + + /* Signer set {2, 3}: one original participant and the new one. */ + ids[0] = 2; + ids[1] = 3; + shares[0] = r.secshares[2]; + shares[1] = r.new_secshare; + { + secp256k1_pubkey signer_pubshares[2]; + signer_pubshares[0] = pubshares[2]; + signer_pubshares[1] = pubshares[3]; + frost_enrollment_test_sign(ids, shares, signer_pubshares, 2, 4, 2, &r.thresh_pk); + } +} + +/* Repair mode: participant 1 "loses" its share and the same protocol run at + * new_id = 1 reproduces it, byte for byte. The share is f(x_1), a fixed value, + * not a fresh random one, so anything short of exact equality is a bug. */ +static void run_frost_enrollment_repair_test(void) { + frost_enrollment_test_run r; + + frost_enrollment_test_full_run(&r, 3, 2, 2, 1); + /* The helper set is {0, 2}: deal() skips the target identifier. */ + CHECK(r.ids[0] == 0); + CHECK(r.ids[1] == 2); + CHECK(secp256k1_memcmp_var(r.new_secshare, r.secshares[1], 32) == 0); + /* And the repaired participant keeps its old public share. */ + CHECK(secp256k1_memcmp_var(&r.new_pubshare, &r.pubshares[1], sizeof(r.new_pubshare)) == 0); +} + +/* An oversized helper set produces the same share: Lagrange interpolation at + * the target is exact for any u >= t points on a degree-(t-1) polynomial. */ +static void run_frost_enrollment_oversized_set_test(void) { + frost_enrollment_test_run r2, r3; + unsigned char share_u2[32]; + + /* Enroll id 3 into a 2-of-3 group with two helpers, then with all three, + * from the same dealt key material. */ + frost_enrollment_test_full_run(&r2, 3, 2, 2, 3); + memcpy(share_u2, r2.new_secshare, 32); + + r3 = r2; + r3.u = 3; + r3.ids[2] = 2; + frost_enrollment_test_round1_gen(&r3); + frost_enrollment_test_round1_agg(&r3); + frost_enrollment_test_round2(&r3); + CHECK(secp256k1_memcmp_var(share_u2, r3.new_secshare, 32) == 0); + /* The derived public share does not depend on the helper set either. */ + CHECK(secp256k1_memcmp_var(&r2.new_pubshare, &r3.new_pubshare, sizeof(r2.new_pubshare)) == 0); +} + +/* A corrupted sigma value must be caught by the public-share check, and the + * output must be wiped rather than left holding a wrong share. Tampered + * pubshares are caught earlier, by the validation step the recommended flow + * runs before the protocol starts. */ +static void run_frost_enrollment_fault_injection_test(void) { + frost_enrollment_test_run r; + unsigned char sigmas[2 * 32]; + unsigned char out[32]; + secp256k1_pubkey tampered[3]; + size_t i; + + /* A clean run first, so that r.new_secshare holds the share the corrupted + * runs below must fail to reproduce. */ + frost_enrollment_test_full_run(&r, 3, 2, 2, 3); + + for (i = 0; i < 2; i++) { + memcpy(sigmas, r.sigmas, sizeof(sigmas)); + sigmas[32 * i] ^= 1; + memset(out, 0xff, sizeof(out)); + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, r.params_hashes[0], &r.new_pubshare) == 0); + CHECK(secp256k1_is_zero_array(out, sizeof(out))); + /* Without the public-share check nothing notices: the sum is a + * perfectly well-formed scalar, just the wrong one. This is what + * makes expected_pubshare load-bearing rather than optional. */ + memset(out, 0xff, sizeof(out)); + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, r.params_hashes[0], NULL) == 1); + CHECK(secp256k1_memcmp_var(out, r.new_secshare, 32) != 0); + } + + /* The recommended flow, not just the module: a tampered public share is + * rejected by secp256k1_frost_threshold_info_validate against the + * independently authenticated threshold public key, before enrollment + * begins. */ + for (i = 0; i < 3; i++) { + unsigned char ser[33]; + size_t len = sizeof(ser); + memcpy(tampered, r.pubshares, sizeof(tampered)); + CHECK(secp256k1_ec_pubkey_serialize(CTX, ser, &len, &tampered[i], SECP256K1_EC_COMPRESSED) == 1); + /* Flip to the other point of the same x-coordinate: still a valid + * pubkey, but no longer on the group's polynomial. */ + ser[0] ^= 1; + CHECK(secp256k1_ec_pubkey_parse(CTX, &tampered[i], ser, len) == 1); + CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, tampered, 3, 2) == 0); + } +} + +/* Parameter and group agreement, from four angles. */ +static void run_frost_enrollment_mismatch_test(void) { + frost_enrollment_test_run r, other; + unsigned char all_shares[2 * 32]; + unsigned char received[2 * 32]; + unsigned char sigma[32]; + unsigned char out[32]; + unsigned char bad_shares[2 * 32]; + unsigned char bad_hash[32]; + unsigned char good_hash[32]; + unsigned char secrand[32]; + uint32_t mismatch_id; + size_t j; + + frost_enrollment_test_deal(&r, 4, 2, 2, 4); + + /* (a) Helper 0 runs round 1.1 for a different target. Helper 1's round + * 1.2 must abort and name helper 0 by IDENTIFIER. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, good_hash, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t) == 1); + testrand256(secrand); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, bad_shares, bad_hash, secrand, r.secshares[0], &r.thresh_pk, r.ids, r.u, r.ids[0], 3, r.n, (uint32_t)r.t) == 1); + CHECK(secp256k1_memcmp_var(bad_hash, good_hash, 32) != 0); + frost_enrollment_test_round1_gen(&r); + CHECK(secp256k1_memcmp_var(r.params_hashes[0], good_hash, 32) == 0); + memcpy(r.params_hashes[0], bad_hash, 32); + memcpy(r.shares[0], bad_shares, sizeof(bad_shares)); + + frost_enrollment_test_collect(&r, 1, all_shares, received); + mismatch_id = 0; + memset(sigma, 0xff, sizeof(sigma)); + CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma, &mismatch_id, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 0); + CHECK(mismatch_id == r.ids[0]); + CHECK(secp256k1_is_zero_array(sigma, sizeof(sigma))); + + /* (b) A caller that ignores the abort and finishes round 1.2 anyway still + * does not end up with a usable share: the public-share check catches the + * inconsistent sum. Defence in depth, rather than a test of the test's + * own control flow. */ + for (j = 0; j < r.u; j++) { + size_t k; + frost_enrollment_test_collect(&r, j, all_shares, received); + /* Simulate the gate having passed: every helper is handed the hash it + * expects, while helper 0's mismatched delta values stay in place. */ + for (k = 0; k < r.u; k++) { + if (k != j) { + memcpy(&received[32 * k], good_hash, 32); + } + } + CHECK(secp256k1_frost_enrollment_share_agg(CTX, &r.sigmas[32 * j], NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[j], r.new_id, r.n, (uint32_t)r.t) == 1); + } + { + secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS]; + frost_enrollment_test_helper_pubshares(&r, helper_pubshares); + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r.new_pubshare, helper_pubshares, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t) == 1); + } + memset(out, 0xff, sizeof(out)); + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, r.sigmas, &r.thresh_pk, r.ids, r.u, r.new_id, r.n, (uint32_t)r.t, good_hash, &r.new_pubshare) == 0); + CHECK(secp256k1_is_zero_array(out, sizeof(out))); + + /* (c) The helpers agree with each other but not with the target: a clean + * run for new_id = 3, handed to a target that believes it is 4. Round 1.2 + * passed everywhere; round 2's own recomputation is what catches it. */ + frost_enrollment_test_full_run(&other, 4, 2, 2, 3); + memset(out, 0xff, sizeof(out)); + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, out, other.sigmas, &other.thresh_pk, other.ids, other.u, 4, other.n, (uint32_t)other.t, other.params_hashes[0], NULL) == 0); + CHECK(secp256k1_is_zero_array(out, sizeof(out))); + + /* (d) Group binding. Two groups with identical (t, n, ids, new_id) get + * different parameters hashes, because the hash commits to the threshold + * public key -- and a hash from one group fails round 1.2 in the other. */ + { + frost_enrollment_test_run a, b; + unsigned char hash_a[32], hash_b[32]; + + frost_enrollment_test_deal(&a, 3, 2, 2, 3); + frost_enrollment_test_deal(&b, 3, 2, 2, 3); + CHECK(secp256k1_memcmp_var(&a.thresh_pk, &b.thresh_pk, sizeof(a.thresh_pk)) != 0); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash_a, &a.thresh_pk, a.ids, a.u, a.new_id, a.n, (uint32_t)a.t) == 1); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash_b, &b.thresh_pk, b.ids, b.u, b.new_id, b.n, (uint32_t)b.t) == 1); + CHECK(secp256k1_memcmp_var(hash_a, hash_b, 32) != 0); + + frost_enrollment_test_round1_gen(&a); + frost_enrollment_test_collect(&a, 1, all_shares, received); + memcpy(&received[0], hash_b, 32); + mismatch_id = 0; + CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma, &mismatch_id, all_shares, received, &a.thresh_pk, a.ids, a.u, a.ids[1], a.new_id, a.n, (uint32_t)a.t) == 0); + CHECK(mismatch_id == a.ids[0]); + } +} + +/* The own slot of received_params_hashes32 is never read, so a caller cannot + * fill it with a received hash and launder a mismatch into a pass. */ +static void run_frost_enrollment_own_slot_test(void) { + frost_enrollment_test_run r; + unsigned char all_shares[2 * 32]; + unsigned char received[2 * 32]; + unsigned char sigma_zero[32], sigma_garbage[32]; + + frost_enrollment_test_deal(&r, 3, 2, 2, 3); + frost_enrollment_test_round1_gen(&r); + + /* Helper 1 aggregates with its own slot zero, as documented. */ + frost_enrollment_test_collect(&r, 1, all_shares, received); + CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma_zero, NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 1); + + /* And again with garbage in that slot. Same result: it is not read. */ + memset(&received[32], 0xa5, 32); + CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma_garbage, NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 1); + CHECK(secp256k1_memcmp_var(sigma_zero, sigma_garbage, 32) == 0); + + /* But a wrong hash in a slot that IS read still aborts, even if the same + * wrong hash sits in the own slot -- the own hash is recomputed, so there + * is nothing to agree with. */ + memset(&received[0], 0xa5, 32); + CHECK(secp256k1_frost_enrollment_share_agg(CTX, sigma_garbage, NULL, all_shares, received, &r.thresh_pk, r.ids, r.u, r.ids[1], r.new_id, r.n, (uint32_t)r.t) == 0); +} + +/* Invalid parameter tuples, including the two deliberate divergences from the + * frost module (threshold >= 2, and enrollment refused at n = 128). */ +static void run_frost_enrollment_invalid_params_test(void) { + frost_enrollment_test_run r; + unsigned char hash32[32]; + unsigned char shares[4 * 32]; + unsigned char secrand[32]; + uint32_t ids[4]; + + frost_enrollment_test_deal(&r, 4, 2, 3, 4); + + /* The valid baseline. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 2) == 1); + + /* Duplicate ids. */ + memcpy(ids, r.ids, 3 * sizeof(ids[0])); + ids[2] = ids[0]; + memset(hash32, 0xff, sizeof(hash32)); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, ids, 3, 4, 4, 2) == 0); + CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32))); + + /* new_id among the helpers. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, r.ids[1], 4, 2) == 0); + + /* new_id past the end: neither enrollment (== n) nor repair (< n). */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 5, 4, 2) == 0); + + /* Too few helpers, and more helpers than participants. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 1, 4, 4, 2) == 0); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 2, 2) == 0); + + /* threshold = 1 is refused, unlike in the frost module. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 1) == 0); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 0) == 0); + /* threshold above the participant count. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, 4, 5) == 0); + + /* A helper id outside 0..n-1. */ + memcpy(ids, r.ids, 3 * sizeof(ids[0])); + ids[2] = 4; + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, ids, 3, 5, 4, 2) == 0); + + /* n above the maximum, and n_ids above the maximum. Both must be caught + * in production builds; neither may ride on the VERIFY_CHECK inside + * secp256k1_frost_sort_ids. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, 3, 4, SECP256K1_FROST_MAX_PARTICIPANTS + 1, 2) == 0); + memset(hash32, 0xff, sizeof(hash32)); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, r.ids, SECP256K1_FROST_MAX_PARTICIPANTS + 1, 4, 4, 2) == 0); + CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32))); + + /* Mode-specific bounds at the maximum: enrollment would produce a + * 129-participant group and is refused; repair leaves n alone and is + * accepted. Both use a helper set of exactly t, so no oversized array is + * involved either way. */ + { + uint32_t big_ids[2]; + big_ids[0] = 0; + big_ids[1] = 1; + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, big_ids, 2, SECP256K1_FROST_MAX_PARTICIPANTS, SECP256K1_FROST_MAX_PARTICIPANTS, 2) == 0); + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, big_ids, 2, SECP256K1_FROST_MAX_PARTICIPANTS - 1, SECP256K1_FROST_MAX_PARTICIPANTS, 2) == 1); + /* One below the maximum, enrollment is fine again. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, big_ids, 2, SECP256K1_FROST_MAX_PARTICIPANTS - 1, SECP256K1_FROST_MAX_PARTICIPANTS - 1, 2) == 1); + } + + /* An uninitialized threshold public key: secp256k1_pubkey_load treats + * that as an API misuse and fires the illegal-argument callback, as it + * does everywhere else in the library. */ + { + secp256k1_pubkey zero_pk; + memset(&zero_pk, 0, sizeof(zero_pk)); + CHECK_ILLEGAL(CTX, secp256k1_frost_enrollment_params_hash(CTX, hash32, &zero_pk, r.ids, 3, 4, 4, 2)); + CHECK(secp256k1_is_zero_array(hash32, sizeof(hash32))); + } + + /* The other entry points reject the same tuples. */ + testrand256(secrand); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, 3, r.ids[0], 4, 4, 1) == 0); + CHECK(secp256k1_is_zero_array(secrand, sizeof(secrand))); + /* my_id must be one of the helpers. */ + testrand256(secrand); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, r.secshares[0], &r.thresh_pk, r.ids, 3, 3, 4, 4, 2) == 0); + { + secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS]; + frost_enrollment_test_helper_pubshares(&r, helper_pubshares); + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r.new_pubshare, helper_pubshares, r.ids, 3, 4, 4, 1) == 0); + } + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, shares, r.sigmas, &r.thresh_pk, r.ids, 3, 4, 4, 1, NULL, NULL) == 0); +} + /* Every entry point must reject an empty helper set and leave its output * zeroed. This also checks that all five symbols are reachable from the test * binary. */ @@ -184,9 +572,261 @@ static void run_frost_enrollment_rejects_empty_set_test(void) { CHECK(secp256k1_is_zero_array(buf32, sizeof(buf32))); } +/* pubshare_derive is a wrapper over frost's derive_pubshare_at. This pins its + * argument plumbing: at an existing participant's identifier it must return + * exactly that participant's public share, and it must agree with a direct + * call to the function it wraps. */ +static void run_frost_enrollment_pubshare_derive_test(void) { + frost_enrollment_test_run r; + secp256k1_pubkey derived, aligned[3]; + secp256k1_ge points[3], expected, got; + secp256k1_gej resultj; + secp256k1_scalar x; + uint32_t ids[3]; + size_t i, k; + + frost_enrollment_test_deal(&r, 4, 3, 3, 4); + + /* Repair mode at every existing identifier: the derived public share is + * the one the dealer produced. */ + for (i = 0; i < 4; i++) { + k = 0; + { + size_t m; + for (m = 0; m < 4 && k < 3; m++) { + if (m != i) { + ids[k] = (uint32_t)m; + k++; + } + } + } + for (k = 0; k < 3; k++) { + aligned[k] = r.pubshares[ids[k]]; + } + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &derived, aligned, ids, 3, (uint32_t)i, 4, 3) == 1); + CHECK(secp256k1_pubkey_load(CTX, &expected, &r.pubshares[i]) == 1); + CHECK(secp256k1_pubkey_load(CTX, &got, &derived) == 1); + CHECK(secp256k1_ge_eq_var(&expected, &got) == 1); + } + + /* And the wrapper passes the target identifier through unchanged. */ + ids[0] = 0; + ids[1] = 1; + ids[2] = 2; + for (k = 0; k < 3; k++) { + aligned[k] = r.pubshares[ids[k]]; + } + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &derived, aligned, ids, 3, 4, 4, 3) == 1); + for (i = 0; i < 3; i++) { + CHECK(secp256k1_pubkey_load(CTX, &points[i], &r.pubshares[i]) == 1); + } + secp256k1_scalar_set_int(&x, 4); + CHECK(secp256k1_frost_derive_pubshare_at(&resultj, ids, points, 3, &x) == 1); + secp256k1_ge_set_gej_var(&expected, &resultj); + CHECK(secp256k1_pubkey_load(CTX, &got, &derived) == 1); + CHECK(secp256k1_ge_eq_var(&expected, &got) == 1); +} + +/* One random (t, n, u) round trip. Each helper is given the identifier set in + * its own random order, which must not change the parameters hash -- while the + * delta buffers stay aligned with whatever order that helper used. */ +static void frost_enrollment_random_iteration(void) { + frost_enrollment_test_run r; + uint32_t perm[SECP256K1_FROST_MAX_PARTICIPANTS][SECP256K1_FROST_MAX_PARTICIPANTS]; + unsigned char hash32[32]; + size_t n, t, u, i, j; + uint32_t new_id; + + /* 2 <= t <= u <= n <= 7 */ + t = 2 + testrand_int(3); + u = t + testrand_int(4); + n = u + testrand_int(8 - (unsigned int)u); + if (n > 7) { + n = 7; + } + if (u > n) { + u = n; + } + /* Enrollment half the time, repair the other half. */ + new_id = testrand_bits(1) ? (uint32_t)n : (uint32_t)testrand_int((unsigned int)n); + if ((size_t)new_id < n && u > n - 1) { + u = n - 1; + } + if (u < t) { + return; + } + + frost_enrollment_test_deal(&r, n, t, u, new_id); + + /* Give every helper its own shuffled view of the identifier set. */ + for (i = 0; i < u; i++) { + memcpy(perm[i], r.ids, u * sizeof(r.ids[0])); + for (j = u; j > 1; j--) { + size_t k = testrand_int((unsigned int)j); + uint32_t tmp = perm[i][j - 1]; + perm[i][j - 1] = perm[i][k]; + perm[i][k] = tmp; + } + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &r.thresh_pk, perm[i], u, new_id, n, (uint32_t)t) == 1); + } + + /* Round 1.1 in each helper's own order. */ + for (i = 0; i < u; i++) { + unsigned char secrand[32]; + testrand256(secrand); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, r.shares[i], r.params_hashes[i], secrand, r.secshares[r.ids[i]], &r.thresh_pk, perm[i], u, r.ids[i], new_id, n, (uint32_t)t) == 1); + /* Order-independent: same digest as the canonical order. */ + CHECK(secp256k1_memcmp_var(r.params_hashes[i], hash32, 32) == 0); + } + + /* Round 1.2, translating each helper's alignment into the canonical one. + * all_shares[k] must be what helper ids[k] produced for helper ids[j], + * which sits at helper k's own position for ids[j]. */ + for (j = 0; j < u; j++) { + unsigned char all_shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + unsigned char received[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + + memset(received, 0, u * 32); + for (i = 0; i < u; i++) { + size_t pos; + for (pos = 0; pos < u; pos++) { + if (perm[i][pos] == r.ids[j]) { + break; + } + } + CHECK(pos < u); + memcpy(&all_shares[32 * i], &r.shares[i][32 * pos], 32); + if (i != j) { + memcpy(&received[32 * i], r.params_hashes[i], 32); + } + } + CHECK(secp256k1_frost_enrollment_share_agg(CTX, &r.sigmas[32 * j], NULL, all_shares, received, &r.thresh_pk, r.ids, u, r.ids[j], new_id, n, (uint32_t)t) == 1); + } + + { + secp256k1_pubkey helper_pubshares[SECP256K1_FROST_MAX_PARTICIPANTS]; + frost_enrollment_test_helper_pubshares(&r, helper_pubshares); + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &r.new_pubshare, helper_pubshares, r.ids, u, new_id, n, (uint32_t)t) == 1); + } + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, r.new_secshare, r.sigmas, &r.thresh_pk, r.ids, u, new_id, n, (uint32_t)t, hash32, &r.new_pubshare) == 1); + + if ((size_t)new_id < n) { + /* Repair reproduces the lost share exactly. */ + CHECK(secp256k1_memcmp_var(r.new_secshare, r.secshares[new_id], 32) == 0); + } else { + /* Enrollment: every t-subset of the extended group that contains the + * new participant reconstructs the same threshold secret, and the + * extended public share table still validates at n+1. */ + secp256k1_pubkey extended[SECP256K1_FROST_MAX_PARTICIPANTS]; + uint32_t sub_ids[SECP256K1_FROST_MAX_PARTICIPANTS]; + const unsigned char *sub_shares[SECP256K1_FROST_MAX_PARTICIPANTS]; + + for (i = 0; i < n; i++) { + extended[i] = r.pubshares[i]; + } + extended[n] = r.new_pubshare; + CHECK(secp256k1_frost_threshold_info_validate(CTX, &r.thresh_pk, extended, n + 1, (uint32_t)t) == 1); + + for (i = 0; i + t <= n + 1; i++) { + for (j = 0; j + 1 < t; j++) { + sub_ids[j] = (uint32_t)(i + j); + sub_shares[j] = r.secshares[i + j]; + } + sub_ids[t - 1] = new_id; + sub_shares[t - 1] = r.new_secshare; + if (sub_ids[t - 2] >= new_id) { + continue; + } + frost_enrollment_test_check_reconstruction(sub_ids, sub_shares, t, &r.thresh_pk); + } + } +} + +/* The frozen regression vectors. They pin the two tag strings, the parameters + * hash encoding and the share-splitting derivation: any change to those is a + * vector-breaking change, and this is where it shows up. */ +static void run_frost_enrollment_vectors_test(void) { + size_t c; + + for (c = 0; c < sizeof(frost_enrollment_vec_cases) / sizeof(frost_enrollment_vec_cases[0]); c++) { + const struct frost_enrollment_vec_case *v = &frost_enrollment_vec_cases[c]; + secp256k1_pubkey thresh_pk, pubshares[SECP256K1_FROST_MAX_PARTICIPANTS], new_pubshare; + unsigned char hash32[32]; + unsigned char shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + unsigned char all_shares[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + unsigned char received[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + unsigned char sigmas[SECP256K1_FROST_MAX_PARTICIPANTS * 32]; + unsigned char secshare[32]; + unsigned char secrand[32]; + unsigned char ser[33]; + size_t len; + size_t i, j; + + CHECK(secp256k1_ec_pubkey_parse(CTX, &thresh_pk, v->thresh_pk33, 33) == 1); + for (i = 0; i < v->n_ids; i++) { + CHECK(secp256k1_ec_pubkey_parse(CTX, &pubshares[i], v->pubshares33[i], 33) == 1); + } + + /* The parameters hash. */ + CHECK(secp256k1_frost_enrollment_params_hash(CTX, hash32, &thresh_pk, v->ids, v->n_ids, v->new_id, v->n_participants, v->threshold) == 1); + CHECK(secp256k1_memcmp_var(hash32, v->params_hash32, 32) == 0); + + /* Round 1.1 for every helper, from the frozen seeds. */ + for (i = 0; i < v->n_ids; i++) { + memcpy(secrand, v->session_secrand32[i], 32); + CHECK(secp256k1_frost_enrollment_shares_gen(CTX, shares, hash32, secrand, v->secshares32[i], &thresh_pk, v->ids, v->n_ids, v->ids[i], v->new_id, v->n_participants, v->threshold) == 1); + CHECK(secp256k1_memcmp_var(hash32, v->params_hash32, 32) == 0); + CHECK(secp256k1_memcmp_var(shares, v->shares32[i], v->n_ids * 32) == 0); + } + + /* Round 1.2 for every helper. */ + for (j = 0; j < v->n_ids; j++) { + memset(received, 0, v->n_ids * 32); + for (i = 0; i < v->n_ids; i++) { + memcpy(&all_shares[32 * i], &v->shares32[i][32 * j], 32); + if (i != j) { + memcpy(&received[32 * i], v->params_hash32, 32); + } + } + CHECK(secp256k1_frost_enrollment_share_agg(CTX, &sigmas[32 * j], NULL, all_shares, received, &thresh_pk, v->ids, v->n_ids, v->ids[j], v->new_id, v->n_participants, v->threshold) == 1); + } + CHECK(secp256k1_memcmp_var(sigmas, v->sigmas32, v->n_ids * 32) == 0); + + /* The derived public share and round 2. */ + CHECK(secp256k1_frost_enrollment_pubshare_derive(CTX, &new_pubshare, pubshares, v->ids, v->n_ids, v->new_id, v->n_participants, v->threshold) == 1); + len = sizeof(ser); + CHECK(secp256k1_ec_pubkey_serialize(CTX, ser, &len, &new_pubshare, SECP256K1_EC_COMPRESSED) == 1); + CHECK(len == 33); + CHECK(secp256k1_memcmp_var(ser, v->new_pubshare33, 33) == 0); + + CHECK(secp256k1_frost_enrollment_secshare_gen(CTX, secshare, sigmas, &thresh_pk, v->ids, v->n_ids, v->new_id, v->n_participants, v->threshold, v->params_hash32, &new_pubshare) == 1); + CHECK(secp256k1_memcmp_var(secshare, v->new_secshare32, 32) == 0); + } +} + +/* COUNT iterations of the above, so that -i scales the fuzzing the way it does + * for the iceberg module's randomized loops. */ +static void run_frost_enrollment_random_test(void) { + int i; + + for (i = 0; i < COUNT; i++) { + frost_enrollment_random_iteration(); + } +} + static const struct tf_test_entry tests_frost_enrollment[] = { - CASE1(run_frost_enrollment_smoke_test), + CASE1(run_frost_enrollment_vectors_test), + CASE1(run_frost_enrollment_reconstruction_test), + CASE1(run_frost_enrollment_signing_test), + CASE1(run_frost_enrollment_repair_test), + CASE1(run_frost_enrollment_oversized_set_test), + CASE1(run_frost_enrollment_fault_injection_test), + CASE1(run_frost_enrollment_mismatch_test), + CASE1(run_frost_enrollment_own_slot_test), + CASE1(run_frost_enrollment_invalid_params_test), CASE1(run_frost_enrollment_rejects_empty_set_test), + CASE1(run_frost_enrollment_pubshare_derive_test), + CASE1(run_frost_enrollment_random_test), }; #endif /* SECP256K1_MODULE_FROST_ENROLLMENT_TESTS_IMPL_H */ diff --git a/src/modules/frost_enrollment/vectors.h b/src/modules/frost_enrollment/vectors.h new file mode 100644 index 00000000..cd48e450 --- /dev/null +++ b/src/modules/frost_enrollment/vectors.h @@ -0,0 +1,101 @@ +/** + * Automatically generated by tools/test_vectors_frost_enrollment_generate.py. + * + * REGRESSION vectors, not cross-validation vectors. FROST enrollment has no + * BIP and no published test vectors, and the reference proof of concept + * (https://github.com/siv2r/frost-enrollment) draws its randomness from + * secrets.randbits, which is not seedable, so there is nothing to check + * against. The generator re-implements the math independently in Python and + * freezes the result. + * + * What these pin: the two tag strings ("FROST enrollment/params_hash" and + * "FROST enrollment/share_split"), the parameters hash serialization, the + * share-splitting derivation, and the identifier conventions. Changing any of + * them is a vector-breaking change. What they do NOT establish is protocol + * correctness -- the algebraic invariants in tests_impl.h carry that. + * + * Used by the tests in src/modules/frost_enrollment/tests_impl.h. */ + +#ifndef SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H +#define SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H + +#define FROST_ENROLLMENT_VEC_MAX_IDS 4 + +struct frost_enrollment_vec_case { + /* Parameters. */ + size_t n_participants; + uint32_t threshold; + size_t n_ids; + uint32_t new_id; + uint32_t ids[FROST_ENROLLMENT_VEC_MAX_IDS]; + /* Group key material, aligned with ids. */ + unsigned char thresh_pk33[33]; + unsigned char pubshares33[FROST_ENROLLMENT_VEC_MAX_IDS][33]; + unsigned char secshares32[FROST_ENROLLMENT_VEC_MAX_IDS][32]; + /* Round 1.1 inputs and outputs. shares32[i] is helper ids[i]'s output + * buffer, aligned with ids. */ + unsigned char session_secrand32[FROST_ENROLLMENT_VEC_MAX_IDS][32]; + unsigned char params_hash32[32]; + unsigned char shares32[FROST_ENROLLMENT_VEC_MAX_IDS][FROST_ENROLLMENT_VEC_MAX_IDS * 32]; + /* Round 1.2 and round 2 outputs. */ + unsigned char sigmas32[FROST_ENROLLMENT_VEC_MAX_IDS * 32]; + unsigned char new_secshare32[32]; + unsigned char new_pubshare33[33]; +}; + +static const struct frost_enrollment_vec_case frost_enrollment_vec_cases[4] = { + { + 3, 2, 2, 3, + { 0, 1 }, + { 0x02, 0x4D, 0x4B, 0x6C, 0xD1, 0x36, 0x10, 0x32, 0xCA, 0x9B, 0xD2, 0xAE, 0xB9, 0xD9, 0x00, 0xAA, 0x4D, 0x45, 0xD9, 0xEA, 0xD8, 0x0A, 0xC9, 0x42, 0x33, 0x74, 0xC4, 0x51, 0xA7, 0x25, 0x4D, 0x07, 0x66 }, + { { 0x03, 0x98, 0x37, 0x03, 0x75, 0xFC, 0xC9, 0xB6, 0xF1, 0x96, 0x17, 0xA5, 0x12, 0x87, 0x4A, 0xF6, 0x5B, 0xAD, 0x0F, 0xB8, 0x92, 0x60, 0x78, 0xB5, 0xF4, 0x0A, 0x1B, 0xEC, 0xBD, 0xA8, 0xE4, 0x20, 0x73 }, { 0x03, 0x02, 0xC7, 0x4C, 0xBB, 0xF9, 0x6E, 0x2E, 0x82, 0xB3, 0xB0, 0xE8, 0x7E, 0x65, 0x01, 0x16, 0x4B, 0xE9, 0x27, 0xE6, 0xC6, 0xEB, 0x00, 0x92, 0x8B, 0xA6, 0xFD, 0x6D, 0xBC, 0x10, 0x57, 0xB0, 0x42 } }, + { { 0xB3, 0xBD, 0x7A, 0x62, 0x81, 0xD4, 0x63, 0x56, 0x3F, 0x71, 0xBE, 0xDD, 0xE3, 0x75, 0xBA, 0x69, 0xCC, 0x39, 0x85, 0xBD, 0x4E, 0x41, 0x43, 0xAC, 0x4B, 0x96, 0x04, 0xFE, 0x80, 0xAC, 0x1D, 0x23 }, { 0x65, 0x78, 0xF2, 0xC3, 0x01, 0xA6, 0xC4, 0xAA, 0x7C, 0xE1, 0x7B, 0xB9, 0xC4, 0xE9, 0x72, 0xD2, 0xDB, 0xC2, 0x2C, 0x91, 0xEB, 0x37, 0xE5, 0x1A, 0xD5, 0x57, 0xA9, 0x6E, 0x2F, 0x1F, 0xF7, 0x03 } }, + { { 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 }, { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11 } }, + { 0x8C, 0x63, 0xA3, 0xAB, 0xFB, 0xEF, 0x2A, 0x76, 0x10, 0x31, 0x15, 0xB8, 0x53, 0xE6, 0x01, 0xD9, 0x4B, 0xD3, 0xE5, 0x2D, 0x1A, 0xC0, 0x8D, 0xA3, 0x5A, 0xEB, 0x3D, 0xD5, 0x49, 0x30, 0x06, 0x1C }, + { { 0x2D, 0xDA, 0x3A, 0xDD, 0x4B, 0x94, 0xF2, 0x56, 0x83, 0xD8, 0x0A, 0xD6, 0x71, 0xEF, 0xC6, 0xD5, 0xB7, 0xE8, 0x0F, 0x9F, 0x05, 0x86, 0x16, 0x87, 0xB3, 0xD2, 0x54, 0x60, 0x80, 0xFC, 0x67, 0x0E, 0x6A, 0xAA, 0xD0, 0x5D, 0xB0, 0xC2, 0x46, 0xFC, 0xFD, 0x44, 0x77, 0x6D, 0xC7, 0x24, 0xC4, 0x54, 0x25, 0x02, 0x9E, 0xB3, 0xBC, 0x88, 0xA2, 0x97, 0x34, 0xA6, 0x5E, 0xBC, 0x1E, 0x17, 0xE1, 0x2E }, { 0xE4, 0xAC, 0x35, 0x16, 0xEF, 0x35, 0x78, 0x3D, 0x15, 0x2A, 0x33, 0x9A, 0xF5, 0x50, 0x33, 0xED, 0x58, 0x1C, 0xC5, 0xA3, 0x41, 0xD2, 0x5D, 0x70, 0x51, 0x03, 0xD0, 0xE0, 0x6E, 0xF0, 0xBD, 0xE7, 0x4B, 0xBE, 0xA3, 0x32, 0x15, 0xBE, 0xD5, 0xC2, 0x61, 0x7A, 0x3F, 0x92, 0x59, 0x6C, 0x24, 0x8B, 0x3B, 0x29, 0xC0, 0x12, 0x7F, 0xD5, 0x51, 0xE0, 0x2F, 0x03, 0x2B, 0x6A, 0x1E, 0x6F, 0x27, 0x22 } }, + { 0x12, 0x86, 0x6F, 0xF4, 0x3A, 0xCA, 0x6A, 0x93, 0x99, 0x02, 0x3E, 0x71, 0x67, 0x3F, 0xFA, 0xC4, 0x55, 0x55, 0xF8, 0x5B, 0x98, 0x0F, 0xD3, 0xBC, 0x45, 0x03, 0xC6, 0xB4, 0x1F, 0xB6, 0xE3, 0xB4, 0xB6, 0x69, 0x73, 0x8F, 0xC6, 0x81, 0x1C, 0xBF, 0x5E, 0xBE, 0xB7, 0x00, 0x20, 0x90, 0xE8, 0xDF, 0x60, 0x2C, 0x5E, 0xC6, 0x3C, 0x5D, 0xF4, 0x77, 0x63, 0xA9, 0x8A, 0x26, 0x3C, 0x87, 0x08, 0x50 }, + { 0xC8, 0xEF, 0xE3, 0x84, 0x01, 0x4B, 0x87, 0x52, 0xF7, 0xC0, 0xF5, 0x71, 0x87, 0xD0, 0xE3, 0xA3, 0xB5, 0x82, 0x57, 0x21, 0xD4, 0x6D, 0xC8, 0x33, 0xA8, 0xAD, 0x50, 0xDA, 0x5C, 0x3D, 0xEC, 0x04 }, + { 0x03, 0xEC, 0x72, 0xD7, 0x1C, 0xD0, 0x9C, 0x26, 0xB2, 0xAE, 0x92, 0x82, 0x33, 0x86, 0x5E, 0x42, 0xD2, 0x63, 0x1F, 0xFB, 0x09, 0x17, 0xF1, 0x81, 0x67, 0x1D, 0x12, 0x09, 0xB1, 0xE8, 0xD1, 0xDC, 0x8F } + }, + { + 3, 2, 3, 3, + { 0, 1, 2 }, + { 0x02, 0x4D, 0x4B, 0x6C, 0xD1, 0x36, 0x10, 0x32, 0xCA, 0x9B, 0xD2, 0xAE, 0xB9, 0xD9, 0x00, 0xAA, 0x4D, 0x45, 0xD9, 0xEA, 0xD8, 0x0A, 0xC9, 0x42, 0x33, 0x74, 0xC4, 0x51, 0xA7, 0x25, 0x4D, 0x07, 0x66 }, + { { 0x03, 0x98, 0x37, 0x03, 0x75, 0xFC, 0xC9, 0xB6, 0xF1, 0x96, 0x17, 0xA5, 0x12, 0x87, 0x4A, 0xF6, 0x5B, 0xAD, 0x0F, 0xB8, 0x92, 0x60, 0x78, 0xB5, 0xF4, 0x0A, 0x1B, 0xEC, 0xBD, 0xA8, 0xE4, 0x20, 0x73 }, { 0x03, 0x02, 0xC7, 0x4C, 0xBB, 0xF9, 0x6E, 0x2E, 0x82, 0xB3, 0xB0, 0xE8, 0x7E, 0x65, 0x01, 0x16, 0x4B, 0xE9, 0x27, 0xE6, 0xC6, 0xEB, 0x00, 0x92, 0x8B, 0xA6, 0xFD, 0x6D, 0xBC, 0x10, 0x57, 0xB0, 0x42 }, { 0x03, 0x7A, 0x50, 0xEC, 0x9F, 0x4E, 0x35, 0x93, 0xD7, 0xDA, 0x1A, 0x7E, 0x7C, 0x53, 0xC3, 0x8C, 0xE9, 0x9E, 0xBF, 0x83, 0x9C, 0x92, 0xBA, 0x15, 0x8F, 0xFA, 0xAA, 0x26, 0x49, 0x0D, 0xEE, 0x11, 0x56 } }, + { { 0xB3, 0xBD, 0x7A, 0x62, 0x81, 0xD4, 0x63, 0x56, 0x3F, 0x71, 0xBE, 0xDD, 0xE3, 0x75, 0xBA, 0x69, 0xCC, 0x39, 0x85, 0xBD, 0x4E, 0x41, 0x43, 0xAC, 0x4B, 0x96, 0x04, 0xFE, 0x80, 0xAC, 0x1D, 0x23 }, { 0x65, 0x78, 0xF2, 0xC3, 0x01, 0xA6, 0xC4, 0xAA, 0x7C, 0xE1, 0x7B, 0xB9, 0xC4, 0xE9, 0x72, 0xD2, 0xDB, 0xC2, 0x2C, 0x91, 0xEB, 0x37, 0xE5, 0x1A, 0xD5, 0x57, 0xA9, 0x6E, 0x2F, 0x1F, 0xF7, 0x03 }, { 0x17, 0x34, 0x6B, 0x23, 0x81, 0x79, 0x25, 0xFE, 0xBA, 0x51, 0x38, 0x95, 0xA6, 0x5D, 0x2B, 0x3B, 0xEB, 0x4A, 0xD3, 0x66, 0x88, 0x2E, 0x86, 0x89, 0x5F, 0x19, 0x4D, 0xDD, 0xDD, 0x93, 0xD0, 0xE3 } }, + { { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, { 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21, 0x21 }, { 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22 } }, + { 0x5B, 0x9F, 0x16, 0x0D, 0x52, 0x50, 0x7B, 0xA8, 0x74, 0x9A, 0xDE, 0x09, 0x21, 0xCB, 0xB9, 0x08, 0x4F, 0x27, 0xE5, 0x48, 0x56, 0x2F, 0xDC, 0x2B, 0x0E, 0x95, 0xDA, 0x0D, 0x43, 0x4F, 0x44, 0xB0 }, + { { 0x45, 0x84, 0xB1, 0x59, 0xF9, 0x51, 0x6B, 0xA6, 0x92, 0x07, 0xAD, 0x37, 0x67, 0x2C, 0x28, 0xCE, 0x8A, 0x8F, 0x32, 0x5E, 0x2F, 0xE3, 0x23, 0x9E, 0xD7, 0x92, 0x4D, 0x57, 0x41, 0xE3, 0x0C, 0x77, 0x2E, 0x3D, 0x93, 0x85, 0xA9, 0xE7, 0xA1, 0xD0, 0x57, 0xD9, 0x46, 0x26, 0x7D, 0x98, 0xA2, 0x54, 0x37, 0x10, 0xFD, 0x6F, 0xEF, 0x96, 0xD5, 0x9E, 0x1F, 0x9C, 0xA6, 0x44, 0x4E, 0xEF, 0xEF, 0x7E, 0x3F, 0xFB, 0x35, 0x82, 0xDE, 0x9B, 0x55, 0xDF, 0x55, 0x90, 0xCB, 0x7F, 0xFE, 0xB0, 0xEF, 0x47, 0x0A, 0x99, 0x55, 0xEF, 0x2E, 0xC7, 0x4A, 0x6F, 0x54, 0x67, 0x11, 0x62, 0xEF, 0xD9, 0x21, 0x2E }, { 0xD6, 0x9D, 0xAA, 0x79, 0x02, 0xF8, 0xE3, 0xED, 0x34, 0x76, 0xD9, 0x5D, 0xF3, 0xC2, 0x6A, 0xDA, 0xF4, 0x1A, 0xAE, 0x8E, 0x31, 0x4D, 0x4C, 0x48, 0xC7, 0xC4, 0xEC, 0x4C, 0x6A, 0xF6, 0x9A, 0xE7, 0x28, 0xEF, 0x39, 0xB0, 0x95, 0x21, 0xD2, 0x94, 0x76, 0x56, 0x02, 0x76, 0xED, 0x6E, 0xDA, 0xC7, 0xE8, 0x99, 0xEA, 0x1E, 0x70, 0x21, 0xAF, 0xBD, 0x7D, 0x45, 0x78, 0xC2, 0xC0, 0xDB, 0x7E, 0x10, 0xD0, 0x08, 0x43, 0x8D, 0x62, 0xF0, 0xFB, 0x7E, 0xDE, 0x8E, 0xB0, 0xFD, 0xD0, 0x12, 0x61, 0xE0, 0xC0, 0x11, 0x78, 0x51, 0xAA, 0xC3, 0x35, 0x5C, 0x7A, 0x65, 0xBA, 0x4C, 0xB7, 0x70, 0xC5, 0xC3 }, { 0x73, 0x7E, 0x87, 0x14, 0x73, 0x3B, 0x18, 0x64, 0x66, 0x80, 0x48, 0xDA, 0x16, 0xBC, 0x9B, 0x6D, 0x93, 0x29, 0xB1, 0x94, 0xCA, 0x08, 0x45, 0x70, 0x95, 0x6B, 0x33, 0x24, 0x74, 0xF3, 0x29, 0x46, 0x65, 0xFB, 0x89, 0xBD, 0x03, 0xB2, 0x51, 0xC6, 0x53, 0xBF, 0xE7, 0x70, 0x70, 0xD1, 0x33, 0xD8, 0xC2, 0x48, 0x5D, 0x67, 0xB8, 0xB4, 0x9B, 0x15, 0x4D, 0x49, 0x56, 0x27, 0xD5, 0x06, 0x1A, 0xD3, 0x6C, 0x23, 0x30, 0x99, 0x0D, 0x7E, 0x07, 0xD1, 0x74, 0xB3, 0x79, 0x76, 0x6B, 0x89, 0xB2, 0x6C, 0x27, 0x1D, 0x48, 0x1D, 0xC5, 0x17, 0x53, 0x51, 0xFA, 0x69, 0xBE, 0xDA, 0x1E, 0xF8, 0x6F, 0xD1 } }, + { 0x8F, 0xA0, 0xE2, 0xE7, 0x6F, 0x85, 0x67, 0xF8, 0x2C, 0xFE, 0xCF, 0x6F, 0x71, 0xAB, 0x2F, 0x18, 0x57, 0x24, 0xB5, 0x9A, 0x7B, 0xF0, 0x15, 0x1C, 0x74, 0xF0, 0x0E, 0x3B, 0x51, 0x96, 0x8F, 0x63, 0xBD, 0x28, 0x56, 0xF3, 0x42, 0xBB, 0xC6, 0x2B, 0x21, 0xEF, 0x30, 0x0D, 0xDB, 0xD8, 0xB0, 0xF4, 0xE1, 0xF3, 0x44, 0xF6, 0x18, 0x6D, 0x20, 0x70, 0xEA, 0x2B, 0x75, 0x2E, 0xE4, 0xD1, 0x88, 0x61, 0x7C, 0x26, 0xA9, 0xA9, 0x4F, 0x0A, 0x59, 0x2F, 0xA8, 0xD2, 0xF5, 0xF4, 0x3A, 0x4D, 0x03, 0x95, 0x37, 0x19, 0x39, 0x77, 0xEF, 0x59, 0x32, 0xE2, 0x09, 0x64, 0x2B, 0xFC, 0xF6, 0x0C, 0x15, 0x81 }, + { 0xC8, 0xEF, 0xE3, 0x84, 0x01, 0x4B, 0x87, 0x52, 0xF7, 0xC0, 0xF5, 0x71, 0x87, 0xD0, 0xE3, 0xA3, 0xB5, 0x82, 0x57, 0x21, 0xD4, 0x6D, 0xC8, 0x33, 0xA8, 0xAD, 0x50, 0xDA, 0x5C, 0x3D, 0xEC, 0x04 }, + { 0x03, 0xEC, 0x72, 0xD7, 0x1C, 0xD0, 0x9C, 0x26, 0xB2, 0xAE, 0x92, 0x82, 0x33, 0x86, 0x5E, 0x42, 0xD2, 0x63, 0x1F, 0xFB, 0x09, 0x17, 0xF1, 0x81, 0x67, 0x1D, 0x12, 0x09, 0xB1, 0xE8, 0xD1, 0xDC, 0x8F } + }, + { + 5, 3, 3, 2, + { 4, 0, 3 }, + { 0x03, 0x46, 0x0A, 0x7B, 0x96, 0x6E, 0xFF, 0xFB, 0x36, 0x94, 0x6F, 0x6D, 0xC3, 0xC1, 0x7F, 0xF7, 0x3A, 0xD7, 0x89, 0xFC, 0x1C, 0x2D, 0xF4, 0x3C, 0x12, 0x58, 0x03, 0x9C, 0xA2, 0xF9, 0xA1, 0xE3, 0xE6 }, + { { 0x02, 0x54, 0x3E, 0xAB, 0xF1, 0x39, 0x63, 0xF8, 0xE7, 0x71, 0x8B, 0x12, 0xE3, 0x7B, 0x83, 0x5C, 0x39, 0xE1, 0x81, 0xE2, 0x0D, 0xA6, 0x2F, 0xC0, 0xD5, 0x50, 0xF8, 0xF4, 0x19, 0xEE, 0x84, 0xD9, 0xB1 }, { 0x02, 0x13, 0x01, 0x9E, 0xAA, 0x53, 0x03, 0x4E, 0x29, 0x99, 0x8D, 0x82, 0x69, 0x3A, 0x0D, 0x11, 0x3E, 0xA4, 0x3F, 0xF4, 0xFF, 0x57, 0x5C, 0x13, 0x6C, 0x26, 0x19, 0x27, 0x89, 0xBD, 0xBA, 0xDE, 0x41 }, { 0x02, 0xA2, 0x22, 0xEB, 0x56, 0xCC, 0x64, 0xD2, 0x3D, 0x40, 0x14, 0x63, 0xDE, 0x34, 0x32, 0xE3, 0xFE, 0xF8, 0x30, 0x44, 0x18, 0xFE, 0x54, 0xF3, 0x6C, 0xF5, 0xC1, 0x21, 0x35, 0x7E, 0x4A, 0x4F, 0xAE } }, + { { 0xE9, 0x8D, 0xCB, 0xFA, 0xA9, 0x0D, 0xC4, 0xF5, 0x05, 0xB9, 0x07, 0xEF, 0x9A, 0xC6, 0xAB, 0x54, 0x9B, 0x78, 0xB0, 0xC7, 0xE6, 0x9B, 0x9B, 0xC2, 0x14, 0xF6, 0x01, 0x1F, 0xCD, 0xE6, 0x5A, 0xB5 }, { 0xAB, 0x57, 0x1E, 0x48, 0xD5, 0x38, 0x7E, 0x88, 0x19, 0x79, 0x39, 0xBB, 0x68, 0x8F, 0x81, 0x47, 0x20, 0xD2, 0x03, 0xCD, 0x47, 0x28, 0xEF, 0xD8, 0x5B, 0x27, 0xB7, 0x4F, 0xBF, 0xD5, 0xEB, 0x5C }, { 0x69, 0x77, 0x2F, 0xDC, 0x37, 0x09, 0x56, 0x48, 0xE3, 0xAE, 0x6A, 0x76, 0x89, 0xCA, 0x78, 0x0A, 0x44, 0x4F, 0xE4, 0x4C, 0xD1, 0xA4, 0xBE, 0xA7, 0x8B, 0x69, 0x28, 0x47, 0x1C, 0x55, 0x40, 0xB4 } }, + { { 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 }, { 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31 }, { 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32, 0x32 } }, + { 0x77, 0x95, 0x32, 0x9D, 0xC5, 0xF2, 0x78, 0x19, 0x9F, 0xD8, 0x64, 0xBB, 0xB4, 0x67, 0x72, 0x15, 0xC0, 0xB8, 0x29, 0xC7, 0x7D, 0x6C, 0x06, 0x56, 0x61, 0xCC, 0x30, 0xBD, 0xE3, 0x96, 0x91, 0x0B }, + { { 0xE6, 0x01, 0xBE, 0xBE, 0xDB, 0x3C, 0xEC, 0x62, 0x8C, 0x1F, 0x1E, 0xAB, 0x90, 0x05, 0x90, 0x2E, 0xE9, 0xC7, 0xC1, 0x28, 0x91, 0x95, 0x70, 0xBD, 0x0C, 0x9D, 0x2F, 0xBF, 0x40, 0xF1, 0x61, 0x2A, 0x9E, 0x57, 0xCE, 0xCA, 0x68, 0x2C, 0xD4, 0xC7, 0x06, 0xAE, 0x89, 0x9E, 0x67, 0xA1, 0x53, 0xB9, 0x88, 0xE3, 0xAE, 0x56, 0x1C, 0x95, 0xC3, 0xDE, 0x6F, 0x10, 0xEF, 0x3D, 0x9C, 0x7B, 0x66, 0x3A, 0x86, 0xDF, 0x8C, 0x79, 0x68, 0x0F, 0x5C, 0x5B, 0xEA, 0x55, 0xD3, 0xBE, 0x3A, 0xF5, 0xC6, 0x6A, 0x12, 0x4D, 0x60, 0x5E, 0x14, 0xBC, 0x8E, 0x18, 0xD9, 0x64, 0xCC, 0xD3, 0x44, 0x27, 0xAE, 0x64 }, { 0xF6, 0x03, 0x83, 0x3A, 0xBB, 0x2F, 0xA4, 0xE6, 0x7D, 0x0E, 0xEC, 0x30, 0xA7, 0x06, 0xA5, 0x4C, 0x36, 0xEB, 0x3D, 0x45, 0x4A, 0x6F, 0x2D, 0xF2, 0xF9, 0x55, 0x36, 0xDA, 0x38, 0xF2, 0xD5, 0xBA, 0x86, 0x82, 0x58, 0xBE, 0xD2, 0x6F, 0x58, 0xB9, 0xB0, 0x23, 0x76, 0x2F, 0xEA, 0x81, 0xD2, 0x1E, 0x67, 0x66, 0xB2, 0x49, 0xAA, 0xE7, 0xF2, 0x55, 0xC8, 0x60, 0x7F, 0x3D, 0x1F, 0x29, 0x87, 0xBC, 0xA0, 0x08, 0xA9, 0x12, 0x95, 0xEA, 0x6C, 0xCB, 0xD7, 0x0C, 0x7C, 0x93, 0xFF, 0xE4, 0xC8, 0xC9, 0x5C, 0x84, 0x20, 0x35, 0xF5, 0x16, 0x48, 0x28, 0x22, 0x75, 0xA5, 0x8F, 0x92, 0xF3, 0xCC, 0x46 }, { 0xBD, 0x14, 0x1C, 0x4E, 0xAB, 0x0A, 0x58, 0xD9, 0x6E, 0xB7, 0xDC, 0x5A, 0x77, 0x7B, 0x1D, 0x7B, 0xCB, 0x50, 0x04, 0x8F, 0x60, 0x4F, 0x06, 0xFD, 0xAE, 0xD6, 0xC7, 0x94, 0xFF, 0xCF, 0x6E, 0xF7, 0x57, 0xE3, 0xD4, 0x8B, 0xB0, 0xEA, 0x24, 0x19, 0xBE, 0x11, 0xBC, 0xB9, 0x3C, 0x0F, 0x03, 0xCA, 0x2E, 0x23, 0x77, 0x84, 0x61, 0x31, 0x32, 0xA3, 0xF0, 0xFF, 0x94, 0xC4, 0xD7, 0xEF, 0x0A, 0x5A, 0x22, 0x51, 0xA4, 0x4B, 0x42, 0xC2, 0xA0, 0xC3, 0x58, 0x1E, 0xF4, 0xDF, 0xAE, 0xD9, 0x29, 0x71, 0x88, 0xC0, 0xF2, 0x41, 0xCA, 0xE1, 0x2F, 0x65, 0x99, 0xEC, 0xC3, 0x0D, 0x83, 0x82, 0x08, 0x75 } }, + { 0x99, 0x19, 0x5E, 0x48, 0x41, 0x76, 0xEA, 0x22, 0x77, 0xE5, 0xE7, 0x36, 0xAE, 0x87, 0x52, 0xF9, 0x76, 0xA5, 0x49, 0x2F, 0xDD, 0xC2, 0x65, 0x36, 0x35, 0x24, 0x71, 0x14, 0xD9, 0x47, 0x23, 0x59, 0x7C, 0xBD, 0xFC, 0x14, 0xEB, 0x86, 0x51, 0x9A, 0x74, 0xE3, 0xBC, 0x87, 0x8E, 0x32, 0x29, 0xA3, 0x63, 0xBE, 0xFB, 0x3D, 0x79, 0x66, 0x48, 0x9C, 0x68, 0x9E, 0xA4, 0xB2, 0xC3, 0x5D, 0xB7, 0x0F, 0x49, 0x39, 0xD9, 0xD7, 0x40, 0xBC, 0x69, 0xEB, 0x19, 0x81, 0x45, 0x31, 0xE9, 0xB3, 0xB8, 0xA6, 0x3C, 0xE3, 0x95, 0xEF, 0x25, 0x6B, 0x65, 0x6A, 0xD5, 0xF4, 0xD6, 0xE3, 0x8A, 0x67, 0x41, 0xDE }, + { 0x5F, 0x11, 0x34, 0x34, 0x6D, 0xB9, 0xA5, 0xA8, 0x06, 0x4A, 0xE8, 0xF0, 0x26, 0x6D, 0x35, 0x44, 0x5C, 0x98, 0xFD, 0x75, 0xCD, 0x4B, 0x73, 0x01, 0xB3, 0xE5, 0x8E, 0x1E, 0x56, 0xD5, 0xDB, 0x05 }, + { 0x02, 0x47, 0xE5, 0x9F, 0xCF, 0x15, 0x5E, 0xCB, 0x62, 0x47, 0xDE, 0x52, 0xB7, 0x57, 0x59, 0x1A, 0x3E, 0x3B, 0xF8, 0xEB, 0x2B, 0x57, 0x24, 0x4B, 0xF5, 0x84, 0xF9, 0x1B, 0x52, 0x15, 0xCE, 0x7C, 0x8C } + }, + { + 6, 4, 4, 6, + { 0, 2, 3, 5 }, + { 0x03, 0x80, 0x03, 0x89, 0x51, 0xDF, 0x18, 0x6D, 0x2F, 0x43, 0x9A, 0xA6, 0x33, 0xC3, 0x32, 0xF3, 0x0F, 0x1D, 0x78, 0xDE, 0x48, 0xE0, 0x9D, 0xDD, 0xFB, 0xB4, 0x2F, 0x61, 0x18, 0xBC, 0xC9, 0x11, 0x70 }, + { { 0x02, 0x97, 0xF2, 0xDD, 0x64, 0x80, 0xC8, 0x04, 0x44, 0x5E, 0x26, 0x60, 0x56, 0x90, 0x77, 0xA5, 0xDD, 0x3C, 0xBF, 0x77, 0xBB, 0x10, 0x0F, 0x3A, 0xBA, 0x93, 0xC0, 0xF1, 0xE3, 0x02, 0xF9, 0xFA, 0xCD }, { 0x02, 0xFD, 0x49, 0xCD, 0x57, 0xE2, 0xB0, 0x7B, 0x1E, 0x6A, 0xEE, 0x83, 0x37, 0x90, 0xDF, 0x69, 0x94, 0x53, 0x94, 0x7C, 0xF4, 0xE3, 0xBE, 0x64, 0x7D, 0x5B, 0x9C, 0x83, 0x7D, 0x0D, 0xEA, 0x18, 0x10 }, { 0x02, 0xCC, 0xE4, 0xE5, 0xDD, 0x62, 0x4E, 0x42, 0x08, 0x38, 0xC0, 0x92, 0xF1, 0x93, 0xEC, 0x47, 0x42, 0xEA, 0xA4, 0xAE, 0xA5, 0xDF, 0x89, 0x14, 0x11, 0x9F, 0xCB, 0x53, 0xED, 0x9A, 0x0B, 0xA1, 0xD9 }, { 0x02, 0xBF, 0x4C, 0xEF, 0xF9, 0x81, 0x62, 0x6E, 0x40, 0xAD, 0x77, 0x41, 0xFE, 0x22, 0xC2, 0x97, 0xA3, 0xDB, 0x90, 0x71, 0xCD, 0x7D, 0x94, 0x1D, 0x74, 0xE8, 0xB3, 0x86, 0xA5, 0x4F, 0xC5, 0x55, 0x2F } }, + { { 0x82, 0x1E, 0x21, 0x58, 0xE3, 0xA4, 0x8C, 0x53, 0x1B, 0xF7, 0xCF, 0x24, 0xC1, 0x8C, 0x44, 0xA0, 0x7F, 0x37, 0x12, 0xB0, 0x90, 0xF7, 0xAB, 0xC8, 0xF5, 0xC8, 0xDA, 0xFB, 0x83, 0xAB, 0x56, 0xCF }, { 0x5B, 0x8E, 0xEC, 0x15, 0xEA, 0x88, 0x54, 0xD9, 0x75, 0x0B, 0x4F, 0x92, 0xD2, 0xE6, 0x7D, 0x79, 0x1E, 0x5B, 0xE6, 0x4B, 0x67, 0xAC, 0x7C, 0xAB, 0x73, 0xBD, 0xAA, 0x0F, 0x43, 0x3D, 0x62, 0x97 }, { 0xFB, 0x5A, 0x8F, 0x7F, 0xC6, 0x2C, 0x78, 0xAD, 0x59, 0x9C, 0x5C, 0x4C, 0x88, 0xD8, 0xF4, 0xD1, 0x2E, 0x75, 0xA1, 0x78, 0x6A, 0xA3, 0xAF, 0xA5, 0x18, 0x84, 0xC7, 0xFA, 0x1E, 0x98, 0x44, 0xCC }, { 0xAB, 0x71, 0x43, 0x4D, 0xA5, 0x55, 0x57, 0xB2, 0xB6, 0xF2, 0x48, 0x88, 0xCE, 0x79, 0x0B, 0xB0, 0x1D, 0x37, 0x00, 0x50, 0x32, 0x53, 0x8B, 0xC8, 0x20, 0xC1, 0xF6, 0xFD, 0xFB, 0x4E, 0x80, 0x93 } }, + { { 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40 }, { 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41 }, { 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42 }, { 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43, 0x43 } }, + { 0x38, 0xD6, 0x35, 0x27, 0x21, 0x01, 0x29, 0x3F, 0xB4, 0xC8, 0x11, 0x7B, 0x06, 0xDB, 0x92, 0xD3, 0x49, 0x42, 0xB6, 0x4E, 0x6B, 0x8C, 0x07, 0x39, 0xF3, 0xB7, 0x5B, 0xA9, 0x02, 0x22, 0xF2, 0x73 }, + { { 0x6B, 0xE2, 0x93, 0xC2, 0xD3, 0x28, 0x76, 0x27, 0xED, 0x0C, 0xF2, 0x23, 0x1F, 0xC4, 0x59, 0x0F, 0xA0, 0x80, 0xC2, 0xF6, 0x81, 0xAE, 0x96, 0x22, 0x3C, 0x2C, 0x00, 0x86, 0x07, 0xF1, 0x1B, 0x61, 0x6F, 0xD1, 0x3D, 0xC9, 0x55, 0xED, 0xAB, 0x3D, 0x71, 0x17, 0x03, 0x44, 0x05, 0x75, 0x87, 0x3A, 0x86, 0x15, 0xA5, 0xDD, 0xEF, 0xB6, 0x31, 0xF2, 0x38, 0x15, 0xAA, 0xE7, 0x87, 0x3D, 0xF8, 0x1B, 0x42, 0x7D, 0x4D, 0xB1, 0xBC, 0x4E, 0x96, 0x97, 0x23, 0xAE, 0xDE, 0xB8, 0xEF, 0x22, 0x52, 0x46, 0x55, 0x9C, 0x89, 0xD3, 0xD0, 0xEA, 0x38, 0x4C, 0x50, 0x9A, 0x43, 0x41, 0x37, 0x41, 0x99, 0x23, 0xAD, 0xC2, 0xD3, 0x6B, 0x59, 0x26, 0x43, 0x15, 0x72, 0xFD, 0x3F, 0x6A, 0xD1, 0x6B, 0xB1, 0xF9, 0x93, 0x14, 0xBF, 0xAB, 0x48, 0xAB, 0xFB, 0x5F, 0xF2, 0x12, 0x10, 0x6C, 0xA5, 0x50, 0xE6, 0x5D }, { 0x4F, 0x76, 0x7A, 0x5A, 0xD9, 0xE5, 0xA3, 0xA4, 0xBA, 0x36, 0xAF, 0x19, 0xB9, 0xDB, 0xB0, 0xF2, 0xFA, 0x8E, 0xB1, 0x13, 0xE2, 0x38, 0x56, 0x16, 0xF5, 0xCD, 0xE6, 0xB3, 0x53, 0xEB, 0x96, 0xE0, 0x74, 0xAE, 0xAD, 0x17, 0xB8, 0xD3, 0x33, 0xCD, 0xF7, 0xA8, 0xDB, 0xA7, 0x21, 0xDE, 0x62, 0x09, 0xA4, 0x23, 0xF9, 0xA6, 0xC4, 0x53, 0xEE, 0x46, 0x6B, 0xC3, 0x6B, 0x15, 0xAC, 0x62, 0x59, 0xCB, 0x9D, 0x56, 0xC9, 0x53, 0x6A, 0x4D, 0xBE, 0xF2, 0x40, 0x98, 0x8D, 0x46, 0xC7, 0xD4, 0x2E, 0x90, 0x22, 0x88, 0x82, 0x0F, 0x1D, 0x94, 0xC4, 0xA5, 0xD8, 0xE3, 0x0D, 0xE0, 0x58, 0x76, 0x92, 0xE1, 0xB1, 0x30, 0xD3, 0x7B, 0xC2, 0x92, 0x68, 0x27, 0x6C, 0xA9, 0xD6, 0xB0, 0xD5, 0x25, 0x36, 0xDD, 0x54, 0x87, 0x62, 0xFF, 0x22, 0x2D, 0x0D, 0x3A, 0xE0, 0x96, 0xFD, 0x11, 0x41, 0x29, 0xE5, 0x7A }, { 0x67, 0x9E, 0xAD, 0xD5, 0x31, 0x32, 0xA6, 0x30, 0xBA, 0x82, 0xAD, 0xFC, 0x2F, 0x9C, 0xDE, 0x31, 0x39, 0xED, 0xD0, 0x92, 0x78, 0x9D, 0x2C, 0xE0, 0x47, 0xEE, 0xEF, 0x91, 0x75, 0x8C, 0x95, 0x58, 0x27, 0xEF, 0x16, 0xB7, 0x70, 0x5A, 0x78, 0xB3, 0xD7, 0x82, 0xD4, 0xF0, 0x7B, 0x79, 0x9D, 0x0B, 0x7A, 0x2B, 0x3F, 0xE3, 0x09, 0xED, 0x0F, 0x1F, 0x02, 0xE7, 0x81, 0x76, 0x18, 0x8F, 0xC1, 0x8D, 0xA9, 0xC5, 0x3B, 0x51, 0x44, 0x2B, 0x63, 0x1D, 0x74, 0x31, 0x29, 0x8A, 0xD4, 0x1A, 0x30, 0xCC, 0x3B, 0xC3, 0x7B, 0x2B, 0xD0, 0x43, 0xEE, 0xFD, 0x29, 0xD9, 0x22, 0x82, 0x9F, 0x7E, 0x5C, 0xBE, 0xD9, 0x42, 0xC2, 0x23, 0x01, 0x95, 0x9B, 0x48, 0x93, 0x57, 0xE2, 0x56, 0x5D, 0x6B, 0x80, 0xAA, 0xB6, 0x66, 0x1B, 0xE5, 0x1E, 0x56, 0xD7, 0xD5, 0xA8, 0x2B, 0x83, 0xDA, 0x39, 0x49, 0xC0, 0xB3 }, { 0xA7, 0xE3, 0x05, 0xC0, 0x28, 0x61, 0xA2, 0xA6, 0xAB, 0x80, 0xEC, 0x58, 0x54, 0x5E, 0xDF, 0xDC, 0x3A, 0xB6, 0xA0, 0x81, 0xCA, 0xDF, 0x0A, 0xC2, 0xEE, 0x3E, 0xF0, 0x92, 0xD8, 0x6D, 0x2B, 0x04, 0x87, 0xEE, 0xF7, 0xD7, 0x98, 0xD3, 0xEC, 0xEE, 0xDA, 0x68, 0x89, 0x44, 0x56, 0x45, 0x03, 0xFE, 0xCD, 0xAD, 0x08, 0xFD, 0xEC, 0x7C, 0x43, 0xF2, 0xBF, 0xCF, 0x41, 0x57, 0xDA, 0x2F, 0x37, 0x58, 0xE3, 0x89, 0xA3, 0x5E, 0x82, 0x07, 0x41, 0xB3, 0xAC, 0x9E, 0x60, 0xE7, 0x84, 0x15, 0x6F, 0x14, 0x9F, 0x4E, 0x50, 0xC9, 0x6F, 0x44, 0xE8, 0x95, 0x0E, 0xCA, 0x4F, 0x59, 0xAF, 0x8B, 0xEE, 0x42, 0xEE, 0x81, 0x00, 0x90, 0xE3, 0x29, 0x9A, 0xCA, 0x1E, 0x24, 0x0A, 0xC4, 0x27, 0x35, 0xFC, 0x4E, 0xD6, 0xFA, 0x08, 0x87, 0x14, 0x5A, 0xC5, 0x4F, 0x9E, 0x52, 0xBA, 0x48, 0xB6, 0x13, 0x3F, 0x51 } }, + { 0xCA, 0xDA, 0xC1, 0xB3, 0x06, 0xA2, 0x62, 0xA4, 0x0D, 0x47, 0x3B, 0x91, 0x5D, 0x9B, 0xC8, 0x11, 0x55, 0x05, 0x08, 0x37, 0xF8, 0x1A, 0x83, 0xA0, 0xA8, 0x55, 0x68, 0xD0, 0xD9, 0xA0, 0x31, 0x5C, 0x94, 0x5D, 0xF9, 0x70, 0x17, 0xEF, 0x44, 0xAE, 0x1A, 0xAB, 0x3D, 0x1F, 0xF9, 0x12, 0x8A, 0x4F, 0xB7, 0x63, 0x0B, 0x7E, 0xFB, 0x2A, 0xD3, 0x0E, 0xA6, 0xBD, 0x7A, 0x3E, 0x56, 0x29, 0x09, 0x8A, 0x6D, 0x22, 0xF5, 0xB4, 0xEC, 0xCE, 0xFA, 0x5A, 0x85, 0x16, 0xF6, 0x72, 0x0F, 0x26, 0x20, 0xB9, 0xDD, 0xD9, 0x1E, 0x0A, 0xCF, 0x76, 0x94, 0x0C, 0xE2, 0x7C, 0x05, 0xE4, 0x3E, 0x55, 0xF4, 0x82, 0x26, 0xB7, 0x69, 0x9B, 0x00, 0x77, 0xE1, 0x4F, 0x91, 0x23, 0x03, 0x36, 0x2B, 0x32, 0x65, 0xD4, 0x44, 0xEF, 0xB0, 0x62, 0x8F, 0xB0, 0xC5, 0x0C, 0xD9, 0xB0, 0x2F, 0xFA, 0x65, 0x35, 0x08, 0x18 }, + { 0xF3, 0x13, 0x1A, 0x73, 0x0B, 0xD8, 0x82, 0xFC, 0x3E, 0x2C, 0x72, 0x59, 0x91, 0x06, 0xD8, 0xF0, 0x74, 0x82, 0x05, 0x3D, 0xA3, 0x24, 0x0F, 0x8D, 0x4B, 0x6C, 0xBA, 0x61, 0x03, 0x1D, 0xF6, 0x3F }, + { 0x03, 0xE1, 0xBF, 0xC6, 0x86, 0xB3, 0x60, 0x2C, 0xF7, 0xD2, 0x8A, 0x47, 0x77, 0x4A, 0x9E, 0x54, 0x67, 0xD5, 0xC7, 0x82, 0x71, 0x90, 0xAA, 0xA5, 0x4A, 0xB1, 0xB2, 0xB4, 0x8A, 0xF2, 0x00, 0x8B, 0x4E } + }, +}; + +#endif /* SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H */ diff --git a/tools/test_vectors_frost_enrollment_generate.py b/tools/test_vectors_frost_enrollment_generate.py new file mode 100755 index 00000000..bf8adbe0 --- /dev/null +++ b/tools/test_vectors_frost_enrollment_generate.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 + +"""Generates src/modules/frost_enrollment/vectors.h. + +These are REGRESSION vectors, not cross-validation vectors. FROST enrollment +has no BIP and therefore no published test vectors, and the reference proof of +concept (https://github.com/siv2r/frost-enrollment) draws its randomness from +secrets.randbits, which is not seedable -- so there is nothing to check the C +implementation against. This script therefore re-implements the same math +independently, in plain Python, and freezes the result. + +What that buys is real but bounded: it pins the two tag strings, the exact +parameters hash serialization, the share-splitting derivation and the +identifier conventions, so that any change to them is a loud, deliberate, +vector-breaking change rather than a silent one. It is NOT evidence that the +protocol is implemented correctly -- the algebraic invariants in +src/modules/frost_enrollment/tests_impl.h are what carry that. + +The one thing this file does establish independently is the group arithmetic: +the elliptic curve operations below are written from the secp256k1 parameters +rather than borrowed from the library, so a vector mismatch in the derived +public share or the threshold key really is a disagreement between two +implementations. + +Usage: %s > src/modules/frost_enrollment/vectors.h +""" + +import hashlib +import sys +import textwrap + +# secp256k1 domain parameters. +P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798 +GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8 +G = (GX, GY) + +MAX_PARTICIPANTS = 128 + + +# --- group arithmetic (points are (x, y) or None for infinity) --- + + +def point_add(a, b): + if a is None: + return b + if b is None: + return a + if a[0] == b[0] and (a[1] + b[1]) % P == 0: + return None + if a == b: + lam = 3 * a[0] * a[0] * pow(2 * a[1], P - 2, P) % P + else: + lam = (b[1] - a[1]) * pow(b[0] - a[0], P - 2, P) % P + x = (lam * lam - a[0] - b[0]) % P + return (x, (lam * (a[0] - x) - a[1]) % P) + + +def point_mul(point, scalar): + result = None + scalar %= ORDER + while scalar: + if scalar & 1: + result = point_add(result, point) + point = point_add(point, point) + scalar >>= 1 + return result + + +def cbytes(point): + """33-byte compressed serialization.""" + assert point is not None + return bytes([2 + (point[1] & 1)]) + point[0].to_bytes(32, "big") + + +# --- hashing --- + + +def tagged_hash(tag, msg): + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + + +def ser32(x): + return x.to_bytes(4, "big") + + +# --- the protocol, mirroring src/modules/frost_enrollment/enrollment_impl.h --- + + +def params_hash(thresh_pk, ids, new_id, n_participants, threshold): + msg = cbytes(thresh_pk) + msg += ser32(n_participants) + ser32(threshold) + ser32(new_id) + msg += ser32(len(ids)) + for i in sorted(ids): + msg += ser32(i) + return tagged_hash("FROST enrollment/params_hash", msg) + + +def lagrange_at(ids, my_id, new_id): + """The Lagrange basis polynomial of my_id over ids, at the target. + + Identifier space: the x-coordinate of identifier id is id + 1, so an + x-coordinate difference is an identifier difference and the +1 cancels.""" + num, deno = 1, 1 + for other in ids: + if other == my_id: + continue + num = num * (new_id - other) % ORDER + deno = deno * (my_id - other) % ORDER + return num * pow(deno, ORDER - 2, ORDER) % ORDER + + +def derive_mask(rand32, ph32, my_id, recipient_id): + msg = rand32 + ph32 + ser32(my_id) + ser32(recipient_id) + # from_bytes_wrapping: reduce mod the group order rather than reject. + return int.from_bytes(tagged_hash("FROST enrollment/share_split", msg), "big") % ORDER + + +def shares_gen(secshare, thresh_pk, ids, my_id, new_id, n_participants, threshold, secrand32): + ph32 = params_hash(thresh_pk, ids, new_id, n_participants, threshold) + v = lagrange_at(ids, my_id, new_id) * secshare % ORDER + rand32 = bytes( + a ^ b + for a, b in zip( + tagged_hash("FROST enrollment/share_split", secrand32), + secshare.to_bytes(32, "big"), + ) + ) + out = [0] * len(ids) + my_pos = ids.index(my_id) + for j, recipient in enumerate(ids): + if j == my_pos: + continue + out[j] = derive_mask(rand32, ph32, my_id, recipient) + v = (v - out[j]) % ORDER + out[my_pos] = v + return out, ph32 + + +def trusted_dealer_keygen(thresh_sk, n_participants, threshold): + """The frost module's trusted dealer (src/modules/frost/keygen_impl.h).""" + coeffs = [] + for i in range(1, threshold): + h = tagged_hash("BIP0445/trusted/keygen", thresh_sk.to_bytes(32, "big") + ser32(i)) + c = int.from_bytes(h, "big") + assert 0 < c < ORDER + coeffs.append(c) + secshares = [] + for i in range(n_participants): + x = i + 1 + share = 0 + for c in coeffs: + share = (share * x + c) % ORDER + share = (share * x + thresh_sk) % ORDER + assert share != 0 + secshares.append(share) + return secshares, point_mul(G, thresh_sk), [point_mul(G, s) for s in secshares] + + +def run_case(thresh_sk, n_participants, threshold, ids, new_id, seeds): + secshares, thresh_pk, pubshares = trusted_dealer_keygen(thresh_sk, n_participants, threshold) + ph32 = params_hash(thresh_pk, ids, new_id, n_participants, threshold) + + shares = [] + for k, my_id in enumerate(ids): + out, ph = shares_gen( + secshares[my_id], thresh_pk, ids, my_id, new_id, n_participants, threshold, seeds[k] + ) + assert ph == ph32 + shares.append(out) + + # Round 1.2: helper j sums entry j of every helper's output. + sigmas = [sum(shares[i][j] for i in range(len(ids))) % ORDER for j in range(len(ids))] + + # Round 2, and the independent check that the result really is f(x_new). + new_secshare = sum(sigmas) % ORDER + expected = sum(lagrange_at(ids, i, new_id) * secshares[i] for i in ids) % ORDER + assert new_secshare == expected + new_pubshare = None + for i in ids: + new_pubshare = point_add(new_pubshare, point_mul(pubshares[i], lagrange_at(ids, i, new_id))) + assert new_pubshare == point_mul(G, new_secshare) + + return { + "n_participants": n_participants, + "threshold": threshold, + "ids": ids, + "new_id": new_id, + "thresh_pk": thresh_pk, + "pubshares": [pubshares[i] for i in ids], + "secshares": [secshares[i] for i in ids], + "seeds": seeds, + "params_hash": ph32, + "shares": shares, + "sigmas": sigmas, + "new_secshare": new_secshare, + "new_pubshare": new_pubshare, + } + + +# --- C emission --- + + +def byte_array(b): + return "{ %s }" % ", ".join("0x%02X" % x for x in b) + + +def scalar_array(x): + return byte_array(x.to_bytes(32, "big")) + + +def indent(s, level=1): + return textwrap.indent(s, 4 * level * " ") + + +def emit_case(c): + n_ids = len(c["ids"]) + lines = [] + lines.append("%d, %d, %d, %d," % (c["n_participants"], c["threshold"], n_ids, c["new_id"])) + lines.append("{ %s }," % ", ".join(str(i) for i in c["ids"])) + lines.append("%s," % byte_array(cbytes(c["thresh_pk"]))) + lines.append("{ %s }," % ", ".join(byte_array(cbytes(p)) for p in c["pubshares"])) + lines.append("{ %s }," % ", ".join(scalar_array(s) for s in c["secshares"])) + lines.append("{ %s }," % ", ".join(byte_array(s) for s in c["seeds"])) + lines.append("%s," % byte_array(c["params_hash"])) + lines.append( + "{ %s }," + % ", ".join( + "{ %s }" % ", ".join("0x%02X" % b for s in row for b in s.to_bytes(32, "big")) + for row in c["shares"] + ) + ) + lines.append( + "{ %s }," + % ", ".join("0x%02X" % b for s in c["sigmas"] for b in s.to_bytes(32, "big")) + ) + lines.append("%s," % scalar_array(c["new_secshare"])) + lines.append("%s" % byte_array(cbytes(c["new_pubshare"]))) + return "{\n" + indent("\n".join(lines)) + "\n}," + + +# Fixed inputs. Nothing here is random at run time: the whole point is that +# regenerating this file without an intentional change reproduces it byte for +# byte. +CASES = [ + # A 2-of-3 group enrolling a fourth participant with the minimum helper + # set. The base case, and the one the module documentation walks through. + # This key has EVEN Y; the three below have odd Y. Nothing in enrollment + # depends on the parity of the threshold key -- unlike frost signing, it + # never takes an x-only view of it -- so this is coverage rather than a + # distinction the code makes. + dict( + thresh_sk=0x0202020202020202020202020202020202020202020202020202020202020202, + n_participants=3, + threshold=2, + ids=[0, 1], + new_id=3, + seeds=[bytes([0x10 + i] * 32) for i in range(2)], + ), + # The same group with an oversized helper set: u = 3 > t = 2. The resulting + # share must be the one the u = 2 case produces, which the C test checks + # separately; here it is simply frozen. + dict( + thresh_sk=0x0202020202020202020202020202020202020202020202020202020202020202, + n_participants=3, + threshold=2, + ids=[0, 1, 2], + new_id=3, + seeds=[bytes([0x20 + i] * 32) for i in range(3)], + ), + # Repair: a 3-of-5 group reproducing participant 2's lost share. The helper + # set is deliberately unsorted, to pin that the parameters hash + # canonicalizes identifiers while the share buffers follow the caller's + # order. + dict( + thresh_sk=0x02030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2021, + n_participants=5, + threshold=3, + ids=[4, 0, 3], + new_id=2, + seeds=[bytes([0x30 + i] * 32) for i in range(3)], + ), + # A larger enrollment, 4-of-6 to 4-of-7, and the only case whose DERIVED + # public share has odd Y. + dict( + thresh_sk=0x1122334455667788990011223344556677889900112233445566778899001122, + n_participants=6, + threshold=4, + ids=[0, 2, 3, 5], + new_id=6, + seeds=[bytes([0x40 + i] * 32) for i in range(4)], + ), +] + + +def main(): + cases = [run_case(**c) for c in CASES] + max_ids = max(len(c["ids"]) for c in cases) + + out = """/** + * Automatically generated by tools/test_vectors_frost_enrollment_generate.py. + * + * REGRESSION vectors, not cross-validation vectors. FROST enrollment has no + * BIP and no published test vectors, and the reference proof of concept + * (https://github.com/siv2r/frost-enrollment) draws its randomness from + * secrets.randbits, which is not seedable, so there is nothing to check + * against. The generator re-implements the math independently in Python and + * freezes the result. + * + * What these pin: the two tag strings ("FROST enrollment/params_hash" and + * "FROST enrollment/share_split"), the parameters hash serialization, the + * share-splitting derivation, and the identifier conventions. Changing any of + * them is a vector-breaking change. What they do NOT establish is protocol + * correctness -- the algebraic invariants in tests_impl.h carry that. + * + * Used by the tests in src/modules/frost_enrollment/tests_impl.h. */ + +#ifndef SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H +#define SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H + +#define FROST_ENROLLMENT_VEC_MAX_IDS %d + +struct frost_enrollment_vec_case { + /* Parameters. */ + size_t n_participants; + uint32_t threshold; + size_t n_ids; + uint32_t new_id; + uint32_t ids[FROST_ENROLLMENT_VEC_MAX_IDS]; + /* Group key material, aligned with ids. */ + unsigned char thresh_pk33[33]; + unsigned char pubshares33[FROST_ENROLLMENT_VEC_MAX_IDS][33]; + unsigned char secshares32[FROST_ENROLLMENT_VEC_MAX_IDS][32]; + /* Round 1.1 inputs and outputs. shares32[i] is helper ids[i]'s output + * buffer, aligned with ids. */ + unsigned char session_secrand32[FROST_ENROLLMENT_VEC_MAX_IDS][32]; + unsigned char params_hash32[32]; + unsigned char shares32[FROST_ENROLLMENT_VEC_MAX_IDS][FROST_ENROLLMENT_VEC_MAX_IDS * 32]; + /* Round 1.2 and round 2 outputs. */ + unsigned char sigmas32[FROST_ENROLLMENT_VEC_MAX_IDS * 32]; + unsigned char new_secshare32[32]; + unsigned char new_pubshare33[33]; +}; + +static const struct frost_enrollment_vec_case frost_enrollment_vec_cases[%d] = { +""" % ( + max_ids, + len(cases), + ) + for c in cases: + out += indent(emit_case(c)) + "\n" + out += "};\n\n#endif /* SECP256K1_MODULE_FROST_ENROLLMENT_VECTORS_H */\n" + sys.stdout.write(out) + + +if __name__ == "__main__": + main()