ofek 7 years ago
parent
commit
db0516a95d
  1. 1
      coincurve/__init__.py
  2. 81
      coincurve/keys.py
  3. 58
      coincurve/utils.py
  4. 2
      setup.py
  5. 13
      tests/samples.py
  6. 23
      tests/test_keys.py

1
coincurve/__init__.py

@ -1,2 +1,3 @@
from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.keys import PrivateKey, PublicKey
from coincurve.utils import verify_signature

81
coincurve/keys.py

@ -1,7 +1,15 @@
from coincurve import GLOBAL_CONTEXT
from coincurve.ecdsa import cdata_to_der, recoverable_to_der
from asn1crypto.keys import (
ECDomainParameters, ECPointBitString, ECPrivateKey, PrivateKeyAlgorithm,
PrivateKeyInfo
)
from coincurve.context import GLOBAL_CONTEXT
from coincurve.ecdsa import cdata_to_der, der_to_cdata, recoverable_to_der
from coincurve.flags import EC_COMPRESSED, EC_UNCOMPRESSED
from coincurve.utils import get_valid_secret, sha256, validate_secret
from coincurve.utils import (
bytes_to_int, der_to_pem, get_valid_secret, int_to_bytes, pem_to_der,
sha256, validate_secret
)
from ._libsecp256k1 import ffi, lib
@ -14,12 +22,6 @@ class PrivateKey:
self.secret, self.context
)
def update_public_key(self):
res = lib.secp256k1_ec_pubkey_create(
self.context.ctx, self.public_key.public_key, self.secret
)
assert res == 1
def sign(self, message, hasher=sha256):
msg_hash = hasher(message)
if len(msg_hash) != 32:
@ -70,7 +72,7 @@ class PrivateKey:
if update:
self.secret = secret
self.update_public_key()
self._update_public_key()
return self
return PrivateKey(secret, self.context)
@ -95,11 +97,64 @@ class PrivateKey:
if update:
self.secret = secret
self.update_public_key()
self._update_public_key()
return self
return PrivateKey(secret, self.context)
def to_int(self):
return bytes_to_int(self.secret)
def to_pem(self):
return der_to_pem(self.to_der())
def to_der(self):
pk = ECPrivateKey({
'version': 'ecPrivkeyVer1',
'private_key': self.to_int(),
'public_key': ECPointBitString(
self.public_key.format(compressed=False)
)
})
return PrivateKeyInfo({
'version': 0,
'private_key_algorithm': PrivateKeyAlgorithm({
'algorithm': 'ec',
'parameters': ECDomainParameters(
name='named',
value='1.3.132.0.10'
)}),
'private_key': pk
}).dump()
@classmethod
def from_int(cls, num):
return PrivateKey(int_to_bytes(num))
@classmethod
def from_pem(cls, pem):
return PrivateKey(
int_to_bytes(
PrivateKeyInfo.load(
pem_to_der(pem)
).native['private_key']['private_key'])
)
@classmethod
def from_der(cls, der):
return PrivateKey(
int_to_bytes(
PrivateKeyInfo.load(der).native['private_key']['private_key']
)
)
def _update_public_key(self):
res = lib.secp256k1_ec_pubkey_create(
self.context.ctx, self.public_key.public_key, self.secret
)
assert res == 1
class PublicKey:
def __init__(self, data, context=GLOBAL_CONTEXT):
@ -172,10 +227,10 @@ class PublicKey:
raise ValueError('Message hash must be 32 bytes long.')
verified = lib.secp256k1_ecdsa_verify(
self.context.ctx, signature, msg_hash, self.public_key
self.context.ctx, der_to_cdata(signature), msg_hash, self.public_key
)
# Performance hack to avoid global bool() lookup.
# A performance hack to avoid global bool() lookup.
return not not verified
def ecdh(self, scalar):

58
coincurve/utils.py

@ -1,7 +1,11 @@
from base64 import b64decode, b64encode
from binascii import unhexlify
from hashlib import sha256 as _sha256
from os import urandom
from coincurve.context import GLOBAL_CONTEXT
from ._libsecp256k1 import ffi, lib
GROUP_ORDER = (b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff'
b'\xfe\xba\xae\xdc\xe6\xafH\xa0;\xbf\xd2^\x8c\xd06AA')
KEY_SIZE = 32
@ -10,6 +14,28 @@ PEM_HEADER = b'-----BEGIN PRIVATE KEY-----\n'
PEM_FOOTER = b'-----END PRIVATE KEY-----\n'
if hasattr(int, "from_bytes"):
def bytes_to_int(bytestr):
return int.from_bytes(bytestr, 'big')
else:
def bytes_to_int(bytestr):
return int(bytestr.encode('hex'), 16)
if hasattr(int, "to_bytes"):
def int_to_bytes(num):
return num.to_bytes((num.bit_length() + 7) // 8 or 1, 'big')
else:
def int_to_bytes(num):
hexed = '{:x}'.format(num)
# Handle odd-length hex strings.
if len(hexed) & 1:
hexed = '0' + hexed
return unhexlify(hexed)
def sha256(bytestr):
return _sha256(bytestr).digest()
@ -48,3 +74,35 @@ def validate_secret(secret):
raise ValueError('Secret scalar must be greater than 0 and less than '
'or equal to the group order.')
return pad_scalar(secret)
def verify_signature(signature, message, public_key, hasher=sha256, context=GLOBAL_CONTEXT):
length = len(public_key)
if length not in (33, 65):
raise ValueError('{} is an invalid length for a public key.'
''.format(length))
pubkey = ffi.new('secp256k1_pubkey *')
res = lib.secp256k1_ec_pubkey_parse(
context.ctx, pubkey, public_key, length
)
assert res == 1
msg_hash = hasher(message)
if len(msg_hash) != 32:
raise ValueError('Message hash must be 32 bytes long.')
sig = ffi.new('secp256k1_ecdsa_signature *')
res = lib.secp256k1_ecdsa_signature_parse_der(
context.ctx, sig, signature, len(signature)
)
assert res == 1
verified = lib.secp256k1_ecdsa_verify(
context.ctx, sig, msg_hash, pubkey
)
# A performance hack to avoid global bool() lookup.
return not not verified

2
setup.py

@ -273,7 +273,7 @@ setup(
download_url='https://github.com/ofek/coincurve',
license='MIT',
install_requires=['cffi>=1.3.0'],
install_requires=['asn1crypto', 'cffi>=1.3.0'],
tests_require=['pytest>=2.8.7'],
packages=find_packages(exclude=('_cffi_build', '_cffi_build.*', 'libsecp256k1')),

13
tests/samples.py

@ -19,3 +19,16 @@ PUBLIC_KEY_UNCOMPRESSED = (b"\x04=\\(u\xc9\xbd\x11hu\xa7\x1a]\xb6L\xff\xcb\x139k
b"LQ[r\xeb\x10\xf1\xfd\x8f?\x03\xb4/J+%[\xfc\x9a\xa9\xe3")
PUBLIC_KEY_X = 27753912938952041417634381842191885283234814940840273460372041880794577257268
PUBLIC_KEY_Y = 53663045980837260634637807506183816949039230809110041985901491152185762425315
MESSAGE = (b'\xdfw\xeb)\t2R8\xda5\x02\xadE\xdd\xce\xd2\xe0\xb4\xf1\x81\xe7\xdf'
b':\xce\x82m\xcf\x99\xf3o\x9d\xe6\xfb\xe4\x98O\x88\xcfh\xbe\xfd\xc2'
b'{\xafm\xb3\xff\xb4QR\xffPu$\xfc>A\'\x03t\xc5\xf9\xd8\xf3I,\xaa"*'
b'\xd7q\xfe\xb7]\x11\xa9uB\'d\x89\x03\'3\xb8/\x80\xa2#\x00\xa2\xfe'
b'\xff\xae\xb0\x86\xc1/ o\xc8]?\xa05L\xff8\x8az\x92\xc9\xab\x9fg0|'
b'\\5\x98\xfaG\x9b#\xec\x1a\xc5\x10\xd6\x08\x9c:\x01"\x0c\x812O/i'
b'\xc4WI\x0c\r\xd8\x81-m1_\x14]$\xf8\x16\xef\x1e\x1d\xb0"Q\x1a\xcf'
b'`R\xae\x0c"r2\x9a\xa3\xdb\xc4W}<c\xd8\x0e\xb5\x96\x99\x87\xdeU'
b'\x84\x1a?No\x10T\xf8\xb8\xd3\x18\xa4\xaf')
SIGNATURE = (b"0E\x02!\x00\xee$\x1b\x0e@fa\xd4<\x17)\xa7\n\xd0\xd7\xef\x90\xcd\x13"
b"\xad`\xc1\x06[\xe0\x821\x96\xe29\x80'\x02 \r\x02\x13\xd2\xaf?\x92G"
b"\x80&8\x1cVz%2\xb0\x8a\xd0l\x0b4\x9c~\x93\x18\xad\xe4J\x9c-\n")

23
tests/test_keys.py

@ -1,7 +1,26 @@
from coincurve import PrivateKey, PublicKey
from .samples import PRIVATE_KEY_BYTES, PUBLIC_KEY_COMPRESSED
from os import urandom
from coincurve.keys import PrivateKey, PublicKey
from coincurve.utils import verify_signature
from .samples import (
PRIVATE_KEY_BYTES, PUBLIC_KEY_COMPRESSED, PUBLIC_KEY_UNCOMPRESSED,
MESSAGE, SIGNATURE
)
class TestPrivateKey:
def test_public_key(self):
assert PrivateKey(PRIVATE_KEY_BYTES).public_key.format() == PUBLIC_KEY_COMPRESSED
def test_signature_correct(self):
private_key = PrivateKey()
public_key = private_key.public_key
message = urandom(200)
signature = private_key.sign(message)
assert verify_signature(signature, message, public_key.format(compressed=True))
assert verify_signature(signature, message, public_key.format(compressed=False))
def test_signature_deterministic(self):
assert PrivateKey(PRIVATE_KEY_BYTES).sign(MESSAGE) == SIGNATURE

Loading…
Cancel
Save