blob: 6baf53e1074321cf257143c9f6aec9946213c617 [file] [log] [blame]
Gilles Peskine09940492021-01-26 22:16:30 +01001#!/usr/bin/env python3
2"""Generate test data for PSA cryptographic mechanisms.
3"""
4
5# Copyright The Mbed TLS Contributors
6# SPDX-License-Identifier: Apache-2.0
7#
8# Licensed under the Apache License, Version 2.0 (the "License"); you may
9# not use this file except in compliance with the License.
10# You may obtain a copy of the License at
11#
12# http://www.apache.org/licenses/LICENSE-2.0
13#
14# Unless required by applicable law or agreed to in writing, software
15# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17# See the License for the specific language governing permissions and
18# limitations under the License.
19
20import argparse
Gilles Peskine14e428f2021-01-26 22:19:21 +010021import os
22import re
Gilles Peskine09940492021-01-26 22:16:30 +010023import sys
Gilles Peskined169d602021-02-16 14:16:25 +010024from typing import FrozenSet, Iterable, List, Optional, TypeVar
Gilles Peskine09940492021-01-26 22:16:30 +010025
26import scripts_path # pylint: disable=unused-import
Gilles Peskine14e428f2021-01-26 22:19:21 +010027from mbedtls_dev import crypto_knowledge
Gilles Peskine09940492021-01-26 22:16:30 +010028from mbedtls_dev import macro_collector
Gilles Peskine14e428f2021-01-26 22:19:21 +010029from mbedtls_dev import test_case
Gilles Peskine09940492021-01-26 22:16:30 +010030
31T = TypeVar('T') #pylint: disable=invalid-name
32
Gilles Peskine14e428f2021-01-26 22:19:21 +010033
Gilles Peskine7f756872021-02-16 12:13:12 +010034def psa_want_symbol(name: str) -> str:
Gilles Peskineaf172842021-01-27 18:24:48 +010035 """Return the PSA_WANT_xxx symbol associated with a PSA crypto feature."""
36 if name.startswith('PSA_'):
37 return name[:4] + 'WANT_' + name[4:]
38 else:
39 raise ValueError('Unable to determine the PSA_WANT_ symbol for ' + name)
40
Gilles Peskine7f756872021-02-16 12:13:12 +010041def finish_family_dependency(dep: str, bits: int) -> str:
42 """Finish dep if it's a family dependency symbol prefix.
43
44 A family dependency symbol prefix is a PSA_WANT_ symbol that needs to be
45 qualified by the key size. If dep is such a symbol, finish it by adjusting
46 the prefix and appending the key size. Other symbols are left unchanged.
47 """
48 return re.sub(r'_FAMILY_(.*)', r'_\1_' + str(bits), dep)
49
50def finish_family_dependencies(dependencies: List[str], bits: int) -> List[str]:
51 """Finish any family dependency symbol prefixes.
52
53 Apply `finish_family_dependency` to each element of `dependencies`.
54 """
55 return [finish_family_dependency(dep, bits) for dep in dependencies]
Gilles Peskineaf172842021-01-27 18:24:48 +010056
Gilles Peskined169d602021-02-16 14:16:25 +010057# A temporary hack: at the time of writing, not all dependency symbols
58# are implemented yet. Skip test cases for which the dependency symbols are
59# not available. Once all dependency symbols are available, this hack must
60# be removed so that a bug in the dependency symbols proprely leads to a test
61# failure.
62def read_implemented_dependencies(filename: str) -> FrozenSet[str]:
63 return frozenset(symbol
64 for line in open(filename)
65 for symbol in re.findall(r'\bPSA_WANT_\w+\b', line))
66IMPLEMENTED_DEPENDENCIES = read_implemented_dependencies('include/psa/crypto_config.h')
67def hack_dependencies_not_implemented(dependencies: List[str]) -> None:
68 if not all(dep.lstrip('!') in IMPLEMENTED_DEPENDENCIES
69 for dep in dependencies):
70 dependencies.append('DEPENDENCY_NOT_IMPLEMENTED_YET')
71
Gilles Peskine14e428f2021-01-26 22:19:21 +010072
Gilles Peskineb94ea512021-03-10 02:12:08 +010073class Information:
74 """Gather information about PSA constructors."""
Gilles Peskine09940492021-01-26 22:16:30 +010075
Gilles Peskineb94ea512021-03-10 02:12:08 +010076 def __init__(self) -> None:
Gilles Peskine09940492021-01-26 22:16:30 +010077 self.constructors = self.read_psa_interface()
78
79 @staticmethod
Gilles Peskine09940492021-01-26 22:16:30 +010080 def remove_unwanted_macros(
81 constructors: macro_collector.PSAMacroCollector
82 ) -> None:
83 # Mbed TLS doesn't support DSA. Don't attempt to generate any related
84 # test case.
85 constructors.key_types.discard('PSA_KEY_TYPE_DSA_KEY_PAIR')
86 constructors.key_types.discard('PSA_KEY_TYPE_DSA_PUBLIC_KEY')
87 constructors.algorithms_from_hash.pop('PSA_ALG_DSA', None)
88 constructors.algorithms_from_hash.pop('PSA_ALG_DETERMINISTIC_DSA', None)
89
90 def read_psa_interface(self) -> macro_collector.PSAMacroCollector:
91 """Return the list of known key types, algorithms, etc."""
92 constructors = macro_collector.PSAMacroCollector()
93 header_file_names = ['include/psa/crypto_values.h',
94 'include/psa/crypto_extra.h']
95 for header_file_name in header_file_names:
96 with open(header_file_name, 'rb') as header_file:
97 constructors.read_file(header_file)
98 self.remove_unwanted_macros(constructors)
99 return constructors
100
Gilles Peskine14e428f2021-01-26 22:19:21 +0100101
Gilles Peskineb94ea512021-03-10 02:12:08 +0100102def test_case_for_key_type_not_supported(
103 verb: str, key_type: str, bits: int,
104 dependencies: List[str],
105 *args: str,
106 param_descr: str = ''
107) -> test_case.TestCase:
108 """Return one test case exercising a key creation method
109 for an unsupported key type or size.
110 """
111 hack_dependencies_not_implemented(dependencies)
112 tc = test_case.TestCase()
113 short_key_type = re.sub(r'PSA_(KEY_TYPE|ECC_FAMILY)_', r'', key_type)
114 adverb = 'not' if dependencies else 'never'
115 if param_descr:
116 adverb = param_descr + ' ' + adverb
117 tc.set_description('PSA {} {} {}-bit {} supported'
118 .format(verb, short_key_type, bits, adverb))
119 tc.set_dependencies(dependencies)
120 tc.set_function(verb + '_not_supported')
121 tc.set_arguments([key_type] + list(args))
122 return tc
123
124class NotSupported:
125 """Generate test cases for when something is not supported."""
126
127 def __init__(self, info: Information) -> None:
128 self.constructors = info.constructors
Gilles Peskine14e428f2021-01-26 22:19:21 +0100129
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100130 ALWAYS_SUPPORTED = frozenset([
131 'PSA_KEY_TYPE_DERIVE',
132 'PSA_KEY_TYPE_RAW_DATA',
133 ])
Gilles Peskine14e428f2021-01-26 22:19:21 +0100134 def test_cases_for_key_type_not_supported(
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100135 self,
Gilles Peskineaf172842021-01-27 18:24:48 +0100136 kt: crypto_knowledge.KeyType,
137 param: Optional[int] = None,
138 param_descr: str = '',
Gilles Peskine14e428f2021-01-26 22:19:21 +0100139 ) -> List[test_case.TestCase]:
Gilles Peskineaf172842021-01-27 18:24:48 +0100140 """Return test cases exercising key creation when the given type is unsupported.
141
142 If param is present and not None, emit test cases conditioned on this
143 parameter not being supported. If it is absent or None, emit test cases
144 conditioned on the base type not being supported.
145 """
Gilles Peskine60b29fe2021-02-16 14:06:50 +0100146 if kt.name in self.ALWAYS_SUPPORTED:
147 # Don't generate test cases for key types that are always supported.
148 # They would be skipped in all configurations, which is noise.
Gilles Peskine14e428f2021-01-26 22:19:21 +0100149 return []
Gilles Peskineaf172842021-01-27 18:24:48 +0100150 import_dependencies = [('!' if param is None else '') +
151 psa_want_symbol(kt.name)]
152 if kt.params is not None:
153 import_dependencies += [('!' if param == i else '') +
154 psa_want_symbol(sym)
155 for i, sym in enumerate(kt.params)]
Gilles Peskine14e428f2021-01-26 22:19:21 +0100156 if kt.name.endswith('_PUBLIC_KEY'):
157 generate_dependencies = []
158 else:
159 generate_dependencies = import_dependencies
160 test_cases = []
161 for bits in kt.sizes_to_test():
162 test_cases.append(test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100163 'import', kt.expression, bits,
164 finish_family_dependencies(import_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100165 test_case.hex_string(kt.key_material(bits)),
166 param_descr=param_descr,
Gilles Peskine14e428f2021-01-26 22:19:21 +0100167 ))
Gilles Peskineaf172842021-01-27 18:24:48 +0100168 if not generate_dependencies and param is not None:
169 # If generation is impossible for this key type, rather than
170 # supported or not depending on implementation capabilities,
171 # only generate the test case once.
172 continue
Gilles Peskine14e428f2021-01-26 22:19:21 +0100173 test_cases.append(test_case_for_key_type_not_supported(
Gilles Peskine7f756872021-02-16 12:13:12 +0100174 'generate', kt.expression, bits,
175 finish_family_dependencies(generate_dependencies, bits),
Gilles Peskineaf172842021-01-27 18:24:48 +0100176 str(bits),
177 param_descr=param_descr,
Gilles Peskine14e428f2021-01-26 22:19:21 +0100178 ))
179 # To be added: derive
180 return test_cases
181
Gilles Peskineb94ea512021-03-10 02:12:08 +0100182 def generate_not_supported(self) -> List[test_case.TestCase]:
Gilles Peskine14e428f2021-01-26 22:19:21 +0100183 """Generate test cases that exercise the creation of keys of unsupported types."""
184 test_cases = []
185 for key_type in sorted(self.constructors.key_types):
186 kt = crypto_knowledge.KeyType(key_type)
187 test_cases += self.test_cases_for_key_type_not_supported(kt)
Gilles Peskineaf172842021-01-27 18:24:48 +0100188 # To be added: parametrized key types FFDH
189 for curve_family in sorted(self.constructors.ecc_curves):
190 for constr in ('PSA_KEY_TYPE_ECC_KEY_PAIR',
191 'PSA_KEY_TYPE_ECC_PUBLIC_KEY'):
192 kt = crypto_knowledge.KeyType(constr, [curve_family])
193 test_cases += self.test_cases_for_key_type_not_supported(
194 kt, param_descr='type')
195 test_cases += self.test_cases_for_key_type_not_supported(
196 kt, 0, param_descr='curve')
Gilles Peskineb94ea512021-03-10 02:12:08 +0100197 return test_cases
198
199
200class TestGenerator:
201 """Generate test data."""
202
203 def __init__(self, options) -> None:
204 self.test_suite_directory = self.get_option(options, 'directory',
205 'tests/suites')
206 self.info = Information()
207
208 @staticmethod
209 def get_option(options, name: str, default: T) -> T:
210 value = getattr(options, name, None)
211 return default if value is None else value
212
213 def write_test_data_file(self, basename: str,
214 test_cases: Iterable[test_case.TestCase]) -> None:
215 """Write the test cases to a .data file.
216
217 The output file is ``basename + '.data'`` in the test suite directory.
218 """
219 filename = os.path.join(self.test_suite_directory, basename + '.data')
220 test_case.write_data_file(filename, test_cases)
221
222 def generate_all(self) -> None:
223 test_cases = NotSupported(self.info).generate_not_supported()
Gilles Peskine14e428f2021-01-26 22:19:21 +0100224 self.write_test_data_file(
225 'test_suite_psa_crypto_not_supported.generated',
226 test_cases)
227
Gilles Peskine09940492021-01-26 22:16:30 +0100228def main(args):
229 """Command line entry point."""
230 parser = argparse.ArgumentParser(description=__doc__)
231 options = parser.parse_args(args)
232 generator = TestGenerator(options)
Gilles Peskine14e428f2021-01-26 22:19:21 +0100233 generator.generate_all()
Gilles Peskine09940492021-01-26 22:16:30 +0100234
235if __name__ == '__main__':
236 main(sys.argv[1:])