extmod/secp256k1mod.c: add sign, verify, pubkey recover funcs

This commit is contained in:
The MMGen Project 2025-06-29 14:04:46 +00:00
commit 730a112f69
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
3 changed files with 272 additions and 1 deletions

View file

@ -25,6 +25,7 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <secp256k1.h>
#include <secp256k1_recovery.h>
#include "random.h"
static secp256k1_context * create_context(
@ -196,6 +197,160 @@ static PyObject * pubkey_check(PyObject *self, PyObject *args) {
return Py_BuildValue("I", 1);
}
/*
* returns 64-byte serialized signature (r + s) plus integer recovery ID in range 0-3
*/
static PyObject * sign_msghash(PyObject *self, PyObject *args) {
const unsigned char * msghash_bytes;
const unsigned char * privkey_bytes;
Py_ssize_t msghash_bytes_len;
Py_ssize_t privkey_bytes_len;
if (!PyArg_ParseTuple(
args,
"y#y#",
&msghash_bytes,
&msghash_bytes_len,
&privkey_bytes,
&privkey_bytes_len)) {
PyErr_SetString(PyExc_ValueError, "Unable to parse extension mod arguments");
return NULL;
}
if (msghash_bytes_len != 32) {
PyErr_SetString(PyExc_RuntimeError, "Invalid message hash length (not 32 bytes)");
return NULL;
}
secp256k1_context *ctx = create_context(1);
if (!privkey_check(ctx, privkey_bytes, privkey_bytes_len, "Private key")) {
return NULL;
}
secp256k1_ecdsa_recoverable_signature rsig;
unsigned char rsig_serialized[65];
int recid;
if (!secp256k1_ecdsa_sign_recoverable(ctx, &rsig, msghash_bytes, privkey_bytes, NULL, NULL)) {
PyErr_SetString(PyExc_ValueError, "Unable to sign message hash");
return NULL;
}
if (!secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, rsig_serialized, &recid, &rsig)) {
PyErr_SetString(PyExc_ValueError, "Unable to serialize signature");
return NULL;
}
/* truncate serialized sig to 64 bytes */
return Py_BuildValue("y#I", rsig_serialized, 64, recid);
}
static PyObject * verify_sig(PyObject *self, PyObject *args) {
const unsigned char * sig_bytes;
const unsigned char * msghash_bytes;
const unsigned char * pubkey_bytes;
Py_ssize_t sig_bytes_len;
Py_ssize_t msghash_bytes_len;
Py_ssize_t pubkey_bytes_len;
if (!PyArg_ParseTuple(
args,
"y#y#y#",
&sig_bytes,
&sig_bytes_len,
&msghash_bytes,
&msghash_bytes_len,
&pubkey_bytes,
&pubkey_bytes_len)) {
PyErr_SetString(PyExc_ValueError, "Unable to parse extension mod arguments");
return NULL;
}
if (sig_bytes_len != 64) {
PyErr_SetString(PyExc_RuntimeError, "Invalid signature length (not 64 bytes)");
return NULL;
}
if (msghash_bytes_len != 32) {
PyErr_SetString(PyExc_RuntimeError, "Invalid message hash length (not 32 bytes)");
return NULL;
}
secp256k1_ecdsa_signature sig;
secp256k1_pubkey pubkey;
secp256k1_context *ctx = create_context(1);
if (!secp256k1_ecdsa_signature_parse_compact(ctx, &sig, sig_bytes)) {
PyErr_SetString(PyExc_RuntimeError, "Failed to parse signature");
return NULL;
}
if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, pubkey_bytes, pubkey_bytes_len)) {
PyErr_SetString(PyExc_RuntimeError, "Failed to parse public key");
return NULL;
}
/* returns 1 on valid sig, 0 on invalid sig */
return Py_BuildValue("I", secp256k1_ecdsa_verify(ctx, &sig, msghash_bytes, &pubkey));
}
static PyObject * pubkey_recover(PyObject *self, PyObject *args) {
const unsigned char * msghash_bytes;
const unsigned char * sig_bytes;
int recid;
int compressed;
Py_ssize_t msghash_bytes_len;
Py_ssize_t sig_bytes_len;
if (!PyArg_ParseTuple(
args,
"y#y#ii",
&msghash_bytes,
&msghash_bytes_len,
&sig_bytes,
&sig_bytes_len,
&recid,
&compressed)) {
PyErr_SetString(PyExc_ValueError, "Unable to parse extension mod arguments");
return NULL;
}
if (recid < 0 || recid > 3) {
PyErr_SetString(PyExc_RuntimeError, "Invalid recovery ID (not in range 0-3)");
return NULL;
}
if (sig_bytes_len != 64) {
PyErr_SetString(PyExc_RuntimeError, "Invalid signature length (not 64 bytes)");
return NULL;
}
if (msghash_bytes_len != 32) {
PyErr_SetString(PyExc_RuntimeError, "Invalid message hash length (not 32 bytes)");
return NULL;
}
secp256k1_context *ctx = create_context(1);
secp256k1_ecdsa_recoverable_signature rsig;
secp256k1_pubkey pubkey;
size_t pubkey_bytes_len = compressed == 1 ? 33 : 65;
unsigned char pubkey_bytes[pubkey_bytes_len];
if (!secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, &rsig, sig_bytes, recid)) {
PyErr_SetString(PyExc_RuntimeError, "Failed to parse signature");
return NULL;
}
if (!secp256k1_ecdsa_recover(ctx, &pubkey, &rsig, msghash_bytes)) {
PyErr_SetString(PyExc_RuntimeError, "Failed to recover public key");
return NULL;
}
if (secp256k1_ec_pubkey_serialize(ctx, pubkey_bytes, &pubkey_bytes_len, &pubkey,
compressed == 1 ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED) != 1) {
PyErr_SetString(PyExc_RuntimeError, "Failed to serialize public key");
return NULL;
}
return Py_BuildValue("y#", pubkey_bytes, pubkey_bytes_len);
}
/* https://docs.python.org/3/howto/cporting.html */
struct module_state {
@ -223,6 +378,24 @@ static PyMethodDef secp256k1_methods[] = {
METH_VARARGS,
"Check a serialized pubkey, ensuring the encoded point is not point-at-infinity"
},
{
"sign_msghash",
sign_msghash,
METH_VARARGS,
"Sign a 32-byte message hash with a private key"
},
{
"verify_sig",
verify_sig,
METH_VARARGS,
"Verify a signature"
},
{
"pubkey_recover",
pubkey_recover,
METH_VARARGS,
"Recover a serialized pubkey from a recoverable signature plus signed message"
},
{NULL, NULL}
};

View file

@ -58,6 +58,7 @@ setup(
ext_modules = [Extension(
name = 'mmgen.proto.secp256k1.secp256k1',
sources = ['extmod/secp256k1mod.c'],
depends = ['extmod/random.h'],
libraries = ['gmp', 'secp256k1'] if sys.platform == 'win32' else ['secp256k1'],
include_dirs = ['/usr/local/include'] if sys.platform == 'darwin' else [],
library_dirs = ['/usr/local/lib'] if sys.platform == 'darwin' else [],

View file

@ -4,7 +4,16 @@
test.modtest_d.ecc: elliptic curve unit test for the MMGen suite
"""
from mmgen.proto.secp256k1.secp256k1 import pubkey_gen, pubkey_tweak_add, pubkey_check
import ecdsa
from py_ecc.secp256k1.secp256k1 import ecdsa_raw_sign
from mmgen.proto.secp256k1.secp256k1 import (
pubkey_gen,
pubkey_tweak_add,
pubkey_check,
sign_msghash,
pubkey_recover,
verify_sig)
from ..include.common import vmsg
from ..include.ecc import pubkey_tweak_add_pyecdsa
@ -12,8 +21,95 @@ from mmgen.protocol import CoinProtocol
secp256k1_group_order = CoinProtocol.Secp256k1.secp256k1_group_order
def sign_msghash_pyecc(msghash, privkey):
v, r, s = ecdsa_raw_sign(msghash, privkey)
return (
r.to_bytes(length=32) + s.to_bytes(length=32),
v - 27)
class unit_tests:
def sig_ops(self, name, ut):
vmsg(' Creating and verifying signatures and recovering public keys:')
for mh, pk in (
(1, 1),
(123456789 * 2**222, 12345),
(123456789 * 2**222, 2**256 - 2**129 - 987654321),
(9999999, 2**233),
(12345, 1234 * 2**240),
):
msghash = mh.to_bytes(32, 'big')
privkey = pk.to_bytes(32, 'big')
vmsg(f'\n msg: {msghash.hex()}')
vmsg(f' privkey: {privkey.hex()}')
pubkey = pubkey_gen(privkey, 1)
sig, recid = sign_msghash(msghash, privkey)
sig_chk, _ = sign_msghash_pyecc(msghash, privkey)
if sig != sig_chk:
import time
from mmgen.util import ymsg
ymsg('Warning: signature (libsecp256k1) does not match reference value (py_ecc)!')
time.sleep(1)
vmsg(f' recid: {recid}')
assert recid in (0, 1)
ec_pubkey = ecdsa.VerifyingKey.from_string(pubkey, curve=ecdsa.curves.SECP256k1)
assert ec_pubkey.verify_digest(sig, msghash), 'signature verification failed (py-ecdsa)'
assert verify_sig(sig, msghash, pubkey) == 1, 'signature verification failed (secp256k1)'
pubkey_rec = pubkey_recover(msghash, sig, recid, True)
assert pubkey == pubkey_rec, f'{pubkey.hex()} != {pubkey_rec.hex()}'
return True
def sig_errors(self, name, ut):
vmsg(' Testing error handling for signature ops')
msghash = bytes.fromhex('deadbeef' * 8)
privkey = bytes.fromhex('beadcafe' * 8)
pubkey = pubkey_gen(privkey, 1)
sig, recid = sign_msghash(msghash, privkey)
def sign1(): sign_msghash(1, bytes(32))
def sign2(): sign_msghash(b'\xff' + bytes(32), bytes(32))
def sign3(): sign_msghash(bytes(32), bytes(32))
def verify1(): verify_sig(1, 2, 3)
def verify2(): verify_sig(bytes(64), bytes(32), bytes(33))
def verify3(): assert verify_sig(bytes([99]) + sig[1:], msghash, pubkey) == 1, 'bad signature'
def verify4(): assert verify_sig(sig, msghash, pubkey) == 0, 'good signature'
def verify5(): verify_sig(sig, msghash + bytes([201]), pubkey)
def verify6(): verify_sig(sig + bytes([66]), msghash, pubkey)
def recov1(): pubkey_recover(1, 2, 3)
def recov2(): pubkey_recover(msghash, sig, 8, True)
def recov3(): pubkey_recover(msghash, sig, -3, 1)
def recov4(): pubkey_recover(msghash, bytes([77]) + sig, recid, 1)
def recov5(): pubkey_recover(msghash + bytes([33]), sig, recid, 1)
def recov6():
assert pubkey_recover(msghash[:-1] + bytes([44]), sig, recid, 1) == pubkey, 'bad pubkey'
def recov7():
assert pubkey_recover(msghash, sig, recid, True) != pubkey, 'good pubkey'
bad_data = (
('sign: bad args', 'ValueError', 'Unable to parse', sign1),
('sign: bad msghash len', 'RuntimeError', 'hash length', sign2),
('sign: privkey=0', 'ValueError', 'Private key not in allowable', sign3),
('verify: bad args', 'ValueError', 'Unable to parse', verify1),
('verify: bad pubkey', 'RuntimeError', 'Failed to parse', verify2),
('verify: bad sig', 'AssertionError', 'bad signature', verify3),
('verify: good sig', 'AssertionError', 'good signature', verify4),
('verify: bad msghash len', 'RuntimeError', 'message hash length', verify5),
('verify: bad sig len', 'RuntimeError', 'Invalid signature length', verify6),
('recover: bad args', 'ValueError', 'Unable to parse', recov1),
('recover: bad recid', 'RuntimeError', 'Invalid recovery ID', recov2),
('recover: bad recid', 'RuntimeError', 'Invalid recovery ID', recov3),
('recover: bad sig len', 'RuntimeError', 'Invalid signature length', recov4),
('recover: bad msghash len', 'RuntimeError', 'message hash length', recov5),
('recover: bad pubkey', 'AssertionError', 'bad pubkey', recov6),
('recover: bad pubkey', 'AssertionError', 'good pubkey', recov7),
)
ut.process_bad_data(bad_data, pfx='')
return True
def pubkey_ops(self, name, ut):
vmsg(' Generating pubkey, adding scalar 123456789 to pubkey:')
pk_addend_bytes = int.to_bytes(123456789, length=32, byteorder='big')
@ -43,6 +139,7 @@ class unit_tests:
return True
def pubkey_errors(self, name, ut):
vmsg(' Testing error handling for public key ops')
def gen1(): pubkey_gen(bytes(32), 1)
def gen2(): pubkey_gen(secp256k1_group_order.to_bytes(length=32, byteorder='big'), 1)