From 55c202516d895c552615565a14fcbd6ddb4a36d2 Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Wed, 29 Apr 2026 02:55:18 -0700 Subject: [PATCH] WIP: migrate ECDSA/ECDH off OpenSSL EC to libsecp256k1 Add libsecp256k1 v0.7.1 as src/secp256k1 submodule and introduce crypto_ecdsa / crypto_ecdh wrappers as drop-in replacements for the OpenSSL ECDSA_verify / ECDSA_sign / ECDH_compute_key call sites used by key.cpp and smessage.cpp. Wrappers preserve on-chain compatibility (lax DER parsing, 65-byte recoverable compact sigs, SEC1 priv-key DER round-trip, raw-X ECDH output for smsg KDF). CMake wires the submodule and new sources into the build. Mid-refactor; landing as a checkpoint before stacking sync-pipeline work on top. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitmodules | 3 + CMakeLists.txt | 24 ++ src/CMakeLists.txt | 6 + src/crypto_ecdh.cpp | 56 ++++ src/crypto_ecdh.h | 31 ++ src/crypto_ecdsa.cpp | 389 +++++++++++++++++++++++++ src/crypto_ecdsa.h | 121 ++++++++ src/key.cpp | 669 ++++++++++++++++++------------------------- src/key.h | 14 +- src/main.cpp | 17 ++ src/secp256k1 | 1 + src/smessage.cpp | 65 +++-- 12 files changed, 966 insertions(+), 430 deletions(-) create mode 100644 src/crypto_ecdh.cpp create mode 100644 src/crypto_ecdh.h create mode 100644 src/crypto_ecdsa.cpp create mode 100644 src/crypto_ecdsa.h create mode 160000 src/secp256k1 diff --git a/.gitmodules b/.gitmodules index aa97424..600bedf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "src/tor/tor-src"] path = src/tor/tor-src url = https://gitlab.torproject.org/tpo/core/tor.git +[submodule "src/secp256k1"] + path = src/secp256k1 + url = https://github.com/bitcoin-core/secp256k1 diff --git a/CMakeLists.txt b/CMakeLists.txt index c1e9e33..c33328d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -133,6 +133,30 @@ if(NOT TARGET RocksDB::rocksdb AND NOT TARGET PkgConfig::RocksDB) message(STATUS "Found RocksDB (manual probe): ${ROCKSDB_LIBRARY}") endif() +# libsecp256k1 — vendored as a git submodule under src/secp256k1. Provides +# ECDSA signing/verification, pubkey recovery (via the recovery module), and +# ECDH for secure messaging. Configure the submodule's build for our needs: +# only ECDH + recovery, none of the test/benchmark/extra-module bloat, and +# don't install (we link statically against the in-tree target). +if(NOT EXISTS "${CMAKE_SOURCE_DIR}/src/secp256k1/CMakeLists.txt") + message(FATAL_ERROR + "src/secp256k1 is empty. Run: git submodule update --init --recursive") +endif() +set(SECP256K1_DISABLE_SHARED ON CACHE INTERNAL "") +set(SECP256K1_INSTALL OFF CACHE INTERNAL "") +set(SECP256K1_BUILD_BENCHMARK OFF CACHE INTERNAL "") +set(SECP256K1_BUILD_TESTS OFF CACHE INTERNAL "") +set(SECP256K1_BUILD_EXHAUSTIVE_TESTS OFF CACHE INTERNAL "") +set(SECP256K1_BUILD_CTIME_TESTS OFF CACHE INTERNAL "") +set(SECP256K1_BUILD_EXAMPLES OFF CACHE INTERNAL "") +set(SECP256K1_ENABLE_MODULE_ECDH ON CACHE INTERNAL "") +set(SECP256K1_ENABLE_MODULE_RECOVERY ON CACHE INTERNAL "") +set(SECP256K1_ENABLE_MODULE_EXTRAKEYS OFF CACHE INTERNAL "") +set(SECP256K1_ENABLE_MODULE_SCHNORRSIG OFF CACHE INTERNAL "") +set(SECP256K1_ENABLE_MODULE_MUSIG OFF CACHE INTERNAL "") +set(SECP256K1_ENABLE_MODULE_ELLSWIFT OFF CACHE INTERNAL "") +add_subdirectory(src/secp256k1 EXCLUDE_FROM_ALL) + if(BUILD_QT) find_package(Qt5 5.9 REQUIRED COMPONENTS Core Gui Widgets) find_package(Qt5 COMPONENTS LinguistTools QUIET) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b0e6c85..cb5738f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -42,6 +42,8 @@ set(CORE_SOURCES bootstrap.cpp checkpoints.cpp crypter.cpp + crypto_ecdh.cpp + crypto_ecdsa.cpp db.cpp key.cpp keystore.cpp @@ -147,6 +149,10 @@ if(USE_ZMQ) target_link_libraries(triangles_common PUBLIC PkgConfig::ZMQ) endif() +# libsecp256k1 (mandatory) — ECDH / ECDSA replacement for OpenSSL EC. +# Provided by add_subdirectory(src/secp256k1) in the top-level CMakeLists. +target_link_libraries(triangles_common PUBLIC secp256k1) + # RocksDB (mandatory) if(TARGET RocksDB::rocksdb) target_link_libraries(triangles_common PUBLIC RocksDB::rocksdb) diff --git a/src/crypto_ecdh.cpp b/src/crypto_ecdh.cpp new file mode 100644 index 0000000..fccd6d0 --- /dev/null +++ b/src/crypto_ecdh.cpp @@ -0,0 +1,56 @@ +// Copyright (c) 2026 The Triangles developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "crypto_ecdh.h" + +#include +#include + +#include +#include + +namespace { + +// One process-wide context is sufficient for ECDH — no signing or verification +// flags needed. Created lazily on first use; libsecp256k1 contexts are +// thread-safe for read-only operations like ECDH. +secp256k1_context* GetECDHContext() +{ + static std::once_flag once; + static secp256k1_context* ctx = nullptr; + std::call_once(once, []() { + ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + }); + return ctx; +} + +// Hash function callback that returns the raw X coordinate of the shared +// point. Mirrors OpenSSL's ECDH_compute_key behaviour when the KDF is NULL. +int hash_xonly(unsigned char* output, + const unsigned char* x32, + const unsigned char* /*y32*/, + void* /*data*/) +{ + std::memcpy(output, x32, 32); + return 1; +} + +} // namespace + +bool ECDH_xonly_secp256k1(unsigned char out32[32], + const unsigned char privkey32[32], + const unsigned char* pubkey, + std::size_t pubkey_len) +{ + if (pubkey_len != 33 && pubkey_len != 65) return false; + + secp256k1_context* ctx = GetECDHContext(); + if (!ctx) return false; + + secp256k1_pubkey pk; + if (!secp256k1_ec_pubkey_parse(ctx, &pk, pubkey, pubkey_len)) + return false; + + return secp256k1_ecdh(ctx, out32, &pk, privkey32, hash_xonly, nullptr) == 1; +} diff --git a/src/crypto_ecdh.h b/src/crypto_ecdh.h new file mode 100644 index 0000000..2bcf210 --- /dev/null +++ b/src/crypto_ecdh.h @@ -0,0 +1,31 @@ +// Copyright (c) 2026 The Triangles developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +#ifndef TRIANGLES_CRYPTO_ECDH_H +#define TRIANGLES_CRYPTO_ECDH_H + +#include + +/** + * Compute the shared secret X coordinate via secp256k1 ECDH. + * + * Output matches OpenSSL's ECDH_compute_key(buf, 32, peer_pub, our_priv, NULL) + * — i.e. the raw X coordinate of the shared point, with no KDF applied. This + * preserves bit-for-bit compatibility with smessage's existing key derivation + * (which feeds the X coordinate into SHA-512 itself), so historical encrypted + * messages remain decryptable after the migration off OpenSSL EC. + * + * @param out32 32-byte buffer for the shared X coordinate. + * @param privkey32 32-byte secret scalar (big-endian). + * @param pubkey Peer public key, serialized as either 33 bytes (compressed) + * or 65 bytes (uncompressed). + * @param pubkey_len 33 or 65; any other length fails immediately. + * @return true on success, false if the public key is malformed or the + * private key is invalid (zero / >= curve order). + */ +bool ECDH_xonly_secp256k1(unsigned char out32[32], + const unsigned char privkey32[32], + const unsigned char* pubkey, + std::size_t pubkey_len); + +#endif // TRIANGLES_CRYPTO_ECDH_H diff --git a/src/crypto_ecdsa.cpp b/src/crypto_ecdsa.cpp new file mode 100644 index 0000000..faa5fda --- /dev/null +++ b/src/crypto_ecdsa.cpp @@ -0,0 +1,389 @@ +// Copyright (c) 2026 The Triangles developers +// Copyright (c) 2015 Pieter Wuille (lax DER parser, MIT licence) +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include "crypto_ecdsa.h" + +#include +#include + +#include +#include + +namespace { + +// Combined VERIFY + SIGN context. libsecp256k1 contexts are thread-safe for +// signing and verification once created. In libsecp256k1 >= 0.2 these flags +// are accepted but increasingly no-ops; passing both keeps us compatible with +// older versions still in distro packages. +secp256k1_context* GetEcdsaContext() +{ + static std::once_flag once; + static secp256k1_context* ctx = nullptr; + std::call_once(once, []() { + ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN); + }); + return ctx; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Lax DER parser, vendored from Bitcoin Core (contrib/lax_der_parsing.c). +// +// libsecp256k1's strict parser rejects DER encodings that OpenSSL has +// historically accepted: non-minimal length bytes, extra leading zeros on R/S, +// negative integers, etc. Many such signatures already exist on chain. This +// parser tolerates them, normalises (R, S) into a 64-byte compact buffer, and +// hands that to libsecp256k1's compact-signature parser. Anything that still +// fails to fit (e.g. R or S exceeding 32 bytes after stripping leading zeros) +// is treated as zero so the verify call returns a clean failure rather than +// crashing. +// ───────────────────────────────────────────────────────────────────────────── +int ecdsa_signature_parse_der_lax(const secp256k1_context* ctx, + secp256k1_ecdsa_signature* sig, + const unsigned char* input, + std::size_t inputlen) +{ + std::size_t rpos, rlen, spos, slen; + std::size_t pos = 0; + std::size_t lenbyte; + unsigned char tmpsig[64] = {0}; + int overflow = 0; + + // Initialise sig with a parseable but invalid signature so the caller + // always gets a defined value back even on early-exit paths. + secp256k1_ecdsa_signature_parse_compact(ctx, sig, tmpsig); + + // SEQUENCE tag. + if (pos == inputlen || input[pos] != 0x30) return 0; + pos++; + + // SEQUENCE length (skipped — we trust the inner element lengths). + if (pos == inputlen) return 0; + lenbyte = input[pos++]; + if (lenbyte & 0x80) { + lenbyte -= 0x80; + if (lenbyte > inputlen - pos) return 0; + pos += lenbyte; + } + + // R: INTEGER tag. + if (pos == inputlen || input[pos] != 0x02) return 0; + pos++; + + // R: length. + if (pos == inputlen) return 0; + lenbyte = input[pos++]; + if (lenbyte & 0x80) { + lenbyte -= 0x80; + if (lenbyte > inputlen - pos) return 0; + while (lenbyte > 0 && input[pos] == 0) { pos++; lenbyte--; } + if (lenbyte >= sizeof(std::size_t)) return 0; + rlen = 0; + while (lenbyte > 0) { rlen = (rlen << 8) + input[pos]; pos++; lenbyte--; } + } else { + rlen = lenbyte; + } + if (rlen > inputlen - pos) return 0; + rpos = pos; + pos += rlen; + + // S: INTEGER tag. + if (pos == inputlen || input[pos] != 0x02) return 0; + pos++; + + // S: length. + if (pos == inputlen) return 0; + lenbyte = input[pos++]; + if (lenbyte & 0x80) { + lenbyte -= 0x80; + if (lenbyte > inputlen - pos) return 0; + while (lenbyte > 0 && input[pos] == 0) { pos++; lenbyte--; } + if (lenbyte >= sizeof(std::size_t)) return 0; + slen = 0; + while (lenbyte > 0) { slen = (slen << 8) + input[pos]; pos++; lenbyte--; } + } else { + slen = lenbyte; + } + if (slen > inputlen - pos) return 0; + spos = pos; + + // Strip leading zeros from R and place right-aligned in tmpsig[0..32). + while (rlen > 0 && input[rpos] == 0) { rlen--; rpos++; } + if (rlen > 32) { + overflow = 1; + } else { + std::memcpy(tmpsig + 32 - rlen, input + rpos, rlen); + } + + // Strip leading zeros from S and place right-aligned in tmpsig[32..64). + while (slen > 0 && input[spos] == 0) { slen--; spos++; } + if (slen > 32) { + overflow = 1; + } else { + std::memcpy(tmpsig + 64 - slen, input + spos, slen); + } + + if (!overflow) { + overflow = !secp256k1_ecdsa_signature_parse_compact(ctx, sig, tmpsig); + } + if (overflow) { + std::memset(tmpsig, 0, 64); + secp256k1_ecdsa_signature_parse_compact(ctx, sig, tmpsig); + } + return 1; +} + +} // namespace + +bool ECDSA_verify_secp256k1(const unsigned char hash32[32], + const unsigned char* sig, std::size_t sig_len, + const unsigned char* pubkey, std::size_t pubkey_len) +{ + if (sig_len == 0) return false; + if (pubkey_len != 33 && pubkey_len != 65) return false; + + secp256k1_context* ctx = GetEcdsaContext(); + if (!ctx) return false; + + secp256k1_pubkey pk; + if (!secp256k1_ec_pubkey_parse(ctx, &pk, pubkey, pubkey_len)) + return false; + + secp256k1_ecdsa_signature parsed_sig; + if (!ecdsa_signature_parse_der_lax(ctx, &parsed_sig, sig, sig_len)) + return false; + + return secp256k1_ecdsa_verify(ctx, &parsed_sig, hash32, &pk) == 1; +} + +bool ECDSA_sign_secp256k1(unsigned char* out, std::size_t* out_len, + const unsigned char hash32[32], + const unsigned char privkey32[32]) +{ + if (!out || !out_len) return false; + secp256k1_context* ctx = GetEcdsaContext(); + if (!ctx) return false; + + secp256k1_ecdsa_signature sig; + if (!secp256k1_ecdsa_sign(ctx, &sig, hash32, privkey32, nullptr, nullptr)) + return false; + + return secp256k1_ecdsa_signature_serialize_der(ctx, out, out_len, &sig) == 1; +} + +bool ECDSA_sign_compact_secp256k1(unsigned char out65[65], + const unsigned char hash32[32], + const unsigned char privkey32[32], + bool fCompressed) +{ + secp256k1_context* ctx = GetEcdsaContext(); + if (!ctx) return false; + + secp256k1_ecdsa_recoverable_signature recsig; + if (!secp256k1_ecdsa_sign_recoverable(ctx, &recsig, hash32, privkey32, nullptr, nullptr)) + return false; + + int recid = -1; + if (!secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, &out65[1], &recid, &recsig)) + return false; + if (recid < 0 || recid > 3) return false; + + out65[0] = static_cast(27 + recid + (fCompressed ? 4 : 0)); + return true; +} + +bool ECDSA_recover_compact_secp256k1(unsigned char* pubkey_out, + std::size_t* pubkey_len_out, + const unsigned char hash32[32], + const unsigned char sig65[65]) +{ + if (!pubkey_out || !pubkey_len_out) return false; + + int header = sig65[0]; + if (header < 27 || header >= 35) return false; + bool fCompressed = (header >= 31); + int recid = (header - 27) & 0x3; + + secp256k1_context* ctx = GetEcdsaContext(); + if (!ctx) return false; + + secp256k1_ecdsa_recoverable_signature recsig; + if (!secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, &recsig, &sig65[1], recid)) + return false; + + secp256k1_pubkey pk; + if (!secp256k1_ecdsa_recover(ctx, &pk, &recsig, hash32)) + return false; + + std::size_t out_len = fCompressed ? 33 : 65; + if (!secp256k1_ec_pubkey_serialize(ctx, pubkey_out, &out_len, &pk, + fCompressed ? SECP256K1_EC_COMPRESSED + : SECP256K1_EC_UNCOMPRESSED)) + return false; + + *pubkey_len_out = out_len; + return true; +} + +bool ECDSA_seckey_verify_secp256k1(const unsigned char privkey32[32]) +{ + secp256k1_context* ctx = GetEcdsaContext(); + if (!ctx) return false; + return secp256k1_ec_seckey_verify(ctx, privkey32) == 1; +} + +bool ECDSA_pubkey_verify_secp256k1(const unsigned char* pubkey, std::size_t pubkey_len) +{ + if (pubkey_len != 33 && pubkey_len != 65) return false; + secp256k1_context* ctx = GetEcdsaContext(); + if (!ctx) return false; + secp256k1_pubkey pk; + return secp256k1_ec_pubkey_parse(ctx, &pk, pubkey, pubkey_len) == 1; +} + +bool ECDSA_pubkey_from_privkey_secp256k1(unsigned char* out, std::size_t* out_len_out, + const unsigned char privkey32[32], + bool fCompressed) +{ + if (!out || !out_len_out) return false; + secp256k1_context* ctx = GetEcdsaContext(); + if (!ctx) return false; + + secp256k1_pubkey pk; + if (!secp256k1_ec_pubkey_create(ctx, &pk, privkey32)) + return false; + + std::size_t len = fCompressed ? 33 : 65; + if (!secp256k1_ec_pubkey_serialize(ctx, out, &len, &pk, + fCompressed ? SECP256K1_EC_COMPRESSED + : SECP256K1_EC_UNCOMPRESSED)) + return false; + *out_len_out = len; + return true; +} + +// ───────────────────────────────────────────────────────────────────────────── +// SEC1 / RFC-5915 DER codec for secp256k1 ECPrivateKey +// +// Vendored from Bitcoin Core (src/key.cpp), MIT-licensed. The decoder is lax +// about details (matches OpenSSL's d2i_ECPrivateKey lenience); the encoder +// writes the exact byte layout that OpenSSL's i2d_ECPrivateKey produces for +// this curve so wallet.dat records remain interchangeable across versions. +// +// Compressed pubkey: 214 bytes +// Uncompressed pubkey: 279 bytes +// +// The static templates below carry every byte except the 32-byte private +// scalar and the public key bytes, which are spliced into the precomputed +// offsets at encode time. +// ───────────────────────────────────────────────────────────────────────────── + +namespace { + +const unsigned char der_template_compressed[214] = { + 0x30,0x81,0xD3,0x02,0x01,0x01,0x04,0x20, + /* private key (32 bytes) at offset 8 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0xA0,0x81,0x85,0x30,0x81,0x82,0x02,0x01,0x01,0x30,0x2C,0x06,0x07,0x2A,0x86,0x48, + 0xCE,0x3D,0x01,0x01,0x02,0x21,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,0xFF,0xFF,0xFC,0x2F,0x30,0x06,0x04,0x01,0x00, + 0x04,0x01,0x07,0x04,0x21,0x02,0x79,0xBE,0x66,0x7E,0xF9,0xDC,0xBB,0xAC,0x55,0xA0, + 0x62,0x95,0xCE,0x87,0x0B,0x07,0x02,0x9B,0xFC,0xDB,0x2D,0xCE,0x28,0xD9,0x59,0xF2, + 0x81,0x5B,0x16,0xF8,0x17,0x98,0x02,0x21,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,0xBA,0xAE,0xDC,0xE6,0xAF,0x48,0xA0, + 0x3B,0xBF,0xD2,0x5E,0x8C,0xD0,0x36,0x41,0x41,0x02,0x01,0x01,0xA1,0x24,0x03,0x22, + 0x00, + /* compressed pubkey (33 bytes) at offset 181 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 +}; + +const unsigned char der_template_uncompressed[279] = { + 0x30,0x82,0x01,0x13,0x02,0x01,0x01,0x04,0x20, + /* private key (32 bytes) at offset 9 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0xA0,0x81,0xA5,0x30,0x81,0xA2,0x02,0x01,0x01,0x30,0x2C,0x06,0x07,0x2A,0x86,0x48, + 0xCE,0x3D,0x01,0x01,0x02,0x21,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,0xFF,0xFF,0xFC,0x2F,0x30,0x06,0x04,0x01,0x00, + 0x04,0x01,0x07,0x04,0x41,0x04,0x79,0xBE,0x66,0x7E,0xF9,0xDC,0xBB,0xAC,0x55,0xA0, + 0x62,0x95,0xCE,0x87,0x0B,0x07,0x02,0x9B,0xFC,0xDB,0x2D,0xCE,0x28,0xD9,0x59,0xF2, + 0x81,0x5B,0x16,0xF8,0x17,0x98,0x48,0x3A,0xDA,0x77,0x26,0xA3,0xC4,0x65,0x5D,0xA4, + 0xFB,0xFC,0x0E,0x11,0x08,0xA8,0xFD,0x17,0xB4,0x48,0xA6,0x85,0x54,0x19,0x9C,0x47, + 0xD0,0x8F,0xFB,0x10,0xD4,0xB8,0x02,0x21,0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, + 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFE,0xBA,0xAE,0xDC,0xE6,0xAF,0x48,0xA0, + 0x3B,0xBF,0xD2,0x5E,0x8C,0xD0,0x36,0x41,0x41,0x02,0x01,0x01,0xA1,0x44,0x03,0x42, + 0x00, + /* uncompressed pubkey (65 bytes) at offset 214 */ + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0 +}; + +} // namespace + +bool ECDSA_privkey_export_der_secp256k1(unsigned char* out, std::size_t* out_len_out, + const unsigned char privkey32[32], + bool fCompressed) +{ + if (!out || !out_len_out) return false; + + secp256k1_context* ctx = GetEcdsaContext(); + if (!ctx) return false; + + secp256k1_pubkey pk; + if (!secp256k1_ec_pubkey_create(ctx, &pk, privkey32)) + return false; + + if (fCompressed) { + std::memcpy(out, der_template_compressed, sizeof(der_template_compressed)); + std::memcpy(out + 8, privkey32, 32); + std::size_t pub_len = 33; + if (!secp256k1_ec_pubkey_serialize(ctx, out + 181, &pub_len, &pk, SECP256K1_EC_COMPRESSED)) + return false; + *out_len_out = sizeof(der_template_compressed); + } else { + std::memcpy(out, der_template_uncompressed, sizeof(der_template_uncompressed)); + std::memcpy(out + 9, privkey32, 32); + std::size_t pub_len = 65; + if (!secp256k1_ec_pubkey_serialize(ctx, out + 214, &pub_len, &pk, SECP256K1_EC_UNCOMPRESSED)) + return false; + *out_len_out = sizeof(der_template_uncompressed); + } + return true; +} + +bool ECDSA_privkey_import_der_secp256k1(unsigned char privkey32_out[32], + const unsigned char* der, std::size_t der_len) +{ + // Lax SEC1/RFC-5915 ECPrivateKey parser. We only need to find the OCTET + // STRING containing the private key scalar; everything else (curve params, + // optional public key) is informational. Mirrors Bitcoin Core's + // ec_privkey_import_der. + const unsigned char* end = der + der_len; + if (end < der + 1 || *(der++) != 0x30) return false; + + // Outer SEQUENCE length — variable length encoding. + if (der >= end) return false; + int lenb = *(der++); + if (lenb < 0x80) { + // short form, ignore + } else { + int n = lenb & 0x7F; + if (n == 0 || n > 2) return false; + if (der + n > end) return false; + der += n; + } + + // Version INTEGER (1). + if (der + 3 > end || der[0] != 0x02 || der[1] != 0x01 || der[2] != 0x01) return false; + der += 3; + + // privateKey OCTET STRING (length 32). + if (der + 2 > end || der[0] != 0x04 || der[1] != 0x20) return false; + der += 2; + if (der + 32 > end) return false; + std::memcpy(privkey32_out, der, 32); + + // Validate the result against the curve order; reject zero / >= n. + return ECDSA_seckey_verify_secp256k1(privkey32_out); +} diff --git a/src/crypto_ecdsa.h b/src/crypto_ecdsa.h new file mode 100644 index 0000000..9ffb019 --- /dev/null +++ b/src/crypto_ecdsa.h @@ -0,0 +1,121 @@ +// Copyright (c) 2026 The Triangles developers +// Distributed under the MIT/X11 software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +#ifndef TRIANGLES_CRYPTO_ECDSA_H +#define TRIANGLES_CRYPTO_ECDSA_H + +#include + +/** + * Verify a DER-encoded secp256k1 ECDSA signature using libsecp256k1. + * + * Drop-in replacement for OpenSSL's + * ECDSA_verify(0, hash, 32, sig, sig_len, pkey) + * with one important caveat baked in: the DER input is parsed *laxly* + * (Bitcoin Core's `lax_der_parsing` algorithm), so historical non-canonical + * encodings already on chain — extra padding, leading zeros, length-byte + * quirks that OpenSSL's permissive ASN.1 reader once accepted — continue + * to verify. Strict-DER-only parsing here would silently fork the chain. + * + * High-S signatures are accepted (libsecp256k1's verify behaviour by default). + * No malleability check is applied; that is policy and lives elsewhere. + * + * @param hash32 32-byte message hash to verify against. + * @param sig DER-encoded signature bytes. + * @param sig_len Length of `sig`. + * @param pubkey Serialized public key (33 bytes compressed or 65 uncompressed). + * @param pubkey_len 33 or 65; any other length fails immediately. + * @return true iff the signature is valid for (hash32, pubkey). + */ +bool ECDSA_verify_secp256k1(const unsigned char hash32[32], + const unsigned char* sig, std::size_t sig_len, + const unsigned char* pubkey, std::size_t pubkey_len); + +/** + * Sign `hash32` with `privkey32` and write a DER-encoded signature to `out`. + * + * libsecp256k1 uses RFC 6979 deterministic nonces, so signature bytes will + * differ from OpenSSL's random-nonce output for the same key+hash, but any + * resulting signature is equally valid. Low-S is enforced automatically. + * + * @param out Output buffer; must be at least `*out_len` bytes. + * libsecp256k1 produces at most 72 bytes of DER. + * @param out_len In: capacity of `out`. Out: bytes actually written. + * @param hash32 32-byte message hash to sign. + * @param privkey32 32-byte secret scalar. + * @return true on success. + */ +bool ECDSA_sign_secp256k1(unsigned char* out, std::size_t* out_len, + const unsigned char hash32[32], + const unsigned char privkey32[32]); + +/** + * Produce a 65-byte recoverable compact signature. + * + * Output layout matches the existing wire format: + * out[0] = 27 + recid + (fCompressed ? 4 : 0) + * out[1..33) = R (big-endian, 32 bytes) + * out[33..65) = S (big-endian, 32 bytes) + * + * @param out65 65-byte output buffer. + * @param hash32 32-byte message hash to sign. + * @param privkey32 32-byte secret scalar. + * @param fCompressed Whether the matching public key is compressed; affects + * the recid offset in the header byte. + * @return true on success. + */ +bool ECDSA_sign_compact_secp256k1(unsigned char out65[65], + const unsigned char hash32[32], + const unsigned char privkey32[32], + bool fCompressed); + +/** + * Recover the signing public key from a 65-byte compact signature (as produced + * by ECDSA_sign_compact_secp256k1) and a message hash. + * + * The header byte's "compressed" flag determines whether the recovered key is + * serialized as 33 bytes (compressed) or 65 bytes (uncompressed). + * + * @param pubkey_out Output buffer; needs at least 65 bytes capacity. + * @param pubkey_len_out Receives the actual serialized length (33 or 65). + * @param hash32 32-byte message hash that was signed. + * @param sig65 65-byte compact signature. + * @return true if recovery succeeded. + */ +bool ECDSA_recover_compact_secp256k1(unsigned char* pubkey_out, + std::size_t* pubkey_len_out, + const unsigned char hash32[32], + const unsigned char sig65[65]); + +/** Return true iff `privkey32` is a valid secp256k1 secret (in (0, n)). */ +bool ECDSA_seckey_verify_secp256k1(const unsigned char privkey32[32]); + +/** Return true iff `pubkey/pubkey_len` parses as a valid secp256k1 point. */ +bool ECDSA_pubkey_verify_secp256k1(const unsigned char* pubkey, std::size_t pubkey_len); + +/** + * Derive the public key for `privkey32` and serialize it. + * @param out Output buffer; must be at least 65 bytes. + * @param out_len_out Receives the actual length (33 or 65). + * @param privkey32 32-byte secret scalar. + * @param fCompressed Whether to serialize compressed (33B) or uncompressed (65B). + * @return true on success. + */ +bool ECDSA_pubkey_from_privkey_secp256k1(unsigned char* out, std::size_t* out_len_out, + const unsigned char privkey32[32], + bool fCompressed); + +/** + * SEC1/RFC-5915 DER ECPrivateKey encoder/decoder for the secp256k1 curve. + * Output bytes match the layout produced by OpenSSL's i2d_ECPrivateKey on this + * curve (compressed = 214 bytes, uncompressed = 279 bytes), so wallet.dat + * records written by previous OpenSSL-EC builds remain readable, and records + * we write remain readable by older OpenSSL-based builds. + */ +bool ECDSA_privkey_export_der_secp256k1(unsigned char* out, std::size_t* out_len_out, + const unsigned char privkey32[32], + bool fCompressed); +bool ECDSA_privkey_import_der_secp256k1(unsigned char privkey32_out[32], + const unsigned char* der, std::size_t der_len); + +#endif // TRIANGLES_CRYPTO_ECDSA_H diff --git a/src/key.cpp b/src/key.cpp index 6bf60f3..8119412 100644 --- a/src/key.cpp +++ b/src/key.cpp @@ -1,213 +1,33 @@ // Copyright (c) 2009-2012 The Bitcoin developers +// Copyright (c) 2026 The Triangles developers // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include +#include -#include -#include +#include // OPENSSL_cleanse for secure wipe of secret bytes +#include // RAND_bytes for new-key entropy +#include "crypto_ecdsa.h" #include "key.h" -// Generate a private key from just the secret parameter -int EC_KEY_regenerate_key(EC_KEY *eckey, BIGNUM *priv_key) +// ───────────────────────────────────────────────────────────────────────────── +// Order-of-generator constants (still used by CheckSignatureElement, the only +// caller into the BigEndian comparison helper below). Kept here so the file +// remains self-contained. +// ───────────────────────────────────────────────────────────────────────────── + +namespace { + +int CompareBigEndian(const unsigned char* c1, std::size_t c1len, + const unsigned char* c2, std::size_t c2len) { - int ok = 0; - BN_CTX *ctx = NULL; - EC_POINT *pub_key = NULL; - - if (!eckey) return 0; - - const EC_GROUP *group = EC_KEY_get0_group(eckey); - - if ((ctx = BN_CTX_new()) == NULL) - goto err; - - pub_key = EC_POINT_new(group); - - if (pub_key == NULL) - goto err; - - if (!EC_POINT_mul(group, pub_key, priv_key, NULL, NULL, ctx)) - goto err; - - EC_KEY_set_private_key(eckey,priv_key); - EC_KEY_set_public_key(eckey,pub_key); - - ok = 1; - -err: - - if (pub_key) - EC_POINT_free(pub_key); - if (ctx != NULL) - BN_CTX_free(ctx); - - return(ok); -} - -// Perform ECDSA key recovery (see SEC1 4.1.6) for curves over (mod p)-fields -// recid selects which key is recovered -// if check is non-zero, additional checks are performed -int ECDSA_SIG_recover_key_GFp(EC_KEY *eckey, ECDSA_SIG *ecsig, const unsigned char *msg, int msglen, int recid, int check) -{ - if (!eckey) return 0; - - int ret = 0; - BN_CTX *ctx = NULL; - - BIGNUM *x = NULL; - BIGNUM *e = NULL; - BIGNUM *order = NULL; - BIGNUM *sor = NULL; - BIGNUM *eor = NULL; - BIGNUM *field = NULL; - EC_POINT *R = NULL; - EC_POINT *O = NULL; - EC_POINT *Q = NULL; - BIGNUM *rr = NULL; - BIGNUM *zero = NULL; - int n = 0; - int i = recid / 2; - - const EC_GROUP *group = EC_KEY_get0_group(eckey); - if ((ctx = BN_CTX_new()) == NULL) { ret = -1; goto err; } - BN_CTX_start(ctx); - const BIGNUM *sig_r, *sig_s; - ECDSA_SIG_get0(ecsig, &sig_r, &sig_s); - order = BN_CTX_get(ctx); - if (!EC_GROUP_get_order(group, order, ctx)) { ret = -2; goto err; } - x = BN_CTX_get(ctx); - if (!BN_copy(x, order)) { ret=-1; goto err; } - if (!BN_mul_word(x, i)) { ret=-1; goto err; } - if (!BN_add(x, x, sig_r)) { ret=-1; goto err; } - field = BN_CTX_get(ctx); - if (!EC_GROUP_get_curve_GFp(group, field, NULL, NULL, ctx)) { ret=-2; goto err; } - if (BN_cmp(x, field) >= 0) { ret=0; goto err; } - if ((R = EC_POINT_new(group)) == NULL) { ret = -2; goto err; } - if (!EC_POINT_set_compressed_coordinates_GFp(group, R, x, recid % 2, ctx)) { ret=0; goto err; } - if (check) - { - if ((O = EC_POINT_new(group)) == NULL) { ret = -2; goto err; } - if (!EC_POINT_mul(group, O, NULL, R, order, ctx)) { ret=-2; goto err; } - if (!EC_POINT_is_at_infinity(group, O)) { ret = 0; goto err; } - } - if ((Q = EC_POINT_new(group)) == NULL) { ret = -2; goto err; } - n = EC_GROUP_get_degree(group); - e = BN_CTX_get(ctx); - if (!BN_bin2bn(msg, msglen, e)) { ret=-1; goto err; } - if (8*msglen > n) BN_rshift(e, e, 8-(n & 7)); - zero = BN_CTX_get(ctx); - BN_zero(zero); - if (!BN_mod_sub(e, zero, e, order, ctx)) { ret=-1; goto err; } - rr = BN_CTX_get(ctx); - if (!BN_mod_inverse(rr, sig_r, order, ctx)) { ret=-1; goto err; } - sor = BN_CTX_get(ctx); - if (!BN_mod_mul(sor, sig_s, rr, order, ctx)) { ret=-1; goto err; } - eor = BN_CTX_get(ctx); - if (!BN_mod_mul(eor, e, rr, order, ctx)) { ret=-1; goto err; } - if (!EC_POINT_mul(group, Q, eor, R, sor, ctx)) { ret=-2; goto err; } - if (!EC_KEY_set_public_key(eckey, Q)) { ret=-2; goto err; } - - ret = 1; - -err: - if (ctx) { - BN_CTX_end(ctx); - BN_CTX_free(ctx); - } - if (R != NULL) EC_POINT_free(R); - if (O != NULL) EC_POINT_free(O); - if (Q != NULL) EC_POINT_free(Q); - return ret; -} - -void CKey::SetCompressedPubKey() -{ - EC_KEY_set_conv_form(pkey, POINT_CONVERSION_COMPRESSED); - fCompressedPubKey = true; -} - -void CKey::SetUnCompressedPubKey() -{ - EC_KEY_set_conv_form(pkey, POINT_CONVERSION_UNCOMPRESSED); - fCompressedPubKey = false; -} - -EC_KEY* CKey::GetECKey() -{ - return pkey; -} - -void CKey::Reset() -{ - fCompressedPubKey = false; - if (pkey != NULL) - EC_KEY_free(pkey); - pkey = EC_KEY_new_by_curve_name(NID_secp256k1); - if (pkey == NULL) - throw key_error("CKey::CKey() : EC_KEY_new_by_curve_name failed"); - fSet = false; -} - -CKey::CKey() -{ - pkey = NULL; - Reset(); -} - -CKey::CKey(const CKey& b) -{ - pkey = EC_KEY_dup(b.pkey); - if (pkey == NULL) - throw key_error("CKey::CKey(const CKey&) : EC_KEY_dup failed"); - fSet = b.fSet; -} - -CKey& CKey::operator=(const CKey& b) -{ - if (!EC_KEY_copy(pkey, b.pkey)) - throw key_error("CKey::operator=(const CKey&) : EC_KEY_copy failed"); - fSet = b.fSet; - return (*this); -} - -CKey::~CKey() -{ - EC_KEY_free(pkey); -} - -bool CKey::IsNull() const -{ - return !fSet; -} - -bool CKey::IsCompressed() const -{ - return fCompressedPubKey; -} - -int CompareBigEndian(const unsigned char *c1, size_t c1len, const unsigned char *c2, size_t c2len) { - while (c1len > c2len) { - if (*c1) - return 1; - c1++; - c1len--; - } - while (c2len > c1len) { - if (*c2) - return -1; - c2++; - c2len--; - } + while (c1len > c2len) { if (*c1) return 1; c1++; c1len--; } + while (c2len > c1len) { if (*c2) return -1; c2++; c2len--; } while (c1len > 0) { - if (*c1 > *c2) - return 1; - if (*c2 > *c1) - return -1; - c1++; - c2++; - c1len--; + if (*c1 > *c2) return 1; + if (*c2 > *c1) return -1; + c1++; c2++; c1len--; } return 0; } @@ -228,277 +48,332 @@ const unsigned char vchMaxModHalfOrder[32] = { 0xDF,0xE9,0x2F,0x46,0x68,0x1B,0x20,0xA0 }; -const unsigned char vchZero[0] = {}; +const unsigned char vchZero[1] = { 0 }; -bool CKey::CheckSignatureElement(const unsigned char *vch, int len, bool half) { - return CompareBigEndian(vch, len, vchZero, 0) > 0 && - CompareBigEndian(vch, len, half ? vchMaxModHalfOrder : vchMaxModOrder, 32) <= 0; +} // namespace + +bool CKey::CheckSignatureElement(const unsigned char* vchIn, int len, bool half) +{ + return CompareBigEndian(vchIn, len, vchZero, 0) > 0 && + CompareBigEndian(vchIn, len, half ? vchMaxModHalfOrder : vchMaxModOrder, 32) <= 0; } +// ───────────────────────────────────────────────────────────────────────────── +// Lifecycle +// ───────────────────────────────────────────────────────────────────────────── + +void CKey::Reset() +{ + OPENSSL_cleanse(vch, sizeof(vch)); + vchPubKey.clear(); + fSet = false; + fHavePrivKey = false; + fCompressedPubKey = false; +} + +CKey::CKey() +{ + std::memset(vch, 0, sizeof(vch)); + vchPubKey.clear(); + fSet = false; + fHavePrivKey = false; + fCompressedPubKey = false; +} + +CKey::CKey(const CKey& b) +{ + *this = b; +} + +CKey& CKey::operator=(const CKey& b) +{ + if (this == &b) return *this; + std::memcpy(vch, b.vch, sizeof(vch)); + vchPubKey = b.vchPubKey; + fSet = b.fSet; + fHavePrivKey = b.fHavePrivKey; + fCompressedPubKey = b.fCompressedPubKey; + return *this; +} + +CKey::~CKey() +{ + OPENSSL_cleanse(vch, sizeof(vch)); +} + +bool CKey::IsNull() const { return !fSet; } +bool CKey::IsCompressed() const { return fCompressedPubKey; } + +// ───────────────────────────────────────────────────────────────────────────── +// Compression toggle +// +// In the new model the pubkey is always cached at the current compression. If +// we hold the private key we can re-derive trivially; if we only hold a public +// key, callers don't toggle compression in practice in this codebase, so we +// just flip the flag and rely on the next SetPubKey/SetSecret to refresh the +// cache. +// ───────────────────────────────────────────────────────────────────────────── + +void CKey::SetCompressedPubKey() +{ + if (fCompressedPubKey) return; + fCompressedPubKey = true; + if (fSet && fHavePrivKey) { + std::size_t len = 33; + vchPubKey.resize(len); + if (!ECDSA_pubkey_from_privkey_secp256k1(&vchPubKey[0], &len, vch, /*fCompressed=*/true)) { + Reset(); + return; + } + vchPubKey.resize(len); + } +} + +void CKey::SetUnCompressedPubKey() +{ + if (!fCompressedPubKey && fSet) return; + fCompressedPubKey = false; + if (fSet && fHavePrivKey) { + std::size_t len = 65; + vchPubKey.resize(len); + if (!ECDSA_pubkey_from_privkey_secp256k1(&vchPubKey[0], &len, vch, /*fCompressed=*/false)) { + Reset(); + return; + } + vchPubKey.resize(len); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Key generation / load / store +// ───────────────────────────────────────────────────────────────────────────── + void CKey::MakeNewKey(bool fCompressed) { - if (!EC_KEY_generate_key(pkey)) - throw key_error("CKey::MakeNewKey() : EC_KEY_generate_key failed"); - if (fCompressed) - SetCompressedPubKey(); - fSet = true; + // Sample 32 bytes of entropy and reject any that fall outside (0, n). + // Probability of needing a retry is ~2^-128. + do { + if (RAND_bytes(vch, sizeof(vch)) != 1) + throw key_error("CKey::MakeNewKey() : RAND_bytes failed"); + } while (!ECDSA_seckey_verify_secp256k1(vch)); + + fSet = true; + fHavePrivKey = true; + fCompressedPubKey = fCompressed; + + std::size_t len = fCompressed ? 33 : 65; + vchPubKey.resize(len); + if (!ECDSA_pubkey_from_privkey_secp256k1(&vchPubKey[0], &len, vch, fCompressed)) { + Reset(); + throw key_error("CKey::MakeNewKey() : failed to derive public key"); + } + vchPubKey.resize(len); } bool CKey::SetPrivKey(const CPrivKey& vchPrivKey) { - const unsigned char* pbegin = &vchPrivKey[0]; - if (d2i_ECPrivateKey(&pkey, &pbegin, vchPrivKey.size())) - { - // In testing, d2i_ECPrivateKey can return true - // but fill in pkey with a key that fails - // EC_KEY_check_key, so: - if (EC_KEY_check_key(pkey)) - { - fSet = true; - return true; - } + unsigned char raw[32]; + if (!ECDSA_privkey_import_der_secp256k1(raw, &vchPrivKey[0], vchPrivKey.size())) { + OPENSSL_cleanse(raw, sizeof(raw)); + Reset(); + return false; } - // If vchPrivKey data is bad d2i_ECPrivateKey() can - // leave pkey in a state where calling EC_KEY_free() - // crashes. To avoid that, set pkey to NULL and - // leak the memory (a leak is better than a crash) - pkey = NULL; - Reset(); - return false; + + // Carry the compressed flag out of the DER blob. The two valid sizes + // produced by ECDSA_privkey_export_der_secp256k1 are 214 (compressed) and + // 279 (uncompressed); foreign DER blobs are best-effort but those two + // cover every record this codebase has ever written. + bool fCompressed = (vchPrivKey.size() == 214); + + CSecret secret(raw, raw + 32); + OPENSSL_cleanse(raw, sizeof(raw)); + return SetSecret(secret, fCompressed); } bool CKey::SetSecret(const CSecret& vchSecret, bool fCompressed) { - EC_KEY_free(pkey); - pkey = EC_KEY_new_by_curve_name(NID_secp256k1); - if (pkey == NULL) - throw key_error("CKey::SetSecret() : EC_KEY_new_by_curve_name failed"); if (vchSecret.size() != 32) throw key_error("CKey::SetSecret() : secret must be 32 bytes"); - BIGNUM *bn = BN_bin2bn(&vchSecret[0],32,BN_new()); - if (bn == NULL) - throw key_error("CKey::SetSecret() : BN_bin2bn failed"); - if (!EC_KEY_regenerate_key(pkey,bn)) - { - BN_clear_free(bn); - throw key_error("CKey::SetSecret() : EC_KEY_regenerate_key failed"); + if (!ECDSA_seckey_verify_secp256k1(&vchSecret[0])) + throw key_error("CKey::SetSecret() : secret is not a valid scalar"); + + std::memcpy(vch, &vchSecret[0], 32); + fSet = true; + fHavePrivKey = true; + // Preserve sticky-compression behaviour from the OpenSSL implementation: + // if either the explicit argument or the previously-set flag is true, + // the result is compressed. + bool fComp = fCompressed || fCompressedPubKey; + fCompressedPubKey = fComp; + + std::size_t len = fComp ? 33 : 65; + vchPubKey.resize(len); + if (!ECDSA_pubkey_from_privkey_secp256k1(&vchPubKey[0], &len, vch, fComp)) { + Reset(); + return false; } - BN_clear_free(bn); - fSet = true; - if (fCompressed || fCompressedPubKey) - SetCompressedPubKey(); + vchPubKey.resize(len); return true; } -CSecret CKey::GetSecret(bool &fCompressed) const +CSecret CKey::GetSecret(bool& fCompressed) const { - CSecret vchRet; - vchRet.resize(32); - const BIGNUM *bn = EC_KEY_get0_private_key(pkey); - int nBytes = BN_num_bytes(bn); - if (bn == NULL) - throw key_error("CKey::GetSecret() : EC_KEY_get0_private_key failed"); - int n=BN_bn2bin(bn,&vchRet[32 - nBytes]); - if (n != nBytes) - throw key_error("CKey::GetSecret(): BN_bn2bin failed"); + if (!fSet || !fHavePrivKey) + throw key_error("CKey::GetSecret() : key is not set or has no private component"); + CSecret out(vch, vch + 32); fCompressed = fCompressedPubKey; - return vchRet; + return out; } CPrivKey CKey::GetPrivKey() const { - int nSize = i2d_ECPrivateKey(pkey, NULL); - if (!nSize) - throw key_error("CKey::GetPrivKey() : i2d_ECPrivateKey failed"); - CPrivKey vchPrivKey(nSize, 0); - unsigned char* pbegin = &vchPrivKey[0]; - if (i2d_ECPrivateKey(pkey, &pbegin) != nSize) - throw key_error("CKey::GetPrivKey() : i2d_ECPrivateKey returned unexpected size"); - return vchPrivKey; + if (!fSet || !fHavePrivKey) + throw key_error("CKey::GetPrivKey() : key is not set or has no private component"); + + // Max possible output: 279 bytes (uncompressed). + CPrivKey out(279, 0); + std::size_t out_len = out.size(); + if (!ECDSA_privkey_export_der_secp256k1(&out[0], &out_len, vch, fCompressedPubKey)) + throw key_error("CKey::GetPrivKey() : DER export failed"); + out.resize(out_len); + return out; } -bool CKey::SetPubKey(const CPubKey& vchPubKey) +bool CKey::SetPubKey(const CPubKey& cpub) { - const unsigned char* pbegin = &vchPubKey.vchPubKey[0]; - if (o2i_ECPublicKey(&pkey, &pbegin, vchPubKey.vchPubKey.size())) - { - fSet = true; - if (vchPubKey.vchPubKey.size() == 33) - SetCompressedPubKey(); - return true; + const std::vector& vchPub = cpub.vchPubKey; + if (vchPub.size() != 33 && vchPub.size() != 65) { + Reset(); + return false; } - pkey = NULL; - Reset(); - return false; + if (!ECDSA_pubkey_verify_secp256k1(&vchPub[0], vchPub.size())) { + Reset(); + return false; + } + vchPubKey = vchPub; + fSet = true; + fHavePrivKey = false; + fCompressedPubKey = (vchPub.size() == 33); + return true; } CPubKey CKey::GetPubKey() const { - int nSize = i2o_ECPublicKey(pkey, NULL); - if (!nSize) - throw key_error("CKey::GetPubKey() : i2o_ECPublicKey failed"); - std::vector vchPubKey(nSize, 0); - unsigned char* pbegin = &vchPubKey[0]; - if (i2o_ECPublicKey(pkey, &pbegin) != nSize) - throw key_error("CKey::GetPubKey() : i2o_ECPublicKey returned unexpected size"); return CPubKey(vchPubKey); } +// ───────────────────────────────────────────────────────────────────────────── +// Sign / verify / recover (all delegate to crypto_ecdsa wrappers) +// ───────────────────────────────────────────────────────────────────────────── + bool CKey::Sign(uint256 hash, std::vector& vchSig) { vchSig.clear(); - ECDSA_SIG *sig = ECDSA_do_sign((unsigned char*)&hash, sizeof(hash), pkey); - if (sig == NULL) + if (!fSet || !fHavePrivKey) return false; + + // libsecp256k1's max DER output is 72 bytes; allocate that and shrink. + vchSig.resize(72); + std::size_t sig_len = vchSig.size(); + if (!ECDSA_sign_secp256k1(&vchSig[0], &sig_len, + reinterpret_cast(&hash), + vch)) + { + vchSig.clear(); return false; - BN_CTX *ctx = BN_CTX_new(); - BN_CTX_start(ctx); - const EC_GROUP *group = EC_KEY_get0_group(pkey); - BIGNUM *order = BN_CTX_get(ctx); - BIGNUM *halforder = BN_CTX_get(ctx); - EC_GROUP_get_order(group, order, ctx); - BN_rshift1(halforder, order); - const BIGNUM *sig_r, *sig_s; - ECDSA_SIG_get0(sig, &sig_r, &sig_s); - if (BN_cmp(sig_s, halforder) > 0) { - // enforce low S values, by negating the value (modulo the order) if above order/2. - BIGNUM *new_s = BN_new(); - BN_sub(new_s, order, sig_s); - BIGNUM *dup_r = BN_dup(sig_r); - ECDSA_SIG_set0(sig, dup_r, new_s); } - BN_CTX_end(ctx); - BN_CTX_free(ctx); - unsigned int nSize = ECDSA_size(pkey); - vchSig.resize(nSize); // Make sure it is big enough - unsigned char *pos = &vchSig[0]; - nSize = i2d_ECDSA_SIG(sig, &pos); - ECDSA_SIG_free(sig); - vchSig.resize(nSize); // Shrink to fit actual size + vchSig.resize(sig_len); return true; } -// create a compact signature (65 bytes), which allows reconstructing the used public key -// The format is one header byte, followed by two times 32 bytes for the serialized r and s values. -// The header byte: 0x1B = first key with even y, 0x1C = first key with odd y, -// 0x1D = second key with even y, 0x1E = second key with odd y +// Compact signature (65 bytes): one header byte (encoding recid + compression) +// followed by 32-byte r and 32-byte s. bool CKey::SignCompact(uint256 hash, std::vector& vchSig) { - bool fOk = false; - ECDSA_SIG *sig = ECDSA_do_sign((unsigned char*)&hash, sizeof(hash), pkey); - if (sig==NULL) - return false; vchSig.clear(); - vchSig.resize(65,0); - const BIGNUM *sig_r, *sig_s; - ECDSA_SIG_get0(sig, &sig_r, &sig_s); - int nBitsR = BN_num_bits(sig_r); - int nBitsS = BN_num_bits(sig_s); - if (nBitsR <= 256 && nBitsS <= 256) + if (!fSet || !fHavePrivKey) return false; + + vchSig.resize(65, 0); + if (!ECDSA_sign_compact_secp256k1(&vchSig[0], + reinterpret_cast(&hash), + vch, + fCompressedPubKey)) { - int nRecId = -1; - for (int i=0; i<4; i++) - { - CKey keyRec; - keyRec.fSet = true; - if (fCompressedPubKey) - keyRec.SetCompressedPubKey(); - if (ECDSA_SIG_recover_key_GFp(keyRec.pkey, sig, (unsigned char*)&hash, sizeof(hash), i, 1) == 1) - if (keyRec.GetPubKey() == this->GetPubKey()) - { - nRecId = i; - break; - } - } - - if (nRecId == -1) - { - ECDSA_SIG_free(sig); - throw key_error("CKey::SignCompact() : unable to construct recoverable key"); - } - - vchSig[0] = nRecId+27+(fCompressedPubKey ? 4 : 0); - BN_bn2bin(sig_r,&vchSig[33-(nBitsR+7)/8]); - BN_bn2bin(sig_s,&vchSig[65-(nBitsS+7)/8]); - fOk = true; + vchSig.clear(); + return false; } - ECDSA_SIG_free(sig); - return fOk; + return true; } -// reconstruct public key from a compact signature -// This is only slightly more CPU intensive than just verifying it. -// If this function succeeds, the recovered public key is guaranteed to be valid -// (the signature is a valid signature of the given data for that key) bool CKey::SetCompactSignature(uint256 hash, const std::vector& vchSig) { - if (vchSig.size() != 65) - return false; + if (vchSig.size() != 65) return false; int nV = vchSig[0]; - if (nV<27 || nV>=35) - return false; - ECDSA_SIG *sig = ECDSA_SIG_new(); - BIGNUM *sig_r = BN_bin2bn(&vchSig[1],32,NULL); - BIGNUM *sig_s = BN_bin2bn(&vchSig[33],32,NULL); - ECDSA_SIG_set0(sig, sig_r, sig_s); + if (nV < 27 || nV >= 35) return false; - EC_KEY_free(pkey); - pkey = EC_KEY_new_by_curve_name(NID_secp256k1); - if (nV >= 31) - { - SetCompressedPubKey(); - nV -= 4; - } - if (ECDSA_SIG_recover_key_GFp(pkey, sig, (unsigned char*)&hash, sizeof(hash), nV - 27, 0) == 1) - { - fSet = true; - ECDSA_SIG_free(sig); - return true; - } - ECDSA_SIG_free(sig); - return false; + unsigned char pubkey[65]; + std::size_t pubkey_len = 0; + if (!ECDSA_recover_compact_secp256k1(pubkey, &pubkey_len, + reinterpret_cast(&hash), + &vchSig[0])) + return false; + + std::vector vchPub(pubkey, pubkey + pubkey_len); + return SetPubKey(CPubKey(vchPub)); } bool CKey::Verify(uint256 hash, const std::vector& vchSig) { - // -1 = error, 0 = bad sig, 1 = good - if (ECDSA_verify(0, (unsigned char*)&hash, sizeof(hash), &vchSig[0], vchSig.size(), pkey) != 1) - return false; + if (vchSig.empty() || !fSet) return false; - return true; + return ECDSA_verify_secp256k1( + reinterpret_cast(&hash), + &vchSig[0], vchSig.size(), + &vchPubKey[0], vchPubKey.size()); } bool CKey::VerifyCompact(uint256 hash, const std::vector& vchSig) { CKey key; - if (!key.SetCompactSignature(hash, vchSig)) - return false; - if (GetPubKey() != key.GetPubKey()) - return false; - - return true; + if (!key.SetCompactSignature(hash, vchSig)) return false; + return GetPubKey() == key.GetPubKey(); } bool CKey::IsValid() { - if (!fSet) - return false; + if (!fSet) return false; - if (!EC_KEY_check_key(pkey)) - return false; + if (fHavePrivKey) { + if (!ECDSA_seckey_verify_secp256k1(vch)) return false; - bool fCompr; - CSecret secret = GetSecret(fCompr); - CKey key2; - key2.SetSecret(secret, fCompr); - return GetPubKey() == key2.GetPubKey(); + // Re-derive the pubkey and check it matches the cache. This is the + // libsecp256k1 equivalent of OpenSSL's "consistency between priv and + // pub" check the original implementation performed. + unsigned char rederived[65]; + std::size_t rederived_len = 0; + if (!ECDSA_pubkey_from_privkey_secp256k1(rederived, &rederived_len, vch, fCompressedPubKey)) + return false; + if (rederived_len != vchPubKey.size()) return false; + return std::memcmp(rederived, &vchPubKey[0], rederived_len) == 0; + } + + return ECDSA_pubkey_verify_secp256k1(&vchPubKey[0], vchPubKey.size()); } -bool ECC_InitSanityCheck() { - EC_KEY *pkey = EC_KEY_new_by_curve_name(NID_secp256k1); - if(pkey == NULL) - return false; - EC_KEY_free(pkey); +// ───────────────────────────────────────────────────────────────────────────── +// Startup smoke test for the cryptography backend. +// ───────────────────────────────────────────────────────────────────────────── - // TODO Is there more EC functionality that could be missing? +bool ECC_InitSanityCheck() +{ + // Verify that libsecp256k1 can validate a trivially-known good secret + // (the scalar 1) and reject zero. If either of these fails, the linked + // library is broken and we should refuse to start. + static const unsigned char one[32] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 + }; + static const unsigned char zero[32] = {0}; + if (!ECDSA_seckey_verify_secp256k1(one)) return false; + if ( ECDSA_seckey_verify_secp256k1(zero)) return false; return true; } diff --git a/src/key.h b/src/key.h index c61dc0d..dafa62d 100644 --- a/src/key.h +++ b/src/key.h @@ -13,8 +13,6 @@ #include "uint256.h" #include "util.h" -#include // for EC_KEY definition - // secp160k1 // const unsigned int PRIVATE_KEY_SIZE = 192; // const unsigned int PUBLIC_KEY_SIZE = 41; @@ -105,20 +103,22 @@ typedef std::vector > CPrivKey; // CSecret is a serialization of just the secret parameter (32 bytes) typedef std::vector > CSecret; -/** An encapsulated OpenSSL Elliptic Curve key (public and/or private) */ +/** An encapsulated secp256k1 elliptic-curve key (public and/or private). */ class CKey { protected: - EC_KEY* pkey; + // 32-byte private scalar. Valid iff fSet && fHavePrivKey. + unsigned char vch[32]; + // Cached serialized public key (33 or 65 bytes). Valid iff fSet. + std::vector vchPubKey; bool fSet; bool fCompressedPubKey; + bool fHavePrivKey; public: void SetCompressedPubKey(); void SetUnCompressedPubKey(); - - EC_KEY* GetECKey(); - + void Reset(); CKey(); diff --git a/src/main.cpp b/src/main.cpp index 2419f3a..e993510 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6039,6 +6039,7 @@ bool SendMessages(CNode* pto, bool fSendTrickle) const unsigned int nInFlight = CountHeaderSyncInFlight(); static int64_t nLastHeaderPlannerControl = 0; static int64_t nLastHeaderWatchdog = 0; + static int64_t nLastBlockPlannerControl = 0; if (nLastNewHeaderTime == 0) nLastNewHeaderTime = nNowSec; @@ -6076,6 +6077,22 @@ bool SendMessages(CNode* pto, bool fSendTrickle) if (nNowSec - pto->nLastIbdHeaderRequest >= nMinInterval) RequestHeaderSyncRefill(pto, hashBestHeaderSync, nMinInterval, "heartbeat"); + + // Keep the block planner alive even when no new headers arrive and + // no blocks are being accepted. Without this periodic kick, the + // redundant-request and timeout logic inside QueueHeaderSyncBlocksParallel() + // only runs on header arrivals or block acceptance, so IBD can park + // indefinitely behind one missing frontier block. + if (nNowSec - nLastBlockPlannerControl >= HEADER_SYNC_CONTROL_INTERVAL_SECONDS && + hashBestHeaderSync != 0 && + nPlannerDepth > 0) + { + const unsigned int nRequeued = QueueHeaderSyncBlocksParallel(HEADER_DOWNLOAD_WINDOW); + if (nRequeued > 0) + printf("IBD-DIAG: block-planner control queued %u block requests (plannerDepth=%u inflight=%u)\n", + nRequeued, nPlannerDepth, nInFlight); + nLastBlockPlannerControl = nNowSec; + } } // diff --git a/src/secp256k1 b/src/secp256k1 new file mode 160000 index 0000000..ea174fe --- /dev/null +++ b/src/secp256k1 @@ -0,0 +1 @@ +Subproject commit ea174fe045e1832548cd3b7090958afe9573ad2b diff --git a/src/smessage.cpp b/src/smessage.cpp index fc88a8a..c443a65 100644 --- a/src/smessage.cpp +++ b/src/smessage.cpp @@ -41,8 +41,6 @@ Notes: #include #include -#include -#include #include #include #include @@ -53,6 +51,7 @@ Notes: #include "base58.h" +#include "crypto_ecdh.h" #include "db.h" #include "init.h" // pwalletMain #include "txdb.h" @@ -3682,7 +3681,7 @@ int SecureMsgEncrypt(SecureMessage& smsg, std::string& addressFrom, std::string& 3 addressFrom is invalid. 4 addressTo is invalid. 5 Could not get public key for addressTo. - 6 ECDH_compute_key failed + 6 ECDH key derivation failed 7 Could not get private key for addressFrom. 8 Could not allocate memory. 9 Could not compress message data. @@ -3777,22 +3776,26 @@ int SecureMsgEncrypt(SecureMessage& smsg, std::string& addressFrom, std::string& std::vector vchP; vchP.resize(32); - EC_KEY* pkeyr = keyR.GetECKey(); - EC_KEY* pkeyK = keyK.GetECKey(); - - // always seems to be 32, worth checking? - //int field_size = EC_GROUP_get_degree(EC_KEY_get0_group(pkeyr)); - //int secret_len = (field_size+7)/8; - //printf("secret_len %d.\n", secret_len); - - // -- ECDH_compute_key returns the same P if fed compressed or uncompressed public keys - int lenP = ECDH_compute_key(&vchP[0], 32, EC_KEY_get0_public_key(pkeyK), pkeyr, NULL); - - if (lenP != 32) + + bool fCompressedR = false; + CSecret secretR = keyR.GetSecret(fCompressedR); + if (secretR.size() != 32) { - printf("ECDH_compute_key failed, lenP: %d.\n", lenP); + printf("ECDH: keyR secret has unexpected size %zu.\n", secretR.size()); return 6; - }; + } + std::vector vchPubK = keyK.GetPubKey().Raw(); + if (vchPubK.size() != 33 && vchPubK.size() != 65) + { + printf("ECDH: keyK pubkey has unexpected size %zu.\n", vchPubK.size()); + return 6; + } + + if (!ECDH_xonly_secp256k1(&vchP[0], &secretR[0], &vchPubK[0], vchPubK.size())) + { + printf("ECDH (encrypt): secp256k1_ecdh failed.\n"); + return 6; + } CPubKey cpkR = keyR.GetPubKey(); if (!cpkR.IsValid() @@ -3980,7 +3983,7 @@ int SecureMsgSend(std::string& addressFrom, std::string& addressTo, std::string& case 3: sError = "Invalid addressFrom."; break; case 4: sError = "Invalid addressTo."; break; case 5: sError = "Could not get public key for addressTo."; break; - case 6: sError = "ECDH_compute_key failed."; break; + case 6: sError = "ECDH key derivation failed."; break; case 7: sError = "Could not get private key for addressFrom."; break; case 8: sError = "Could not allocate memory."; break; case 9: sError = "Could not compress message data."; break; @@ -4193,16 +4196,26 @@ int SecureMsgDecrypt(bool fTestOnly, std::string& address, unsigned char *pHeade // -- Do an EC point multiply with private key k and public key R. This gives you public key P. std::vector vchP; vchP.resize(32); - EC_KEY* pkeyk = keyDest.GetECKey(); - EC_KEY* pkeyR = keyR.GetECKey(); - - int lenPdec = ECDH_compute_key(&vchP[0], 32, EC_KEY_get0_public_key(pkeyR), pkeyk, NULL); - - if (lenPdec != 32) + + bool fCompressedDest = false; + CSecret secretDest = keyDest.GetSecret(fCompressedDest); + if (secretDest.size() != 32) { - printf("ECDH_compute_key failed, lenPdec: %d.\n", lenPdec); + printf("ECDH: keyDest secret has unexpected size %zu.\n", secretDest.size()); return 1; - }; + } + std::vector vchPubR = keyR.GetPubKey().Raw(); + if (vchPubR.size() != 33 && vchPubR.size() != 65) + { + printf("ECDH: keyR pubkey has unexpected size %zu.\n", vchPubR.size()); + return 1; + } + + if (!ECDH_xonly_secp256k1(&vchP[0], &secretDest[0], &vchPubR[0], vchPubR.size())) + { + printf("ECDH (decrypt): secp256k1_ecdh failed.\n"); + return 1; + } // -- Use public key P to calculate the SHA512 hash H.