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