55c202516d
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) <noreply@anthropic.com>
390 lines
15 KiB
C++
390 lines
15 KiB
C++
// 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 <cstring>
|
|
#include <mutex>
|
|
|
|
#include <secp256k1.h>
|
|
#include <secp256k1_recovery.h>
|
|
|
|
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<unsigned char>(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);
|
|
}
|