| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- from __future__ import with_statement, division
- import hashlib
- try:
- from hashlib import algorithms_available
- except ImportError: # pragma: no cover
- algorithms_available = [
- "md5",
- "sha1",
- "sha224",
- "sha256",
- "sha384",
- "sha512",
- ]
- # skip algorithms broken by change to OpenSSL 3.0 and early versions
- # of hashlib that list algorithms that require the legacy provider to work
- # https://bugs.python.org/issue38820
- algorithms_available = [
- i
- for i in algorithms_available
- if i not in ("mdc2", "md2", "md4", "whirlpool", "ripemd160")
- ]
- from functools import partial
- import pytest
- import sys
- import hypothesis.strategies as st
- from hypothesis import note, assume, given, settings, example
- 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, SECP112r2, SECP128r1
- from .der import (
- encode_integer,
- encode_bitstring,
- encode_octet_string,
- encode_oid,
- encode_sequence,
- encode_constructed,
- )
- from .ellipticcurve import CurveEdTw
- example_data = b"some data to sign"
- """Since the data is hashed for processing, really any string will do."""
- hash_and_size = [
- (name, hashlib.new(name).digest_size) for name in algorithms_available
- ]
- """Pairs of hash names and their output sizes.
- Needed for pairing with curves as we don't support hashes
- bigger than order sizes of curves."""
- if "--fast" in sys.argv: # pragma: no cover
- curves = [SECP112r2, SECP128r1]
- keys_and_sigs = []
- """Name of the curve+hash combination, VerifyingKey and DER signature."""
- # for hypothesis strategy shrinking we want smallest curves and hashes first
- for curve in sorted(curves, key=lambda x: x.baselen):
- for hash_alg in [
- name
- for name, size in sorted(hash_and_size, key=lambda x: x[1])
- if 0 < size <= curve.baselen
- ]:
- sk = SigningKey.generate(
- curve, hashfunc=partial(hashlib.new, hash_alg)
- )
- keys_and_sigs.append(
- (
- "{0} {1}".format(curve, hash_alg),
- sk.verifying_key,
- sk.sign(example_data, sigencode=sigencode_der),
- )
- )
- # first make sure that the signatures can be verified
- @pytest.mark.parametrize(
- "verifying_key,signature",
- [pytest.param(vk, sig, id=name) for name, vk, sig in keys_and_sigs],
- )
- def test_signatures(verifying_key, signature):
- assert verifying_key.verify(
- signature, example_data, sigdecode=sigdecode_der
- )
- @st.composite
- def st_fuzzed_sig(draw, keys_and_sigs): # pragma: no cover
- """
- Hypothesis strategy that generates pairs of VerifyingKey and malformed
- signatures created by fuzzing of a valid signature.
- """
- name, verifying_key, old_sig = draw(st.sampled_from(keys_and_sigs))
- note("Configuration: {0}".format(name))
- sig = bytearray(old_sig)
- # decide which bytes should be removed
- to_remove = draw(
- st.lists(st.integers(min_value=0, max_value=len(sig) - 1), unique=True)
- )
- to_remove.sort()
- for i in reversed(to_remove):
- del sig[i]
- note("Remove bytes: {0}".format(to_remove))
- # decide which bytes of the original signature should be changed
- xors = None
- if sig: # pragma: no branch
- xors = draw(
- st.dictionaries(
- st.integers(min_value=0, max_value=len(sig) - 1),
- st.integers(min_value=1, max_value=255),
- )
- )
- for i, val in xors.items():
- sig[i] ^= val
- note("xors: {0}".format(xors))
- # decide where new data should be inserted
- insert_pos = draw(st.integers(min_value=0, max_value=len(sig)))
- # NIST521p signature is about 140 bytes long, test slightly longer
- insert_data = draw(st.binary(max_size=256))
- sig = sig[:insert_pos] + insert_data + sig[insert_pos:]
- note(
- "Inserted at position {0} bytes: {1!r}".format(insert_pos, insert_data)
- )
- sig = bytes(sig)
- # make sure that there was performed at least one mutation on the data
- assume(to_remove or xors or insert_data)
- # and that the mutations didn't cancel each-other out
- assume(sig != old_sig)
- return verifying_key, sig
- params = {}
- # not supported in hypothesis 2.0.0
- if sys.version_info >= (2, 7): # pragma: no branch
- from hypothesis import HealthCheck
- # deadline=5s because NIST521p are slow to verify
- params["deadline"] = 5000
- params["suppress_health_check"] = [
- HealthCheck.data_too_large,
- HealthCheck.filter_too_much,
- HealthCheck.too_slow,
- ]
- if "--fast" in sys.argv: # pragma: no cover
- params["max_examples"] = 20
- slow_params = dict(params)
- if "--fast" in sys.argv: # pragma: no cover
- slow_params["max_examples"] = 1
- else:
- slow_params["max_examples"] = 10
- @settings(**slow_params)
- @given(st_fuzzed_sig(keys_and_sigs))
- def test_fuzzed_der_signatures(args):
- verifying_key, sig = args
- with pytest.raises(BadSignatureError):
- verifying_key.verify(sig, example_data, sigdecode=sigdecode_der)
- @st.composite
- def st_random_der_ecdsa_sig_value(draw): # pragma: no cover
- """
- 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 = int(verifying_key.curve.order)
- # the encode_integer doesn't support 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.
- # Test all numbers around the ones that can show up (around order)
- # way smaller and slightly bigger
- r = draw(
- st.integers(min_value=0, max_value=order << 4)
- | st.integers(min_value=order >> 2, max_value=order + 1)
- )
- s = draw(
- st.integers(min_value=0, max_value=order << 4)
- | st.integers(min_value=order >> 2, max_value=order + 1)
- )
- sig = encode_sequence(encode_integer(r), encode_integer(s))
- return verifying_key, sig
- @settings(**slow_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): # pragma: no cover
- """
- Hypothesis strategy that returns a random positive integer as DER
- INTEGER.
- Parameters are passed to hypothesis.strategy.integer.
- """
- if "min_value" not in kwargs: # pragma: no branch
- kwargs["min_value"] = 0
- return st.builds(encode_integer, st.integers(*args, **kwargs))
- @st.composite
- def st_der_bit_string(draw, *args, **kwargs): # pragma: no cover
- """
- 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): # pragma: no cover
- """
- 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(): # pragma: no cover
- """
- Hypothesis strategy that returns DER NULL object.
- """
- return st.just(b"\x05\x00")
- @st.composite
- def st_der_oid(draw): # pragma: no cover
- """
- Hypothesis strategy that returns DER OBJECT IDENTIFIER objects.
- """
- first = draw(st.integers(min_value=0, max_value=2))
- if first < 2:
- second = draw(st.integers(min_value=0, max_value=39))
- else:
- second = draw(st.integers(min_value=0, max_value=2**512))
- rest = draw(
- st.lists(st.integers(min_value=0, max_value=2**512), max_size=50)
- )
- return encode_oid(first, second, *rest)
- def st_der(): # pragma: no cover
- """
- 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( # pragma: no branch
- st.just(b"")
- | st_der_integer(max_value=2**4096)
- | st_der_bit_string(max_size=1024**2)
- | st_der_octet_string(max_size=1024**2)
- | st_der_null()
- | st_der_oid(),
- lambda children: st.builds(encode_octet_string, 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, max_size=200)
- )
- | st.builds(
- encode_constructed,
- st.integers(min_value=0, max_value=0x3F),
- st.one_of(children),
- ),
- max_leaves=40,
- )
- @settings(**slow_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(**slow_params)
- @given(st.sampled_from(keys_and_sigs), st.binary(max_size=1024**2))
- @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
- if not isinstance(verifying_key.curve.curve, CurveEdTw)
- ]
- """
- Name of the curve+hash combination, VerifyingKey and signature as a
- byte string.
- """
- keys_and_string_sigs += [
- (
- name,
- verifying_key,
- sig,
- )
- for name, verifying_key, sig in keys_and_sigs
- if isinstance(verifying_key.curve.curve, CurveEdTw)
- ]
- @settings(**slow_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)
|