secp256k1 extmod: add pubkey_tweak_add(), pubkey_check() functions
Testing:
$ test/unit_tests.py -v ecc
This commit is contained in:
parent
21a6e95a10
commit
0c94427bcd
4 changed files with 222 additions and 1 deletions
|
|
@ -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 @@
|
|||
14.1.dev1
|
||||
14.1.dev2
|
||||
|
|
|
|||
43
test/include/ecc.py
Executable file
43
test/include/ecc.py
Executable 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
84
test/unit_tests_d/ut_ecc.py
Executable 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue