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