macro_collector.py 22 KB

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