Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/electrum_ecc/ecc_fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ def load_library():
secp256k1.secp256k1_schnorrsig_sign32.argtypes = [c_void_p, POINTER(c_char), POINTER(c_char), POINTER(c_char), POINTER(c_char)]
secp256k1.secp256k1_schnorrsig_sign32.restype = c_int

secp256k1.secp256k1_schnorrsig_sign_custom.argtypes = [c_void_p, POINTER(c_char), POINTER(c_char), c_size_t, POINTER(c_char), c_void_p]
secp256k1.secp256k1_schnorrsig_sign_custom.restype = c_int

secp256k1.secp256k1_schnorrsig_verify.argtypes = [c_void_p, POINTER(c_char), POINTER(c_char), c_size_t, POINTER(c_char)]
secp256k1.secp256k1_schnorrsig_verify.restype = c_int
except (OSError, AttributeError):
Expand Down
60 changes: 40 additions & 20 deletions src/electrum_ecc/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,27 @@
import secrets
from typing import Union, Tuple, Optional, Callable
from ctypes import (
byref, c_char_p, c_size_t, create_string_buffer, cast,
byref, c_char_p, c_size_t, create_string_buffer, cast, Structure, c_ubyte, c_void_p,
)

from . import ecc_fast
from .ecc_fast import _libsecp256k1, SECP256K1_EC_UNCOMPRESSED, LibModuleMissing
from .ecdsa_sigformat import ecdsa_sig64_from_r_and_s


class _SchnorrsigExtraparams(Structure):
"""secp256k1_schnorrsig_extraparams: a NULL noncefp selects the BIP-340
nonce function, and ndata is then its 32 bytes of auxiliary randomness."""
_fields_ = [
("magic", c_ubyte * 4),
("noncefp", c_void_p),
("ndata", c_void_p),
]


_SCHNORRSIG_EXTRAPARAMS_MAGIC = (c_ubyte * 4)(0xDA, 0x6F, 0xB3, 0x8C)


def assert_bytes(x):
assert isinstance(x, (bytes, bytearray))

Expand Down Expand Up @@ -292,18 +305,20 @@ def ecdsa_verify(
return False
return True

def schnorr_verify(self, sig64: bytes, msg32: bytes) -> bool:
assert isinstance(sig64, bytes), type(sig64)
assert len(sig64) == 64, len(sig64)
assert isinstance(msg32, bytes), type(msg32)
assert len(msg32) == 32, len(msg32)
def schnorr_verify(self, sig64: bytes, msg: bytes) -> bool:
"""Verifies a BIP-340 schnorr signature over msg, a message of any length."""
if not isinstance(sig64, bytes):
raise TypeError(f"sig64 must be bytes, not {type(sig64).__name__}")
if len(sig64) != 64:
raise ValueError(f"sig64 must be 64 bytes, not {len(sig64)}")
if not isinstance(msg, bytes):
raise TypeError(f"msg must be bytes, not {type(msg).__name__}")
if not ecc_fast.HAS_SCHNORR:
raise LibModuleMissing(
'libsecp256k1 library found but it was built '
'without required modules (--enable-module-schnorrsig --enable-module-extrakeys)')
msglen = 32
pubkey = self._to_libsecp256k1_xonly_pubkey_ptr()
if 1 != _libsecp256k1.secp256k1_schnorrsig_verify(_libsecp256k1.ctx, sig64, msg32, msglen, pubkey):
if 1 != _libsecp256k1.secp256k1_schnorrsig_verify(_libsecp256k1.ctx, sig64, msg, len(msg), pubkey):
return False
return True

Expand Down Expand Up @@ -437,19 +452,21 @@ def sign_with_extra_entropy(extra_entropy):
sig = sigencode(r, s)
return sig

def schnorr_sign(self, msg32: bytes, *, aux_rand32: bytes = None) -> bytes:
"""Creates a BIP-340 schnorr signature for the given message (hash)
and using the optional auxiliary random data.
def schnorr_sign(self, msg: bytes, *, aux_rand32: bytes = None) -> bytes:
"""Creates a BIP-340 schnorr signature for msg, a message of any length,
using the optional auxiliary random data.

note: msg32 is supposed to be a 32 byte hash of the message to be signed.
The BIP recommends using bip340_tagged_hash for hashing the message.
note: the BIP recommends signing a 32 byte tagged hash of the message,
see bip340_tagged_hash.
"""
assert isinstance(msg32, bytes), type(msg32)
assert len(msg32) == 32, len(msg32)
if not isinstance(msg, bytes):
raise TypeError(f"msg must be bytes, not {type(msg).__name__}")
if aux_rand32 is None:
aux_rand32 = bytes(32)
assert isinstance(aux_rand32, bytes), type(aux_rand32)
assert len(aux_rand32) == 32, len(aux_rand32)
if not isinstance(aux_rand32, bytes):
raise TypeError(f"aux_rand32 must be bytes, not {type(aux_rand32).__name__}")
if len(aux_rand32) != 32:
raise ValueError(f"aux_rand32 must be 32 bytes, not {len(aux_rand32)}")
if not ecc_fast.HAS_SCHNORR:
raise LibModuleMissing(
'libsecp256k1 library found but it was built '
Expand All @@ -462,12 +479,15 @@ def schnorr_sign(self, msg32: bytes, *, aux_rand32: bytes = None) -> bytes:
raise Exception('secret key was invalid')
# sign msg and verify sig
sig64 = create_string_buffer(64)
ret = _libsecp256k1.secp256k1_schnorrsig_sign32(
_libsecp256k1.ctx, sig64, msg32, keypair, aux_rand32)
aux_buf = create_string_buffer(aux_rand32, 32)
extraparams = _SchnorrsigExtraparams(
_SCHNORRSIG_EXTRAPARAMS_MAGIC, None, cast(aux_buf, c_void_p))
ret = _libsecp256k1.secp256k1_schnorrsig_sign_custom(
_libsecp256k1.ctx, sig64, msg, len(msg), keypair, byref(extraparams))
sig64 = bytes(sig64)
if 1 != ret:
raise Exception('signing failure')
if not self.schnorr_verify(sig64, msg32):
if not self.schnorr_verify(sig64, msg):
raise Exception("sanity check failed: signature we just created does not verify!")
return sig64

Expand Down
21 changes: 21 additions & 0 deletions tests/test_schnorr.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ def test_vectors_from_bip0340(self):
12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size
13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order
14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size
15,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,,71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63,TRUE,message of size 0 (added 2022-12)
16,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,11,08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF,TRUE,message of size 1 (added 2022-12)
17,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,0102030405060708090A0B0C0D0E0F1011,5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5,TRUE,message of size 17 (added 2022-12)
18,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999,403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367,TRUE,message of size 100 (added 2022-12)
"""
with io.StringIO(bip0340_vectors) as f:
csvreader = csv.reader(f)
Expand All @@ -54,6 +58,23 @@ def test_vectors_from_bip0340(self):
expected_res = True if expected_res == "TRUE" else False
self.assertEqual(expected_res, is_sig_good)

def test_schnorr_rejects_bad_lengths(self):
seckey = ECPrivkey(bytes.fromhex("B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF"))
msg = sha256(b"hello there")
sig = seckey.schnorr_sign(msg)
with self.assertRaises(ValueError):
seckey.schnorr_sign(msg, aux_rand32=bytes(31))
with self.assertRaises(ValueError):
seckey.schnorr_verify(sig[:63], msg)
with self.assertRaises(TypeError):
seckey.schnorr_sign(msg.hex())
with self.assertRaises(TypeError):
seckey.schnorr_verify(sig, msg.hex())
# a message of another length is a different message, not a truncation
self.assertTrue(seckey.schnorr_verify(sig, msg))
self.assertFalse(seckey.schnorr_verify(sig, msg[:31]))
self.assertFalse(seckey.schnorr_verify(sig, msg + b"\x00"))

def test_sign_schnorr_aux_rand(self):
seckey = ECPrivkey(bytes.fromhex("B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF"))
msg32 = sha256(b"hello there")
Expand Down