blob: 0c9a9a5b2e9d7c9b9a5f5c05a3780554d02b4ba9 [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 Peskine3cf3a8e2021-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 Peskine3cf3a8e2021-03-30 19:09:05 +0200302
303
304class InputsForTest(PSAMacroEnumerator):
305 # 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]
315 # Sets of names per type
316 self.statuses.add('PSA_SUCCESS')
317 self.algorithms.add('0xffffffff')
318 self.ecc_curves.add('0xff')
319 self.dh_groups.add('0xff')
320 self.key_types.add('0xffff')
321 self.key_usage_flags.add('0x80000000')
322
323 # Hard-coded values for unknown algorithms
324 #
325 # These have to have values that are correct for their respective
326 # PSA_ALG_IS_xxx macros, but are also not currently assigned and are
327 # not likely to be assigned in the near future.
328 self.hash_algorithms.add('0x020000fe') # 0x020000ff is PSA_ALG_ANY_HASH
329 self.mac_algorithms.add('0x03007fff')
330 self.ka_algorithms.add('0x09fc0000')
331 self.kdf_algorithms.add('0x080000ff')
332 # For AEAD algorithms, the only variability is over the tag length,
333 # and this only applies to known algorithms, so don't test an
334 # unknown algorithm.
335
336 # Identifier prefixes
337 self.table_by_prefix = {
338 'ERROR': self.statuses,
339 'ALG': self.algorithms,
340 'ECC_CURVE': self.ecc_curves,
341 'DH_GROUP': self.dh_groups,
342 'KEY_TYPE': self.key_types,
343 'KEY_USAGE': self.key_usage_flags,
344 } #type: Dict[str, Set[str]]
345 # Test functions
346 self.table_by_test_function = {
347 # Any function ending in _algorithm also gets added to
348 # self.algorithms.
349 'key_type': [self.key_types],
350 'block_cipher_key_type': [self.key_types],
351 'stream_cipher_key_type': [self.key_types],
352 'ecc_key_family': [self.ecc_curves],
353 'ecc_key_types': [self.ecc_curves],
354 'dh_key_family': [self.dh_groups],
355 'dh_key_types': [self.dh_groups],
356 'hash_algorithm': [self.hash_algorithms],
357 'mac_algorithm': [self.mac_algorithms],
358 'cipher_algorithm': [],
359 'hmac_algorithm': [self.mac_algorithms],
360 'aead_algorithm': [self.aead_algorithms],
361 'key_derivation_algorithm': [self.kdf_algorithms],
362 'key_agreement_algorithm': [self.ka_algorithms],
363 'asymmetric_signature_algorithm': [],
364 'asymmetric_signature_wildcard': [self.algorithms],
365 'asymmetric_encryption_algorithm': [],
366 'other_algorithm': [],
367 } #type: Dict[str, List[Set[str]]]
368 self.arguments_for['mac_length'] += ['1', '63']
369 self.arguments_for['min_mac_length'] += ['1', '63']
370 self.arguments_for['tag_length'] += ['1', '63']
371 self.arguments_for['min_tag_length'] += ['1', '63']
372
373 def get_names(self, type_word: str) -> Set[str]:
374 """Return the set of known names of values of the given type."""
375 return {
376 'status': self.statuses,
377 'algorithm': self.algorithms,
378 'ecc_curve': self.ecc_curves,
379 'dh_group': self.dh_groups,
380 'key_type': self.key_types,
381 'key_usage': self.key_usage_flags,
382 }[type_word]
383
384 # Regex for interesting header lines.
385 # Groups: 1=macro name, 2=type, 3=argument list (optional).
386 _header_line_re = \
387 re.compile(r'#define +' +
388 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
389 r'(?:\(([^\n()]*)\))?')
390 # Regex of macro names to exclude.
391 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
392 # Additional excluded macros.
393 _excluded_names = set([
394 # Macros that provide an alternative way to build the same
395 # algorithm as another macro.
396 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG',
397 'PSA_ALG_FULL_LENGTH_MAC',
398 # Auxiliary macro whose name doesn't fit the usual patterns for
399 # auxiliary macros.
400 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG_CASE',
401 ])
402 def parse_header_line(self, line: str) -> None:
403 """Parse a C header line, looking for "#define PSA_xxx"."""
404 m = re.match(self._header_line_re, line)
405 if not m:
406 return
407 name = m.group(1)
408 self.all_declared.add(name)
409 if re.search(self._excluded_name_re, name) or \
410 name in self._excluded_names:
411 return
412 dest = self.table_by_prefix.get(m.group(2))
413 if dest is None:
414 return
415 dest.add(name)
416 if m.group(3):
417 self.argspecs[name] = self._argument_split(m.group(3))
418
419 _nonascii_re = re.compile(rb'[^\x00-\x7f]+') #type: Pattern
420 def parse_header(self, filename: str) -> None:
421 """Parse a C header file, looking for "#define PSA_xxx"."""
422 with read_file_lines(filename, binary=True) as lines:
423 for line in lines:
424 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
425 self.parse_header_line(line)
426
427 _macro_identifier_re = re.compile(r'[A-Z]\w+')
428 def generate_undeclared_names(self, expr: str) -> Iterable[str]:
429 for name in re.findall(self._macro_identifier_re, expr):
430 if name not in self.all_declared:
431 yield name
432
433 def accept_test_case_line(self, function: str, argument: str) -> bool:
434 #pylint: disable=unused-argument
435 undeclared = list(self.generate_undeclared_names(argument))
436 if undeclared:
437 raise Exception('Undeclared names in test case', undeclared)
438 return True
439
440 def add_test_case_line(self, function: str, argument: str) -> None:
441 """Parse a test case data line, looking for algorithm metadata tests."""
442 sets = []
443 if function.endswith('_algorithm'):
444 sets.append(self.algorithms)
445 if function == 'key_agreement_algorithm' and \
446 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
447 # We only want *raw* key agreement algorithms as such, so
448 # exclude ones that are already chained with a KDF.
449 # Keep the expression as one to test as an algorithm.
450 function = 'other_algorithm'
451 sets += self.table_by_test_function[function]
452 if self.accept_test_case_line(function, argument):
453 for s in sets:
454 s.add(argument)
455
456 # Regex matching a *.data line containing a test function call and
457 # its arguments. The actual definition is partly positional, but this
458 # regex is good enough in practice.
459 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
460 def parse_test_cases(self, filename: str) -> None:
461 """Parse a test case file (*.data), looking for algorithm metadata tests."""
462 with read_file_lines(filename) as lines:
463 for line in lines:
464 m = re.match(self._test_case_line_re, line)
465 if m:
466 self.add_test_case_line(m.group(1), m.group(2))