diff --git a/extmod/secp256k1mod.c b/extmod/secp256k1mod.c index d5c350a9..8fdea24c 100755 --- a/extmod/secp256k1mod.c +++ b/extmod/secp256k1mod.c @@ -25,6 +25,7 @@ #define PY_SSIZE_T_CLEAN #include #include +#include #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} }; diff --git a/setup.py b/setup.py index 510f942d..83d77cb5 100755 --- a/setup.py +++ b/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 [], diff --git a/test/modtest_d/ecc.py b/test/modtest_d/ecc.py index f504a041..46a7cb8e 100755 --- a/test/modtest_d/ecc.py +++ b/test/modtest_d/ecc.py @@ -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)