Skip to content

Commit cdcfd3b

Browse files
committed
add ability to use libsecp256k1 for signing (supports RFC6979)
Two new functions are added to core/key.py: is_libsec256k1_available() -- to check that libsec256k1 is present in the system use_libsecp256k1_for_signing(do_use) - if do_use is True, sign() function will use libsec256k1 (it will check _libsecp256k1_enable_signing global flag) if do_use is False, _libsecp256k1_enable_signing flag is set to False, and openssl will be used within sign() function.
1 parent 05cbb3c commit cdcfd3b

File tree

2 files changed

+126
-1
lines changed

2 files changed

+126
-1
lines changed

bitcoin/core/key.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232

3333
_ssl = ctypes.cdll.LoadLibrary(ctypes.util.find_library('ssl') or 'libeay32')
3434

35+
_libsecp256k1_path = ctypes.util.find_library('secp256k1')
36+
_libsecp256k1_enable_signing = False
37+
_libsecp256k1_context = None
38+
_libsecp256k1 = None
39+
40+
3541
class OpenSSLException(EnvironmentError):
3642
pass
3743

@@ -185,12 +191,49 @@ def _check_res_void_p(val, func, args): # pylint: disable=unused-argument
185191
_ssl.o2i_ECPublicKey.restype = ctypes.c_void_p
186192
_ssl.o2i_ECPublicKey.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long]
187193

194+
_ssl.BN_num_bits.restype = ctypes.c_int
195+
_ssl.BN_num_bits.argtypes = [ctypes.c_void_p]
196+
_ssl.EC_KEY_get0_private_key.restype = ctypes.c_void_p
197+
188198
# this specifies the curve used with ECDSA.
189199
_NID_secp256k1 = 714 # from openssl/obj_mac.h
190200

191201
# test that OpenSSL supports secp256k1
192202
_ssl.EC_KEY_new_by_curve_name(_NID_secp256k1)
193203

204+
SECP256K1_FLAGS_TYPE_CONTEXT = (1 << 0)
205+
SECP256K1_FLAGS_BIT_CONTEXT_SIGN = (1 << 9)
206+
SECP256K1_CONTEXT_SIGN = \
207+
(SECP256K1_FLAGS_TYPE_CONTEXT | SECP256K1_FLAGS_BIT_CONTEXT_SIGN)
208+
209+
210+
def is_libsec256k1_available():
211+
return _libsecp256k1_path is not None
212+
213+
214+
def use_libsecp256k1_for_signing(do_use):
215+
global _libsecp256k1
216+
global _libsecp256k1_context
217+
global _libsecp256k1_enable_signing
218+
219+
if not do_use:
220+
_libsecp256k1_enable_signing = False
221+
return
222+
223+
if not is_libsec256k1_available():
224+
raise ImportError("unable to locate libsecp256k1")
225+
226+
if _libsecp256k1_context is None:
227+
_libsecp256k1 = ctypes.cdll.LoadLibrary(_libsecp256k1_path)
228+
_libsecp256k1.secp256k1_context_create.restype = ctypes.c_void_p
229+
_libsecp256k1.secp256k1_context_create.errcheck = _check_res_void_p
230+
_libsecp256k1_context = _libsecp256k1.secp256k1_context_create(SECP256K1_CONTEXT_SIGN)
231+
assert(_libsecp256k1_context is not None)
232+
233+
_libsecp256k1_enable_signing = True
234+
235+
236+
194237
# From openssl/ecdsa.h
195238
class ECDSA_SIG_st(ctypes.Structure):
196239
_fields_ = [("r", ctypes.c_void_p),
@@ -258,12 +301,39 @@ def get_ecdh_key(self, other_pubkey, kdf=lambda k: hashlib.sha256(k).digest()):
258301
r = self.get_raw_ecdh_key(other_pubkey)
259302
return kdf(r)
260303

304+
def get_raw_privkey(self):
305+
bn = _ssl.EC_KEY_get0_private_key(self.k)
306+
bn = ctypes.c_void_p(bn)
307+
size = (_ssl.BN_num_bits(bn) + 7) / 8
308+
mb = ctypes.create_string_buffer(int(size))
309+
_ssl.BN_bn2bin(bn, mb)
310+
return mb.raw.rjust(32, b'\x00')
311+
312+
def _sign_with_libsecp256k1(self, hash):
313+
raw_sig = ctypes.create_string_buffer(64)
314+
result = _libsecp256k1.secp256k1_ecdsa_sign(
315+
_libsecp256k1_context, raw_sig, hash, self.get_raw_privkey(), None, None)
316+
assert 1 == result
317+
sig_size0 = ctypes.c_size_t()
318+
sig_size0.value = 75
319+
mb_sig = ctypes.create_string_buffer(sig_size0.value)
320+
result = _libsecp256k1.secp256k1_ecdsa_signature_serialize_der(
321+
_libsecp256k1_context, mb_sig, ctypes.byref(sig_size0), raw_sig)
322+
assert 1 == result
323+
# libsecp256k1 creates signatures already in lower-S form, no further
324+
# conversion needed.
325+
return mb_sig.raw[:sig_size0.value]
326+
327+
261328
def sign(self, hash): # pylint: disable=redefined-builtin
262329
if not isinstance(hash, bytes):
263330
raise TypeError('Hash must be bytes instance; got %r' % hash.__class__)
264331
if len(hash) != 32:
265332
raise ValueError('Hash must be exactly 32 bytes long')
266333

334+
if _libsecp256k1_enable_signing:
335+
return self._sign_with_libsecp256k1(hash)
336+
267337
sig_size0 = ctypes.c_uint32()
268338
sig_size0.value = _ssl.ECDSA_size(self.k)
269339
mb_sig = ctypes.create_string_buffer(sig_size0.value)

bitcoin/tests/test_wallet.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111

1212
from __future__ import absolute_import, division, print_function, unicode_literals
1313

14+
import hashlib
1415
import unittest
1516

1617
from bitcoin.core import b2x, x
1718
from bitcoin.core.script import CScript, IsLowDERSignature
18-
from bitcoin.core.key import CPubKey
19+
from bitcoin.core.key import CPubKey, is_libsec256k1_available, use_libsecp256k1_for_signing
1920
from bitcoin.wallet import *
2021

2122
class Test_CBitcoinAddress(unittest.TestCase):
@@ -238,3 +239,57 @@ def test_sign_invalid_hash(self):
238239
hash = b'\x00' * 32
239240
with self.assertRaises(ValueError):
240241
sig = key.sign(hash[0:-2])
242+
243+
244+
class Test_RFC6979(unittest.TestCase):
245+
def test(self):
246+
if not is_libsec256k1_available():
247+
return
248+
249+
use_libsecp256k1_for_signing(True)
250+
251+
# Test Vectors for RFC 6979 ECDSA, secp256k1, SHA-256
252+
# (private key, message, expected k, expected signature)
253+
test_vectors = [
254+
(0x1, "Satoshi Nakamoto", 0x8F8A276C19F4149656B280621E358CCE24F5F52542772691EE69063B74F15D15, "934b1ea10a4b3c1757e2b0c017d0b6143ce3c9a7e6a4a49860d7a6ab210ee3d82442ce9d2b916064108014783e923ec36b49743e2ffa1c4496f01a512aafd9e5"),
255+
(0x1, "All those moments will be lost in time, like tears in rain. Time to die...", 0x38AA22D72376B4DBC472E06C3BA403EE0A394DA63FC58D88686C611ABA98D6B3, "8600dbd41e348fe5c9465ab92d23e3db8b98b873beecd930736488696438cb6b547fe64427496db33bf66019dacbf0039c04199abb0122918601db38a72cfc21"),
256+
(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140, "Satoshi Nakamoto", 0x33A19B60E25FB6F4435AF53A3D42D493644827367E6453928554F43E49AA6F90, "fd567d121db66e382991534ada77a6bd3106f0a1098c231e47993447cd6af2d06b39cd0eb1bc8603e159ef5c20a5c8ad685a45b06ce9bebed3f153d10d93bed5"),
257+
(0xf8b8af8ce3c7cca5e300d33939540c10d45ce001b8f252bfbc57ba0342904181, "Alan Turing", 0x525A82B70E67874398067543FD84C83D30C175FDC45FDEEE082FE13B1D7CFDF1, "7063ae83e7f62bbb171798131b4a0564b956930092b33b07b395615d9ec7e15c58dfcc1e00a35e1572f366ffe34ba0fc47db1e7189759b9fb233c5b05ab388ea"),
258+
(0xe91671c46231f833a6406ccbea0e3e392c76c167bac1cb013f6f1013980455c2, "There is a computer disease that anybody who works with computers knows about. It's a very serious disease and it interferes completely with the work. The trouble with computers is that you 'play' with them!", 0x1F4B84C23A86A221D233F2521BE018D9318639D5B8BBD6374A8A59232D16AD3D, "b552edd27580141f3b2a5463048cb7cd3e047b97c9f98076c32dbdf85a68718b279fa72dd19bfae05577e06c7c0c1900c371fcd5893f7e1d56a37d30174671f6")
259+
]
260+
for vector in test_vectors:
261+
secret = CBitcoinSecret.from_secret_bytes(x('{:064x}'.format(vector[0])))
262+
encoded_sig = secret.sign(hashlib.sha256(vector[1].encode('utf8')).digest())
263+
264+
assert(encoded_sig[0] == 0x30)
265+
assert(encoded_sig[1] == len(encoded_sig)-2)
266+
assert(encoded_sig[2] == 0x02)
267+
268+
rlen = encoded_sig[3]
269+
rpos = 4
270+
assert(rlen in (32, 33))
271+
272+
if rlen == 33:
273+
assert(encoded_sig[rpos] == 0)
274+
rpos += 1
275+
rlen -= 1
276+
277+
rval = encoded_sig[rpos:rpos+rlen]
278+
spos = rpos+rlen
279+
assert(encoded_sig[spos] == 0x02)
280+
281+
spos += 1
282+
slen = encoded_sig[spos]
283+
assert(slen in (32, 33))
284+
285+
spos += 1
286+
if slen == 33:
287+
assert(encoded_sig[spos] == 0)
288+
spos += 1
289+
slen -= 1
290+
291+
sval = encoded_sig[spos:spos+slen]
292+
sig = b2x(rval + sval)
293+
assert(str(sig) == vector[3])
294+
295+
use_libsecp256k1_for_signing(False)

0 commit comments

Comments
 (0)