blob: e7655f7b529c39e0d67cdb8c577da722ec6c74bd [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 Peskinef8223ab2021-03-10 15:07:16 +010061def automatic_dependencies(*expressions: str) -> List[str]:
62 """Infer dependencies of a test case by looking for PSA_xxx symbols.
63
64 The arguments are strings which should be C expressions. Do not use
65 string literals or comments as this function is not smart enough to
66 skip them.
67 """
68 used = set()
69 for expr in expressions:
70 used.update(re.findall(r'PSA_(?:ALG|ECC_FAMILY|KEY_TYPE)_\w+', expr))
71 return sorted(psa_want_symbol(name) for name in used)
72
Gilles Peskined169d602021-02-16 14:16:25 +010073# A temporary hack: at the time of writing, not all dependency symbols
74# are implemented yet. Skip test cases for which the dependency symbols are
75# not available. Once all dependency symbols are available, this hack must
76# be removed so that a bug in the dependency symbols proprely leads to a test
77# failure.
78def read_implemented_dependencies(filename: str) -> FrozenSet[str]:
79 return frozenset(symbol
80 for line in open(filename)
81 for symbol in re.findall(r'\bPSA_WANT_\w+\b', line))
82IMPLEMENTED_DEPENDENCIES = read_implemented_dependencies('include/psa/crypto_config.h')
83def hack_dependencies_not_implemented(dependencies: List[str]) -> None:
84 if not all(dep.lstrip('!') in IMPLEMENTED_DEPENDENCIES
85 for dep in dependencies):
86 dependencies.append('DEPENDENCY_NOT_IMPLEMENTED_YET')
87
Gilles Peskine14e428f2021-01-26 22:19:21 +010088
Gilles Peskineb94ea512021-03-10 02:12:08 +010089class Information:
90 """Gather information about PSA constructors."""
Gilles Peskine09940492021-01-26 22:16:30 +010091
Gilles Peskineb94ea512021-03-10 02:12:08 +010092 def __init__(self) -> None:
Gilles Peskine09940492021-01-26 22:16:30 +010093 self.constructors = self.read_psa_interface()
94
95 @staticmethod
Gilles Peskine09940492021-01-26 22:16:30 +010096 def remove_unwanted_macros(
97 constructors: macro_collector.PSAMacroCollector
98 ) -> None:
99 # Mbed TLS doesn't support DSA. Don't attempt to generate any related
100 # test case.
101 constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR')
102 constructors.key_types.discard('PSA_KEY_TYPE_DSA_PUBLIC_KEY')
103 constructors.algorithms_from_hash.pop('PSA_ALG_DSA', None)
104 constructors.algorithms_from_hash.pop('PSA_ALG_DETERMINISTIC_DSA', None)
105
106 def read_psa_interface(self) -> macro_collector.PSAMacroCollector:
107 """Return the list of known key types, algorithms, etc."""
Gilles Peskine66c0b7b2021-03-30 21:46:35 +0200108 constructors = macro_collector.InputsForTest()
Gilles Peskine09940492021-01-26 22:16:30 +0100109 header_file_names = ['include/psa/crypto_values.h',
110 'include/psa/crypto_extra.h']
111 for header_file_name in header_file_names:
112 with open(header_file_name, 'rb') as header_file:
113 constructors.read_file(header_file)
114 self.remove_unwanted_macros(constructors)
Gilles Peskine66c0b7b2021-03-30 21:46:35 +0200115 constructors.gather_arguments()
Gilles Peskine09940492021-01-26 22:16:30 +0100116 return constructors
117
Gilles Peskine14e428f2021-01-26 22:19:21 +0100118
Gilles Peskineb94ea512021-03-10 02:12:08 +0100119def test_case_for_key_type_not_supported(
120 verb: str, key_type: str, bits: int,
121 dependencies: List[str],
122 *args: str,
123 param_descr: str = ''
124) -> test_case.TestCase:
125 """Return one test case exercising a key creation method
126 for an unsupported key type or size.
127 """
128 hack_dependencies_not_implemented(dependencies)
129 tc = test_case.TestCase()
130 short_key_type = re.sub(r'PSA_(KEY_TYPE|ECC_FAMILY)_', r'', key_type)
131 adverb = 'not' if dependencies else 'never'
132 if param_descr:
133 adverb = param_descr + ' ' + adverb
134 tc.set_description('PSA {} {} {}-bit {} supported'
135 .format(verb, short_key_type, bits, adverb))
136 tc.set_dependencies(dependencies)
137 tc.set_function(verb + '_not_supported')
138 tc.set_arguments([key_type] + list(args))
139 return tc
140
141class NotSupported:
142 """Generate test cases for when something is not supported."""
143
144 def __init__(self, info: Information) -> None:
145 self.constructors = info.constructors
Gilles Peskine14e428f2021-01-26 22:19:21 +0100146
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100147 ALWAYS_SUPPORTED = frozenset([
148 'PSA_KEY_TYPE_DERIVE',
149 'PSA_KEY_TYPE_RAW_DATA',
150 ])
Gilles Peskine14e428f2021-01-26 22:19:21 +0100151 def test_cases_for_key_type_not_supported(
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100152 self,
Gilles Peskineaf172842021-01-27 18:24:48 +0100153 kt: crypto_knowledge.KeyType,
154 param: Optional[int] = None,
155 param_descr: str = '',
Gilles Peskine3d778392021-02-17 15:11:05 +0100156 ) -> Iterator[test_case.TestCase]:
Gilles Peskineaf172842021-01-27 18:24:48 +0100157 """Return test cases exercising key creation when the given type is unsupported.
158
159 If param is present and not None, emit test cases conditioned on this
160 parameter not being supported. If it is absent or None, emit test cases
161 conditioned on the base type not being supported.
162 """
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100163 if kt.name in self.ALWAYS_SUPPORTED:
164 # Don't generate test cases for key types that are always supported.
165 # They would be skipped in all configurations, which is noise.
Gilles Peskine3d778392021-02-17 15:11:05 +0100166 return
Gilles Peskineaf172842021-01-27 18:24:48 +0100167 import_dependencies = [('!' if param is None else '') +
168 psa_want_symbol(kt.name)]
169 if kt.params is not None:
170 import_dependencies += [('!' if param == i else '') +
171 psa_want_symbol(sym)
172 for i, sym in enumerate(kt.params)]
Gilles Peskine14e428f2021-01-26 22:19:21 +0100173 if kt.name.endswith('_PUBLIC_KEY'):
174 generate_dependencies = []
175 else:
176 generate_dependencies = import_dependencies
Gilles Peskine14e428f2021-01-26 22:19:21 +0100177 for bits in kt.sizes_to_test():
Gilles Peskine3d778392021-02-17 15:11:05 +0100178 yield test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100179 'import', kt.expression, bits,
180 finish_family_dependencies(import_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100181 test_case.hex_string(kt.key_material(bits)),
182 param_descr=param_descr,
Gilles Peskine3d778392021-02-17 15:11:05 +0100183 )
Gilles Peskineaf172842021-01-27 18:24:48 +0100184 if not generate_dependencies and param is not None:
185 # If generation is impossible for this key type, rather than
186 # supported or not depending on implementation capabilities,
187 # only generate the test case once.
188 continue
Gilles Peskine3d778392021-02-17 15:11:05 +0100189 yield test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100190 'generate', kt.expression, bits,
191 finish_family_dependencies(generate_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100192 str(bits),
193 param_descr=param_descr,
Gilles Peskine3d778392021-02-17 15:11:05 +0100194 )
Gilles Peskine14e428f2021-01-26 22:19:21 +0100195 # To be added: derive
Gilles Peskine14e428f2021-01-26 22:19:21 +0100196
Gilles Peskine3d778392021-02-17 15:11:05 +0100197 def test_cases_for_not_supported(self) -> Iterator[test_case.TestCase]:
Gilles Peskine14e428f2021-01-26 22:19:21 +0100198 """Generate test cases that exercise the creation of keys of unsupported types."""
Gilles Peskine14e428f2021-01-26 22:19:21 +0100199 for key_type in sorted(self.constructors.key_types):
200 kt = crypto_knowledge.KeyType(key_type)
Gilles Peskine3d778392021-02-17 15:11:05 +0100201 yield from self.test_cases_for_key_type_not_supported(kt)
Gilles Peskineaf172842021-01-27 18:24:48 +0100202 for curve_family in sorted(self.constructors.ecc_curves):
203 for constr in ('PSA_KEY_TYPE_ECC_KEY_PAIR',
204 'PSA_KEY_TYPE_ECC_PUBLIC_KEY'):
205 kt = crypto_knowledge.KeyType(constr, [curve_family])
Gilles Peskine3d778392021-02-17 15:11:05 +0100206 yield from self.test_cases_for_key_type_not_supported(
Gilles Peskineaf172842021-01-27 18:24:48 +0100207 kt, param_descr='type')
Gilles Peskine3d778392021-02-17 15:11:05 +0100208 yield from self.test_cases_for_key_type_not_supported(
Gilles Peskineaf172842021-01-27 18:24:48 +0100209 kt, 0, param_descr='curve')
Gilles Peskineb94ea512021-03-10 02:12:08 +0100210
211
Gilles Peskine897dff92021-03-10 15:03:44 +0100212class StorageKey(psa_storage.Key):
213 """Representation of a key for storage format testing."""
214
215 def __init__(self, *, description: str, **kwargs) -> None:
216 super().__init__(**kwargs)
217 self.description = description #type: str
218
219class StorageFormat:
220 """Storage format stability test cases."""
221
222 def __init__(self, info: Information, version: int, forward: bool) -> None:
223 """Prepare to generate test cases for storage format stability.
224
225 * `info`: information about the API. See the `Information` class.
226 * `version`: the storage format version to generate test cases for.
227 * `forward`: if true, generate forward compatibility test cases which
228 save a key and check that its representation is as intended. Otherwise
229 generate backward compatibility test cases which inject a key
230 representation and check that it can be read and used.
231 """
232 self.constructors = info.constructors
233 self.version = version
234 self.forward = forward
235
236 def make_test_case(self, key: StorageKey) -> test_case.TestCase:
237 """Construct a storage format test case for the given key.
238
239 If ``forward`` is true, generate a forward compatibility test case:
240 create a key and validate that it has the expected representation.
241 Otherwise generate a backward compatibility test case: inject the
242 key representation into storage and validate that it can be read
243 correctly.
244 """
245 verb = 'save' if self.forward else 'read'
246 tc = test_case.TestCase()
247 tc.set_description('PSA storage {}: {}'.format(verb, key.description))
Gilles Peskinef8223ab2021-03-10 15:07:16 +0100248 dependencies = automatic_dependencies(
249 key.lifetime.string, key.type.string,
250 key.usage.string, key.alg.string, key.alg2.string,
251 )
252 dependencies = finish_family_dependencies(dependencies, key.bits)
253 tc.set_dependencies(dependencies)
Gilles Peskine897dff92021-03-10 15:03:44 +0100254 tc.set_function('key_storage_' + verb)
255 if self.forward:
256 extra_arguments = []
257 else:
258 # Some test keys have the RAW_DATA type and attributes that don't
259 # necessarily make sense. We do this to validate numerical
260 # encodings of the attributes.
261 # Raw data keys have no useful exercise anyway so there is no
262 # loss of test coverage.
263 exercise = key.type.string != 'PSA_KEY_TYPE_RAW_DATA'
264 extra_arguments = ['1' if exercise else '0']
265 tc.set_arguments([key.lifetime.string,
266 key.type.string, str(key.bits),
267 key.usage.string, key.alg.string, key.alg2.string,
268 '"' + key.material.hex() + '"',
269 '"' + key.hex() + '"',
270 *extra_arguments])
271 return tc
272
273 def key_for_usage_flags(
274 self,
275 usage_flags: List[str],
276 short: Optional[str] = None
277 ) -> StorageKey:
278 """Construct a test key for the given key usage."""
279 usage = ' | '.join(usage_flags) if usage_flags else '0'
280 if short is None:
281 short = re.sub(r'\bPSA_KEY_USAGE_', r'', usage)
282 description = 'usage: ' + short
283 key = StorageKey(version=self.version,
284 id=1, lifetime=0x00000001,
285 type='PSA_KEY_TYPE_RAW_DATA', bits=8,
286 usage=usage, alg=0, alg2=0,
287 material=b'K',
288 description=description)
289 return key
290
291 def all_keys_for_usage_flags(self) -> Iterator[StorageKey]:
292 """Generate test keys covering usage flags."""
293 known_flags = sorted(self.constructors.key_usage_flags)
294 yield self.key_for_usage_flags(['0'])
295 for usage_flag in known_flags:
296 yield self.key_for_usage_flags([usage_flag])
297 for flag1, flag2 in zip(known_flags,
298 known_flags[1:] + [known_flags[0]]):
299 yield self.key_for_usage_flags([flag1, flag2])
300 yield self.key_for_usage_flags(known_flags, short='all known')
301
Gilles Peskinef8223ab2021-03-10 15:07:16 +0100302 def keys_for_type(
303 self,
304 key_type: str,
305 params: Optional[Iterable[str]] = None
306 ) -> Iterator[StorageKey]:
307 """Generate test keys for the given key type.
308
309 For key types that depend on a parameter (e.g. elliptic curve family),
310 `param` is the parameter to pass to the constructor. Only a single
311 parameter is supported.
312 """
313 kt = crypto_knowledge.KeyType(key_type, params)
314 for bits in kt.sizes_to_test():
315 usage_flags = 'PSA_KEY_USAGE_EXPORT'
316 alg = 0
317 alg2 = 0
318 key_material = kt.key_material(bits)
319 short_expression = re.sub(r'\bPSA_(?:KEY_TYPE|ECC_FAMILY)_',
320 r'',
321 kt.expression)
322 description = 'type: {} {}-bit'.format(short_expression, bits)
323 key = StorageKey(version=self.version,
324 id=1, lifetime=0x00000001,
325 type=kt.expression, bits=bits,
326 usage=usage_flags, alg=alg, alg2=alg2,
327 material=key_material,
328 description=description)
329 yield key
330
331 def all_keys_for_types(self) -> Iterator[StorageKey]:
332 """Generate test keys covering key types and their representations."""
333 for key_type in sorted(self.constructors.key_types):
334 yield from self.keys_for_type(key_type)
335 for key_type in sorted(self.constructors.key_types_from_curve):
336 for curve in sorted(self.constructors.ecc_curves):
337 yield from self.keys_for_type(key_type, [curve])
338 ## Diffie-Hellman (FFDH) is not supported yet, either in
339 ## crypto_knowledge.py or in Mbed TLS.
340 # for key_type in sorted(self.constructors.key_types_from_group):
341 # for group in sorted(self.constructors.dh_groups):
342 # yield from self.keys_for_type(key_type, [group])
343
Gilles Peskined86bc522021-03-10 15:08:57 +0100344 def keys_for_algorithm(self, alg: str) -> Iterator[StorageKey]:
345 """Generate test keys for the specified algorithm."""
346 # For now, we don't have information on the compatibility of key
347 # types and algorithms. So we just test the encoding of algorithms,
348 # and not that operations can be performed with them.
349 descr = alg
350 usage = 'PSA_KEY_USAGE_EXPORT'
351 key1 = StorageKey(version=self.version,
352 id=1, lifetime=0x00000001,
353 type='PSA_KEY_TYPE_RAW_DATA', bits=8,
354 usage=usage, alg=alg, alg2=0,
355 material=b'K',
356 description='alg: ' + descr)
357 yield key1
358 key2 = StorageKey(version=self.version,
359 id=1, lifetime=0x00000001,
360 type='PSA_KEY_TYPE_RAW_DATA', bits=8,
361 usage=usage, alg=0, alg2=alg,
362 material=b'L',
363 description='alg2: ' + descr)
364 yield key2
365
366 def all_keys_for_algorithms(self) -> Iterator[StorageKey]:
367 """Generate test keys covering algorithm encodings."""
368 for alg in sorted(self.constructors.algorithms):
369 yield from self.keys_for_algorithm(alg)
370 # To do: algorithm constructors with parameters
371
Gilles Peskine897dff92021-03-10 15:03:44 +0100372 def all_test_cases(self) -> Iterator[test_case.TestCase]:
373 """Generate all storage format test cases."""
Gilles Peskinef07866a2021-04-12 14:43:05 +0200374 # First build a list of all keys, then construct all the corresponding
375 # test cases. This allows all required information to be obtained in
376 # one go, which is a significant performance gain as the information
377 # includes numerical values obtained by compiling a C program.
378 keys = [] #type: List[StorageKey]
379 keys += self.all_keys_for_usage_flags()
380 keys += self.all_keys_for_types()
381 keys += self.all_keys_for_algorithms()
382 for key in keys:
Gilles Peskined86bc522021-03-10 15:08:57 +0100383 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)
434 generator = TestGenerator(options)
Gilles Peskine0298bda2021-03-10 02:34:37 +0100435 if options.list:
436 for name in sorted(generator.TARGETS):
437 print(generator.filename_for(name))
438 return
439 if options.targets:
440 # Allow "-" as a special case so you can run
441 # ``generate_psa_tests.py - $targets`` and it works uniformly whether
442 # ``$targets`` is empty or not.
443 options.targets = [os.path.basename(re.sub(r'\.data\Z', r'', target))
444 for target in options.targets
445 if target != '-']
446 else:
447 options.targets = sorted(generator.TARGETS)
448 for target in options.targets:
449 generator.generate_target(target)
Gilles Peskine09940492021-01-26 22:16:30 +0100450
451if __name__ == '__main__':
452 main(sys.argv[1:])