diff --git a/bitforge/__init__.py b/bitforge/__init__.py index d897406..af0f422 100644 --- a/bitforge/__init__.py +++ b/bitforge/__init__.py @@ -1,13 +1,14 @@ from network import Network from privkey import PrivateKey -# from pubkey import PublicKey +from pubkey import PublicKey # from address import Address # from script import Script # from hdprivkey import HDPrivateKey from unit import Unit -from uri import URI +# from uri import URI import network import privkey +import pubkey import unit -import uri +# import uri diff --git a/bitforge/network.py b/bitforge/network.py index b33885f..06ddf0d 100644 --- a/bitforge/network.py +++ b/bitforge/network.py @@ -29,6 +29,7 @@ class InvalidField(Error, error.StringError): 'aliases', # All the network aliases # Cryptography parameters + # WARNING: only curves over prime fields are currently supported 'curve', # Elliptic curve used for the crypto 'hash_function', # Signature hashing function diff --git a/bitforge/privkey.py b/bitforge/privkey.py index 47ca0c0..e06b5f5 100644 --- a/bitforge/privkey.py +++ b/bitforge/privkey.py @@ -3,7 +3,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import ec -from bitforge import encoding, error, network, tools +from bitforge import encoding, error, network, pubkey, tools class Error(error.BitforgeError): @@ -20,7 +20,7 @@ class InvalidEncoding(Error): BasePrivateKey = collections.namedtuple('PrivateKey', [ 'key', # Elliptic curve private key - 'network', + 'network', # Bitcoin-compatible network 'compressed', # Whether the public key should be serialized in compressed format ]) @@ -77,9 +77,12 @@ def from_secret_exponent(cls, exponent, network=network.default, compressed=True @classmethod def from_bytes(cls, data, network=network.default, compressed=True, backend=default_backend()): - """Create a private key from its raw binary encoding. + """Create a private key from its raw binary encoding (in SEC1 format). The input buffer should be a zero-padded big endian unsigned integer. + For more info on this format, see: + + http://www.secg.org/sec1-v2.pdf, section 2.3.6 """ if len(data) != tools.elliptic_curve_key_size(network.curve): @@ -112,20 +115,21 @@ def from_wif(cls, wif, backend=default_backend()): # The first byte determines the network try: - network_ = network.Network.get_by_field('wif_prefix', data[0]) + prefix = data.pop(0) except IndexError: raise InvalidEncoding('Invalid WIF length') + try: + network_ = network.Network.get_by_field('wif_prefix', prefix) + except network.UnknownNetwork as e: raise InvalidEncoding(e.message) - data.pop(0) - - key_size = tools.elliptic_curve_key_size(network_.curve) - # If the public key should be compressed-encoded, there will be an # extra 1 byte at the end + key_size = tools.elliptic_curve_key_size(network_.curve) + compressed = True if len(data) == key_size + 1 else False if compressed and data[-1] == 1: diff --git a/bitforge/pubkey.py b/bitforge/pubkey.py index e78ce82..7850f6e 100644 --- a/bitforge/pubkey.py +++ b/bitforge/pubkey.py @@ -1,84 +1,146 @@ import collections -from utils.secp256k1 import generator_secp256k1 -import network, utils -from network import Network -from address import Address -from errors import * -from encoding import * +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import ec -def find_network(value, attr = 'name'): - try: - return Network.get_by_field(attr, value) - except: - raise PublicKey.UnknownNetwork(attr, value) +from bitforge import encoding, error, network, tools -BasePublicKey = collections.namedtuple('PublicKey', - ['pair', 'network', 'compressed'] -) +# Magic numbers for the SEC1 public key format (TODO: shouldn't be here!) +SEC1_MAGIC_COMPRESSED_0 = 2 +SEC1_MAGIC_COMPRESSED_1 = 3 +SEC1_MAGIC_NOT_COMPRESSED = 4 -class PublicKey(BasePublicKey): - class Error(BitforgeError): - pass +class Error(error.BitforgeError): + pass + - class InvalidPair(Error, ObjectError): - "The PublicKey pair {object} is invalid (not a point of the curve)" +class InvalidPoint(Error): + """Invalid key (the point represented by the key is not on the curve)""" - class UnknownNetwork(Error, Network.UnknownNetwork): - "No network for PublicKey with an attribute '{key}' of value {value}" - class InvalidBinary(Error, StringError): - "The buffer {string} is not in any recognized format" +class InvalidEncoding(Error): + pass - class InvalidHex(Error, InvalidHex): - "The PublicKey string {string} is not valid hexadecimal" +BasePublicKey = collections.namedtuple('PublicKey', [ + 'key', # Elliptic curve public key + 'network', # Bitcoin-compatible network + 'compressed', # Whether the key should be serialized in compressed format +]) - def __new__(cls, pair, network = network.default, compressed = True): - if not utils.ecdsa.is_public_pair_valid(generator_secp256k1, pair): - raise PublicKey.InvalidPair(pair) - return super(PublicKey, cls).__new__(cls, pair, network, compressed) +class PublicKey(BasePublicKey): + """Bitcoin public key.""" + + def __new__(cls, key, network=network.default, compressed=True): + """Create a Bitcoin public key from an EC public key.""" - @staticmethod - def from_private_key(privkey): - pair = utils.public_pair_for_secret_exponent( - utils.generator_secp256k1, privkey.secret - ) + return super(PublicKey, cls).__new__(cls, key, network, compressed) - # The constructor will validate the pair - return PublicKey(pair, privkey.network, privkey.compressed) + @classmethod + def from_point(cls, x, y, network=network.default, compressed=True, backend=default_backend()): + """Create a public key from its point coordinates. + + A public key is a point on an elliptic curve, i.e. a pair (x, y) that + satisfies the curve equation. + """ + + public_numbers = ec.EllipticCurvePublicNumbers(x, y, network.curve) - @staticmethod - def from_bytes(bytes, network = network.default): try: - pair = utils.encoding.sec_to_public_pair(bytes) - compressed = utils.encoding.is_sec_compressed(bytes) - except: - raise PublicKey.InvalidBinary(bytes) + key = public_numbers.public_key(backend) + except ValueError: + raise InvalidPoint() + + return cls(key, network, compressed) - return PublicKey(pair, network, compressed) + @classmethod + def from_bytes(cls, data, network=network.default, backend=default_backend()): + """Create a public key from its raw binary encoding (in SEC1 format). - @staticmethod - def from_hex(string, network = network.default): + For more info on this format, see: + + http://www.secg.org/sec1-v2.pdf, section 2.3.4 + """ + + data = bytearray(data) + + # A public key is a point (x, y) in the elliptic curve, and each + # coordinate is represented by a unsigned integer of key_size bytes + key_size = tools.elliptic_curve_key_size(network.curve) + + # The first byte determines whether the key is compressed try: - bytes = decode_hex(string) - except InvalidHex: - raise PublicKey.InvalidHex(string) + prefix = data.pop(0) + + except IndexError: + raise InvalidEncoding('Invalid key length (buffer is empty)') + + # If the key is compressed-encoded, only the x coordinate is present + compressed = True if len(data) == key_size else False - return PublicKey.from_bytes(bytes, network) + if not compressed and len(data) != 2 * key_size: + raise InvalidEncoding('Invalid key length') + # The first key_size bytes after the prefix are the x coordinate + x = encoding.b2i_bigendian(bytes(data[:key_size])) + + if compressed: + # If the key is compressed, the y coordinate should be computed + + if prefix == SEC1_MAGIC_COMPRESSED_0: + y_parity = 0 + elif prefix == SEC1_MAGIC_COMPRESSED_1: + y_parity = 1 + else: + raise InvalidEncoding('Invalid prefix for compressed key') + + y = tools.ec_public_y_from_x_and_curve(x, y_parity, network.curve) + if y is None: + raise InvalidPoint() + else: + # If the key isn't compressed, the last key_size bytes are the y + # coordinate + + if prefix != SEC1_MAGIC_NOT_COMPRESSED: + raise InvalidEncoding('Invalid prefix for non-compressed key') + + y = encoding.b2i_bigendian(bytes(data[key_size:])) + + return cls.from_point(x, y, network, compressed, backend) + + # def address(self): + # """TODO""" + # + # return Address.from_public_key(self) def to_bytes(self): - return utils.encoding.public_pair_to_sec(self.pair, self.compressed) + """TODO""" + + public_numbers = self.key.public_numbers() + key_size = tools.elliptic_curve_key_size(self.network.curve) + + prefix = SEC1_MAGIC_NOT_COMPRESSED + + if self.compressed: + if public_numbers.y % 2 == 0: + prefix = SEC1_MAGIC_COMPRESSED_0 + else: + prefix = SEC1_MAGIC_COMPRESSED_1 + + data = bytearray() + data.append(prefix) + + x = encoding.i2b_bigendian(public_numbers.x, key_size) + data.extend(x) - def to_hex(self): - return binascii.hexlify(self.to_bytes()) + if not self.compressed: + y = encoding.i2b_bigendian(public_numbers.y, key_size) + data.extend(y) - def to_address(self): - return Address.from_public_key(self) + return bytes(data) def __repr__(self): return "" % (self.to_hex(), self.network.name) diff --git a/bitforge/tools.py b/bitforge/tools.py index 0b52e36..ba87eeb 100644 --- a/bitforge/tools.py +++ b/bitforge/tools.py @@ -1,4 +1,7 @@ -import error +import ecdsa +from cryptography.hazmat.primitives.asymmetric import ec + +from bitforge import error class Buffer(bytearray): @@ -29,3 +32,35 @@ def elliptic_curve_key_size(curve): """Size (in bytes) of an elliptic curve private key.""" return (curve.key_size + 7) // 8 + + +# TODO: this is a temporary hack. Ideally, it should be implemented by +# cryptography. +def ec_public_y_from_x_and_curve(x, y_parity, curve): + """Compute the y coordinate of a point in an elliptic curve. + + For more info, see: + + http://www.secg.org/sec1-v2.pdf, section 2.3.4, step 2.4.1 + """ + + assert isinstance(curve, ec.SECP256K1) + curve = ecdsa.SECP256k1.curve + + # The curve equation over F_p is: + # y^2 = x^3 + ax + b + a, b, p = curve.a(), curve.b(), curve.p() + + alpha = (pow(x, 3, p) + a * x + b) % p + + try: + beta = ecdsa.numbertheory.square_root_mod_prime(alpha, p) + except ecdsa.numbertheory.SquareRootError: + return None + + beta_parity = beta % 2 + + if beta_parity != y_parity: + beta = p - beta + + return beta diff --git a/tests/pubkey.py b/tests/pubkey.py index cfd16bd..1af6bb7 100644 --- a/tests/pubkey.py +++ b/tests/pubkey.py @@ -1,7 +1,6 @@ -from pytest import raises, fixture, fail -import bitforge.network -from bitforge.privkey import PrivateKey -from bitforge.pubkey import PublicKey +from pytest import raises, fixture + +from bitforge import encoding, privkey, pubkey, network data = { @@ -27,89 +26,75 @@ class TestPublicKey: def test_invalid_pair(self): - with raises(PublicKey.InvalidPair): - PublicKey((0, 0)) - - - def test_unknown_network(self): - with raises(PublicKey.UnknownNetwork): - PublicKey(data['pubkey_pair'], network = 'a') - + with raises(pubkey.InvalidPoint): + pubkey.PublicKey.from_point(0, 0) def test_from_private_key(self): - privkey = PrivateKey.from_hex(data['privkey_hex']) - pubkey = PublicKey.from_private_key(privkey) - - assert pubkey.network is privkey.network - assert pubkey.compressed == privkey.compressed - assert pubkey.pair == data['pubkey_pair'] - - - def test_from_hex_compressed(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed']) - - assert pubkey.pair == data['pubkey_pair'] - assert pubkey.compressed is True - assert pubkey.network is bitforge.network.default - - - def test_to_hex_compressed(self): - string = data['pubkey_hex']['compressed'] - assert PublicKey.from_hex(string).to_hex() == string - - - def test_from_hex_uncompressed(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed']) - - assert pubkey.pair == data['pubkey_pair'] - assert pubkey.compressed is False - assert pubkey.network is bitforge.network.default - - - def test_from_hex_errors(self): - with raises(PublicKey.InvalidHex): PublicKey.from_hex('a') - with raises(PublicKey.InvalidHex): PublicKey.from_hex('a@') - - - def test_to_hex_uncompressed(self): - string = data['pubkey_hex']['uncompressed'] - assert PublicKey.from_hex(string).to_hex() == string - + raw = encoding.a2b_hex(data['privkey_hex']) + priv = privkey.PrivateKey.from_bytes(raw) + pub = priv.public_key() + + assert pub.network is priv.network + assert pub.compressed == priv.compressed + assert pub.key.public_numbers().x == data['pubkey_pair'][0] + assert pub.key.public_numbers().y == data['pubkey_pair'][1] + + # def test_from_hex_compressed(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed']) + # + # assert pubkey.pair == data['pubkey_pair'] + # assert pubkey.compressed is True + # assert pubkey.network is bitforge.network.default + # + # def test_to_hex_compressed(self): + # string = data['pubkey_hex']['compressed'] + # assert PublicKey.from_hex(string).to_hex() == string + # + # def test_from_hex_uncompressed(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed']) + # + # assert pubkey.pair == data['pubkey_pair'] + # assert pubkey.compressed is False + # assert pubkey.network is bitforge.network.default + # + # def test_from_hex_errors(self): + # with raises(PublicKey.InvalidHex): PublicKey.from_hex('a') + # with raises(PublicKey.InvalidHex): PublicKey.from_hex('a@') + # + # def test_to_hex_uncompressed(self): + # string = data['pubkey_hex']['uncompressed'] + # assert PublicKey.from_hex(string).to_hex() == string def test_from_bytes(self): - pubkey = PublicKey.from_bytes(data['pubkey_bin']) + pub = pubkey.PublicKey.from_bytes(data['pubkey_bin']) - assert pubkey.pair == data['pubkey_pair'] - assert pubkey.compressed is True - assert pubkey.network is bitforge.network.default + assert pub.key.public_numbers().x == data['pubkey_pair'][0] + assert pub.key.public_numbers().y == data['pubkey_pair'][1] + assert pub.compressed is True + assert pub.network is network.default - with raises(PublicKey.InvalidBinary): - PublicKey.from_bytes('a') - - with raises(PublicKey.InvalidBinary): - PublicKey.from_bytes('a' * 70) + with raises(pubkey.InvalidEncoding): + pubkey.PublicKey.from_bytes('a') + with raises(pubkey.InvalidEncoding): + pubkey.PublicKey.from_bytes('a' * 70) def test_to_bytes(self): - bytes = data['pubkey_bin'] - assert PublicKey.from_bytes(bytes).to_bytes() == bytes - - - def test_to_address_live_compress(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed'], bitforge.network.livenet) - assert pubkey.to_address().to_string() == data['address']['live_compressed'] - - - def test_to_address_live_uncompress(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed'], bitforge.network.livenet) - assert pubkey.to_address().to_string() == data['address']['live_uncompressed'] + raw = data['pubkey_bin'] + assert pubkey.PublicKey.from_bytes(raw).to_bytes() == raw + # def test_to_address_live_compress(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed'], bitforge.network.livenet) + # assert pubkey.to_address().to_string() == data['address']['live_compressed'] - def test_to_address_test_compress(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed'], bitforge.network.testnet) - assert pubkey.to_address().to_string() == data['address']['test_compressed'] + # def test_to_address_live_uncompress(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed'], bitforge.network.livenet) + # assert pubkey.to_address().to_string() == data['address']['live_uncompressed'] + # def test_to_address_test_compress(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['compressed'], bitforge.network.testnet) + # assert pubkey.to_address().to_string() == data['address']['test_compressed'] - def test_to_address_test_uncompress(self): - pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed'], bitforge.network.testnet) - assert pubkey.to_address().to_string() == data['address']['test_uncompressed'] + # def test_to_address_test_uncompress(self): + # pubkey = PublicKey.from_hex(data['pubkey_hex']['uncompressed'], bitforge.network.testnet) + # assert pubkey.to_address().to_string() == data['address']['test_uncompressed']