blob: 669c75da91efce70428b94f43d11f58d030b1f4c [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."""
108 constructors = macro_collector.PSAMacroCollector()
109 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)
115 return constructors
116
Gilles Peskine14e428f2021-01-26 22:19:21 +0100117
Gilles Peskineb94ea512021-03-10 02:12:08 +0100118def test_case_for_key_type_not_supported(
119 verb: str, key_type: str, bits: int,
120 dependencies: List[str],
121 *args: str,
122 param_descr: str = ''
123) -> test_case.TestCase:
124 """Return one test case exercising a key creation method
125 for an unsupported key type or size.
126 """
127 hack_dependencies_not_implemented(dependencies)
128 tc = test_case.TestCase()
129 short_key_type = re.sub(r'PSA_(KEY_TYPE|ECC_FAMILY)_', r'', key_type)
130 adverb = 'not' if dependencies else 'never'
131 if param_descr:
132 adverb = param_descr + ' ' + adverb
133 tc.set_description('PSA {} {} {}-bit {} supported'
134 .format(verb, short_key_type, bits, adverb))
135 tc.set_dependencies(dependencies)
136 tc.set_function(verb + '_not_supported')
137 tc.set_arguments([key_type] + list(args))
138 return tc
139
140class NotSupported:
141 """Generate test cases for when something is not supported."""
142
143 def __init__(self, info: Information) -> None:
144 self.constructors = info.constructors
Gilles Peskine14e428f2021-01-26 22:19:21 +0100145
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100146 ALWAYS_SUPPORTED = frozenset([
147 'PSA_KEY_TYPE_DERIVE',
148 'PSA_KEY_TYPE_RAW_DATA',
149 ])
Gilles Peskine14e428f2021-01-26 22:19:21 +0100150 def test_cases_for_key_type_not_supported(
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100151 self,
Gilles Peskineaf172842021-01-27 18:24:48 +0100152 kt: crypto_knowledge.KeyType,
153 param: Optional[int] = None,
154 param_descr: str = '',
Gilles Peskine3d778392021-02-17 15:11:05 +0100155 ) -> Iterator[test_case.TestCase]:
Gilles Peskineaf172842021-01-27 18:24:48 +0100156 """Return test cases exercising key creation when the given type is unsupported.
157
158 If param is present and not None, emit test cases conditioned on this
159 parameter not being supported. If it is absent or None, emit test cases
160 conditioned on the base type not being supported.
161 """
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100162 if kt.name in self.ALWAYS_SUPPORTED:
163 # Don't generate test cases for key types that are always supported.
164 # They would be skipped in all configurations, which is noise.
Gilles Peskine3d778392021-02-17 15:11:05 +0100165 return
Gilles Peskineaf172842021-01-27 18:24:48 +0100166 import_dependencies = [('!' if param is None else '') +
167 psa_want_symbol(kt.name)]
168 if kt.params is not None:
169 import_dependencies += [('!' if param == i else '') +
170 psa_want_symbol(sym)
171 for i, sym in enumerate(kt.params)]
Gilles Peskine14e428f2021-01-26 22:19:21 +0100172 if kt.name.endswith('_PUBLIC_KEY'):
173 generate_dependencies = []
174 else:
175 generate_dependencies = import_dependencies
Gilles Peskine14e428f2021-01-26 22:19:21 +0100176 for bits in kt.sizes_to_test():
Gilles Peskine3d778392021-02-17 15:11:05 +0100177 yield test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100178 'import', kt.expression, bits,
179 finish_family_dependencies(import_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100180 test_case.hex_string(kt.key_material(bits)),
181 param_descr=param_descr,
Gilles Peskine3d778392021-02-17 15:11:05 +0100182 )
Gilles Peskineaf172842021-01-27 18:24:48 +0100183 if not generate_dependencies and param is not None:
184 # If generation is impossible for this key type, rather than
185 # supported or not depending on implementation capabilities,
186 # only generate the test case once.
187 continue
Gilles Peskine3d778392021-02-17 15:11:05 +0100188 yield test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100189 'generate', kt.expression, bits,
190 finish_family_dependencies(generate_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100191 str(bits),
192 param_descr=param_descr,
Gilles Peskine3d778392021-02-17 15:11:05 +0100193 )
Gilles Peskine14e428f2021-01-26 22:19:21 +0100194 # To be added: derive
Gilles Peskine14e428f2021-01-26 22:19:21 +0100195
Gilles Peskine3d778392021-02-17 15:11:05 +0100196 def test_cases_for_not_supported(self) -> Iterator[test_case.TestCase]:
Gilles Peskine14e428f2021-01-26 22:19:21 +0100197 """Generate test cases that exercise the creation of keys of unsupported types."""
Gilles Peskine14e428f2021-01-26 22:19:21 +0100198 for key_type in sorted(self.constructors.key_types):
199 kt = crypto_knowledge.KeyType(key_type)
Gilles Peskine3d778392021-02-17 15:11:05 +0100200 yield from self.test_cases_for_key_type_not_supported(kt)
Gilles Peskineaf172842021-01-27 18:24:48 +0100201 for curve_family in sorted(self.constructors.ecc_curves):
202 for constr in ('PSA_KEY_TYPE_ECC_KEY_PAIR',
203 'PSA_KEY_TYPE_ECC_PUBLIC_KEY'):
204 kt = crypto_knowledge.KeyType(constr, [curve_family])
Gilles Peskine3d778392021-02-17 15:11:05 +0100205 yield from self.test_cases_for_key_type_not_supported(
Gilles Peskineaf172842021-01-27 18:24:48 +0100206 kt, param_descr='type')
Gilles Peskine3d778392021-02-17 15:11:05 +0100207 yield from self.test_cases_for_key_type_not_supported(
Gilles Peskineaf172842021-01-27 18:24:48 +0100208 kt, 0, param_descr='curve')
Gilles Peskineb94ea512021-03-10 02:12:08 +0100209
210
Gilles Peskine897dff92021-03-10 15:03:44 +0100211class StorageKey(psa_storage.Key):
212 """Representation of a key for storage format testing."""
213
214 def __init__(self, *, description: str, **kwargs) -> None:
215 super().__init__(**kwargs)
216 self.description = description #type: str
217
218class StorageFormat:
219 """Storage format stability test cases."""
220
221 def __init__(self, info: Information, version: int, forward: bool) -> None:
222 """Prepare to generate test cases for storage format stability.
223
224 * `info`: information about the API. See the `Information` class.
225 * `version`: the storage format version to generate test cases for.
226 * `forward`: if true, generate forward compatibility test cases which
227 save a key and check that its representation is as intended. Otherwise
228 generate backward compatibility test cases which inject a key
229 representation and check that it can be read and used.
230 """
231 self.constructors = info.constructors
232 self.version = version
233 self.forward = forward
234
235 def make_test_case(self, key: StorageKey) -> test_case.TestCase:
236 """Construct a storage format test case for the given key.
237
238 If ``forward`` is true, generate a forward compatibility test case:
239 create a key and validate that it has the expected representation.
240 Otherwise generate a backward compatibility test case: inject the
241 key representation into storage and validate that it can be read
242 correctly.
243 """
244 verb = 'save' if self.forward else 'read'
245 tc = test_case.TestCase()
246 tc.set_description('PSA storage {}: {}'.format(verb, key.description))
Gilles Peskinef8223ab2021-03-10 15:07:16 +0100247 dependencies = automatic_dependencies(
248 key.lifetime.string, key.type.string,
249 key.usage.string, key.alg.string, key.alg2.string,
250 )
251 dependencies = finish_family_dependencies(dependencies, key.bits)
252 tc.set_dependencies(dependencies)
Gilles Peskine897dff92021-03-10 15:03:44 +0100253 tc.set_function('key_storage_' + verb)
254 if self.forward:
255 extra_arguments = []
256 else:
257 # Some test keys have the RAW_DATA type and attributes that don't
258 # necessarily make sense. We do this to validate numerical
259 # encodings of the attributes.
260 # Raw data keys have no useful exercise anyway so there is no
261 # loss of test coverage.
262 exercise = key.type.string != 'PSA_KEY_TYPE_RAW_DATA'
263 extra_arguments = ['1' if exercise else '0']
264 tc.set_arguments([key.lifetime.string,
265 key.type.string, str(key.bits),
266 key.usage.string, key.alg.string, key.alg2.string,
267 '"' + key.material.hex() + '"',
268 '"' + key.hex() + '"',
269 *extra_arguments])
270 return tc
271
272 def key_for_usage_flags(
273 self,
274 usage_flags: List[str],
275 short: Optional[str] = None
276 ) -> StorageKey:
277 """Construct a test key for the given key usage."""
278 usage = ' | '.join(usage_flags) if usage_flags else '0'
279 if short is None:
280 short = re.sub(r'\bPSA_KEY_USAGE_', r'', usage)
281 description = 'usage: ' + short
282 key = StorageKey(version=self.version,
283 id=1, lifetime=0x00000001,
284 type='PSA_KEY_TYPE_RAW_DATA', bits=8,
285 usage=usage, alg=0, alg2=0,
286 material=b'K',
287 description=description)
288 return key
289
290 def all_keys_for_usage_flags(self) -> Iterator[StorageKey]:
291 """Generate test keys covering usage flags."""
292 known_flags = sorted(self.constructors.key_usage_flags)
293 yield self.key_for_usage_flags(['0'])
294 for usage_flag in known_flags:
295 yield self.key_for_usage_flags([usage_flag])
296 for flag1, flag2 in zip(known_flags,
297 known_flags[1:] + [known_flags[0]]):
298 yield self.key_for_usage_flags([flag1, flag2])
299 yield self.key_for_usage_flags(known_flags, short='all known')
300
Gilles Peskinef8223ab2021-03-10 15:07:16 +0100301 def keys_for_type(
302 self,
303 key_type: str,
304 params: Optional[Iterable[str]] = None
305 ) -> Iterator[StorageKey]:
306 """Generate test keys for the given key type.
307
308 For key types that depend on a parameter (e.g. elliptic curve family),
309 `param` is the parameter to pass to the constructor. Only a single
310 parameter is supported.
311 """
312 kt = crypto_knowledge.KeyType(key_type, params)
313 for bits in kt.sizes_to_test():
314 usage_flags = 'PSA_KEY_USAGE_EXPORT'
315 alg = 0
316 alg2 = 0
317 key_material = kt.key_material(bits)
318 short_expression = re.sub(r'\bPSA_(?:KEY_TYPE|ECC_FAMILY)_',
319 r'',
320 kt.expression)
321 description = 'type: {} {}-bit'.format(short_expression, bits)
322 key = StorageKey(version=self.version,
323 id=1, lifetime=0x00000001,
324 type=kt.expression, bits=bits,
325 usage=usage_flags, alg=alg, alg2=alg2,
326 material=key_material,
327 description=description)
328 yield key
329
330 def all_keys_for_types(self) -> Iterator[StorageKey]:
331 """Generate test keys covering key types and their representations."""
332 for key_type in sorted(self.constructors.key_types):
333 yield from self.keys_for_type(key_type)
334 for key_type in sorted(self.constructors.key_types_from_curve):
335 for curve in sorted(self.constructors.ecc_curves):
336 yield from self.keys_for_type(key_type, [curve])
337 ## Diffie-Hellman (FFDH) is not supported yet, either in
338 ## crypto_knowledge.py or in Mbed TLS.
339 # for key_type in sorted(self.constructors.key_types_from_group):
340 # for group in sorted(self.constructors.dh_groups):
341 # yield from self.keys_for_type(key_type, [group])
342
Gilles Peskined86bc522021-03-10 15:08:57 +0100343 def keys_for_algorithm(self, alg: str) -> Iterator[StorageKey]:
344 """Generate test keys for the specified algorithm."""
345 # For now, we don't have information on the compatibility of key
346 # types and algorithms. So we just test the encoding of algorithms,
347 # and not that operations can be performed with them.
348 descr = alg
349 usage = 'PSA_KEY_USAGE_EXPORT'
350 key1 = StorageKey(version=self.version,
351 id=1, lifetime=0x00000001,
352 type='PSA_KEY_TYPE_RAW_DATA', bits=8,
353 usage=usage, alg=alg, alg2=0,
354 material=b'K',
355 description='alg: ' + descr)
356 yield key1
357 key2 = StorageKey(version=self.version,
358 id=1, lifetime=0x00000001,
359 type='PSA_KEY_TYPE_RAW_DATA', bits=8,
360 usage=usage, alg=0, alg2=alg,
361 material=b'L',
362 description='alg2: ' + descr)
363 yield key2
364
365 def all_keys_for_algorithms(self) -> Iterator[StorageKey]:
366 """Generate test keys covering algorithm encodings."""
367 for alg in sorted(self.constructors.algorithms):
368 yield from self.keys_for_algorithm(alg)
369 # To do: algorithm constructors with parameters
370
Gilles Peskine897dff92021-03-10 15:03:44 +0100371 def all_test_cases(self) -> Iterator[test_case.TestCase]:
372 """Generate all storage format test cases."""
373 for key in self.all_keys_for_usage_flags():
374 yield self.make_test_case(key)
Gilles Peskinef8223ab2021-03-10 15:07:16 +0100375 for key in self.all_keys_for_types():
376 yield self.make_test_case(key)
Gilles Peskined86bc522021-03-10 15:08:57 +0100377 for key in self.all_keys_for_algorithms():
378 yield self.make_test_case(key)
Gilles Peskinef8223ab2021-03-10 15:07:16 +0100379 # To do: vary id, lifetime
Gilles Peskine897dff92021-03-10 15:03:44 +0100380
381
Gilles Peskineb94ea512021-03-10 02:12:08 +0100382class TestGenerator:
383 """Generate test data."""
384
385 def __init__(self, options) -> None:
386 self.test_suite_directory = self.get_option(options, 'directory',
387 'tests/suites')
388 self.info = Information()
389
390 @staticmethod
391 def get_option(options, name: str, default: T) -> T:
392 value = getattr(options, name, None)
393 return default if value is None else value
394
Gilles Peskine0298bda2021-03-10 02:34:37 +0100395 def filename_for(self, basename: str) -> str:
396 """The location of the data file with the specified base name."""
397 return os.path.join(self.test_suite_directory, basename + '.data')
398
Gilles Peskineb94ea512021-03-10 02:12:08 +0100399 def write_test_data_file(self, basename: str,
400 test_cases: Iterable[test_case.TestCase]) -> None:
401 """Write the test cases to a .data file.
402
403 The output file is ``basename + '.data'`` in the test suite directory.
404 """
Gilles Peskine0298bda2021-03-10 02:34:37 +0100405 filename = self.filename_for(basename)
Gilles Peskineb94ea512021-03-10 02:12:08 +0100406 test_case.write_data_file(filename, test_cases)
407
Gilles Peskine0298bda2021-03-10 02:34:37 +0100408 TARGETS = {
409 'test_suite_psa_crypto_not_supported.generated':
Gilles Peskine3d778392021-02-17 15:11:05 +0100410 lambda info: NotSupported(info).test_cases_for_not_supported(),
Gilles Peskine897dff92021-03-10 15:03:44 +0100411 'test_suite_psa_crypto_storage_format.current':
412 lambda info: StorageFormat(info, 0, True).all_test_cases(),
413 'test_suite_psa_crypto_storage_format.v0':
414 lambda info: StorageFormat(info, 0, False).all_test_cases(),
Gilles Peskine0298bda2021-03-10 02:34:37 +0100415 } #type: Dict[str, Callable[[Information], Iterable[test_case.TestCase]]]
416
417 def generate_target(self, name: str) -> None:
418 test_cases = self.TARGETS[name](self.info)
419 self.write_test_data_file(name, test_cases)
Gilles Peskine14e428f2021-01-26 22:19:21 +0100420
Gilles Peskine09940492021-01-26 22:16:30 +0100421def main(args):
422 """Command line entry point."""
423 parser = argparse.ArgumentParser(description=__doc__)
Gilles Peskine0298bda2021-03-10 02:34:37 +0100424 parser.add_argument('--list', action='store_true',
425 help='List available targets and exit')
426 parser.add_argument('targets', nargs='*', metavar='TARGET',
427 help='Target file to generate (default: all; "-": none)')
Gilles Peskine09940492021-01-26 22:16:30 +0100428 options = parser.parse_args(args)
429 generator = TestGenerator(options)
Gilles Peskine0298bda2021-03-10 02:34:37 +0100430 if options.list:
431 for name in sorted(generator.TARGETS):
432 print(generator.filename_for(name))
433 return
434 if options.targets:
435 # Allow "-" as a special case so you can run
436 # ``generate_psa_tests.py - $targets`` and it works uniformly whether
437 # ``$targets`` is empty or not.
438 options.targets = [os.path.basename(re.sub(r'\.data\Z', r'', target))
439 for target in options.targets
440 if target != '-']
441 else:
442 options.targets = sorted(generator.TARGETS)
443 for target in options.targets:
444 generator.generate_target(target)
Gilles Peskine09940492021-01-26 22:16:30 +0100445
446if __name__ == '__main__':
447 main(sys.argv[1:])