Browse Source

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

The MMGen Project 5 months ago
parent
commit
730a112f69
3 changed files with 272 additions and 1 deletions
  1. 173 0
      extmod/secp256k1mod.c
  2. 1 0
      setup.py
  3. 98 1
      test/modtest_d/ecc.py

+ 173 - 0
extmod/secp256k1mod.c

@@ -25,6 +25,7 @@
 #define PY_SSIZE_T_CLEAN
 #define PY_SSIZE_T_CLEAN
 #include <Python.h>
 #include <Python.h>
 #include <secp256k1.h>
 #include <secp256k1.h>
+#include <secp256k1_recovery.h>
 #include "random.h"
 #include "random.h"
 
 
 static secp256k1_context * create_context(
 static secp256k1_context * create_context(
@@ -196,6 +197,160 @@ static PyObject * pubkey_check(PyObject *self, PyObject *args) {
 	return Py_BuildValue("I", 1);
 	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 */
 /* https://docs.python.org/3/howto/cporting.html */
 
 
 struct module_state {
 struct module_state {
@@ -223,6 +378,24 @@ static PyMethodDef secp256k1_methods[] = {
 		METH_VARARGS,
 		METH_VARARGS,
 		"Check a serialized pubkey, ensuring the encoded point is not point-at-infinity"
 		"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}
 	{NULL, NULL}
 };
 };
 
 

+ 1 - 0
setup.py

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

+ 98 - 1
test/modtest_d/ecc.py

@@ -4,7 +4,16 @@
 test.modtest_d.ecc: elliptic curve unit test for the MMGen suite
 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.common import vmsg
 from ..include.ecc import pubkey_tweak_add_pyecdsa
 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
 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:
 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):
 	def pubkey_ops(self, name, ut):
 		vmsg('  Generating pubkey, adding scalar 123456789 to pubkey:')
 		vmsg('  Generating pubkey, adding scalar 123456789 to pubkey:')
 		pk_addend_bytes = int.to_bytes(123456789, length=32, byteorder='big')
 		pk_addend_bytes = int.to_bytes(123456789, length=32, byteorder='big')
@@ -43,6 +139,7 @@ class unit_tests:
 		return True
 		return True
 
 
 	def pubkey_errors(self, name, ut):
 	def pubkey_errors(self, name, ut):
+		vmsg('  Testing error handling for public key ops')
 
 
 		def gen1(): pubkey_gen(bytes(32), 1)
 		def gen1(): pubkey_gen(bytes(32), 1)
 		def gen2(): pubkey_gen(secp256k1_group_order.to_bytes(length=32, byteorder='big'), 1)
 		def gen2(): pubkey_gen(secp256k1_group_order.to_bytes(length=32, byteorder='big'), 1)