Skip to content

Commit

Permalink
Allow to grind ECDSA signature to have low-R
Browse files Browse the repository at this point in the history
  • Loading branch information
dgpv committed Dec 4, 2020
1 parent 40233aa commit db9933c
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 12 deletions.
59 changes: 52 additions & 7 deletions bitcointx/core/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ def load_openssl_library(path: Optional[str] = None) -> Optional[ctypes.CDLL]:
return handle


def _raw_sig_has_low_r(raw_sig: bytes) -> bool:
compact_sig = ctypes.create_string_buffer(64)
result = _secp256k1.secp256k1_ecdsa_signature_serialize_compact(
secp256k1_context_sign, compact_sig, raw_sig)
assert result == 1

# In DER serialization, all values are interpreted as big-endian,
# signed integers. The highest bit in the integer indicates
# its signed-ness; 0 is positive, 1 is negative.
# When the value is interpreted as a negative integer,
# it must be converted to a positive value by prepending a 0x00 byte
# so that the highest bit is 0. We can avoid this prepending by ensuring
# that our highest bit is always 0, and thus we must check that the
# first byte is less than 0x80.
return compact_sig.raw[0] < 0x80


class CKeyBase:
"""An encapsulated private key
Expand Down Expand Up @@ -185,25 +202,53 @@ def secret_bytes(self) -> bytes:
def pub(self) -> 'CPubKey':
return self.__pub

def sign(self, hash: Union[bytes, bytearray]) -> bytes:
def sign(self, hash: Union[bytes, bytearray], *,
_ecdsa_sig_grind_low_r: bool = True,
_ecdsa_sig_extra_entropy: int = 0
) -> bytes:

ensure_isinstance(hash, (bytes, bytearray), 'hash')
if len(hash) != 32:
raise ValueError('Hash must be exactly 32 bytes long')

raw_sig = ctypes.create_string_buffer(64)
result = _secp256k1.secp256k1_ecdsa_sign(
secp256k1_context_sign, raw_sig, hash, self.secret_bytes, None, None)
if 1 != result:
assert(result == 0)
raise RuntimeError('secp256k1_ecdsa_sign returned failure')

if _ecdsa_sig_grind_low_r:
counter = 0
else:
counter = _ecdsa_sig_extra_entropy

def maybe_extra_entropy() -> Optional[bytes]:
if counter == 0:
return None

# mimic Bitcoin Core that uses 32-bit counter for the entropy
assert counter < 2**32
return counter.to_bytes(4, byteorder="little") + b'\x00'*28

while True:
result = _secp256k1.secp256k1_ecdsa_sign(
secp256k1_context_sign, raw_sig, hash, self.secret_bytes, None,
maybe_extra_entropy())
if 1 != result:
assert(result == 0)
raise RuntimeError('secp256k1_ecdsa_sign returned failure')

if not _ecdsa_sig_grind_low_r or _raw_sig_has_low_r(raw_sig.raw):
break

counter += 1

sig_size0 = ctypes.c_size_t()
sig_size0.value = SIGNATURE_SIZE
mb_sig = ctypes.create_string_buffer(sig_size0.value)
mb_sig = ctypes.create_string_buffer(SIGNATURE_SIZE)

result = _secp256k1.secp256k1_ecdsa_signature_serialize_der(
secp256k1_context_sign, mb_sig, ctypes.byref(sig_size0), raw_sig)
if 1 != result:
assert(result == 0)
raise RuntimeError('secp256k1_ecdsa_signature_parse_der returned failure')

# secp256k1 creates signatures already in lower-S form, no further
# conversion needed.
return mb_sig.raw[:sig_size0.value]
Expand Down
3 changes: 3 additions & 0 deletions bitcointx/core/secp256k1.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ def _add_function_definitions(_secp256k1: ctypes.CDLL) -> None:
_secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact.restype = ctypes.c_int
_secp256k1.secp256k1_ecdsa_recoverable_signature_serialize_compact.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int), ctypes.c_char_p]

_secp256k1.secp256k1_ecdsa_signature_serialize_compact.restype = ctypes.c_int
_secp256k1.secp256k1_ecdsa_signature_serialize_compact.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]

_secp256k1.secp256k1_ecdsa_recover.restype = ctypes.c_int
_secp256k1.secp256k1_ecdsa_recover.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p]

Expand Down
38 changes: 38 additions & 0 deletions bitcointx/tests/test_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import unittest
import warnings
import hashlib

from bitcointx.core.key import CKey, CPubKey
from bitcointx.core import x
Expand Down Expand Up @@ -111,3 +112,40 @@ def test_invalid_key(self) -> None:

with self.assertRaises(ValueError):
CKey(b'\xff'*32)

def test_signature_grind(self) -> None:
k = CKey(x('12b004fff7f4b69ef8650e767f18f11ede158148b425660723b9f9a66e61f747'))

msg = "A message to be signed"
msg_hash = hashlib.sha256(msg.encode('ascii')).digest()

# When explicit entropy is specified, we should see at least one
# high R signature within 20 signatures
high_r_found = False
for i in range(1, 21):
sig = k.sign(msg_hash,
_ecdsa_sig_grind_low_r=False,
_ecdsa_sig_extra_entropy=i)
if sig[3] == 0x21:
self.assertEqual(sig[4], 0x00)
high_r_found = True
break

self.assertTrue(high_r_found)

# When grinding for low-R, we should always see low R signatures
# that are less than 70 bytes in 256 tries
# We should see at least one signature that is less than 70 bytes.
small_sig_found = False
for i in range(256):
msg = "A message to be signed" + str(i)
msg_hash = hashlib.sha256(msg.encode('ascii')).digest()
sig = k.sign(msg_hash)

self.assertLessEqual(len(sig), 70)
self.assertLessEqual(sig[3], 0x20)

if len(sig) < 70:
small_sig_found = True

self.assertTrue(small_sig_found)
Loading

0 comments on commit db9933c

Please sign in to comment.