Browse Source

secp256k1 extmod: add `pubkey_tweak_add()`, `pubkey_check()` functions

Testing:

    $ test/unit_tests.py -v ecc
The MMGen Project 11 months ago
parent
commit
0c94427bcd
4 changed files with 222 additions and 1 deletions
  1. 94 0
      extmod/secp256k1mod.c
  2. 1 1
      mmgen/data/version
  3. 43 0
      test/include/ecc.py
  4. 84 0
      test/unit_tests_d/ut_ecc.py

+ 94 - 0
extmod/secp256k1mod.c

@@ -41,6 +41,38 @@ int privkey_check(
 	return 1;
 }
 
+int pubkey_parse_with_check(
+		const secp256k1_context * ctx,
+		secp256k1_pubkey *        pubkey_ptr,
+		const unsigned char *     pubkey_bytes,
+		const Py_ssize_t          pubkey_bytes_len
+	) {
+	if (ctx == NULL) {
+		PyErr_SetString(PyExc_RuntimeError, "Context initialization failed");
+		return 0;
+	}
+	if (pubkey_bytes_len == 33) {
+		if (pubkey_bytes[0] != 3 && pubkey_bytes[0] != 2) {
+			PyErr_SetString(PyExc_ValueError, "Invalid first byte for serialized compressed public key");
+			return 0;
+		}
+	} else if (pubkey_bytes_len == 65) {
+		if (pubkey_bytes[0] != 4) {
+			PyErr_SetString(PyExc_ValueError, "Invalid first byte for serialized uncompressed public key");
+			return 0;
+		}
+	} else {
+		PyErr_SetString(PyExc_ValueError, "Serialized public key length not 33 or 65 bytes");
+		return 0;
+	}
+	/* checks for point-at-infinity (via secp256k1_pubkey_save) */
+	if (secp256k1_ec_pubkey_parse(ctx, pubkey_ptr, pubkey_bytes, pubkey_bytes_len) != 1) {
+		PyErr_SetString(PyExc_ValueError, "Public key could not be parsed or encodes point-at-infinity");
+		return 0;
+	}
+	return 1;
+}
+
 static PyObject * pubkey_gen(PyObject *self, PyObject *args) {
 	const unsigned char * privkey_bytes;
 	const Py_ssize_t privkey_bytes_len;
@@ -72,6 +104,56 @@ static PyObject * pubkey_gen(PyObject *self, PyObject *args) {
 	return Py_BuildValue("y#", pubkey_bytes, pubkey_bytes_len);
 }
 
+static PyObject * pubkey_tweak_add(PyObject *self, PyObject *args) {
+	const unsigned char * pubkey_bytes;
+	const unsigned char * tweak_bytes;
+	const Py_ssize_t pubkey_bytes_len;
+	const Py_ssize_t tweak_bytes_len;
+	if (!PyArg_ParseTuple(args, "y#y#", &pubkey_bytes, &pubkey_bytes_len, &tweak_bytes, &tweak_bytes_len)) {
+		PyErr_SetString(PyExc_ValueError, "Unable to parse extension mod arguments");
+		return NULL;
+	}
+	secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
+	secp256k1_pubkey pubkey;
+	if (!pubkey_parse_with_check(ctx, &pubkey, pubkey_bytes, pubkey_bytes_len)) {
+		return NULL;
+	}
+	if (!privkey_check(ctx, tweak_bytes, tweak_bytes_len, "Tweak")) {
+		return NULL;
+	}
+	/* checks for point-at-infinity (via secp256k1_pubkey_save) */
+	if (secp256k1_ec_pubkey_tweak_add(ctx, &pubkey, tweak_bytes) != 1) {
+		PyErr_SetString(PyExc_RuntimeError, "Adding public key points failed or result was point-at-infinity");
+		return NULL;
+	}
+	unsigned char new_pubkey_bytes[pubkey_bytes_len];
+	if (secp256k1_ec_pubkey_serialize(
+			ctx,
+			new_pubkey_bytes,
+			(size_t*) &pubkey_bytes_len,
+			&pubkey,
+			pubkey_bytes_len == 33 ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED) != 1) {
+		PyErr_SetString(PyExc_RuntimeError, "Public key serialization failed");
+		return NULL;
+	}
+	return Py_BuildValue("y#", new_pubkey_bytes, pubkey_bytes_len);
+}
+
+static PyObject * pubkey_check(PyObject *self, PyObject *args) {
+	const unsigned char * pubkey_bytes;
+	const Py_ssize_t pubkey_bytes_len;
+	if (!PyArg_ParseTuple(args, "y#", &pubkey_bytes, &pubkey_bytes_len)) {
+		PyErr_SetString(PyExc_ValueError, "Unable to parse extension mod arguments");
+		return NULL;
+	}
+	secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
+	secp256k1_pubkey pubkey;
+	if (!pubkey_parse_with_check(ctx, &pubkey, pubkey_bytes, pubkey_bytes_len)) {
+		return NULL;
+	}
+	return Py_BuildValue("I", 1);
+}
+
 /* https://docs.python.org/3/howto/cporting.html */
 
 struct module_state {
@@ -87,6 +169,18 @@ static PyMethodDef secp256k1_methods[] = {
 		METH_VARARGS,
 		"Generate a serialized pubkey from privkey bytes"
 	},
+	{
+		"pubkey_tweak_add",
+		pubkey_tweak_add,
+		METH_VARARGS,
+		"Add scalar bytes to a serialized pubkey, returning a serialized pubkey"
+	},
+	{
+		"pubkey_check",
+		pubkey_check,
+		METH_VARARGS,
+		"Check a serialized pubkey, ensuring the encoded point is not point-at-infinity"
+	},
     {NULL, NULL}
 };
 

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-14.1.dev1
+14.1.dev2

+ 43 - 0
test/include/ecc.py

@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2023 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+test.include.ecc: elliptic curve utilities for the MMGen test suite
+"""
+
+import ecdsa
+from mmgen.proto.secp256k1.keygen import pubkey_format
+
+def _pubkey_to_pub_point(vk_bytes):
+	try:
+		return ecdsa.VerifyingKey.from_string(vk_bytes, curve=ecdsa.curves.SECP256k1).pubkey.point
+	except Exception as e:
+		raise ValueError(f'invalid pubkey {vk_bytes.hex()}\n    {type(e).__name__}: {e}')
+
+def _check_pub_point(pub_point,vk_bytes,addend_bytes=None):
+	if pub_point is ecdsa.ellipticcurve.INFINITY:
+		raise ValueError(
+			'pubkey {}{} produced key with point at infinity!'.format(
+				vk_bytes.hex(),
+				'' if addend_bytes is None else f' + {addend_bytes.hex()}'))
+
+def pubkey_check_py_ecdsa(vk_bytes):
+	_check_pub_point(_pubkey_to_pub_point(vk_bytes), vk_bytes)
+
+def pubkey_tweak_add_py_ecdsa(vk_bytes,pk_addend_bytes):
+	pk_addend = int.from_bytes(pk_addend_bytes)
+	point_sum = (
+		_pubkey_to_pub_point(vk_bytes) +
+		ecdsa.SigningKey.from_secret_exponent(pk_addend, curve=ecdsa.SECP256k1).verifying_key.pubkey.point
+	)
+	_check_pub_point(point_sum, vk_bytes, pk_addend_bytes)
+	return pubkey_format(
+		ecdsa.VerifyingKey.from_public_point(point_sum, curve=ecdsa.curves.SECP256k1).to_string(),
+		compressed = len(vk_bytes) == 33)

+ 84 - 0
test/unit_tests_d/ut_ecc.py

@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+
+"""
+test.unit_tests_d.ut_ecc: elliptic curve unit test for the MMGen suite
+"""
+
+from mmgen.color import gray,pink,blue
+from mmgen.proto.secp256k1.secp256k1 import pubkey_gen,pubkey_tweak_add,pubkey_check
+
+from ..include.common import cfg,qmsg,vmsg
+from ..include.ecc import pubkey_tweak_add_py_ecdsa
+from mmgen.protocol import CoinProtocol
+
+secp256k1_group_order = CoinProtocol.Secp256k1.secp256k1_group_order
+
+class unit_tests:
+
+	def pubkey_ops(self,name,ut):
+		vmsg(f'  Generating pubkey, adding scalar 123456789 to pubkey:')
+		pk_addend_bytes = int.to_bytes(123456789,length=32)
+
+		for privkey in (
+				'beadcafe' * 8,
+				f'{1:064x}',
+				f'{secp256k1_group_order-1:x}',
+			):
+			vmsg(f'  privkey = 0x{privkey}')
+			for compressed,length in ((False,65),(True,33)):
+				vmsg(f'    {compressed=}')
+				pubkey_bytes = pubkey_gen(bytes.fromhex(privkey),int(compressed))
+				pubkey_check(pubkey_bytes)
+				vmsg(f'      pubkey:  {pubkey_bytes.hex()}')
+
+				res1 = pubkey_tweak_add(pubkey_bytes, pk_addend_bytes)
+				pubkey_check(res1)
+				vmsg(f'      tweaked: {res1.hex()}')
+
+				res2 = pubkey_tweak_add_py_ecdsa(pubkey_bytes,pk_addend_bytes)
+				pubkey_check(res2)
+
+				assert len(res1) == length
+				assert res1 == res2
+
+		return True
+
+	def pubkey_errors(self,name,ut):
+
+		def gen1(): pubkey_gen(bytes(32),1)
+		def gen2(): pubkey_gen(secp256k1_group_order.to_bytes(length=32),1)
+		def gen3(): pubkey_gen((secp256k1_group_order+1).to_bytes(length=32),1)
+		def gen4(): pubkey_gen(bytes.fromhex('ff'*32),1)
+		def gen5(): pubkey_gen(bytes.fromhex('ab'*31),1)
+		def gen6(): pubkey_gen(bytes.fromhex('ab'*33),1)
+
+		pubkey_bytes = pubkey_gen(bytes.fromhex('beadcafe'*8), 1)
+		def tweak1(): pubkey_tweak_add(pubkey_bytes,bytes(32))
+		def tweak2(): pubkey_tweak_add(bytes.fromhex('03'*64),int.to_bytes(1,length=32))
+
+		def check1(): pubkey_check(bytes.fromhex('04'*33))
+		def check2(): pubkey_check(bytes.fromhex('03'*65))
+		def check3(): pubkey_check(bytes.fromhex('02'*65))
+		def check4(): pubkey_check(bytes.fromhex('03'*64))
+		def check5(): pubkey_check(b'')
+
+		bad_data = (
+			('privkey == 0',              'ValueError', 'Private key not in allowable range', gen1),
+			('privkey == group order',    'ValueError', 'Private key not in allowable range', gen2),
+			('privkey == group order+1',  'ValueError', 'Private key not in allowable range', gen3),
+			('privkey == 2^256-1',        'ValueError', 'Private key not in allowable range', gen4),
+			('len(privkey) == 31',        'ValueError', 'Private key length not 32 bytes',    gen5),
+			('len(privkey) == 33',        'ValueError', 'Private key length not 32 bytes',    gen6),
+
+			('tweak == 0',                'ValueError', 'Tweak not in allowable range',       tweak1),
+			('pubkey length == 64',       'ValueError', 'Serialized public key length not',   tweak2),
+
+			('invalid pubkey (33 bytes)', 'ValueError', 'Invalid first byte',                 check1),
+			('invalid pubkey (65 bytes)', 'ValueError', 'Invalid first byte',                 check2),
+			('invalid pubkey (65 bytes)', 'ValueError', 'Invalid first byte',                 check3),
+			('pubkey length == 64',       'ValueError', 'Serialized public key length not',   check4),
+			('pubkey length == 0',        'ValueError', 'Serialized public key length not',   check5),
+		)
+
+		ut.process_bad_data(bad_data,pfx='')
+		return True