iceberg: add the example program
Port examples/iceberg.c from the source tree: a full Iceberg session demonstrating the call order from the module docs -- distributed key generation, pubshare_gen/pubkey_agg to obtain the group public key, nonce_gen/nonce_agg into an ordinary MuSig2 public nonce, and partial_sign/partial_sig_agg into an ordinary MuSig2 partial signature. One content adaptation: the secp256k1_musig_nonce_process call gains a NULL adaptor argument, matching this repo's zkp musig variant. Wired like the chilldkg example: autotools noinst_PROGRAMS + TESTS entry under ENABLE_MODULE_ICEBERG (the example runs as part of make check), CMake example target in examples/CMakeLists.txt, and iceberg_example added to .gitignore. Verified: ./iceberg_example runs to completion (exit 0) under both build systems.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -57,6 +57,7 @@ contrib/gh-pr-create.sh
|
||||
|
||||
frost_example
|
||||
chilldkg_example
|
||||
iceberg_example
|
||||
|
||||
### CMake
|
||||
/CMakeUserPresets.json
|
||||
|
||||
11
Makefile.am
11
Makefile.am
@@ -234,6 +234,17 @@ chilldkg_example_LDFLAGS += -lbcrypt
|
||||
endif
|
||||
TESTS += chilldkg_example
|
||||
endif
|
||||
if ENABLE_MODULE_ICEBERG
|
||||
noinst_PROGRAMS += iceberg_example
|
||||
iceberg_example_SOURCES = examples/iceberg.c
|
||||
iceberg_example_CPPFLAGS = -I$(top_srcdir)/include -DSECP256K1_STATIC
|
||||
iceberg_example_LDADD = libsecp256k1.la
|
||||
iceberg_example_LDFLAGS = -static
|
||||
if BUILD_WINDOWS
|
||||
iceberg_example_LDFLAGS += -lbcrypt
|
||||
endif
|
||||
TESTS += iceberg_example
|
||||
endif
|
||||
endif
|
||||
|
||||
### Precomputed tables
|
||||
|
||||
@@ -39,3 +39,7 @@ endif()
|
||||
if(SECP256K1_ENABLE_MODULE_CHILLDKG)
|
||||
add_example(chilldkg)
|
||||
endif()
|
||||
|
||||
if(SECP256K1_ENABLE_MODULE_ICEBERG)
|
||||
add_example(iceberg)
|
||||
endif()
|
||||
|
||||
725
examples/iceberg.c
Normal file
725
examples/iceberg.c
Normal file
@@ -0,0 +1,725 @@
|
||||
/*************************************************************************
|
||||
* To the extent possible under law, the author(s) have dedicated all *
|
||||
* copyright and related and neighboring rights to the software in this *
|
||||
* file to the public domain worldwide. This software is distributed *
|
||||
* without any warranty. For the CC0 Public Domain Dedication, see *
|
||||
* EXAMPLES_COPYING or https://creativecommons.org/publicdomain/zero/1.0 *
|
||||
*************************************************************************/
|
||||
|
||||
/** A 3-of-7 group signing beside an ordinary MuSig2 cosigner.
|
||||
*
|
||||
* The group behaves as one MuSig2 participant, and the finished signature is
|
||||
* an ordinary BIP-340 signature that records nothing about the group.
|
||||
*
|
||||
* Three roles appear below and they run different code, which is the thing
|
||||
* worth keeping straight while reading:
|
||||
*
|
||||
* participant holds a share, produces a nonce and a signature share, and
|
||||
* never sees the whole key. There are seven of them.
|
||||
* coordinator moves messages around and combines them. Untrusted: every
|
||||
* check here assumes it is hostile.
|
||||
* cosigner an ordinary MuSig2 signer that knows nothing about any of
|
||||
* this and calls the plain musig API.
|
||||
*
|
||||
* The participants are wiped between the two rounds and rebuilt from storage,
|
||||
* because no secret nonce survives that gap; see reboot_participants for what
|
||||
* does survive it, which is not nothing. One call is made that is expected to
|
||||
* fail, in refusals_are_refused.
|
||||
*
|
||||
* See also include/secp256k1_iceberg.h and doc/iceberg.md.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <secp256k1.h>
|
||||
#include <secp256k1_extrakeys.h>
|
||||
#include <secp256k1_schnorrsig.h>
|
||||
#include <secp256k1_musig.h>
|
||||
#include <secp256k1_iceberg.h>
|
||||
/* The trusted dealer lives in its own header, and is not part of the installed
|
||||
* API. See the note at the top of it. */
|
||||
#include <secp256k1_iceberg_dealer.h>
|
||||
|
||||
#include "examples_util.h"
|
||||
|
||||
#define N 7 /* participants in the group */
|
||||
#define T 3 /* how many of them can sign */
|
||||
#define MU (2 * T - 1) /* how many must take part in round one */
|
||||
|
||||
/* One participant's memory: one secret, and one note to itself.
|
||||
*
|
||||
* The share is the secret and never changes. `answered` is the highest session
|
||||
* label this participant has signed under. The library does not know about it and
|
||||
* could not, since it holds nothing between calls, but answering twice under one
|
||||
* label gives away the key share, so somebody has to remember, and the somebody
|
||||
* is the caller. */
|
||||
struct participant {
|
||||
secp256k1_iceberg_share share;
|
||||
unsigned char answered[32];
|
||||
};
|
||||
|
||||
/* The same participant's disk. A reboot loses the struct above, not this one,
|
||||
* and both fields have to come back. See reboot_participants. */
|
||||
struct storage {
|
||||
unsigned char share_bytes[SECP256K1_ICEBERG_SHARE_MAX_LEN];
|
||||
size_t share_len;
|
||||
unsigned char answered_bytes[32];
|
||||
};
|
||||
|
||||
/* The threshold side. Set up once, then unchanged for the life of the group. */
|
||||
struct group {
|
||||
struct participant member[N];
|
||||
struct storage disk[N];
|
||||
secp256k1_iceberg_pubshare pubshare[N];
|
||||
secp256k1_pubkey pubkey;
|
||||
};
|
||||
|
||||
/* The other side: an ordinary MuSig2 signer, which knows nothing about any of
|
||||
* the above. Everything it holds is its own, including the secret nonce that
|
||||
* the group deliberately does not have an equivalent of. */
|
||||
struct cosigner {
|
||||
secp256k1_keypair keypair;
|
||||
secp256k1_pubkey pubkey;
|
||||
secp256k1_musig_secnonce secnonce;
|
||||
};
|
||||
|
||||
/* What the two of them add up to. Computed once, at key aggregation, and the
|
||||
* only thing a verifier ever sees. */
|
||||
struct shared_key {
|
||||
secp256k1_musig_keyagg_cache keyagg_cache;
|
||||
secp256k1_xonly_pubkey output;
|
||||
};
|
||||
|
||||
/* One attempt at one signature. Everything here is public. */
|
||||
struct signing_session {
|
||||
unsigned char sid[32];
|
||||
secp256k1_iceberg_pubnonce contribution[MU];
|
||||
const secp256k1_iceberg_pubnonce *contribution_ptr[MU];
|
||||
secp256k1_musig_pubnonce group_nonce; /* the group's, after interpolating */
|
||||
secp256k1_musig_pubnonce cosigner_nonce; /* the cosigner's, as published */
|
||||
secp256k1_musig_aggnonce cosigner_aggnonce; /* the cosigners' alone */
|
||||
secp256k1_musig_session musig_session;
|
||||
};
|
||||
|
||||
static void heading(const char *text) {
|
||||
printf("\n%s\n", text);
|
||||
}
|
||||
|
||||
static void step(const char *text) {
|
||||
printf(" %-55s", text);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
/* Deal the shares and publish the group's key.
|
||||
*
|
||||
* This is a trusted dealer: for the length of one call, this machine holds
|
||||
* enough to reconstruct the group's private key. Acceptable for testing, and
|
||||
* where one party is trusted already. A distributed key generation produces
|
||||
* the same shares without that moment ever existing, and this module does not
|
||||
* provide one. */
|
||||
static int deal_shares(const secp256k1_context *ctx, struct group *group) {
|
||||
secp256k1_iceberg_share *share_ptr[N];
|
||||
const secp256k1_iceberg_pubshare *pubshare_ptr[N];
|
||||
unsigned char seed[32];
|
||||
unsigned int k;
|
||||
int ok;
|
||||
|
||||
step("Dealing shares (secp256k1_iceberg_shares_gen)");
|
||||
if (!fill_random(seed, sizeof(seed))) {
|
||||
return 0;
|
||||
}
|
||||
for (k = 0; k < N; k++) {
|
||||
share_ptr[k] = &group->member[k].share;
|
||||
}
|
||||
ok = secp256k1_iceberg_shares_gen(ctx, share_ptr, N, T, seed);
|
||||
secure_erase(seed, sizeof(seed));
|
||||
if (!ok) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
|
||||
/* A share is one seed per (t-1)-subset that leaves this participant out,
|
||||
* so it is large and grows quickly with the group. Each participant keeps
|
||||
* its own and nothing else. */
|
||||
step("Storing them (secp256k1_iceberg_share_serialize)");
|
||||
for (k = 0; k < N; k++) {
|
||||
group->disk[k].share_len = sizeof(group->disk[k].share_bytes);
|
||||
if (!secp256k1_iceberg_share_serialize(ctx, group->disk[k].share_bytes,
|
||||
&group->disk[k].share_len, &group->member[k].share)) {
|
||||
return 0;
|
||||
}
|
||||
/* Nothing answered yet, so the lowest possible label. Do this once, at
|
||||
* dealing; doing it again later throws the protection away. */
|
||||
memset(group->member[k].answered, 0, sizeof(group->member[k].answered));
|
||||
memcpy(group->disk[k].answered_bytes, group->member[k].answered, 32);
|
||||
}
|
||||
printf("ok, %lu bytes each\n", (unsigned long)group->disk[0].share_len);
|
||||
|
||||
step("Group public key (secp256k1_iceberg_pubkey_agg)");
|
||||
for (k = 0; k < N; k++) {
|
||||
if (!secp256k1_iceberg_pubshare_gen(ctx, &group->pubshare[k], &group->member[k].share, NULL)) {
|
||||
return 0;
|
||||
}
|
||||
pubshare_ptr[k] = &group->pubshare[k];
|
||||
}
|
||||
/* 2t-1 public shares are more than the key needs, since t of them already
|
||||
* determine it, and the surplus is the point: they have to agree, so a
|
||||
* participant that published a wrong one is caught now rather than at
|
||||
* signing time. */
|
||||
if (!secp256k1_iceberg_pubkey_agg(ctx, &group->pubkey, pubshare_ptr, MU, N, T)) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Aggregate the group's key with the cosigner's, and tweak the result.
|
||||
*
|
||||
* Nothing below this point knows that one of the two keys is a group. The
|
||||
* tweak belongs to this outer session: musig_nonce_process sets its term aside
|
||||
* and musig_partial_sig_agg adds it once, at the top. No Iceberg call is even
|
||||
* told the tweak happened, which is what stops it being counted twice. */
|
||||
static int aggregate_keys(const secp256k1_context *ctx, struct group *group, struct cosigner *cosigner, struct shared_key *shared) {
|
||||
const secp256k1_pubkey *pubkeys[2];
|
||||
secp256k1_pubkey output_pk;
|
||||
unsigned char taptweak[32];
|
||||
unsigned char seckey[32];
|
||||
int ok;
|
||||
|
||||
step("Cosigner keypair (secp256k1_keypair_create)");
|
||||
ok = fill_random(seckey, sizeof(seckey))
|
||||
&& secp256k1_keypair_create(ctx, &cosigner->keypair, seckey)
|
||||
&& secp256k1_keypair_pub(ctx, &cosigner->pubkey, &cosigner->keypair);
|
||||
/* The keypair holds everything needed from here on, so the raw key does not
|
||||
* outlive this call; secp256k1_keypair_sec brings it back when required. */
|
||||
secure_erase(seckey, sizeof(seckey));
|
||||
if (!ok) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
|
||||
step("Aggregating (secp256k1_musig_pubkey_agg)");
|
||||
pubkeys[0] = &group->pubkey;
|
||||
pubkeys[1] = &cosigner->pubkey;
|
||||
if (!secp256k1_musig_pubkey_agg(ctx, NULL, &shared->keyagg_cache, pubkeys, 2)) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
|
||||
/* The cache records the hash of the key list, not the list, so nothing ties
|
||||
* it to the group's key later. Run this once, here, rather than trusting a
|
||||
* cache round two cannot check. */
|
||||
step("Checking the cache (secp256k1_iceberg_keyagg_check)");
|
||||
if (!secp256k1_iceberg_keyagg_check(ctx, &shared->keyagg_cache, pubkeys, 2,
|
||||
&group->pubkey)) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
|
||||
step("Tweaking (secp256k1_musig_pubkey_xonly_tweak_add)");
|
||||
if (!fill_random(taptweak, sizeof(taptweak))
|
||||
|| !secp256k1_musig_pubkey_xonly_tweak_add(ctx, &output_pk, &shared->keyagg_cache, taptweak)
|
||||
|| !secp256k1_xonly_pubkey_from_pubkey(ctx, &shared->output, NULL, &output_pk)) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Round one, the group's half. Note the arguments: a share and a label. No
|
||||
* message, no cosigner, nothing from anybody else. */
|
||||
static int group_round_one(const secp256k1_context *ctx, struct group *group,
|
||||
struct signing_session *session) {
|
||||
unsigned int k;
|
||||
|
||||
/* The label is the most delicate value in the scheme, and it is the
|
||||
* caller's to choose. Every participant's secret nonces are a function of
|
||||
* (its seeds, this label), and the seeds never change, so whoever picks the
|
||||
* label picks everyone's secrets.
|
||||
*
|
||||
* Here it is a fixed byte string, which is fine for an example and wrong
|
||||
* for anything else. In Lightning it is the commitment number: unique,
|
||||
* strictly increasing over the channel's life, and known before the
|
||||
* transaction is assembled, which is what lets this round run at all,
|
||||
* since the message does not exist yet. */
|
||||
step("Choosing the session label");
|
||||
memset(session->sid, 0x2c, sizeof(session->sid));
|
||||
printf("ok, ");
|
||||
print_hex(session->sid, 4);
|
||||
|
||||
step("Participant nonces (secp256k1_iceberg_nonce_gen)");
|
||||
for (k = 0; k < MU; k++) {
|
||||
if (!secp256k1_iceberg_nonce_gen(ctx, &session->contribution[k], &group->member[k].share, NULL, session->sid)) {
|
||||
return 0;
|
||||
}
|
||||
session->contribution_ptr[k] = &session->contribution[k];
|
||||
}
|
||||
printf("ok, %d of the %d participants\n", MU, N);
|
||||
|
||||
/* Verification, not addition. The contributions are points on a degree
|
||||
* t-1 polynomial in the exponent; this checks that they lie on one and
|
||||
* then interpolates. It is most of what the group costs, and it is what
|
||||
* stops a single participant biasing the group's nonce.
|
||||
*
|
||||
* NULL is the group's internal aggregate, which nothing takes back. */
|
||||
step("Combining them (secp256k1_iceberg_nonce_agg)");
|
||||
if (!secp256k1_iceberg_nonce_agg(ctx, &session->group_nonce, NULL,
|
||||
session->contribution_ptr, MU, N, T, &group->pubkey)) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Round one, the cosigner's half. Ordinary MuSig2, and it does not know a group
|
||||
* exists. Runs before, after or alongside the function above. */
|
||||
static int cosigner_round_one(const secp256k1_context *ctx, struct cosigner *cosigner, struct shared_key *shared,
|
||||
struct signing_session *session, const unsigned char *msg32) {
|
||||
const secp256k1_musig_pubnonce *just_the_cosigner[1];
|
||||
unsigned char secrand[32];
|
||||
unsigned char seckey[32];
|
||||
int ok;
|
||||
|
||||
step("Cosigner nonce (secp256k1_musig_nonce_gen)");
|
||||
ok = fill_random(secrand, sizeof(secrand))
|
||||
&& secp256k1_keypair_sec(ctx, seckey, &cosigner->keypair)
|
||||
&& secp256k1_musig_nonce_gen(ctx, &cosigner->secnonce, &session->cosigner_nonce,
|
||||
secrand, seckey, &cosigner->pubkey,
|
||||
msg32, &shared->keyagg_cache, NULL);
|
||||
secure_erase(secrand, sizeof(secrand));
|
||||
secure_erase(seckey, sizeof(seckey));
|
||||
if (!ok) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Round two needs the cosigners' aggregate on its own, separately from the
|
||||
* one that includes the group. With one cosigner it is an aggregate of one,
|
||||
* which is not a special case anywhere. */
|
||||
just_the_cosigner[0] = &session->cosigner_nonce;
|
||||
if (!secp256k1_musig_nonce_agg(ctx, &session->cosigner_aggnonce, just_the_cosigner, 1)) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Both halves have published. Combine them into the session everybody signs
|
||||
* against, which is where the message finally enters. */
|
||||
static int open_session(const secp256k1_context *ctx, struct shared_key *shared,
|
||||
struct signing_session *session, const unsigned char *msg32) {
|
||||
const secp256k1_musig_pubnonce *both[2];
|
||||
secp256k1_musig_aggnonce full_aggnonce;
|
||||
|
||||
step("Session (secp256k1_musig_nonce_process)");
|
||||
both[0] = &session->group_nonce;
|
||||
both[1] = &session->cosigner_nonce;
|
||||
if (!secp256k1_musig_nonce_agg(ctx, &full_aggnonce, both, 2)
|
||||
|| !secp256k1_musig_nonce_process(ctx, &session->musig_session, &full_aggnonce,
|
||||
msg32, &shared->keyagg_cache, NULL)) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Forget the secret nonces, then rebuild from disk.
|
||||
*
|
||||
* A FROST signer has to keep a secret nonce alive across this line, and losing
|
||||
* it, restoring an old backup over it, or running two copies of the signer are
|
||||
* each catastrophic. An Iceberg participant keeps no secret nonce at all: its
|
||||
* nonces are a function of its share and the label, both of which it is given
|
||||
* again.
|
||||
*
|
||||
* That property is narrower than "keeps no state".
|
||||
* A participant still has to remember which labels it has already answered
|
||||
* under, because two answers under one label are two equations in its three
|
||||
* secrets and three are enough to recover its key share. That bookkeeping is
|
||||
* not secret, and it is not optional; it is simply not this library's, since
|
||||
* only the group as a whole can decide which message a label belongs to.
|
||||
*
|
||||
* So wipe the secrets and prove that much. Skip the parse below and the next
|
||||
* call is handed a wiped share, whose magic fails an ARG_CHECK, so it aborts
|
||||
* through the illegal callback rather than returning 0. */
|
||||
static int reboot_participants(const secp256k1_context *ctx, struct group *group) {
|
||||
unsigned int k;
|
||||
|
||||
step("Wiping every participant's secret material");
|
||||
for (k = 0; k < N; k++) {
|
||||
secure_erase(&group->member[k], sizeof(group->member[k]));
|
||||
}
|
||||
printf("ok\n");
|
||||
|
||||
step("Rebuilding (secp256k1_iceberg_share_parse)");
|
||||
for (k = 0; k < N; k++) {
|
||||
if (!secp256k1_iceberg_share_parse(ctx, &group->member[k].share,
|
||||
group->disk[k].share_bytes,
|
||||
group->disk[k].share_len)) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(group->member[k].answered, group->disk[k].answered_bytes, 32);
|
||||
}
|
||||
printf("ok\n");
|
||||
|
||||
/* Note what came back besides the share: `answered`. That record is not
|
||||
* secret, which is why it sits on the same disk in the clear, and it is
|
||||
* not optional, which is why it is restored here rather than left at zero.
|
||||
* A participant that forgets it will answer twice. */
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* One label, one answer: the half of the rule a participant can enforce alone.
|
||||
*
|
||||
* The library cannot do this for you. It holds nothing between calls, so "have I
|
||||
* answered under this label before?" is a question only the caller's storage can
|
||||
* answer. Here that storage is one 32-byte field per participant and the rule is
|
||||
* that a label must be strictly greater than the last one signed under, which is
|
||||
* free when the label is a counter. It refuses a repeat outright rather than
|
||||
* asking whether the repeat was harmless: a label is one signing attempt, so an
|
||||
* honest retry arrives under a new label and never needs the exception.
|
||||
*
|
||||
* The record reaches disk here, before the caller has a share to publish. A
|
||||
* crash between signing and storing is the same as never having stored, and the
|
||||
* next boot answers the label again.
|
||||
*
|
||||
* The other half of the rule, that no two participants answer one label on
|
||||
* different messages, cannot be checked here or anywhere else inside a
|
||||
* participant, because it is a fact about what other people were shown. See
|
||||
* doc/iceberg.md. */
|
||||
static int may_sign_under(struct participant *member, struct storage *disk, const unsigned char *sid32) {
|
||||
if (memcmp(sid32, member->answered, 32) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(member->answered, sid32, 32);
|
||||
memcpy(disk->answered_bytes, member->answered, 32);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The group's half of round two, signed by participants first, first+1 and
|
||||
* first+2.
|
||||
*
|
||||
* Taking `first` is not generality for its own sake. The point of the scheme is
|
||||
* that any t of them will do, so main runs this twice with different people and
|
||||
* compares the two signatures.
|
||||
*
|
||||
* Any t of the seven, and not only the five who were in round one. The last
|
||||
* section of main proves that by having a participant who sat round one out
|
||||
* sign anyway. What each signer does need is the whole round-one set to check
|
||||
* against, which is a different count from the number of signers. */
|
||||
static int group_signs(const secp256k1_context *ctx, struct group *group,
|
||||
struct shared_key *shared, struct signing_session *session,
|
||||
unsigned int first, const unsigned char *msg32,
|
||||
secp256k1_musig_partial_sig *group_partial_sig) {
|
||||
secp256k1_iceberg_partial_sig sig_share[T];
|
||||
const secp256k1_iceberg_partial_sig *sig_share_ptr[T];
|
||||
unsigned int k;
|
||||
|
||||
/* Note what this takes: the group's own round-one contributions, not an
|
||||
* aggregate of them, and not the cosigners', which arrive separately and
|
||||
* already aggregated. Do not read that as an optimization waiting to happen.
|
||||
* The nesting coefficient is a hash of the group's aggregate nonce, so a
|
||||
* coordinator free to invent that aggregate has a coefficient it can vary at
|
||||
* will, and three invented aggregates under one label give three equations in
|
||||
* the same three unknowns, and the third is the key. So the aggregate is
|
||||
* derived here from the contributions instead. */
|
||||
step("Signature shares (secp256k1_iceberg_partial_sign)");
|
||||
for (k = 0; k < T; k++) {
|
||||
if (!may_sign_under(&group->member[first + k], &group->disk[first + k], session->sid)) {
|
||||
printf("refused: participant %d has already answered under this label\n",
|
||||
first + k + 1);
|
||||
return 0;
|
||||
}
|
||||
/* The signer derives the aggregate from the contributions, then checks
|
||||
* the polynomial they determine against the contribution it derives for
|
||||
* itself, which ties the set to this label. It need not have been one of
|
||||
* the contributors. */
|
||||
if (!secp256k1_iceberg_partial_sign(ctx, &sig_share[k], &group->member[first + k].share, NULL,
|
||||
session->sid, session->contribution_ptr, MU,
|
||||
&group->pubkey, &shared->keyagg_cache,
|
||||
msg32, &session->cosigner_aggnonce)) {
|
||||
return 0;
|
||||
}
|
||||
sig_share_ptr[k] = &sig_share[k];
|
||||
}
|
||||
printf("ok, from participants %d, %d and %d\n", first + 1, first + 2, first + 3);
|
||||
|
||||
/* Optional, and the coordinator's to decide on. Skipping it costs nothing
|
||||
* until a share is bad, at which point the final signature simply fails
|
||||
* BIP-340 and says nothing about which of the three caused it. Checking
|
||||
* costs about what producing a share costs, per share.
|
||||
*
|
||||
* That is with exactly t shares, which is what this collects. Hand
|
||||
* secp256k1_iceberg_partial_sig_agg one more than it needs and it refuses a
|
||||
* set that disagrees with itself, cheaply and without naming anybody, for
|
||||
* the same reason pubkey_agg can above: past the threshold there is a spare
|
||||
* point to check against.
|
||||
*
|
||||
* It answers "is this share bad", not "who is lying". A share that fails
|
||||
* here may have been written by somebody other than the member it names,
|
||||
* and the same 0 comes back if this machine has the wrong message. */
|
||||
step("Checking them (secp256k1_iceberg_partial_sig_verify)");
|
||||
for (k = 0; k < T; k++) {
|
||||
if (!secp256k1_iceberg_partial_sig_verify(ctx, &sig_share[k], &group->pubshare[first + k],
|
||||
session->contribution_ptr, MU, N, T,
|
||||
&group->pubkey, &shared->keyagg_cache, msg32,
|
||||
&session->cosigner_aggnonce)) {
|
||||
printf("share from participant %d does not check out\n", first + k + 1);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
printf("ok, all %d\n", T);
|
||||
|
||||
step("Combining (secp256k1_iceberg_partial_sig_agg)");
|
||||
if (!secp256k1_iceberg_partial_sig_agg(ctx, group_partial_sig, sig_share_ptr, T, N, T)) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* The cosigner's half, which happens exactly once.
|
||||
*
|
||||
* secp256k1_musig_partial_sign consumes the secret nonce, zeroing it on
|
||||
* the way out, precisely so that signing twice with it is not something a
|
||||
* caller can do by accident. Which quorum the group fielded is not the
|
||||
* cosigner's business and does not change its answer. */
|
||||
static int cosigner_signs(const secp256k1_context *ctx, struct cosigner *cosigner,
|
||||
struct shared_key *shared, struct signing_session *session,
|
||||
secp256k1_musig_partial_sig *cosigner_partial_sig) {
|
||||
step("Cosigner's share (secp256k1_musig_partial_sign)");
|
||||
if (!secp256k1_musig_partial_sign(ctx, cosigner_partial_sig, &cosigner->secnonce,
|
||||
&cosigner->keypair, &shared->keyagg_cache,
|
||||
&session->musig_session)) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* One group share plus one cosigner share makes an ordinary MuSig2 signature. */
|
||||
static int combine(const secp256k1_context *ctx, struct signing_session *session,
|
||||
const secp256k1_musig_partial_sig *group_partial_sig,
|
||||
const secp256k1_musig_partial_sig *cosigner_partial_sig,
|
||||
unsigned char *sig64) {
|
||||
const secp256k1_musig_partial_sig *both[2];
|
||||
|
||||
step("Final signature (secp256k1_musig_partial_sig_agg)");
|
||||
both[0] = group_partial_sig;
|
||||
both[1] = cosigner_partial_sig;
|
||||
if (!secp256k1_musig_partial_sig_agg(ctx, sig64, &session->musig_session, both, 2)) {
|
||||
return 0;
|
||||
}
|
||||
printf("ok\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* One call that must be refused and one that must not, with the reason beside
|
||||
* each.
|
||||
*
|
||||
* The refusal below is a load-bearing security check that looks like a bug from
|
||||
* outside; the call after it is the case that looks refusable and must not
|
||||
* be. */
|
||||
static int refusals_are_refused(const secp256k1_context *ctx, struct group *group,
|
||||
struct shared_key *shared, struct signing_session *session,
|
||||
const unsigned char *msg32) {
|
||||
secp256k1_iceberg_partial_sig sig_share;
|
||||
|
||||
step("Signing over contributions from another session");
|
||||
{
|
||||
/* The contributions below are a real sharing, internally consistent
|
||||
* and passing the degree check, but they were produced under a
|
||||
* different label. Accepting them would give whoever supplied them a
|
||||
* coefficient in the signing equation that this participant cannot
|
||||
* check, which is worth a signature share to an adversary and nothing
|
||||
* to anybody else.
|
||||
*
|
||||
* The signer catches it by interpolating the contributions and
|
||||
* evaluating the result at its own index: the polynomial does not pass
|
||||
* through the contribution it derives locally for the label it is
|
||||
* signing under. */
|
||||
secp256k1_iceberg_pubnonce elsewhere[MU];
|
||||
const secp256k1_iceberg_pubnonce *elsewhere_ptrs[MU];
|
||||
unsigned char other_sid[32];
|
||||
unsigned int k;
|
||||
|
||||
memcpy(other_sid, session->sid, sizeof(other_sid));
|
||||
other_sid[0] ^= 1;
|
||||
for (k = 0; k < MU; k++) {
|
||||
if (!secp256k1_iceberg_nonce_gen(ctx, &elsewhere[k], &group->member[k].share,
|
||||
NULL, other_sid)) {
|
||||
return 0;
|
||||
}
|
||||
elsewhere_ptrs[k] = &elsewhere[k];
|
||||
}
|
||||
/* The set is a perfectly good sharing. What it is not is a sharing of
|
||||
* *this* label, and only the signer can tell, because only the signer
|
||||
* holds the share that says what its own contribution should have
|
||||
* been. */
|
||||
if (secp256k1_iceberg_partial_sign(ctx, &sig_share, &group->member[0].share, NULL,
|
||||
session->sid, elsewhere_ptrs, MU, &group->pubkey,
|
||||
&shared->keyagg_cache, msg32,
|
||||
&session->cosigner_aggnonce)) {
|
||||
printf("FAILED: it signed, and it should not have\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
printf("refused, correctly\n");
|
||||
|
||||
/* The other half of that check is a thing it must NOT cost. */
|
||||
step("Signing by a member who sat round one out");
|
||||
{
|
||||
/* Participants 6 and 7 were not among the 2t-1 who produced nonces.
|
||||
* They can still sign, because a contribution is a function of the
|
||||
* share and the label, so an absent member can derive what its
|
||||
* contribution would have been and check the polynomial against it
|
||||
* without ever having been one of the contributors.
|
||||
*
|
||||
* This is the property that lets a quorum change between the rounds,
|
||||
* which is most of why the scheme works this way: keys sit in
|
||||
* cold storage and members are routinely absent rather than hostile. */
|
||||
if (!secp256k1_iceberg_partial_sign(ctx, &sig_share, &group->member[N - 1].share, NULL,
|
||||
session->sid, session->contribution_ptr, MU,
|
||||
&group->pubkey, &shared->keyagg_cache, msg32,
|
||||
&session->cosigner_aggnonce)) {
|
||||
printf("FAILED: it refused, and it should not have\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
printf("signed, correctly\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void print_caveats(void) {
|
||||
heading("WHAT THIS EXAMPLE IS NOT");
|
||||
printf(" A trusted dealer deals the shares. Real deployments want a\n");
|
||||
printf(" distributed key generation, which this module does not provide.\n");
|
||||
printf("\n");
|
||||
printf(" Everything is passed as structs in one process. The wire formats\n");
|
||||
printf(" exist: 34 bytes for a public share, 67 for a nonce contribution,\n");
|
||||
printf(" 33 for a signature share. Nothing here uses them, so the\n");
|
||||
printf(" network is the part you still have to write.\n");
|
||||
printf("\n");
|
||||
printf(" Structs in one process also satisfy constraint 2 for free. Over a\n");
|
||||
printf(" network the channel must be authenticated and a contribution taken\n");
|
||||
printf(" only from the member its index names; without that, an adversary\n");
|
||||
printf(" supplying t of the 2t-1 picks the group's nonce and every check\n");
|
||||
printf(" here still passes.\n");
|
||||
printf("\n");
|
||||
printf(" Verifying a share tells you whether it satisfies the equation,\n");
|
||||
printf(" not who is at fault: partial signatures are forgeable, so a share\n");
|
||||
printf(" that fails may have been written by somebody else.\n");
|
||||
printf("\n");
|
||||
printf(" may_sign_under above is half the rule, and the easy half: it\n");
|
||||
printf(" stops one participant answering twice. Nothing anywhere stops two\n");
|
||||
printf(" participants answering one label on different messages. Two answers\n");
|
||||
printf(" under one label are two equations in a participant's three\n");
|
||||
printf(" secrets; three recover its key share.\n");
|
||||
printf("\n");
|
||||
printf(" The label is an argument, rather than something derived from the\n");
|
||||
printf(" message, because round one has to run before the message exists.\n");
|
||||
printf(" In Lightning the nonce is fixed a round-trip before the\n");
|
||||
printf(" transaction is assembled, and the commitment number is what\n");
|
||||
printf(" Lightning supplies instead.\n");
|
||||
printf("\n");
|
||||
printf(" So half of that discipline is yours, never answering twice under\n");
|
||||
printf(" one label, and half is the group's: agree which message a label\n");
|
||||
printf(" belongs to before anyone answers. The unforgeability proof, which\n");
|
||||
printf(" is not yet peer-reviewed, assumes both. See doc/iceberg.md.\n");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
secp256k1_context *ctx;
|
||||
struct group group;
|
||||
struct cosigner cosigner;
|
||||
struct shared_key shared;
|
||||
struct signing_session session;
|
||||
unsigned char msg[32] = "this_could_be_the_hash_of_a_msg";
|
||||
secp256k1_musig_partial_sig group_partial_sig, cosigner_partial_sig;
|
||||
unsigned char sig[64], sig_from_the_others[64];
|
||||
unsigned int k;
|
||||
int ok;
|
||||
|
||||
ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
|
||||
|
||||
printf("Iceberg: a %d-of-%d group signing as one MuSig2 participant.\n\n", T, N);
|
||||
printf(" round one needs %d of the %d participants; round two needs %d\n", MU, N, MU);
|
||||
printf(" online again, not necessarily the same ones, and combines %d\n", T);
|
||||
printf(" signature shares. See doc/iceberg.md for why those differ.\n");
|
||||
|
||||
heading("Setup: once, by a dealer who is then not needed again");
|
||||
ok = deal_shares(ctx, &group);
|
||||
|
||||
if (ok) {
|
||||
heading("Key aggregation: the group is now just a public key");
|
||||
ok = aggregate_keys(ctx, &group, &cosigner, &shared);
|
||||
}
|
||||
if (ok) {
|
||||
/* The two halves are written in this order because something has to
|
||||
* go first on the page. Swap the two calls and the example still
|
||||
* passes, since neither needs anything the other produces, which is why
|
||||
* two Iceberg groups can sign with each other. */
|
||||
heading("Round one: needs 2t-1 participants, in no particular order");
|
||||
ok = group_round_one(ctx, &group, &session)
|
||||
&& cosigner_round_one(ctx, &cosigner, &shared, &session, msg)
|
||||
&& open_session(ctx, &shared, &session, msg);
|
||||
}
|
||||
if (ok) {
|
||||
heading("The gap: every participant forgets its secret nonces");
|
||||
ok = reboot_participants(ctx, &group);
|
||||
}
|
||||
if (ok) {
|
||||
heading("Round two: t shares, from any members, not just round one's");
|
||||
ok = group_signs(ctx, &group, &shared, &session, 0, msg, &group_partial_sig)
|
||||
&& cosigner_signs(ctx, &cosigner, &shared, &session, &cosigner_partial_sig)
|
||||
&& combine(ctx, &session, &group_partial_sig, &cosigner_partial_sig, sig);
|
||||
}
|
||||
if (ok) {
|
||||
step("Verifying (secp256k1_schnorrsig_verify)");
|
||||
ok = secp256k1_schnorrsig_verify(ctx, sig, msg, 32, &shared.output);
|
||||
if (ok) {
|
||||
printf("ok\n ");
|
||||
print_hex(sig, sizeof(sig));
|
||||
printf(" An ordinary BIP-340 signature. Nothing in it records a group.\n");
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
heading("The same signature, from different people");
|
||||
/* Participants 4, 5 and 6 this time, with no overlap with 1, 2 and 3, and
|
||||
* 6 was not in round one either. Both of those matter. Disjoint,
|
||||
* because nobody may answer twice under one label. The library holds
|
||||
* nothing between calls and cannot detect that, so may_sign_under above
|
||||
* is what refuses it. And 6 absent from round one, because a member that
|
||||
* was away can still work out what its contribution would have been.
|
||||
*
|
||||
* Only the group signs again. The cosigner's share is reused, since it
|
||||
* has one secret nonce and spending it twice would be nonce reuse. */
|
||||
ok = group_signs(ctx, &group, &shared, &session, T, msg, &group_partial_sig)
|
||||
&& combine(ctx, &session, &group_partial_sig, &cosigner_partial_sig, sig_from_the_others);
|
||||
}
|
||||
if (ok) {
|
||||
step("Comparing the two signatures");
|
||||
ok = memcmp(sig, sig_from_the_others, sizeof(sig)) == 0;
|
||||
printf(ok ? "byte-identical\n" : "the two quorums disagreed\n");
|
||||
}
|
||||
if (ok) {
|
||||
heading("What the module checks, and what it leaves to you");
|
||||
ok = refusals_are_refused(ctx, &group, &shared, &session, msg);
|
||||
}
|
||||
if (!ok) {
|
||||
printf("FAILED\n");
|
||||
}
|
||||
print_caveats();
|
||||
|
||||
/* Clear the secrets: a bug elsewhere that leaks memory, or an OS that swaps
|
||||
* it to disk, should not find them lying around. The group's shares are the
|
||||
* long-term secret here, and the cosigner's keypair and secret nonce are
|
||||
* the rest. */
|
||||
for (k = 0; k < N; k++) {
|
||||
secure_erase(&group.member[k], sizeof(group.member[k]));
|
||||
secure_erase(&group.disk[k], sizeof(group.disk[k]));
|
||||
}
|
||||
secure_erase(&cosigner, sizeof(cosigner));
|
||||
secp256k1_context_destroy(ctx);
|
||||
return ok ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
Reference in New Issue
Block a user