diff --git a/evm/ecc/backends/coincurve.py b/evm/ecc/backends/coincurve.py index a4c3ccf300..333341fc2a 100644 --- a/evm/ecc/backends/coincurve.py +++ b/evm/ecc/backends/coincurve.py @@ -1,26 +1,15 @@ from .base import BaseECCBackend from evm.utils.ecdsa import ( + decode_signature, encode_signature, ) -from evm.utils.numeric import ( - big_endian_to_int, - safe_ord -) - -from evm.constants import ( - NULL_BYTE, -) - from evm.utils.keccak import ( keccak, ) -from evm.utils.secp256k1 import ( - decode_public_key, - encode_raw_public_key, -) +from evm.utils.secp256k1 import decode_public_key class CoinCurveECCBackend(BaseECCBackend): @@ -40,42 +29,20 @@ def ecdsa_sign(self, msg, private_key): def ecdsa_raw_sign(self, msg_hash, private_key): signature = self.keys.PrivateKey(private_key).sign_recoverable(msg_hash, hasher=None) - v = safe_ord(signature[64]) + 27 - r = big_endian_to_int(signature[0:32]) - s = big_endian_to_int(signature[32:64]) - return v, r, s + return decode_signature(signature) def ecdsa_verify(self, msg, signature, public_key): - signature = signature[1:] + NULL_BYTE - signature = self.__recoverable_to_normal(signature) - return self.keys.PublicKey(public_key).verify(signature, msg, hasher=keccak) + return self.ecdsa_recover(msg, signature) == public_key def ecdsa_raw_verify(self, msg_hash, vrs, raw_public_key): - v, r, s = vrs - signature = encode_signature(v, r, s)[1:] + NULL_BYTE - signature = self.__recoverable_to_normal(signature) - public_key = encode_raw_public_key(raw_public_key) - return self.keys.PublicKey(public_key).verify(signature, msg_hash, hasher=None) + return self.ecdsa_raw_recover(msg_hash, vrs) == raw_public_key def ecdsa_recover(self, msg, signature): - signature = signature[1:] + NULL_BYTE - return self.keys.PublicKey.from_signature_and_message(signature, - msg, - hasher=keccak - ).format(compressed=False) + return self.keys.PublicKey.from_signature_and_message( + signature, msg, hasher=keccak).format(compressed=False)[1:] def ecdsa_raw_recover(self, msg_hash, vrs): - v, r, s = vrs - signature = encode_signature(v, r, s)[1:] + NULL_BYTE - raw_public_key = self.keys.PublicKey.from_signature_and_message(signature, - msg_hash, - hasher=None - ).format(compressed=False) - return decode_public_key(raw_public_key) - - def __recoverable_to_normal(self, signature): - return self.ecdsa.cdata_to_der( - self.ecdsa.recoverable_convert( - self.ecdsa.deserialize_recoverable(signature) - ) - ) + signature = encode_signature(*vrs) + public_key = self.keys.PublicKey.from_signature_and_message( + signature, msg_hash, hasher=None).format(compressed=False)[1:] + return decode_public_key(public_key) diff --git a/evm/utils/address.py b/evm/utils/address.py index 5b5cb9c401..2cf9d71d09 100644 --- a/evm/utils/address.py +++ b/evm/utils/address.py @@ -27,4 +27,8 @@ def private_key_to_address(private_key): def public_key_to_address(public_key): - return keccak(public_key[1:])[-20:] + if len(public_key) != 64: + raise ValueError( + "Unexpected public key format: {}. Public keys must be 64 bytes long and must not " + "include the fixed \x04 prefix".format(public_key)) + return keccak(public_key)[-20:] diff --git a/evm/utils/ecdsa.py b/evm/utils/ecdsa.py index 591fb3da91..63d31f2a27 100644 --- a/evm/utils/ecdsa.py +++ b/evm/utils/ecdsa.py @@ -49,11 +49,11 @@ class BadSignature(ValueError): def encode_signature(v, r, s): - vb = int_to_byte(v) + vb = int_to_byte(v - 27) rb = pad32(int_to_big_endian(r)) sb = pad32(int_to_big_endian(s)) - return b''.join((vb, rb, sb)) + return b''.join((rb, sb, vb)) def decode_signature(signature): @@ -64,13 +64,9 @@ def decode_signature(signature): "signature must be exactly 65 bytes in length: got {0}".format(len(signature)) ) - rb = signature[1:33] - sb = signature[33:65] - - v = signature[0] - r = big_endian_to_int(rb) - s = big_endian_to_int(sb) - + r = big_endian_to_int(signature[0:32]) + s = big_endian_to_int(signature[32:64]) + v = signature[64] + 27 return v, r, s diff --git a/evm/utils/secp256k1.py b/evm/utils/secp256k1.py index 25b54983c5..af8b5f911a 100644 --- a/evm/utils/secp256k1.py +++ b/evm/utils/secp256k1.py @@ -19,15 +19,18 @@ def decode_public_key(public_key): - left = big_endian_to_int(public_key[1:33]) - right = big_endian_to_int(public_key[33:65]) + if len(public_key) != 64: + raise ValueError( + "Unexpected public key format: {}. Public keys must be 64 bytes long and must not " + "include the fixed \x04 prefix".format(public_key)) + left = big_endian_to_int(public_key[0:32]) + right = big_endian_to_int(public_key[32:64]) return left, right def encode_raw_public_key(raw_public_key): left, right = raw_public_key return b''.join(( - b'\x04', pad32(int_to_big_endian(left)), pad32(int_to_big_endian(right)), )) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/coincurve-ecc/test_coincurve_ecc_backend.py b/tests/coincurve-ecc/test_coincurve_ecc_backend.py deleted file mode 100644 index c3306695dd..0000000000 --- a/tests/coincurve-ecc/test_coincurve_ecc_backend.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import absolute_import - -import pytest - -from evm.ecc.backends.pure_python import PurePythonECCBackend -from evm.ecc import ( - get_ecc_backend, -) -from evm.utils.secp256k1 import ( - decode_public_key, -) - - -PRIVATE_KEY = ( - b'E\xa9\x15\xe4\xd0`\x14\x9e\xb46Y`\xe6\xa7\xa4_3C\x93\t0a\x11k\x19~2@\x06_\xf2\xd8' -) -PUBLIC_KEY = ( - b'\x04:QAvFo\xa8\x15\xedH\x1f\xfa\xd0\x91\x10\xa2\xd3D\xf6\xc9\xb7\x8c\x1d\x14\xaf\xc3Q\xc3\xa5\x1b\xe3=\x80r\xe7y9\xdc\x03\xbaDy\x07y\xb7\xa1\x02\x5b\xaf0\x03\xf6s$0\xe2\x0c\xd9\xb7m\x953\x91\xb3' # noqa: E501 -) -RAW_PUBLIC_KEY = decode_public_key(PUBLIC_KEY) - -MSG = b'my message' -MSG_HASH = b'#tpO\xbbmDaqK\xcb\xab\xebj\x16\x0c"E\x9ex\x1b\x08\\\x83lI\x08JG\x0e\xd6\xa4' - -SIGNATURE = ( - b'\x1bw\x84\xe4V\x19\x85\xaf\xeaj\xa9q\x9b\xcf\xc2\xbf\x17\x0c\x8c\xd7\xc3\xd5\xc0\x11\xbd\x80A\xc8\xdbJ\x97\x83\x07\x5b\xb1\xdaD\x00\xe1\xd9#W\x83\rF\xa5gx\xc9"\xdem\xbfu\xa2\x19\xfe\xc6\x83IM\xc7o\xfd\x7f' # noqa: E501 -) - -V = 27 -R = 54060028713369731575288880898058519584012347418583874062392262086259746767623 -S = 41474707565615897636207177895621376369577110960831782659442889110043833138559 - -pytest.importorskip('coincurve') -purePython = PurePythonECCBackend() - - -@pytest.fixture(autouse=True) -def with_coincurve_ecc_backend(monkeypatch): - monkeypatch.setenv( - 'CHAIN_ECC_BACKEND_CLASS', - 'evm.ecc.backends.coincurve.CoinCurveECCBackend', - ) - - -def test_ecdsa_sign(): - signature = get_ecc_backend().ecdsa_sign(MSG, PRIVATE_KEY) - assert signature == purePython.ecdsa_sign(MSG, PRIVATE_KEY) - - -def test_ecdsa_raw_sign(): - raw_signature = get_ecc_backend().ecdsa_raw_sign(MSG_HASH, PRIVATE_KEY) - assert raw_signature == purePython.ecdsa_raw_sign(MSG_HASH, PRIVATE_KEY) - - -def test_ecdsa_verify(): - is_valid = get_ecc_backend().ecdsa_verify(MSG, SIGNATURE, PUBLIC_KEY) - assert is_valid is purePython.ecdsa_verify(MSG, SIGNATURE, PUBLIC_KEY) - - -def test_ecdsa_raw_verify(): - is_valid = get_ecc_backend().ecdsa_raw_verify(MSG_HASH, (V, R, S), RAW_PUBLIC_KEY) - assert is_valid is purePython.ecdsa_raw_verify(MSG_HASH, (V, R, S), RAW_PUBLIC_KEY) - - -def test_ecdsa_recover(): - public_key = get_ecc_backend().ecdsa_recover(MSG, SIGNATURE) - assert public_key == purePython.ecdsa_recover(MSG, SIGNATURE) - - -def test_ecdsa_raw_recover(): - raw_public_key = get_ecc_backend().ecdsa_raw_recover(MSG_HASH, (V, R, S)) - assert raw_public_key == purePython.ecdsa_raw_recover(MSG_HASH, (V, R, S)) diff --git a/tests/core/ecdsa-utils/test_ecdsa_utils.py b/tests/core/ecdsa-utils/test_ecdsa_utils.py index 8a4610da77..dd6994abc3 100644 --- a/tests/core/ecdsa-utils/test_ecdsa_utils.py +++ b/tests/core/ecdsa-utils/test_ecdsa_utils.py @@ -1,6 +1,6 @@ -from evm.utils.keccak import ( - keccak, -) +import pytest + +from evm.utils.keccak import keccak from evm.utils.secp256k1 import ( decode_public_key, encode_raw_public_key, @@ -8,75 +8,27 @@ from evm.utils.ecdsa import ( encode_signature, decode_signature, - ecdsa_sign, - ecdsa_raw_sign, - ecdsa_verify, - ecdsa_raw_verify, ecdsa_verify_address, - ecdsa_recover, - ecdsa_raw_recover, -) - - -PRIVATE_KEY = ( - b'E\xa9\x15\xe4\xd0`\x14\x9e\xb46Y`\xe6\xa7\xa4_3C\x93\t0a\x11k\x19~2@\x06_\xf2\xd8' ) -PUBLIC_KEY = ( - b'\x04:QAvFo\xa8\x15\xedH\x1f\xfa\xd0\x91\x10\xa2\xd3D\xf6\xc9\xb7\x8c\x1d\x14\xaf\xc3Q\xc3\xa5\x1b\xe3=\x80r\xe7y9\xdc\x03\xbaDy\x07y\xb7\xa1\x02\x5b\xaf0\x03\xf6s$0\xe2\x0c\xd9\xb7m\x953\x91\xb3' # noqa: E501 +from tests.ecdsa_fixtures import ( + MSG, + SECRETS, ) -RAW_PUBLIC_KEY = decode_public_key(PUBLIC_KEY) -ADDRESS = ( - b'\xa9OSt\xfc\xe5\xed\xbc\x8e*\x86\x97\xc1S1g~n\xbf\x0b' -) - -MSG = b'my message' -MSG_HASH = b'#tpO\xbbmDaqK\xcb\xab\xebj\x16\x0c"E\x9ex\x1b\x08\\\x83lI\x08JG\x0e\xd6\xa4' - -V = 27 -R = 54060028713369731575288880898058519584012347418583874062392262086259746767623 -S = 41474707565615897636207177895621376369577110960831782659442889110043833138559 - - -assert keccak(MSG) == MSG_HASH - - -assert encode_raw_public_key(decode_public_key(PUBLIC_KEY)) == PUBLIC_KEY - - -def test_raw_signing(): - v, r, s = ecdsa_raw_sign(MSG_HASH, PRIVATE_KEY) - assert ecdsa_raw_verify(MSG_HASH, (v, r, s), RAW_PUBLIC_KEY) - - -def test_raw_recover(): - raw_public_key = ecdsa_raw_recover(MSG_HASH, (V, R, S)) - recovered_public_key = encode_raw_public_key(raw_public_key) - assert recovered_public_key == PUBLIC_KEY - - -def test_raw_verify(): - assert ecdsa_raw_verify(MSG_HASH, (V, R, S), RAW_PUBLIC_KEY) - - -def test_signature_encoding_and_decoding(): - signature = encode_signature(V, R, S) - v, r, s, = decode_signature(signature) - assert v == V - assert r == R - assert s == S -def test_signing_and_verifying_with_public_key(): - signature = ecdsa_sign(MSG, PRIVATE_KEY) - assert ecdsa_verify(MSG, signature, PUBLIC_KEY) +@pytest.mark.parametrize("label, d", sorted(SECRETS.items())) +def test_encode_decode_raw_public_key(label, d): + assert encode_raw_public_key(decode_public_key(d['pubkey'])) == d['pubkey'] -def test_signing_and_verifying_with_address(): - signature = ecdsa_sign(MSG, PRIVATE_KEY) - assert ecdsa_verify_address(MSG, signature, ADDRESS) +@pytest.mark.parametrize("label, d", sorted(SECRETS.items())) +def test_signature_encoding_and_decoding(label, d): + v, r, s, = decode_signature(d['sig']) + assert (v, r, s) == d['raw_sig'] + assert encode_signature(v, r, s) == d['sig'] -def test_recovering_public_key(): - signature = ecdsa_sign(MSG, PRIVATE_KEY) - recovered_public_key = ecdsa_recover(MSG, signature) - assert recovered_public_key == PUBLIC_KEY +@pytest.mark.parametrize("label, d", sorted(SECRETS.items())) +def test_verify_address(label, d): + addr = keccak(d['pubkey'])[-20:] + assert ecdsa_verify_address(MSG, d['sig'], addr) diff --git a/tests/core/secp256k1-utils/test_public_key_conversion.py b/tests/core/secp256k1-utils/test_public_key_conversion.py index a5fadb5ebf..d61af5a16e 100644 --- a/tests/core/secp256k1-utils/test_public_key_conversion.py +++ b/tests/core/secp256k1-utils/test_public_key_conversion.py @@ -4,16 +4,10 @@ private_key_to_public_key, ) +from tests.ecdsa_fixtures import SECRETS -@pytest.mark.parametrize( - 'private_key,expected', - ( - ( - b"\xe9\x87=y\xc6\xd8}\xc0\xfbjWxc3\x89\xf4E2\x130=\xa6\x1f \xbdg\xfc#:\xa32b", - b"\x04X\x8d *\xfc\xc1\xeeJ\xb5%LxG\xec%\xb9\xa15\xbb\xda\x0f+\xc6\x9e\xe1\xa7\x14t\x9f\xd7}\xc9\xf8\x8f\xf2\xa0\r~u-D\xcb\xe1n\x1e\xbc\xf0\x89\x0bv\xec|x\x88a\t\xde\xe7l\xcf\xc8DT$", # noqa: E501 - ), - ), -) -def test_private_key_to_public_key(private_key, expected): - actual = private_key_to_public_key(private_key) - assert actual == expected + +@pytest.mark.parametrize('label, d', sorted(SECRETS.items())) +def test_private_key_to_public_key(label, d): + actual = private_key_to_public_key(d['privkey']) + assert actual == d['pubkey'] diff --git a/tests/ecc/test_backends.py b/tests/ecc/test_backends.py new file mode 100644 index 0000000000..a14bd43e33 --- /dev/null +++ b/tests/ecc/test_backends.py @@ -0,0 +1,61 @@ +import pytest + +from evm.ecc.backends.pure_python import PurePythonECCBackend +from evm.ecc.backends.coincurve import CoinCurveECCBackend +from evm.utils.ecdsa import decode_signature +from evm.utils.secp256k1 import ( + decode_public_key, + encode_raw_public_key, +) + +from tests.ecdsa_fixtures import ( + MSG, + MSGHASH, + SECRETS, +) + + +backends = [PurePythonECCBackend()] +try: + backends.append(CoinCurveECCBackend()) +except ImportError: + pass + + +@pytest.mark.parametrize("backend", backends) +@pytest.mark.parametrize("label, d", sorted(SECRETS.items())) +def test_ecdsa_sign(backend, label, d): + assert backend.ecdsa_sign(MSG, d['privkey']) == d['sig'] + + +@pytest.mark.parametrize("backend", backends) +@pytest.mark.parametrize("label, d", sorted(SECRETS.items())) +def test_ecdsa_raw_sign(backend, label, d): + assert backend.ecdsa_raw_sign(MSGHASH, d['privkey']) == d['raw_sig'] + + +@pytest.mark.parametrize("backend", backends) +@pytest.mark.parametrize("label, d", sorted(SECRETS.items())) +def test_ecdsa_verify(backend, label, d): + assert backend.ecdsa_verify(MSG, d['sig'], d['pubkey']) + + +@pytest.mark.parametrize("backend", backends) +@pytest.mark.parametrize("label, d", sorted(SECRETS.items())) +def test_ecdsa_raw_verify(backend, label, d): + assert backend.ecdsa_raw_verify( + MSGHASH, decode_signature(d['sig']), decode_public_key(d['pubkey'])) + + +@pytest.mark.parametrize("backend", backends) +@pytest.mark.parametrize("label, d", sorted(SECRETS.items())) +def test_ecdsa_recover(backend, label, d): + pubkey = backend.ecdsa_recover(MSG, d['sig']) + assert pubkey == d['pubkey'] + + +@pytest.mark.parametrize("backend", backends) +@pytest.mark.parametrize("label, d", sorted(SECRETS.items())) +def test_ecdsa_raw_recover(backend, label, d): + raw_public_key = backend.ecdsa_raw_recover(MSGHASH, d['raw_sig']) + assert encode_raw_public_key(raw_public_key) == d['pubkey'] diff --git a/tests/ecdsa_fixtures.py b/tests/ecdsa_fixtures.py new file mode 100644 index 0000000000..9863b1e218 --- /dev/null +++ b/tests/ecdsa_fixtures.py @@ -0,0 +1,51 @@ +from rlp.utils import decode_hex + +from evm.utils.keccak import keccak + + +MSG = b'message' +MSGHASH = keccak(MSG) + +# This is a sample of signatures generated with a known-good implementation of the ECDSA +# algorithm, which we use to test our ECC backends. If necessary, it can be generated from scratch +# with the following code: +""" +from devp2p import crypto +from rlp.utils import encode_hex +msg = b'message' +msghash = crypto.sha3(b'message') +for secret in ['alice', 'bob', 'eve']: + print("'{}': dict(".format(secret)) + privkey = crypto.mk_privkey(secret) + pubkey = crypto.privtopub(privkey) + print(" privkey='{}',".format(encode_hex(privkey))) + print(" pubkey='{}',".format(encode_hex(crypto.privtopub(privkey)))) + ecc = crypto.ECCx(raw_privkey=privkey) + sig = ecc.sign(msghash) + print(" sig='{}',".format(encode_hex(sig))) + print(" raw_sig='{}')".format(crypto._decode_sig(sig))) + assert crypto.ecdsa_recover(msghash, sig) == pubkey +""" +SECRETS = { + "alice": dict( + privkey=decode_hex(b'9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501'), + pubkey=decode_hex(b'5eed5fa3a67696c334762bb4823e585e2ee579aba3558d9955296d6c04541b426078dbd48d74af1fd0c72aa1a05147cf17be6b60bdbed6ba19b08ec28445b0ca'), # noqa: E501 + sig=decode_hex(b'b20e2ea5d3cbaa83c1e0372f110cf12535648613b479b64c1a8c1a20c5021f380434d07ec5795e3f789794351658e80b7faf47a46328f41e019d7b853745cdfd01'), # noqa: E501 + raw_sig=(28, + 80536744857756143861726945576089915884233437828013729338039544043241440681784, + 1902566422691403459035240420865094128779958320521066670269403689808757640701)), + "bob": dict( + privkey=decode_hex(b'38e47a7b719dce63662aeaf43440326f551b8a7ee198cee35cb5d517f2d296a2'), + pubkey=decode_hex(b'347746ccb908e583927285fa4bd202f08e2f82f09c920233d89c47c79e48f937d049130e3d1c14cf7b21afefc057f71da73dec8e8ff74ff47dc6a574ccd5d570'), # noqa: E501 + sig=decode_hex(b'5c48ea4f0f2257fa23bd25e6fcb0b75bbe2ff9bbda0167118dab2bb6e31ba76e691dbdaf2a231fc9958cd8edd99507121f8184042e075cf10f98ba88abff1f3601'), # noqa: E501 + raw_sig=(28, + 41741612198399299636429810387160790514780876799439767175315078161978521003886, + 47545396818609319588074484786899049290652725314938191835667190243225814114102)), + "eve": dict( + privkey=decode_hex(b'876be0999ed9b7fc26f1b270903ef7b0c35291f89407903270fea611c85f515c'), + pubkey=decode_hex(b'c06641f0d04f64dba13eac9e52999f2d10a1ff0ca68975716b6583dee0318d91e7c2aed363ed22edeba2215b03f6237184833fd7d4ad65f75c2c1d5ea0abecc0'), # noqa: E501 + sig=decode_hex(b'babeefc5082d3ca2e0bc80532ab38f9cfb196fb9977401b2f6a98061f15ed603603d0af084bf906b2cdf6cdde8b2e1c3e51a41af5e9adec7f3643b3f1aa2aadf00'), # noqa: E501 + raw_sig=(27, + 84467545608142925331782333363288012579669270632210954476013542647119929595395, + 43529886636775750164425297556346136250671451061152161143648812009114516499167)) + } diff --git a/tox.ini b/tox.ini index 3e45b8aca2..c1fc698cc8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist= - py{27,34,35}-{core,coincurve,leveldb, state-all,state-fast,state-slow,blockchain-fast,blockchain-slow,transactions,vm-all,vm-fast,vm-limits,vm-performance} + py{27,34,35}-{core,ecc,leveldb, state-all,state-fast,state-slow,blockchain-fast,blockchain-slow,transactions,vm-all,vm-fast,vm-limits,vm-performance} flake8 [flake8] @@ -16,7 +16,7 @@ commands= blockchain-fast: py.test {posargs:tests/json-fixtures/test_blockchain.py -m "not blockchain_slow"} blockchain-slow: py.test {posargs:tests/json-fixtures/test_blockchain.py -m blockchain_slow} core: py.test {posargs:tests/core} - coincurve: py.test {posargs:tests/coincurve-ecc} + ecc: py.test {posargs:tests/ecc} leveldb: py.test {posargs:tests/level-db} state-all: py.test {posargs:tests/json-fixtures/test_state.py} state-fast: py.test {posargs:tests/json-fixtures/test_state.py -m "not state_slow"}