ecdsa -> coincurve

This commit is contained in:
Victor Shyba 2018-09-21 17:12:07 -03:00 committed by Lex Berezhny
parent 0b75bb4052
commit 8a87195f55
6 changed files with 124 additions and 106 deletions

View file

@ -6,7 +6,7 @@ source =
torba
.tox/*/lib/python*/site-packages/torba
[mypy-twisted.*,cryptography.*,ecdsa.*,pbkdf2]
[mypy-twisted.*,cryptography.*,coincurve.*,pbkdf2]
ignore_missing_imports = True
[pylint]

View file

@ -27,7 +27,7 @@ setup(
python_requires='>=3.6',
install_requires=(
'twisted',
'ecdsa',
'coincurve',
'pbkdf2',
'cryptography'
),

View file

@ -0,0 +1,65 @@
expected_ids = [
b'948adae2a128c0bd1fa238117fd0d9690961f26e',
b'cd9f4f2adde7de0a53ab6d326bb6a62b489876dd',
b'c479e02a74a809ffecff60255d1c14f4081a197a',
b'4bab2fb2c424f31f170b15ec53c4a596db9d6710',
b'689cb7c621f57b7c398e7e04ed9a5098ab8389e9',
b'75116d6a689a0f9b56fe7cfec9cbbd0e16814288',
b'2439f0993fb298497dd7f317b9737c356f664a86',
b'32f1cb4799008cf5496bb8cafdaf59d5dabec6af',
b'fa29aa536353904e9cc813b0cf18efcc09e5ad13',
b'37df34002f34d7875428a2977df19be3f4f40a31',
b'8c8a72b5d2747a3e7e05ed85110188769d5656c3',
b'e5c8ef10c5bdaa79c9a237a096f50df4dcac27f0',
b'4d5270dc100fba85974665c20cd0f95d4822e8d1',
b'e76b07da0cdd59915475cd310599544b9744fa34',
b'6f009bccf8be99707161abb279d8ccf8fd953721',
b'f32f08b722cc8607c3f7f192b4d5f13a74c85785',
b'46f4430a5c91b9b799e9be6b47ac7a749d8d9f30',
b'ebbf9850abe0aae2d09e7e3ebd6b51f01282f39b',
b'5f6655438f8ddc6b2f6ea8197c8babaffc9f5c09',
b'e194e70ee8711b0ed765608121e4cceb551cdf28'
]
expected_privkeys = [
b'95557ee9a2bb7665e67e45246658b5c839f7dcd99b6ebc800eeebccd28bf134a',
b'689b6921f65647a8e4fc1497924730c92ad4ad183f10fac2bdee65cc8fb6dcf9',
b'977ee018b448c530327b7e927cc3645ca4cb152c5dd98e1bd917c52fd46fc80a',
b'3c7fb05b0ab4da8b292e895f574f8213cadfe81b84ded7423eab61c5f884c8ae',
b'b21fc7be1e69182827538683a48ac9d95684faf6c1c6deabb6e513d8c76afcc9',
b'a5021734dbbf1d090b15509ba00f2c04a3d5afc19939b4594ca0850d4190b923',
b'07dfe0aa94c1b948dc935be1f8179f3050353b46f3a3134e77c70e66208be72d',
b'c331b2fb82cd91120b0703ee312042a854a51a8d945aa9e70fb14d68b0366fe1',
b'3aa59ec4d8f1e7ce2775854b5e82433535b6e3503f9a8e7c4e60aac066d44718',
b'ccc8b4ca73b266b4a0c89a9d33c4ec7532b434c9294c26832355e5e2bee2e005',
b'280c074d8982e56d70c404072252c309694a6e5c05457a6abbe8fc225c2dfd52',
b'546cee26da713a3a64b2066d5e3a52b7c1d927396d1ba8a3d9f6e3e973398856',
b'7fbc4615d5e819eee22db440c5bcc4ff25bb046841c41a192003a6d9abfbafbf',
b'5b63f13011cab965feea3a41fac2d7a877aa710ab20e2a9a1708474e3c05c050',
b'394b36f528947557d317fd40a4adde5514c8745a5f64185421fa2c0c4a158938',
b'8f101c8f5290ae6c0dd76d210b7effacd7f12db18f3befab711f533bde084c76',
b'6637a656f897a66080fbe60027d32c3f4ebc0e3b5f96123a33f932a091b039c2',
b'2815aa6667c042a3a4565fb789890cd33e380d047ed712759d097d479df71051',
b'120e761c6382b07a9548650a20b3b9dd74b906093260fa6f92f790ba71f79e8d',
b'823c8a613ea539f730a968518993195174bf973ed75c734b6898022867165d7b'
]
expected_hardened_privkeys = [
b'abdba45b0459e7804beb68edb899e58a5c2636bf67d096711904001406afbd4c',
b'c9e804d4b8fdd99ef6ab2b0ca627a57f4283c28e11e9152ad9d3f863404d940e',
b'4cf87d68ae99711261f8cb8e1bde83b8703ff5d689ef70ce23106d1e6e8ed4bd',
b'dbf8d578c77f9bf62bb2ad40975e253af1e1d44d53abf84a22d2be29b9488f7f',
b'633bb840505521ffe39cb89a04fb8bff3298d6b64a5d8f170aca1e456d6f89b9',
b'92e80a38791bd8ba2105b9867fd58ac2cc4fb9853e18141b7fee1884bc5aae69',
b'd3663339af1386d05dd90ee20f627661ae87ddb1db0c2dc73fd8a4485930d0e7',
b'09a448303452d241b8a25670b36cc758975b97e88f62b6f25cd9084535e3c13a',
b'ee22eb77df05ff53e9c2ba797c1f2ebf97ec4cf5a99528adec94972674aeabed',
b'935facccb6120659c5b7c606a457c797e5a10ce4a728346e1a3a963251169651',
b'8ac9b4a48da1def375640ca03bc6711040dfd4eea7106d42bb4c2de83d7f595e',
b'51ecd3f7565c2b86d5782dbde2175ab26a7b896022564063fafe153588610be9',
b'04918252f6b6f51cd75957289b56a324b45cc085df80839137d740f9ada6c062',
b'2efbd0c839af971e3769c26938d776990ebf097989df4861535a7547a2701483',
b'85c6e31e6b27bd188291a910f4a7faba7fceb3e09df72884b10907ecc1491cd0',
b'05e245131885bebda993a31bb14ac98b794062a50af639ad22010aed1e533a54',
b'ddca42cf7db93f3a3f0723d5fee4c21bf60b7afac35d5c30eb34bd91b35cc609',
b'324a5c16030e0c3947e4dcd2b5057fd3a4d5bed96b23e3b476b2af0ab76369c9',
b'da63c41cdb398cdcd93e832f3e198528afbb4065821b026c143cec910d8362f0'
]

View file

@ -1,9 +1,10 @@
from binascii import unhexlify
from binascii import unhexlify, hexlify
from twisted.trial import unittest
from .key_fixtures import expected_ids, expected_privkeys, expected_hardened_privkeys
from torba.bip32 import PubKey, PrivateKey, from_extended_key_string
from torba.coin.bitcoinsegwit import MainNetLedger as ledger_class
class BIP32Tests(unittest.TestCase):
def test_pubkey_validation(self):
@ -32,7 +33,10 @@ class BIP32Tests(unittest.TestCase):
)
with self.assertRaisesRegex(ValueError, 'invalid BIP32 public key child number'):
pubkey.child(-1)
self.assertIsInstance(pubkey.child(1), PubKey)
for i in range(20):
new_key = pubkey.child(i)
self.assertIsInstance(new_key, PubKey)
self.assertEqual(hexlify(new_key.identifier()), expected_ids[i])
def test_private_key_validation(self):
with self.assertRaisesRegex(TypeError, 'private key must be raw bytes'):
@ -49,16 +53,35 @@ class BIP32Tests(unittest.TestCase):
)
ec_point = private_key.ec_point()
self.assertEqual(
ec_point.x(), 30487144161998778625547553412379759661411261804838752332906558028921886299019
ec_point[0], 30487144161998778625547553412379759661411261804838752332906558028921886299019
)
self.assertEqual(
ec_point.y(), 86198965946979720220333266272536217633917099472454294641561154971209433250106
ec_point[1], 86198965946979720220333266272536217633917099472454294641561154971209433250106
)
self.assertEqual(private_key.address(), '1GVM5dEhThbiyCZ9gqBZBv6p9whga7MTXo' )
with self.assertRaisesRegex(ValueError, 'invalid BIP32 private key child number'):
private_key.child(-1)
self.assertIsInstance(private_key.child(PrivateKey.HARDENED), PrivateKey)
def test_private_key_derivation(self):
private_key = PrivateKey(
ledger_class({
'db': ledger_class.database_class(':memory:'),
'headers': ledger_class.headers_class(':memory:'),
}),
unhexlify('2423f3dc6087d9683f73a684935abc0ccd8bc26370588f56653128c6a6f0bf7c'),
b'abcd'*8, 0, 1
)
for i in range(20):
new_privkey = private_key.child(i)
self.assertIsInstance(new_privkey, PrivateKey)
self.assertEqual(hexlify(new_privkey.private_key_bytes), expected_privkeys[i])
for i in range(PrivateKey.HARDENED + 1, private_key.HARDENED + 20):
new_privkey = private_key.child(i)
self.assertIsInstance(new_privkey, PrivateKey)
self.assertEqual(hexlify(new_privkey.private_key_bytes), expected_hardened_privkeys[i - 1 - PrivateKey.HARDENED])
def test_from_extended_keys(self):
ledger = ledger_class({
'db': ledger_class.database_class(':memory:'),

View file

@ -7,16 +7,10 @@
# and warranty status of this software.
""" Logic for BIP32 Hierarchical Key Derivation. """
import struct
import hashlib
import ecdsa
import ecdsa.ellipticcurve as EC
import ecdsa.numbertheory as NT
from coincurve import PublicKey, PrivateKey as _PrivateKey
from torba.hash import Base58, hmac_sha512, hash160, double_sha256
from torba.util import cachedproperty, bytes_to_int, int_to_bytes
from torba.util import cachedproperty
class DerivationError(Exception):
@ -26,8 +20,6 @@ class DerivationError(Exception):
class _KeyBase:
""" A BIP32 Key, public or private. """
CURVE = ecdsa.SECP256k1
def __init__(self, ledger, chain_code, n, depth, parent):
if not isinstance(chain_code, (bytes, bytearray)):
raise TypeError('chain code must be raw bytes')
@ -63,7 +55,7 @@ class _KeyBase:
raise ValueError('raw_serkey must have length 33')
return (ver_bytes + bytes((self.depth,))
+ self.parent_fingerprint() + struct.pack('>I', self.n)
+ self.parent_fingerprint() + self.n.to_bytes(4, 'big')
+ self.chain_code + raw_serkey)
def identifier(self):
@ -90,43 +82,26 @@ class PubKey(_KeyBase):
def __init__(self, ledger, pubkey, chain_code, n, depth, parent=None):
super().__init__(ledger, chain_code, n, depth, parent)
if isinstance(pubkey, ecdsa.VerifyingKey):
if isinstance(pubkey, PublicKey):
self.verifying_key = pubkey
else:
self.verifying_key = self._verifying_key_from_pubkey(pubkey)
@classmethod
def _verifying_key_from_pubkey(cls, pubkey):
""" Converts a 33-byte compressed pubkey into an ecdsa.VerifyingKey object. """
""" Converts a 33-byte compressed pubkey into an PublicKey object. """
if not isinstance(pubkey, (bytes, bytearray)):
raise TypeError('pubkey must be raw bytes')
if len(pubkey) != 33:
raise ValueError('pubkey must be 33 bytes')
if pubkey[0] not in (2, 3):
raise ValueError('invalid pubkey prefix byte')
curve = cls.CURVE.curve
is_odd = pubkey[0] == 3
x = bytes_to_int(pubkey[1:])
# p is the finite field order
a, b, p = curve.a(), curve.b(), curve.p() # pylint: disable=invalid-name
y2 = pow(x, 3, p) + b # pylint: disable=invalid-name
assert a == 0 # Otherwise y2 += a * pow(x, 2, p)
y = NT.square_root_mod_prime(y2 % p, p)
if bool(y & 1) != is_odd:
y = p - y
point = EC.Point(curve, x, y)
return ecdsa.VerifyingKey.from_public_point(point, curve=cls.CURVE)
return PublicKey(pubkey)
@cachedproperty
def pubkey_bytes(self):
""" Return the compressed public key as 33 bytes. """
point = self.verifying_key.pubkey.point
prefix = bytes((2 + (point.y() & 1),))
padded_bytes = _exponent_to_bytes(point.x())
return prefix + padded_bytes
return self.verifying_key.format(True)
@cachedproperty
def address(self):
@ -134,28 +109,17 @@ class PubKey(_KeyBase):
return self.ledger.public_key_to_address(self.pubkey_bytes)
def ec_point(self):
return self.verifying_key.pubkey.point
return self.verifying_key.point()
def child(self, n):
def child(self, n: int):
""" Return the derived child extended pubkey at index N. """
if not 0 <= n < (1 << 31):
raise ValueError('invalid BIP32 public key child number')
msg = self.pubkey_bytes + struct.pack('>I', n)
L, R = self._hmac_sha512(msg) # pylint: disable=invalid-name
curve = self.CURVE
L = bytes_to_int(L) # pylint: disable=invalid-name
if L >= curve.order:
raise DerivationError
point = curve.generator * L + self.ec_point()
if point == EC.INFINITY:
raise DerivationError
verkey = ecdsa.VerifyingKey.from_public_point(point, curve=curve)
return PubKey(self.ledger, verkey, R, n, self.depth + 1, self)
msg = self.pubkey_bytes + n.to_bytes(4, 'big')
L_b, R_b = self._hmac_sha512(msg) # pylint: disable=invalid-name
derived_key = self.verifying_key.add(L_b)
return PubKey(self.ledger, derived_key, R_b, n, self.depth + 1, self)
def identifier(self):
""" Return the key's identifier as 20 bytes. """
@ -169,20 +133,6 @@ class PubKey(_KeyBase):
)
class LowSValueSigningKey(ecdsa.SigningKey):
"""
Enforce low S values in signatures
BIP-0062: https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki#low-s-values-in-signatures
"""
def sign_number(self, number, entropy=None, k=None):
order = self.privkey.order
r, s = ecdsa.SigningKey.sign_number(self, number, entropy, k) # pylint: disable=invalid-name
if s > order / 2:
s = order - s
return r, s
class PrivateKey(_KeyBase):
"""A BIP32 private key."""
@ -190,16 +140,15 @@ class PrivateKey(_KeyBase):
def __init__(self, ledger, privkey, chain_code, n, depth, parent=None):
super().__init__(ledger, chain_code, n, depth, parent)
if isinstance(privkey, ecdsa.SigningKey):
if isinstance(privkey, _PrivateKey):
self.signing_key = privkey
else:
self.signing_key = self._signing_key_from_privkey(privkey)
@classmethod
def _signing_key_from_privkey(cls, private_key):
""" Converts a 32-byte private key into an ecdsa.SigningKey object. """
exponent = cls._private_key_secret_exponent(private_key)
return LowSValueSigningKey.from_secret_exponent(exponent, curve=cls.CURVE)
""" Converts a 32-byte private key into an coincurve.PrivateKey object. """
return _PrivateKey.from_int(PrivateKey._private_key_secret_exponent(private_key))
@classmethod
def _private_key_secret_exponent(cls, private_key):
@ -208,10 +157,7 @@ class PrivateKey(_KeyBase):
raise TypeError('private key must be raw bytes')
if len(private_key) != 32:
raise ValueError('private key must be 32 bytes')
exponent = bytes_to_int(private_key)
if not 1 <= exponent < cls.CURVE.order:
raise ValueError('private key represents an invalid exponent')
return exponent
return int.from_bytes(private_key, 'big')
@classmethod
def from_seed(cls, ledger, seed):
@ -223,12 +169,12 @@ class PrivateKey(_KeyBase):
@cachedproperty
def private_key_bytes(self):
""" Return the serialized private key (no leading zero byte). """
return _exponent_to_bytes(self.secret_exponent())
return self.signing_key.secret
@cachedproperty
def public_key(self):
""" Return the corresponding extended public key. """
verifying_key = self.signing_key.get_verifying_key()
verifying_key = self.signing_key.public_key
parent_pubkey = self.parent.public_key if self.parent else None
return PubKey(self.ledger, verifying_key, self.chain_code, self.n, self.depth,
parent_pubkey)
@ -238,7 +184,7 @@ class PrivateKey(_KeyBase):
def secret_exponent(self):
""" Return the private key as a secret exponent. """
return self.signing_key.privkey.secret_multiplier
return self.signing_key.to_int()
def wif(self):
""" Return the private key encoded in Wallet Import Format. """
@ -258,24 +204,14 @@ class PrivateKey(_KeyBase):
else:
serkey = self.public_key.pubkey_bytes
msg = serkey + struct.pack('>I', n)
L, R = self._hmac_sha512(msg) # pylint: disable=invalid-name
curve = self.CURVE
L = bytes_to_int(L) # pylint: disable=invalid-name
exponent = (L + bytes_to_int(self.private_key_bytes)) % curve.order
if exponent == 0 or L >= curve.order:
raise DerivationError
privkey = _exponent_to_bytes(exponent)
return PrivateKey(self.ledger, privkey, R, n, self.depth + 1, self)
msg = serkey + n.to_bytes(4, 'big')
L_b, R_b = self._hmac_sha512(msg) # pylint: disable=invalid-name
derived_key = self.signing_key.add(L_b)
return PrivateKey(self.ledger, derived_key, R_b, n, self.depth + 1, self)
def sign(self, data):
""" Produce a signature for piece of data by double hashing it and signing the hash. """
key = self.signing_key
digest = double_sha256(data)
return key.sign_digest_deterministic(digest, hashlib.sha256, ecdsa.util.sigencode_der)
return self.signing_key.sign(data, hasher=double_sha256)
def identifier(self):
"""Return the key's identifier as 20 bytes."""
@ -289,11 +225,6 @@ class PrivateKey(_KeyBase):
)
def _exponent_to_bytes(exponent):
"""Convert an exponent to 32 big-endian bytes"""
return (bytes((0,)*32) + int_to_bytes(exponent))[-32:]
def _from_extended_key(ledger, ekey):
"""Return a PubKey or PrivateKey from an extended key raw bytes."""
if not isinstance(ekey, (bytes, bytearray)):
@ -302,8 +233,7 @@ def _from_extended_key(ledger, ekey):
raise ValueError('extended key must have length 78')
depth = ekey[4]
# fingerprint = ekey[5:9]
n, = struct.unpack('>I', ekey[9:13])
n = int.from_bytes(ekey[9:13], 'big')
chain_code = ekey[13:45]
if ekey[:4] == ledger.extended_public_key_prefix:

View file

@ -8,8 +8,8 @@ import importlib
import unicodedata
import string
from binascii import hexlify
from secrets import randbelow
import ecdsa
import pbkdf2
from torba.hash import hmac_sha512
@ -138,9 +138,9 @@ class Mnemonic:
# rounding
n = int(math.ceil(num_bits/bpw) * bpw)
entropy = 1
while entropy < pow(2, n - bpw):
while 0 < entropy < pow(2, n - bpw):
# try again if seed would not contain enough words
entropy = ecdsa.util.randrange(pow(2, n))
entropy = randbelow(pow(2, n))
nonce = 0
while True:
nonce += 1