From 8a87195f551fff954b44093d7cbab05233b1c634 Mon Sep 17 00:00:00 2001 From: Victor Shyba Date: Fri, 21 Sep 2018 17:12:07 -0300 Subject: [PATCH] ecdsa -> coincurve --- setup.cfg | 2 +- setup.py | 2 +- tests/unit/key_fixtures.py | 65 ++++++++++++++++++++ tests/unit/test_bip32.py | 33 ++++++++-- torba/bip32.py | 122 ++++++++----------------------------- torba/mnemonic.py | 6 +- 6 files changed, 124 insertions(+), 106 deletions(-) create mode 100644 tests/unit/key_fixtures.py diff --git a/setup.cfg b/setup.cfg index de6c397aa..30f659a16 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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] diff --git a/setup.py b/setup.py index 49fca8b3b..f5b827c0c 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( python_requires='>=3.6', install_requires=( 'twisted', - 'ecdsa', + 'coincurve', 'pbkdf2', 'cryptography' ), diff --git a/tests/unit/key_fixtures.py b/tests/unit/key_fixtures.py new file mode 100644 index 000000000..a5cdc04c7 --- /dev/null +++ b/tests/unit/key_fixtures.py @@ -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' +] diff --git a/tests/unit/test_bip32.py b/tests/unit/test_bip32.py index 5bd9fc407..59e7756f2 100644 --- a/tests/unit/test_bip32.py +++ b/tests/unit/test_bip32.py @@ -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:'), diff --git a/torba/bip32.py b/torba/bip32.py index 7ef76aea6..7772356ac 100644 --- a/torba/bip32.py +++ b/torba/bip32.py @@ -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: diff --git a/torba/mnemonic.py b/torba/mnemonic.py index c207bbab9..11451b6d9 100644 --- a/torba/mnemonic.py +++ b/torba/mnemonic.py @@ -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