macro_collector.py 22 KB

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