diff --git a/.gitignore b/.gitignore index 49bbeffb..05fbfd4a 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ coverage-html .tox nosetests.xml t/ +.hypothesis/ # Translations *.mo diff --git a/.travis.yml b/.travis.yml index f2cbba52..8d1f6a89 100644 --- a/.travis.yml +++ b/.travis.yml @@ -60,7 +60,7 @@ before_install: TRAVIS_COMMIT_RANGE=$PR_FIRST^..$TRAVIS_COMMIT fi # sanity check current commit - - git rev-parse HEAD + - BRANCH=$(git rev-parse HEAD) - echo "TRAVIS_COMMIT_RANGE=$TRAVIS_COMMIT_RANGE" - git fetch origin master:refs/remotes/origin/master @@ -78,7 +78,6 @@ install: script: - if [[ $TOX_ENV ]]; then tox -e $TOX_ENV; fi - tox -e speed - - cp diff-instrumental.py diff-instrumental-2.py - | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then git checkout $PR_FIRST^ @@ -86,23 +85,21 @@ script: files="$(ls src/ecdsa/test*.py | grep -v test_malformed_sigs.py)" instrumental -t ecdsa -i 'test.*|.*_version' `which pytest` $files instrumental -f .instrumental.cov -s - instrumental -f .instrumental.cov -s | python diff-instrumental-2.py --save .diff-instrumental - git checkout $TRAVIS_COMMIT - instrumental -t ecdsa -i 'test.*|.*_version' `which pytest` $files + instrumental -f .instrumental.cov -s | python diff-instrumental.py --save .diff-instrumental + git checkout $BRANCH + instrumental -t ecdsa -i 'test.*|.*_version' `which pytest` src/ecdsa instrumental -f .instrumental.cov -sr fi - | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST == "false" ]]; then - # exclude the super slow test_malformed_sigs.py, until #127 is merged - files="$(ls src/ecdsa/test*.py | grep -v test_malformed_sigs.py)" - instrumental -t ecdsa -i 'test.*|.*_version' `which pytest` $files + instrumental -t ecdsa -i 'test.*|.*_version' `which pytest` src/ecdsa instrumental -f .instrumental.cov -s # just log the values when merging - instrumental -f .instrumental.cov -s | python diff-instrumental-2.py + instrumental -f .instrumental.cov -s | python diff-instrumental.py fi - | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then - instrumental -f .instrumental.cov -s | python diff-instrumental-2.py --read .diff-instrumental --fail-under 70 --max-difference -0.1 + instrumental -f .instrumental.cov -s | python diff-instrumental.py --read .diff-instrumental --fail-under 70 --max-difference -0.1 fi after_success: - if [[ -z $INSTRUMENTAL ]]; then coveralls; fi diff --git a/build-requirements-2.6.txt b/build-requirements-2.6.txt index 980e00df..9f39acd0 100644 --- a/build-requirements-2.6.txt +++ b/build-requirements-2.6.txt @@ -2,3 +2,4 @@ tox coveralls<1.3.0 idna<2.8 unittest2 +hypothesis<3 diff --git a/build-requirements-3.3.txt b/build-requirements-3.3.txt index 68f8c292..28dd401b 100644 --- a/build-requirements-3.3.txt +++ b/build-requirements-3.3.txt @@ -3,3 +3,5 @@ pluggy<0.6 tox<3 wheel<0.30 virtualenv==15.2.0 +enum34 +hypothesis<3.44 diff --git a/build-requirements.txt b/build-requirements.txt index 6918d2e8..072f7a9d 100644 --- a/build-requirements.txt +++ b/build-requirements.txt @@ -1,2 +1,4 @@ tox python-coveralls +hypothesis +pytest>=4.6.0 diff --git a/src/ecdsa/curves.py b/src/ecdsa/curves.py index b61f4b45..2555d938 100644 --- a/src/ecdsa/curves.py +++ b/src/ecdsa/curves.py @@ -8,14 +8,13 @@ # will need to mark it as deprecated later __all__ = ["UnknownCurveError", "orderlen", "Curve", "NIST192p", "NIST224p", "NIST256p", "NIST384p", "NIST521p", "curves", - "find_curve"] + "find_curve", "SECP256k1"] class UnknownCurveError(Exception): pass -# the NIST curves class Curve: def __init__(self, name, curve, generator, oid, openssl_name=None): self.name = name @@ -29,25 +28,41 @@ def __init__(self, name, curve, generator, oid, openssl_name=None): self.oid = oid self.encoded_oid = der.encode_oid(*oid) + def __repr__(self): + return self.name + + +# the NIST curves NIST192p = Curve("NIST192p", ecdsa.curve_192, ecdsa.generator_192, (1, 2, 840, 10045, 3, 1, 1), "prime192v1") + + NIST224p = Curve("NIST224p", ecdsa.curve_224, ecdsa.generator_224, (1, 3, 132, 0, 33), "secp224r1") + + NIST256p = Curve("NIST256p", ecdsa.curve_256, ecdsa.generator_256, (1, 2, 840, 10045, 3, 1, 7), "prime256v1") + + NIST384p = Curve("NIST384p", ecdsa.curve_384, ecdsa.generator_384, (1, 3, 132, 0, 34), "secp384r1") + + NIST521p = Curve("NIST521p", ecdsa.curve_521, ecdsa.generator_521, (1, 3, 132, 0, 35), "secp521r1") + + SECP256k1 = Curve("SECP256k1", ecdsa.curve_secp256k1, ecdsa.generator_secp256k1, (1, 3, 132, 0, 10), "secp256k1") + curves = [NIST192p, NIST224p, NIST256p, NIST384p, NIST521p, SECP256k1] diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index 4f926429..b0898411 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -125,6 +125,11 @@ def __init__(self, _error__please_use_generate=None): self.default_hashfunc = None self.pubkey = None + def __repr__(self): + pub_key = self.to_string("compressed") + return "VerifyingKey.from_string({0!r}, {1!r}, {2})".format( + pub_key, self.curve, self.default_hashfunc().name) + @classmethod def from_public_point(cls, point, curve=NIST192p, hashfunc=sha1): """ diff --git a/src/ecdsa/test_malformed_sigs.py b/src/ecdsa/test_malformed_sigs.py index fffb1b3b..1e800456 100644 --- a/src/ecdsa/test_malformed_sigs.py +++ b/src/ecdsa/test_malformed_sigs.py @@ -1,87 +1,288 @@ from __future__ import with_statement, division -import pytest import hashlib +try: + from hashlib import algorithms_available +except ImportError: + algorithms_available = [ + "md5", "sha1", "sha224", "sha256", "sha384", "sha512"] +from functools import partial +import pytest +import sys +from six import binary_type +import hypothesis.strategies as st +from hypothesis import note, assume, given, settings, example -from six import b, binary_type -from .keys import SigningKey, VerifyingKey +from .keys import SigningKey from .keys import BadSignatureError from .util import sigencode_der, sigencode_string from .util import sigdecode_der, sigdecode_string -from .curves import curves, NIST256p, NIST521p - -der_sigs = [] -example_data = b("some data to sign") - -# Just NIST256p with SHA256 is 560 test cases, all curves with all hashes is -# few thousand slow test cases; execute the most interesting only - -#for curve in curves: -for curve in [NIST521p]: - #for hash_alg in ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"]: - for hash_alg in ["sha256"]: - key = SigningKey.generate(curve) - signature = key.sign(example_data, hashfunc=getattr(hashlib, hash_alg), - sigencode=sigencode_der) - for pos in range(len(signature)): - for xor in (1<= (2, 7): + # deadline=2s because NIST521p are slow to verify + params["deadline"] = 2000 + + +@settings(**params) +@given(st_fuzzed_sig(keys_and_sigs)) +def test_fuzzed_der_signatures(params): + verifying_key, sig = params + + with pytest.raises(BadSignatureError): + verifying_key.verify(sig, example_data, sigdecode=sigdecode_der) + + +@st.composite +def st_random_der_ecdsa_sig_value(draw): + """ + Hypothesis strategy for selecting random values and encoding them + to ECDSA-Sig-Value object:: + + ECDSA-Sig-Value ::= SEQUENCE { + r INTEGER, + s INTEGER + } + """ + name, verifying_key, _ = draw(st.sampled_from(keys_and_sigs)) + note("Configuration: {0}".format(name)) + order = verifying_key.curve.order + + # the encode_integer doesn't suport negative numbers, would be nice + # to generate them too, but we have coverage for remove_integer() + # verifying that it doesn't accept them, so meh + r = draw(st.integers(min_value=0) | + st.integers(min_value=order >> 2, max_value=order+1)) + s = draw(st.integers(min_value=0) | + st.integers(min_value=order >> 2, max_value=order+1)) + + sig = encode_sequence(encode_integer(r), encode_integer(s)) + + return verifying_key, sig + + +@settings(**params) +@given(st_random_der_ecdsa_sig_value()) +def test_random_der_ecdsa_sig_value(params): + """ + Check if random values encoded in ECDSA-Sig-Value structure are rejected + as signature. + """ + verifying_key, sig = params + + with pytest.raises(BadSignatureError): + verifying_key.verify(sig, example_data, sigdecode=sigdecode_der) + + +def st_der_integer(*args, **kwargs): + """ + Hypothesis strategy that returns a random positive integer as DER + INTEGER. + Parameters are passed to hypothesis.strategy.integer. + """ + if "min_value" not in kwargs: + kwargs["min_value"] = 0 + return st.builds(encode_integer, st.integers(*args, **kwargs)) + + +@st.composite +def st_der_bit_string(draw, *args, **kwargs): + """ + Hypothesis strategy that returns a random DER BIT STRING. + Parameters are passed to hypothesis.strategy.binary. + """ + data = draw(st.binary(*args, **kwargs)) + if data: + unused = draw(st.integers(min_value=0, max_value=7)) + data = bytearray(data) + data[-1] &= - (2**unused) + data = bytes(data) + else: + unused = 0 + return encode_bitstring(data, unused) + + +def st_der_octet_string(*args, **kwargs): + """ + Hypothesis strategy that returns a random DER OCTET STRING object. + Parameters are passed to hypothesis.strategy.binary + """ + return st.builds(encode_octet_string, st.binary(*args, **kwargs)) + + +def st_der_null(): + """ + Hypothesis strategy that returns DER NULL object. + """ + return st.just(b'\x05\x00') + + +@st.composite +def st_der_oid(draw): + """ + Hypothesis strategy that returns DER OBJECT IDENTIFIER objects. + """ + first = draw(st.integers(min_value=0, max_value=2)) + second = draw(st.integers(min_value=0, max_value=39)) + rest = draw(st.lists(st.integers(min_value=0))) + return encode_oid(first, second, *rest) + + +def st_der(): + """ + Hypothesis strategy that returns random DER structures. + + A valid DER structure is any primitive object, an octet encoding + of a valid DER structure, sequence of valid DER objects or a constructed + encoding of any of the above. + """ + return st.recursive( + st.just(b'') | st_der_integer() | st_der_bit_string() | + st_der_octet_string() | st_der_null() | st_der_oid(), + lambda children: + st.builds(lambda x: encode_octet_string(x), st.one_of(children)) | + st.builds(lambda x: encode_bitstring(x, 0), st.one_of(children)) | + st.builds(lambda x: encode_sequence(*x), st.lists(children)) | + st.builds(lambda tag, x: + encode_constructed(tag, x), + st.integers(min_value=0, max_value=0x3f), + st.one_of(children)) + ) + + +@settings(**params) +@given(st.sampled_from(keys_and_sigs), st_der()) +def test_random_der_as_signature(params, der): + """Check if random DER structures are rejected as signature""" + name, verifying_key, _ = params + + with pytest.raises(BadSignatureError): + verifying_key.verify(der, example_data, sigdecode=sigdecode_der) + + +@settings(**params) +@given(st.sampled_from(keys_and_sigs), st.binary()) +@example( + keys_and_sigs[0], + encode_sequence(encode_integer(0), encode_integer(0))) +@example( + keys_and_sigs[0], + encode_sequence(encode_integer(1), encode_integer(1)) + b'\x00') +@example( + keys_and_sigs[0], + encode_sequence(*[encode_integer(1)] * 3)) +def test_random_bytes_as_signature(params, der): + """Check if random bytes are rejected as signature""" + name, verifying_key, _ = params + + with pytest.raises(BadSignatureError): + verifying_key.verify(der, example_data, sigdecode=sigdecode_der) + + +keys_and_string_sigs = [ + (name, verifying_key, + sigencode_string(*sigdecode_der(sig, verifying_key.curve.order), + order=verifying_key.curve.order)) + for name, verifying_key, sig in keys_and_sigs] +""" +Name of the curve+hash combination, VerifyingKey and signature as a +byte string. +""" + + +@settings(**params) +@given(st_fuzzed_sig(keys_and_string_sigs)) +def test_fuzzed_string_signatures(params): + verifying_key, sig = params + + with pytest.raises(BadSignatureError): + verifying_key.verify(sig, example_data, sigdecode=sigdecode_string) diff --git a/tox.ini b/tox.ini index a10f3965..fbaf823e 100644 --- a/tox.ini +++ b/tox.ini @@ -6,16 +6,21 @@ envlist = py26, py27, py33, py34, py35, py36, py37, py38, py, pypy, pypy3 deps = py{33}: py<1.5 py{33}: pytest<3.3 + py{33}: enum34 + py{33}: hypothesis<3.44 py{26}: unittest2 + py{26}: hypothesis<3 py{26,27,34,35,36,37,38,py,py3}: pytest + py{27,34,35,36,37,38,py,py3}: hypothesis py: pytest + py: hypothesis py{33}: wheel<0.30 coverage commands = coverage run --branch -m pytest {posargs:src/ecdsa} [testenv:coverage] sitepackages=True -commands = coverage run --branch -m pytest {posargs:src/ecdsa} +commands = coverage run --branch -m pytest --hypothesis-show-statistics {posargs:src/ecdsa} [testenv:speed] commands = {envpython} speed.py