Skip to content

Commit

Permalink
PublicKey backed by cryptography
Browse files Browse the repository at this point in the history
  • Loading branch information
esneider committed May 18, 2015
1 parent 89d1c23 commit 4a826f3
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 143 deletions.
7 changes: 4 additions & 3 deletions bitforge/__init__.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions bitforge/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 12 additions & 8 deletions bitforge/privkey.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
])

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
170 changes: 116 additions & 54 deletions bitforge/pubkey.py
Original file line number Diff line number Diff line change
@@ -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 "<PublicKey: %s, network: %s>" % (self.to_hex(), self.network.name)
37 changes: 36 additions & 1 deletion bitforge/tools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import error
import ecdsa
from cryptography.hazmat.primitives.asymmetric import ec

from bitforge import error


class Buffer(bytearray):
Expand Down Expand Up @@ -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
Loading

0 comments on commit 4a826f3

Please sign in to comment.