blob: daae6b057adf1e85714e8eac15fb6602c758cdca [file] [log] [blame]
Gilles Peskineb39e3ec2019-01-29 08:50:20 +01001#!/usr/bin/env python3
2
3# Copyright (c) 2018, Arm Limited, All Rights Reserved.
4# SPDX-License-Identifier: Apache-2.0
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17#
18# This file is part of Mbed TLS (https://tls.mbed.org)
19
20"""Test Mbed TLS with a subset of algorithms.
21"""
22
23import argparse
24import os
25import re
26import shutil
27import subprocess
28import sys
29import traceback
30
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +010031class Colors:
32 """Minimalistic support for colored output.
33Each field of an object of this class is either None if colored output
34is not possible or not desired, or a pair of strings (start, stop) such
35that outputting start switches the text color to the desired color and
36stop switches the text color back to the default."""
37 red = None
38 green = None
39 bold_red = None
40 bold_green = None
41 def __init__(self, options=None):
42 if not options or options.color in ['no', 'never']:
43 want_color = False
44 elif options.color in ['yes', 'always']:
45 want_color = True
46 else:
47 want_color = sys.stderr.isatty()
48 if want_color:
49 # Assume ANSI compatible terminal
50 normal = '\033[0m'
51 self.red = ('\033[31m', normal)
52 self.green = ('\033[32m', normal)
53 self.bold_red = ('\033[1;31m', normal)
54 self.bold_green = ('\033[1;32m', normal)
55NO_COLORS = Colors(None)
56
57def log_line(text, prefix='depends.py:', suffix='', color=None):
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010058 """Print a status message."""
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +010059 if color != None:
60 prefix = color[0] + prefix
61 suffix = suffix + color[1]
62 sys.stderr.write(prefix + ' ' + text + suffix + '\n')
Gilles Peskine46c82562019-01-29 18:42:55 +010063 sys.stderr.flush()
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010064
Gilles Peskine54aa5c62019-01-29 18:46:34 +010065def log_command(cmd):
66 """Print a trace of the specified command.
67cmd is a list of strings: a command name and its arguments."""
68 log_line(' '.join(cmd), prefix='+')
69
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010070def backup_config(options):
Gilles Peskinebf7537d2019-01-29 18:52:16 +010071 """Back up the library configuration file (config.h).
72If the backup file already exists, it is presumed to be the desired backup,
73so don't make another backup."""
74 if os.path.exists(options.config_backup):
75 options.own_backup = False
76 else:
77 options.own_backup = True
78 shutil.copy(options.config, options.config_backup)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010079
Gilles Peskinebf7537d2019-01-29 18:52:16 +010080def restore_config(options):
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010081 """Restore the library configuration file (config.h).
Gilles Peskinebf7537d2019-01-29 18:52:16 +010082Remove the backup file if it was saved earlier."""
83 if options.own_backup:
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010084 shutil.move(options.config_backup, options.config)
85 else:
86 shutil.copy(options.config_backup, options.config)
Gilles Peskinebf7537d2019-01-29 18:52:16 +010087
Gilles Peskine54aa5c62019-01-29 18:46:34 +010088def run_config_pl(options, args):
89 """Run scripts/config.pl with the specified arguments."""
90 cmd = ['scripts/config.pl']
91 if options.config != 'include/mbedtls/config.h':
92 cmd += ['--file', options.config]
93 cmd += args
94 log_command(cmd)
95 subprocess.check_call(cmd)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010096
97class Job:
98 """A job builds the library in a specific configuration and runs some tests."""
99 def __init__(self, name, config_settings, commands):
100 """Build a job object.
101The job uses the configuration described by config_settings. This is a
102dictionary where the keys are preprocessor symbols and the values are
103booleans or strings. A boolean indicates whether or not to #define the
104symbol. With a string, the symbol is #define'd to that value.
105After setting the configuration, the job runs the programs specified by
106commands. This is a list of lists of strings; each list of string is a
107command name and its arguments and is passed to subprocess.call with
108shell=False."""
109 self.name = name
110 self.config_settings = config_settings
111 self.commands = commands
112
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100113 def announce(self, colors, what):
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100114 '''Announce the start or completion of a job.
115If what is None, announce the start of the job.
116If what is True, announce that the job has passed.
117If what is False, announce that the job has failed.'''
118 if what is True:
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100119 log_line(self.name + ' PASSED', color=colors.green)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100120 elif what is False:
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100121 log_line(self.name + ' FAILED', color=colors.red)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100122 else:
123 log_line('starting ' + self.name)
124
Gilles Peskinebf7537d2019-01-29 18:52:16 +0100125 def set_reference_config(self, options):
126 """Change the library configuration file (config.h) to the reference state.
127 The reference state is the one from which the tested configurations are
128 derived."""
129 # Turn off memory management options that are not relevant to
130 # the tests and slow them down.
131 run_config_pl(options, ['full'])
132 run_config_pl(options, ['unset', 'MBEDTLS_MEMORY_BACKTRACE'])
133 run_config_pl(options, ['unset', 'MBEDTLS_MEMORY_BUFFER_ALLOC_C'])
134 run_config_pl(options, ['unset', 'MBEDTLS_MEMORY_DEBUG'])
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100135
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100136 def configure(self, options):
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100137 '''Set library configuration options as required for the job.
138config_file_name indicates which file to modify.'''
Gilles Peskinebf7537d2019-01-29 18:52:16 +0100139 self.set_reference_config(options)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100140 for key, value in sorted(self.config_settings.items()):
141 if value is True:
142 args = ['set', key]
143 elif value is False:
144 args = ['unset', key]
145 else:
146 args = ['set', key, value]
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100147 run_config_pl(options, args)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100148
149 def test(self, options):
150 '''Run the job's build and test commands.
151Return True if all the commands succeed and False otherwise.
152If options.keep_going is false, stop as soon as one command fails. Otherwise
153run all the commands, except that if the first command fails, none of the
154other commands are run (typically, the first command is a build command
155and subsequent commands are tests that cannot run if the build failed).'''
156 built = False
157 success = True
158 for command in self.commands:
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100159 log_command(command)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100160 ret = subprocess.call(command)
161 if ret != 0:
162 if command[0] not in ['make', options.make_command]:
163 log_line('*** [{}] Error {}'.format(' '.join(command), ret))
164 if not options.keep_going or not built:
165 return False
166 success = False
167 built = True
168 return success
169
170# SSL/TLS versions up to 1.1 and corresponding options. These require
171# both MD5 and SHA-1.
172ssl_pre_1_2_dependencies = ['MBEDTLS_SSL_CBC_RECORD_SPLITTING',
173 'MBEDTLS_SSL_PROTO_SSL3',
174 'MBEDTLS_SSL_PROTO_TLS1',
175 'MBEDTLS_SSL_PROTO_TLS1_1']
176
177# If the configuration option A requires B, make sure that
178# B in reverse_dependencies[A].
179reverse_dependencies = {
180 'MBEDTLS_ECDSA_C': ['MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED'],
181 'MBEDTLS_ECP_C': ['MBEDTLS_ECDSA_C',
182 'MBEDTLS_ECDH_C',
183 'MBEDTLS_ECJPAKE_C',
184 'MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
185 'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED',
186 'MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED',
187 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
188 'MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED'],
189 'MBEDTLS_MD5_C': ssl_pre_1_2_dependencies,
190 'MBEDTLS_PKCS1_V21': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
191 'MBEDTLS_PKCS1_V15': ['MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
192 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
193 'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
194 'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'],
195 'MBEDTLS_RSA_C': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT',
196 'MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
197 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
198 'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
199 'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'],
200 'MBEDTLS_SHA1_C': ssl_pre_1_2_dependencies,
201 'MBEDTLS_X509_RSASSA_PSS_SUPPORT': [],
202}
203
204def turn_off_dependencies(config_settings):
205 """For every option turned off config_settings, also turn off what depends on it.
206An option O is turned off if config_settings[O] is False."""
207 for key, value in sorted(config_settings.items()):
208 if value is not False:
209 continue
210 for dep in reverse_dependencies.get(key, []):
211 config_settings[dep] = False
212
213class Domain:
214 """A domain is a set of jobs that all relate to a particular configuration aspect."""
215 pass
216
217class ExclusiveDomain(Domain):
218 """A domain consisting of a set of conceptually-equivalent settings.
219Establish a list of configuration symbols. For each symbol, run a test job
220with this symbol set and the others unset, and a test job with this symbol
221unset and the others set."""
222 def __init__(self, symbols, commands):
223 self.jobs = []
224 for invert in [False, True]:
225 base_config_settings = {}
226 for symbol in symbols:
227 base_config_settings[symbol] = invert
228 for symbol in symbols:
229 description = '!' + symbol if invert else symbol
230 config_settings = base_config_settings.copy()
231 config_settings[symbol] = not invert
232 turn_off_dependencies(config_settings)
233 job = Job(description, config_settings, commands)
234 self.jobs.append(job)
235
236class ComplementaryDomain:
237 """A domain consisting of a set of loosely-related settings.
238Establish a list of configuration symbols. For each symbol, run a test job
239with this symbol unset."""
240 def __init__(self, symbols, commands):
241 self.jobs = []
242 for symbol in symbols:
243 description = '!' + symbol
244 config_settings = {symbol: False}
245 turn_off_dependencies(config_settings)
246 job = Job(description, config_settings, commands)
247 self.jobs.append(job)
248
249class DomainData:
250 """Collect data about the library."""
251 def collect_config_symbols(self, options):
252 """Read the list of settings from config.h.
253Return them in a generator."""
254 with open(options.config) as config_file:
255 rx = re.compile(r'\s*(?://\s*)?#define\s+(\w+)\s*(?:$|/[/*])')
256 for line in config_file:
257 m = re.match(rx, line)
258 if m:
259 yield m.group(1)
260
261 def config_symbols_matching(self, regexp):
262 """List the config.h settings matching regexp."""
263 return [symbol for symbol in self.all_config_symbols
264 if re.match(regexp, symbol)]
265
266 def __init__(self, options):
267 """Gather data about the library and establish a list of domains to test."""
268 build_command = [options.make_command, 'CFLAGS=-Werror']
269 build_and_test = [build_command, [options.make_command, 'test']]
270 self.all_config_symbols = set(self.collect_config_symbols(options))
271 # Find hash modules by name.
272 hash_symbols = self.config_symbols_matching(r'MBEDTLS_(MD|RIPEMD|SHA)[0-9]+_C\Z')
273 # Find elliptic curve enabling macros by name.
274 curve_symbols = self.config_symbols_matching(r'MBEDTLS_ECP_DP_\w+_ENABLED\Z')
275 # Find key exchange enabling macros by name.
276 key_exchange_symbols = self.config_symbols_matching(r'MBEDTLS_KEY_EXCHANGE_\w+_ENABLED\Z')
277 self.domains = {
278 # Elliptic curves. Run the test suites.
279 'curves': ExclusiveDomain(curve_symbols, build_and_test),
280 # Hash algorithms. Exclude configurations with only one
281 # hash which is obsolete. Run the test suites.
282 'hashes': ExclusiveDomain(hash_symbols, build_and_test),
283 # Key exchange types. Just check the build.
284 'kex': ExclusiveDomain(key_exchange_symbols, [build_command]),
285 # Public-key algorithms. Run the test suites.
286 'pkalgs': ComplementaryDomain(['MBEDTLS_ECDSA_C',
287 'MBEDTLS_ECP_C',
288 'MBEDTLS_PKCS1_V21',
289 'MBEDTLS_PKCS1_V15',
290 'MBEDTLS_RSA_C',
291 'MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
292 build_and_test),
293 }
294 self.jobs = {}
295 for domain in self.domains.values():
296 for job in domain.jobs:
297 self.jobs[job.name] = job
298
299 def get_jobs(self, name):
300 """Return the list of jobs identified by the given name.
301A name can either be the name of a domain or the name of one specific job."""
302 if name in self.domains:
303 return sorted(self.domains[name].jobs, key=lambda job: job.name)
304 else:
305 return [self.jobs[name]]
306
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100307def run(options, job, colors=NO_COLORS):
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100308 """Run the specified job (a Job instance)."""
309 subprocess.check_call([options.make_command, 'clean'])
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100310 job.announce(colors, None)
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100311 job.configure(options)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100312 success = job.test(options)
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100313 job.announce(colors, success)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100314 return success
315
316def main(options, domain_data):
317 """Run the desired jobs.
318domain_data should be a DomainData instance that describes the available
319domains and jobs.
320Run the jobs listed in options.domains."""
321 if not hasattr(options, 'config_backup'):
322 options.config_backup = options.config + '.bak'
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100323 colors = Colors(options)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100324 jobs = []
325 failures = []
326 successes = []
327 for name in options.domains:
328 jobs += domain_data.get_jobs(name)
329 backup_config(options)
330 try:
331 for job in jobs:
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100332 success = run(options, job, colors=colors)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100333 if not success:
334 if options.keep_going:
335 failures.append(job.name)
336 else:
337 return False
338 else:
339 successes.append(job.name)
Gilles Peskinebf7537d2019-01-29 18:52:16 +0100340 restore_config(options)
341 except:
342 # Restore the configuration, except in stop-on-error mode if there
343 # was an error, where we leave the failing configuration up for
344 # developer convenience.
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100345 if options.keep_going:
Gilles Peskinebf7537d2019-01-29 18:52:16 +0100346 restore_config(options)
347 raise
Gilles Peskinee85163b2019-01-29 18:50:03 +0100348 if successes:
349 log_line('{} passed'.format(' '.join(successes)), color=colors.bold_green)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100350 if failures:
Gilles Peskinee85163b2019-01-29 18:50:03 +0100351 log_line('{} FAILED'.format(' '.join(failures)), color=colors.bold_red)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100352 return False
353 else:
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100354 return True
355
356
357if __name__ == '__main__':
358 try:
359 parser = argparse.ArgumentParser(description=__doc__)
Gilles Peskine0fa7cbe2019-01-29 18:48:48 +0100360 parser.add_argument('--color', metavar='WHEN',
361 help='Colorize the output (always/auto/never)',
362 choices=['always', 'auto', 'never'], default='auto')
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100363 parser.add_argument('-c', '--config', metavar='FILE',
364 help='Configuration file to modify',
365 default='include/mbedtls/config.h')
366 parser.add_argument('-C', '--directory', metavar='DIR',
367 help='Change to this directory before anything else',
368 default='.')
369 parser.add_argument('-k', '--keep-going',
370 help='Try all configurations even if some fail (default)',
371 action='store_true', dest='keep_going', default=True)
372 parser.add_argument('-e', '--no-keep-going',
373 help='Stop as soon as a configuration fails',
374 action='store_false', dest='keep_going')
375 parser.add_argument('--list-jobs',
376 help='List supported jobs and exit',
377 action='append_const', dest='list', const='jobs')
378 parser.add_argument('--list-domains',
379 help='List supported domains and exit',
380 action='append_const', dest='list', const='domains')
381 parser.add_argument('--make-command', metavar='CMD',
382 help='Command to run instead of make (e.g. gmake)',
383 action='store', default='make')
384 parser.add_argument('domains', metavar='DOMAIN', nargs='*',
385 help='The domain(s) to test (default: all)',
386 default=True)
387 options = parser.parse_args()
388 os.chdir(options.directory)
389 domain_data = DomainData(options)
390 if options.domains == True:
391 options.domains = sorted(domain_data.domains.keys())
392 if options.list:
393 for what in options.list:
394 for key in sorted(getattr(domain_data, what).keys()):
395 print(key)
396 exit(0)
397 else:
398 sys.exit(0 if main(options, domain_data) else 1)
399 except SystemExit:
400 raise
401 except:
402 traceback.print_exc()
403 exit(3)