blob: 4a4cd709520931e15ce3ba860a69f2aee2310b54 [file] [log] [blame]
Gilles Peskine09940492021-01-26 22:16:30 +01001#!/usr/bin/env python3
2"""Generate test data for PSA cryptographic mechanisms.
3"""
4
5# Copyright The Mbed TLS Contributors
6# SPDX-License-Identifier: Apache-2.0
7#
8# Licensed under the Apache License, Version 2.0 (the "License"); you may
9# not use this file except in compliance with the License.
10# You may obtain a copy of the License at
11#
12# http://www.apache.org/licenses/LICENSE-2.0
13#
14# Unless required by applicable law or agreed to in writing, software
15# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17# See the License for the specific language governing permissions and
18# limitations under the License.
19
20import argparse
Gilles Peskine14e428f2021-01-26 22:19:21 +010021import os
22import re
Gilles Peskine09940492021-01-26 22:16:30 +010023import sys
Gilles Peskined169d602021-02-16 14:16:25 +010024from typing import FrozenSet, Iterable, List, Optional, TypeVar
Gilles Peskine09940492021-01-26 22:16:30 +010025
26import scripts_path # pylint: disable=unused-import
Gilles Peskine14e428f2021-01-26 22:19:21 +010027from mbedtls_dev import crypto_knowledge
Gilles Peskine09940492021-01-26 22:16:30 +010028from mbedtls_dev import macro_collector
Gilles Peskine14e428f2021-01-26 22:19:21 +010029from mbedtls_dev import test_case
Gilles Peskine09940492021-01-26 22:16:30 +010030
31T = TypeVar('T') #pylint: disable=invalid-name
32
Gilles Peskine14e428f2021-01-26 22:19:21 +010033
Gilles Peskine7f756872021-02-16 12:13:12 +010034def psa_want_symbol(name: str) -> str:
Gilles Peskineaf172842021-01-27 18:24:48 +010035 """Return the PSA_WANT_xxx symbol associated with a PSA crypto feature."""
36 if name.startswith('PSA_'):
37 return name[:4] + 'WANT_' + name[4:]
38 else:
39 raise ValueError('Unable to determine the PSA_WANT_ symbol for ' + name)
40
Gilles Peskine7f756872021-02-16 12:13:12 +010041def finish_family_dependency(dep: str, bits: int) -> str:
42 """Finish dep if it's a family dependency symbol prefix.
43
44 A family dependency symbol prefix is a PSA_WANT_ symbol that needs to be
45 qualified by the key size. If dep is such a symbol, finish it by adjusting
46 the prefix and appending the key size. Other symbols are left unchanged.
47 """
48 return re.sub(r'_FAMILY_(.*)', r'_\1_' + str(bits), dep)
49
50def finish_family_dependencies(dependencies: List[str], bits: int) -> List[str]:
51 """Finish any family dependency symbol prefixes.
52
53 Apply `finish_family_dependency` to each element of `dependencies`.
54 """
55 return [finish_family_dependency(dep, bits) for dep in dependencies]
Gilles Peskineaf172842021-01-27 18:24:48 +010056
Gilles Peskined169d602021-02-16 14:16:25 +010057# A temporary hack: at the time of writing, not all dependency symbols
58# are implemented yet. Skip test cases for which the dependency symbols are
59# not available. Once all dependency symbols are available, this hack must
60# be removed so that a bug in the dependency symbols proprely leads to a test
61# failure.
62def read_implemented_dependencies(filename: str) -> FrozenSet[str]:
63 return frozenset(symbol
64 for line in open(filename)
65 for symbol in re.findall(r'\bPSA_WANT_\w+\b', line))
66IMPLEMENTED_DEPENDENCIES = read_implemented_dependencies('include/psa/crypto_config.h')
67def hack_dependencies_not_implemented(dependencies: List[str]) -> None:
68 if not all(dep.lstrip('!') in IMPLEMENTED_DEPENDENCIES
69 for dep in dependencies):
70 dependencies.append('DEPENDENCY_NOT_IMPLEMENTED_YET')
71
Gilles Peskineaf172842021-01-27 18:24:48 +010072def test_case_for_key_type_not_supported(
73 verb: str, key_type: str, bits: int,
74 dependencies: List[str],
75 *args: str,
76 param_descr: str = '',
77) -> test_case.TestCase:
Gilles Peskine14e428f2021-01-26 22:19:21 +010078 """Return one test case exercising a key creation method
79 for an unsupported key type or size.
80 """
Gilles Peskined169d602021-02-16 14:16:25 +010081 hack_dependencies_not_implemented(dependencies)
Gilles Peskine14e428f2021-01-26 22:19:21 +010082 tc = test_case.TestCase()
Gilles Peskineaf172842021-01-27 18:24:48 +010083 short_key_type = re.sub(r'PSA_(KEY_TYPE|ECC_FAMILY)_', r'', key_type)
Gilles Peskine14e428f2021-01-26 22:19:21 +010084 adverb = 'not' if dependencies else 'never'
Gilles Peskineaf172842021-01-27 18:24:48 +010085 if param_descr:
86 adverb = param_descr + ' ' + adverb
Gilles Peskine14e428f2021-01-26 22:19:21 +010087 tc.set_description('PSA {} {} {}-bit {} supported'
Gilles Peskineaf172842021-01-27 18:24:48 +010088 .format(verb, short_key_type, bits, adverb))
Gilles Peskine14e428f2021-01-26 22:19:21 +010089 tc.set_dependencies(dependencies)
90 tc.set_function(verb + '_not_supported')
91 tc.set_arguments([key_type] + list(args))
92 return tc
93
Gilles Peskine09940492021-01-26 22:16:30 +010094class TestGenerator:
95 """Gather information and generate test data."""
96
97 def __init__(self, options):
98 self.test_suite_directory = self.get_option(options, 'directory',
99 'tests/suites')
100 self.constructors = self.read_psa_interface()
101
102 @staticmethod
103 def get_option(options, name: str, default: T) -> T:
104 value = getattr(options, name, None)
105 return default if value is None else value
106
107 @staticmethod
108 def remove_unwanted_macros(
109 constructors: macro_collector.PSAMacroCollector
110 ) -> None:
111 # Mbed TLS doesn't support DSA. Don't attempt to generate any related
112 # test case.
113 constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR')
114 constructors.key_types.discard('PSA_KEY_TYPE_DSA_PUBLIC_KEY')
115 constructors.algorithms_from_hash.pop('PSA_ALG_DSA', None)
116 constructors.algorithms_from_hash.pop('PSA_ALG_DETERMINISTIC_DSA', None)
117
118 def read_psa_interface(self) -> macro_collector.PSAMacroCollector:
119 """Return the list of known key types, algorithms, etc."""
120 constructors = macro_collector.PSAMacroCollector()
121 header_file_names = ['include/psa/crypto_values.h',
122 'include/psa/crypto_extra.h']
123 for header_file_name in header_file_names:
124 with open(header_file_name, 'rb') as header_file:
125 constructors.read_file(header_file)
126 self.remove_unwanted_macros(constructors)
127 return constructors
128
Gilles Peskine14e428f2021-01-26 22:19:21 +0100129 def write_test_data_file(self, basename: str,
130 test_cases: Iterable[test_case.TestCase]) -> None:
131 """Write the test cases to a .data file.
132
133 The output file is ``basename + '.data'`` in the test suite directory.
134 """
135 filename = os.path.join(self.test_suite_directory, basename + '.data')
136 test_case.write_data_file(filename, test_cases)
137
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100138 ALWAYS_SUPPORTED = frozenset([
139 'PSA_KEY_TYPE_DERIVE',
140 'PSA_KEY_TYPE_RAW_DATA',
141 ])
Gilles Peskine14e428f2021-01-26 22:19:21 +0100142 def test_cases_for_key_type_not_supported(
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100143 self,
Gilles Peskineaf172842021-01-27 18:24:48 +0100144 kt: crypto_knowledge.KeyType,
145 param: Optional[int] = None,
146 param_descr: str = '',
Gilles Peskine14e428f2021-01-26 22:19:21 +0100147 ) -> List[test_case.TestCase]:
Gilles Peskineaf172842021-01-27 18:24:48 +0100148 """Return test cases exercising key creation when the given type is unsupported.
149
150 If param is present and not None, emit test cases conditioned on this
151 parameter not being supported. If it is absent or None, emit test cases
152 conditioned on the base type not being supported.
153 """
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100154 if kt.name in self.ALWAYS_SUPPORTED:
155 # Don't generate test cases for key types that are always supported.
156 # They would be skipped in all configurations, which is noise.
Gilles Peskine14e428f2021-01-26 22:19:21 +0100157 return []
Gilles Peskineaf172842021-01-27 18:24:48 +0100158 import_dependencies = [('!' if param is None else '') +
159 psa_want_symbol(kt.name)]
160 if kt.params is not None:
161 import_dependencies += [('!' if param == i else '') +
162 psa_want_symbol(sym)
163 for i, sym in enumerate(kt.params)]
Gilles Peskine14e428f2021-01-26 22:19:21 +0100164 if kt.name.endswith('_PUBLIC_KEY'):
165 generate_dependencies = []
166 else:
167 generate_dependencies = import_dependencies
168 test_cases = []
169 for bits in kt.sizes_to_test():
170 test_cases.append(test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100171 'import', kt.expression, bits,
172 finish_family_dependencies(import_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100173 test_case.hex_string(kt.key_material(bits)),
174 param_descr=param_descr,
Gilles Peskine14e428f2021-01-26 22:19:21 +0100175 ))
Gilles Peskineaf172842021-01-27 18:24:48 +0100176 if not generate_dependencies and param is not None:
177 # If generation is impossible for this key type, rather than
178 # supported or not depending on implementation capabilities,
179 # only generate the test case once.
180 continue
Gilles Peskine14e428f2021-01-26 22:19:21 +0100181 test_cases.append(test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100182 'generate', kt.expression, bits,
183 finish_family_dependencies(generate_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100184 str(bits),
185 param_descr=param_descr,
Gilles Peskine14e428f2021-01-26 22:19:21 +0100186 ))
187 # To be added: derive
188 return test_cases
189
190 def generate_not_supported(self) -> None:
191 """Generate test cases that exercise the creation of keys of unsupported types."""
192 test_cases = []
193 for key_type in sorted(self.constructors.key_types):
194 kt = crypto_knowledge.KeyType(key_type)
195 test_cases += self.test_cases_for_key_type_not_supported(kt)
Gilles Peskineaf172842021-01-27 18:24:48 +0100196 # To be added: parametrized key types FFDH
197 for curve_family in sorted(self.constructors.ecc_curves):
198 for constr in ('PSA_KEY_TYPE_ECC_KEY_PAIR',
199 'PSA_KEY_TYPE_ECC_PUBLIC_KEY'):
200 kt = crypto_knowledge.KeyType(constr, [curve_family])
201 test_cases += self.test_cases_for_key_type_not_supported(
202 kt, param_descr='type')
203 test_cases += self.test_cases_for_key_type_not_supported(
204 kt, 0, param_descr='curve')
Gilles Peskine14e428f2021-01-26 22:19:21 +0100205 self.write_test_data_file(
206 'test_suite_psa_crypto_not_supported.generated',
207 test_cases)
208
209 def generate_all(self):
210 self.generate_not_supported()
211
Gilles Peskine09940492021-01-26 22:16:30 +0100212def main(args):
213 """Command line entry point."""
214 parser = argparse.ArgumentParser(description=__doc__)
215 options = parser.parse_args(args)
216 generator = TestGenerator(options)
Gilles Peskine14e428f2021-01-26 22:19:21 +0100217 generator.generate_all()
Gilles Peskine09940492021-01-26 22:16:30 +0100218
219if __name__ == '__main__':
220 main(sys.argv[1:])