Skip to content

Commit a7fbe9b

Browse files
committed
fullagg: Add example
1 parent 269ea1d commit a7fbe9b

File tree

3 files changed

+314
-0
lines changed

3 files changed

+314
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ecdsa_example
1212
schnorr_example
1313
ellswift_example
1414
musig_example
15+
fullagg_example
1516
*.exe
1617
*.so
1718
*.a

examples/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@ endif()
2929
if(SECP256K1_ENABLE_MODULE_MUSIG)
3030
add_example(musig)
3131
endif()
32+
33+
if(SECP256K1_ENABLE_MODULE_SCHNORRSIG_FULLAGG)
34+
add_example(fullagg)
35+
endif()

examples/fullagg.c

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/*************************************************************************
2+
* Written in 2025 by Fabian Jahr *
3+
* To the extent possible under law, the author(s) have dedicated all *
4+
* copyright and related and neighboring rights to the software in this *
5+
* file to the public domain worldwide. This software is distributed *
6+
* without any warranty. For the CC0 Public Domain Dedication, see *
7+
* EXAMPLES_COPYING or https://creativecommons.org/publicdomain/zero/1.0 *
8+
*************************************************************************/
9+
10+
/** This file demonstrates how to use the FullAgg module to create an
11+
* aggregate signature where each signer signs a different message.
12+
*
13+
* FullAgg (DahLIAS) allows multiple signers to create a single aggregate
14+
* signature that proves each signer signed their respective message.
15+
*/
16+
17+
#include <stdio.h>
18+
#include <stdlib.h>
19+
#include <assert.h>
20+
#include <string.h>
21+
22+
#include <secp256k1.h>
23+
#include <secp256k1_extrakeys.h>
24+
#include <secp256k1_schnorrsig_fullagg.h>
25+
26+
#include "examples_util.h"
27+
28+
#define N_SIGNERS 3
29+
30+
struct signer_secrets {
31+
secp256k1_keypair keypair;
32+
secp256k1_fullagg_secnonce secnonce;
33+
};
34+
35+
struct signer {
36+
secp256k1_pubkey pubkey;
37+
secp256k1_fullagg_pubnonce pubnonce;
38+
secp256k1_fullagg_partial_sig partial_sig;
39+
unsigned char message[32];
40+
};
41+
42+
/* Create a key pair, store it in signer_secrets->keypair and signer->pubkey */
43+
static int create_keypair(const secp256k1_context* ctx, struct signer_secrets *signer_secrets, struct signer *signer) {
44+
unsigned char seckey[32];
45+
46+
if (!fill_random(seckey, sizeof(seckey))) {
47+
printf("Failed to generate randomness\n");
48+
return 0;
49+
}
50+
/* Try to create a keypair with a valid context. This only fails if the
51+
* secret key is zero or out of range (greater than secp256k1's order). Note
52+
* that the probability of this occurring is negligible with a properly
53+
* functioning random number generator. */
54+
if (!secp256k1_keypair_create(ctx, &signer_secrets->keypair, seckey)) {
55+
return 0;
56+
}
57+
if (!secp256k1_keypair_pub(ctx, &signer->pubkey, &signer_secrets->keypair)) {
58+
return 0;
59+
}
60+
61+
secure_erase(seckey, sizeof(seckey));
62+
return 1;
63+
}
64+
65+
static void setup_messages(struct signer *signers) {
66+
memset(signers[0].message, 0, 32);
67+
memset(signers[1].message, 0, 32);
68+
memset(signers[2].message, 0, 32);
69+
memcpy(signers[0].message, "jonas", 5);
70+
memcpy(signers[1].message, "tim", 3);
71+
memcpy(signers[2].message, "yannick", 7);
72+
}
73+
74+
/* Each signer generates their nonce pair (R1_i, R2_i) */
75+
static int nonce_generation_round(const secp256k1_context* ctx,
76+
struct signer_secrets *signer_secrets,
77+
struct signer *signers) {
78+
int i;
79+
for (i = 0; i < N_SIGNERS; i++) {
80+
unsigned char seckey[32];
81+
unsigned char session_secrand[32];
82+
83+
/* Create random session ID. It is absolutely necessary that the session ID
84+
* is unique for every call of secp256k1_fullagg_nonce_gen. Otherwise
85+
* it's trivial for an attacker to extract the secret key! */
86+
if (!fill_random(session_secrand, sizeof(session_secrand))) {
87+
return 0;
88+
}
89+
if (!secp256k1_keypair_sec(ctx, seckey, &signer_secrets[i].keypair)) {
90+
return 0;
91+
}
92+
93+
/* Initialize session and create secret nonce for signing and public
94+
* nonce to send to the coordinator. Each signer provides their own
95+
* message here, which binds the nonce to their specific message. */
96+
if (!secp256k1_fullagg_nonce_gen(ctx, &signer_secrets[i].secnonce, &signers[i].pubnonce,
97+
session_secrand, seckey, &signers[i].pubkey,
98+
signers[i].message, NULL)) {
99+
return 0;
100+
}
101+
102+
secure_erase(seckey, sizeof(seckey));
103+
}
104+
return 1;
105+
}
106+
107+
/* Each signer creates their partial signature */
108+
static int partial_signing_round(const secp256k1_context* ctx,
109+
struct signer_secrets *signer_secrets,
110+
struct signer *signers,
111+
const secp256k1_fullagg_session *session,
112+
const secp256k1_pubkey **pubkeys,
113+
const unsigned char **messages,
114+
const secp256k1_fullagg_pubnonce **pubnonces) {
115+
int i;
116+
for (i = 0; i < N_SIGNERS; i++) {
117+
/* Each signer computes their partial signature:
118+
* - First computes their challenge c_i = H_sig(L, R, X_i, m_i) where L is the list of all (Xi, mi) pairs
119+
* - Then computes s_i = r1_i + b*r2_i + c_i*x_i
120+
* The partial_sign function will clear the secnonce by setting it to 0. That's because
121+
* you must _never_ reuse the secnonce. If you do, you effectively reuse the nonce and
122+
* leak the secret key. */
123+
if (!secp256k1_fullagg_partial_sign(ctx, &signers[i].partial_sig,
124+
&signer_secrets[i].secnonce,
125+
&signer_secrets[i].keypair,
126+
session, pubkeys, messages,
127+
pubnonces, i)) {
128+
return 0;
129+
}
130+
}
131+
return 1;
132+
}
133+
134+
/* Verify each partial signature individually */
135+
static int verify_partial_signatures(const secp256k1_context* ctx,
136+
struct signer *signers,
137+
const secp256k1_fullagg_session *session,
138+
const secp256k1_pubkey **pubkeys,
139+
const unsigned char **messages) {
140+
int i;
141+
/* The coordinator can optionally verify each partial signature individually
142+
* before aggregating them. This helps identify which signer(s) may have
143+
* produced invalid signatures if the aggregate signature fails to verify. */
144+
for (i = 0; i < N_SIGNERS; i++) {
145+
if (!secp256k1_fullagg_partial_sig_verify(ctx, &signers[i].partial_sig,
146+
&signers[i].pubnonce,
147+
&signers[i].pubkey,
148+
session, pubkeys, messages, i)) {
149+
printf("Partial signature %d failed to verify\n", i);
150+
return 0;
151+
}
152+
}
153+
return 1;
154+
}
155+
156+
int main(void) {
157+
secp256k1_context* ctx;
158+
int i;
159+
struct signer_secrets signer_secrets[N_SIGNERS];
160+
struct signer signers[N_SIGNERS];
161+
const secp256k1_pubkey *pubkeys[N_SIGNERS];
162+
const unsigned char *messages[N_SIGNERS];
163+
const secp256k1_fullagg_pubnonce *pubnonces[N_SIGNERS];
164+
const secp256k1_fullagg_partial_sig *partial_sigs[N_SIGNERS];
165+
secp256k1_fullagg_session session;
166+
secp256k1_fullagg_aggnonce agg_pubnonce;
167+
unsigned char sig[64];
168+
169+
ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
170+
171+
printf("\n=== Running FullAgg Example ===\n");
172+
printf("Creating a single signature for %d signers with different messages\n\n", N_SIGNERS);
173+
174+
printf("Signers: Creating key pairs... ");
175+
fflush(stdout);
176+
for (i = 0; i < N_SIGNERS; i++) {
177+
if (!create_keypair(ctx, &signer_secrets[i], &signers[i])) {
178+
printf("FAILED\n");
179+
return EXIT_FAILURE;
180+
}
181+
pubkeys[i] = &signers[i].pubkey;
182+
}
183+
printf("ok\n");
184+
185+
printf("Signers: Setting up messages... ");
186+
fflush(stdout);
187+
setup_messages(signers);
188+
for (i = 0; i < N_SIGNERS; i++) {
189+
messages[i] = signers[i].message;
190+
}
191+
printf("ok\n");
192+
193+
printf("Signers: Generating nonces... ");
194+
fflush(stdout);
195+
/* In FullAgg, we use two nonces per signer to prevent rogue-key attacks
196+
* without requiring a proof of possession. */
197+
if (!nonce_generation_round(ctx, signer_secrets, signers)) {
198+
printf("FAILED\n");
199+
return EXIT_FAILURE;
200+
}
201+
for (i = 0; i < N_SIGNERS; i++) {
202+
pubnonces[i] = &signers[i].pubnonce;
203+
}
204+
printf("ok\n");
205+
206+
printf("Coordinator: Aggregating nonces... ");
207+
fflush(stdout);
208+
/* The coordinator (can be any party) collects all public nonces and
209+
* aggregates them: R1 = sum(R1_i), R2 = sum(R2_i) */
210+
if (!secp256k1_fullagg_nonce_agg(ctx, &agg_pubnonce, pubnonces, N_SIGNERS)) {
211+
printf("FAILED\n");
212+
return EXIT_FAILURE;
213+
}
214+
printf("ok\n");
215+
216+
printf("All: Initializing session... ");
217+
fflush(stdout);
218+
/* The session computes:
219+
* - The nonce coefficient b = H_non(R1, R2, all Xi, all mi, all R2_i)
220+
* - The final nonce R = R1 + b*R2
221+
* This binds the final nonce to all signers' keys, messages, and individual nonces. */
222+
if (!secp256k1_fullagg_session_init(ctx, &session, &agg_pubnonce,
223+
pubkeys, messages, pubnonces, N_SIGNERS)) {
224+
printf("FAILED\n");
225+
return EXIT_FAILURE;
226+
}
227+
printf("ok\n");
228+
229+
printf("Signers: Creating partial signatures... ");
230+
fflush(stdout);
231+
if (!partial_signing_round(ctx, signer_secrets, signers, &session,
232+
pubkeys, messages, pubnonces)) {
233+
printf("FAILED\n");
234+
return EXIT_FAILURE;
235+
}
236+
for (i = 0; i < N_SIGNERS; i++) {
237+
partial_sigs[i] = &signers[i].partial_sig;
238+
}
239+
printf("ok\n");
240+
241+
printf("Coordinator: Verifying partial signatures... ");
242+
fflush(stdout);
243+
if (!verify_partial_signatures(ctx, signers, &session, pubkeys, messages)) {
244+
printf("FAILED\n");
245+
return EXIT_FAILURE;
246+
}
247+
printf("ok\n");
248+
249+
printf("Coordinator: Aggregating signatures... ");
250+
fflush(stdout);
251+
/* The final signature is (R, s) where s = sum(s_i) */
252+
if (!secp256k1_fullagg_partial_sig_agg(ctx, sig, &session, partial_sigs, N_SIGNERS)) {
253+
printf("FAILED\n");
254+
return EXIT_FAILURE;
255+
}
256+
printf("ok\n");
257+
258+
printf("All: Verifying aggregate signature... ");
259+
fflush(stdout);
260+
/* Verify the aggregate signature against all public keys and their messages.
261+
* The verification checks that s*G = R + sum(c_i*X_i) where each c_i is
262+
* computed from the specific message m_i that signer i signed. */
263+
if (!secp256k1_fullagg_verify(ctx, sig, pubkeys, messages, N_SIGNERS)) {
264+
printf("FAILED\n");
265+
return EXIT_FAILURE;
266+
}
267+
printf("ok\n");
268+
269+
/* Test that the signature is specific to these exact messages */
270+
printf("Testing message binding... ");
271+
fflush(stdout);
272+
273+
/* Modify one message to verify the signature is bound to specific messages */
274+
signers[1].message[0] ^= 0xFF;
275+
276+
if (secp256k1_fullagg_verify(ctx, sig, pubkeys, messages, N_SIGNERS)) {
277+
printf("FAILED (signature verified with modified message)\n");
278+
return EXIT_FAILURE;
279+
}
280+
281+
/* Restore the original message and verify it works again */
282+
signers[1].message[0] ^= 0xFF;
283+
284+
if (!secp256k1_fullagg_verify(ctx, sig, pubkeys, messages, N_SIGNERS)) {
285+
printf("FAILED (signature doesn't verify after restoring)\n");
286+
return EXIT_FAILURE;
287+
}
288+
printf("ok\n");
289+
290+
printf("\nFinal Aggregate Signature: ");
291+
for (i = 0; i < 64; i++) {
292+
printf("%02x", sig[i]);
293+
if (i == 31) printf(" ");
294+
}
295+
printf("\n");
296+
297+
/* It's best practice to try to clear secrets from memory after using them.
298+
* This is done because some bugs can allow an attacker to leak memory, for
299+
* example through "out of bounds" array access (see Heartbleed), or the OS
300+
* swapping them to disk. Hence, we overwrite secret key material with zeros.
301+
*
302+
* Here we are preventing these writes from being optimized out, as any good compiler
303+
* will remove any writes that aren't used. */
304+
for (i = 0; i < N_SIGNERS; i++) {
305+
secure_erase(&signer_secrets[i], sizeof(signer_secrets[i]));
306+
}
307+
secp256k1_context_destroy(ctx);
308+
return EXIT_SUCCESS;
309+
}

0 commit comments

Comments
 (0)