Skip to content

Commit ab1fa13

Browse files
committed
fuzzing of DER signatures with hypothesis
use hypothesis to generate malformed signatures by introducing different changes for different curves made with different hashes
1 parent ebc2199 commit ab1fa13

File tree

6 files changed

+121
-37
lines changed

6 files changed

+121
-37
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ coverage-html
3333
.tox
3434
nosetests.xml
3535
t/
36+
.hypothesis/
3637

3738
# Translations
3839
*.mo

build-requirements-2.6.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ tox
22
coveralls<1.3.0
33
idna<2.8
44
unittest2
5+
hypothesis<3

build-requirements-3.3.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ pluggy<0.6
33
tox<3
44
wheel<0.30
55
virtualenv==15.2.0
6+
enum34
7+
hypothesis<3.44

build-requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
tox
22
python-coveralls
3+
hypothesis
4+
pytest>=4.6.0

src/ecdsa/test_malformed_sigs.py

Lines changed: 110 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,124 @@
11
from __future__ import with_statement, division
22

3-
import pytest
43
import hashlib
4+
try:
5+
from hashlib import algorithms_available
6+
except ImportError:
7+
algorithms_available = [
8+
"md5", "sha1", "sha224", "sha256", "sha384", "sha512"]
9+
from functools import partial
10+
import pytest
11+
import sys
12+
from six import binary_type
13+
import hypothesis.strategies as st
14+
from hypothesis import note, assume, given, settings
515

6-
from six import b, binary_type
7-
from .keys import SigningKey, VerifyingKey
16+
from .keys import SigningKey
817
from .keys import BadSignatureError
918
from .util import sigencode_der, sigencode_string
1019
from .util import sigdecode_der, sigdecode_string
11-
from .curves import curves, NIST256p, NIST521p
20+
from .curves import curves, NIST256p
1221

13-
der_sigs = []
14-
example_data = b("some data to sign")
1522

16-
# Just NIST256p with SHA256 is 560 test cases, all curves with all hashes is
17-
# few thousand slow test cases; execute the most interesting only
23+
example_data = b"some data to sign"
24+
"""Since the data is hashed for processing, really any string will do."""
1825

19-
#for curve in curves:
20-
for curve in [NIST521p]:
21-
#for hash_alg in ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"]:
22-
for hash_alg in ["sha256"]:
23-
key = SigningKey.generate(curve)
24-
signature = key.sign(example_data, hashfunc=getattr(hashlib, hash_alg),
25-
sigencode=sigencode_der)
26-
for pos in range(len(signature)):
27-
for xor in (1<<i for i in range(8)):
28-
der_sigs.append(pytest.param(
29-
key.verifying_key, hash_alg,
30-
signature, pos, xor,
31-
id="{0}-{1}-pos-{2}-xor-{3}".format(
32-
curve.name, hash_alg, pos, xor)))
33-
34-
35-
@pytest.mark.parametrize("verifying_key,hash_alg,signature,pos,xor", der_sigs)
36-
def test_fuzzed_der_signatures(verifying_key, hash_alg, signature, pos, xor):
37-
# check if a malformed DER encoded signature causes the same exception
38-
# to be raised irrespective of the type of error
39-
sig = bytearray(signature)
40-
sig[pos] ^= xor
41-
sig = binary_type(sig)
4226

43-
try:
44-
verifying_key.verify(sig, example_data, getattr(hashlib, hash_alg),
45-
sigdecode_der)
46-
assert False
47-
except BadSignatureError:
48-
assert True
27+
hash_and_size = [(name, hashlib.new(name).digest_size)
28+
for name in algorithms_available]
29+
"""Pairs of hash names and their output sizes.
30+
Needed for pairing with curves as we don't support hashes
31+
bigger than order sizes of curves."""
32+
33+
34+
keys_and_sigs = []
35+
"""Name of the curve+hash combination, VerifyingKey and DER signature."""
36+
37+
38+
# for hypothesis strategy shrinking we want smallest curves and hashes first
39+
for curve in sorted(curves, key=lambda x: x.baselen):
40+
for hash_alg in [name for name, size in
41+
sorted(hash_and_size, key=lambda x: x[1])
42+
if 0 < size <= curve.baselen]:
43+
sk = SigningKey.generate(
44+
curve,
45+
hashfunc=partial(hashlib.new, hash_alg))
46+
47+
keys_and_sigs.append(
48+
("{0} {1}".format(curve, hash_alg),
49+
sk.verifying_key,
50+
sk.sign(example_data, sigencode=sigencode_der)))
51+
52+
53+
# first make sure that the signatures can be verified
54+
@pytest.mark.parametrize(
55+
"verifying_key,signature",
56+
[pytest.param(vk, sig, id=name) for name, vk, sig in keys_and_sigs])
57+
def test_signatures(verifying_key, signature):
58+
assert verifying_key.verify(signature, example_data,
59+
sigdecode=sigdecode_der)
60+
61+
62+
@st.composite
63+
def st_fuzzed_sig(draw):
64+
"""
65+
Hypothesis strategy that generates pairs of VerifyingKey and malformed
66+
signatures created by fuzzing of a valid signature.
67+
"""
68+
name, verifying_key, old_sig = draw(st.sampled_from(keys_and_sigs))
69+
note("Configuration: {0}".format(name))
70+
71+
sig = bytearray(old_sig)
72+
73+
# decide which bytes should be removed
74+
to_remove = draw(st.lists(
75+
st.integers(min_value=0, max_value=len(sig)-1),
76+
unique=True))
77+
to_remove.sort()
78+
for i in reversed(to_remove):
79+
del sig[i]
80+
note("Remove bytes: {0}".format(to_remove))
81+
82+
# decide which bytes of the original signature should be changed
83+
xors = draw(st.dictionaries(
84+
st.integers(min_value=0, max_value=len(sig)-1),
85+
st.integers(min_value=1, max_value=255)))
86+
for i, val in xors.items():
87+
sig[i] ^= val
88+
note("xors: {0}".format(xors))
89+
90+
# decide where new data should be inserted
91+
insert_pos = draw(st.integers(min_value=0, max_value=len(sig)))
92+
# NIST521p signature is about 140 bytes long, test slightly longer
93+
insert_data = draw(st.binary(max_size=256))
94+
95+
sig = sig[:insert_pos] + insert_data + sig[insert_pos:]
96+
note("Inserted at position {0} bytes: {1!r}"
97+
.format(insert_pos, insert_data))
98+
99+
sig = bytes(sig)
100+
# make sure that there was performed at least one mutation on the data
101+
assume(to_remove or xors or insert_data)
102+
# and that the mutations didn't cancel each-other out
103+
assume(sig != old_sig)
104+
105+
return verifying_key, sig
106+
107+
108+
params = {}
109+
# not supported in hypothesis 2.0.0
110+
if sys.version_info >= (2, 7):
111+
# deadline=5s because NIST521p are slow to verify
112+
params["deadline"] = 5000
113+
114+
115+
@settings(**params)
116+
@given(st_fuzzed_sig())
117+
def test_fuzzed_der_signatures(args):
118+
verifying_key, sig = args
119+
120+
with pytest.raises(BadSignatureError):
121+
verifying_key.verify(sig, example_data, sigdecode=sigdecode_der)
49122

50123

51124
####

tox.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@ envlist = py26, py27, py33, py34, py35, py36, py37, py38, py, pypy, pypy3
66
deps =
77
py{33}: py<1.5
88
py{33}: pytest<3.3
9+
py{33}: enum34
10+
py{33}: hypothesis<3.44
911
py{26}: unittest2
12+
py{26}: hypothesis<3
1013
py{26,27,34,35,36,37,38,py,py3}: pytest
14+
py{27,34,35,36,37,38,py,py3}: hypothesis
1115
py: pytest
16+
py: hypothesis
1217
py{33}: wheel<0.30
1318
coverage
1419
commands = coverage run --branch -m pytest {posargs:src/ecdsa}

0 commit comments

Comments
 (0)