blob: 21a5a81f8997837023d58f21d1f51777bf17797f [file] [log] [blame]
Gilles Peskine09940492021-01-26 22:16:30 +01001#!/usr/bin/env python3
2"""Generate test data for PSA cryptographic mechanisms.
Gilles Peskine0298bda2021-03-10 02:34:37 +01003
4With no arguments, generate all test data. With non-option arguments,
5generate only the specified files.
Gilles Peskine09940492021-01-26 22:16:30 +01006"""
7
8# Copyright The Mbed TLS Contributors
9# SPDX-License-Identifier: Apache-2.0
10#
11# Licensed under the Apache License, Version 2.0 (the "License"); you may
12# not use this file except in compliance with the License.
13# You may obtain a copy of the License at
14#
15# http://www.apache.org/licenses/LICENSE-2.0
16#
17# Unless required by applicable law or agreed to in writing, software
18# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
19# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20# See the License for the specific language governing permissions and
21# limitations under the License.
22
23import argparse
Gilles Peskine14e428f2021-01-26 22:19:21 +010024import os
25import re
Gilles Peskine09940492021-01-26 22:16:30 +010026import sys
Gilles Peskine3d778392021-02-17 15:11:05 +010027from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional, TypeVar
Gilles Peskine09940492021-01-26 22:16:30 +010028
29import scripts_path # pylint: disable=unused-import
Gilles Peskine14e428f2021-01-26 22:19:21 +010030from mbedtls_dev import crypto_knowledge
Gilles Peskine09940492021-01-26 22:16:30 +010031from mbedtls_dev import macro_collector
Gilles Peskine14e428f2021-01-26 22:19:21 +010032from mbedtls_dev import test_case
Gilles Peskine09940492021-01-26 22:16:30 +010033
34T = TypeVar('T') #pylint: disable=invalid-name
35
Gilles Peskine14e428f2021-01-26 22:19:21 +010036
Gilles Peskine7f756872021-02-16 12:13:12 +010037def psa_want_symbol(name: str) -> str:
Gilles Peskineaf172842021-01-27 18:24:48 +010038 """Return the PSA_WANT_xxx symbol associated with a PSA crypto feature."""
39 if name.startswith('PSA_'):
40 return name[:4] + 'WANT_' + name[4:]
41 else:
42 raise ValueError('Unable to determine the PSA_WANT_ symbol for ' + name)
43
Gilles Peskine7f756872021-02-16 12:13:12 +010044def finish_family_dependency(dep: str, bits: int) -> str:
45 """Finish dep if it's a family dependency symbol prefix.
46
47 A family dependency symbol prefix is a PSA_WANT_ symbol that needs to be
48 qualified by the key size. If dep is such a symbol, finish it by adjusting
49 the prefix and appending the key size. Other symbols are left unchanged.
50 """
51 return re.sub(r'_FAMILY_(.*)', r'_\1_' + str(bits), dep)
52
53def finish_family_dependencies(dependencies: List[str], bits: int) -> List[str]:
54 """Finish any family dependency symbol prefixes.
55
56 Apply `finish_family_dependency` to each element of `dependencies`.
57 """
58 return [finish_family_dependency(dep, bits) for dep in dependencies]
Gilles Peskineaf172842021-01-27 18:24:48 +010059
Gilles Peskined169d602021-02-16 14:16:25 +010060# A temporary hack: at the time of writing, not all dependency symbols
61# are implemented yet. Skip test cases for which the dependency symbols are
62# not available. Once all dependency symbols are available, this hack must
63# be removed so that a bug in the dependency symbols proprely leads to a test
64# failure.
65def read_implemented_dependencies(filename: str) -> FrozenSet[str]:
66 return frozenset(symbol
67 for line in open(filename)
68 for symbol in re.findall(r'\bPSA_WANT_\w+\b', line))
69IMPLEMENTED_DEPENDENCIES = read_implemented_dependencies('include/psa/crypto_config.h')
70def hack_dependencies_not_implemented(dependencies: List[str]) -> None:
71 if not all(dep.lstrip('!') in IMPLEMENTED_DEPENDENCIES
72 for dep in dependencies):
73 dependencies.append('DEPENDENCY_NOT_IMPLEMENTED_YET')
74
Gilles Peskine14e428f2021-01-26 22:19:21 +010075
Gilles Peskineb94ea512021-03-10 02:12:08 +010076class Information:
77 """Gather information about PSA constructors."""
Gilles Peskine09940492021-01-26 22:16:30 +010078
Gilles Peskineb94ea512021-03-10 02:12:08 +010079 def __init__(self) -> None:
Gilles Peskine09940492021-01-26 22:16:30 +010080 self.constructors = self.read_psa_interface()
81
82 @staticmethod
Gilles Peskine09940492021-01-26 22:16:30 +010083 def remove_unwanted_macros(
84 constructors: macro_collector.PSAMacroCollector
85 ) -> None:
86 # Mbed TLS doesn't support DSA. Don't attempt to generate any related
87 # test case.
88 constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR')
89 constructors.key_types.discard('PSA_KEY_TYPE_DSA_PUBLIC_KEY')
90 constructors.algorithms_from_hash.pop('PSA_ALG_DSA', None)
91 constructors.algorithms_from_hash.pop('PSA_ALG_DETERMINISTIC_DSA', None)
92
93 def read_psa_interface(self) -> macro_collector.PSAMacroCollector:
94 """Return the list of known key types, algorithms, etc."""
95 constructors = macro_collector.PSAMacroCollector()
96 header_file_names = ['include/psa/crypto_values.h',
97 'include/psa/crypto_extra.h']
98 for header_file_name in header_file_names:
99 with open(header_file_name, 'rb') as header_file:
100 constructors.read_file(header_file)
101 self.remove_unwanted_macros(constructors)
102 return constructors
103
Gilles Peskine14e428f2021-01-26 22:19:21 +0100104
Gilles Peskineb94ea512021-03-10 02:12:08 +0100105def test_case_for_key_type_not_supported(
106 verb: str, key_type: str, bits: int,
107 dependencies: List[str],
108 *args: str,
109 param_descr: str = ''
110) -> test_case.TestCase:
111 """Return one test case exercising a key creation method
112 for an unsupported key type or size.
113 """
114 hack_dependencies_not_implemented(dependencies)
115 tc = test_case.TestCase()
116 short_key_type = re.sub(r'PSA_(KEY_TYPE|ECC_FAMILY)_', r'', key_type)
117 adverb = 'not' if dependencies else 'never'
118 if param_descr:
119 adverb = param_descr + ' ' + adverb
120 tc.set_description('PSA {} {} {}-bit {} supported'
121 .format(verb, short_key_type, bits, adverb))
122 tc.set_dependencies(dependencies)
123 tc.set_function(verb + '_not_supported')
124 tc.set_arguments([key_type] + list(args))
125 return tc
126
127class NotSupported:
128 """Generate test cases for when something is not supported."""
129
130 def __init__(self, info: Information) -> None:
131 self.constructors = info.constructors
Gilles Peskine14e428f2021-01-26 22:19:21 +0100132
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100133 ALWAYS_SUPPORTED = frozenset([
134 'PSA_KEY_TYPE_DERIVE',
135 'PSA_KEY_TYPE_RAW_DATA',
136 ])
Gilles Peskine14e428f2021-01-26 22:19:21 +0100137 def test_cases_for_key_type_not_supported(
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100138 self,
Gilles Peskineaf172842021-01-27 18:24:48 +0100139 kt: crypto_knowledge.KeyType,
140 param: Optional[int] = None,
141 param_descr: str = '',
Gilles Peskine3d778392021-02-17 15:11:05 +0100142 ) -> Iterator[test_case.TestCase]:
Gilles Peskineaf172842021-01-27 18:24:48 +0100143 """Return test cases exercising key creation when the given type is unsupported.
144
145 If param is present and not None, emit test cases conditioned on this
146 parameter not being supported. If it is absent or None, emit test cases
147 conditioned on the base type not being supported.
148 """
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100149 if kt.name in self.ALWAYS_SUPPORTED:
150 # Don't generate test cases for key types that are always supported.
151 # They would be skipped in all configurations, which is noise.
Gilles Peskine3d778392021-02-17 15:11:05 +0100152 return
Gilles Peskineaf172842021-01-27 18:24:48 +0100153 import_dependencies = [('!' if param is None else '') +
154 psa_want_symbol(kt.name)]
155 if kt.params is not None:
156 import_dependencies += [('!' if param == i else '') +
157 psa_want_symbol(sym)
158 for i, sym in enumerate(kt.params)]
Gilles Peskine14e428f2021-01-26 22:19:21 +0100159 if kt.name.endswith('_PUBLIC_KEY'):
160 generate_dependencies = []
161 else:
162 generate_dependencies = import_dependencies
Gilles Peskine14e428f2021-01-26 22:19:21 +0100163 for bits in kt.sizes_to_test():
Gilles Peskine3d778392021-02-17 15:11:05 +0100164 yield test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100165 'import', kt.expression, bits,
166 finish_family_dependencies(import_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100167 test_case.hex_string(kt.key_material(bits)),
168 param_descr=param_descr,
Gilles Peskine3d778392021-02-17 15:11:05 +0100169 )
Gilles Peskineaf172842021-01-27 18:24:48 +0100170 if not generate_dependencies and param is not None:
171 # If generation is impossible for this key type, rather than
172 # supported or not depending on implementation capabilities,
173 # only generate the test case once.
174 continue
Gilles Peskine3d778392021-02-17 15:11:05 +0100175 yield test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100176 'generate', kt.expression, bits,
177 finish_family_dependencies(generate_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100178 str(bits),
179 param_descr=param_descr,
Gilles Peskine3d778392021-02-17 15:11:05 +0100180 )
Gilles Peskine14e428f2021-01-26 22:19:21 +0100181 # To be added: derive
Gilles Peskine14e428f2021-01-26 22:19:21 +0100182
Gilles Peskine3d778392021-02-17 15:11:05 +0100183 def test_cases_for_not_supported(self) -> Iterator[test_case.TestCase]:
Gilles Peskine14e428f2021-01-26 22:19:21 +0100184 """Generate test cases that exercise the creation of keys of unsupported types."""
Gilles Peskine14e428f2021-01-26 22:19:21 +0100185 for key_type in sorted(self.constructors.key_types):
186 kt = crypto_knowledge.KeyType(key_type)
Gilles Peskine3d778392021-02-17 15:11:05 +0100187 yield from self.test_cases_for_key_type_not_supported(kt)
Gilles Peskineaf172842021-01-27 18:24:48 +0100188 for curve_family in sorted(self.constructors.ecc_curves):
189 for constr in ('PSA_KEY_TYPE_ECC_KEY_PAIR',
190 'PSA_KEY_TYPE_ECC_PUBLIC_KEY'):
191 kt = crypto_knowledge.KeyType(constr, [curve_family])
Gilles Peskine3d778392021-02-17 15:11:05 +0100192 yield from self.test_cases_for_key_type_not_supported(
Gilles Peskineaf172842021-01-27 18:24:48 +0100193 kt, param_descr='type')
Gilles Peskine3d778392021-02-17 15:11:05 +0100194 yield from self.test_cases_for_key_type_not_supported(
Gilles Peskineaf172842021-01-27 18:24:48 +0100195 kt, 0, param_descr='curve')
Gilles Peskineb94ea512021-03-10 02:12:08 +0100196
197
198class TestGenerator:
199 """Generate test data."""
200
201 def __init__(self, options) -> None:
202 self.test_suite_directory = self.get_option(options, 'directory',
203 'tests/suites')
204 self.info = Information()
205
206 @staticmethod
207 def get_option(options, name: str, default: T) -> T:
208 value = getattr(options, name, None)
209 return default if value is None else value
210
Gilles Peskine0298bda2021-03-10 02:34:37 +0100211 def filename_for(self, basename: str) -> str:
212 """The location of the data file with the specified base name."""
213 return os.path.join(self.test_suite_directory, basename + '.data')
214
Gilles Peskineb94ea512021-03-10 02:12:08 +0100215 def write_test_data_file(self, basename: str,
216 test_cases: Iterable[test_case.TestCase]) -> None:
217 """Write the test cases to a .data file.
218
219 The output file is ``basename + '.data'`` in the test suite directory.
220 """
Gilles Peskine0298bda2021-03-10 02:34:37 +0100221 filename = self.filename_for(basename)
Gilles Peskineb94ea512021-03-10 02:12:08 +0100222 test_case.write_data_file(filename, test_cases)
223
Gilles Peskine0298bda2021-03-10 02:34:37 +0100224 TARGETS = {
225 'test_suite_psa_crypto_not_supported.generated':
Gilles Peskine3d778392021-02-17 15:11:05 +0100226 lambda info: NotSupported(info).test_cases_for_not_supported(),
Gilles Peskine0298bda2021-03-10 02:34:37 +0100227 } #type: Dict[str, Callable[[Information], Iterable[test_case.TestCase]]]
228
229 def generate_target(self, name: str) -> None:
230 test_cases = self.TARGETS[name](self.info)
231 self.write_test_data_file(name, test_cases)
Gilles Peskine14e428f2021-01-26 22:19:21 +0100232
Gilles Peskine09940492021-01-26 22:16:30 +0100233def main(args):
234 """Command line entry point."""
235 parser = argparse.ArgumentParser(description=__doc__)
Gilles Peskine0298bda2021-03-10 02:34:37 +0100236 parser.add_argument('--list', action='store_true',
237 help='List available targets and exit')
238 parser.add_argument('targets', nargs='*', metavar='TARGET',
239 help='Target file to generate (default: all; "-": none)')
Gilles Peskine09940492021-01-26 22:16:30 +0100240 options = parser.parse_args(args)
241 generator = TestGenerator(options)
Gilles Peskine0298bda2021-03-10 02:34:37 +0100242 if options.list:
243 for name in sorted(generator.TARGETS):
244 print(generator.filename_for(name))
245 return
246 if options.targets:
247 # Allow "-" as a special case so you can run
248 # ``generate_psa_tests.py - $targets`` and it works uniformly whether
249 # ``$targets`` is empty or not.
250 options.targets = [os.path.basename(re.sub(r'\.data\Z', r'', target))
251 for target in options.targets
252 if target != '-']
253 else:
254 options.targets = sorted(generator.TARGETS)
255 for target in options.targets:
256 generator.generate_target(target)
Gilles Peskine09940492021-01-26 22:16:30 +0100257
258if __name__ == '__main__':
259 main(sys.argv[1:])