-
Notifications
You must be signed in to change notification settings - Fork 216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
FROST #138
base: master
Are you sure you want to change the base?
FROST #138
Changes from 1 commit
2f3fa4c
2336b02
197fb7e
f606507
17c47e9
67c21be
ef15156
c2d48a7
fb9c5e8
ef0f4aa
68b2867
cac1662
07b7321
5c9fb08
421bc0a
7ee2fc0
2b8fcf0
82b3646
18fbec7
09698c8
05fb9fd
5f76404
4442867
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
This commit adds an example file to demonstrate how to use the module.
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,294 @@ | ||
/*********************************************************************** | ||
* Copyright (c) 2021-2024 Jesse Posner * | ||
* Distributed under the MIT software license, see the accompanying * | ||
* file COPYING or https://www.opensource.org/licenses/mit-license.php.* | ||
***********************************************************************/ | ||
|
||
/** | ||
* This file demonstrates how to use the FROST module to create a threshold | ||
* signature. Additionally, see the documentation in include/secp256k1_frost.h. | ||
*/ | ||
|
||
#include <stdio.h> | ||
#include <assert.h> | ||
#include <string.h> | ||
|
||
#include <secp256k1.h> | ||
#include <secp256k1_schnorrsig.h> | ||
#include <secp256k1_frost.h> | ||
|
||
#include "examples_util.h" | ||
/* Number of public keys involved in creating the aggregate signature */ | ||
#define N_SIGNERS 5 | ||
|
||
/* Threshold required in creating the aggregate signature */ | ||
#define THRESHOLD 3 | ||
|
||
struct signer_secrets { | ||
secp256k1_keypair keypair; | ||
secp256k1_frost_share agg_share; | ||
secp256k1_frost_secnonce secnonce; | ||
unsigned char seed[32]; | ||
}; | ||
|
||
struct signer { | ||
secp256k1_pubkey pubshare; | ||
secp256k1_frost_pubnonce pubnonce; | ||
secp256k1_frost_session session; | ||
secp256k1_frost_partial_sig partial_sig; | ||
secp256k1_pubkey vss_commitment[THRESHOLD]; | ||
unsigned char vss_hash[32]; | ||
unsigned char pok[64]; | ||
unsigned char id[33]; | ||
}; | ||
|
||
/* Create a key pair and store it in seckey and pubkey */ | ||
int create_keypair_and_seed(const secp256k1_context* ctx, struct signer_secrets *signer_secrets, struct signer *signer) { | ||
unsigned char seckey[32]; | ||
secp256k1_pubkey pubkey_tmp; | ||
size_t size = 33; | ||
|
||
while (1) { | ||
if (!fill_random(seckey, sizeof(seckey))) { | ||
printf("Failed to generate randomness\n"); | ||
return 1; | ||
} | ||
if (secp256k1_keypair_create(ctx, &signer_secrets->keypair, seckey)) { | ||
break; | ||
} | ||
} | ||
if (!secp256k1_keypair_pub(ctx, &pubkey_tmp, &signer_secrets->keypair)) { | ||
return 0; | ||
} | ||
if (!secp256k1_ec_pubkey_serialize(ctx, signer->id, &size, &pubkey_tmp, SECP256K1_EC_COMPRESSED)) { | ||
return 0; | ||
} | ||
if (!fill_random(signer_secrets->seed, sizeof(signer_secrets->seed))) { | ||
return 0; | ||
} | ||
return 1; | ||
} | ||
|
||
/* Create shares and coefficient commitments */ | ||
int create_shares(const secp256k1_context* ctx, struct signer_secrets *signer_secrets, struct signer *signer, secp256k1_xonly_pubkey *pk) { | ||
int i, j; | ||
secp256k1_frost_share shares[N_SIGNERS][N_SIGNERS]; | ||
const secp256k1_pubkey *vss_commitments[N_SIGNERS]; | ||
const unsigned char *ids[N_SIGNERS]; | ||
|
||
for (i = 0; i < N_SIGNERS; i++) { | ||
vss_commitments[i] = signer[i].vss_commitment; | ||
ids[i] = signer[i].id; | ||
} | ||
|
||
for (i = 0; i < N_SIGNERS; i++) { | ||
/* Generate a polynomial share for the participants */ | ||
if (!secp256k1_frost_shares_gen(ctx, shares[i], signer[i].vss_commitment, signer[i].pok, signer_secrets[i].seed, THRESHOLD, N_SIGNERS, ids)) { | ||
return 0; | ||
} | ||
} | ||
|
||
/* KeyGen communication round 1: exchange shares and coefficient | ||
* commitments */ | ||
for (i = 0; i < N_SIGNERS; i++) { | ||
const secp256k1_frost_share *assigned_shares[N_SIGNERS]; | ||
|
||
/* Each participant receives a share from each participant (including | ||
* themselves) corresponding to their index. */ | ||
for (j = 0; j < N_SIGNERS; j++) { | ||
assigned_shares[j] = &shares[j][i]; | ||
} | ||
/* Each participant aggregates the shares they received. */ | ||
jesseposner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!secp256k1_frost_share_agg(ctx, &signer_secrets[i].agg_share, pk, assigned_shares, vss_commitments, N_SIGNERS, THRESHOLD, signer[i].id)) { | ||
return 0; | ||
} | ||
for (j = 0; j < N_SIGNERS; j++) { | ||
/* Each participant verifies their shares. share_agg calls this | ||
* internally, so it is only neccessary to call this function if | ||
* share_agg returns an error, to determine which participant(s) | ||
* submitted faulty data. */ | ||
if (!secp256k1_frost_share_verify(ctx, THRESHOLD, signer[i].id, assigned_shares[j], &vss_commitments[j])) { | ||
return 0; | ||
} | ||
/* Each participant generates public verification shares that are | ||
* used for verifying partial signatures. */ | ||
if (!secp256k1_frost_compute_pubshare(ctx, &signer[j].pubshare, THRESHOLD, signer[j].id, vss_commitments, N_SIGNERS)) { | ||
return 0; | ||
} | ||
} | ||
} | ||
|
||
return 1; | ||
} | ||
|
||
/* Tweak the pubkey corresponding to the provided tweak cache, update the cache | ||
* and return the tweaked aggregate pk. */ | ||
int tweak(const secp256k1_context* ctx, secp256k1_xonly_pubkey *pk, secp256k1_frost_tweak_cache *cache) { | ||
secp256k1_pubkey output_pk; | ||
unsigned char ordinary_tweak[32] = "this could be a BIP32 tweak...."; | ||
unsigned char xonly_tweak[32] = "this could be a taproot tweak.."; | ||
|
||
if (!secp256k1_frost_pubkey_tweak(ctx, cache, pk)) { | ||
return 0; | ||
} | ||
|
||
/* Ordinary tweaking which, for example, allows deriving multiple child | ||
* public keys from a single aggregate key using BIP32 */ | ||
if (!secp256k1_frost_pubkey_ec_tweak_add(ctx, NULL, cache, ordinary_tweak)) { | ||
return 0; | ||
} | ||
/* If one is not interested in signing, the same output_pk can be obtained | ||
* by calling `secp256k1_frost_pubkey_get` right after key aggregation to | ||
* get the full pubkey and then call `secp256k1_ec_pubkey_tweak_add`. */ | ||
|
||
/* Xonly tweaking which, for example, allows creating taproot commitments */ | ||
if (!secp256k1_frost_pubkey_xonly_tweak_add(ctx, &output_pk, cache, xonly_tweak)) { | ||
return 0; | ||
} | ||
/* Note that if we wouldn't care about signing, we can arrive at the same | ||
* output_pk by providing the untweaked public key to | ||
* `secp256k1_xonly_pubkey_tweak_add` (after converting it to an xonly pubkey | ||
* if necessary with `secp256k1_xonly_pubkey_from_pubkey`). */ | ||
|
||
/* Now we convert the output_pk to an xonly pubkey to allow to later verify | ||
* the Schnorr signature against it. For this purpose we can ignore the | ||
* `pk_parity` output argument; we would need it if we would have to open | ||
* the taproot commitment. */ | ||
if (!secp256k1_xonly_pubkey_from_pubkey(ctx, pk, NULL, &output_pk)) { | ||
return 0; | ||
} | ||
return 1; | ||
} | ||
|
||
/* Sign a message hash with the given threshold and aggregate shares and store | ||
* the result in sig */ | ||
int sign(const secp256k1_context* ctx, struct signer_secrets *signer_secrets, struct signer *signer, const unsigned char* msg32, secp256k1_xonly_pubkey *pk, unsigned char *sig64, const secp256k1_frost_tweak_cache *cache) { | ||
int i; | ||
int signer_id = 0; | ||
int signers[THRESHOLD]; | ||
int is_signer[N_SIGNERS]; | ||
const secp256k1_frost_pubnonce *pubnonces[THRESHOLD]; | ||
const unsigned char *ids[THRESHOLD]; | ||
const secp256k1_frost_partial_sig *partial_sigs[THRESHOLD]; | ||
|
||
for (i = 0; i < N_SIGNERS; i++) { | ||
unsigned char session_id[32]; | ||
/* Create random session ID. It is absolutely necessary that the session ID | ||
* is unique for every call of secp256k1_frost_nonce_gen. Otherwise | ||
* it's trivial for an attacker to extract the secret key! */ | ||
if (!fill_random(session_id, sizeof(session_id))) { | ||
return 0; | ||
} | ||
/* Initialize session and create secret nonce for signing and public | ||
* nonce to send to the other signers. */ | ||
if (!secp256k1_frost_nonce_gen(ctx, &signer_secrets[i].secnonce, &signer[i].pubnonce, session_id, &signer_secrets[i].agg_share, msg32, pk, NULL)) { | ||
return 0; | ||
} | ||
is_signer[i] = 0; /* Initialize is_signer */ | ||
} | ||
/* Select a random subset of signers */ | ||
for (i = 0; i < THRESHOLD; i++) { | ||
unsigned int subset_seed; | ||
|
||
while (1) { | ||
if (!fill_random((unsigned char*)&subset_seed, sizeof(subset_seed))) { | ||
return 0; | ||
} | ||
signer_id = subset_seed % N_SIGNERS; | ||
/* Check if signer has already been assigned */ | ||
if (!is_signer[signer_id]) { | ||
is_signer[signer_id] = 1; | ||
signers[i] = signer_id; | ||
break; | ||
} | ||
} | ||
/* Mark signer as assigned */ | ||
pubnonces[i] = &signer[signer_id].pubnonce; | ||
/* pubkeys[i] = &signer[signer_id].pubkey; */ | ||
ids[i] = signer[signer_id].id; | ||
} | ||
/* Signing communication round 1: Exchange nonces */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If there is only signing round in this variant, the reference to the 1st can be dropped, unless there are multiples other rounds. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first signing communication round is the nonce exchange, and the second round is the partial signature exchange below. There's an inconsistency in the labeling that I will fix: |
||
for (i = 0; i < THRESHOLD; i++) { | ||
signer_id = signers[i]; | ||
if (!secp256k1_frost_nonce_process(ctx, &signer[signer_id].session, pubnonces, THRESHOLD, msg32, pk, signer[signer_id].id, ids, cache, NULL)) { | ||
return 0; | ||
} | ||
/* partial_sign will clear the secnonce by setting it to 0. That's because | ||
* you must _never_ reuse the secnonce (or use the same session_id to | ||
* create a secnonce). If you do, you effectively reuse the nonce and | ||
* leak the secret key. */ | ||
if (!secp256k1_frost_partial_sign(ctx, &signer[signer_id].partial_sig, &signer_secrets[signer_id].secnonce, &signer_secrets[signer_id].agg_share, &signer[signer_id].session, cache)) { | ||
return 0; | ||
} | ||
partial_sigs[i] = &signer[signer_id].partial_sig; | ||
} | ||
/* Communication round 2: A production system would exchange | ||
* partial signatures here before moving on. */ | ||
for (i = 0; i < THRESHOLD; i++) { | ||
signer_id = signers[i]; | ||
/* To check whether signing was successful, it suffices to either verify | ||
* the aggregate signature with the aggregate public key using | ||
* secp256k1_schnorrsig_verify, or verify all partial signatures of all | ||
* signers individually. Verifying the aggregate signature is cheaper but | ||
* verifying the individual partial signatures has the advantage that it | ||
* can be used to determine which of the partial signatures are invalid | ||
* (if any), i.e., which of the partial signatures cause the aggregate | ||
* signature to be invalid and thus the protocol run to fail. It's also | ||
* fine to first verify the aggregate sig, and only verify the individual | ||
* sigs if it does not work. | ||
*/ | ||
if (!secp256k1_frost_partial_sig_verify(ctx, &signer[signer_id].partial_sig, &signer[signer_id].pubnonce, &signer[signer_id].pubshare, &signer[signer_id].session, cache)) { | ||
return 0; | ||
} | ||
} | ||
return secp256k1_frost_partial_sig_agg(ctx, sig64, &signer[signer_id].session, partial_sigs, THRESHOLD); | ||
} | ||
|
||
int main(void) { | ||
secp256k1_context* ctx; | ||
int i; | ||
struct signer_secrets signer_secrets[N_SIGNERS]; | ||
struct signer signers[N_SIGNERS]; | ||
secp256k1_xonly_pubkey pk; | ||
secp256k1_frost_tweak_cache cache; | ||
unsigned char msg[32] = "this_could_be_the_hash_of_a_msg!"; | ||
unsigned char sig[64]; | ||
|
||
/* Create a context for signing and verification */ | ||
ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); | ||
printf("Creating key pairs......"); | ||
for (i = 0; i < N_SIGNERS; i++) { | ||
if (!create_keypair_and_seed(ctx, &signer_secrets[i], &signers[i])) { | ||
printf("FAILED\n"); | ||
return 1; | ||
} | ||
} | ||
printf("ok\n"); | ||
printf("Creating shares........."); | ||
if (!create_shares(ctx, signer_secrets, signers, &pk)) { | ||
printf("FAILED\n"); | ||
return 1; | ||
} | ||
printf("ok\n"); | ||
printf("Tweaking................"); | ||
/* Optionally tweak the aggregate key */ | ||
if (!tweak(ctx, &pk, &cache)) { | ||
printf("FAILED\n"); | ||
return 1; | ||
} | ||
printf("ok\n"); | ||
printf("Signing message........."); | ||
if (!sign(ctx, signer_secrets, signers, msg, &pk, sig, &cache)) { | ||
printf("FAILED\n"); | ||
return 1; | ||
} | ||
printf("ok\n"); | ||
printf("Verifying signature....."); | ||
if (!secp256k1_schnorrsig_verify(ctx, sig, msg, 32, &pk)) { | ||
printf("FAILED\n"); | ||
return 1; | ||
} | ||
printf("ok\n"); | ||
secp256k1_context_destroy(ctx); | ||
return 0; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked why the valgrind CI jobs fail and it looks like
signer_secrets[i].seed
is never initialized.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good catch. Thanks for tracking that down!