Skip to content

Commit

Permalink
Grind for low R when signing (#14)
Browse files Browse the repository at this point in the history
* grid low R by default
* fix bip39 test for micropython
  • Loading branch information
stepansnigirev authored Jul 7, 2021
1 parent 1aa94cd commit 5abcd6f
Show file tree
Hide file tree
Showing 8 changed files with 53 additions and 26 deletions.
39 changes: 23 additions & 16 deletions src/embit/ec.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
class ECError(EmbitError):
pass

class Signature(EmbitBase):
def __init__(self, sig):
self._sig = sig

def write_to(self, stream) -> int:
return stream.write(secp256k1.ecdsa_signature_serialize_der(self._sig))

@classmethod
def read_from(cls, stream):
der = stream.read(2)
der += stream.read(der[1])
return cls(secp256k1.ecdsa_signature_parse_der(der))

class PublicKey(EmbitKey):
def __init__(self, point: bytes, compressed: bool = True):
Expand Down Expand Up @@ -140,8 +152,17 @@ def from_base58(cls, s):
def get_public_key(self):
return PublicKey(secp256k1.ec_pubkey_create(self._secret), self.compressed)

def sign(self, msg_hash):
return Signature(secp256k1.ecdsa_sign(msg_hash, self._secret))
def sign(self, msg_hash, grind=True):
sig = Signature(secp256k1.ecdsa_sign(msg_hash, self._secret))
if grind:
counter = 1
while len(sig.serialize()) > 70:
sig = Signature(secp256k1.ecdsa_sign(msg_hash, self._secret, None, counter.to_bytes(32, 'little')))
counter += 1
# just in case we get in infinite loop for some reason
if counter > 200:
break
return sig

def write_to(self, stream) -> int:
# return a copy of the secret
Expand All @@ -155,17 +176,3 @@ def read_from(cls, stream):
@property
def is_private(self) -> bool:
return True


class Signature(EmbitBase):
def __init__(self, sig):
self._sig = sig

def write_to(self, stream) -> int:
return stream.write(secp256k1.ecdsa_signature_serialize_der(self._sig))

@classmethod
def read_from(cls, stream):
der = stream.read(2)
der += stream.read(der[1])
return cls(secp256k1.ecdsa_signature_parse_der(der))
8 changes: 5 additions & 3 deletions src/embit/util/ctypes_secp256k1.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def _init(flags=(CONTEXT_SIGN | CONTEXT_VERIFY)):
c_char_p,
c_char_p,
c_void_p,
c_void_p,
c_char_p,
]
secp256k1.secp256k1_ecdsa_sign.restype = c_int

Expand Down Expand Up @@ -448,13 +448,15 @@ def ecdsa_verify(sig, msg, pub, context=_secp.ctx):
return bool(r)


def ecdsa_sign(msg, secret, context=_secp.ctx):
def ecdsa_sign(msg, secret, nonce_function=None, extra_data=None, context=_secp.ctx):
if len(msg) != 32:
raise ValueError("Message should be 32 bytes long")
if len(secret) != 32:
raise ValueError("Secret key should be 32 bytes long")
if extra_data and len(extra_data) != 32:
raise ValueError("Extra data should be 32 bytes long")
sig = bytes(64)
r = _secp.secp256k1_ecdsa_sign(context, sig, msg, secret, None, None)
r = _secp.secp256k1_ecdsa_sign(context, sig, msg, secret, nonce_function, extra_data)
if r == 0:
raise ValueError("Failed to sign")
return sig
Expand Down
10 changes: 7 additions & 3 deletions src/embit/util/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,14 +400,16 @@ def get_pubkey(self):
ret.compressed = self.compressed
return ret

def sign_ecdsa(self, msg, low_s=True):
def sign_ecdsa(self, msg, nonce_function=None, extra_data=None, low_s=True):
"""Construct a DER-encoded ECDSA signature with this key.
See https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm for the
ECDSA signer algorithm."""
assert self.valid
z = int.from_bytes(msg, "big")
k = deterministic_k(self.secret, z)
if nonce_function is None:
nonce_function = deterministic_k
k = nonce_function(self.secret, z, extra_data=extra_data)
R = SECP256K1.affine(SECP256K1.mul([(SECP256K1_G, k)]))
r = R[0] % SECP256K1_ORDER
s = (modinv(k, SECP256K1_ORDER) * (z + self.secret * r)) % SECP256K1_ORDER
Expand All @@ -427,14 +429,16 @@ def sign_ecdsa(self, msg, low_s=True):
)


def deterministic_k(secret, z):
def deterministic_k(secret, z, extra_data=None):
# RFC6979, optimized for secp256k1
k = b"\x00" * 32
v = b"\x01" * 32
if z > SECP256K1_ORDER:
z -= SECP256K1_ORDER
z_bytes = z.to_bytes(32, "big")
secret_bytes = secret.to_bytes(32, "big")
if extra_data is not None:
z_bytes += extra_data
k = hmac.new(k, v + b"\x00" + secret_bytes + z_bytes, "sha256").digest()
v = hmac.new(k, v, "sha256").digest()
k = hmac.new(k, v + b"\x01" + secret_bytes + z_bytes, "sha256").digest()
Expand Down
4 changes: 2 additions & 2 deletions src/embit/util/py_secp256k1.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,14 @@ def ecdsa_verify(sig, msg, pub, context=None):
return pubkey.verify_ecdsa(ecdsa_signature_serialize_der(sig), msg)


def ecdsa_sign(msg, secret, context=None):
def ecdsa_sign(msg, secret, nonce_function=None, extra_data=None, context=None):
if len(msg) != 32:
raise ValueError("Message should be 32 bytes long")
if len(secret) != 32:
raise ValueError("Secret key should be 32 bytes long")
pk = _key.ECKey()
pk.set(secret, False)
sig = pk.sign_ecdsa(msg)
sig = pk.sign_ecdsa(msg, nonce_function, extra_data)
return ecdsa_signature_parse_der(sig)


Expand Down
5 changes: 5 additions & 0 deletions tests/tests/test_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ def test_cross(self):
msg = b"7" * 32
sig = ctypes_secp256k1.ecdsa_sign(msg, secret)
self.assertEqual(sig, py_secp256k1.ecdsa_sign(msg, secret))

# check that extra data is handled in the same way
sig = ctypes_secp256k1.ecdsa_sign(msg, secret, None, b"1"*32)
self.assertEqual(sig, py_secp256k1.ecdsa_sign(msg, secret, None, b"1"*32))

compact = py_secp256k1.ecdsa_signature_serialize_compact(sig)
der = py_secp256k1.ecdsa_signature_serialize_der(sig)
self.assertEqual(
Expand Down
2 changes: 1 addition & 1 deletion tests/tests/test_bip39.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ def test_alternate_wordlist(self):
for mnemonic, expected_seed in mnemonics:
self.assertTrue(mnemonic_is_valid(mnemonic, wordlist=ES_WORDLIST))

self.assertEqual(mnemonic_to_seed(mnemonic, wordlist=ES_WORDLIST).hex(), expected_seed)
self.assertEqual(hexlify(mnemonic_to_seed(mnemonic, wordlist=ES_WORDLIST)).decode(), expected_seed)

self.assertEqual(
mnemonic_from_bytes(
Expand Down
9 changes: 9 additions & 0 deletions tests/tests/test_ecc.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ def test_identity(self):
g_hex = hexlify(der)
self.assertEqual(answer, g_hex)

def test_grind(self):
pk = PrivateKey(b"1"*32)
msgs = [ bytes([i]*32) for i in range(255) ]
for msg in msgs:
sig = pk.sign(msg)
der = sig.serialize()
# low R is grinded
self.assertTrue(len(der)<=70)

def test_pubkeys(self):
valid_keys = [
(
Expand Down
2 changes: 1 addition & 1 deletion tests/tests/test_psbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def test_sign(self):
exp_partial_sigs = [
{
"029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f": "3044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01",
"02dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7": "3045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01",
"02dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7": "30440220631a989fe738a92ad01986023312c19214fe2802b39e5cbc1ac3678806c692c3022039db6c387bd267716dfdb3d4d8da50b8e85d213326ba7c7daaa4c0ce41eb922301",
},
{
"03089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc": "3044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01",
Expand Down

0 comments on commit 5abcd6f

Please sign in to comment.