• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2"""Generate test data for PSA cryptographic mechanisms.
3
4With no arguments, generate all test data. With non-option arguments,
5generate only the specified files.
6"""
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
24import os
25import posixpath
26import re
27import sys
28from typing import Callable, Dict, FrozenSet, Iterable, Iterator, List, Optional, TypeVar
29
30import scripts_path # pylint: disable=unused-import
31from mbedtls_dev import build_tree
32from mbedtls_dev import crypto_knowledge
33from mbedtls_dev import macro_collector
34from mbedtls_dev import psa_storage
35from mbedtls_dev import test_case
36
37T = TypeVar('T') #pylint: disable=invalid-name
38
39
40def psa_want_symbol(name: str) -> str:
41    """Return the PSA_WANT_xxx symbol associated with a PSA crypto feature."""
42    if name.startswith('PSA_'):
43        return name[:4] + 'WANT_' + name[4:]
44    else:
45        raise ValueError('Unable to determine the PSA_WANT_ symbol for ' + name)
46
47def finish_family_dependency(dep: str, bits: int) -> str:
48    """Finish dep if it's a family dependency symbol prefix.
49
50    A family dependency symbol prefix is a PSA_WANT_ symbol that needs to be
51    qualified by the key size. If dep is such a symbol, finish it by adjusting
52    the prefix and appending the key size. Other symbols are left unchanged.
53    """
54    return re.sub(r'_FAMILY_(.*)', r'_\1_' + str(bits), dep)
55
56def finish_family_dependencies(dependencies: List[str], bits: int) -> List[str]:
57    """Finish any family dependency symbol prefixes.
58
59    Apply `finish_family_dependency` to each element of `dependencies`.
60    """
61    return [finish_family_dependency(dep, bits) for dep in dependencies]
62
63SYMBOLS_WITHOUT_DEPENDENCY = frozenset([
64    'PSA_ALG_AEAD_WITH_AT_LEAST_THIS_LENGTH_TAG', # modifier, only in policies
65    'PSA_ALG_AEAD_WITH_SHORTENED_TAG', # modifier
66    'PSA_ALG_ANY_HASH', # only in policies
67    'PSA_ALG_AT_LEAST_THIS_LENGTH_MAC', # modifier, only in policies
68    'PSA_ALG_KEY_AGREEMENT', # chaining
69    'PSA_ALG_TRUNCATED_MAC', # modifier
70])
71def automatic_dependencies(*expressions: str) -> List[str]:
72    """Infer dependencies of a test case by looking for PSA_xxx symbols.
73
74    The arguments are strings which should be C expressions. Do not use
75    string literals or comments as this function is not smart enough to
76    skip them.
77    """
78    used = set()
79    for expr in expressions:
80        used.update(re.findall(r'PSA_(?:ALG|ECC_FAMILY|KEY_TYPE)_\w+', expr))
81    used.difference_update(SYMBOLS_WITHOUT_DEPENDENCY)
82    return sorted(psa_want_symbol(name) for name in used)
83
84# A temporary hack: at the time of writing, not all dependency symbols
85# are implemented yet. Skip test cases for which the dependency symbols are
86# not available. Once all dependency symbols are available, this hack must
87# be removed so that a bug in the dependency symbols proprely leads to a test
88# failure.
89def read_implemented_dependencies(filename: str) -> FrozenSet[str]:
90    return frozenset(symbol
91                     for line in open(filename)
92                     for symbol in re.findall(r'\bPSA_WANT_\w+\b', line))
93_implemented_dependencies = None #type: Optional[FrozenSet[str]] #pylint: disable=invalid-name
94def hack_dependencies_not_implemented(dependencies: List[str]) -> None:
95    global _implemented_dependencies #pylint: disable=global-statement,invalid-name
96    if _implemented_dependencies is None:
97        _implemented_dependencies = \
98            read_implemented_dependencies('include/psa/crypto_config.h')
99    if not all((dep.lstrip('!') in _implemented_dependencies or 'PSA_WANT' not in dep)
100               for dep in dependencies):
101        dependencies.append('DEPENDENCY_NOT_IMPLEMENTED_YET')
102
103
104class Information:
105    """Gather information about PSA constructors."""
106
107    def __init__(self) -> None:
108        self.constructors = self.read_psa_interface()
109
110    @staticmethod
111    def remove_unwanted_macros(
112            constructors: macro_collector.PSAMacroEnumerator
113    ) -> None:
114        # Mbed TLS doesn't support finite-field DH yet and will not support
115        # finite-field DSA. Don't attempt to generate any related test case.
116        constructors.key_types.discard('PSA_KEY_TYPE_DH_KEY_PAIR')
117        constructors.key_types.discard('PSA_KEY_TYPE_DH_PUBLIC_KEY')
118        constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR')
119        constructors.key_types.discard('PSA_KEY_TYPE_DSA_PUBLIC_KEY')
120
121    def read_psa_interface(self) -> macro_collector.PSAMacroEnumerator:
122        """Return the list of known key types, algorithms, etc."""
123        constructors = macro_collector.InputsForTest()
124        header_file_names = ['include/psa/crypto_values.h',
125                             'include/psa/crypto_extra.h']
126        test_suites = ['tests/suites/test_suite_psa_crypto_metadata.data']
127        for header_file_name in header_file_names:
128            constructors.parse_header(header_file_name)
129        for test_cases in test_suites:
130            constructors.parse_test_cases(test_cases)
131        self.remove_unwanted_macros(constructors)
132        constructors.gather_arguments()
133        return constructors
134
135
136def test_case_for_key_type_not_supported(
137        verb: str, key_type: str, bits: int,
138        dependencies: List[str],
139        *args: str,
140        param_descr: str = ''
141) -> test_case.TestCase:
142    """Return one test case exercising a key creation method
143    for an unsupported key type or size.
144    """
145    hack_dependencies_not_implemented(dependencies)
146    tc = test_case.TestCase()
147    short_key_type = re.sub(r'PSA_(KEY_TYPE|ECC_FAMILY)_', r'', key_type)
148    adverb = 'not' if dependencies else 'never'
149    if param_descr:
150        adverb = param_descr + ' ' + adverb
151    tc.set_description('PSA {} {} {}-bit {} supported'
152                       .format(verb, short_key_type, bits, adverb))
153    tc.set_dependencies(dependencies)
154    tc.set_function(verb + '_not_supported')
155    tc.set_arguments([key_type] + list(args))
156    return tc
157
158class NotSupported:
159    """Generate test cases for when something is not supported."""
160
161    def __init__(self, info: Information) -> None:
162        self.constructors = info.constructors
163
164    ALWAYS_SUPPORTED = frozenset([
165        'PSA_KEY_TYPE_DERIVE',
166        'PSA_KEY_TYPE_RAW_DATA',
167    ])
168    def test_cases_for_key_type_not_supported(
169            self,
170            kt: crypto_knowledge.KeyType,
171            param: Optional[int] = None,
172            param_descr: str = '',
173    ) -> Iterator[test_case.TestCase]:
174        """Return test cases exercising key creation when the given type is unsupported.
175
176        If param is present and not None, emit test cases conditioned on this
177        parameter not being supported. If it is absent or None, emit test cases
178        conditioned on the base type not being supported.
179        """
180        if kt.name in self.ALWAYS_SUPPORTED:
181            # Don't generate test cases for key types that are always supported.
182            # They would be skipped in all configurations, which is noise.
183            return
184        import_dependencies = [('!' if param is None else '') +
185                               psa_want_symbol(kt.name)]
186        if kt.params is not None:
187            import_dependencies += [('!' if param == i else '') +
188                                    psa_want_symbol(sym)
189                                    for i, sym in enumerate(kt.params)]
190        if kt.name.endswith('_PUBLIC_KEY'):
191            generate_dependencies = []
192        else:
193            generate_dependencies = import_dependencies
194        for bits in kt.sizes_to_test():
195            yield test_case_for_key_type_not_supported(
196                'import', kt.expression, bits,
197                finish_family_dependencies(import_dependencies, bits),
198                test_case.hex_string(kt.key_material(bits)),
199                param_descr=param_descr,
200            )
201            if not generate_dependencies and param is not None:
202                # If generation is impossible for this key type, rather than
203                # supported or not depending on implementation capabilities,
204                # only generate the test case once.
205                continue
206                # For public key we expect that key generation fails with
207                # INVALID_ARGUMENT. It is handled by KeyGenerate class.
208            if not kt.name.endswith('_PUBLIC_KEY'):
209                yield test_case_for_key_type_not_supported(
210                    'generate', kt.expression, bits,
211                    finish_family_dependencies(generate_dependencies, bits),
212                    str(bits),
213                    param_descr=param_descr,
214                )
215            # To be added: derive
216
217    ECC_KEY_TYPES = ('PSA_KEY_TYPE_ECC_KEY_PAIR',
218                     'PSA_KEY_TYPE_ECC_PUBLIC_KEY')
219
220    def test_cases_for_not_supported(self) -> Iterator[test_case.TestCase]:
221        """Generate test cases that exercise the creation of keys of unsupported types."""
222        for key_type in sorted(self.constructors.key_types):
223            if key_type in self.ECC_KEY_TYPES:
224                continue
225            kt = crypto_knowledge.KeyType(key_type)
226            yield from self.test_cases_for_key_type_not_supported(kt)
227        for curve_family in sorted(self.constructors.ecc_curves):
228            for constr in self.ECC_KEY_TYPES:
229                kt = crypto_knowledge.KeyType(constr, [curve_family])
230                yield from self.test_cases_for_key_type_not_supported(
231                    kt, param_descr='type')
232                yield from self.test_cases_for_key_type_not_supported(
233                    kt, 0, param_descr='curve')
234
235def test_case_for_key_generation(
236        key_type: str, bits: int,
237        dependencies: List[str],
238        *args: str,
239        result: str = ''
240) -> test_case.TestCase:
241    """Return one test case exercising a key generation.
242    """
243    hack_dependencies_not_implemented(dependencies)
244    tc = test_case.TestCase()
245    short_key_type = re.sub(r'PSA_(KEY_TYPE|ECC_FAMILY)_', r'', key_type)
246    tc.set_description('PSA {} {}-bit'
247                       .format(short_key_type, bits))
248    tc.set_dependencies(dependencies)
249    tc.set_function('generate_key')
250    tc.set_arguments([key_type] + list(args) + [result])
251
252    return tc
253
254class KeyGenerate:
255    """Generate positive and negative (invalid argument) test cases for key generation."""
256
257    def __init__(self, info: Information) -> None:
258        self.constructors = info.constructors
259
260    ECC_KEY_TYPES = ('PSA_KEY_TYPE_ECC_KEY_PAIR',
261                     'PSA_KEY_TYPE_ECC_PUBLIC_KEY')
262
263    @staticmethod
264    def test_cases_for_key_type_key_generation(
265            kt: crypto_knowledge.KeyType
266    ) -> Iterator[test_case.TestCase]:
267        """Return test cases exercising key generation.
268
269        All key types can be generated except for public keys. For public key
270        PSA_ERROR_INVALID_ARGUMENT status is expected.
271        """
272        result = 'PSA_SUCCESS'
273
274        import_dependencies = [psa_want_symbol(kt.name)]
275        if kt.params is not None:
276            import_dependencies += [psa_want_symbol(sym)
277                                    for i, sym in enumerate(kt.params)]
278        if kt.name.endswith('_PUBLIC_KEY'):
279            # The library checks whether the key type is a public key generically,
280            # before it reaches a point where it needs support for the specific key
281            # type, so it returns INVALID_ARGUMENT for unsupported public key types.
282            generate_dependencies = []
283            result = 'PSA_ERROR_INVALID_ARGUMENT'
284        else:
285            generate_dependencies = import_dependencies
286            if kt.name == 'PSA_KEY_TYPE_RSA_KEY_PAIR':
287                generate_dependencies.append("MBEDTLS_GENPRIME")
288        for bits in kt.sizes_to_test():
289            yield test_case_for_key_generation(
290                kt.expression, bits,
291                finish_family_dependencies(generate_dependencies, bits),
292                str(bits),
293                result
294            )
295
296    def test_cases_for_key_generation(self) -> Iterator[test_case.TestCase]:
297        """Generate test cases that exercise the generation of keys."""
298        for key_type in sorted(self.constructors.key_types):
299            if key_type in self.ECC_KEY_TYPES:
300                continue
301            kt = crypto_knowledge.KeyType(key_type)
302            yield from self.test_cases_for_key_type_key_generation(kt)
303        for curve_family in sorted(self.constructors.ecc_curves):
304            for constr in self.ECC_KEY_TYPES:
305                kt = crypto_knowledge.KeyType(constr, [curve_family])
306                yield from self.test_cases_for_key_type_key_generation(kt)
307
308class StorageKey(psa_storage.Key):
309    """Representation of a key for storage format testing."""
310
311    IMPLICIT_USAGE_FLAGS = {
312        'PSA_KEY_USAGE_SIGN_HASH': 'PSA_KEY_USAGE_SIGN_MESSAGE',
313        'PSA_KEY_USAGE_VERIFY_HASH': 'PSA_KEY_USAGE_VERIFY_MESSAGE'
314    } #type: Dict[str, str]
315    """Mapping of usage flags to the flags that they imply."""
316
317    def __init__(
318            self,
319            usage: str,
320            without_implicit_usage: Optional[bool] = False,
321            **kwargs
322    ) -> None:
323        """Prepare to generate a key.
324
325        * `usage`                 : The usage flags used for the key.
326        * `without_implicit_usage`: Flag to defide to apply the usage extension
327        """
328        super().__init__(usage=usage, **kwargs)
329
330        if not without_implicit_usage:
331            for flag, implicit in self.IMPLICIT_USAGE_FLAGS.items():
332                if self.usage.value() & psa_storage.Expr(flag).value() and \
333                   self.usage.value() & psa_storage.Expr(implicit).value() == 0:
334                    self.usage = psa_storage.Expr(self.usage.string + ' | ' + implicit)
335
336class StorageTestData(StorageKey):
337    """Representation of test case data for storage format testing."""
338
339    def __init__(
340            self,
341            description: str,
342            expected_usage: Optional[str] = None,
343            **kwargs
344    ) -> None:
345        """Prepare to generate test data
346
347        * `description`   : used for the the test case names
348        * `expected_usage`: the usage flags generated as the expected usage flags
349                            in the test cases. CAn differ from the usage flags
350                            stored in the keys because of the usage flags extension.
351        """
352        super().__init__(**kwargs)
353        self.description = description #type: str
354        self.expected_usage = expected_usage if expected_usage else self.usage.string #type: str
355
356class StorageFormat:
357    """Storage format stability test cases."""
358
359    def __init__(self, info: Information, version: int, forward: bool) -> None:
360        """Prepare to generate test cases for storage format stability.
361
362        * `info`: information about the API. See the `Information` class.
363        * `version`: the storage format version to generate test cases for.
364        * `forward`: if true, generate forward compatibility test cases which
365          save a key and check that its representation is as intended. Otherwise
366          generate backward compatibility test cases which inject a key
367          representation and check that it can be read and used.
368        """
369        self.constructors = info.constructors #type: macro_collector.PSAMacroEnumerator
370        self.version = version #type: int
371        self.forward = forward #type: bool
372
373    def make_test_case(self, key: StorageTestData) -> test_case.TestCase:
374        """Construct a storage format test case for the given key.
375
376        If ``forward`` is true, generate a forward compatibility test case:
377        create a key and validate that it has the expected representation.
378        Otherwise generate a backward compatibility test case: inject the
379        key representation into storage and validate that it can be read
380        correctly.
381        """
382        verb = 'save' if self.forward else 'read'
383        tc = test_case.TestCase()
384        tc.set_description('PSA storage {}: {}'.format(verb, key.description))
385        dependencies = automatic_dependencies(
386            key.lifetime.string, key.type.string,
387            key.expected_usage, key.alg.string, key.alg2.string,
388        )
389        dependencies = finish_family_dependencies(dependencies, key.bits)
390        tc.set_dependencies(dependencies)
391        tc.set_function('key_storage_' + verb)
392        if self.forward:
393            extra_arguments = []
394        else:
395            flags = []
396            # Some test keys have the RAW_DATA type and attributes that don't
397            # necessarily make sense. We do this to validate numerical
398            # encodings of the attributes.
399            # Raw data keys have no useful exercise anyway so there is no
400            # loss of test coverage.
401            if key.type.string != 'PSA_KEY_TYPE_RAW_DATA':
402                flags.append('TEST_FLAG_EXERCISE')
403            if 'READ_ONLY' in key.lifetime.string:
404                flags.append('TEST_FLAG_READ_ONLY')
405            extra_arguments = [' | '.join(flags) if flags else '0']
406        tc.set_arguments([key.lifetime.string,
407                          key.type.string, str(key.bits),
408                          key.expected_usage, key.alg.string, key.alg2.string,
409                          '"' + key.material.hex() + '"',
410                          '"' + key.hex() + '"',
411                          *extra_arguments])
412        return tc
413
414    def key_for_lifetime(
415            self,
416            lifetime: str,
417    ) -> StorageTestData:
418        """Construct a test key for the given lifetime."""
419        short = lifetime
420        short = re.sub(r'PSA_KEY_LIFETIME_FROM_PERSISTENCE_AND_LOCATION',
421                       r'', short)
422        short = re.sub(r'PSA_KEY_[A-Z]+_', r'', short)
423        description = 'lifetime: ' + short
424        key = StorageTestData(version=self.version,
425                              id=1, lifetime=lifetime,
426                              type='PSA_KEY_TYPE_RAW_DATA', bits=8,
427                              usage='PSA_KEY_USAGE_EXPORT', alg=0, alg2=0,
428                              material=b'L',
429                              description=description)
430        return key
431
432    def all_keys_for_lifetimes(self) -> Iterator[StorageTestData]:
433        """Generate test keys covering lifetimes."""
434        lifetimes = sorted(self.constructors.lifetimes)
435        expressions = self.constructors.generate_expressions(lifetimes)
436        for lifetime in expressions:
437            # Don't attempt to create or load a volatile key in storage
438            if 'VOLATILE' in lifetime:
439                continue
440            # Don't attempt to create a read-only key in storage,
441            # but do attempt to load one.
442            if 'READ_ONLY' in lifetime and self.forward:
443                continue
444            yield self.key_for_lifetime(lifetime)
445
446    def keys_for_usage_flags(
447            self,
448            usage_flags: List[str],
449            short: Optional[str] = None,
450            test_implicit_usage: Optional[bool] = False
451    ) -> Iterator[StorageTestData]:
452        """Construct a test key for the given key usage."""
453        usage = ' | '.join(usage_flags) if usage_flags else '0'
454        if short is None:
455            short = re.sub(r'\bPSA_KEY_USAGE_', r'', usage)
456        extra_desc = ' with implication' if test_implicit_usage else ''
457        description = 'usage' + extra_desc + ': ' + short
458        key1 = StorageTestData(version=self.version,
459                               id=1, lifetime=0x00000001,
460                               type='PSA_KEY_TYPE_RAW_DATA', bits=8,
461                               expected_usage=usage,
462                               usage=usage, alg=0, alg2=0,
463                               material=b'K',
464                               description=description)
465        yield key1
466
467        if test_implicit_usage:
468            description = 'usage without implication' + ': ' + short
469            key2 = StorageTestData(version=self.version,
470                                   id=1, lifetime=0x00000001,
471                                   type='PSA_KEY_TYPE_RAW_DATA', bits=8,
472                                   without_implicit_usage=True,
473                                   usage=usage, alg=0, alg2=0,
474                                   material=b'K',
475                                   description=description)
476            yield key2
477
478    def generate_keys_for_usage_flags(self, **kwargs) -> Iterator[StorageTestData]:
479        """Generate test keys covering usage flags."""
480        known_flags = sorted(self.constructors.key_usage_flags)
481        yield from self.keys_for_usage_flags(['0'], **kwargs)
482        for usage_flag in known_flags:
483            yield from self.keys_for_usage_flags([usage_flag], **kwargs)
484        for flag1, flag2 in zip(known_flags,
485                                known_flags[1:] + [known_flags[0]]):
486            yield from self.keys_for_usage_flags([flag1, flag2], **kwargs)
487
488    def generate_key_for_all_usage_flags(self) -> Iterator[StorageTestData]:
489        known_flags = sorted(self.constructors.key_usage_flags)
490        yield from self.keys_for_usage_flags(known_flags, short='all known')
491
492    def all_keys_for_usage_flags(self) -> Iterator[StorageTestData]:
493        yield from self.generate_keys_for_usage_flags()
494        yield from self.generate_key_for_all_usage_flags()
495
496    def keys_for_type(
497            self,
498            key_type: str,
499            params: Optional[Iterable[str]] = None
500    ) -> Iterator[StorageTestData]:
501        """Generate test keys for the given key type.
502
503        For key types that depend on a parameter (e.g. elliptic curve family),
504        `param` is the parameter to pass to the constructor. Only a single
505        parameter is supported.
506        """
507        kt = crypto_knowledge.KeyType(key_type, params)
508        for bits in kt.sizes_to_test():
509            usage_flags = 'PSA_KEY_USAGE_EXPORT'
510            alg = 0
511            alg2 = 0
512            key_material = kt.key_material(bits)
513            short_expression = re.sub(r'\bPSA_(?:KEY_TYPE|ECC_FAMILY)_',
514                                      r'',
515                                      kt.expression)
516            description = 'type: {} {}-bit'.format(short_expression, bits)
517            key = StorageTestData(version=self.version,
518                                  id=1, lifetime=0x00000001,
519                                  type=kt.expression, bits=bits,
520                                  usage=usage_flags, alg=alg, alg2=alg2,
521                                  material=key_material,
522                                  description=description)
523            yield key
524
525    def all_keys_for_types(self) -> Iterator[StorageTestData]:
526        """Generate test keys covering key types and their representations."""
527        key_types = sorted(self.constructors.key_types)
528        for key_type in self.constructors.generate_expressions(key_types):
529            yield from self.keys_for_type(key_type)
530
531    def keys_for_algorithm(self, alg: str) -> Iterator[StorageTestData]:
532        """Generate test keys for the specified algorithm."""
533        # For now, we don't have information on the compatibility of key
534        # types and algorithms. So we just test the encoding of algorithms,
535        # and not that operations can be performed with them.
536        descr = re.sub(r'PSA_ALG_', r'', alg)
537        descr = re.sub(r',', r', ', re.sub(r' +', r'', descr))
538        usage = 'PSA_KEY_USAGE_EXPORT'
539        key1 = StorageTestData(version=self.version,
540                               id=1, lifetime=0x00000001,
541                               type='PSA_KEY_TYPE_RAW_DATA', bits=8,
542                               usage=usage, alg=alg, alg2=0,
543                               material=b'K',
544                               description='alg: ' + descr)
545        yield key1
546        key2 = StorageTestData(version=self.version,
547                               id=1, lifetime=0x00000001,
548                               type='PSA_KEY_TYPE_RAW_DATA', bits=8,
549                               usage=usage, alg=0, alg2=alg,
550                               material=b'L',
551                               description='alg2: ' + descr)
552        yield key2
553
554    def all_keys_for_algorithms(self) -> Iterator[StorageTestData]:
555        """Generate test keys covering algorithm encodings."""
556        algorithms = sorted(self.constructors.algorithms)
557        for alg in self.constructors.generate_expressions(algorithms):
558            yield from self.keys_for_algorithm(alg)
559
560    def generate_all_keys(self) -> Iterator[StorageTestData]:
561        """Generate all keys for the test cases."""
562        yield from self.all_keys_for_lifetimes()
563        yield from self.all_keys_for_usage_flags()
564        yield from self.all_keys_for_types()
565        yield from self.all_keys_for_algorithms()
566
567    def all_test_cases(self) -> Iterator[test_case.TestCase]:
568        """Generate all storage format test cases."""
569        # First build a list of all keys, then construct all the corresponding
570        # test cases. This allows all required information to be obtained in
571        # one go, which is a significant performance gain as the information
572        # includes numerical values obtained by compiling a C program.
573        all_keys = list(self.generate_all_keys())
574        for key in all_keys:
575            if key.location_value() != 0:
576                # Skip keys with a non-default location, because they
577                # require a driver and we currently have no mechanism to
578                # determine whether a driver is available.
579                continue
580            yield self.make_test_case(key)
581
582class StorageFormatForward(StorageFormat):
583    """Storage format stability test cases for forward compatibility."""
584
585    def __init__(self, info: Information, version: int) -> None:
586        super().__init__(info, version, True)
587
588class StorageFormatV0(StorageFormat):
589    """Storage format stability test cases for version 0 compatibility."""
590
591    def __init__(self, info: Information) -> None:
592        super().__init__(info, 0, False)
593
594    def all_keys_for_usage_flags(self) -> Iterator[StorageTestData]:
595        """Generate test keys covering usage flags."""
596        yield from self.generate_keys_for_usage_flags(test_implicit_usage=True)
597        yield from self.generate_key_for_all_usage_flags()
598
599    def keys_for_implicit_usage(
600            self,
601            implyer_usage: str,
602            alg: str,
603            key_type: crypto_knowledge.KeyType
604    ) -> StorageTestData:
605        # pylint: disable=too-many-locals
606        """Generate test keys for the specified implicit usage flag,
607           algorithm and key type combination.
608        """
609        bits = key_type.sizes_to_test()[0]
610        implicit_usage = StorageKey.IMPLICIT_USAGE_FLAGS[implyer_usage]
611        usage_flags = 'PSA_KEY_USAGE_EXPORT'
612        material_usage_flags = usage_flags + ' | ' + implyer_usage
613        expected_usage_flags = material_usage_flags + ' | ' + implicit_usage
614        alg2 = 0
615        key_material = key_type.key_material(bits)
616        usage_expression = re.sub(r'PSA_KEY_USAGE_', r'', implyer_usage)
617        alg_expression = re.sub(r'PSA_ALG_', r'', alg)
618        alg_expression = re.sub(r',', r', ', re.sub(r' +', r'', alg_expression))
619        key_type_expression = re.sub(r'\bPSA_(?:KEY_TYPE|ECC_FAMILY)_',
620                                     r'',
621                                     key_type.expression)
622        description = 'implied by {}: {} {} {}-bit'.format(
623            usage_expression, alg_expression, key_type_expression, bits)
624        key = StorageTestData(version=self.version,
625                              id=1, lifetime=0x00000001,
626                              type=key_type.expression, bits=bits,
627                              usage=material_usage_flags,
628                              expected_usage=expected_usage_flags,
629                              without_implicit_usage=True,
630                              alg=alg, alg2=alg2,
631                              material=key_material,
632                              description=description)
633        return key
634
635    def gather_key_types_for_sign_alg(self) -> Dict[str, List[str]]:
636        # pylint: disable=too-many-locals
637        """Match possible key types for sign algorithms."""
638        # To create a valid combinaton both the algorithms and key types
639        # must be filtered. Pair them with keywords created from its names.
640        incompatible_alg_keyword = frozenset(['RAW', 'ANY', 'PURE'])
641        incompatible_key_type_keywords = frozenset(['MONTGOMERY'])
642        keyword_translation = {
643            'ECDSA': 'ECC',
644            'ED[0-9]*.*' : 'EDWARDS'
645        }
646        exclusive_keywords = {
647            'EDWARDS': 'ECC'
648        }
649        key_types = set(self.constructors.generate_expressions(self.constructors.key_types))
650        algorithms = set(self.constructors.generate_expressions(self.constructors.sign_algorithms))
651        alg_with_keys = {} #type: Dict[str, List[str]]
652        translation_table = str.maketrans('(', '_', ')')
653        for alg in algorithms:
654            # Generate keywords from the name of the algorithm
655            alg_keywords = set(alg.partition('(')[0].split(sep='_')[2:])
656            # Translate keywords for better matching with the key types
657            for keyword in alg_keywords.copy():
658                for pattern, replace in keyword_translation.items():
659                    if re.match(pattern, keyword):
660                        alg_keywords.remove(keyword)
661                        alg_keywords.add(replace)
662            # Filter out incompatible algortihms
663            if not alg_keywords.isdisjoint(incompatible_alg_keyword):
664                continue
665
666            for key_type in key_types:
667                # Generate keywords from the of the key type
668                key_type_keywords = set(key_type.translate(translation_table).split(sep='_')[3:])
669
670                # Remove ambigious keywords
671                for keyword1, keyword2 in exclusive_keywords.items():
672                    if keyword1 in key_type_keywords:
673                        key_type_keywords.remove(keyword2)
674
675                if key_type_keywords.isdisjoint(incompatible_key_type_keywords) and\
676                   not key_type_keywords.isdisjoint(alg_keywords):
677                    if alg in alg_with_keys:
678                        alg_with_keys[alg].append(key_type)
679                    else:
680                        alg_with_keys[alg] = [key_type]
681        return alg_with_keys
682
683    def all_keys_for_implicit_usage(self) -> Iterator[StorageTestData]:
684        """Generate test keys for usage flag extensions."""
685        # Generate a key type and algorithm pair for each extendable usage
686        # flag to generate a valid key for exercising. The key is generated
687        # without usage extension to check the extension compatiblity.
688        alg_with_keys = self.gather_key_types_for_sign_alg()
689
690        for usage in sorted(StorageKey.IMPLICIT_USAGE_FLAGS, key=str):
691            for alg in sorted(alg_with_keys):
692                for key_type in sorted(alg_with_keys[alg]):
693                    # The key types must be filtered to fit the specific usage flag.
694                    kt = crypto_knowledge.KeyType(key_type)
695                    if kt.is_valid_for_signature(usage):
696                        yield self.keys_for_implicit_usage(usage, alg, kt)
697
698    def generate_all_keys(self) -> Iterator[StorageTestData]:
699        yield from super().generate_all_keys()
700        yield from self.all_keys_for_implicit_usage()
701
702class TestGenerator:
703    """Generate test data."""
704
705    def __init__(self, options) -> None:
706        self.test_suite_directory = self.get_option(options, 'directory',
707                                                    'tests/suites')
708        self.info = Information()
709
710    @staticmethod
711    def get_option(options, name: str, default: T) -> T:
712        value = getattr(options, name, None)
713        return default if value is None else value
714
715    def filename_for(self, basename: str) -> str:
716        """The location of the data file with the specified base name."""
717        return posixpath.join(self.test_suite_directory, basename + '.data')
718
719    def write_test_data_file(self, basename: str,
720                             test_cases: Iterable[test_case.TestCase]) -> None:
721        """Write the test cases to a .data file.
722
723        The output file is ``basename + '.data'`` in the test suite directory.
724        """
725        filename = self.filename_for(basename)
726        test_case.write_data_file(filename, test_cases)
727
728    TARGETS = {
729        'test_suite_psa_crypto_generate_key.generated':
730        lambda info: KeyGenerate(info).test_cases_for_key_generation(),
731        'test_suite_psa_crypto_not_supported.generated':
732        lambda info: NotSupported(info).test_cases_for_not_supported(),
733        'test_suite_psa_crypto_storage_format.current':
734        lambda info: StorageFormatForward(info, 0).all_test_cases(),
735        'test_suite_psa_crypto_storage_format.v0':
736        lambda info: StorageFormatV0(info).all_test_cases(),
737    } #type: Dict[str, Callable[[Information], Iterable[test_case.TestCase]]]
738
739    def generate_target(self, name: str) -> None:
740        test_cases = self.TARGETS[name](self.info)
741        self.write_test_data_file(name, test_cases)
742
743def main(args):
744    """Command line entry point."""
745    parser = argparse.ArgumentParser(description=__doc__)
746    parser.add_argument('--list', action='store_true',
747                        help='List available targets and exit')
748    parser.add_argument('--list-for-cmake', action='store_true',
749                        help='Print \';\'-separated list of available targets and exit')
750    parser.add_argument('--directory', metavar='DIR',
751                        help='Output directory (default: tests/suites)')
752    parser.add_argument('targets', nargs='*', metavar='TARGET',
753                        help='Target file to generate (default: all; "-": none)')
754    options = parser.parse_args(args)
755    build_tree.chdir_to_root()
756    generator = TestGenerator(options)
757    if options.list:
758        for name in sorted(generator.TARGETS):
759            print(generator.filename_for(name))
760        return
761    # List in a cmake list format (i.e. ';'-separated)
762    if options.list_for_cmake:
763        print(';'.join(generator.filename_for(name)
764                       for name in sorted(generator.TARGETS)), end='')
765        return
766    if options.targets:
767        # Allow "-" as a special case so you can run
768        # ``generate_psa_tests.py - $targets`` and it works uniformly whether
769        # ``$targets`` is empty or not.
770        options.targets = [os.path.basename(re.sub(r'\.data\Z', r'', target))
771                           for target in options.targets
772                           if target != '-']
773    else:
774        options.targets = sorted(generator.TARGETS)
775    for target in options.targets:
776        generator.generate_target(target)
777
778if __name__ == '__main__':
779    main(sys.argv[1:])
780