From 0b3dfb7039601d8269b73f3184d166bfeade5fbc Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Wed, 30 Jul 2025 14:30:25 +0200 Subject: [PATCH 1/2] Add ECDSA pubkey recovery usage example Co-authored-by: josibake --- .gitignore | 1 + CHANGELOG.md | 3 + Makefile.am | 11 +++ README.md | 1 + examples/CMakeLists.txt | 4 + examples/recovery.c | 158 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 178 insertions(+) create mode 100644 examples/recovery.c diff --git a/.gitignore b/.gitignore index ce33a84adf..67a0d6d300 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ exhaustive_tests precompute_ecmult_gen precompute_ecmult ctime_tests +recovery_example ecdh_example ecdsa_example schnorr_example diff --git a/CHANGELOG.md b/CHANGELOG.md index 53c787b38f..2be81fdd5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +#### Added + - Added usage example for ECDSA public key recovery. + ## [0.7.0] - 2025-07-21 #### Added diff --git a/Makefile.am b/Makefile.am index d511853b05..b0a30af69a 100644 --- a/Makefile.am +++ b/Makefile.am @@ -163,6 +163,17 @@ if BUILD_WINDOWS ecdsa_example_LDFLAGS += -lbcrypt endif TESTS += ecdsa_example +if ENABLE_MODULE_RECOVERY +noinst_PROGRAMS += recovery_example +recovery_example_SOURCES = examples/recovery.c +recovery_example_CPPFLAGS = -I$(top_srcdir)/include -DSECP256K1_STATIC +recovery_example_LDADD = libsecp256k1.la +recovery_example_LDFLAGS = -static +if BUILD_WINDOWS +recovery_example_LDFLAGS += -lbcrypt +endif +TESTS += recovery_example +endif if ENABLE_MODULE_ECDH noinst_PROGRAMS += ecdh_example ecdh_example_SOURCES = examples/ecdh.c diff --git a/README.md b/README.md index 3d3118adf9..cda1fbc230 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ Usage examples Usage examples can be found in the [examples](examples) directory. To compile them you need to configure with `--enable-examples`. * [ECDSA example](examples/ecdsa.c) * [Schnorr signatures example](examples/schnorr.c) + * [ECDSA public key recovery example](examples/recovery.c) * [Deriving a shared secret (ECDH) example](examples/ecdh.c) * [ElligatorSwift key exchange example](examples/ellswift.c) * [MuSig2 Schnorr multi-signatures example](examples/musig.c) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index c9da9de6be..352989f724 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -14,6 +14,10 @@ endfunction() add_example(ecdsa) +if(SECP256K1_ENABLE_MODULE_RECOVERY) + add_example(recovery) +endif() + if(SECP256K1_ENABLE_MODULE_ECDH) add_example(ecdh) endif() diff --git a/examples/recovery.c b/examples/recovery.c new file mode 100644 index 0000000000..529cfdbb1d --- /dev/null +++ b/examples/recovery.c @@ -0,0 +1,158 @@ +/************************************************************************* + * 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 * + *************************************************************************/ + +/** This file demonstrates how to use the recovery module to create a + * recoverable ECDSA signature and extract the corresponding + * public key from it. + */ + +#include +#include +#include +#include + +#include +#include + +#include "examples_util.h" + +/* Use my_memcmp_var instead of memcmp. + * + * Normally, memcmp should be fine, but we use my_memcmp_var + * here to avoid a false positive from valgrind on macOS. + * TODO: remove this in the event the bug is fixed with valgrind in the future. + */ +static int my_memcmp_var(const void *s1, const void *s2, size_t n) { + const unsigned char *p1 = s1, *p2 = s2; + size_t i; + + for (i = 0; i < n; i++) { + int diff = p1[i] - p2[i]; + if (diff != 0) { + return diff; + } + } + return 0; +} + +int main(void) { + unsigned char msg[32] = "this_could_be_the_hash_of_a_msg"; + unsigned char seckey[32]; + unsigned char randomize[32]; + unsigned char recoverable_sig_ser[64]; + unsigned char serialized_pubkey[33]; + unsigned char serialized_recovered_pubkey[33]; + size_t len; + int return_val, recovery_id; + secp256k1_pubkey pubkey, recovered_pubkey; + secp256k1_ecdsa_recoverable_signature recoverable_sig; + secp256k1_ecdsa_signature normal_sig; + + /* Before we can call actual API functions, we need to create a "context". */ + secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + if (!fill_random(randomize, sizeof(randomize))) { + printf("Failed to generate randomness\n"); + return EXIT_FAILURE; + } + /* Randomizing the context is recommended to protect against side-channel + * leakage. See `secp256k1_context_randomize` in secp256k1.h for more + * information about it. This should never fail. */ + return_val = secp256k1_context_randomize(ctx, randomize); + assert(return_val); + + /*** Key Generation ***/ + if (!fill_random(seckey, sizeof(seckey))) { + printf("Failed to generate randomness\n"); + return EXIT_FAILURE; + } + /* Try to create a public key with a valid context. This only fails if the + * secret key is zero or out of range (greater than secp256k1's order). Note + * that the probability of this occurring is negligible with a properly + * functioning random number generator. */ + if (!secp256k1_ec_pubkey_create(ctx, &pubkey, seckey)) { + printf("Generated secret key is invalid. This indicates an issue with the random number generator.\n"); + return EXIT_FAILURE; + } + + /* Serialize the public key. Should always return 1 for a valid public key. */ + len = sizeof(serialized_pubkey); + return_val = secp256k1_ec_pubkey_serialize(ctx, serialized_pubkey, &len, &pubkey, SECP256K1_EC_COMPRESSED); + assert(return_val); + + /*** Signing ***/ + + /* Signing with a valid context, verified secret key + * and the default nonce function should never fail. */ + return_val = secp256k1_ecdsa_sign_recoverable(ctx, &recoverable_sig, msg, seckey, NULL, NULL); + assert(return_val); + + /* Serialize in compact format (64 bytes + recovery id integer) */ + return_val = secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, + recoverable_sig_ser, &recovery_id, &recoverable_sig); + assert(return_val); + + /*** Public key recovery / verification ***/ + + /* Deserialize the recoverable signature. This will return 0 if the signature can't be parsed correctly. */ + if (!secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, &recoverable_sig, recoverable_sig_ser, recovery_id)) { + printf("Failed parsing the recoverable signature\n"); + return EXIT_FAILURE; + } + + /* Recover the public key */ + if (!secp256k1_ecdsa_recover(ctx, &recovered_pubkey, &recoverable_sig, msg)) { + printf("Public key recovery failed\n"); + return EXIT_FAILURE; + } + len = sizeof(serialized_recovered_pubkey); + return_val = secp256k1_ec_pubkey_serialize(ctx, serialized_recovered_pubkey, + &len, &recovered_pubkey, SECP256K1_EC_COMPRESSED); + assert(return_val); + + /* Successful recovery guarantees a correct signature, but we also do an explicit verification + do demonstrate how to convert a recoverable to a normal ECDSA signature */ + return_val = secp256k1_ecdsa_recoverable_signature_convert(ctx, &normal_sig, &recoverable_sig); + assert(return_val); + /* A converted recoverable signature doesn't necessarily follow the low-s rule that is required + * to pass `secp256k1_ecdsa_verify`, so we have to normalize it first (note that in this specific + * example that's a no-op, as `secp256k1_ecdsa_sign_recoverable` always creates low-s signatures, + * but in general the verifier is a different entity and can't rely on that) */ + secp256k1_ecdsa_signature_normalize(ctx, &normal_sig, &normal_sig); + if (!secp256k1_ecdsa_verify(ctx, &normal_sig, msg, &recovered_pubkey)) { + printf("Signature verification with converted recoverable signature failed\n"); + return EXIT_FAILURE; + } + + /* Actual public key and recovered public key should match */ + return_val = my_memcmp_var(serialized_pubkey, serialized_recovered_pubkey, sizeof(serialized_pubkey)); + assert(return_val == 0); + + printf(" Secret Key: "); + print_hex(seckey, sizeof(seckey)); + printf(" Public Key: "); + print_hex(serialized_pubkey, sizeof(serialized_pubkey)); + printf(" Rec. signature: "); + print_hex(recoverable_sig_ser, sizeof(recoverable_sig_ser)); + printf(" Recovery id: %d\n", recovery_id); + printf("Rec. public key: "); + print_hex(serialized_recovered_pubkey, sizeof(serialized_recovered_pubkey)); + + /* This will clear everything from the context and free the memory */ + secp256k1_context_destroy(ctx); + + /* It's best practice to try to clear secrets from memory after using them. + * This is done because some bugs can allow an attacker to leak memory, for + * example through "out of bounds" array access (see Heartbleed), or the OS + * swapping them to disk. Hence, we overwrite the secret key buffer with zeros. + * + * Here we are preventing these writes from being optimized out, as any good compiler + * will remove any writes that aren't used. */ + secure_erase(seckey, sizeof(seckey)); + + return EXIT_SUCCESS; +} From de5b2230fcaa267a61ec04a3cfb4fb1b793c8766 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Wed, 30 Jul 2025 14:34:00 +0200 Subject: [PATCH 2/2] examples: add missing dots in paragraphs about context randomization --- examples/ecdh.c | 2 +- examples/ecdsa.c | 2 +- examples/schnorr.c | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/ecdh.c b/examples/ecdh.c index 67b8c2047a..bb3b6d5b30 100644 --- a/examples/ecdh.c +++ b/examples/ecdh.c @@ -37,7 +37,7 @@ int main(void) { return EXIT_FAILURE; } /* Randomizing the context is recommended to protect against side-channel - * leakage See `secp256k1_context_randomize` in secp256k1.h for more + * leakage. See `secp256k1_context_randomize` in secp256k1.h for more * information about it. This should never fail. */ return_val = secp256k1_context_randomize(ctx, randomize); assert(return_val); diff --git a/examples/ecdsa.c b/examples/ecdsa.c index ae16c180dc..3f127d4be3 100644 --- a/examples/ecdsa.c +++ b/examples/ecdsa.c @@ -44,7 +44,7 @@ int main(void) { return EXIT_FAILURE; } /* Randomizing the context is recommended to protect against side-channel - * leakage See `secp256k1_context_randomize` in secp256k1.h for more + * leakage. See `secp256k1_context_randomize` in secp256k1.h for more * information about it. This should never fail. */ return_val = secp256k1_context_randomize(ctx, randomize); assert(return_val); diff --git a/examples/schnorr.c b/examples/schnorr.c index 49baed24be..88455a26a7 100644 --- a/examples/schnorr.c +++ b/examples/schnorr.c @@ -38,7 +38,7 @@ int main(void) { return EXIT_FAILURE; } /* Randomizing the context is recommended to protect against side-channel - * leakage See `secp256k1_context_randomize` in secp256k1.h for more + * leakage. See `secp256k1_context_randomize` in secp256k1.h for more * information about it. This should never fail. */ return_val = secp256k1_context_randomize(ctx, randomize); assert(return_val);