blob: 68bfe77be9a1507d8df728241428ff51243a8ef0 [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
Bence Szépkúti1e148272020-08-07 13:07:28 +020010# Copyright The Mbed TLS Contributors
Bence Szépkútic7da1fe2020-05-26 01:54:15 +020011# SPDX-License-Identifier: Apache-2.0
12#
13# Licensed under the Apache License, Version 2.0 (the "License"); you may
14# not use this file except in compliance with the License.
15# You may obtain a copy of the License at
16#
17# http://www.apache.org/licenses/LICENSE-2.0
18#
19# Unless required by applicable law or agreed to in writing, software
20# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
21# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22# See the License for the specific language governing permissions and
23# limitations under the License.
Bence Szépkúti700ee442020-05-26 00:33:31 +020024
Gilles Peskine24827022018-09-25 18:49:23 +020025import argparse
Gilles Peskinea5000f12019-11-21 17:51:11 +010026from collections import namedtuple
Gilles Peskine24827022018-09-25 18:49:23 +020027import itertools
28import os
29import platform
30import re
31import subprocess
32import sys
33import tempfile
34
Gilles Peskinea0a315c2018-10-19 11:27:10 +020035class ReadFileLineException(Exception):
36 def __init__(self, filename, line_number):
37 message = 'in {} at {}'.format(filename, line_number)
38 super(ReadFileLineException, self).__init__(message)
39 self.filename = filename
40 self.line_number = line_number
41
42class read_file_lines:
Gilles Peskine54f54452019-05-27 18:31:59 +020043 # Dear Pylint, conventionally, a context manager class name is lowercase.
44 # pylint: disable=invalid-name,too-few-public-methods
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020045 """Context manager to read a text file line by line.
46
47 ```
48 with read_file_lines(filename) as lines:
49 for line in lines:
50 process(line)
51 ```
52 is equivalent to
53 ```
54 with open(filename, 'r') as input_file:
55 for line in input_file:
56 process(line)
57 ```
58 except that if process(line) raises an exception, then the read_file_lines
59 snippet annotates the exception with the file name and line number.
60 """
Gilles Peskine49af2d32019-12-06 19:20:13 +010061 def __init__(self, filename, binary=False):
Gilles Peskinea0a315c2018-10-19 11:27:10 +020062 self.filename = filename
63 self.line_number = 'entry'
Gilles Peskine54f54452019-05-27 18:31:59 +020064 self.generator = None
Gilles Peskine49af2d32019-12-06 19:20:13 +010065 self.binary = binary
Gilles Peskinea0a315c2018-10-19 11:27:10 +020066 def __enter__(self):
Gilles Peskine49af2d32019-12-06 19:20:13 +010067 self.generator = enumerate(open(self.filename,
68 'rb' if self.binary else 'r'))
Gilles Peskinea0a315c2018-10-19 11:27:10 +020069 return self
70 def __iter__(self):
71 for line_number, content in self.generator:
72 self.line_number = line_number
73 yield content
74 self.line_number = 'exit'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020075 def __exit__(self, exc_type, exc_value, exc_traceback):
76 if exc_type is not None:
Gilles Peskinea0a315c2018-10-19 11:27:10 +020077 raise ReadFileLineException(self.filename, self.line_number) \
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020078 from exc_value
Gilles Peskinea0a315c2018-10-19 11:27:10 +020079
Gilles Peskine24827022018-09-25 18:49:23 +020080class Inputs:
Gilles Peskine8c8694c2019-11-21 19:22:45 +010081 # pylint: disable=too-many-instance-attributes
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020082 """Accumulate information about macros to test.
Gilles Peskine4408dfd2019-11-21 17:16:21 +010083
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020084 This includes macro names as well as information about their arguments
85 when applicable.
86 """
87
Gilles Peskine24827022018-09-25 18:49:23 +020088 def __init__(self):
Gilles Peskine2bcfc712019-11-21 19:49:26 +010089 self.all_declared = set()
Gilles Peskine24827022018-09-25 18:49:23 +020090 # Sets of names per type
91 self.statuses = set(['PSA_SUCCESS'])
92 self.algorithms = set(['0xffffffff'])
Gilles Peskinef65ed6f2019-12-04 17:18:41 +010093 self.ecc_curves = set(['0xff'])
94 self.dh_groups = set(['0xff'])
95 self.key_types = set(['0xffff'])
Gilles Peskine24827022018-09-25 18:49:23 +020096 self.key_usage_flags = set(['0x80000000'])
Gilles Peskine434899f2018-10-19 11:30:26 +020097 # Hard-coded value for unknown algorithms
Bence Szépkúti7e37bf92020-12-08 07:33:08 +010098 self.hash_algorithms = set(['0x020000fe'])
99 self.mac_algorithms = set(['0x0300ffff'])
100 self.ka_algorithms = set(['0x09fc0000'])
101 self.kdf_algorithms = set(['0x080000ff'])
Gilles Peskine434899f2018-10-19 11:30:26 +0200102 # For AEAD algorithms, the only variability is over the tag length,
103 # and this only applies to known algorithms, so don't test an
104 # unknown algorithm.
105 self.aead_algorithms = set()
Gilles Peskine24827022018-09-25 18:49:23 +0200106 # Identifier prefixes
107 self.table_by_prefix = {
108 'ERROR': self.statuses,
109 'ALG': self.algorithms,
Gilles Peskine98a710c2019-11-21 18:58:36 +0100110 'ECC_CURVE': self.ecc_curves,
111 'DH_GROUP': self.dh_groups,
Gilles Peskine24827022018-09-25 18:49:23 +0200112 'KEY_TYPE': self.key_types,
113 'KEY_USAGE': self.key_usage_flags,
114 }
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100115 # Test functions
116 self.table_by_test_function = {
Gilles Peskine8fa13482019-11-25 17:10:12 +0100117 # Any function ending in _algorithm also gets added to
118 # self.algorithms.
119 'key_type': [self.key_types],
Gilles Peskinef8210f22019-12-02 17:26:44 +0100120 'block_cipher_key_type': [self.key_types],
121 'stream_cipher_key_type': [self.key_types],
Gilles Peskine228abc52019-12-03 17:24:19 +0100122 'ecc_key_family': [self.ecc_curves],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100123 'ecc_key_types': [self.ecc_curves],
Gilles Peskine228abc52019-12-03 17:24:19 +0100124 'dh_key_family': [self.dh_groups],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100125 'dh_key_types': [self.dh_groups],
126 'hash_algorithm': [self.hash_algorithms],
127 'mac_algorithm': [self.mac_algorithms],
128 'cipher_algorithm': [],
129 'hmac_algorithm': [self.mac_algorithms],
130 'aead_algorithm': [self.aead_algorithms],
131 'key_derivation_algorithm': [self.kdf_algorithms],
132 'key_agreement_algorithm': [self.ka_algorithms],
133 'asymmetric_signature_algorithm': [],
134 'asymmetric_signature_wildcard': [self.algorithms],
135 'asymmetric_encryption_algorithm': [],
136 'other_algorithm': [],
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100137 }
Gilles Peskine24827022018-09-25 18:49:23 +0200138 # macro name -> list of argument names
139 self.argspecs = {}
140 # argument name -> list of values
Gilles Peskine434899f2018-10-19 11:30:26 +0200141 self.arguments_for = {
142 'mac_length': ['1', '63'],
143 'tag_length': ['1', '63'],
144 }
Gilles Peskine24827022018-09-25 18:49:23 +0200145
Gilles Peskineffe2d6e2019-11-21 17:17:01 +0100146 def get_names(self, type_word):
147 """Return the set of known names of values of the given type."""
148 return {
149 'status': self.statuses,
150 'algorithm': self.algorithms,
151 'ecc_curve': self.ecc_curves,
152 'dh_group': self.dh_groups,
153 'key_type': self.key_types,
154 'key_usage': self.key_usage_flags,
155 }[type_word]
156
Gilles Peskine24827022018-09-25 18:49:23 +0200157 def gather_arguments(self):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200158 """Populate the list of values for macro arguments.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100159
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200160 Call this after parsing all the inputs.
161 """
Gilles Peskine24827022018-09-25 18:49:23 +0200162 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200163 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
Gilles Peskine882e57e2019-04-12 00:12:07 +0200164 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
Gilles Peskine17542082019-01-04 19:46:31 +0100165 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200166 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
Gilles Peskine24827022018-09-25 18:49:23 +0200167 self.arguments_for['curve'] = sorted(self.ecc_curves)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200168 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine24827022018-09-25 18:49:23 +0200169
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200170 @staticmethod
171 def _format_arguments(name, arguments):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200172 """Format a macro call with arguments.."""
Gilles Peskine24827022018-09-25 18:49:23 +0200173 return name + '(' + ', '.join(arguments) + ')'
174
175 def distribute_arguments(self, name):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200176 """Generate macro calls with each tested argument set.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100177
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200178 If name is a macro without arguments, just yield "name".
179 If name is a macro with arguments, yield a series of
180 "name(arg1,...,argN)" where each argument takes each possible
181 value at least once.
182 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200183 try:
184 if name not in self.argspecs:
185 yield name
186 return
187 argspec = self.argspecs[name]
188 if argspec == []:
189 yield name + '()'
190 return
191 argument_lists = [self.arguments_for[arg] for arg in argspec]
192 arguments = [values[0] for values in argument_lists]
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200193 yield self._format_arguments(name, arguments)
Gilles Peskine54f54452019-05-27 18:31:59 +0200194 # Dear Pylint, enumerate won't work here since we're modifying
195 # the array.
196 # pylint: disable=consider-using-enumerate
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200197 for i in range(len(arguments)):
198 for value in argument_lists[i][1:]:
199 arguments[i] = value
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200200 yield self._format_arguments(name, arguments)
Gilles Peskinef96ed662018-10-19 11:29:56 +0200201 arguments[i] = argument_lists[0][0]
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200202 except BaseException as e:
203 raise Exception('distribute_arguments({})'.format(name)) from e
Gilles Peskine24827022018-09-25 18:49:23 +0200204
Gilles Peskine5a994c12019-11-21 16:46:51 +0100205 def generate_expressions(self, names):
206 return itertools.chain(*map(self.distribute_arguments, names))
207
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200208 _argument_split_re = re.compile(r' *, *')
209 @classmethod
210 def _argument_split(cls, arguments):
211 return re.split(cls._argument_split_re, arguments)
212
Gilles Peskine24827022018-09-25 18:49:23 +0200213 # Regex for interesting header lines.
214 # Groups: 1=macro name, 2=type, 3=argument list (optional).
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200215 _header_line_re = \
Gilles Peskine24827022018-09-25 18:49:23 +0200216 re.compile(r'#define +' +
Gilles Peskine98a710c2019-11-21 18:58:36 +0100217 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
Gilles Peskine24827022018-09-25 18:49:23 +0200218 r'(?:\(([^\n()]*)\))?')
219 # Regex of macro names to exclude.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200220 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
Gilles Peskinec68ce962018-10-19 11:31:52 +0200221 # Additional excluded macros.
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200222 _excluded_names = set([
223 # Macros that provide an alternative way to build the same
224 # algorithm as another macro.
225 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH',
226 'PSA_ALG_FULL_LENGTH_MAC',
227 # Auxiliary macro whose name doesn't fit the usual patterns for
228 # auxiliary macros.
229 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH_CASE',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200230 ])
Gilles Peskine24827022018-09-25 18:49:23 +0200231 def parse_header_line(self, line):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200232 """Parse a C header line, looking for "#define PSA_xxx"."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200233 m = re.match(self._header_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200234 if not m:
235 return
236 name = m.group(1)
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100237 self.all_declared.add(name)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200238 if re.search(self._excluded_name_re, name) or \
239 name in self._excluded_names:
Gilles Peskine24827022018-09-25 18:49:23 +0200240 return
241 dest = self.table_by_prefix.get(m.group(2))
242 if dest is None:
243 return
244 dest.add(name)
245 if m.group(3):
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200246 self.argspecs[name] = self._argument_split(m.group(3))
Gilles Peskine24827022018-09-25 18:49:23 +0200247
Gilles Peskine49af2d32019-12-06 19:20:13 +0100248 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
Gilles Peskine24827022018-09-25 18:49:23 +0200249 def parse_header(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200250 """Parse a C header file, looking for "#define PSA_xxx"."""
Gilles Peskine49af2d32019-12-06 19:20:13 +0100251 with read_file_lines(filename, binary=True) as lines:
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200252 for line in lines:
Gilles Peskine49af2d32019-12-06 19:20:13 +0100253 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
Gilles Peskine24827022018-09-25 18:49:23 +0200254 self.parse_header_line(line)
255
Gilles Peskine49af2d32019-12-06 19:20:13 +0100256 _macro_identifier_re = re.compile(r'[A-Z]\w+')
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100257 def generate_undeclared_names(self, expr):
258 for name in re.findall(self._macro_identifier_re, expr):
259 if name not in self.all_declared:
260 yield name
261
262 def accept_test_case_line(self, function, argument):
263 #pylint: disable=unused-argument
264 undeclared = list(self.generate_undeclared_names(argument))
265 if undeclared:
266 raise Exception('Undeclared names in test case', undeclared)
267 return True
268
Gilles Peskine24827022018-09-25 18:49:23 +0200269 def add_test_case_line(self, function, argument):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200270 """Parse a test case data line, looking for algorithm metadata tests."""
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100271 sets = []
Gilles Peskine24827022018-09-25 18:49:23 +0200272 if function.endswith('_algorithm'):
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100273 sets.append(self.algorithms)
Gilles Peskine79616682019-11-21 20:08:10 +0100274 if function == 'key_agreement_algorithm' and \
275 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
276 # We only want *raw* key agreement algorithms as such, so
277 # exclude ones that are already chained with a KDF.
278 # Keep the expression as one to test as an algorithm.
279 function = 'other_algorithm'
Gilles Peskine8fa13482019-11-25 17:10:12 +0100280 sets += self.table_by_test_function[function]
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100281 if self.accept_test_case_line(function, argument):
282 for s in sets:
283 s.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200284
285 # Regex matching a *.data line containing a test function call and
286 # its arguments. The actual definition is partly positional, but this
287 # regex is good enough in practice.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200288 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
Gilles Peskine24827022018-09-25 18:49:23 +0200289 def parse_test_cases(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200290 """Parse a test case file (*.data), looking for algorithm metadata tests."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200291 with read_file_lines(filename) as lines:
292 for line in lines:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200293 m = re.match(self._test_case_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200294 if m:
295 self.add_test_case_line(m.group(1), m.group(2))
296
Gilles Peskine84a45812019-11-21 19:50:33 +0100297def gather_inputs(headers, test_suites, inputs_class=Inputs):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200298 """Read the list of inputs to test psa_constant_names with."""
Gilles Peskine84a45812019-11-21 19:50:33 +0100299 inputs = inputs_class()
Gilles Peskine24827022018-09-25 18:49:23 +0200300 for header in headers:
301 inputs.parse_header(header)
302 for test_cases in test_suites:
303 inputs.parse_test_cases(test_cases)
304 inputs.gather_arguments()
305 return inputs
306
307def remove_file_if_exists(filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200308 """Remove the specified file, ignoring errors."""
Gilles Peskine24827022018-09-25 18:49:23 +0200309 if not filename:
310 return
311 try:
312 os.remove(filename)
Gilles Peskine54f54452019-05-27 18:31:59 +0200313 except OSError:
Gilles Peskine24827022018-09-25 18:49:23 +0200314 pass
315
Gilles Peskinefc622112020-12-11 00:27:14 +0100316def create_c_file(file_label):
317 """Create a temporary C file.
318
319 * ``file_label``: a string that will be included in the file name.
320
321 Return ```(c_file, c_name, exe_name)``` where ``c_file`` is a Python
322 stream open for writing to the file, ``c_name`` is the name of the file
323 and ``exe_name`` is the name of the executable that will be produced
324 by compiling the file.
325 """
326 c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(file_label),
327 suffix='.c')
328 exe_suffix = '.exe' if platform.system() == 'Windows' else ''
329 exe_name = c_name[:-2] + exe_suffix
330 remove_file_if_exists(exe_name)
331 c_file = os.fdopen(c_fd, 'w', encoding='ascii')
332 return c_file, c_name, exe_name
333
334def generate_c_printf_expressions(c_file, cast_to, printf_format, expressions):
335 """Generate C instructions to print the value of ``expressions``.
336
337 Write the code with ``c_file``'s ``write`` method.
338
339 Each expression is cast to the type ``cast_to`` and printed with the
340 printf format ``printf_format``.
341 """
342 for expr in expressions:
343 c_file.write(' printf("{}\\n", ({}) {});\n'
344 .format(printf_format, cast_to, expr))
345
346def generate_c_file(c_file,
347 caller, header,
348 main_generator):
349 """Generate a temporary C source file.
350
351 * ``c_file`` is an open stream on the C source file.
352 * ``caller``: an informational string written in a comment at the top
353 of the file.
354 * ``header``: extra code to insert before any function in the generated
355 C file.
356 * ``main_generator``: a function called with ``c_file`` as its sole argument
357 to generate the body of the ``main()`` function.
358 """
359 c_file.write('/* Generated by {} */'
360 .format(caller))
361 c_file.write('''
Gilles Peskine24827022018-09-25 18:49:23 +0200362#include <stdio.h>
Gilles Peskinefc622112020-12-11 00:27:14 +0100363''')
364 c_file.write(header)
365 c_file.write('''
Gilles Peskine24827022018-09-25 18:49:23 +0200366int main(void)
367{
368''')
Gilles Peskinefc622112020-12-11 00:27:14 +0100369 main_generator(c_file)
370 c_file.write(''' return 0;
Gilles Peskine24827022018-09-25 18:49:23 +0200371}
372''')
Gilles Peskinefc622112020-12-11 00:27:14 +0100373
374def get_c_expression_values(
375 cast_to, printf_format,
376 expressions,
377 caller=__name__, file_label='',
378 header='', include_path=None,
379 keep_c=False,
380): # pylint: disable=too-many-arguments
381 """Generate and run a program to print out numerical values for expressions.
382
383 * ``cast_to``: a C type.
384 * ``printf_format``: a printf format suitable for the type ``cast_to``.
385 * ``header``: extra code to insert before any function in the generated
386 C file.
387 * ``expressions``: a list of C language expressions that have the type
388 ``type``.
389 * ``include_path``: a list of directories containing header files.
390 * ``keep_c``: if true, keep the temporary C file (presumably for debugging
391 purposes).
392
393 Return the list of values of the ``expressions``.
394 """
395 if include_path is None:
396 include_path = []
397 c_name = None
398 exe_name = None
399 try:
400 c_file, c_name, exe_name = create_c_file(file_label)
401 generate_c_file(
402 c_file, caller, header,
403 lambda c_file: generate_c_printf_expressions(c_file,
404 cast_to, printf_format,
405 expressions)
406 )
Gilles Peskine24827022018-09-25 18:49:23 +0200407 c_file.close()
408 cc = os.getenv('CC', 'cc')
409 subprocess.check_call([cc] +
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100410 ['-I' + dir for dir in include_path] +
Gilles Peskine24827022018-09-25 18:49:23 +0200411 ['-o', exe_name, c_name])
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100412 if keep_c:
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200413 sys.stderr.write('List of {} tests kept at {}\n'
Gilles Peskinefc622112020-12-11 00:27:14 +0100414 .format(caller, c_name))
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200415 else:
416 os.remove(c_name)
Gilles Peskine24827022018-09-25 18:49:23 +0200417 output = subprocess.check_output([exe_name])
418 return output.decode('ascii').strip().split('\n')
419 finally:
420 remove_file_if_exists(exe_name)
421
Gilles Peskinefc622112020-12-11 00:27:14 +0100422def run_c(type_word, expressions, include_path=None, keep_c=False):
423 """Generate and run a program to print out numerical values for expressions."""
424 if type_word == 'status':
425 cast_to = 'long'
426 printf_format = '%ld'
427 else:
428 cast_to = 'unsigned long'
429 printf_format = '0x%08lx'
430 return get_c_expression_values(
431 cast_to, printf_format,
432 expressions,
433 caller='test_psa_constant_names.py for {} values'.format(type_word),
434 file_label=type_word,
435 header='#include <psa/crypto.h>',
436 include_path=include_path,
437 keep_c=keep_c
438 )
439
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200440NORMALIZE_STRIP_RE = re.compile(r'\s+')
Gilles Peskine24827022018-09-25 18:49:23 +0200441def normalize(expr):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200442 """Normalize the C expression so as not to care about trivial differences.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100443
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200444 Currently "trivial differences" means whitespace.
445 """
Gilles Peskine5a6dc892019-11-21 16:48:07 +0100446 return re.sub(NORMALIZE_STRIP_RE, '', expr)
Gilles Peskine24827022018-09-25 18:49:23 +0200447
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100448def collect_values(inputs, type_word, include_path=None, keep_c=False):
Gilles Peskinec2317112019-11-21 17:17:39 +0100449 """Generate expressions using known macro names and calculate their values.
450
451 Return a list of pairs of (expr, value) where expr is an expression and
452 value is a string representation of its integer value.
453 """
454 names = inputs.get_names(type_word)
455 expressions = sorted(inputs.generate_expressions(names))
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100456 values = run_c(type_word, expressions,
457 include_path=include_path, keep_c=keep_c)
Gilles Peskinec2317112019-11-21 17:17:39 +0100458 return expressions, values
459
Gilles Peskine24609332019-11-21 17:44:21 +0100460class Tests:
461 """An object representing tests and their results."""
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100462
Gilles Peskinea5000f12019-11-21 17:51:11 +0100463 Error = namedtuple('Error',
464 ['type', 'expression', 'value', 'output'])
465
Gilles Peskine24609332019-11-21 17:44:21 +0100466 def __init__(self, options):
467 self.options = options
468 self.count = 0
469 self.errors = []
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100470
Gilles Peskine24609332019-11-21 17:44:21 +0100471 def run_one(self, inputs, type_word):
472 """Test psa_constant_names for the specified type.
Gilles Peskine24827022018-09-25 18:49:23 +0200473
Gilles Peskine24609332019-11-21 17:44:21 +0100474 Run the program on the names for this type.
475 Use the inputs to figure out what arguments to pass to macros that
476 take arguments.
477 """
478 expressions, values = collect_values(inputs, type_word,
479 include_path=self.options.include,
480 keep_c=self.options.keep_c)
481 output = subprocess.check_output([self.options.program, type_word] +
482 values)
483 outputs = output.decode('ascii').strip().split('\n')
484 self.count += len(expressions)
485 for expr, value, output in zip(expressions, values, outputs):
Gilles Peskine32558482019-12-03 19:03:35 +0100486 if self.options.show:
487 sys.stdout.write('{} {}\t{}\n'.format(type_word, value, output))
Gilles Peskine24609332019-11-21 17:44:21 +0100488 if normalize(expr) != normalize(output):
Gilles Peskinea5000f12019-11-21 17:51:11 +0100489 self.errors.append(self.Error(type=type_word,
490 expression=expr,
491 value=value,
492 output=output))
Gilles Peskine24827022018-09-25 18:49:23 +0200493
Gilles Peskine24609332019-11-21 17:44:21 +0100494 def run_all(self, inputs):
495 """Run psa_constant_names on all the gathered inputs."""
496 for type_word in ['status', 'algorithm', 'ecc_curve', 'dh_group',
497 'key_type', 'key_usage']:
498 self.run_one(inputs, type_word)
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100499
Gilles Peskine24609332019-11-21 17:44:21 +0100500 def report(self, out):
501 """Describe each case where the output is not as expected.
502
503 Write the errors to ``out``.
504 Also write a total.
505 """
Gilles Peskinea5000f12019-11-21 17:51:11 +0100506 for error in self.errors:
Gilles Peskine24609332019-11-21 17:44:21 +0100507 out.write('For {} "{}", got "{}" (value: {})\n'
Gilles Peskinea5000f12019-11-21 17:51:11 +0100508 .format(error.type, error.expression,
509 error.output, error.value))
Gilles Peskine24609332019-11-21 17:44:21 +0100510 out.write('{} test cases'.format(self.count))
511 if self.errors:
512 out.write(', {} FAIL\n'.format(len(self.errors)))
513 else:
514 out.write(' PASS\n')
Gilles Peskine24827022018-09-25 18:49:23 +0200515
Gilles Peskine69f93b52019-11-21 16:49:50 +0100516HEADERS = ['psa/crypto.h', 'psa/crypto_extra.h', 'psa/crypto_values.h']
517TEST_SUITES = ['tests/suites/test_suite_psa_crypto_metadata.data']
518
Gilles Peskine54f54452019-05-27 18:31:59 +0200519def main():
Gilles Peskine24827022018-09-25 18:49:23 +0200520 parser = argparse.ArgumentParser(description=globals()['__doc__'])
521 parser.add_argument('--include', '-I',
522 action='append', default=['include'],
523 help='Directory for header files')
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200524 parser.add_argument('--keep-c',
525 action='store_true', dest='keep_c', default=False,
526 help='Keep the intermediate C file')
527 parser.add_argument('--no-keep-c',
528 action='store_false', dest='keep_c',
529 help='Don\'t keep the intermediate C file (default)')
Gilles Peskine8f5a5012019-11-21 16:49:10 +0100530 parser.add_argument('--program',
531 default='programs/psa/psa_constant_names',
532 help='Program to test')
Gilles Peskine32558482019-12-03 19:03:35 +0100533 parser.add_argument('--show',
534 action='store_true',
535 help='Keep the intermediate C file')
536 parser.add_argument('--no-show',
537 action='store_false', dest='show',
538 help='Don\'t show tested values (default)')
Gilles Peskine24827022018-09-25 18:49:23 +0200539 options = parser.parse_args()
Gilles Peskine69f93b52019-11-21 16:49:50 +0100540 headers = [os.path.join(options.include[0], h) for h in HEADERS]
541 inputs = gather_inputs(headers, TEST_SUITES)
Gilles Peskine24609332019-11-21 17:44:21 +0100542 tests = Tests(options)
543 tests.run_all(inputs)
544 tests.report(sys.stdout)
545 if tests.errors:
Gilles Peskine8b022352020-03-24 18:36:56 +0100546 sys.exit(1)
Gilles Peskine54f54452019-05-27 18:31:59 +0200547
548if __name__ == '__main__':
549 main()