Skip to content

Commit 8e2920e

Browse files
committed
Merge pull request #53
f6e8dc9 Generate low-S DER signatures (Ross Nicoll) 4073127 Added more unit tests on hash signing. (Ross Nicoll)
2 parents e479589 + f6e8dc9 commit 8e2920e

4 files changed

Lines changed: 116 additions & 4 deletions

File tree

bitcoin/core/key.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import hashlib
2222
import sys
2323

24+
import bitcoin.core.script
25+
2426
_ssl = ctypes.cdll.LoadLibrary(ctypes.util.find_library('ssl') or 'libeay32')
2527

2628
# this specifies the curve used with ECDSA.
@@ -36,6 +38,11 @@ def _check_result (val, func, args):
3638
_ssl.EC_KEY_new_by_curve_name.restype = ctypes.c_void_p
3739
_ssl.EC_KEY_new_by_curve_name.errcheck = _check_result
3840

41+
# From openssl/ecdsa.h
42+
class ECDSA_SIG_st(ctypes.Structure):
43+
_fields_ = [("r", ctypes.c_void_p),
44+
("s", ctypes.c_void_p)]
45+
3946
class CECKey:
4047
"""Wrapper around OpenSSL's EC_KEY"""
4148

@@ -99,7 +106,6 @@ def get_ecdh_key(self, other_pubkey, kdf=lambda k: hashlib.sha256(k).digest()):
99106
return kdf(r)
100107

101108
def sign(self, hash):
102-
# FIXME: need unit tests for below cases
103109
if not isinstance(hash, bytes):
104110
raise TypeError('Hash must be bytes instance; got %r' % hash.__class__)
105111
if len(hash) != 32:
@@ -110,7 +116,39 @@ def sign(self, hash):
110116
mb_sig = ctypes.create_string_buffer(sig_size0.value)
111117
result = _ssl.ECDSA_sign(0, hash, len(hash), mb_sig, ctypes.byref(sig_size0), self.k)
112118
assert 1 == result
113-
return mb_sig.raw[:sig_size0.value]
119+
if bitcoin.core.script.IsLowDERSignature(mb_sig.raw[:sig_size0.value]):
120+
return mb_sig.raw[:sig_size0.value]
121+
else:
122+
return self.signature_to_low_s(mb_sig.raw[:sig_size0.value])
123+
124+
def signature_to_low_s(self, sig):
125+
der_sig = ECDSA_SIG_st()
126+
_ssl.d2i_ECDSA_SIG(ctypes.byref(ctypes.pointer(der_sig)), ctypes.byref(ctypes.c_char_p(sig)), len(sig))
127+
group = _ssl.EC_KEY_get0_group(self.k)
128+
order = _ssl.BN_new()
129+
halforder = _ssl.BN_new()
130+
ctx = _ssl.BN_CTX_new()
131+
_ssl.EC_GROUP_get_order(group, order, ctx)
132+
_ssl.BN_rshift1(halforder, order)
133+
134+
# Verify that s is over half the order of the curve before we actually subtract anything from it
135+
if _ssl.BN_cmp(der_sig.s, halforder) > 0:
136+
_ssl.BN_sub(der_sig.s, order, der_sig.s)
137+
138+
_ssl.BN_free(halforder)
139+
_ssl.BN_free(order)
140+
_ssl.BN_CTX_free(ctx)
141+
142+
derlen = _ssl.i2d_ECDSA_SIG(ctypes.pointer(der_sig), 0)
143+
if derlen == 0:
144+
_ssl.ECDSA_SIG_free(der_sig)
145+
return None
146+
new_sig = ctypes.create_string_buffer(derlen)
147+
_ssl.i2d_ECDSA_SIG(ctypes.pointer(der_sig), ctypes.byref(ctypes.pointer(new_sig)))
148+
_ssl.BN_free(der_sig.r)
149+
_ssl.BN_free(der_sig.s)
150+
151+
return new_sig.raw
114152

115153
def verify(self, hash, sig):
116154
"""Verify a DER signature"""

bitcoin/core/script.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
_bchr = lambda x: bytes([x])
2626
_bord = lambda x: x
2727

28+
import array
2829
import copy
2930
import struct
3031

@@ -797,6 +798,57 @@ def FindAndDelete(script, sig):
797798
r += script[last_sop_idx:]
798799
return CScript(r)
799800

801+
def IsLowDERSignature(sig):
802+
"""
803+
Loosely correlates with IsLowDERSignature() from script/interpreter.cpp
804+
Verifies that the S value in a DER signature is the lowest possible value.
805+
Used by BIP62 malleability fixes.
806+
"""
807+
length_r = sig[3]
808+
if isinstance(length_r, str):
809+
length_r = int(struct.unpack('B', length_r)[0])
810+
length_s = sig[5 + length_r]
811+
if isinstance(length_s, str):
812+
length_s = int(struct.unpack('B', length_s)[0])
813+
s_val = list(struct.unpack(str(length_s) + 'B', sig[6 + length_r:6 + length_r + length_s]))
814+
815+
# If the S value is above the order of the curve divided by two, its
816+
# complement modulo the order could have been used instead, which is
817+
# one byte shorter when encoded correctly.
818+
max_mod_half_order = [
819+
0x7f,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
820+
0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
821+
0x5d,0x57,0x6e,0x73,0x57,0xa4,0x50,0x1d,
822+
0xdf,0xe9,0x2f,0x46,0x68,0x1b,0x20,0xa0]
823+
824+
return CompareBigEndian(s_val, [0]) > 0 and \
825+
CompareBigEndian(s_val, max_mod_half_order) <= 0
826+
827+
def CompareBigEndian(c1, c2):
828+
"""
829+
Loosely matches CompareBigEndian() from eccryptoverify.cpp
830+
Compares two arrays of bytes, and returns a negative value if the first is
831+
less than the second, 0 if they're equal, and a positive value if the
832+
first is greater than the second.
833+
"""
834+
c1 = list(c1)
835+
c2 = list(c2)
836+
837+
# Adjust starting positions until remaining lengths of the two arrays match
838+
while len(c1) > len(c2):
839+
if c1.pop(0) > 0:
840+
return 1
841+
while len(c2) > len(c1):
842+
if c2.pop(0) > 0:
843+
return -1
844+
845+
while len(c1) > 0:
846+
diff = c1.pop(0) - c2.pop(0)
847+
if diff != 0:
848+
return diff
849+
850+
return 0
851+
800852

801853
def RawSignatureHash(script, txTo, inIdx, hashtype):
802854
"""Consensus-correct SignatureHash
@@ -1008,4 +1060,5 @@ def SignatureHash(script, txTo, inIdx, hashtype):
10081060
'FindAndDelete',
10091061
'RawSignatureHash',
10101062
'SignatureHash',
1063+
'IsLowDERSignature',
10111064
)

bitcoin/tests/test_script.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,11 @@ def T(redeemScript, expected_hex_bytes):
426426

427427
with self.assertRaises(ValueError):
428428
CScript([b'a' * 518]).to_p2sh_scriptPubKey()
429+
430+
class Test_IsLowDERSignature(unittest.TestCase):
431+
def test_high_s_value(self):
432+
sig = x('3046022100820121109528efda8bb20ca28788639e5ba5b365e0a84f8bd85744321e7312c6022100a7c86a21446daa405306fe10d0a9906e37d1a2c6b6fdfaaf6700053058029bbe')
433+
self.assertFalse(IsLowDERSignature(sig))
434+
def test_low_s_value(self):
435+
sig = x('3045022100b135074e08cc93904a1712b2600d3cb01899a5b1cc7498caa4b8585bcf5f27e7022074ab544045285baef0a63f0fb4c95e577dcbf5c969c0bf47c7da8e478909d669')
436+
self.assertTrue(IsLowDERSignature(sig))

bitcoin/tests/test_wallet.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import unittest
1515

1616
from bitcoin.core import b2x, x
17-
from bitcoin.core.script import CScript
17+
from bitcoin.core.script import CScript, IsLowDERSignature
1818
from bitcoin.core.key import CPubKey
1919
from bitcoin.wallet import *
2020

@@ -218,7 +218,20 @@ def test_sign(self):
218218
hash = b'\x00' * 32
219219
sig = key.sign(hash)
220220

221-
# FIXME: need better tests than this
221+
# Check a valid signature
222222
self.assertTrue(key.pub.verify(hash, sig))
223+
self.assertTrue(IsLowDERSignature(sig))
224+
225+
# Check invalid hash returns false
223226
self.assertFalse(key.pub.verify(b'\xFF'*32, sig))
227+
# Check invalid signature returns false
224228
self.assertFalse(key.pub.verify(hash, sig[0:-1] + b'\x00'))
229+
230+
def test_sign_invalid_hash(self):
231+
key = CBitcoinSecret('5KJvsngHeMpm884wtkJNzQGaCErckhHJBGFsvd3VyK5qMZXj3hS')
232+
with self.assertRaises(TypeError):
233+
sig = key.sign('0' * 32)
234+
235+
hash = b'\x00' * 32
236+
with self.assertRaises(ValueError):
237+
sig = key.sign(hash[0:-2])

0 commit comments

Comments
 (0)