extmod/secp256k1mod.c: add sign, verify, pubkey recover funcs
This commit is contained in:
parent
fbeda2f071
commit
730a112f69
3 changed files with 272 additions and 1 deletions
|
|
@ -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}
|
||||
};
|
||||
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -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 [],
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue