|
| 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