2019-01-29 08:50:20 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
# Copyright (c) 2018, Arm Limited, All Rights Reserved.
|
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
|
|
# not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
#
|
|
|
|
# This file is part of Mbed TLS (https://tls.mbed.org)
|
|
|
|
|
|
|
|
"""Test Mbed TLS with a subset of algorithms.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import shutil
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
def log_line(text, prefix='depends.py'):
|
|
|
|
"""Print a status message."""
|
|
|
|
sys.stderr.write(prefix + ' ' + text + '\n')
|
2019-01-29 18:42:55 +01:00
|
|
|
sys.stderr.flush()
|
2019-01-29 08:50:20 +01:00
|
|
|
|
|
|
|
def backup_config(options):
|
|
|
|
"""Back up the library configuration file (config.h)."""
|
|
|
|
shutil.copy(options.config, options.config_backup)
|
|
|
|
|
|
|
|
def restore_config(options, done=False):
|
|
|
|
"""Restore the library configuration file (config.h).
|
|
|
|
If done is true, remove the backup file."""
|
|
|
|
if done:
|
|
|
|
shutil.move(options.config_backup, options.config)
|
|
|
|
else:
|
|
|
|
shutil.copy(options.config_backup, options.config)
|
|
|
|
|
|
|
|
class Job:
|
|
|
|
"""A job builds the library in a specific configuration and runs some tests."""
|
|
|
|
def __init__(self, name, config_settings, commands):
|
|
|
|
"""Build a job object.
|
|
|
|
The job uses the configuration described by config_settings. This is a
|
|
|
|
dictionary where the keys are preprocessor symbols and the values are
|
|
|
|
booleans or strings. A boolean indicates whether or not to #define the
|
|
|
|
symbol. With a string, the symbol is #define'd to that value.
|
|
|
|
After setting the configuration, the job runs the programs specified by
|
|
|
|
commands. This is a list of lists of strings; each list of string is a
|
|
|
|
command name and its arguments and is passed to subprocess.call with
|
|
|
|
shell=False."""
|
|
|
|
self.name = name
|
|
|
|
self.config_settings = config_settings
|
|
|
|
self.commands = commands
|
|
|
|
|
|
|
|
def announce(self, what):
|
|
|
|
'''Announce the start or completion of a job.
|
|
|
|
If what is None, announce the start of the job.
|
|
|
|
If what is True, announce that the job has passed.
|
|
|
|
If what is False, announce that the job has failed.'''
|
|
|
|
if what is True:
|
|
|
|
log_line(self.name + ' PASSED')
|
|
|
|
elif what is False:
|
|
|
|
log_line(self.name + ' FAILED')
|
|
|
|
else:
|
|
|
|
log_line('starting ' + self.name)
|
|
|
|
|
|
|
|
def trace_command(self, cmd):
|
|
|
|
'''Print a trace of the specified command.
|
|
|
|
cmd is a list of strings: a command name and its arguments.'''
|
|
|
|
log_line(' '.join(cmd), prefix='+')
|
|
|
|
|
|
|
|
def configure(self, config_file_name):
|
|
|
|
'''Set library configuration options as required for the job.
|
|
|
|
config_file_name indicates which file to modify.'''
|
|
|
|
for key, value in sorted(self.config_settings.items()):
|
|
|
|
if value is True:
|
|
|
|
args = ['set', key]
|
|
|
|
elif value is False:
|
|
|
|
args = ['unset', key]
|
|
|
|
else:
|
|
|
|
args = ['set', key, value]
|
|
|
|
cmd = ['scripts/config.pl']
|
|
|
|
if config_file_name != 'include/mbedtls/config.h':
|
|
|
|
cmd += ['--file', config_file_name]
|
|
|
|
cmd += args
|
|
|
|
self.trace_command(cmd)
|
|
|
|
subprocess.check_call(cmd)
|
|
|
|
|
|
|
|
def test(self, options):
|
|
|
|
'''Run the job's build and test commands.
|
|
|
|
Return True if all the commands succeed and False otherwise.
|
|
|
|
If options.keep_going is false, stop as soon as one command fails. Otherwise
|
|
|
|
run all the commands, except that if the first command fails, none of the
|
|
|
|
other commands are run (typically, the first command is a build command
|
|
|
|
and subsequent commands are tests that cannot run if the build failed).'''
|
|
|
|
built = False
|
|
|
|
success = True
|
|
|
|
for command in self.commands:
|
|
|
|
self.trace_command(command)
|
|
|
|
ret = subprocess.call(command)
|
|
|
|
if ret != 0:
|
|
|
|
if command[0] not in ['make', options.make_command]:
|
|
|
|
log_line('*** [{}] Error {}'.format(' '.join(command), ret))
|
|
|
|
if not options.keep_going or not built:
|
|
|
|
return False
|
|
|
|
success = False
|
|
|
|
built = True
|
|
|
|
return success
|
|
|
|
|
|
|
|
# SSL/TLS versions up to 1.1 and corresponding options. These require
|
|
|
|
# both MD5 and SHA-1.
|
|
|
|
ssl_pre_1_2_dependencies = ['MBEDTLS_SSL_CBC_RECORD_SPLITTING',
|
|
|
|
'MBEDTLS_SSL_PROTO_SSL3',
|
|
|
|
'MBEDTLS_SSL_PROTO_TLS1',
|
|
|
|
'MBEDTLS_SSL_PROTO_TLS1_1']
|
|
|
|
|
|
|
|
# If the configuration option A requires B, make sure that
|
|
|
|
# B in reverse_dependencies[A].
|
|
|
|
reverse_dependencies = {
|
|
|
|
'MBEDTLS_ECDSA_C': ['MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED'],
|
|
|
|
'MBEDTLS_ECP_C': ['MBEDTLS_ECDSA_C',
|
|
|
|
'MBEDTLS_ECDH_C',
|
|
|
|
'MBEDTLS_ECJPAKE_C',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED'],
|
|
|
|
'MBEDTLS_MD5_C': ssl_pre_1_2_dependencies,
|
|
|
|
'MBEDTLS_PKCS1_V21': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
|
|
|
|
'MBEDTLS_PKCS1_V15': ['MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'],
|
|
|
|
'MBEDTLS_RSA_C': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_RSA_PSK_ENABLED',
|
|
|
|
'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED'],
|
|
|
|
'MBEDTLS_SHA1_C': ssl_pre_1_2_dependencies,
|
|
|
|
'MBEDTLS_X509_RSASSA_PSS_SUPPORT': [],
|
|
|
|
}
|
|
|
|
|
|
|
|
def turn_off_dependencies(config_settings):
|
|
|
|
"""For every option turned off config_settings, also turn off what depends on it.
|
|
|
|
An option O is turned off if config_settings[O] is False."""
|
|
|
|
for key, value in sorted(config_settings.items()):
|
|
|
|
if value is not False:
|
|
|
|
continue
|
|
|
|
for dep in reverse_dependencies.get(key, []):
|
|
|
|
config_settings[dep] = False
|
|
|
|
|
|
|
|
class Domain:
|
|
|
|
"""A domain is a set of jobs that all relate to a particular configuration aspect."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
class ExclusiveDomain(Domain):
|
|
|
|
"""A domain consisting of a set of conceptually-equivalent settings.
|
|
|
|
Establish a list of configuration symbols. For each symbol, run a test job
|
|
|
|
with this symbol set and the others unset, and a test job with this symbol
|
|
|
|
unset and the others set."""
|
|
|
|
def __init__(self, symbols, commands):
|
|
|
|
self.jobs = []
|
|
|
|
for invert in [False, True]:
|
|
|
|
base_config_settings = {}
|
|
|
|
for symbol in symbols:
|
|
|
|
base_config_settings[symbol] = invert
|
|
|
|
for symbol in symbols:
|
|
|
|
description = '!' + symbol if invert else symbol
|
|
|
|
config_settings = base_config_settings.copy()
|
|
|
|
config_settings[symbol] = not invert
|
|
|
|
turn_off_dependencies(config_settings)
|
|
|
|
job = Job(description, config_settings, commands)
|
|
|
|
self.jobs.append(job)
|
|
|
|
|
|
|
|
class ComplementaryDomain:
|
|
|
|
"""A domain consisting of a set of loosely-related settings.
|
|
|
|
Establish a list of configuration symbols. For each symbol, run a test job
|
|
|
|
with this symbol unset."""
|
|
|
|
def __init__(self, symbols, commands):
|
|
|
|
self.jobs = []
|
|
|
|
for symbol in symbols:
|
|
|
|
description = '!' + symbol
|
|
|
|
config_settings = {symbol: False}
|
|
|
|
turn_off_dependencies(config_settings)
|
|
|
|
job = Job(description, config_settings, commands)
|
|
|
|
self.jobs.append(job)
|
|
|
|
|
|
|
|
class DomainData:
|
|
|
|
"""Collect data about the library."""
|
|
|
|
def collect_config_symbols(self, options):
|
|
|
|
"""Read the list of settings from config.h.
|
|
|
|
Return them in a generator."""
|
|
|
|
with open(options.config) as config_file:
|
|
|
|
rx = re.compile(r'\s*(?://\s*)?#define\s+(\w+)\s*(?:$|/[/*])')
|
|
|
|
for line in config_file:
|
|
|
|
m = re.match(rx, line)
|
|
|
|
if m:
|
|
|
|
yield m.group(1)
|
|
|
|
|
|
|
|
def config_symbols_matching(self, regexp):
|
|
|
|
"""List the config.h settings matching regexp."""
|
|
|
|
return [symbol for symbol in self.all_config_symbols
|
|
|
|
if re.match(regexp, symbol)]
|
|
|
|
|
|
|
|
def __init__(self, options):
|
|
|
|
"""Gather data about the library and establish a list of domains to test."""
|
|
|
|
build_command = [options.make_command, 'CFLAGS=-Werror']
|
|
|
|
build_and_test = [build_command, [options.make_command, 'test']]
|
|
|
|
self.all_config_symbols = set(self.collect_config_symbols(options))
|
|
|
|
# Find hash modules by name.
|
|
|
|
hash_symbols = self.config_symbols_matching(r'MBEDTLS_(MD|RIPEMD|SHA)[0-9]+_C\Z')
|
|
|
|
# Find elliptic curve enabling macros by name.
|
|
|
|
curve_symbols = self.config_symbols_matching(r'MBEDTLS_ECP_DP_\w+_ENABLED\Z')
|
|
|
|
# Find key exchange enabling macros by name.
|
|
|
|
key_exchange_symbols = self.config_symbols_matching(r'MBEDTLS_KEY_EXCHANGE_\w+_ENABLED\Z')
|
|
|
|
self.domains = {
|
|
|
|
# Elliptic curves. Run the test suites.
|
|
|
|
'curves': ExclusiveDomain(curve_symbols, build_and_test),
|
|
|
|
# Hash algorithms. Exclude configurations with only one
|
|
|
|
# hash which is obsolete. Run the test suites.
|
|
|
|
'hashes': ExclusiveDomain(hash_symbols, build_and_test),
|
|
|
|
# Key exchange types. Just check the build.
|
|
|
|
'kex': ExclusiveDomain(key_exchange_symbols, [build_command]),
|
|
|
|
# Public-key algorithms. Run the test suites.
|
|
|
|
'pkalgs': ComplementaryDomain(['MBEDTLS_ECDSA_C',
|
|
|
|
'MBEDTLS_ECP_C',
|
|
|
|
'MBEDTLS_PKCS1_V21',
|
|
|
|
'MBEDTLS_PKCS1_V15',
|
|
|
|
'MBEDTLS_RSA_C',
|
|
|
|
'MBEDTLS_X509_RSASSA_PSS_SUPPORT'],
|
|
|
|
build_and_test),
|
|
|
|
}
|
|
|
|
self.jobs = {}
|
|
|
|
for domain in self.domains.values():
|
|
|
|
for job in domain.jobs:
|
|
|
|
self.jobs[job.name] = job
|
|
|
|
|
|
|
|
def get_jobs(self, name):
|
|
|
|
"""Return the list of jobs identified by the given name.
|
|
|
|
A name can either be the name of a domain or the name of one specific job."""
|
|
|
|
if name in self.domains:
|
|
|
|
return sorted(self.domains[name].jobs, key=lambda job: job.name)
|
|
|
|
else:
|
|
|
|
return [self.jobs[name]]
|
|
|
|
|
|
|
|
def run(options, job):
|
|
|
|
"""Run the specified job (a Job instance)."""
|
|
|
|
subprocess.check_call([options.make_command, 'clean'])
|
|
|
|
job.announce(None)
|
|
|
|
job.configure(options.config)
|
|
|
|
success = job.test(options)
|
|
|
|
job.announce(success)
|
|
|
|
return success
|
|
|
|
|
|
|
|
def main(options, domain_data):
|
|
|
|
"""Run the desired jobs.
|
|
|
|
domain_data should be a DomainData instance that describes the available
|
|
|
|
domains and jobs.
|
|
|
|
Run the jobs listed in options.domains."""
|
|
|
|
if not hasattr(options, 'config_backup'):
|
|
|
|
options.config_backup = options.config + '.bak'
|
|
|
|
jobs = []
|
|
|
|
failures = []
|
|
|
|
successes = []
|
|
|
|
for name in options.domains:
|
|
|
|
jobs += domain_data.get_jobs(name)
|
|
|
|
backup_config(options)
|
|
|
|
try:
|
|
|
|
for job in jobs:
|
|
|
|
success = run(options, job)
|
|
|
|
if not success:
|
|
|
|
if options.keep_going:
|
|
|
|
failures.append(job.name)
|
|
|
|
else:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
successes.append(job.name)
|
|
|
|
restore_config(options)
|
|
|
|
finally:
|
|
|
|
if options.keep_going:
|
|
|
|
restore_config(options, True)
|
|
|
|
if failures:
|
|
|
|
if successes:
|
|
|
|
log_line('{} passed; {} FAILED'.format(' '.join(successes),
|
|
|
|
' '.join(failures)))
|
|
|
|
else:
|
|
|
|
log_line('{} FAILED'.format(' '.join(failures)))
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
log_line('{} passed'.format(' '.join(successes)))
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
try:
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
|
|
parser.add_argument('-c', '--config', metavar='FILE',
|
|
|
|
help='Configuration file to modify',
|
|
|
|
default='include/mbedtls/config.h')
|
|
|
|
parser.add_argument('-C', '--directory', metavar='DIR',
|
|
|
|
help='Change to this directory before anything else',
|
|
|
|
default='.')
|
|
|
|
parser.add_argument('-k', '--keep-going',
|
|
|
|
help='Try all configurations even if some fail (default)',
|
|
|
|
action='store_true', dest='keep_going', default=True)
|
|
|
|
parser.add_argument('-e', '--no-keep-going',
|
|
|
|
help='Stop as soon as a configuration fails',
|
|
|
|
action='store_false', dest='keep_going')
|
|
|
|
parser.add_argument('--list-jobs',
|
|
|
|
help='List supported jobs and exit',
|
|
|
|
action='append_const', dest='list', const='jobs')
|
|
|
|
parser.add_argument('--list-domains',
|
|
|
|
help='List supported domains and exit',
|
|
|
|
action='append_const', dest='list', const='domains')
|
|
|
|
parser.add_argument('--make-command', metavar='CMD',
|
|
|
|
help='Command to run instead of make (e.g. gmake)',
|
|
|
|
action='store', default='make')
|
|
|
|
parser.add_argument('domains', metavar='DOMAIN', nargs='*',
|
|
|
|
help='The domain(s) to test (default: all)',
|
|
|
|
default=True)
|
|
|
|
options = parser.parse_args()
|
|
|
|
os.chdir(options.directory)
|
|
|
|
domain_data = DomainData(options)
|
|
|
|
if options.domains == True:
|
|
|
|
options.domains = sorted(domain_data.domains.keys())
|
|
|
|
if options.list:
|
|
|
|
for what in options.list:
|
|
|
|
for key in sorted(getattr(domain_data, what).keys()):
|
|
|
|
print(key)
|
|
|
|
exit(0)
|
|
|
|
else:
|
|
|
|
sys.exit(0 if main(options, domain_data) else 1)
|
|
|
|
except SystemExit:
|
|
|
|
raise
|
|
|
|
except:
|
|
|
|
traceback.print_exc()
|
|
|
|
exit(3)
|