blob: 6ae393643af3717153aefeef65c75f2ff290f6c7 [file] [log] [blame]
Gilles Peskine24827022018-09-25 18:49:23 +02001#!/usr/bin/env python3
Gilles Peskinea3b93ff2019-06-03 11:23:56 +02002"""Test the program psa_constant_names.
Gilles Peskine24827022018-09-25 18:49:23 +02003Gather constant names from header files and test cases. Compile a C program
4to print out their numerical values, feed these numerical values to
5psa_constant_names, and check that the output is the original name.
6Return 0 if all test cases pass, 1 if the output was not always as expected,
Gilles Peskinea3b93ff2019-06-03 11:23:56 +02007or 1 (with a Python backtrace) if there was an operational error.
8"""
Gilles Peskine24827022018-09-25 18:49:23 +02009
10import argparse
11import itertools
12import os
13import platform
14import re
15import subprocess
16import sys
17import tempfile
18
Gilles Peskinea0a315c2018-10-19 11:27:10 +020019class ReadFileLineException(Exception):
20 def __init__(self, filename, line_number):
21 message = 'in {} at {}'.format(filename, line_number)
22 super(ReadFileLineException, self).__init__(message)
23 self.filename = filename
24 self.line_number = line_number
25
26class read_file_lines:
Gilles Peskine54f54452019-05-27 18:31:59 +020027 # Dear Pylint, conventionally, a context manager class name is lowercase.
28 # pylint: disable=invalid-name,too-few-public-methods
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020029 """Context manager to read a text file line by line.
30
31 ```
32 with read_file_lines(filename) as lines:
33 for line in lines:
34 process(line)
35 ```
36 is equivalent to
37 ```
38 with open(filename, 'r') as input_file:
39 for line in input_file:
40 process(line)
41 ```
42 except that if process(line) raises an exception, then the read_file_lines
43 snippet annotates the exception with the file name and line number.
44 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +020045 def __init__(self, filename):
46 self.filename = filename
47 self.line_number = 'entry'
Gilles Peskine54f54452019-05-27 18:31:59 +020048 self.generator = None
Gilles Peskinea0a315c2018-10-19 11:27:10 +020049 def __enter__(self):
50 self.generator = enumerate(open(self.filename, 'r'))
51 return self
52 def __iter__(self):
53 for line_number, content in self.generator:
54 self.line_number = line_number
55 yield content
56 self.line_number = 'exit'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020057 def __exit__(self, exc_type, exc_value, exc_traceback):
58 if exc_type is not None:
Gilles Peskinea0a315c2018-10-19 11:27:10 +020059 raise ReadFileLineException(self.filename, self.line_number) \
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020060 from exc_value
Gilles Peskinea0a315c2018-10-19 11:27:10 +020061
Gilles Peskine24827022018-09-25 18:49:23 +020062class Inputs:
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020063 """Accumulate information about macros to test.
64 This includes macro names as well as information about their arguments
65 when applicable.
66 """
67
Gilles Peskine24827022018-09-25 18:49:23 +020068 def __init__(self):
69 # Sets of names per type
70 self.statuses = set(['PSA_SUCCESS'])
71 self.algorithms = set(['0xffffffff'])
72 self.ecc_curves = set(['0xffff'])
Gilles Peskinedcaefae2019-05-16 12:55:35 +020073 self.dh_groups = set(['0xffff'])
Gilles Peskine24827022018-09-25 18:49:23 +020074 self.key_types = set(['0xffffffff'])
75 self.key_usage_flags = set(['0x80000000'])
Gilles Peskine434899f2018-10-19 11:30:26 +020076 # Hard-coded value for unknown algorithms
Darryl Green61b7f612019-02-04 16:00:21 +000077 self.hash_algorithms = set(['0x010000fe'])
Gilles Peskine434899f2018-10-19 11:30:26 +020078 self.mac_algorithms = set(['0x02ff00ff'])
Gilles Peskine882e57e2019-04-12 00:12:07 +020079 self.ka_algorithms = set(['0x30fc0000'])
80 self.kdf_algorithms = set(['0x200000ff'])
Gilles Peskine434899f2018-10-19 11:30:26 +020081 # For AEAD algorithms, the only variability is over the tag length,
82 # and this only applies to known algorithms, so don't test an
83 # unknown algorithm.
84 self.aead_algorithms = set()
Gilles Peskine24827022018-09-25 18:49:23 +020085 # Identifier prefixes
86 self.table_by_prefix = {
87 'ERROR': self.statuses,
88 'ALG': self.algorithms,
89 'CURVE': self.ecc_curves,
Gilles Peskinedcaefae2019-05-16 12:55:35 +020090 'GROUP': self.dh_groups,
Gilles Peskine24827022018-09-25 18:49:23 +020091 'KEY_TYPE': self.key_types,
92 'KEY_USAGE': self.key_usage_flags,
93 }
94 # macro name -> list of argument names
95 self.argspecs = {}
96 # argument name -> list of values
Gilles Peskine434899f2018-10-19 11:30:26 +020097 self.arguments_for = {
98 'mac_length': ['1', '63'],
99 'tag_length': ['1', '63'],
100 }
Gilles Peskine24827022018-09-25 18:49:23 +0200101
102 def gather_arguments(self):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200103 """Populate the list of values for macro arguments.
104 Call this after parsing all the inputs.
105 """
Gilles Peskine24827022018-09-25 18:49:23 +0200106 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200107 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
Gilles Peskine882e57e2019-04-12 00:12:07 +0200108 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
Gilles Peskine17542082019-01-04 19:46:31 +0100109 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200110 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
Gilles Peskine24827022018-09-25 18:49:23 +0200111 self.arguments_for['curve'] = sorted(self.ecc_curves)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200112 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine24827022018-09-25 18:49:23 +0200113
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200114 @staticmethod
115 def _format_arguments(name, arguments):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200116 """Format a macro call with arguments.."""
Gilles Peskine24827022018-09-25 18:49:23 +0200117 return name + '(' + ', '.join(arguments) + ')'
118
119 def distribute_arguments(self, name):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200120 """Generate macro calls with each tested argument set.
121 If name is a macro without arguments, just yield "name".
122 If name is a macro with arguments, yield a series of
123 "name(arg1,...,argN)" where each argument takes each possible
124 value at least once.
125 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200126 try:
127 if name not in self.argspecs:
128 yield name
129 return
130 argspec = self.argspecs[name]
131 if argspec == []:
132 yield name + '()'
133 return
134 argument_lists = [self.arguments_for[arg] for arg in argspec]
135 arguments = [values[0] for values in argument_lists]
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200136 yield self._format_arguments(name, arguments)
Gilles Peskine54f54452019-05-27 18:31:59 +0200137 # Dear Pylint, enumerate won't work here since we're modifying
138 # the array.
139 # pylint: disable=consider-using-enumerate
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200140 for i in range(len(arguments)):
141 for value in argument_lists[i][1:]:
142 arguments[i] = value
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200143 yield self._format_arguments(name, arguments)
Gilles Peskinef96ed662018-10-19 11:29:56 +0200144 arguments[i] = argument_lists[0][0]
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200145 except BaseException as e:
146 raise Exception('distribute_arguments({})'.format(name)) from e
Gilles Peskine24827022018-09-25 18:49:23 +0200147
Gilles Peskine5a994c12019-11-21 16:46:51 +0100148 def generate_expressions(self, names):
149 return itertools.chain(*map(self.distribute_arguments, names))
150
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200151 _argument_split_re = re.compile(r' *, *')
152 @classmethod
153 def _argument_split(cls, arguments):
154 return re.split(cls._argument_split_re, arguments)
155
Gilles Peskine24827022018-09-25 18:49:23 +0200156 # Regex for interesting header lines.
157 # Groups: 1=macro name, 2=type, 3=argument list (optional).
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200158 _header_line_re = \
Gilles Peskine24827022018-09-25 18:49:23 +0200159 re.compile(r'#define +' +
160 r'(PSA_((?:KEY_)?[A-Z]+)_\w+)' +
161 r'(?:\(([^\n()]*)\))?')
162 # Regex of macro names to exclude.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200163 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
Gilles Peskinec68ce962018-10-19 11:31:52 +0200164 # Additional excluded macros.
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200165 _excluded_names = set([
166 # Macros that provide an alternative way to build the same
167 # algorithm as another macro.
168 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH',
169 'PSA_ALG_FULL_LENGTH_MAC',
170 # Auxiliary macro whose name doesn't fit the usual patterns for
171 # auxiliary macros.
172 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH_CASE',
173 # PSA_ALG_ECDH and PSA_ALG_FFDH are excluded for now as the script
174 # currently doesn't support them.
175 'PSA_ALG_ECDH',
176 'PSA_ALG_FFDH',
177 # Deprecated aliases.
178 'PSA_ERROR_UNKNOWN_ERROR',
179 'PSA_ERROR_OCCUPIED_SLOT',
180 'PSA_ERROR_EMPTY_SLOT',
181 'PSA_ERROR_INSUFFICIENT_CAPACITY',
Gilles Peskine19835122019-05-17 12:06:55 +0200182 'PSA_ERROR_TAMPERING_DETECTED',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200183 ])
Gilles Peskine24827022018-09-25 18:49:23 +0200184 def parse_header_line(self, line):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200185 """Parse a C header line, looking for "#define PSA_xxx"."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200186 m = re.match(self._header_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200187 if not m:
188 return
189 name = m.group(1)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200190 if re.search(self._excluded_name_re, name) or \
191 name in self._excluded_names:
Gilles Peskine24827022018-09-25 18:49:23 +0200192 return
193 dest = self.table_by_prefix.get(m.group(2))
194 if dest is None:
195 return
196 dest.add(name)
197 if m.group(3):
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200198 self.argspecs[name] = self._argument_split(m.group(3))
Gilles Peskine24827022018-09-25 18:49:23 +0200199
200 def parse_header(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200201 """Parse a C header file, looking for "#define PSA_xxx"."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200202 with read_file_lines(filename) as lines:
203 for line in lines:
Gilles Peskine24827022018-09-25 18:49:23 +0200204 self.parse_header_line(line)
205
206 def add_test_case_line(self, function, argument):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200207 """Parse a test case data line, looking for algorithm metadata tests."""
Gilles Peskine24827022018-09-25 18:49:23 +0200208 if function.endswith('_algorithm'):
Darryl Greenb8fe0682019-02-06 13:21:31 +0000209 # As above, ECDH and FFDH algorithms are excluded for now.
210 # Support for them will be added in the future.
Darryl Greenec079502019-01-29 15:48:00 +0000211 if 'ECDH' in argument or 'FFDH' in argument:
212 return
Gilles Peskine24827022018-09-25 18:49:23 +0200213 self.algorithms.add(argument)
214 if function == 'hash_algorithm':
215 self.hash_algorithms.add(argument)
Gilles Peskine434899f2018-10-19 11:30:26 +0200216 elif function in ['mac_algorithm', 'hmac_algorithm']:
217 self.mac_algorithms.add(argument)
218 elif function == 'aead_algorithm':
219 self.aead_algorithms.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200220 elif function == 'key_type':
221 self.key_types.add(argument)
222 elif function == 'ecc_key_types':
223 self.ecc_curves.add(argument)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200224 elif function == 'dh_key_types':
225 self.dh_groups.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200226
227 # Regex matching a *.data line containing a test function call and
228 # its arguments. The actual definition is partly positional, but this
229 # regex is good enough in practice.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200230 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
Gilles Peskine24827022018-09-25 18:49:23 +0200231 def parse_test_cases(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200232 """Parse a test case file (*.data), looking for algorithm metadata tests."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200233 with read_file_lines(filename) as lines:
234 for line in lines:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200235 m = re.match(self._test_case_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200236 if m:
237 self.add_test_case_line(m.group(1), m.group(2))
238
239def gather_inputs(headers, test_suites):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200240 """Read the list of inputs to test psa_constant_names with."""
Gilles Peskine24827022018-09-25 18:49:23 +0200241 inputs = Inputs()
242 for header in headers:
243 inputs.parse_header(header)
244 for test_cases in test_suites:
245 inputs.parse_test_cases(test_cases)
246 inputs.gather_arguments()
247 return inputs
248
249def remove_file_if_exists(filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200250 """Remove the specified file, ignoring errors."""
Gilles Peskine24827022018-09-25 18:49:23 +0200251 if not filename:
252 return
253 try:
254 os.remove(filename)
Gilles Peskine54f54452019-05-27 18:31:59 +0200255 except OSError:
Gilles Peskine24827022018-09-25 18:49:23 +0200256 pass
257
Gilles Peskine5a994c12019-11-21 16:46:51 +0100258def run_c(options, type_word, expressions):
259 """Generate and run a program to print out numerical values for expressions."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200260 if type_word == 'status':
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100261 cast_to = 'long'
262 printf_format = '%ld'
263 else:
264 cast_to = 'unsigned long'
265 printf_format = '0x%08lx'
Gilles Peskine24827022018-09-25 18:49:23 +0200266 c_name = None
267 exe_name = None
268 try:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200269 c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(type_word),
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100270 suffix='.c',
Gilles Peskine24827022018-09-25 18:49:23 +0200271 dir='programs/psa')
272 exe_suffix = '.exe' if platform.system() == 'Windows' else ''
273 exe_name = c_name[:-2] + exe_suffix
274 remove_file_if_exists(exe_name)
275 c_file = os.fdopen(c_fd, 'w', encoding='ascii')
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100276 c_file.write('/* Generated by test_psa_constant_names.py for {} values */'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200277 .format(type_word))
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100278 c_file.write('''
Gilles Peskine24827022018-09-25 18:49:23 +0200279#include <stdio.h>
280#include <psa/crypto.h>
281int main(void)
282{
283''')
Gilles Peskine5a994c12019-11-21 16:46:51 +0100284 for expr in expressions:
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100285 c_file.write(' printf("{}\\n", ({}) {});\n'
Gilles Peskine5a994c12019-11-21 16:46:51 +0100286 .format(printf_format, cast_to, expr))
Gilles Peskine24827022018-09-25 18:49:23 +0200287 c_file.write(''' return 0;
288}
289''')
290 c_file.close()
291 cc = os.getenv('CC', 'cc')
292 subprocess.check_call([cc] +
293 ['-I' + dir for dir in options.include] +
294 ['-o', exe_name, c_name])
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200295 if options.keep_c:
296 sys.stderr.write('List of {} tests kept at {}\n'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200297 .format(type_word, c_name))
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200298 else:
299 os.remove(c_name)
Gilles Peskine24827022018-09-25 18:49:23 +0200300 output = subprocess.check_output([exe_name])
301 return output.decode('ascii').strip().split('\n')
302 finally:
303 remove_file_if_exists(exe_name)
304
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200305NORMALIZE_STRIP_RE = re.compile(r'\s+')
Gilles Peskine24827022018-09-25 18:49:23 +0200306def normalize(expr):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200307 """Normalize the C expression so as not to care about trivial differences.
308 Currently "trivial differences" means whitespace.
309 """
Gilles Peskine5a6dc892019-11-21 16:48:07 +0100310 return re.sub(NORMALIZE_STRIP_RE, '', expr)
Gilles Peskine24827022018-09-25 18:49:23 +0200311
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200312def do_test(options, inputs, type_word, names):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200313 """Test psa_constant_names for the specified type.
314 Run program on names.
315 Use inputs to figure out what arguments to pass to macros that
316 take arguments.
317 """
Gilles Peskine5a994c12019-11-21 16:46:51 +0100318 expressions = sorted(inputs.generate_expressions(names))
319 values = run_c(options, type_word, expressions)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200320 output = subprocess.check_output([options.program, type_word] + values)
Gilles Peskine24827022018-09-25 18:49:23 +0200321 outputs = output.decode('ascii').strip().split('\n')
Gilles Peskine5a994c12019-11-21 16:46:51 +0100322 errors = [(type_word, expr, value, output)
323 for (expr, value, output) in zip(expressions, values, outputs)
324 if normalize(expr) != normalize(output)]
325 return len(expressions), errors
Gilles Peskine24827022018-09-25 18:49:23 +0200326
327def report_errors(errors):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200328 """Describe each case where the output is not as expected."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200329 for type_word, name, value, output in errors:
Gilles Peskine24827022018-09-25 18:49:23 +0200330 print('For {} "{}", got "{}" (value: {})'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200331 .format(type_word, name, output, value))
Gilles Peskine24827022018-09-25 18:49:23 +0200332
333def run_tests(options, inputs):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200334 """Run psa_constant_names on all the gathered inputs.
335 Return a tuple (count, errors) where count is the total number of inputs
336 that were tested and errors is the list of cases where the output was
337 not as expected.
338 """
Gilles Peskine24827022018-09-25 18:49:23 +0200339 count = 0
340 errors = []
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200341 for type_word, names in [('status', inputs.statuses),
342 ('algorithm', inputs.algorithms),
343 ('ecc_curve', inputs.ecc_curves),
344 ('dh_group', inputs.dh_groups),
345 ('key_type', inputs.key_types),
346 ('key_usage', inputs.key_usage_flags)]:
347 c, e = do_test(options, inputs, type_word, names)
Gilles Peskine24827022018-09-25 18:49:23 +0200348 count += c
349 errors += e
350 return count, errors
351
Gilles Peskine69f93b52019-11-21 16:49:50 +0100352HEADERS = ['psa/crypto.h', 'psa/crypto_extra.h', 'psa/crypto_values.h']
353TEST_SUITES = ['tests/suites/test_suite_psa_crypto_metadata.data']
354
Gilles Peskine54f54452019-05-27 18:31:59 +0200355def main():
Gilles Peskine24827022018-09-25 18:49:23 +0200356 parser = argparse.ArgumentParser(description=globals()['__doc__'])
357 parser.add_argument('--include', '-I',
358 action='append', default=['include'],
359 help='Directory for header files')
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200360 parser.add_argument('--keep-c',
361 action='store_true', dest='keep_c', default=False,
362 help='Keep the intermediate C file')
363 parser.add_argument('--no-keep-c',
364 action='store_false', dest='keep_c',
365 help='Don\'t keep the intermediate C file (default)')
Gilles Peskine8f5a5012019-11-21 16:49:10 +0100366 parser.add_argument('--program',
367 default='programs/psa/psa_constant_names',
368 help='Program to test')
Gilles Peskine24827022018-09-25 18:49:23 +0200369 options = parser.parse_args()
Gilles Peskine69f93b52019-11-21 16:49:50 +0100370 headers = [os.path.join(options.include[0], h) for h in HEADERS]
371 inputs = gather_inputs(headers, TEST_SUITES)
Gilles Peskine24827022018-09-25 18:49:23 +0200372 count, errors = run_tests(options, inputs)
373 report_errors(errors)
374 if errors == []:
375 print('{} test cases PASS'.format(count))
376 else:
377 print('{} test cases, {} FAIL'.format(count, len(errors)))
378 exit(1)
Gilles Peskine54f54452019-05-27 18:31:59 +0200379
380if __name__ == '__main__':
381 main()