blob: f5f81c8a4574f12469d328adb57f7030eda6a4be [file] [log] [blame]
Gilles Peskinee7c44552021-01-25 21:40:45 +01001"""Collect macro definitions from header files.
2"""
3
4# Copyright The Mbed TLS Contributors
5# SPDX-License-Identifier: Apache-2.0
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18
Gilles Peskine22fcf1b2021-03-10 01:02:39 +010019import itertools
Gilles Peskinee7c44552021-01-25 21:40:45 +010020import re
Gilles Peskineb4edff92021-03-30 19:09:05 +020021from typing import Dict, Iterable, Iterator, List, Optional, Pattern, Set, Tuple, Union
22
23
24class ReadFileLineException(Exception):
25 def __init__(self, filename: str, line_number: Union[int, str]) -> None:
26 message = 'in {} at {}'.format(filename, line_number)
27 super(ReadFileLineException, self).__init__(message)
28 self.filename = filename
29 self.line_number = line_number
30
31
32class read_file_lines:
33 # Dear Pylint, conventionally, a context manager class name is lowercase.
34 # pylint: disable=invalid-name,too-few-public-methods
35 """Context manager to read a text file line by line.
36
37 ```
38 with read_file_lines(filename) as lines:
39 for line in lines:
40 process(line)
41 ```
42 is equivalent to
43 ```
44 with open(filename, 'r') as input_file:
45 for line in input_file:
46 process(line)
47 ```
48 except that if process(line) raises an exception, then the read_file_lines
49 snippet annotates the exception with the file name and line number.
50 """
51 def __init__(self, filename: str, binary: bool = False) -> None:
52 self.filename = filename
53 self.line_number = 'entry' #type: Union[int, str]
54 self.generator = None #type: Optional[Iterable[Tuple[int, str]]]
55 self.binary = binary
56 def __enter__(self) -> 'read_file_lines':
57 self.generator = enumerate(open(self.filename,
58 'rb' if self.binary else 'r'))
59 return self
60 def __iter__(self) -> Iterator[str]:
61 assert self.generator is not None
62 for line_number, content in self.generator:
63 self.line_number = line_number
64 yield content
65 self.line_number = 'exit'
66 def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
67 if exc_type is not None:
68 raise ReadFileLineException(self.filename, self.line_number) \
69 from exc_value
Gilles Peskine22fcf1b2021-03-10 01:02:39 +010070
71
72class PSAMacroEnumerator:
73 """Information about constructors of various PSA Crypto types.
74
75 This includes macro names as well as information about their arguments
76 when applicable.
77
78 This class only provides ways to enumerate expressions that evaluate to
79 values of the covered types. Derived classes are expected to populate
80 the set of known constructors of each kind, as well as populate
81 `self.arguments_for` for arguments that are not of a kind that is
82 enumerated here.
83 """
84
85 def __init__(self) -> None:
86 """Set up an empty set of known constructor macros.
87 """
88 self.statuses = set() #type: Set[str]
89 self.algorithms = set() #type: Set[str]
90 self.ecc_curves = set() #type: Set[str]
91 self.dh_groups = set() #type: Set[str]
92 self.key_types = set() #type: Set[str]
93 self.key_usage_flags = set() #type: Set[str]
94 self.hash_algorithms = set() #type: Set[str]
95 self.mac_algorithms = set() #type: Set[str]
96 self.ka_algorithms = set() #type: Set[str]
97 self.kdf_algorithms = set() #type: Set[str]
98 self.aead_algorithms = set() #type: Set[str]
99 # macro name -> list of argument names
100 self.argspecs = {} #type: Dict[str, List[str]]
101 # argument name -> list of values
102 self.arguments_for = {
103 'mac_length': [],
104 'min_mac_length': [],
105 'tag_length': [],
106 'min_tag_length': [],
107 } #type: Dict[str, List[str]]
108
109 def gather_arguments(self) -> None:
110 """Populate the list of values for macro arguments.
111
112 Call this after parsing all the inputs.
113 """
114 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
115 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
116 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
117 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
118 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
119 self.arguments_for['curve'] = sorted(self.ecc_curves)
120 self.arguments_for['group'] = sorted(self.dh_groups)
121
122 @staticmethod
123 def _format_arguments(name: str, arguments: Iterable[str]) -> str:
124 """Format a macro call with arguments.."""
125 return name + '(' + ', '.join(arguments) + ')'
126
127 _argument_split_re = re.compile(r' *, *')
128 @classmethod
129 def _argument_split(cls, arguments: str) -> List[str]:
130 return re.split(cls._argument_split_re, arguments)
131
132 def distribute_arguments(self, name: str) -> Iterator[str]:
133 """Generate macro calls with each tested argument set.
134
135 If name is a macro without arguments, just yield "name".
136 If name is a macro with arguments, yield a series of
137 "name(arg1,...,argN)" where each argument takes each possible
138 value at least once.
139 """
140 try:
141 if name not in self.argspecs:
142 yield name
143 return
144 argspec = self.argspecs[name]
145 if argspec == []:
146 yield name + '()'
147 return
148 argument_lists = [self.arguments_for[arg] for arg in argspec]
149 arguments = [values[0] for values in argument_lists]
150 yield self._format_arguments(name, arguments)
151 # Dear Pylint, enumerate won't work here since we're modifying
152 # the array.
153 # pylint: disable=consider-using-enumerate
154 for i in range(len(arguments)):
155 for value in argument_lists[i][1:]:
156 arguments[i] = value
157 yield self._format_arguments(name, arguments)
158 arguments[i] = argument_lists[0][0]
159 except BaseException as e:
160 raise Exception('distribute_arguments({})'.format(name)) from e
161
162 def generate_expressions(self, names: Iterable[str]) -> Iterator[str]:
163 """Generate expressions covering values constructed from the given names.
164
165 `names` can be any iterable collection of macro names.
166
167 For example:
168 * ``generate_expressions(['PSA_ALG_CMAC', 'PSA_ALG_HMAC'])``
169 generates ``'PSA_ALG_CMAC'`` as well as ``'PSA_ALG_HMAC(h)'`` for
170 every known hash algorithm ``h``.
171 * ``macros.generate_expressions(macros.key_types)`` generates all
172 key types.
173 """
174 return itertools.chain(*map(self.distribute_arguments, names))
175
Gilles Peskinee7c44552021-01-25 21:40:45 +0100176
Gilles Peskine33c601c2021-03-10 01:25:50 +0100177class PSAMacroCollector(PSAMacroEnumerator):
Gilles Peskinee7c44552021-01-25 21:40:45 +0100178 """Collect PSA crypto macro definitions from C header files.
179 """
180
Gilles Peskine10ab2672021-03-10 00:59:53 +0100181 def __init__(self, include_intermediate: bool = False) -> None:
Gilles Peskine13d60eb2021-01-25 22:42:14 +0100182 """Set up an object to collect PSA macro definitions.
183
184 Call the read_file method of the constructed object on each header file.
185
186 * include_intermediate: if true, include intermediate macros such as
187 PSA_XXX_BASE that do not designate semantic values.
188 """
Gilles Peskine33c601c2021-03-10 01:25:50 +0100189 super().__init__()
Gilles Peskine13d60eb2021-01-25 22:42:14 +0100190 self.include_intermediate = include_intermediate
Gilles Peskine10ab2672021-03-10 00:59:53 +0100191 self.key_types_from_curve = {} #type: Dict[str, str]
192 self.key_types_from_group = {} #type: Dict[str, str]
Gilles Peskine10ab2672021-03-10 00:59:53 +0100193 self.algorithms_from_hash = {} #type: Dict[str, str]
Gilles Peskinee7c44552021-01-25 21:40:45 +0100194
Gilles Peskine10ab2672021-03-10 00:59:53 +0100195 def is_internal_name(self, name: str) -> bool:
Gilles Peskinef8deb752021-01-25 22:41:45 +0100196 """Whether this is an internal macro. Internal macros will be skipped."""
Gilles Peskine13d60eb2021-01-25 22:42:14 +0100197 if not self.include_intermediate:
198 if name.endswith('_BASE') or name.endswith('_NONE'):
199 return True
200 if '_CATEGORY_' in name:
201 return True
Gilles Peskine0655b4f2021-01-25 22:44:36 +0100202 return name.endswith('_FLAG') or name.endswith('_MASK')
Gilles Peskinef8deb752021-01-25 22:41:45 +0100203
Gilles Peskine33c601c2021-03-10 01:25:50 +0100204 def record_algorithm_subtype(self, name: str, expansion: str) -> None:
205 """Record the subtype of an algorithm constructor.
206
207 Given a ``PSA_ALG_xxx`` macro name and its expansion, if the algorithm
208 is of a subtype that is tracked in its own set, add it to the relevant
209 set.
210 """
211 # This code is very ad hoc and fragile. It should be replaced by
212 # something more robust.
213 if re.match(r'MAC(?:_|\Z)', name):
214 self.mac_algorithms.add(name)
215 elif re.match(r'KDF(?:_|\Z)', name):
216 self.kdf_algorithms.add(name)
217 elif re.search(r'0x020000[0-9A-Fa-f]{2}', expansion):
218 self.hash_algorithms.add(name)
219 elif re.search(r'0x03[0-9A-Fa-f]{6}', expansion):
220 self.mac_algorithms.add(name)
221 elif re.search(r'0x05[0-9A-Fa-f]{6}', expansion):
222 self.aead_algorithms.add(name)
223 elif re.search(r'0x09[0-9A-Fa-f]{2}0000', expansion):
224 self.ka_algorithms.add(name)
225 elif re.search(r'0x08[0-9A-Fa-f]{6}', expansion):
226 self.kdf_algorithms.add(name)
227
Gilles Peskinee7c44552021-01-25 21:40:45 +0100228 # "#define" followed by a macro name with either no parameters
229 # or a single parameter and a non-empty expansion.
230 # Grab the macro name in group 1, the parameter name if any in group 2
231 # and the expansion in group 3.
232 _define_directive_re = re.compile(r'\s*#\s*define\s+(\w+)' +
233 r'(?:\s+|\((\w+)\)\s*)' +
234 r'(.+)')
235 _deprecated_definition_re = re.compile(r'\s*MBEDTLS_DEPRECATED')
236
237 def read_line(self, line):
238 """Parse a C header line and record the PSA identifier it defines if any.
239 This function analyzes lines that start with "#define PSA_"
240 (up to non-significant whitespace) and skips all non-matching lines.
241 """
242 # pylint: disable=too-many-branches
243 m = re.match(self._define_directive_re, line)
244 if not m:
245 return
246 name, parameter, expansion = m.groups()
247 expansion = re.sub(r'/\*.*?\*/|//.*', r' ', expansion)
Gilles Peskine33c601c2021-03-10 01:25:50 +0100248 if parameter:
249 self.argspecs[name] = [parameter]
Gilles Peskinee7c44552021-01-25 21:40:45 +0100250 if re.match(self._deprecated_definition_re, expansion):
251 # Skip deprecated values, which are assumed to be
252 # backward compatibility aliases that share
253 # numerical values with non-deprecated values.
254 return
Gilles Peskinef8deb752021-01-25 22:41:45 +0100255 if self.is_internal_name(name):
Gilles Peskinee7c44552021-01-25 21:40:45 +0100256 # Macro only to build actual values
257 return
258 elif (name.startswith('PSA_ERROR_') or name == 'PSA_SUCCESS') \
259 and not parameter:
260 self.statuses.add(name)
261 elif name.startswith('PSA_KEY_TYPE_') and not parameter:
262 self.key_types.add(name)
263 elif name.startswith('PSA_KEY_TYPE_') and parameter == 'curve':
264 self.key_types_from_curve[name] = name[:13] + 'IS_' + name[13:]
265 elif name.startswith('PSA_KEY_TYPE_') and parameter == 'group':
266 self.key_types_from_group[name] = name[:13] + 'IS_' + name[13:]
267 elif name.startswith('PSA_ECC_FAMILY_') and not parameter:
268 self.ecc_curves.add(name)
269 elif name.startswith('PSA_DH_FAMILY_') and not parameter:
270 self.dh_groups.add(name)
271 elif name.startswith('PSA_ALG_') and not parameter:
272 if name in ['PSA_ALG_ECDSA_BASE',
273 'PSA_ALG_RSA_PKCS1V15_SIGN_BASE']:
274 # Ad hoc skipping of duplicate names for some numerical values
275 return
276 self.algorithms.add(name)
Gilles Peskine33c601c2021-03-10 01:25:50 +0100277 self.record_algorithm_subtype(name, expansion)
Gilles Peskinee7c44552021-01-25 21:40:45 +0100278 elif name.startswith('PSA_ALG_') and parameter == 'hash_alg':
279 if name in ['PSA_ALG_DSA', 'PSA_ALG_ECDSA']:
280 # A naming irregularity
281 tester = name[:8] + 'IS_RANDOMIZED_' + name[8:]
282 else:
283 tester = name[:8] + 'IS_' + name[8:]
284 self.algorithms_from_hash[name] = tester
285 elif name.startswith('PSA_KEY_USAGE_') and not parameter:
Gilles Peskine33c601c2021-03-10 01:25:50 +0100286 self.key_usage_flags.add(name)
Gilles Peskinee7c44552021-01-25 21:40:45 +0100287 else:
288 # Other macro without parameter
289 return
290
291 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
292 _continued_line_re = re.compile(rb'\\\r?\n\Z')
293 def read_file(self, header_file):
294 for line in header_file:
295 m = re.search(self._continued_line_re, line)
296 while m:
297 cont = next(header_file)
298 line = line[:m.start(0)] + cont
299 m = re.search(self._continued_line_re, line)
300 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
301 self.read_line(line)
Gilles Peskineb4edff92021-03-30 19:09:05 +0200302
303
Gilles Peskine3d404b82021-03-30 21:46:35 +0200304class InputsForTest(PSAMacroCollector):
Gilles Peskineb4edff92021-03-30 19:09:05 +0200305 # pylint: disable=too-many-instance-attributes
306 """Accumulate information about macros to test.
307enumerate
308 This includes macro names as well as information about their arguments
309 when applicable.
310 """
311
312 def __init__(self) -> None:
313 super().__init__()
314 self.all_declared = set() #type: Set[str]
Gilles Peskineb4edff92021-03-30 19:09:05 +0200315 # Identifier prefixes
316 self.table_by_prefix = {
317 'ERROR': self.statuses,
318 'ALG': self.algorithms,
319 'ECC_CURVE': self.ecc_curves,
320 'DH_GROUP': self.dh_groups,
321 'KEY_TYPE': self.key_types,
322 'KEY_USAGE': self.key_usage_flags,
323 } #type: Dict[str, Set[str]]
324 # Test functions
325 self.table_by_test_function = {
326 # Any function ending in _algorithm also gets added to
327 # self.algorithms.
328 'key_type': [self.key_types],
329 'block_cipher_key_type': [self.key_types],
330 'stream_cipher_key_type': [self.key_types],
331 'ecc_key_family': [self.ecc_curves],
332 'ecc_key_types': [self.ecc_curves],
333 'dh_key_family': [self.dh_groups],
334 'dh_key_types': [self.dh_groups],
335 'hash_algorithm': [self.hash_algorithms],
336 'mac_algorithm': [self.mac_algorithms],
337 'cipher_algorithm': [],
338 'hmac_algorithm': [self.mac_algorithms],
339 'aead_algorithm': [self.aead_algorithms],
340 'key_derivation_algorithm': [self.kdf_algorithms],
341 'key_agreement_algorithm': [self.ka_algorithms],
342 'asymmetric_signature_algorithm': [],
343 'asymmetric_signature_wildcard': [self.algorithms],
344 'asymmetric_encryption_algorithm': [],
345 'other_algorithm': [],
346 } #type: Dict[str, List[Set[str]]]
347 self.arguments_for['mac_length'] += ['1', '63']
348 self.arguments_for['min_mac_length'] += ['1', '63']
349 self.arguments_for['tag_length'] += ['1', '63']
350 self.arguments_for['min_tag_length'] += ['1', '63']
351
Gilles Peskine3d404b82021-03-30 21:46:35 +0200352 def add_numerical_values(self) -> None:
353 """Add numerical values that are not supported to the known identifiers."""
354 # Sets of names per type
355 self.algorithms.add('0xffffffff')
356 self.ecc_curves.add('0xff')
357 self.dh_groups.add('0xff')
358 self.key_types.add('0xffff')
359 self.key_usage_flags.add('0x80000000')
360
361 # Hard-coded values for unknown algorithms
362 #
363 # These have to have values that are correct for their respective
364 # PSA_ALG_IS_xxx macros, but are also not currently assigned and are
365 # not likely to be assigned in the near future.
366 self.hash_algorithms.add('0x020000fe') # 0x020000ff is PSA_ALG_ANY_HASH
367 self.mac_algorithms.add('0x03007fff')
368 self.ka_algorithms.add('0x09fc0000')
369 self.kdf_algorithms.add('0x080000ff')
370 # For AEAD algorithms, the only variability is over the tag length,
371 # and this only applies to known algorithms, so don't test an
372 # unknown algorithm.
373
Gilles Peskineb4edff92021-03-30 19:09:05 +0200374 def get_names(self, type_word: str) -> Set[str]:
375 """Return the set of known names of values of the given type."""
376 return {
377 'status': self.statuses,
378 'algorithm': self.algorithms,
379 'ecc_curve': self.ecc_curves,
380 'dh_group': self.dh_groups,
381 'key_type': self.key_types,
382 'key_usage': self.key_usage_flags,
383 }[type_word]
384
385 # Regex for interesting header lines.
386 # Groups: 1=macro name, 2=type, 3=argument list (optional).
387 _header_line_re = \
388 re.compile(r'#define +' +
389 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
390 r'(?:\(([^\n()]*)\))?')
391 # Regex of macro names to exclude.
392 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
393 # Additional excluded macros.
394 _excluded_names = set([
395 # Macros that provide an alternative way to build the same
396 # algorithm as another macro.
397 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG',
398 'PSA_ALG_FULL_LENGTH_MAC',
399 # Auxiliary macro whose name doesn't fit the usual patterns for
400 # auxiliary macros.
401 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG_CASE',
402 ])
403 def parse_header_line(self, line: str) -> None:
404 """Parse a C header line, looking for "#define PSA_xxx"."""
405 m = re.match(self._header_line_re, line)
406 if not m:
407 return
408 name = m.group(1)
409 self.all_declared.add(name)
410 if re.search(self._excluded_name_re, name) or \
411 name in self._excluded_names:
412 return
413 dest = self.table_by_prefix.get(m.group(2))
414 if dest is None:
415 return
416 dest.add(name)
417 if m.group(3):
418 self.argspecs[name] = self._argument_split(m.group(3))
419
420 _nonascii_re = re.compile(rb'[^\x00-\x7f]+') #type: Pattern
421 def parse_header(self, filename: str) -> None:
422 """Parse a C header file, looking for "#define PSA_xxx"."""
423 with read_file_lines(filename, binary=True) as lines:
424 for line in lines:
425 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
426 self.parse_header_line(line)
427
428 _macro_identifier_re = re.compile(r'[A-Z]\w+')
429 def generate_undeclared_names(self, expr: str) -> Iterable[str]:
430 for name in re.findall(self._macro_identifier_re, expr):
431 if name not in self.all_declared:
432 yield name
433
434 def accept_test_case_line(self, function: str, argument: str) -> bool:
435 #pylint: disable=unused-argument
436 undeclared = list(self.generate_undeclared_names(argument))
437 if undeclared:
438 raise Exception('Undeclared names in test case', undeclared)
439 return True
440
441 def add_test_case_line(self, function: str, argument: str) -> None:
442 """Parse a test case data line, looking for algorithm metadata tests."""
443 sets = []
444 if function.endswith('_algorithm'):
445 sets.append(self.algorithms)
446 if function == 'key_agreement_algorithm' and \
447 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
448 # We only want *raw* key agreement algorithms as such, so
449 # exclude ones that are already chained with a KDF.
450 # Keep the expression as one to test as an algorithm.
451 function = 'other_algorithm'
452 sets += self.table_by_test_function[function]
453 if self.accept_test_case_line(function, argument):
454 for s in sets:
455 s.add(argument)
456
457 # Regex matching a *.data line containing a test function call and
458 # its arguments. The actual definition is partly positional, but this
459 # regex is good enough in practice.
460 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
461 def parse_test_cases(self, filename: str) -> None:
462 """Parse a test case file (*.data), looking for algorithm metadata tests."""
463 with read_file_lines(filename) as lines:
464 for line in lines:
465 m = re.match(self._test_case_line_re, line)
466 if m:
467 self.add_test_case_line(m.group(1), m.group(2))