blob: a17dc47e778712e225030b052d4f553a18878563 [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 Peskine897dff92021-03-10 15:03:44 +010032from mbedtls_dev import psa_storage
Gilles Peskine14e428f2021-01-26 22:19:21 +010033from mbedtls_dev import test_case
Gilles Peskine09940492021-01-26 22:16:30 +010034
35T = TypeVar('T') #pylint: disable=invalid-name
36
Gilles Peskine14e428f2021-01-26 22:19:21 +010037
Gilles Peskine7f756872021-02-16 12:13:12 +010038def psa_want_symbol(name: str) -> str:
Gilles Peskineaf172842021-01-27 18:24:48 +010039 """Return the PSA_WANT_xxx symbol associated with a PSA crypto feature."""
40 if name.startswith('PSA_'):
41 return name[:4] + 'WANT_' + name[4:]
42 else:
43 raise ValueError('Unable to determine the PSA_WANT_ symbol for ' + name)
44
Gilles Peskine7f756872021-02-16 12:13:12 +010045def finish_family_dependency(dep: str, bits: int) -> str:
46 """Finish dep if it's a family dependency symbol prefix.
47
48 A family dependency symbol prefix is a PSA_WANT_ symbol that needs to be
49 qualified by the key size. If dep is such a symbol, finish it by adjusting
50 the prefix and appending the key size. Other symbols are left unchanged.
51 """
52 return re.sub(r'_FAMILY_(.*)', r'_\1_' + str(bits), dep)
53
54def finish_family_dependencies(dependencies: List[str], bits: int) -> List[str]:
55 """Finish any family dependency symbol prefixes.
56
57 Apply `finish_family_dependency` to each element of `dependencies`.
58 """
59 return [finish_family_dependency(dep, bits) for dep in dependencies]
Gilles Peskineaf172842021-01-27 18:24:48 +010060
Gilles Peskined169d602021-02-16 14:16:25 +010061# A temporary hack: at the time of writing, not all dependency symbols
62# are implemented yet. Skip test cases for which the dependency symbols are
63# not available. Once all dependency symbols are available, this hack must
64# be removed so that a bug in the dependency symbols proprely leads to a test
65# failure.
66def read_implemented_dependencies(filename: str) -> FrozenSet[str]:
67 return frozenset(symbol
68 for line in open(filename)
69 for symbol in re.findall(r'\bPSA_WANT_\w+\b', line))
70IMPLEMENTED_DEPENDENCIES = read_implemented_dependencies('include/psa/crypto_config.h')
71def hack_dependencies_not_implemented(dependencies: List[str]) -> None:
72 if not all(dep.lstrip('!') in IMPLEMENTED_DEPENDENCIES
73 for dep in dependencies):
74 dependencies.append('DEPENDENCY_NOT_IMPLEMENTED_YET')
75
Gilles Peskine14e428f2021-01-26 22:19:21 +010076
Gilles Peskineb94ea512021-03-10 02:12:08 +010077class Information:
78 """Gather information about PSA constructors."""
Gilles Peskine09940492021-01-26 22:16:30 +010079
Gilles Peskineb94ea512021-03-10 02:12:08 +010080 def __init__(self) -> None:
Gilles Peskine09940492021-01-26 22:16:30 +010081 self.constructors = self.read_psa_interface()
82
83 @staticmethod
Gilles Peskine09940492021-01-26 22:16:30 +010084 def remove_unwanted_macros(
85 constructors: macro_collector.PSAMacroCollector
86 ) -> None:
87 # Mbed TLS doesn't support DSA. Don't attempt to generate any related
88 # test case.
89 constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR')
90 constructors.key_types.discard('PSA_KEY_TYPE_DSA_PUBLIC_KEY')
91 constructors.algorithms_from_hash.pop('PSA_ALG_DSA', None)
92 constructors.algorithms_from_hash.pop('PSA_ALG_DETERMINISTIC_DSA', None)
93
94 def read_psa_interface(self) -> macro_collector.PSAMacroCollector:
95 """Return the list of known key types, algorithms, etc."""
96 constructors = macro_collector.PSAMacroCollector()
97 header_file_names = ['include/psa/crypto_values.h',
98 'include/psa/crypto_extra.h']
99 for header_file_name in header_file_names:
100 with open(header_file_name, 'rb') as header_file:
101 constructors.read_file(header_file)
102 self.remove_unwanted_macros(constructors)
103 return constructors
104
Gilles Peskine14e428f2021-01-26 22:19:21 +0100105
Gilles Peskineb94ea512021-03-10 02:12:08 +0100106def test_case_for_key_type_not_supported(
107 verb: str, key_type: str, bits: int,
108 dependencies: List[str],
109 *args: str,
110 param_descr: str = ''
111) -> test_case.TestCase:
112 """Return one test case exercising a key creation method
113 for an unsupported key type or size.
114 """
115 hack_dependencies_not_implemented(dependencies)
116 tc = test_case.TestCase()
117 short_key_type = re.sub(r'PSA_(KEY_TYPE|ECC_FAMILY)_', r'', key_type)
118 adverb = 'not' if dependencies else 'never'
119 if param_descr:
120 adverb = param_descr + ' ' + adverb
121 tc.set_description('PSA {} {} {}-bit {} supported'
122 .format(verb, short_key_type, bits, adverb))
123 tc.set_dependencies(dependencies)
124 tc.set_function(verb + '_not_supported')
125 tc.set_arguments([key_type] + list(args))
126 return tc
127
128class NotSupported:
129 """Generate test cases for when something is not supported."""
130
131 def __init__(self, info: Information) -> None:
132 self.constructors = info.constructors
Gilles Peskine14e428f2021-01-26 22:19:21 +0100133
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100134 ALWAYS_SUPPORTED = frozenset([
135 'PSA_KEY_TYPE_DERIVE',
136 'PSA_KEY_TYPE_RAW_DATA',
137 ])
Gilles Peskine14e428f2021-01-26 22:19:21 +0100138 def test_cases_for_key_type_not_supported(
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100139 self,
Gilles Peskineaf172842021-01-27 18:24:48 +0100140 kt: crypto_knowledge.KeyType,
141 param: Optional[int] = None,
142 param_descr: str = '',
Gilles Peskine3d778392021-02-17 15:11:05 +0100143 ) -> Iterator[test_case.TestCase]:
Gilles Peskineaf172842021-01-27 18:24:48 +0100144 """Return test cases exercising key creation when the given type is unsupported.
145
146 If param is present and not None, emit test cases conditioned on this
147 parameter not being supported. If it is absent or None, emit test cases
148 conditioned on the base type not being supported.
149 """
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100150 if kt.name in self.ALWAYS_SUPPORTED:
151 # Don't generate test cases for key types that are always supported.
152 # They would be skipped in all configurations, which is noise.
Gilles Peskine3d778392021-02-17 15:11:05 +0100153 return
Gilles Peskineaf172842021-01-27 18:24:48 +0100154 import_dependencies = [('!' if param is None else '') +
155 psa_want_symbol(kt.name)]
156 if kt.params is not None:
157 import_dependencies += [('!' if param == i else '') +
158 psa_want_symbol(sym)
159 for i, sym in enumerate(kt.params)]
Gilles Peskine14e428f2021-01-26 22:19:21 +0100160 if kt.name.endswith('_PUBLIC_KEY'):
161 generate_dependencies = []
162 else:
163 generate_dependencies = import_dependencies
Gilles Peskine14e428f2021-01-26 22:19:21 +0100164 for bits in kt.sizes_to_test():
Gilles Peskine3d778392021-02-17 15:11:05 +0100165 yield test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100166 'import', kt.expression, bits,
167 finish_family_dependencies(import_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100168 test_case.hex_string(kt.key_material(bits)),
169 param_descr=param_descr,
Gilles Peskine3d778392021-02-17 15:11:05 +0100170 )
Gilles Peskineaf172842021-01-27 18:24:48 +0100171 if not generate_dependencies and param is not None:
172 # If generation is impossible for this key type, rather than
173 # supported or not depending on implementation capabilities,
174 # only generate the test case once.
175 continue
Gilles Peskine3d778392021-02-17 15:11:05 +0100176 yield test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100177 'generate', kt.expression, bits,
178 finish_family_dependencies(generate_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100179 str(bits),
180 param_descr=param_descr,
Gilles Peskine3d778392021-02-17 15:11:05 +0100181 )
Gilles Peskine14e428f2021-01-26 22:19:21 +0100182 # To be added: derive
Gilles Peskine14e428f2021-01-26 22:19:21 +0100183
Gilles Peskine3d778392021-02-17 15:11:05 +0100184 def test_cases_for_not_supported(self) -> Iterator[test_case.TestCase]:
Gilles Peskine14e428f2021-01-26 22:19:21 +0100185 """Generate test cases that exercise the creation of keys of unsupported types."""
Gilles Peskine14e428f2021-01-26 22:19:21 +0100186 for key_type in sorted(self.constructors.key_types):
187 kt = crypto_knowledge.KeyType(key_type)
Gilles Peskine3d778392021-02-17 15:11:05 +0100188 yield from self.test_cases_for_key_type_not_supported(kt)
Gilles Peskineaf172842021-01-27 18:24:48 +0100189 for curve_family in sorted(self.constructors.ecc_curves):
190 for constr in ('PSA_KEY_TYPE_ECC_KEY_PAIR',
191 'PSA_KEY_TYPE_ECC_PUBLIC_KEY'):
192 kt = crypto_knowledge.KeyType(constr, [curve_family])
Gilles Peskine3d778392021-02-17 15:11:05 +0100193 yield from self.test_cases_for_key_type_not_supported(
Gilles Peskineaf172842021-01-27 18:24:48 +0100194 kt, param_descr='type')
Gilles Peskine3d778392021-02-17 15:11:05 +0100195 yield from self.test_cases_for_key_type_not_supported(
Gilles Peskineaf172842021-01-27 18:24:48 +0100196 kt, 0, param_descr='curve')
Gilles Peskineb94ea512021-03-10 02:12:08 +0100197
198
Gilles Peskine897dff92021-03-10 15:03:44 +0100199class StorageKey(psa_storage.Key):
200 """Representation of a key for storage format testing."""
201
202 def __init__(self, *, description: str, **kwargs) -> None:
203 super().__init__(**kwargs)
204 self.description = description #type: str
205
206class StorageFormat:
207 """Storage format stability test cases."""
208
209 def __init__(self, info: Information, version: int, forward: bool) -> None:
210 """Prepare to generate test cases for storage format stability.
211
212 * `info`: information about the API. See the `Information` class.
213 * `version`: the storage format version to generate test cases for.
214 * `forward`: if true, generate forward compatibility test cases which
215 save a key and check that its representation is as intended. Otherwise
216 generate backward compatibility test cases which inject a key
217 representation and check that it can be read and used.
218 """
219 self.constructors = info.constructors
220 self.version = version
221 self.forward = forward
222
223 def make_test_case(self, key: StorageKey) -> test_case.TestCase:
224 """Construct a storage format test case for the given key.
225
226 If ``forward`` is true, generate a forward compatibility test case:
227 create a key and validate that it has the expected representation.
228 Otherwise generate a backward compatibility test case: inject the
229 key representation into storage and validate that it can be read
230 correctly.
231 """
232 verb = 'save' if self.forward else 'read'
233 tc = test_case.TestCase()
234 tc.set_description('PSA storage {}: {}'.format(verb, key.description))
235 tc.set_function('key_storage_' + verb)
236 if self.forward:
237 extra_arguments = []
238 else:
239 # Some test keys have the RAW_DATA type and attributes that don't
240 # necessarily make sense. We do this to validate numerical
241 # encodings of the attributes.
242 # Raw data keys have no useful exercise anyway so there is no
243 # loss of test coverage.
244 exercise = key.type.string != 'PSA_KEY_TYPE_RAW_DATA'
245 extra_arguments = ['1' if exercise else '0']
246 tc.set_arguments([key.lifetime.string,
247 key.type.string, str(key.bits),
248 key.usage.string, key.alg.string, key.alg2.string,
249 '"' + key.material.hex() + '"',
250 '"' + key.hex() + '"',
251 *extra_arguments])
252 return tc
253
254 def key_for_usage_flags(
255 self,
256 usage_flags: List[str],
257 short: Optional[str] = None
258 ) -> StorageKey:
259 """Construct a test key for the given key usage."""
260 usage = ' | '.join(usage_flags) if usage_flags else '0'
261 if short is None:
262 short = re.sub(r'\bPSA_KEY_USAGE_', r'', usage)
263 description = 'usage: ' + short
264 key = StorageKey(version=self.version,
265 id=1, lifetime=0x00000001,
266 type='PSA_KEY_TYPE_RAW_DATA', bits=8,
267 usage=usage, alg=0, alg2=0,
268 material=b'K',
269 description=description)
270 return key
271
272 def all_keys_for_usage_flags(self) -> Iterator[StorageKey]:
273 """Generate test keys covering usage flags."""
274 known_flags = sorted(self.constructors.key_usage_flags)
275 yield self.key_for_usage_flags(['0'])
276 for usage_flag in known_flags:
277 yield self.key_for_usage_flags([usage_flag])
278 for flag1, flag2 in zip(known_flags,
279 known_flags[1:] + [known_flags[0]]):
280 yield self.key_for_usage_flags([flag1, flag2])
281 yield self.key_for_usage_flags(known_flags, short='all known')
282
283 def all_test_cases(self) -> Iterator[test_case.TestCase]:
284 """Generate all storage format test cases."""
285 for key in self.all_keys_for_usage_flags():
286 yield self.make_test_case(key)
287
288
Gilles Peskineb94ea512021-03-10 02:12:08 +0100289class TestGenerator:
290 """Generate test data."""
291
292 def __init__(self, options) -> None:
293 self.test_suite_directory = self.get_option(options, 'directory',
294 'tests/suites')
295 self.info = Information()
296
297 @staticmethod
298 def get_option(options, name: str, default: T) -> T:
299 value = getattr(options, name, None)
300 return default if value is None else value
301
Gilles Peskine0298bda2021-03-10 02:34:37 +0100302 def filename_for(self, basename: str) -> str:
303 """The location of the data file with the specified base name."""
304 return os.path.join(self.test_suite_directory, basename + '.data')
305
Gilles Peskineb94ea512021-03-10 02:12:08 +0100306 def write_test_data_file(self, basename: str,
307 test_cases: Iterable[test_case.TestCase]) -> None:
308 """Write the test cases to a .data file.
309
310 The output file is ``basename + '.data'`` in the test suite directory.
311 """
Gilles Peskine0298bda2021-03-10 02:34:37 +0100312 filename = self.filename_for(basename)
Gilles Peskineb94ea512021-03-10 02:12:08 +0100313 test_case.write_data_file(filename, test_cases)
314
Gilles Peskine0298bda2021-03-10 02:34:37 +0100315 TARGETS = {
316 'test_suite_psa_crypto_not_supported.generated':
Gilles Peskine3d778392021-02-17 15:11:05 +0100317 lambda info: NotSupported(info).test_cases_for_not_supported(),
Gilles Peskine897dff92021-03-10 15:03:44 +0100318 'test_suite_psa_crypto_storage_format.current':
319 lambda info: StorageFormat(info, 0, True).all_test_cases(),
320 'test_suite_psa_crypto_storage_format.v0':
321 lambda info: StorageFormat(info, 0, False).all_test_cases(),
Gilles Peskine0298bda2021-03-10 02:34:37 +0100322 } #type: Dict[str, Callable[[Information], Iterable[test_case.TestCase]]]
323
324 def generate_target(self, name: str) -> None:
325 test_cases = self.TARGETS[name](self.info)
326 self.write_test_data_file(name, test_cases)
Gilles Peskine14e428f2021-01-26 22:19:21 +0100327
Gilles Peskine09940492021-01-26 22:16:30 +0100328def main(args):
329 """Command line entry point."""
330 parser = argparse.ArgumentParser(description=__doc__)
Gilles Peskine0298bda2021-03-10 02:34:37 +0100331 parser.add_argument('--list', action='store_true',
332 help='List available targets and exit')
333 parser.add_argument('targets', nargs='*', metavar='TARGET',
334 help='Target file to generate (default: all; "-": none)')
Gilles Peskine09940492021-01-26 22:16:30 +0100335 options = parser.parse_args(args)
336 generator = TestGenerator(options)
Gilles Peskine0298bda2021-03-10 02:34:37 +0100337 if options.list:
338 for name in sorted(generator.TARGETS):
339 print(generator.filename_for(name))
340 return
341 if options.targets:
342 # Allow "-" as a special case so you can run
343 # ``generate_psa_tests.py - $targets`` and it works uniformly whether
344 # ``$targets`` is empty or not.
345 options.targets = [os.path.basename(re.sub(r'\.data\Z', r'', target))
346 for target in options.targets
347 if target != '-']
348 else:
349 options.targets = sorted(generator.TARGETS)
350 for target in options.targets:
351 generator.generate_target(target)
Gilles Peskine09940492021-01-26 22:16:30 +0100352
353if __name__ == '__main__':
354 main(sys.argv[1:])