blob: ab8ebffe44b6e73fea54b88252fbf99c88752646 [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 Peskinec86f20a2021-04-22 00:20:47 +020030from mbedtls_dev import build_tree
Gilles Peskine14e428f2021-01-26 22:19:21 +010031from mbedtls_dev import crypto_knowledge
Gilles Peskine09940492021-01-26 22:16:30 +010032from mbedtls_dev import macro_collector
Gilles Peskine897dff92021-03-10 15:03:44 +010033from mbedtls_dev import psa_storage
Gilles Peskine14e428f2021-01-26 22:19:21 +010034from mbedtls_dev import test_case
Gilles Peskine09940492021-01-26 22:16:30 +010035
36T = TypeVar('T') #pylint: disable=invalid-name
37
Gilles Peskine14e428f2021-01-26 22:19:21 +010038
Gilles Peskine7f756872021-02-16 12:13:12 +010039def psa_want_symbol(name: str) -> str:
Gilles Peskineaf172842021-01-27 18:24:48 +010040 """Return the PSA_WANT_xxx symbol associated with a PSA crypto feature."""
41 if name.startswith('PSA_'):
42 return name[:4] + 'WANT_' + name[4:]
43 else:
44 raise ValueError('Unable to determine the PSA_WANT_ symbol for ' + name)
45
Gilles Peskine7f756872021-02-16 12:13:12 +010046def finish_family_dependency(dep: str, bits: int) -> str:
47 """Finish dep if it's a family dependency symbol prefix.
48
49 A family dependency symbol prefix is a PSA_WANT_ symbol that needs to be
50 qualified by the key size. If dep is such a symbol, finish it by adjusting
51 the prefix and appending the key size. Other symbols are left unchanged.
52 """
53 return re.sub(r'_FAMILY_(.*)', r'_\1_' + str(bits), dep)
54
55def finish_family_dependencies(dependencies: List[str], bits: int) -> List[str]:
56 """Finish any family dependency symbol prefixes.
57
58 Apply `finish_family_dependency` to each element of `dependencies`.
59 """
60 return [finish_family_dependency(dep, bits) for dep in dependencies]
Gilles Peskineaf172842021-01-27 18:24:48 +010061
Gilles Peskinef8223ab2021-03-10 15:07:16 +010062def automatic_dependencies(*expressions: str) -> List[str]:
63 """Infer dependencies of a test case by looking for PSA_xxx symbols.
64
65 The arguments are strings which should be C expressions. Do not use
66 string literals or comments as this function is not smart enough to
67 skip them.
68 """
69 used = set()
70 for expr in expressions:
71 used.update(re.findall(r'PSA_(?:ALG|ECC_FAMILY|KEY_TYPE)_\w+', expr))
72 return sorted(psa_want_symbol(name) for name in used)
73
Gilles Peskined169d602021-02-16 14:16:25 +010074# A temporary hack: at the time of writing, not all dependency symbols
75# are implemented yet. Skip test cases for which the dependency symbols are
76# not available. Once all dependency symbols are available, this hack must
77# be removed so that a bug in the dependency symbols proprely leads to a test
78# failure.
79def read_implemented_dependencies(filename: str) -> FrozenSet[str]:
80 return frozenset(symbol
81 for line in open(filename)
82 for symbol in re.findall(r'\bPSA_WANT_\w+\b', line))
Gilles Peskinec86f20a2021-04-22 00:20:47 +020083_implemented_dependencies = None #type: Optional[FrozenSet[str]] #pylint: disable=invalid-name
Gilles Peskined169d602021-02-16 14:16:25 +010084def hack_dependencies_not_implemented(dependencies: List[str]) -> None:
Gilles Peskinec86f20a2021-04-22 00:20:47 +020085 global _implemented_dependencies #pylint: disable=global-statement,invalid-name
86 if _implemented_dependencies is None:
87 _implemented_dependencies = \
88 read_implemented_dependencies('include/psa/crypto_config.h')
89 if not all(dep.lstrip('!') in _implemented_dependencies
Gilles Peskined169d602021-02-16 14:16:25 +010090 for dep in dependencies):
91 dependencies.append('DEPENDENCY_NOT_IMPLEMENTED_YET')
92
Gilles Peskine14e428f2021-01-26 22:19:21 +010093
Gilles Peskineb94ea512021-03-10 02:12:08 +010094class Information:
95 """Gather information about PSA constructors."""
Gilles Peskine09940492021-01-26 22:16:30 +010096
Gilles Peskineb94ea512021-03-10 02:12:08 +010097 def __init__(self) -> None:
Gilles Peskine09940492021-01-26 22:16:30 +010098 self.constructors = self.read_psa_interface()
99
100 @staticmethod
Gilles Peskine09940492021-01-26 22:16:30 +0100101 def remove_unwanted_macros(
102 constructors: macro_collector.PSAMacroCollector
103 ) -> None:
104 # Mbed TLS doesn't support DSA. Don't attempt to generate any related
105 # test case.
106 constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR')
107 constructors.key_types.discard('PSA_KEY_TYPE_DSA_PUBLIC_KEY')
108 constructors.algorithms_from_hash.pop('PSA_ALG_DSA', None)
109 constructors.algorithms_from_hash.pop('PSA_ALG_DETERMINISTIC_DSA', None)
110
111 def read_psa_interface(self) -> macro_collector.PSAMacroCollector:
112 """Return the list of known key types, algorithms, etc."""
113 constructors = macro_collector.PSAMacroCollector()
114 header_file_names = ['include/psa/crypto_values.h',
115 'include/psa/crypto_extra.h']
116 for header_file_name in header_file_names:
117 with open(header_file_name, 'rb') as header_file:
118 constructors.read_file(header_file)
119 self.remove_unwanted_macros(constructors)
120 return constructors
121
Gilles Peskine14e428f2021-01-26 22:19:21 +0100122
Gilles Peskineb94ea512021-03-10 02:12:08 +0100123def test_case_for_key_type_not_supported(
124 verb: str, key_type: str, bits: int,
125 dependencies: List[str],
126 *args: str,
127 param_descr: str = ''
128) -> test_case.TestCase:
129 """Return one test case exercising a key creation method
130 for an unsupported key type or size.
131 """
132 hack_dependencies_not_implemented(dependencies)
133 tc = test_case.TestCase()
134 short_key_type = re.sub(r'PSA_(KEY_TYPE|ECC_FAMILY)_', r'', key_type)
135 adverb = 'not' if dependencies else 'never'
136 if param_descr:
137 adverb = param_descr + ' ' + adverb
138 tc.set_description('PSA {} {} {}-bit {} supported'
139 .format(verb, short_key_type, bits, adverb))
140 tc.set_dependencies(dependencies)
141 tc.set_function(verb + '_not_supported')
142 tc.set_arguments([key_type] + list(args))
143 return tc
144
145class NotSupported:
146 """Generate test cases for when something is not supported."""
147
148 def __init__(self, info: Information) -> None:
149 self.constructors = info.constructors
Gilles Peskine14e428f2021-01-26 22:19:21 +0100150
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100151 ALWAYS_SUPPORTED = frozenset([
152 'PSA_KEY_TYPE_DERIVE',
153 'PSA_KEY_TYPE_RAW_DATA',
154 ])
Gilles Peskine14e428f2021-01-26 22:19:21 +0100155 def test_cases_for_key_type_not_supported(
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100156 self,
Gilles Peskineaf172842021-01-27 18:24:48 +0100157 kt: crypto_knowledge.KeyType,
158 param: Optional[int] = None,
159 param_descr: str = '',
Gilles Peskine3d778392021-02-17 15:11:05 +0100160 ) -> Iterator[test_case.TestCase]:
Gilles Peskineaf172842021-01-27 18:24:48 +0100161 """Return test cases exercising key creation when the given type is unsupported.
162
163 If param is present and not None, emit test cases conditioned on this
164 parameter not being supported. If it is absent or None, emit test cases
165 conditioned on the base type not being supported.
166 """
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100167 if kt.name in self.ALWAYS_SUPPORTED:
168 # Don't generate test cases for key types that are always supported.
169 # They would be skipped in all configurations, which is noise.
Gilles Peskine3d778392021-02-17 15:11:05 +0100170 return
Gilles Peskineaf172842021-01-27 18:24:48 +0100171 import_dependencies = [('!' if param is None else '') +
172 psa_want_symbol(kt.name)]
173 if kt.params is not None:
174 import_dependencies += [('!' if param == i else '') +
175 psa_want_symbol(sym)
176 for i, sym in enumerate(kt.params)]
Gilles Peskine14e428f2021-01-26 22:19:21 +0100177 if kt.name.endswith('_PUBLIC_KEY'):
178 generate_dependencies = []
179 else:
180 generate_dependencies = import_dependencies
Gilles Peskine14e428f2021-01-26 22:19:21 +0100181 for bits in kt.sizes_to_test():
Gilles Peskine3d778392021-02-17 15:11:05 +0100182 yield test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100183 'import', kt.expression, bits,
184 finish_family_dependencies(import_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100185 test_case.hex_string(kt.key_material(bits)),
186 param_descr=param_descr,
Gilles Peskine3d778392021-02-17 15:11:05 +0100187 )
Gilles Peskineaf172842021-01-27 18:24:48 +0100188 if not generate_dependencies and param is not None:
189 # If generation is impossible for this key type, rather than
190 # supported or not depending on implementation capabilities,
191 # only generate the test case once.
192 continue
Gilles Peskine3d778392021-02-17 15:11:05 +0100193 yield test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100194 'generate', kt.expression, bits,
195 finish_family_dependencies(generate_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100196 str(bits),
197 param_descr=param_descr,
Gilles Peskine3d778392021-02-17 15:11:05 +0100198 )
Gilles Peskine14e428f2021-01-26 22:19:21 +0100199 # To be added: derive
Gilles Peskine14e428f2021-01-26 22:19:21 +0100200
Gilles Peskine3d778392021-02-17 15:11:05 +0100201 def test_cases_for_not_supported(self) -> Iterator[test_case.TestCase]:
Gilles Peskine14e428f2021-01-26 22:19:21 +0100202 """Generate test cases that exercise the creation of keys of unsupported types."""
Gilles Peskine14e428f2021-01-26 22:19:21 +0100203 for key_type in sorted(self.constructors.key_types):
204 kt = crypto_knowledge.KeyType(key_type)
Gilles Peskine3d778392021-02-17 15:11:05 +0100205 yield from self.test_cases_for_key_type_not_supported(kt)
Gilles Peskineaf172842021-01-27 18:24:48 +0100206 for curve_family in sorted(self.constructors.ecc_curves):
207 for constr in ('PSA_KEY_TYPE_ECC_KEY_PAIR',
208 'PSA_KEY_TYPE_ECC_PUBLIC_KEY'):
209 kt = crypto_knowledge.KeyType(constr, [curve_family])
Gilles Peskine3d778392021-02-17 15:11:05 +0100210 yield from self.test_cases_for_key_type_not_supported(
Gilles Peskineaf172842021-01-27 18:24:48 +0100211 kt, param_descr='type')
Gilles Peskine3d778392021-02-17 15:11:05 +0100212 yield from self.test_cases_for_key_type_not_supported(
Gilles Peskineaf172842021-01-27 18:24:48 +0100213 kt, 0, param_descr='curve')
Gilles Peskineb94ea512021-03-10 02:12:08 +0100214
215
Gilles Peskine897dff92021-03-10 15:03:44 +0100216class StorageKey(psa_storage.Key):
217 """Representation of a key for storage format testing."""
218
219 def __init__(self, *, description: str, **kwargs) -> None:
220 super().__init__(**kwargs)
221 self.description = description #type: str
222
223class StorageFormat:
224 """Storage format stability test cases."""
225
226 def __init__(self, info: Information, version: int, forward: bool) -> None:
227 """Prepare to generate test cases for storage format stability.
228
229 * `info`: information about the API. See the `Information` class.
230 * `version`: the storage format version to generate test cases for.
231 * `forward`: if true, generate forward compatibility test cases which
232 save a key and check that its representation is as intended. Otherwise
233 generate backward compatibility test cases which inject a key
234 representation and check that it can be read and used.
235 """
236 self.constructors = info.constructors
237 self.version = version
238 self.forward = forward
239
240 def make_test_case(self, key: StorageKey) -> test_case.TestCase:
241 """Construct a storage format test case for the given key.
242
243 If ``forward`` is true, generate a forward compatibility test case:
244 create a key and validate that it has the expected representation.
245 Otherwise generate a backward compatibility test case: inject the
246 key representation into storage and validate that it can be read
247 correctly.
248 """
249 verb = 'save' if self.forward else 'read'
250 tc = test_case.TestCase()
251 tc.set_description('PSA storage {}: {}'.format(verb, key.description))
Gilles Peskinef8223ab2021-03-10 15:07:16 +0100252 dependencies = automatic_dependencies(
253 key.lifetime.string, key.type.string,
254 key.usage.string, key.alg.string, key.alg2.string,
255 )
256 dependencies = finish_family_dependencies(dependencies, key.bits)
257 tc.set_dependencies(dependencies)
Gilles Peskine897dff92021-03-10 15:03:44 +0100258 tc.set_function('key_storage_' + verb)
259 if self.forward:
260 extra_arguments = []
261 else:
262 # Some test keys have the RAW_DATA type and attributes that don't
263 # necessarily make sense. We do this to validate numerical
264 # encodings of the attributes.
265 # Raw data keys have no useful exercise anyway so there is no
266 # loss of test coverage.
267 exercise = key.type.string != 'PSA_KEY_TYPE_RAW_DATA'
268 extra_arguments = ['1' if exercise else '0']
269 tc.set_arguments([key.lifetime.string,
270 key.type.string, str(key.bits),
271 key.usage.string, key.alg.string, key.alg2.string,
272 '"' + key.material.hex() + '"',
273 '"' + key.hex() + '"',
274 *extra_arguments])
275 return tc
276
277 def key_for_usage_flags(
278 self,
279 usage_flags: List[str],
280 short: Optional[str] = None
281 ) -> StorageKey:
282 """Construct a test key for the given key usage."""
283 usage = ' | '.join(usage_flags) if usage_flags else '0'
284 if short is None:
285 short = re.sub(r'\bPSA_KEY_USAGE_', r'', usage)
286 description = 'usage: ' + short
287 key = StorageKey(version=self.version,
288 id=1, lifetime=0x00000001,
289 type='PSA_KEY_TYPE_RAW_DATA', bits=8,
290 usage=usage, alg=0, alg2=0,
291 material=b'K',
292 description=description)
293 return key
294
295 def all_keys_for_usage_flags(self) -> Iterator[StorageKey]:
296 """Generate test keys covering usage flags."""
297 known_flags = sorted(self.constructors.key_usage_flags)
298 yield self.key_for_usage_flags(['0'])
299 for usage_flag in known_flags:
300 yield self.key_for_usage_flags([usage_flag])
301 for flag1, flag2 in zip(known_flags,
302 known_flags[1:] + [known_flags[0]]):
303 yield self.key_for_usage_flags([flag1, flag2])
304 yield self.key_for_usage_flags(known_flags, short='all known')
305
Gilles Peskinef8223ab2021-03-10 15:07:16 +0100306 def keys_for_type(
307 self,
308 key_type: str,
309 params: Optional[Iterable[str]] = None
310 ) -> Iterator[StorageKey]:
311 """Generate test keys for the given key type.
312
313 For key types that depend on a parameter (e.g. elliptic curve family),
314 `param` is the parameter to pass to the constructor. Only a single
315 parameter is supported.
316 """
317 kt = crypto_knowledge.KeyType(key_type, params)
318 for bits in kt.sizes_to_test():
319 usage_flags = 'PSA_KEY_USAGE_EXPORT'
320 alg = 0
321 alg2 = 0
322 key_material = kt.key_material(bits)
323 short_expression = re.sub(r'\bPSA_(?:KEY_TYPE|ECC_FAMILY)_',
324 r'',
325 kt.expression)
326 description = 'type: {} {}-bit'.format(short_expression, bits)
327 key = StorageKey(version=self.version,
328 id=1, lifetime=0x00000001,
329 type=kt.expression, bits=bits,
330 usage=usage_flags, alg=alg, alg2=alg2,
331 material=key_material,
332 description=description)
333 yield key
334
335 def all_keys_for_types(self) -> Iterator[StorageKey]:
336 """Generate test keys covering key types and their representations."""
337 for key_type in sorted(self.constructors.key_types):
338 yield from self.keys_for_type(key_type)
339 for key_type in sorted(self.constructors.key_types_from_curve):
340 for curve in sorted(self.constructors.ecc_curves):
341 yield from self.keys_for_type(key_type, [curve])
342 ## Diffie-Hellman (FFDH) is not supported yet, either in
343 ## crypto_knowledge.py or in Mbed TLS.
344 # for key_type in sorted(self.constructors.key_types_from_group):
345 # for group in sorted(self.constructors.dh_groups):
346 # yield from self.keys_for_type(key_type, [group])
347
Gilles Peskined86bc522021-03-10 15:08:57 +0100348 def keys_for_algorithm(self, alg: str) -> Iterator[StorageKey]:
349 """Generate test keys for the specified algorithm."""
350 # For now, we don't have information on the compatibility of key
351 # types and algorithms. So we just test the encoding of algorithms,
352 # and not that operations can be performed with them.
353 descr = alg
354 usage = 'PSA_KEY_USAGE_EXPORT'
355 key1 = StorageKey(version=self.version,
356 id=1, lifetime=0x00000001,
357 type='PSA_KEY_TYPE_RAW_DATA', bits=8,
358 usage=usage, alg=alg, alg2=0,
359 material=b'K',
360 description='alg: ' + descr)
361 yield key1
362 key2 = StorageKey(version=self.version,
363 id=1, lifetime=0x00000001,
364 type='PSA_KEY_TYPE_RAW_DATA', bits=8,
365 usage=usage, alg=0, alg2=alg,
366 material=b'L',
367 description='alg2: ' + descr)
368 yield key2
369
370 def all_keys_for_algorithms(self) -> Iterator[StorageKey]:
371 """Generate test keys covering algorithm encodings."""
372 for alg in sorted(self.constructors.algorithms):
373 yield from self.keys_for_algorithm(alg)
374 # To do: algorithm constructors with parameters
375
Gilles Peskine897dff92021-03-10 15:03:44 +0100376 def all_test_cases(self) -> Iterator[test_case.TestCase]:
377 """Generate all storage format test cases."""
378 for key in self.all_keys_for_usage_flags():
379 yield self.make_test_case(key)
Gilles Peskinef8223ab2021-03-10 15:07:16 +0100380 for key in self.all_keys_for_types():
381 yield self.make_test_case(key)
Gilles Peskined86bc522021-03-10 15:08:57 +0100382 for key in self.all_keys_for_algorithms():
383 yield self.make_test_case(key)
Gilles Peskinef8223ab2021-03-10 15:07:16 +0100384 # To do: vary id, lifetime
Gilles Peskine897dff92021-03-10 15:03:44 +0100385
386
Gilles Peskineb94ea512021-03-10 02:12:08 +0100387class TestGenerator:
388 """Generate test data."""
389
390 def __init__(self, options) -> None:
391 self.test_suite_directory = self.get_option(options, 'directory',
392 'tests/suites')
393 self.info = Information()
394
395 @staticmethod
396 def get_option(options, name: str, default: T) -> T:
397 value = getattr(options, name, None)
398 return default if value is None else value
399
Gilles Peskine0298bda2021-03-10 02:34:37 +0100400 def filename_for(self, basename: str) -> str:
401 """The location of the data file with the specified base name."""
402 return os.path.join(self.test_suite_directory, basename + '.data')
403
Gilles Peskineb94ea512021-03-10 02:12:08 +0100404 def write_test_data_file(self, basename: str,
405 test_cases: Iterable[test_case.TestCase]) -> None:
406 """Write the test cases to a .data file.
407
408 The output file is ``basename + '.data'`` in the test suite directory.
409 """
Gilles Peskine0298bda2021-03-10 02:34:37 +0100410 filename = self.filename_for(basename)
Gilles Peskineb94ea512021-03-10 02:12:08 +0100411 test_case.write_data_file(filename, test_cases)
412
Gilles Peskine0298bda2021-03-10 02:34:37 +0100413 TARGETS = {
414 'test_suite_psa_crypto_not_supported.generated':
Gilles Peskine3d778392021-02-17 15:11:05 +0100415 lambda info: NotSupported(info).test_cases_for_not_supported(),
Gilles Peskine897dff92021-03-10 15:03:44 +0100416 'test_suite_psa_crypto_storage_format.current':
417 lambda info: StorageFormat(info, 0, True).all_test_cases(),
418 'test_suite_psa_crypto_storage_format.v0':
419 lambda info: StorageFormat(info, 0, False).all_test_cases(),
Gilles Peskine0298bda2021-03-10 02:34:37 +0100420 } #type: Dict[str, Callable[[Information], Iterable[test_case.TestCase]]]
421
422 def generate_target(self, name: str) -> None:
423 test_cases = self.TARGETS[name](self.info)
424 self.write_test_data_file(name, test_cases)
Gilles Peskine14e428f2021-01-26 22:19:21 +0100425
Gilles Peskine09940492021-01-26 22:16:30 +0100426def main(args):
427 """Command line entry point."""
428 parser = argparse.ArgumentParser(description=__doc__)
Gilles Peskine0298bda2021-03-10 02:34:37 +0100429 parser.add_argument('--list', action='store_true',
430 help='List available targets and exit')
431 parser.add_argument('targets', nargs='*', metavar='TARGET',
432 help='Target file to generate (default: all; "-": none)')
Gilles Peskine09940492021-01-26 22:16:30 +0100433 options = parser.parse_args(args)
Gilles Peskinec86f20a2021-04-22 00:20:47 +0200434 build_tree.chdir_to_root()
Gilles Peskine09940492021-01-26 22:16:30 +0100435 generator = TestGenerator(options)
Gilles Peskine0298bda2021-03-10 02:34:37 +0100436 if options.list:
437 for name in sorted(generator.TARGETS):
438 print(generator.filename_for(name))
439 return
440 if options.targets:
441 # Allow "-" as a special case so you can run
442 # ``generate_psa_tests.py - $targets`` and it works uniformly whether
443 # ``$targets`` is empty or not.
444 options.targets = [os.path.basename(re.sub(r'\.data\Z', r'', target))
445 for target in options.targets
446 if target != '-']
447 else:
448 options.targets = sorted(generator.TARGETS)
449 for target in options.targets:
450 generator.generate_target(target)
Gilles Peskine09940492021-01-26 22:16:30 +0100451
452if __name__ == '__main__':
453 main(sys.argv[1:])