blob: 6c55676c7fe383f1bc66be0b81419a4ebab52170 [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
31def log_line(text, prefix='depends.py'):
32 """Print a status message."""
33 sys.stderr.write(prefix + ' ' + text + '\n')
Gilles Peskine46c82562019-01-29 18:42:55 +010034 sys.stderr.flush()
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010035
Gilles Peskine54aa5c62019-01-29 18:46:34 +010036def log_command(cmd):
37 """Print a trace of the specified command.
38cmd is a list of strings: a command name and its arguments."""
39 log_line(' '.join(cmd), prefix='+')
40
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010041def backup_config(options):
42 """Back up the library configuration file (config.h)."""
43 shutil.copy(options.config, options.config_backup)
44
45def restore_config(options, done=False):
46 """Restore the library configuration file (config.h).
47If done is true, remove the backup file."""
48 if done:
49 shutil.move(options.config_backup, options.config)
50 else:
51 shutil.copy(options.config_backup, options.config)
Gilles Peskine54aa5c62019-01-29 18:46:34 +010052def run_config_pl(options, args):
53 """Run scripts/config.pl with the specified arguments."""
54 cmd = ['scripts/config.pl']
55 if options.config != 'include/mbedtls/config.h':
56 cmd += ['--file', options.config]
57 cmd += args
58 log_command(cmd)
59 subprocess.check_call(cmd)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010060
61class Job:
62 """A job builds the library in a specific configuration and runs some tests."""
63 def __init__(self, name, config_settings, commands):
64 """Build a job object.
65The job uses the configuration described by config_settings. This is a
66dictionary where the keys are preprocessor symbols and the values are
67booleans or strings. A boolean indicates whether or not to #define the
68symbol. With a string, the symbol is #define'd to that value.
69After setting the configuration, the job runs the programs specified by
70commands. This is a list of lists of strings; each list of string is a
71command name and its arguments and is passed to subprocess.call with
72shell=False."""
73 self.name = name
74 self.config_settings = config_settings
75 self.commands = commands
76
77 def announce(self, what):
78 '''Announce the start or completion of a job.
79If what is None, announce the start of the job.
80If what is True, announce that the job has passed.
81If what is False, announce that the job has failed.'''
82 if what is True:
83 log_line(self.name + ' PASSED')
84 elif what is False:
85 log_line(self.name + ' FAILED')
86 else:
87 log_line('starting ' + self.name)
88
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010089
Gilles Peskine54aa5c62019-01-29 18:46:34 +010090 def configure(self, options):
Gilles Peskineb39e3ec2019-01-29 08:50:20 +010091 '''Set library configuration options as required for the job.
92config_file_name indicates which file to modify.'''
93 for key, value in sorted(self.config_settings.items()):
94 if value is True:
95 args = ['set', key]
96 elif value is False:
97 args = ['unset', key]
98 else:
99 args = ['set', key, value]
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100100 run_config_pl(options, args)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100101
102 def test(self, options):
103 '''Run the job's build and test commands.
104Return True if all the commands succeed and False otherwise.
105If options.keep_going is false, stop as soon as one command fails. Otherwise
106run all the commands, except that if the first command fails, none of the
107other commands are run (typically, the first command is a build command
108and subsequent commands are tests that cannot run if the build failed).'''
109 built = False
110 success = True
111 for command in self.commands:
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100112 log_command(command)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100113 ret = subprocess.call(command)
114 if ret != 0:
115 if command[0] not in ['make', options.make_command]:
116 log_line('*** [{}] Error {}'.format(' '.join(command), ret))
117 if not options.keep_going or not built:
118 return False
119 success = False
120 built = True
121 return success
122
123# SSL/TLS versions up to 1.1 and corresponding options. These require
124# both MD5 and SHA-1.
125ssl_pre_1_2_dependencies = ['MBEDTLS_SSL_CBC_RECORD_SPLITTING',
126 'MBEDTLS_SSL_PROTO_SSL3',
127 'MBEDTLS_SSL_PROTO_TLS1',
128 'MBEDTLS_SSL_PROTO_TLS1_1']
129
130# If the configuration option A requires B, make sure that
131# B in reverse_dependencies[A].
132reverse_dependencies = {
133 'MBEDTLS_ECDSA_C': ['MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED'],
134 'MBEDTLS_ECP_C': ['MBEDTLS_ECDSA_C',
135 'MBEDTLS_ECDH_C',
136 'MBEDTLS_ECJPAKE_C',
137 'MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
138 'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED',
139 'MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED',
140 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
141 'MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED'],
142 'MBEDTLS_MD5_C': ssl_pre_1_2_dependencies,
143 'MBEDTLS_PKCS1_V21': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
144 'MBEDTLS_PKCS1_V15': ['MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
145 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
146 'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
147 'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'],
148 'MBEDTLS_RSA_C': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT',
149 'MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
150 'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
151 'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
152 'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'],
153 'MBEDTLS_SHA1_C': ssl_pre_1_2_dependencies,
154 'MBEDTLS_X509_RSASSA_PSS_SUPPORT': [],
155}
156
157def turn_off_dependencies(config_settings):
158 """For every option turned off config_settings, also turn off what depends on it.
159An option O is turned off if config_settings[O] is False."""
160 for key, value in sorted(config_settings.items()):
161 if value is not False:
162 continue
163 for dep in reverse_dependencies.get(key, []):
164 config_settings[dep] = False
165
166class Domain:
167 """A domain is a set of jobs that all relate to a particular configuration aspect."""
168 pass
169
170class ExclusiveDomain(Domain):
171 """A domain consisting of a set of conceptually-equivalent settings.
172Establish a list of configuration symbols. For each symbol, run a test job
173with this symbol set and the others unset, and a test job with this symbol
174unset and the others set."""
175 def __init__(self, symbols, commands):
176 self.jobs = []
177 for invert in [False, True]:
178 base_config_settings = {}
179 for symbol in symbols:
180 base_config_settings[symbol] = invert
181 for symbol in symbols:
182 description = '!' + symbol if invert else symbol
183 config_settings = base_config_settings.copy()
184 config_settings[symbol] = not invert
185 turn_off_dependencies(config_settings)
186 job = Job(description, config_settings, commands)
187 self.jobs.append(job)
188
189class ComplementaryDomain:
190 """A domain consisting of a set of loosely-related settings.
191Establish a list of configuration symbols. For each symbol, run a test job
192with this symbol unset."""
193 def __init__(self, symbols, commands):
194 self.jobs = []
195 for symbol in symbols:
196 description = '!' + symbol
197 config_settings = {symbol: False}
198 turn_off_dependencies(config_settings)
199 job = Job(description, config_settings, commands)
200 self.jobs.append(job)
201
202class DomainData:
203 """Collect data about the library."""
204 def collect_config_symbols(self, options):
205 """Read the list of settings from config.h.
206Return them in a generator."""
207 with open(options.config) as config_file:
208 rx = re.compile(r'\s*(?://\s*)?#define\s+(\w+)\s*(?:$|/[/*])')
209 for line in config_file:
210 m = re.match(rx, line)
211 if m:
212 yield m.group(1)
213
214 def config_symbols_matching(self, regexp):
215 """List the config.h settings matching regexp."""
216 return [symbol for symbol in self.all_config_symbols
217 if re.match(regexp, symbol)]
218
219 def __init__(self, options):
220 """Gather data about the library and establish a list of domains to test."""
221 build_command = [options.make_command, 'CFLAGS=-Werror']
222 build_and_test = [build_command, [options.make_command, 'test']]
223 self.all_config_symbols = set(self.collect_config_symbols(options))
224 # Find hash modules by name.
225 hash_symbols = self.config_symbols_matching(r'MBEDTLS_(MD|RIPEMD|SHA)[0-9]+_C\Z')
226 # Find elliptic curve enabling macros by name.
227 curve_symbols = self.config_symbols_matching(r'MBEDTLS_ECP_DP_\w+_ENABLED\Z')
228 # Find key exchange enabling macros by name.
229 key_exchange_symbols = self.config_symbols_matching(r'MBEDTLS_KEY_EXCHANGE_\w+_ENABLED\Z')
230 self.domains = {
231 # Elliptic curves. Run the test suites.
232 'curves': ExclusiveDomain(curve_symbols, build_and_test),
233 # Hash algorithms. Exclude configurations with only one
234 # hash which is obsolete. Run the test suites.
235 'hashes': ExclusiveDomain(hash_symbols, build_and_test),
236 # Key exchange types. Just check the build.
237 'kex': ExclusiveDomain(key_exchange_symbols, [build_command]),
238 # Public-key algorithms. Run the test suites.
239 'pkalgs': ComplementaryDomain(['MBEDTLS_ECDSA_C',
240 'MBEDTLS_ECP_C',
241 'MBEDTLS_PKCS1_V21',
242 'MBEDTLS_PKCS1_V15',
243 'MBEDTLS_RSA_C',
244 'MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
245 build_and_test),
246 }
247 self.jobs = {}
248 for domain in self.domains.values():
249 for job in domain.jobs:
250 self.jobs[job.name] = job
251
252 def get_jobs(self, name):
253 """Return the list of jobs identified by the given name.
254A name can either be the name of a domain or the name of one specific job."""
255 if name in self.domains:
256 return sorted(self.domains[name].jobs, key=lambda job: job.name)
257 else:
258 return [self.jobs[name]]
259
260def run(options, job):
261 """Run the specified job (a Job instance)."""
262 subprocess.check_call([options.make_command, 'clean'])
263 job.announce(None)
Gilles Peskine54aa5c62019-01-29 18:46:34 +0100264 job.configure(options)
Gilles Peskineb39e3ec2019-01-29 08:50:20 +0100265 success = job.test(options)
266 job.announce(success)
267 return success
268
269def main(options, domain_data):
270 """Run the desired jobs.
271domain_data should be a DomainData instance that describes the available
272domains and jobs.
273Run the jobs listed in options.domains."""
274 if not hasattr(options, 'config_backup'):
275 options.config_backup = options.config + '.bak'
276 jobs = []
277 failures = []
278 successes = []
279 for name in options.domains:
280 jobs += domain_data.get_jobs(name)
281 backup_config(options)
282 try:
283 for job in jobs:
284 success = run(options, job)
285 if not success:
286 if options.keep_going:
287 failures.append(job.name)
288 else:
289 return False
290 else:
291 successes.append(job.name)
292 restore_config(options)
293 finally:
294 if options.keep_going:
295 restore_config(options, True)
296 if failures:
297 if successes:
298 log_line('{} passed; {} FAILED'.format(' '.join(successes),
299 ' '.join(failures)))
300 else:
301 log_line('{} FAILED'.format(' '.join(failures)))
302 return False
303 else:
304 log_line('{} passed'.format(' '.join(successes)))
305 return True
306
307
308if __name__ == '__main__':
309 try:
310 parser = argparse.ArgumentParser(description=__doc__)
311 parser.add_argument('-c', '--config', metavar='FILE',
312 help='Configuration file to modify',
313 default='include/mbedtls/config.h')
314 parser.add_argument('-C', '--directory', metavar='DIR',
315 help='Change to this directory before anything else',
316 default='.')
317 parser.add_argument('-k', '--keep-going',
318 help='Try all configurations even if some fail (default)',
319 action='store_true', dest='keep_going', default=True)
320 parser.add_argument('-e', '--no-keep-going',
321 help='Stop as soon as a configuration fails',
322 action='store_false', dest='keep_going')
323 parser.add_argument('--list-jobs',
324 help='List supported jobs and exit',
325 action='append_const', dest='list', const='jobs')
326 parser.add_argument('--list-domains',
327 help='List supported domains and exit',
328 action='append_const', dest='list', const='domains')
329 parser.add_argument('--make-command', metavar='CMD',
330 help='Command to run instead of make (e.g. gmake)',
331 action='store', default='make')
332 parser.add_argument('domains', metavar='DOMAIN', nargs='*',
333 help='The domain(s) to test (default: all)',
334 default=True)
335 options = parser.parse_args()
336 os.chdir(options.directory)
337 domain_data = DomainData(options)
338 if options.domains == True:
339 options.domains = sorted(domain_data.domains.keys())
340 if options.list:
341 for what in options.list:
342 for key in sorted(getattr(domain_data, what).keys()):
343 print(key)
344 exit(0)
345 else:
346 sys.exit(0 if main(options, domain_data) else 1)
347 except SystemExit:
348 raise
349 except:
350 traceback.print_exc()
351 exit(3)