Skip to content

Commit 623c1b4

Browse files
authored
Merge pull request #21 from Mbed-TLS/dev/davidhorstmann-arm/add-test-generation-files
Add test generation files to framework
2 parents e156a8e + 7231e5e commit 623c1b4

9 files changed

+4978
-0
lines changed

scripts/generate_bignum_tests.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#!/usr/bin/env python3
2+
"""Generate test data for bignum functions.
3+
4+
With no arguments, generate all test data. With non-option arguments,
5+
generate only the specified files.
6+
7+
Class structure:
8+
9+
Child classes of test_data_generation.BaseTarget (file targets) represent an output
10+
file. These indicate where test cases will be written to, for all subclasses of
11+
this target. Multiple file targets should not reuse a `target_basename`.
12+
13+
Each subclass derived from a file target can either be:
14+
- A concrete class, representing a test function, which generates test cases.
15+
- An abstract class containing shared methods and attributes, not associated
16+
with a test function. An example is BignumOperation, which provides
17+
common features used for bignum binary operations.
18+
19+
Both concrete and abstract subclasses can be derived from, to implement
20+
additional test cases (see BignumCmp and BignumCmpAbs for examples of deriving
21+
from abstract and concrete classes).
22+
23+
24+
Adding test case generation for a function:
25+
26+
A subclass representing the test function should be added, deriving from a
27+
file target such as BignumTarget. This test class must set/implement the
28+
following:
29+
- test_function: the function name from the associated .function file.
30+
- test_name: a descriptive name or brief summary to refer to the test
31+
function.
32+
- arguments(): a method to generate the list of arguments required for the
33+
test_function.
34+
- generate_function_tests(): a method to generate TestCases for the function.
35+
This should create instances of the class with required input data, and
36+
call `.create_test_case()` to yield the TestCase.
37+
38+
Additional details and other attributes/methods are given in the documentation
39+
of BaseTarget in test_data_generation.py.
40+
"""
41+
42+
# Copyright The Mbed TLS Contributors
43+
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
44+
45+
import sys
46+
47+
from abc import ABCMeta
48+
from typing import List
49+
50+
from mbedtls_framework import test_data_generation
51+
from mbedtls_framework import bignum_common
52+
# Import modules containing additional test classes
53+
# Test function classes in these modules will be registered by
54+
# the framework
55+
from mbedtls_framework import bignum_core, bignum_mod_raw, bignum_mod # pylint: disable=unused-import
56+
57+
class BignumTarget(test_data_generation.BaseTarget):
58+
#pylint: disable=too-few-public-methods
59+
"""Target for bignum (legacy) test case generation."""
60+
target_basename = 'test_suite_bignum.generated'
61+
62+
63+
class BignumOperation(bignum_common.OperationCommon, BignumTarget,
64+
metaclass=ABCMeta):
65+
#pylint: disable=abstract-method
66+
"""Common features for bignum operations in legacy tests."""
67+
unique_combinations_only = True
68+
input_values = [
69+
"", "0", "-", "-0",
70+
"7b", "-7b",
71+
"0000000000000000123", "-0000000000000000123",
72+
"1230000000000000000", "-1230000000000000000"
73+
]
74+
75+
def description_suffix(self) -> str:
76+
#pylint: disable=no-self-use # derived classes need self
77+
"""Text to add at the end of the test case description."""
78+
return ""
79+
80+
def description(self) -> str:
81+
"""Generate a description for the test case.
82+
83+
If not set, case_description uses the form A `symbol` B, where symbol
84+
is used to represent the operation. Descriptions of each value are
85+
generated to provide some context to the test case.
86+
"""
87+
if not self.case_description:
88+
self.case_description = "{} {} {}".format(
89+
self.value_description(self.arg_a),
90+
self.symbol,
91+
self.value_description(self.arg_b)
92+
)
93+
description_suffix = self.description_suffix()
94+
if description_suffix:
95+
self.case_description += " " + description_suffix
96+
return super().description()
97+
98+
@staticmethod
99+
def value_description(val) -> str:
100+
"""Generate a description of the argument val.
101+
102+
This produces a simple description of the value, which is used in test
103+
case naming to add context.
104+
"""
105+
if val == "":
106+
return "0 (null)"
107+
if val == "-":
108+
return "negative 0 (null)"
109+
if val == "0":
110+
return "0 (1 limb)"
111+
112+
if val[0] == "-":
113+
tmp = "negative"
114+
val = val[1:]
115+
else:
116+
tmp = "positive"
117+
if val[0] == "0":
118+
tmp += " with leading zero limb"
119+
elif len(val) > 10:
120+
tmp = "large " + tmp
121+
return tmp
122+
123+
124+
class BignumCmp(BignumOperation):
125+
"""Test cases for bignum value comparison."""
126+
count = 0
127+
test_function = "mpi_cmp_mpi"
128+
test_name = "MPI compare"
129+
input_cases = [
130+
("-2", "-3"),
131+
("-2", "-2"),
132+
("2b4", "2b5"),
133+
("2b5", "2b6")
134+
]
135+
136+
def __init__(self, val_a, val_b) -> None:
137+
super().__init__(val_a, val_b)
138+
self._result = int(self.int_a > self.int_b) - int(self.int_a < self.int_b)
139+
self.symbol = ["<", "==", ">"][self._result + 1]
140+
141+
def result(self) -> List[str]:
142+
return [str(self._result)]
143+
144+
145+
class BignumCmpAbs(BignumCmp):
146+
"""Test cases for absolute bignum value comparison."""
147+
count = 0
148+
test_function = "mpi_cmp_abs"
149+
test_name = "MPI compare (abs)"
150+
151+
def __init__(self, val_a, val_b) -> None:
152+
super().__init__(val_a.strip("-"), val_b.strip("-"))
153+
154+
155+
class BignumAdd(BignumOperation):
156+
"""Test cases for bignum value addition."""
157+
count = 0
158+
symbol = "+"
159+
test_function = "mpi_add_mpi"
160+
test_name = "MPI add"
161+
input_cases = bignum_common.combination_pairs(
162+
[
163+
"1c67967269c6", "9cde3",
164+
"-1c67967269c6", "-9cde3",
165+
]
166+
)
167+
168+
def __init__(self, val_a: str, val_b: str) -> None:
169+
super().__init__(val_a, val_b)
170+
self._result = self.int_a + self.int_b
171+
172+
def description_suffix(self) -> str:
173+
if (self.int_a >= 0 and self.int_b >= 0):
174+
return "" # obviously positive result or 0
175+
if (self.int_a <= 0 and self.int_b <= 0):
176+
return "" # obviously negative result or 0
177+
# The sign of the result is not obvious, so indicate it
178+
return ", result{}0".format('>' if self._result > 0 else
179+
'<' if self._result < 0 else '=')
180+
181+
def result(self) -> List[str]:
182+
return [bignum_common.quote_str("{:x}".format(self._result))]
183+
184+
if __name__ == '__main__':
185+
# Use the section of the docstring relevant to the CLI as description
186+
test_data_generation.main(sys.argv[1:], "\n".join(__doc__.splitlines()[:4]))

scripts/generate_ecp_tests.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env python3
2+
"""Generate test data for ecp functions.
3+
4+
The command line usage, class structure and available methods are the same
5+
as in generate_bignum_tests.py.
6+
"""
7+
8+
# Copyright The Mbed TLS Contributors
9+
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
10+
11+
import sys
12+
13+
from mbedtls_framework import test_data_generation
14+
# Import modules containing additional test classes
15+
# Test function classes in these modules will be registered by
16+
# the framework
17+
from mbedtls_framework import ecp # pylint: disable=unused-import
18+
19+
if __name__ == '__main__':
20+
# Use the section of the docstring relevant to the CLI as description
21+
test_data_generation.main(sys.argv[1:], "\n".join(__doc__.splitlines()[:4]))

scripts/generate_pkcs7_tests.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright The Mbed TLS Contributors
4+
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
5+
#
6+
7+
"""
8+
Make fuzz like testing for pkcs7 tests
9+
Given a valid DER pkcs7 file add tests to the test_suite_pkcs7.data file
10+
- It is expected that the pkcs7_asn1_fail( data_t *pkcs7_buf )
11+
function is defined in test_suite_pkcs7.function
12+
- This is not meant to be portable code, if anything it is meant to serve as
13+
documentation for showing how those ugly tests in test_suite_pkcs7.data were created
14+
"""
15+
16+
17+
import sys
18+
from os.path import exists
19+
20+
PKCS7_TEST_FILE = "../suites/test_suite_pkcs7.data"
21+
22+
class Test: # pylint: disable=too-few-public-methods
23+
"""
24+
A instance of a test in test_suite_pkcs7.data
25+
"""
26+
def __init__(self, name, depends, func_call):
27+
self.name = name
28+
self.depends = depends
29+
self.func_call = func_call
30+
31+
# pylint: disable=no-self-use
32+
def to_string(self):
33+
return "\n" + self.name + "\n" + self.depends + "\n" + self.func_call + "\n"
34+
35+
class TestData:
36+
"""
37+
Take in test_suite_pkcs7.data file.
38+
Allow for new tests to be added.
39+
"""
40+
mandatory_dep = "MBEDTLS_MD_CAN_SHA256"
41+
test_name = "PKCS7 Parse Failure Invalid ASN1"
42+
test_function = "pkcs7_asn1_fail:"
43+
def __init__(self, file_name):
44+
self.file_name = file_name
45+
self.last_test_num, self.old_tests = self.read_test_file(file_name)
46+
self.new_tests = []
47+
48+
# pylint: disable=no-self-use
49+
def read_test_file(self, file):
50+
"""
51+
Parse the test_suite_pkcs7.data file.
52+
"""
53+
tests = []
54+
if not exists(file):
55+
print(file + " Does not exist")
56+
sys.exit()
57+
with open(file, "r", encoding='UTF-8') as fp:
58+
data = fp.read()
59+
lines = [line.strip() for line in data.split('\n') if len(line.strip()) > 1]
60+
i = 0
61+
while i < len(lines):
62+
if "depends" in lines[i+1]:
63+
tests.append(Test(lines[i], lines[i+1], lines[i+2]))
64+
i += 3
65+
else:
66+
tests.append(Test(lines[i], None, lines[i+1]))
67+
i += 2
68+
latest_test_num = float(tests[-1].name.split('#')[1])
69+
return latest_test_num, tests
70+
71+
def add(self, name, func_call):
72+
self.last_test_num += 1
73+
self.new_tests.append(Test(self.test_name + ": " + name + " #" + \
74+
str(self.last_test_num), "depends_on:" + self.mandatory_dep, \
75+
self.test_function + '"' + func_call + '"'))
76+
77+
def write_changes(self):
78+
with open(self.file_name, 'a', encoding='UTF-8') as fw:
79+
fw.write("\n")
80+
for t in self.new_tests:
81+
fw.write(t.to_string())
82+
83+
84+
def asn1_mutate(data):
85+
"""
86+
We have been given an asn1 structure representing a pkcs7.
87+
We want to return an array of slightly modified versions of this data
88+
they should be modified in a way which makes the structure invalid
89+
90+
We know that asn1 structures are:
91+
|---1 byte showing data type---|----byte(s) for length of data---|---data content--|
92+
We know that some data types can contain other data types.
93+
Return a dictionary of reasons and mutated data types.
94+
"""
95+
96+
# off the bat just add bytes to start and end of the buffer
97+
mutations = []
98+
reasons = []
99+
mutations.append(["00"] + data)
100+
reasons.append("Add null byte to start")
101+
mutations.append(data + ["00"])
102+
reasons.append("Add null byte to end")
103+
# for every asn1 entry we should attempt to:
104+
# - change the data type tag
105+
# - make the length longer than actual
106+
# - make the length shorter than actual
107+
i = 0
108+
while i < len(data):
109+
tag_i = i
110+
leng_i = tag_i + 1
111+
data_i = leng_i + 1 + (int(data[leng_i][1], 16) if data[leng_i][0] == '8' else 0)
112+
if data[leng_i][0] == '8':
113+
length = int(''.join(data[leng_i + 1: data_i]), 16)
114+
else:
115+
length = int(data[leng_i], 16)
116+
117+
tag = data[tag_i]
118+
print("Looking at ans1: offset " + str(i) + " tag = " + tag + \
119+
", length = " + str(length)+ ":")
120+
print(''.join(data[data_i:data_i+length]))
121+
# change tag to something else
122+
if tag == "02":
123+
# turn integers into octet strings
124+
new_tag = "04"
125+
else:
126+
# turn everything else into an integer
127+
new_tag = "02"
128+
mutations.append(data[:tag_i] + [new_tag] + data[leng_i:])
129+
reasons.append("Change tag " + tag + " to " + new_tag)
130+
131+
# change lengths to too big
132+
# skip any edge cases which would cause carry over
133+
if int(data[data_i - 1], 16) < 255:
134+
new_length = str(hex(int(data[data_i - 1], 16) + 1))[2:]
135+
if len(new_length) == 1:
136+
new_length = "0"+new_length
137+
mutations.append(data[:data_i -1] + [new_length] + data[data_i:])
138+
reasons.append("Change length from " + str(length) + " to " \
139+
+ str(length + 1))
140+
# we can add another test here for tags that contain other tags \
141+
# where they have more data than there containing tags account for
142+
if tag in ["30", "a0", "31"]:
143+
mutations.append(data[:data_i -1] + [new_length] + \
144+
data[data_i:data_i + length] + ["00"] + \
145+
data[data_i + length:])
146+
reasons.append("Change contents of tag " + tag + " to contain \
147+
one unaccounted extra byte")
148+
# change lengths to too small
149+
if int(data[data_i - 1], 16) > 0:
150+
new_length = str(hex(int(data[data_i - 1], 16) - 1))[2:]
151+
if len(new_length) == 1:
152+
new_length = "0"+new_length
153+
mutations.append(data[:data_i -1] + [new_length] + data[data_i:])
154+
reasons.append("Change length from " + str(length) + " to " + str(length - 1))
155+
156+
# some tag types contain other tag types so we should iterate into the data
157+
if tag in ["30", "a0", "31"]:
158+
i = data_i
159+
else:
160+
i = data_i + length
161+
162+
return list(zip(reasons, mutations))
163+
164+
if __name__ == "__main__":
165+
if len(sys.argv) < 2:
166+
print("USAGE: " + sys.argv[0] + " <pkcs7_der_file>")
167+
sys.exit()
168+
169+
DATA_FILE = sys.argv[1]
170+
TEST_DATA = TestData(PKCS7_TEST_FILE)
171+
with open(DATA_FILE, 'rb') as f:
172+
DATA_STR = f.read().hex()
173+
# make data an array of byte strings eg ['de','ad','be','ef']
174+
HEX_DATA = list(map(''.join, [[DATA_STR[i], DATA_STR[i+1]] for i in range(0, len(DATA_STR), \
175+
2)]))
176+
# returns tuples of test_names and modified data buffers
177+
MUT_ARR = asn1_mutate(HEX_DATA)
178+
179+
print("made " + str(len(MUT_ARR)) + " new tests")
180+
for new_test in MUT_ARR:
181+
TEST_DATA.add(new_test[0], ''.join(new_test[1]))
182+
183+
TEST_DATA.write_changes()

0 commit comments

Comments
 (0)