blob: c02555e880285bbdfe15642844056108c268b47d [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
Gilles Peskinea5000f12019-11-21 17:51:11 +010011from collections import namedtuple
Gilles Peskine24827022018-09-25 18:49:23 +020012import itertools
13import os
14import platform
15import re
16import subprocess
17import sys
18import tempfile
19
Gilles Peskinea0a315c2018-10-19 11:27:10 +020020class ReadFileLineException(Exception):
21 def __init__(self, filename, line_number):
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
27class read_file_lines:
Gilles Peskine54f54452019-05-27 18:31:59 +020028 # Dear Pylint, conventionally, a context manager class name is lowercase.
29 # pylint: disable=invalid-name,too-few-public-methods
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020030 """Context manager to read a text file line by line.
31
32 ```
33 with read_file_lines(filename) as lines:
34 for line in lines:
35 process(line)
36 ```
37 is equivalent to
38 ```
39 with open(filename, 'r') as input_file:
40 for line in input_file:
41 process(line)
42 ```
43 except that if process(line) raises an exception, then the read_file_lines
44 snippet annotates the exception with the file name and line number.
45 """
Gilles Peskine49af2d32019-12-06 19:20:13 +010046 def __init__(self, filename, binary=False):
Gilles Peskinea0a315c2018-10-19 11:27:10 +020047 self.filename = filename
48 self.line_number = 'entry'
Gilles Peskine54f54452019-05-27 18:31:59 +020049 self.generator = None
Gilles Peskine49af2d32019-12-06 19:20:13 +010050 self.binary = binary
Gilles Peskinea0a315c2018-10-19 11:27:10 +020051 def __enter__(self):
Gilles Peskine49af2d32019-12-06 19:20:13 +010052 self.generator = enumerate(open(self.filename,
53 'rb' if self.binary else 'r'))
Gilles Peskinea0a315c2018-10-19 11:27:10 +020054 return self
55 def __iter__(self):
56 for line_number, content in self.generator:
57 self.line_number = line_number
58 yield content
59 self.line_number = 'exit'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020060 def __exit__(self, exc_type, exc_value, exc_traceback):
61 if exc_type is not None:
Gilles Peskinea0a315c2018-10-19 11:27:10 +020062 raise ReadFileLineException(self.filename, self.line_number) \
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020063 from exc_value
Gilles Peskinea0a315c2018-10-19 11:27:10 +020064
Gilles Peskine24827022018-09-25 18:49:23 +020065class Inputs:
Gilles Peskine8c8694c2019-11-21 19:22:45 +010066 # pylint: disable=too-many-instance-attributes
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020067 """Accumulate information about macros to test.
Gilles Peskine4408dfd2019-11-21 17:16:21 +010068
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020069 This includes macro names as well as information about their arguments
70 when applicable.
71 """
72
Gilles Peskine24827022018-09-25 18:49:23 +020073 def __init__(self):
Gilles Peskine2bcfc712019-11-21 19:49:26 +010074 self.all_declared = set()
Gilles Peskine24827022018-09-25 18:49:23 +020075 # Sets of names per type
76 self.statuses = set(['PSA_SUCCESS'])
77 self.algorithms = set(['0xffffffff'])
Gilles Peskinef65ed6f2019-12-04 17:18:41 +010078 self.ecc_curves = set(['0xff'])
79 self.dh_groups = set(['0xff'])
80 self.key_types = set(['0xffff'])
Gilles Peskine24827022018-09-25 18:49:23 +020081 self.key_usage_flags = set(['0x80000000'])
Gilles Peskine434899f2018-10-19 11:30:26 +020082 # Hard-coded value for unknown algorithms
Darryl Green61b7f612019-02-04 16:00:21 +000083 self.hash_algorithms = set(['0x010000fe'])
Gilles Peskine434899f2018-10-19 11:30:26 +020084 self.mac_algorithms = set(['0x02ff00ff'])
Gilles Peskine882e57e2019-04-12 00:12:07 +020085 self.ka_algorithms = set(['0x30fc0000'])
86 self.kdf_algorithms = set(['0x200000ff'])
Gilles Peskine434899f2018-10-19 11:30:26 +020087 # For AEAD algorithms, the only variability is over the tag length,
88 # and this only applies to known algorithms, so don't test an
89 # unknown algorithm.
90 self.aead_algorithms = set()
Gilles Peskine24827022018-09-25 18:49:23 +020091 # Identifier prefixes
92 self.table_by_prefix = {
93 'ERROR': self.statuses,
94 'ALG': self.algorithms,
Gilles Peskine98a710c2019-11-21 18:58:36 +010095 'ECC_CURVE': self.ecc_curves,
96 'DH_GROUP': self.dh_groups,
Gilles Peskine24827022018-09-25 18:49:23 +020097 'KEY_TYPE': self.key_types,
98 'KEY_USAGE': self.key_usage_flags,
99 }
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100100 # Test functions
101 self.table_by_test_function = {
Gilles Peskine8fa13482019-11-25 17:10:12 +0100102 # Any function ending in _algorithm also gets added to
103 # self.algorithms.
104 'key_type': [self.key_types],
Gilles Peskinef8210f22019-12-02 17:26:44 +0100105 'block_cipher_key_type': [self.key_types],
106 'stream_cipher_key_type': [self.key_types],
Gilles Peskine228abc52019-12-03 17:24:19 +0100107 'ecc_key_family': [self.ecc_curves],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100108 'ecc_key_types': [self.ecc_curves],
Gilles Peskine228abc52019-12-03 17:24:19 +0100109 'dh_key_family': [self.dh_groups],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100110 'dh_key_types': [self.dh_groups],
111 'hash_algorithm': [self.hash_algorithms],
112 'mac_algorithm': [self.mac_algorithms],
113 'cipher_algorithm': [],
114 'hmac_algorithm': [self.mac_algorithms],
115 'aead_algorithm': [self.aead_algorithms],
116 'key_derivation_algorithm': [self.kdf_algorithms],
117 'key_agreement_algorithm': [self.ka_algorithms],
118 'asymmetric_signature_algorithm': [],
119 'asymmetric_signature_wildcard': [self.algorithms],
120 'asymmetric_encryption_algorithm': [],
121 'other_algorithm': [],
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100122 }
Gilles Peskine24827022018-09-25 18:49:23 +0200123 # macro name -> list of argument names
124 self.argspecs = {}
125 # argument name -> list of values
Gilles Peskine434899f2018-10-19 11:30:26 +0200126 self.arguments_for = {
127 'mac_length': ['1', '63'],
128 'tag_length': ['1', '63'],
129 }
Gilles Peskine24827022018-09-25 18:49:23 +0200130
Gilles Peskineffe2d6e2019-11-21 17:17:01 +0100131 def get_names(self, type_word):
132 """Return the set of known names of values of the given type."""
133 return {
134 'status': self.statuses,
135 'algorithm': self.algorithms,
136 'ecc_curve': self.ecc_curves,
137 'dh_group': self.dh_groups,
138 'key_type': self.key_types,
139 'key_usage': self.key_usage_flags,
140 }[type_word]
141
Gilles Peskine24827022018-09-25 18:49:23 +0200142 def gather_arguments(self):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200143 """Populate the list of values for macro arguments.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100144
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200145 Call this after parsing all the inputs.
146 """
Gilles Peskine24827022018-09-25 18:49:23 +0200147 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200148 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
Gilles Peskine882e57e2019-04-12 00:12:07 +0200149 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
Gilles Peskine17542082019-01-04 19:46:31 +0100150 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200151 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
Gilles Peskine24827022018-09-25 18:49:23 +0200152 self.arguments_for['curve'] = sorted(self.ecc_curves)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200153 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine24827022018-09-25 18:49:23 +0200154
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200155 @staticmethod
156 def _format_arguments(name, arguments):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200157 """Format a macro call with arguments.."""
Gilles Peskine24827022018-09-25 18:49:23 +0200158 return name + '(' + ', '.join(arguments) + ')'
159
160 def distribute_arguments(self, name):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200161 """Generate macro calls with each tested argument set.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100162
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200163 If name is a macro without arguments, just yield "name".
164 If name is a macro with arguments, yield a series of
165 "name(arg1,...,argN)" where each argument takes each possible
166 value at least once.
167 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200168 try:
169 if name not in self.argspecs:
170 yield name
171 return
172 argspec = self.argspecs[name]
173 if argspec == []:
174 yield name + '()'
175 return
176 argument_lists = [self.arguments_for[arg] for arg in argspec]
177 arguments = [values[0] for values in argument_lists]
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200178 yield self._format_arguments(name, arguments)
Gilles Peskine54f54452019-05-27 18:31:59 +0200179 # Dear Pylint, enumerate won't work here since we're modifying
180 # the array.
181 # pylint: disable=consider-using-enumerate
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200182 for i in range(len(arguments)):
183 for value in argument_lists[i][1:]:
184 arguments[i] = value
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200185 yield self._format_arguments(name, arguments)
Gilles Peskinef96ed662018-10-19 11:29:56 +0200186 arguments[i] = argument_lists[0][0]
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200187 except BaseException as e:
188 raise Exception('distribute_arguments({})'.format(name)) from e
Gilles Peskine24827022018-09-25 18:49:23 +0200189
Gilles Peskine5a994c12019-11-21 16:46:51 +0100190 def generate_expressions(self, names):
191 return itertools.chain(*map(self.distribute_arguments, names))
192
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200193 _argument_split_re = re.compile(r' *, *')
194 @classmethod
195 def _argument_split(cls, arguments):
196 return re.split(cls._argument_split_re, arguments)
197
Gilles Peskine24827022018-09-25 18:49:23 +0200198 # Regex for interesting header lines.
199 # Groups: 1=macro name, 2=type, 3=argument list (optional).
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200200 _header_line_re = \
Gilles Peskine24827022018-09-25 18:49:23 +0200201 re.compile(r'#define +' +
Gilles Peskine98a710c2019-11-21 18:58:36 +0100202 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
Gilles Peskine24827022018-09-25 18:49:23 +0200203 r'(?:\(([^\n()]*)\))?')
204 # Regex of macro names to exclude.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200205 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
Gilles Peskinec68ce962018-10-19 11:31:52 +0200206 # Additional excluded macros.
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200207 _excluded_names = set([
208 # Macros that provide an alternative way to build the same
209 # algorithm as another macro.
210 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH',
211 'PSA_ALG_FULL_LENGTH_MAC',
212 # Auxiliary macro whose name doesn't fit the usual patterns for
213 # auxiliary macros.
214 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH_CASE',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200215 ])
Gilles Peskine24827022018-09-25 18:49:23 +0200216 def parse_header_line(self, line):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200217 """Parse a C header line, looking for "#define PSA_xxx"."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200218 m = re.match(self._header_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200219 if not m:
220 return
221 name = m.group(1)
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100222 self.all_declared.add(name)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200223 if re.search(self._excluded_name_re, name) or \
224 name in self._excluded_names:
Gilles Peskine24827022018-09-25 18:49:23 +0200225 return
226 dest = self.table_by_prefix.get(m.group(2))
227 if dest is None:
228 return
229 dest.add(name)
230 if m.group(3):
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200231 self.argspecs[name] = self._argument_split(m.group(3))
Gilles Peskine24827022018-09-25 18:49:23 +0200232
Gilles Peskine49af2d32019-12-06 19:20:13 +0100233 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
Gilles Peskine24827022018-09-25 18:49:23 +0200234 def parse_header(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200235 """Parse a C header file, looking for "#define PSA_xxx"."""
Gilles Peskine49af2d32019-12-06 19:20:13 +0100236 with read_file_lines(filename, binary=True) as lines:
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200237 for line in lines:
Gilles Peskine49af2d32019-12-06 19:20:13 +0100238 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
Gilles Peskine24827022018-09-25 18:49:23 +0200239 self.parse_header_line(line)
240
Gilles Peskine49af2d32019-12-06 19:20:13 +0100241 _macro_identifier_re = re.compile(r'[A-Z]\w+')
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100242 def generate_undeclared_names(self, expr):
243 for name in re.findall(self._macro_identifier_re, expr):
244 if name not in self.all_declared:
245 yield name
246
247 def accept_test_case_line(self, function, argument):
248 #pylint: disable=unused-argument
249 undeclared = list(self.generate_undeclared_names(argument))
250 if undeclared:
251 raise Exception('Undeclared names in test case', undeclared)
252 return True
253
Gilles Peskine24827022018-09-25 18:49:23 +0200254 def add_test_case_line(self, function, argument):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200255 """Parse a test case data line, looking for algorithm metadata tests."""
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100256 sets = []
Gilles Peskine24827022018-09-25 18:49:23 +0200257 if function.endswith('_algorithm'):
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100258 sets.append(self.algorithms)
Gilles Peskine79616682019-11-21 20:08:10 +0100259 if function == 'key_agreement_algorithm' and \
260 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
261 # We only want *raw* key agreement algorithms as such, so
262 # exclude ones that are already chained with a KDF.
263 # Keep the expression as one to test as an algorithm.
264 function = 'other_algorithm'
Gilles Peskine8fa13482019-11-25 17:10:12 +0100265 sets += self.table_by_test_function[function]
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100266 if self.accept_test_case_line(function, argument):
267 for s in sets:
268 s.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200269
270 # Regex matching a *.data line containing a test function call and
271 # its arguments. The actual definition is partly positional, but this
272 # regex is good enough in practice.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200273 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
Gilles Peskine24827022018-09-25 18:49:23 +0200274 def parse_test_cases(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200275 """Parse a test case file (*.data), looking for algorithm metadata tests."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200276 with read_file_lines(filename) as lines:
277 for line in lines:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200278 m = re.match(self._test_case_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200279 if m:
280 self.add_test_case_line(m.group(1), m.group(2))
281
Gilles Peskine84a45812019-11-21 19:50:33 +0100282def gather_inputs(headers, test_suites, inputs_class=Inputs):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200283 """Read the list of inputs to test psa_constant_names with."""
Gilles Peskine84a45812019-11-21 19:50:33 +0100284 inputs = inputs_class()
Gilles Peskine24827022018-09-25 18:49:23 +0200285 for header in headers:
286 inputs.parse_header(header)
287 for test_cases in test_suites:
288 inputs.parse_test_cases(test_cases)
289 inputs.gather_arguments()
290 return inputs
291
292def remove_file_if_exists(filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200293 """Remove the specified file, ignoring errors."""
Gilles Peskine24827022018-09-25 18:49:23 +0200294 if not filename:
295 return
296 try:
297 os.remove(filename)
Gilles Peskine54f54452019-05-27 18:31:59 +0200298 except OSError:
Gilles Peskine24827022018-09-25 18:49:23 +0200299 pass
300
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100301def run_c(type_word, expressions, include_path=None, keep_c=False):
Gilles Peskine5a994c12019-11-21 16:46:51 +0100302 """Generate and run a program to print out numerical values for expressions."""
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100303 if include_path is None:
304 include_path = []
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200305 if type_word == 'status':
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100306 cast_to = 'long'
307 printf_format = '%ld'
308 else:
309 cast_to = 'unsigned long'
310 printf_format = '0x%08lx'
Gilles Peskine24827022018-09-25 18:49:23 +0200311 c_name = None
312 exe_name = None
313 try:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200314 c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(type_word),
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100315 suffix='.c',
Gilles Peskine24827022018-09-25 18:49:23 +0200316 dir='programs/psa')
317 exe_suffix = '.exe' if platform.system() == 'Windows' else ''
318 exe_name = c_name[:-2] + exe_suffix
319 remove_file_if_exists(exe_name)
320 c_file = os.fdopen(c_fd, 'w', encoding='ascii')
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100321 c_file.write('/* Generated by test_psa_constant_names.py for {} values */'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200322 .format(type_word))
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100323 c_file.write('''
Gilles Peskine24827022018-09-25 18:49:23 +0200324#include <stdio.h>
325#include <psa/crypto.h>
326int main(void)
327{
328''')
Gilles Peskine5a994c12019-11-21 16:46:51 +0100329 for expr in expressions:
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100330 c_file.write(' printf("{}\\n", ({}) {});\n'
Gilles Peskine5a994c12019-11-21 16:46:51 +0100331 .format(printf_format, cast_to, expr))
Gilles Peskine24827022018-09-25 18:49:23 +0200332 c_file.write(''' return 0;
333}
334''')
335 c_file.close()
336 cc = os.getenv('CC', 'cc')
337 subprocess.check_call([cc] +
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100338 ['-I' + dir for dir in include_path] +
Gilles Peskine24827022018-09-25 18:49:23 +0200339 ['-o', exe_name, c_name])
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100340 if keep_c:
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200341 sys.stderr.write('List of {} tests kept at {}\n'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200342 .format(type_word, c_name))
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200343 else:
344 os.remove(c_name)
Gilles Peskine24827022018-09-25 18:49:23 +0200345 output = subprocess.check_output([exe_name])
346 return output.decode('ascii').strip().split('\n')
347 finally:
348 remove_file_if_exists(exe_name)
349
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200350NORMALIZE_STRIP_RE = re.compile(r'\s+')
Gilles Peskine24827022018-09-25 18:49:23 +0200351def normalize(expr):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200352 """Normalize the C expression so as not to care about trivial differences.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100353
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200354 Currently "trivial differences" means whitespace.
355 """
Gilles Peskine5a6dc892019-11-21 16:48:07 +0100356 return re.sub(NORMALIZE_STRIP_RE, '', expr)
Gilles Peskine24827022018-09-25 18:49:23 +0200357
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100358def collect_values(inputs, type_word, include_path=None, keep_c=False):
Gilles Peskinec2317112019-11-21 17:17:39 +0100359 """Generate expressions using known macro names and calculate their values.
360
361 Return a list of pairs of (expr, value) where expr is an expression and
362 value is a string representation of its integer value.
363 """
364 names = inputs.get_names(type_word)
365 expressions = sorted(inputs.generate_expressions(names))
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100366 values = run_c(type_word, expressions,
367 include_path=include_path, keep_c=keep_c)
Gilles Peskinec2317112019-11-21 17:17:39 +0100368 return expressions, values
369
Gilles Peskine24609332019-11-21 17:44:21 +0100370class Tests:
371 """An object representing tests and their results."""
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100372
Gilles Peskinea5000f12019-11-21 17:51:11 +0100373 Error = namedtuple('Error',
374 ['type', 'expression', 'value', 'output'])
375
Gilles Peskine24609332019-11-21 17:44:21 +0100376 def __init__(self, options):
377 self.options = options
378 self.count = 0
379 self.errors = []
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100380
Gilles Peskine24609332019-11-21 17:44:21 +0100381 def run_one(self, inputs, type_word):
382 """Test psa_constant_names for the specified type.
Gilles Peskine24827022018-09-25 18:49:23 +0200383
Gilles Peskine24609332019-11-21 17:44:21 +0100384 Run the program on the names for this type.
385 Use the inputs to figure out what arguments to pass to macros that
386 take arguments.
387 """
388 expressions, values = collect_values(inputs, type_word,
389 include_path=self.options.include,
390 keep_c=self.options.keep_c)
391 output = subprocess.check_output([self.options.program, type_word] +
392 values)
393 outputs = output.decode('ascii').strip().split('\n')
394 self.count += len(expressions)
395 for expr, value, output in zip(expressions, values, outputs):
Gilles Peskine32558482019-12-03 19:03:35 +0100396 if self.options.show:
397 sys.stdout.write('{} {}\t{}\n'.format(type_word, value, output))
Gilles Peskine24609332019-11-21 17:44:21 +0100398 if normalize(expr) != normalize(output):
Gilles Peskinea5000f12019-11-21 17:51:11 +0100399 self.errors.append(self.Error(type=type_word,
400 expression=expr,
401 value=value,
402 output=output))
Gilles Peskine24827022018-09-25 18:49:23 +0200403
Gilles Peskine24609332019-11-21 17:44:21 +0100404 def run_all(self, inputs):
405 """Run psa_constant_names on all the gathered inputs."""
406 for type_word in ['status', 'algorithm', 'ecc_curve', 'dh_group',
407 'key_type', 'key_usage']:
408 self.run_one(inputs, type_word)
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100409
Gilles Peskine24609332019-11-21 17:44:21 +0100410 def report(self, out):
411 """Describe each case where the output is not as expected.
412
413 Write the errors to ``out``.
414 Also write a total.
415 """
Gilles Peskinea5000f12019-11-21 17:51:11 +0100416 for error in self.errors:
Gilles Peskine24609332019-11-21 17:44:21 +0100417 out.write('For {} "{}", got "{}" (value: {})\n'
Gilles Peskinea5000f12019-11-21 17:51:11 +0100418 .format(error.type, error.expression,
419 error.output, error.value))
Gilles Peskine24609332019-11-21 17:44:21 +0100420 out.write('{} test cases'.format(self.count))
421 if self.errors:
422 out.write(', {} FAIL\n'.format(len(self.errors)))
423 else:
424 out.write(' PASS\n')
Gilles Peskine24827022018-09-25 18:49:23 +0200425
Gilles Peskine69f93b52019-11-21 16:49:50 +0100426HEADERS = ['psa/crypto.h', 'psa/crypto_extra.h', 'psa/crypto_values.h']
427TEST_SUITES = ['tests/suites/test_suite_psa_crypto_metadata.data']
428
Gilles Peskine54f54452019-05-27 18:31:59 +0200429def main():
Gilles Peskine24827022018-09-25 18:49:23 +0200430 parser = argparse.ArgumentParser(description=globals()['__doc__'])
431 parser.add_argument('--include', '-I',
432 action='append', default=['include'],
433 help='Directory for header files')
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200434 parser.add_argument('--keep-c',
435 action='store_true', dest='keep_c', default=False,
436 help='Keep the intermediate C file')
437 parser.add_argument('--no-keep-c',
438 action='store_false', dest='keep_c',
439 help='Don\'t keep the intermediate C file (default)')
Gilles Peskine8f5a5012019-11-21 16:49:10 +0100440 parser.add_argument('--program',
441 default='programs/psa/psa_constant_names',
442 help='Program to test')
Gilles Peskine32558482019-12-03 19:03:35 +0100443 parser.add_argument('--show',
444 action='store_true',
445 help='Keep the intermediate C file')
446 parser.add_argument('--no-show',
447 action='store_false', dest='show',
448 help='Don\'t show tested values (default)')
Gilles Peskine24827022018-09-25 18:49:23 +0200449 options = parser.parse_args()
Gilles Peskine69f93b52019-11-21 16:49:50 +0100450 headers = [os.path.join(options.include[0], h) for h in HEADERS]
451 inputs = gather_inputs(headers, TEST_SUITES)
Gilles Peskine24609332019-11-21 17:44:21 +0100452 tests = Tests(options)
453 tests.run_all(inputs)
454 tests.report(sys.stdout)
455 if tests.errors:
Gilles Peskine24827022018-09-25 18:49:23 +0200456 exit(1)
Gilles Peskine54f54452019-05-27 18:31:59 +0200457
458if __name__ == '__main__':
459 main()