blob: 2d6e3826e87225870605f49191621267ceb690c7 [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úti700ee442020-05-26 00:33:31 +020010# Copyright (C) 2018-2020, Arm Limited, All Rights Reserved
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#
25# This file is part of Mbed TLS (https://tls.mbed.org)
26
Gilles Peskine24827022018-09-25 18:49:23 +020027import argparse
Gilles Peskinea5000f12019-11-21 17:51:11 +010028from collections import namedtuple
Gilles Peskine24827022018-09-25 18:49:23 +020029import itertools
30import os
31import platform
32import re
33import subprocess
34import sys
35import tempfile
36
Gilles Peskinea0a315c2018-10-19 11:27:10 +020037class ReadFileLineException(Exception):
38 def __init__(self, filename, line_number):
39 message = 'in {} at {}'.format(filename, line_number)
40 super(ReadFileLineException, self).__init__(message)
41 self.filename = filename
42 self.line_number = line_number
43
44class read_file_lines:
Gilles Peskine54f54452019-05-27 18:31:59 +020045 # Dear Pylint, conventionally, a context manager class name is lowercase.
46 # pylint: disable=invalid-name,too-few-public-methods
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020047 """Context manager to read a text file line by line.
48
49 ```
50 with read_file_lines(filename) as lines:
51 for line in lines:
52 process(line)
53 ```
54 is equivalent to
55 ```
56 with open(filename, 'r') as input_file:
57 for line in input_file:
58 process(line)
59 ```
60 except that if process(line) raises an exception, then the read_file_lines
61 snippet annotates the exception with the file name and line number.
62 """
Gilles Peskine49af2d32019-12-06 19:20:13 +010063 def __init__(self, filename, binary=False):
Gilles Peskinea0a315c2018-10-19 11:27:10 +020064 self.filename = filename
65 self.line_number = 'entry'
Gilles Peskine54f54452019-05-27 18:31:59 +020066 self.generator = None
Gilles Peskine49af2d32019-12-06 19:20:13 +010067 self.binary = binary
Gilles Peskinea0a315c2018-10-19 11:27:10 +020068 def __enter__(self):
Gilles Peskine49af2d32019-12-06 19:20:13 +010069 self.generator = enumerate(open(self.filename,
70 'rb' if self.binary else 'r'))
Gilles Peskinea0a315c2018-10-19 11:27:10 +020071 return self
72 def __iter__(self):
73 for line_number, content in self.generator:
74 self.line_number = line_number
75 yield content
76 self.line_number = 'exit'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020077 def __exit__(self, exc_type, exc_value, exc_traceback):
78 if exc_type is not None:
Gilles Peskinea0a315c2018-10-19 11:27:10 +020079 raise ReadFileLineException(self.filename, self.line_number) \
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020080 from exc_value
Gilles Peskinea0a315c2018-10-19 11:27:10 +020081
Gilles Peskine24827022018-09-25 18:49:23 +020082class Inputs:
Gilles Peskine8c8694c2019-11-21 19:22:45 +010083 # pylint: disable=too-many-instance-attributes
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020084 """Accumulate information about macros to test.
Gilles Peskine4408dfd2019-11-21 17:16:21 +010085
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020086 This includes macro names as well as information about their arguments
87 when applicable.
88 """
89
Gilles Peskine24827022018-09-25 18:49:23 +020090 def __init__(self):
Gilles Peskine2bcfc712019-11-21 19:49:26 +010091 self.all_declared = set()
Gilles Peskine24827022018-09-25 18:49:23 +020092 # Sets of names per type
93 self.statuses = set(['PSA_SUCCESS'])
94 self.algorithms = set(['0xffffffff'])
Gilles Peskinef65ed6f2019-12-04 17:18:41 +010095 self.ecc_curves = set(['0xff'])
96 self.dh_groups = set(['0xff'])
97 self.key_types = set(['0xffff'])
Gilles Peskine24827022018-09-25 18:49:23 +020098 self.key_usage_flags = set(['0x80000000'])
Gilles Peskine434899f2018-10-19 11:30:26 +020099 # Hard-coded value for unknown algorithms
Darryl Green61b7f612019-02-04 16:00:21 +0000100 self.hash_algorithms = set(['0x010000fe'])
Gilles Peskine434899f2018-10-19 11:30:26 +0200101 self.mac_algorithms = set(['0x02ff00ff'])
Gilles Peskine882e57e2019-04-12 00:12:07 +0200102 self.ka_algorithms = set(['0x30fc0000'])
103 self.kdf_algorithms = set(['0x200000ff'])
Gilles Peskine434899f2018-10-19 11:30:26 +0200104 # For AEAD algorithms, the only variability is over the tag length,
105 # and this only applies to known algorithms, so don't test an
106 # unknown algorithm.
107 self.aead_algorithms = set()
Gilles Peskine24827022018-09-25 18:49:23 +0200108 # Identifier prefixes
109 self.table_by_prefix = {
110 'ERROR': self.statuses,
111 'ALG': self.algorithms,
Gilles Peskine98a710c2019-11-21 18:58:36 +0100112 'ECC_CURVE': self.ecc_curves,
113 'DH_GROUP': self.dh_groups,
Gilles Peskine24827022018-09-25 18:49:23 +0200114 'KEY_TYPE': self.key_types,
115 'KEY_USAGE': self.key_usage_flags,
116 }
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100117 # Test functions
118 self.table_by_test_function = {
Gilles Peskine8fa13482019-11-25 17:10:12 +0100119 # Any function ending in _algorithm also gets added to
120 # self.algorithms.
121 'key_type': [self.key_types],
Gilles Peskinef8210f22019-12-02 17:26:44 +0100122 'block_cipher_key_type': [self.key_types],
123 'stream_cipher_key_type': [self.key_types],
Gilles Peskine228abc52019-12-03 17:24:19 +0100124 'ecc_key_family': [self.ecc_curves],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100125 'ecc_key_types': [self.ecc_curves],
Gilles Peskine228abc52019-12-03 17:24:19 +0100126 'dh_key_family': [self.dh_groups],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100127 'dh_key_types': [self.dh_groups],
128 'hash_algorithm': [self.hash_algorithms],
129 'mac_algorithm': [self.mac_algorithms],
130 'cipher_algorithm': [],
131 'hmac_algorithm': [self.mac_algorithms],
132 'aead_algorithm': [self.aead_algorithms],
133 'key_derivation_algorithm': [self.kdf_algorithms],
134 'key_agreement_algorithm': [self.ka_algorithms],
135 'asymmetric_signature_algorithm': [],
136 'asymmetric_signature_wildcard': [self.algorithms],
137 'asymmetric_encryption_algorithm': [],
138 'other_algorithm': [],
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100139 }
Gilles Peskine24827022018-09-25 18:49:23 +0200140 # macro name -> list of argument names
141 self.argspecs = {}
142 # argument name -> list of values
Gilles Peskine434899f2018-10-19 11:30:26 +0200143 self.arguments_for = {
144 'mac_length': ['1', '63'],
145 'tag_length': ['1', '63'],
146 }
Gilles Peskine24827022018-09-25 18:49:23 +0200147
Gilles Peskineffe2d6e2019-11-21 17:17:01 +0100148 def get_names(self, type_word):
149 """Return the set of known names of values of the given type."""
150 return {
151 'status': self.statuses,
152 'algorithm': self.algorithms,
153 'ecc_curve': self.ecc_curves,
154 'dh_group': self.dh_groups,
155 'key_type': self.key_types,
156 'key_usage': self.key_usage_flags,
157 }[type_word]
158
Gilles Peskine24827022018-09-25 18:49:23 +0200159 def gather_arguments(self):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200160 """Populate the list of values for macro arguments.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100161
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200162 Call this after parsing all the inputs.
163 """
Gilles Peskine24827022018-09-25 18:49:23 +0200164 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200165 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
Gilles Peskine882e57e2019-04-12 00:12:07 +0200166 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
Gilles Peskine17542082019-01-04 19:46:31 +0100167 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200168 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
Gilles Peskine24827022018-09-25 18:49:23 +0200169 self.arguments_for['curve'] = sorted(self.ecc_curves)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200170 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine24827022018-09-25 18:49:23 +0200171
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200172 @staticmethod
173 def _format_arguments(name, arguments):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200174 """Format a macro call with arguments.."""
Gilles Peskine24827022018-09-25 18:49:23 +0200175 return name + '(' + ', '.join(arguments) + ')'
176
177 def distribute_arguments(self, name):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200178 """Generate macro calls with each tested argument set.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100179
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200180 If name is a macro without arguments, just yield "name".
181 If name is a macro with arguments, yield a series of
182 "name(arg1,...,argN)" where each argument takes each possible
183 value at least once.
184 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200185 try:
186 if name not in self.argspecs:
187 yield name
188 return
189 argspec = self.argspecs[name]
190 if argspec == []:
191 yield name + '()'
192 return
193 argument_lists = [self.arguments_for[arg] for arg in argspec]
194 arguments = [values[0] for values in argument_lists]
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200195 yield self._format_arguments(name, arguments)
Gilles Peskine54f54452019-05-27 18:31:59 +0200196 # Dear Pylint, enumerate won't work here since we're modifying
197 # the array.
198 # pylint: disable=consider-using-enumerate
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200199 for i in range(len(arguments)):
200 for value in argument_lists[i][1:]:
201 arguments[i] = value
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200202 yield self._format_arguments(name, arguments)
Gilles Peskinef96ed662018-10-19 11:29:56 +0200203 arguments[i] = argument_lists[0][0]
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200204 except BaseException as e:
205 raise Exception('distribute_arguments({})'.format(name)) from e
Gilles Peskine24827022018-09-25 18:49:23 +0200206
Gilles Peskine5a994c12019-11-21 16:46:51 +0100207 def generate_expressions(self, names):
208 return itertools.chain(*map(self.distribute_arguments, names))
209
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200210 _argument_split_re = re.compile(r' *, *')
211 @classmethod
212 def _argument_split(cls, arguments):
213 return re.split(cls._argument_split_re, arguments)
214
Gilles Peskine24827022018-09-25 18:49:23 +0200215 # Regex for interesting header lines.
216 # Groups: 1=macro name, 2=type, 3=argument list (optional).
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200217 _header_line_re = \
Gilles Peskine24827022018-09-25 18:49:23 +0200218 re.compile(r'#define +' +
Gilles Peskine98a710c2019-11-21 18:58:36 +0100219 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
Gilles Peskine24827022018-09-25 18:49:23 +0200220 r'(?:\(([^\n()]*)\))?')
221 # Regex of macro names to exclude.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200222 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
Gilles Peskinec68ce962018-10-19 11:31:52 +0200223 # Additional excluded macros.
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200224 _excluded_names = set([
225 # Macros that provide an alternative way to build the same
226 # algorithm as another macro.
227 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH',
228 'PSA_ALG_FULL_LENGTH_MAC',
229 # Auxiliary macro whose name doesn't fit the usual patterns for
230 # auxiliary macros.
231 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH_CASE',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200232 ])
Gilles Peskine24827022018-09-25 18:49:23 +0200233 def parse_header_line(self, line):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200234 """Parse a C header line, looking for "#define PSA_xxx"."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200235 m = re.match(self._header_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200236 if not m:
237 return
238 name = m.group(1)
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100239 self.all_declared.add(name)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200240 if re.search(self._excluded_name_re, name) or \
241 name in self._excluded_names:
Gilles Peskine24827022018-09-25 18:49:23 +0200242 return
243 dest = self.table_by_prefix.get(m.group(2))
244 if dest is None:
245 return
246 dest.add(name)
247 if m.group(3):
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200248 self.argspecs[name] = self._argument_split(m.group(3))
Gilles Peskine24827022018-09-25 18:49:23 +0200249
Gilles Peskine49af2d32019-12-06 19:20:13 +0100250 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
Gilles Peskine24827022018-09-25 18:49:23 +0200251 def parse_header(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200252 """Parse a C header file, looking for "#define PSA_xxx"."""
Gilles Peskine49af2d32019-12-06 19:20:13 +0100253 with read_file_lines(filename, binary=True) as lines:
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200254 for line in lines:
Gilles Peskine49af2d32019-12-06 19:20:13 +0100255 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
Gilles Peskine24827022018-09-25 18:49:23 +0200256 self.parse_header_line(line)
257
Gilles Peskine49af2d32019-12-06 19:20:13 +0100258 _macro_identifier_re = re.compile(r'[A-Z]\w+')
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100259 def generate_undeclared_names(self, expr):
260 for name in re.findall(self._macro_identifier_re, expr):
261 if name not in self.all_declared:
262 yield name
263
264 def accept_test_case_line(self, function, argument):
265 #pylint: disable=unused-argument
266 undeclared = list(self.generate_undeclared_names(argument))
267 if undeclared:
268 raise Exception('Undeclared names in test case', undeclared)
269 return True
270
Gilles Peskine24827022018-09-25 18:49:23 +0200271 def add_test_case_line(self, function, argument):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200272 """Parse a test case data line, looking for algorithm metadata tests."""
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100273 sets = []
Gilles Peskine24827022018-09-25 18:49:23 +0200274 if function.endswith('_algorithm'):
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100275 sets.append(self.algorithms)
Gilles Peskine79616682019-11-21 20:08:10 +0100276 if function == 'key_agreement_algorithm' and \
277 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
278 # We only want *raw* key agreement algorithms as such, so
279 # exclude ones that are already chained with a KDF.
280 # Keep the expression as one to test as an algorithm.
281 function = 'other_algorithm'
Gilles Peskine8fa13482019-11-25 17:10:12 +0100282 sets += self.table_by_test_function[function]
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100283 if self.accept_test_case_line(function, argument):
284 for s in sets:
285 s.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200286
287 # Regex matching a *.data line containing a test function call and
288 # its arguments. The actual definition is partly positional, but this
289 # regex is good enough in practice.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200290 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
Gilles Peskine24827022018-09-25 18:49:23 +0200291 def parse_test_cases(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200292 """Parse a test case file (*.data), looking for algorithm metadata tests."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200293 with read_file_lines(filename) as lines:
294 for line in lines:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200295 m = re.match(self._test_case_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200296 if m:
297 self.add_test_case_line(m.group(1), m.group(2))
298
Gilles Peskine84a45812019-11-21 19:50:33 +0100299def gather_inputs(headers, test_suites, inputs_class=Inputs):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200300 """Read the list of inputs to test psa_constant_names with."""
Gilles Peskine84a45812019-11-21 19:50:33 +0100301 inputs = inputs_class()
Gilles Peskine24827022018-09-25 18:49:23 +0200302 for header in headers:
303 inputs.parse_header(header)
304 for test_cases in test_suites:
305 inputs.parse_test_cases(test_cases)
306 inputs.gather_arguments()
307 return inputs
308
309def remove_file_if_exists(filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200310 """Remove the specified file, ignoring errors."""
Gilles Peskine24827022018-09-25 18:49:23 +0200311 if not filename:
312 return
313 try:
314 os.remove(filename)
Gilles Peskine54f54452019-05-27 18:31:59 +0200315 except OSError:
Gilles Peskine24827022018-09-25 18:49:23 +0200316 pass
317
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100318def run_c(type_word, expressions, include_path=None, keep_c=False):
Gilles Peskine5a994c12019-11-21 16:46:51 +0100319 """Generate and run a program to print out numerical values for expressions."""
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100320 if include_path is None:
321 include_path = []
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200322 if type_word == 'status':
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100323 cast_to = 'long'
324 printf_format = '%ld'
325 else:
326 cast_to = 'unsigned long'
327 printf_format = '0x%08lx'
Gilles Peskine24827022018-09-25 18:49:23 +0200328 c_name = None
329 exe_name = None
330 try:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200331 c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(type_word),
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100332 suffix='.c',
Gilles Peskine24827022018-09-25 18:49:23 +0200333 dir='programs/psa')
334 exe_suffix = '.exe' if platform.system() == 'Windows' else ''
335 exe_name = c_name[:-2] + exe_suffix
336 remove_file_if_exists(exe_name)
337 c_file = os.fdopen(c_fd, 'w', encoding='ascii')
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100338 c_file.write('/* Generated by test_psa_constant_names.py for {} values */'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200339 .format(type_word))
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100340 c_file.write('''
Gilles Peskine24827022018-09-25 18:49:23 +0200341#include <stdio.h>
342#include <psa/crypto.h>
343int main(void)
344{
345''')
Gilles Peskine5a994c12019-11-21 16:46:51 +0100346 for expr in expressions:
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100347 c_file.write(' printf("{}\\n", ({}) {});\n'
Gilles Peskine5a994c12019-11-21 16:46:51 +0100348 .format(printf_format, cast_to, expr))
Gilles Peskine24827022018-09-25 18:49:23 +0200349 c_file.write(''' return 0;
350}
351''')
352 c_file.close()
353 cc = os.getenv('CC', 'cc')
354 subprocess.check_call([cc] +
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100355 ['-I' + dir for dir in include_path] +
Gilles Peskine24827022018-09-25 18:49:23 +0200356 ['-o', exe_name, c_name])
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100357 if keep_c:
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200358 sys.stderr.write('List of {} tests kept at {}\n'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200359 .format(type_word, c_name))
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200360 else:
361 os.remove(c_name)
Gilles Peskine24827022018-09-25 18:49:23 +0200362 output = subprocess.check_output([exe_name])
363 return output.decode('ascii').strip().split('\n')
364 finally:
365 remove_file_if_exists(exe_name)
366
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200367NORMALIZE_STRIP_RE = re.compile(r'\s+')
Gilles Peskine24827022018-09-25 18:49:23 +0200368def normalize(expr):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200369 """Normalize the C expression so as not to care about trivial differences.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100370
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200371 Currently "trivial differences" means whitespace.
372 """
Gilles Peskine5a6dc892019-11-21 16:48:07 +0100373 return re.sub(NORMALIZE_STRIP_RE, '', expr)
Gilles Peskine24827022018-09-25 18:49:23 +0200374
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100375def collect_values(inputs, type_word, include_path=None, keep_c=False):
Gilles Peskinec2317112019-11-21 17:17:39 +0100376 """Generate expressions using known macro names and calculate their values.
377
378 Return a list of pairs of (expr, value) where expr is an expression and
379 value is a string representation of its integer value.
380 """
381 names = inputs.get_names(type_word)
382 expressions = sorted(inputs.generate_expressions(names))
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100383 values = run_c(type_word, expressions,
384 include_path=include_path, keep_c=keep_c)
Gilles Peskinec2317112019-11-21 17:17:39 +0100385 return expressions, values
386
Gilles Peskine24609332019-11-21 17:44:21 +0100387class Tests:
388 """An object representing tests and their results."""
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100389
Gilles Peskinea5000f12019-11-21 17:51:11 +0100390 Error = namedtuple('Error',
391 ['type', 'expression', 'value', 'output'])
392
Gilles Peskine24609332019-11-21 17:44:21 +0100393 def __init__(self, options):
394 self.options = options
395 self.count = 0
396 self.errors = []
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100397
Gilles Peskine24609332019-11-21 17:44:21 +0100398 def run_one(self, inputs, type_word):
399 """Test psa_constant_names for the specified type.
Gilles Peskine24827022018-09-25 18:49:23 +0200400
Gilles Peskine24609332019-11-21 17:44:21 +0100401 Run the program on the names for this type.
402 Use the inputs to figure out what arguments to pass to macros that
403 take arguments.
404 """
405 expressions, values = collect_values(inputs, type_word,
406 include_path=self.options.include,
407 keep_c=self.options.keep_c)
408 output = subprocess.check_output([self.options.program, type_word] +
409 values)
410 outputs = output.decode('ascii').strip().split('\n')
411 self.count += len(expressions)
412 for expr, value, output in zip(expressions, values, outputs):
Gilles Peskine32558482019-12-03 19:03:35 +0100413 if self.options.show:
414 sys.stdout.write('{} {}\t{}\n'.format(type_word, value, output))
Gilles Peskine24609332019-11-21 17:44:21 +0100415 if normalize(expr) != normalize(output):
Gilles Peskinea5000f12019-11-21 17:51:11 +0100416 self.errors.append(self.Error(type=type_word,
417 expression=expr,
418 value=value,
419 output=output))
Gilles Peskine24827022018-09-25 18:49:23 +0200420
Gilles Peskine24609332019-11-21 17:44:21 +0100421 def run_all(self, inputs):
422 """Run psa_constant_names on all the gathered inputs."""
423 for type_word in ['status', 'algorithm', 'ecc_curve', 'dh_group',
424 'key_type', 'key_usage']:
425 self.run_one(inputs, type_word)
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100426
Gilles Peskine24609332019-11-21 17:44:21 +0100427 def report(self, out):
428 """Describe each case where the output is not as expected.
429
430 Write the errors to ``out``.
431 Also write a total.
432 """
Gilles Peskinea5000f12019-11-21 17:51:11 +0100433 for error in self.errors:
Gilles Peskine24609332019-11-21 17:44:21 +0100434 out.write('For {} "{}", got "{}" (value: {})\n'
Gilles Peskinea5000f12019-11-21 17:51:11 +0100435 .format(error.type, error.expression,
436 error.output, error.value))
Gilles Peskine24609332019-11-21 17:44:21 +0100437 out.write('{} test cases'.format(self.count))
438 if self.errors:
439 out.write(', {} FAIL\n'.format(len(self.errors)))
440 else:
441 out.write(' PASS\n')
Gilles Peskine24827022018-09-25 18:49:23 +0200442
Gilles Peskine69f93b52019-11-21 16:49:50 +0100443HEADERS = ['psa/crypto.h', 'psa/crypto_extra.h', 'psa/crypto_values.h']
444TEST_SUITES = ['tests/suites/test_suite_psa_crypto_metadata.data']
445
Gilles Peskine54f54452019-05-27 18:31:59 +0200446def main():
Gilles Peskine24827022018-09-25 18:49:23 +0200447 parser = argparse.ArgumentParser(description=globals()['__doc__'])
448 parser.add_argument('--include', '-I',
449 action='append', default=['include'],
450 help='Directory for header files')
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200451 parser.add_argument('--keep-c',
452 action='store_true', dest='keep_c', default=False,
453 help='Keep the intermediate C file')
454 parser.add_argument('--no-keep-c',
455 action='store_false', dest='keep_c',
456 help='Don\'t keep the intermediate C file (default)')
Gilles Peskine8f5a5012019-11-21 16:49:10 +0100457 parser.add_argument('--program',
458 default='programs/psa/psa_constant_names',
459 help='Program to test')
Gilles Peskine32558482019-12-03 19:03:35 +0100460 parser.add_argument('--show',
461 action='store_true',
462 help='Keep the intermediate C file')
463 parser.add_argument('--no-show',
464 action='store_false', dest='show',
465 help='Don\'t show tested values (default)')
Gilles Peskine24827022018-09-25 18:49:23 +0200466 options = parser.parse_args()
Gilles Peskine69f93b52019-11-21 16:49:50 +0100467 headers = [os.path.join(options.include[0], h) for h in HEADERS]
468 inputs = gather_inputs(headers, TEST_SUITES)
Gilles Peskine24609332019-11-21 17:44:21 +0100469 tests = Tests(options)
470 tests.run_all(inputs)
471 tests.report(sys.stdout)
472 if tests.errors:
Gilles Peskine8b022352020-03-24 18:36:56 +0100473 sys.exit(1)
Gilles Peskine54f54452019-05-27 18:31:59 +0200474
475if __name__ == '__main__':
476 main()