Skip to content

Commit 8b187ba

Browse files
committed
Merge pull request #55
0b8318c Add signing and verifying of a message compatible with bitcoin core, using pubkey recovery. (Ruben de Vries)
2 parents 0bc631e + 0b8318c commit 8b187ba

File tree

7 files changed

+1417
-1
lines changed

7 files changed

+1417
-1
lines changed

bitcoin/core/key.py

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,21 @@
1515
WARNING: This module does not mlock() secrets; your private keys may end up on
1616
disk in swap! Use with caution!
1717
"""
18-
1918
import ctypes
2019
import ctypes.util
2120
import hashlib
2221
import sys
22+
import bitcoin
23+
import bitcoin.signature
24+
25+
_bchr = chr
26+
_bord = ord
27+
if sys.version > '3':
28+
_bchr = lambda x: bytes([x])
29+
_bord = lambda x: x
30+
from io import BytesIO as BytesIO
31+
else:
32+
from cStringIO import StringIO as BytesIO
2333

2434
import bitcoin.core.script
2535

@@ -128,6 +138,53 @@ def sign(self, hash):
128138
else:
129139
return self.signature_to_low_s(mb_sig.raw[:sig_size0.value])
130140

141+
def sign_compact(self, hash):
142+
if not isinstance(hash, bytes):
143+
raise TypeError('Hash must be bytes instance; got %r' % hash.__class__)
144+
if len(hash) != 32:
145+
raise ValueError('Hash must be exactly 32 bytes long')
146+
147+
sig_size0 = ctypes.c_uint32()
148+
sig_size0.value = _ssl.ECDSA_size(self.k)
149+
mb_sig = ctypes.create_string_buffer(sig_size0.value)
150+
result = _ssl.ECDSA_sign(0, hash, len(hash), mb_sig, ctypes.byref(sig_size0), self.k)
151+
assert 1 == result
152+
153+
if bitcoin.core.script.IsLowDERSignature(mb_sig.raw[:sig_size0.value]):
154+
sig = mb_sig.raw[:sig_size0.value]
155+
else:
156+
sig = self.signature_to_low_s(mb_sig.raw[:sig_size0.value])
157+
158+
sig = bitcoin.signature.DERSignature.deserialize(sig)
159+
160+
r_val = sig.r
161+
s_val = sig.s
162+
163+
# assert that the r and s are less than 32 long, excluding leading 0s
164+
assert len(r_val) <= 32 or r_val[0:-32] == b'\x00'
165+
assert len(s_val) <= 32 or s_val[0:-32] == b'\x00'
166+
167+
# ensure r and s are always 32 chars long by 0padding
168+
r_val = ((b'\x00' * 32) + r_val)[-32:]
169+
s_val = ((b'\x00' * 32) + s_val)[-32:]
170+
171+
# tmp pubkey of self, but always compressed
172+
pubkey = CECKey()
173+
pubkey.set_pubkey(self.get_pubkey())
174+
pubkey.set_compressed(True)
175+
176+
# bitcoin core does <4, but I've seen other places do <2 and I've never seen a i > 1 so far
177+
for i in range(0, 4):
178+
cec_key = CECKey()
179+
cec_key.set_compressed(True)
180+
181+
result = cec_key.recover(r_val, s_val, hash, len(hash), i, 1)
182+
if result == 1:
183+
if cec_key.get_pubkey() == pubkey.get_pubkey():
184+
return r_val + s_val, i
185+
186+
raise ValueError
187+
131188
def signature_to_low_s(self, sig):
132189
der_sig = ECDSA_SIG_st()
133190
_ssl.d2i_ECDSA_SIG(ctypes.byref(ctypes.pointer(der_sig)), ctypes.byref(ctypes.c_char_p(sig)), len(sig))
@@ -185,6 +242,106 @@ def set_compressed(self, compressed):
185242
form = self.POINT_CONVERSION_UNCOMPRESSED
186243
_ssl.EC_KEY_set_conv_form(self.k, form)
187244

245+
def recover(self, sigR, sigS, msg, msglen, recid, check):
246+
"""
247+
Perform ECDSA key recovery (see SEC1 4.1.6) for curves over (mod p)-fields
248+
recid selects which key is recovered
249+
if check is non-zero, additional checks are performed
250+
"""
251+
i = int(recid / 2)
252+
253+
r = None
254+
s = None
255+
ctx = None
256+
R = None
257+
O = None
258+
Q = None
259+
260+
assert len(sigR) == 32, len(sigR)
261+
assert len(sigS) == 32, len(sigS)
262+
263+
try:
264+
r = _ssl.BN_bin2bn(bytes(sigR), len(sigR), _ssl.BN_new())
265+
s = _ssl.BN_bin2bn(bytes( sigS), len(sigS), _ssl.BN_new())
266+
267+
group = _ssl.EC_KEY_get0_group(self.k)
268+
ctx = _ssl.BN_CTX_new()
269+
order = _ssl.BN_CTX_get(ctx)
270+
ctx = _ssl.BN_CTX_new()
271+
272+
if not _ssl.EC_GROUP_get_order(group, order, ctx):
273+
return -2
274+
275+
x = _ssl.BN_CTX_get(ctx)
276+
if not _ssl.BN_copy(x, order):
277+
return -1
278+
if not _ssl.BN_mul_word(x, i):
279+
return -1
280+
if not _ssl.BN_add(x, x, r):
281+
return -1
282+
283+
field = _ssl.BN_CTX_get(ctx)
284+
if not _ssl.EC_GROUP_get_curve_GFp(group, field, None, None, ctx):
285+
return -2
286+
287+
if _ssl.BN_cmp(x, field) >= 0:
288+
return 0
289+
290+
R = _ssl.EC_POINT_new(group)
291+
if R is None:
292+
return -2
293+
if not _ssl.EC_POINT_set_compressed_coordinates_GFp(group, R, x, recid % 2, ctx):
294+
return 0
295+
296+
if check:
297+
O = _ssl.EC_POINT_new(group)
298+
if O is None:
299+
return -2
300+
if not _ssl.EC_POINT_mul(group, O, None, R, order, ctx):
301+
return -2
302+
if not _ssl.EC_POINT_is_at_infinity(group, O):
303+
return 0
304+
305+
Q = _ssl.EC_POINT_new(group)
306+
if Q is None:
307+
return -2
308+
309+
n = _ssl.EC_GROUP_get_degree(group)
310+
e = _ssl.BN_CTX_get(ctx)
311+
if not _ssl.BN_bin2bn(msg, msglen, e):
312+
return -1
313+
314+
if 8 * msglen > n:
315+
_ssl.BN_rshift(e, e, 8 - (n & 7))
316+
317+
zero = _ssl.BN_CTX_get(ctx)
318+
# if not _ssl.BN_zero(zero):
319+
# return -1
320+
if not _ssl.BN_mod_sub(e, zero, e, order, ctx):
321+
return -1
322+
rr = _ssl.BN_CTX_get(ctx)
323+
if not _ssl.BN_mod_inverse(rr, r, order, ctx):
324+
return -1
325+
sor = _ssl.BN_CTX_get(ctx)
326+
if not _ssl.BN_mod_mul(sor, s, rr, order, ctx):
327+
return -1
328+
eor = _ssl.BN_CTX_get(ctx)
329+
if not _ssl.BN_mod_mul(eor, e, rr, order, ctx):
330+
return -1
331+
if not _ssl.EC_POINT_mul(group, Q, eor, R, sor, ctx):
332+
return -2
333+
334+
if not _ssl.EC_KEY_set_public_key(self.k, Q):
335+
return -2
336+
337+
return 1
338+
finally:
339+
if r: _ssl.BN_free(r)
340+
if s: _ssl.BN_free(s)
341+
if ctx: _ssl.BN_CTX_free(ctx)
342+
if R: _ssl.EC_POINT_free(R)
343+
if O: _ssl.EC_POINT_free(O)
344+
if Q: _ssl.EC_POINT_free(Q)
188345

189346
class CPubKey(bytes):
190347
"""An encapsulated public key
@@ -204,6 +361,30 @@ def __new__(cls, buf, _cec_key=None):
204361
self.is_fullyvalid = _cec_key.set_pubkey(self) != 0
205362
return self
206363

364+
@classmethod
365+
def recover_compact(cls, hash, sig):
366+
"""Recover a public key from a compact signature."""
367+
if len(sig) != 65:
368+
raise ValueError("Signature should be 65 characters, not [%d]" % (len(sig), ))
369+
370+
recid = (_bord(sig[0]) - 27) & 3
371+
compressed = (_bord(sig[0]) - 27) & 4 != 0
372+
373+
cec_key = CECKey()
374+
cec_key.set_compressed(compressed)
375+
376+
sigR = sig[1:33]
377+
sigS = sig[33:65]
378+
379+
result = cec_key.recover(sigR, sigS, hash, len(hash), recid, 0)
380+
381+
if result < 1:
382+
return False
383+
384+
pubkey = cec_key.get_pubkey()
385+
386+
return CPubKey(pubkey, _cec_key=cec_key)
387+
207388
@property
208389
def is_valid(self):
209390
return len(self) > 0

bitcoin/signature.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (C) 2012-2014 The python-bitcoinlib developers
2+
#
3+
# This file is part of python-bitcoinlib.
4+
#
5+
# It is subject to the license terms in the LICENSE file found in the top-level
6+
# directory of this distribution.
7+
#
8+
# No part of python-bitcoinlib, including this file, may be copied, modified,
9+
# propagated, or distributed except according to the terms contained in the
10+
# LICENSE file.
11+
12+
from __future__ import absolute_import, division, print_function, unicode_literals
13+
14+
from bitcoin.core.serialize import *
15+
16+
# Py3 compatibility
17+
import sys
18+
19+
if sys.version > '3':
20+
from io import BytesIO as _BytesIO
21+
else:
22+
from cStringIO import StringIO as _BytesIO
23+
24+
25+
class DERSignature(ImmutableSerializable):
26+
__slots__ = ['length', 'r', 's']
27+
28+
def __init__(self, r, s, length):
29+
object.__setattr__(self, 'r', r)
30+
object.__setattr__(self, 's', s)
31+
object.__setattr__(self, 'length', length)
32+
33+
@classmethod
34+
def stream_deserialize(cls, f):
35+
assert ser_read(f, 1) == b"\x30"
36+
rs = BytesSerializer.stream_deserialize(f)
37+
f = _BytesIO(rs)
38+
assert ser_read(f, 1) == b"\x02"
39+
r = BytesSerializer.stream_deserialize(f)
40+
assert ser_read(f, 1) == b"\x02"
41+
s = BytesSerializer.stream_deserialize(f)
42+
return cls(r, s, len(r + s))
43+
44+
def stream_serialize(self, f):
45+
f.write(b"\x30")
46+
f.write(b"\x02")
47+
BytesSerializer.stream_serialize(self.r, f)
48+
f.write(b"\x30")
49+
BytesSerializer.stream_serialize(self.s, f)
50+
51+
def __repr__(self):
52+
return 'DERSignature(%s, %s)' % (self.r, self.s)
53+
54+
55+
__all__ = (
56+
'DERSignature',
57+
)

bitcoin/signmessage.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright (C) 2013-2015 The python-bitcoinlib developers
2+
#
3+
# This file is part of python-bitcoinlib.
4+
#
5+
# It is subject to the license terms in the LICENSE file found in the top-level
6+
# directory of this distribution.
7+
#
8+
# No part of python-bitcoinlib, including this file, may be copied, modified,
9+
# propagated, or distributed except according to the terms contained in the
10+
# LICENSE file.
11+
12+
from __future__ import absolute_import, division, print_function, unicode_literals
13+
14+
from bitcoin.core.key import CPubKey
15+
from bitcoin.core.serialize import ImmutableSerializable
16+
from bitcoin.wallet import P2PKHBitcoinAddress
17+
import bitcoin
18+
import base64
19+
import sys
20+
21+
_bchr = chr
22+
_bord = ord
23+
if sys.version > '3':
24+
long = int
25+
_bchr = lambda x: bytes([x])
26+
_bord = lambda x: x
27+
28+
29+
def VerifyMessage(address, message, sig):
30+
sig = base64.b64decode(sig)
31+
hash = message.GetHash()
32+
33+
pubkey = CPubKey.recover_compact(hash, sig)
34+
35+
return str(P2PKHBitcoinAddress.from_pubkey(pubkey)) == str(address)
36+
37+
38+
def SignMessage(key, message):
39+
sig, i = key.sign_compact(message.GetHash())
40+
41+
meta = 27 + i
42+
if key.is_compressed:
43+
meta += 4
44+
45+
return base64.b64encode(_bchr(meta) + sig)
46+
47+
48+
class BitcoinMessage(ImmutableSerializable):
49+
__slots__ = ['magic', 'message']
50+
51+
def __init__(self, message="", magic="Bitcoin Signed Message:\n"):
52+
object.__setattr__(self, 'message', message.encode("utf-8"))
53+
object.__setattr__(self, 'magic', magic.encode("utf-8"))
54+
55+
@classmethod
56+
def stream_deserialize(cls, f):
57+
magic = bitcoin.core.serialize.BytesSerializer.stream_deserialize(f)
58+
message = bitcoin.core.serialize.BytesSerializer.stream_deserialize(f)
59+
return cls(message, magic)
60+
61+
def stream_serialize(self, f):
62+
bitcoin.core.serialize.BytesSerializer.stream_serialize(self.magic, f)
63+
bitcoin.core.serialize.BytesSerializer.stream_serialize(self.message, f)
64+
65+
def __str__(self):
66+
return self.message.decode('ascii')
67+
68+
def __repr__(self):
69+
return 'BitcoinMessage(%s, %s)' % (self.magic, self.message)

0 commit comments

Comments
 (0)