From 0c94427bcdabd2cc45a41cc3c499178095d06913 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Tue, 12 Dec 2023 10:19:53 +0000 Subject: [PATCH] secp256k1 extmod: add `pubkey_tweak_add()`, `pubkey_check()` functions Testing: $ test/unit_tests.py -v ecc --- extmod/secp256k1mod.c | 94 +++++++++++++++++++++++++++++++++++++ mmgen/data/version | 2 +- test/include/ecc.py | 43 +++++++++++++++++ test/unit_tests_d/ut_ecc.py | 84 +++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 1 deletion(-) create mode 100755 test/include/ecc.py create mode 100755 test/unit_tests_d/ut_ecc.py diff --git a/extmod/secp256k1mod.c b/extmod/secp256k1mod.c index aead92fb..51f628be 100755 --- a/extmod/secp256k1mod.c +++ b/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} }; diff --git a/mmgen/data/version b/mmgen/data/version index 2381aa2a..87676975 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -14.1.dev1 +14.1.dev2 diff --git a/test/include/ecc.py b/test/include/ecc.py new file mode 100755 index 00000000..4f6e9ea0 --- /dev/null +++ b/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 +# 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) diff --git a/test/unit_tests_d/ut_ecc.py b/test/unit_tests_d/ut_ecc.py new file mode 100755 index 00000000..5e437db8 --- /dev/null +++ b/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