Files
triangles_v5/src/crypto_ecdsa.h
T
sami7777 55c202516d 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) <noreply@anthropic.com>
2026-04-29 02:55:18 -07:00

122 lines
5.7 KiB
C++

// 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 <cstddef>
/**
* 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