forked from LBRYCommunity/lbry-sdk
ecdsa -> coincurve
This commit is contained in:
parent
0b75bb4052
commit
8a87195f55
6 changed files with 124 additions and 106 deletions
|
@ -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]
|
||||
|
|
2
setup.py
2
setup.py
|
@ -27,7 +27,7 @@ setup(
|
|||
python_requires='>=3.6',
|
||||
install_requires=(
|
||||
'twisted',
|
||||
'ecdsa',
|
||||
'coincurve',
|
||||
'pbkdf2',
|
||||
'cryptography'
|
||||
),
|
||||
|
|
65
tests/unit/key_fixtures.py
Normal file
65
tests/unit/key_fixtures.py
Normal 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'
|
||||
]
|
|
@ -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:'),
|
||||
|
|
122
torba/bip32.py
122
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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue