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

Testing:

    $ test/unit_tests.py -v ecc
This commit is contained in:
The MMGen Project 2023-12-12 10:19:53 +00:00
commit 0c94427bcd
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
4 changed files with 222 additions and 1 deletions

View file

@ -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}
};

View file

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

43
test/include/ecc.py Executable file
View file

@ -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
test/unit_tests_d/ut_ecc.py Executable file
View file

@ -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