mirror of
https://github.com/Mbed-TLS/mbedtls.git
synced 2025-01-28 00:35:21 +00:00
6763a5546a
Signed-off-by: Gabor Mezei <gabor.mezei@arm.com>
690 lines
32 KiB
Python
Executable File
690 lines
32 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright The Mbed TLS Contributors
|
|
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
|
|
|
|
"""
|
|
Test Mbed TLS with a subset of algorithms.
|
|
|
|
This script can be divided into several steps:
|
|
|
|
First, include/mbedtls/mbedtls_config.h or a different config file passed
|
|
in the arguments is parsed to extract any configuration options (using config.py).
|
|
|
|
Then, test domains (groups of jobs, tests) are built based on predefined data
|
|
collected in the DomainData class. Here, each domain has five major traits:
|
|
- domain name, can be used to run only specific tests via command-line;
|
|
- configuration building method, described in detail below;
|
|
- list of symbols passed to the configuration building method;
|
|
- commands to be run on each job (only build, build and test, or any other custom);
|
|
- optional list of symbols to be excluded from testing.
|
|
|
|
The configuration building method can be one of the three following:
|
|
|
|
- ComplementaryDomain - build a job for each passed symbol by disabling a single
|
|
symbol and its reverse dependencies (defined in REVERSE_DEPENDENCIES);
|
|
|
|
- ExclusiveDomain - build a job where, for each passed symbol, only this particular
|
|
one is defined and other symbols from the list are unset. For each job look for
|
|
any non-standard symbols to set/unset in EXCLUSIVE_GROUPS. These are usually not
|
|
direct dependencies, but rather non-trivial results of other configs missing. Then
|
|
look for any unset symbols and handle their reverse dependencies.
|
|
Examples of EXCLUSIVE_GROUPS usage:
|
|
- MBEDTLS_SHA512_C job turns off all hashes except SHA512. MBEDTLS_SSL_COOKIE_C
|
|
requires either SHA256 or SHA384 to work, so it also has to be disabled.
|
|
This is not a dependency on SHA512_C, but a result of an exclusive domain
|
|
config building method. Relevant field:
|
|
'MBEDTLS_SHA512_C': ['-MBEDTLS_SSL_COOKIE_C'],
|
|
|
|
- DualDomain - combination of the two above - both complementary and exclusive domain
|
|
job generation code will be run. Currently only used for hashes.
|
|
|
|
Lastly, the collected jobs are executed and (optionally) tested, with
|
|
error reporting and coloring as configured in options. Each test starts with
|
|
a full config without a couple of slowing down or unnecessary options
|
|
(see set_reference_config), then the specific job config is derived.
|
|
"""
|
|
import argparse
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import traceback
|
|
from typing import Union
|
|
|
|
# Add the Mbed TLS Python library directory to the module search path
|
|
import scripts_path # pylint: disable=unused-import
|
|
import config
|
|
from mbedtls_framework import c_build_helper
|
|
from mbedtls_framework import crypto_knowledge
|
|
from mbedtls_framework import psa_information
|
|
|
|
class Colors: # pylint: disable=too-few-public-methods
|
|
"""Minimalistic support for colored output.
|
|
Each field of an object of this class is either None if colored output
|
|
is not possible or not desired, or a pair of strings (start, stop) such
|
|
that outputting start switches the text color to the desired color and
|
|
stop switches the text color back to the default."""
|
|
red = None
|
|
green = None
|
|
cyan = None
|
|
bold_red = None
|
|
bold_green = None
|
|
def __init__(self, options=None):
|
|
"""Initialize color profile according to passed options."""
|
|
if not options or options.color in ['no', 'never']:
|
|
want_color = False
|
|
elif options.color in ['yes', 'always']:
|
|
want_color = True
|
|
else:
|
|
want_color = sys.stderr.isatty()
|
|
if want_color:
|
|
# Assume ANSI compatible terminal
|
|
normal = '\033[0m'
|
|
self.red = ('\033[31m', normal)
|
|
self.green = ('\033[32m', normal)
|
|
self.cyan = ('\033[36m', normal)
|
|
self.bold_red = ('\033[1;31m', normal)
|
|
self.bold_green = ('\033[1;32m', normal)
|
|
NO_COLORS = Colors(None)
|
|
|
|
def log_line(text, prefix='depends.py:', suffix='', color=None):
|
|
"""Print a status message."""
|
|
if color is not None:
|
|
prefix = color[0] + prefix
|
|
suffix = suffix + color[1]
|
|
sys.stderr.write(prefix + ' ' + text + suffix + '\n')
|
|
sys.stderr.flush()
|
|
|
|
def log_command(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 option_exists(conf, option):
|
|
return option in conf.settings
|
|
|
|
def set_config_option_value(conf, option, colors, value: Union[bool, str]):
|
|
"""Set/unset a configuration option, optionally specifying a value.
|
|
value can be either True/False (set/unset config option), or a string,
|
|
which will make a symbol defined with a certain value."""
|
|
if not option_exists(conf, option):
|
|
log_line('Symbol {} was not found in {}'.format(option, conf.filename), color=colors.red)
|
|
return False
|
|
|
|
if value is False:
|
|
log_command(['config.py', 'unset', option])
|
|
conf.unset(option)
|
|
elif value is True:
|
|
log_command(['config.py', 'set', option])
|
|
conf.set(option)
|
|
else:
|
|
log_command(['config.py', 'set', option, value])
|
|
conf.set(option, value)
|
|
return True
|
|
|
|
def set_reference_config(conf, colors):
|
|
"""Change the library configuration file (mbedtls_config.h) to the reference state.
|
|
The reference state is the one from which the tested configurations are
|
|
derived."""
|
|
# Turn off options that are not relevant to the tests and slow them down.
|
|
log_command(['config.py', 'full'])
|
|
conf.adapt(config.full_adapter)
|
|
set_config_option_value(conf, 'MBEDTLS_TEST_HOOKS', colors, False)
|
|
|
|
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, colors, 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', color=colors.green)
|
|
elif what is False:
|
|
log_line(self.name + ' FAILED', color=colors.red)
|
|
else:
|
|
log_line('starting ' + self.name, color=colors.cyan)
|
|
|
|
def configure(self, conf, colors):
|
|
'''Set library configuration options as required for the job.'''
|
|
set_reference_config(conf, colors)
|
|
for key, value in sorted(self.config_settings.items()):
|
|
ret = set_config_option_value(conf, key, colors, value)
|
|
if ret is False:
|
|
return False
|
|
return True
|
|
|
|
def _consistency_check(self):
|
|
'''Check if the testable option is consistent with the goal.
|
|
|
|
The purpose of this function to ensure that every option is set or unset according to
|
|
the settings.
|
|
'''
|
|
log_command(['consistency check'])
|
|
c_name = None
|
|
exe_name = None
|
|
header = '#include "mbedtls/build_info.h"\n'
|
|
|
|
# Generate a C error directive for each setting to test if it is active
|
|
for option, value in sorted(self.config_settings.items()):
|
|
header += '#if '
|
|
if value:
|
|
header += '!'
|
|
header += f'defined({option})\n'
|
|
header += f'#error "{option}"\n'
|
|
header += '#endif\n'
|
|
include_path = ['include', 'tf-psa-crypto/include',
|
|
'tf-psa-crypto/drivers/builtin/include']
|
|
|
|
try:
|
|
# Generate a C file, build and run it
|
|
c_file, c_name, exe_name = c_build_helper.create_c_file(self.name)
|
|
c_build_helper.generate_c_file(c_file, 'depends.py', header, lambda x: '')
|
|
c_file.close()
|
|
c_build_helper.compile_c_file(c_name, exe_name, include_path)
|
|
return True
|
|
|
|
except c_build_helper.CompileError as e:
|
|
# Read the command line output to find out which setting has been failed
|
|
failed = {m.group(1) for m in re.finditer('.*#error "(.*)"', e.message) if m}
|
|
log_line('Inconsistent config option(s):')
|
|
for option in sorted(failed):
|
|
log_line(' ' + option)
|
|
return False
|
|
|
|
finally:
|
|
c_build_helper.remove_file_if_exists(c_name)
|
|
c_build_helper.remove_file_if_exists(exe_name)
|
|
|
|
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).'''
|
|
if not self._consistency_check():
|
|
return False
|
|
built = False
|
|
success = True
|
|
for command in self.commands:
|
|
log_command(command)
|
|
env = os.environ.copy()
|
|
if 'MBEDTLS_TEST_CONFIGURATION' in env:
|
|
env['MBEDTLS_TEST_CONFIGURATION'] += '-' + self.name
|
|
ret = subprocess.call(command, env=env)
|
|
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
|
|
|
|
# If the configuration option A requires B, make sure that
|
|
# B in REVERSE_DEPENDENCIES[A].
|
|
# All the information here should be contained in check_config.h or check_crypto_config.h.
|
|
# This file includes a copy because it changes rarely and it would be a pain
|
|
# to extract automatically.
|
|
REVERSE_DEPENDENCIES = {
|
|
'PSA_WANT_KEY_TYPE_AES': ['PSA_WANT_ALG_PBKDF2_AES_CMAC_PRF_128',
|
|
'MBEDTLS_CTR_DRBG_C',
|
|
'MBEDTLS_NIST_KW_C',
|
|
'MBEDTLS_AES_C'],
|
|
'PSA_WANT_KEY_TYPE_ARIA': ['MBEDTLS_ARIA_C'],
|
|
'PSA_WANT_KEY_TYPE_CAMELLIA': ['MBEDTLS_CAMELLIA_C'],
|
|
'PSA_WANT_KEY_TYPE_CHACHA20': ['PSA_WANT_ALG_CHACHA20_POLY1305',
|
|
'PSA_WANT_ALG_STREAM_CIPHER',
|
|
'MBEDTLS_CHACHA20_C',
|
|
'MBEDTLS_CHACHAPOLY_C'],
|
|
'PSA_WANT_KEY_TYPE_DES': ['MBEDTLS_DES_C'],
|
|
'PSA_WANT_ALG_CCM': ['PSA_WANT_ALG_CCM_STAR_NO_TAG',
|
|
'MBEDTLS_CCM_C'],
|
|
'PSA_WANT_ALG_CMAC': ['PSA_WANT_ALG_PBKDF2_AES_CMAC_PRF_128',
|
|
'MBEDTLS_CMAC_C'],
|
|
'PSA_WANT_ALG_GCM': ['MBEDTLS_GCM_C'],
|
|
|
|
'PSA_WANT_ALG_CBC_NO_PADDING': ['MBEDTLS_CIPHER_MODE_CBC'],
|
|
'PSA_WANT_ALG_CBC_PKCS7': ['MBEDTLS_CIPHER_MODE_CBC'],
|
|
'PSA_WANT_ALG_CFB': ['MBEDTLS_CIPHER_MODE_CFB'],
|
|
'PSA_WANT_ALG_CTR': ['MBEDTLS_CIPHER_MODE_CTR'],
|
|
'PSA_WANT_ALG_OFB': ['MBEDTLS_CIPHER_MODE_OFB'],
|
|
'PSA_WANT_ALG_XTS': ['MBEDTLS_CIPHER_MODE_XTS'],
|
|
|
|
'MBEDTLS_CIPHER_PADDING_PKCS7': ['MBEDTLS_PKCS5_C',
|
|
'MBEDTLS_PKCS12_C',
|
|
'PSA_WANT_ALG_CBC_PKCS7'],
|
|
'MBEDTLS_CIPHER_PADDING_ONE_AND_ZEROS': ['MBEDTLS_CIPHER_MODE_CBC'],
|
|
'MBEDTLS_CIPHER_PADDING_ZEROS': ['MBEDTLS_CIPHER_MODE_CBC'],
|
|
'MBEDTLS_CIPHER_PADDING_ZEROS_AND_LEN': ['MBEDTLS_CIPHER_MODE_CBC'],
|
|
|
|
'MBEDTLS_ECP_DP_BP256R1_ENABLED': ['PSA_WANT_ECC_BRAINPOOL_P_R1_256'],
|
|
'MBEDTLS_ECP_DP_BP384R1_ENABLED': ['PSA_WANT_ECC_BRAINPOOL_P_R1_384'],
|
|
'MBEDTLS_ECP_DP_BP512R1_ENABLED': ['PSA_WANT_ECC_BRAINPOOL_P_R1_512'],
|
|
'MBEDTLS_ECP_DP_CURVE25519_ENABLED': ['PSA_WANT_ECC_MONTGOMERY_255'],
|
|
'MBEDTLS_ECP_DP_CURVE448_ENABLED': ['PSA_WANT_ECC_MONTGOMERY_448'],
|
|
'MBEDTLS_ECP_DP_SECP192R1_ENABLED': ['PSA_WANT_ECC_SECP_R1_192'],
|
|
'MBEDTLS_ECP_DP_SECP224R1_ENABLED': ['PSA_WANT_ECC_SECP_R1_224'],
|
|
'MBEDTLS_ECP_DP_SECP256R1_ENABLED': ['PSA_WANT_ECC_SECP_R1_256',
|
|
'PSA_WANT_ALG_JPAKE',
|
|
'MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED'],
|
|
'MBEDTLS_ECP_DP_SECP384R1_ENABLED': ['PSA_WANT_ECC_SECP_R1_384'],
|
|
'MBEDTLS_ECP_DP_SECP512R1_ENABLED': ['PSA_WANT_ECC_SECP_R1_512'],
|
|
'MBEDTLS_ECP_DP_SECP521R1_ENABLED': ['PSA_WANT_ECC_SECP_R1_521'],
|
|
'MBEDTLS_ECP_DP_SECP192K1_ENABLED': ['PSA_WANT_ECC_SECP_K1_192'],
|
|
'MBEDTLS_ECP_DP_SECP256K1_ENABLED': ['PSA_WANT_ECC_SECP_K1_256'],
|
|
|
|
'MBEDTLS_ECDSA_C': ['MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED',
|
|
'MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA_ENABLED',
|
|
'PSA_WANT_ALG_ECDSA',
|
|
'PSA_WANT_ALG_DETERMINISTIC_ECDSA'],
|
|
'MBEDTLS_ECP_C': ['MBEDTLS_ECDSA_C',
|
|
'MBEDTLS_ECDH_C', 'PSA_WANT_ALG_ECDH',
|
|
'MBEDTLS_ECJPAKE_C',
|
|
'MBEDTLS_ECP_RESTARTABLE',
|
|
'MBEDTLS_PK_PARSE_EC_EXTENDED',
|
|
'MBEDTLS_PK_PARSE_EC_COMPRESSED',
|
|
'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED',
|
|
'MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED',
|
|
'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
|
|
'MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_EPHEMERAL_ENABLED',
|
|
'MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_PSK_EPHEMERAL_ENABLED',
|
|
'PSA_WANT_KEY_TYPE_ECC_PUBLIC_KEY',
|
|
'PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_BASIC',
|
|
'PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT',
|
|
'PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_EXPORT',
|
|
'PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_GENERATE',
|
|
'PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_DERIVE'],
|
|
'MBEDTLS_ECJPAKE_C': ['MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED',
|
|
'PSA_WANT_ALG_JPAKE'],
|
|
'MBEDTLS_PKCS1_V21': ['MBEDTLS_X509_RSASSA_PSS_SUPPORT',
|
|
'PSA_WANT_ALG_RSA_OAEP',
|
|
'PSA_WANT_ALG_RSA_PSS'],
|
|
'MBEDTLS_PKCS1_V15': ['MBEDTLS_KEY_EXCHANGE_DHE_RSA_ENABLED',
|
|
'MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED',
|
|
'MBEDTLS_KEY_EXCHANGE_RSA_ENABLED',
|
|
'PSA_WANT_ALG_RSA_PKCS1V15_CRYPT',
|
|
'PSA_WANT_ALG_RSA_PKCS1V15_SIGN'],
|
|
'MBEDTLS_RSA_C': ['MBEDTLS_PKCS1_V15',
|
|
'MBEDTLS_PKCS1_V21',
|
|
'MBEDTLS_KEY_EXCHANGE_ECDH_RSA_ENABLED',
|
|
'PSA_WANT_KEY_TYPE_RSA_PUBLIC_KEY',
|
|
'PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_BASIC',
|
|
'PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_IMPORT',
|
|
'PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_EXPORT',
|
|
'PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_GENERATE'],
|
|
|
|
'MBEDTLS_MD5_C' : ['PSA_WANT_ALG_MD5'],
|
|
'MBEDTLS_RIPEMD160_C' : ['PSA_WANT_ALG_RIPEMD160'],
|
|
'MBEDTLS_SHA1_C' : ['PSA_WANT_ALG_SHA_1'],
|
|
'MBEDTLS_SHA224_C': ['MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED',
|
|
'MBEDTLS_ENTROPY_FORCE_SHA256',
|
|
'MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_IF_PRESENT',
|
|
'MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_ONLY',
|
|
'PSA_WANT_ALG_SHA_224'],
|
|
'MBEDTLS_SHA256_C': ['MBEDTLS_KEY_EXCHANGE_ECJPAKE_ENABLED',
|
|
'MBEDTLS_ENTROPY_FORCE_SHA256',
|
|
'MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_IF_PRESENT',
|
|
'MBEDTLS_SHA256_USE_ARMV8_A_CRYPTO_ONLY',
|
|
'MBEDTLS_LMS_C',
|
|
'MBEDTLS_LMS_PRIVATE',
|
|
'PSA_WANT_ALG_SHA_256',
|
|
'PSA_WANT_ALG_TLS12_ECJPAKE_TO_PMS'],
|
|
'MBEDTLS_SHA384_C' : ['PSA_WANT_ALG_SHA_384'],
|
|
'MBEDTLS_SHA512_C': ['MBEDTLS_SHA512_USE_A64_CRYPTO_IF_PRESENT',
|
|
'MBEDTLS_SHA512_USE_A64_CRYPTO_ONLY',
|
|
'PSA_WANT_ALG_SHA_512'],
|
|
'MBEDTLS_SHA3_C' : ['PSA_WANT_ALG_SHA3_224',
|
|
'PSA_WANT_ALG_SHA3_256',
|
|
'PSA_WANT_ALG_SHA3_384',
|
|
'PSA_WANT_ALG_SHA3_512'],
|
|
}
|
|
|
|
# If an option is tested in an exclusive test, alter the following defines.
|
|
# These are not necessarily dependencies, but just minimal required changes
|
|
# if a given define is the only one enabled from an exclusive group.
|
|
EXCLUSIVE_GROUPS = {
|
|
'MBEDTLS_SHA512_C': ['-MBEDTLS_SSL_COOKIE_C',
|
|
'-MBEDTLS_SSL_TLS_C'],
|
|
'MBEDTLS_ECP_DP_CURVE448_ENABLED': ['-MBEDTLS_ECDSA_C',
|
|
'-MBEDTLS_ECDSA_DETERMINISTIC',
|
|
'-MBEDTLS_ECJPAKE_C',],
|
|
'MBEDTLS_ECP_DP_CURVE25519_ENABLED': ['-MBEDTLS_ECDSA_C',
|
|
'-MBEDTLS_ECDSA_DETERMINISTIC',
|
|
'-MBEDTLS_ECJPAKE_C'],
|
|
'PSA_WANT_KEY_TYPE_ARIA': ['-PSA_WANT_ALG_CMAC',
|
|
'-PSA_WANT_ALG_CCM',
|
|
'-PSA_WANT_ALG_GCM',
|
|
'-MBEDTLS_SSL_TICKET_C',
|
|
'-MBEDTLS_SSL_CONTEXT_SERIALIZATION'],
|
|
'PSA_WANT_KEY_TYPE_CAMELLIA': ['-PSA_WANT_ALG_CMAC'],
|
|
'PSA_WANT_KEY_TYPE_CHACHA20': ['-PSA_WANT_ALG_CMAC',
|
|
'-PSA_WANT_ALG_CCM',
|
|
'-PSA_WANT_ALG_GCM',
|
|
'-PSA_WANT_ALG_ECB_NO_PADDING'],
|
|
'PSA_WANT_KEY_TYPE_DES': ['-PSA_WANT_ALG_CCM',
|
|
'-PSA_WANT_ALG_GCM',
|
|
'-MBEDTLS_SSL_TICKET_C',
|
|
'-MBEDTLS_SSL_CONTEXT_SERIALIZATION'],
|
|
}
|
|
def handle_exclusive_groups(config_settings, symbol):
|
|
"""For every symbol tested in an exclusive group check if there are other
|
|
defines to be altered. """
|
|
for dep in EXCLUSIVE_GROUPS.get(symbol, []):
|
|
unset = dep.startswith('-')
|
|
dep = dep[1:]
|
|
config_settings[dep] = not unset
|
|
|
|
def turn_off_dependencies(config_settings, exclude=None):
|
|
"""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.
|
|
Handle the dependencies recursively.
|
|
|
|
If 'exclude' is a symbol, ensure its dependencies are not turned off while dependencies
|
|
of other settings are turned off.
|
|
"""
|
|
|
|
# Determine recursively the settings that should not be turned off for the sake of 'exclude'.
|
|
excludes = set()
|
|
if exclude:
|
|
revdep = set(REVERSE_DEPENDENCIES.get(exclude, []))
|
|
while revdep:
|
|
dep = revdep.pop()
|
|
excludes.add(dep)
|
|
revdep.update(set(REVERSE_DEPENDENCIES.get(dep, [])) - excludes)
|
|
|
|
for key, value in sorted(config_settings.items()):
|
|
if value is not False:
|
|
continue
|
|
|
|
# Save the processed settings to handle cross referencies.
|
|
# Start with set of settings that we do not want to turn off.
|
|
history = excludes.copy()
|
|
revdep = set(REVERSE_DEPENDENCIES.get(key, [])) - excludes
|
|
while revdep:
|
|
dep = revdep.pop()
|
|
history.add(dep)
|
|
config_settings[dep] = False
|
|
# Do not add symbols which are already processed
|
|
revdep.update(set(REVERSE_DEPENDENCIES.get(dep, [])) - history)
|
|
|
|
class BaseDomain: # pylint: disable=too-few-public-methods, unused-argument
|
|
"""A base class for all domains."""
|
|
def __init__(self, symbols, commands, exclude):
|
|
"""Initialize the jobs container"""
|
|
self.jobs = []
|
|
|
|
class ExclusiveDomain(BaseDomain): # pylint: disable=too-few-public-methods
|
|
"""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."""
|
|
def __init__(self, symbols, commands, exclude=None):
|
|
"""Build a domain for the specified list of configuration symbols.
|
|
The domain contains a set of jobs that enable one of the elements
|
|
of symbols and disable the others.
|
|
Each job runs the specified commands.
|
|
If exclude is a regular expression, skip generated jobs whose description
|
|
would match this regular expression."""
|
|
super().__init__(symbols, commands, exclude)
|
|
base_config_settings = {}
|
|
for symbol in symbols:
|
|
base_config_settings[symbol] = False
|
|
for symbol in symbols:
|
|
description = symbol
|
|
if exclude and re.match(exclude, description):
|
|
continue
|
|
config_settings = base_config_settings.copy()
|
|
config_settings[symbol] = True
|
|
handle_exclusive_groups(config_settings, symbol)
|
|
turn_off_dependencies(config_settings, symbol)
|
|
job = Job(description, config_settings, commands)
|
|
self.jobs.append(job)
|
|
|
|
class ComplementaryDomain(BaseDomain): # pylint: disable=too-few-public-methods
|
|
"""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.
|
|
If exclude is a regular expression, skip generated jobs whose description
|
|
would match this regular expression."""
|
|
def __init__(self, symbols, commands, exclude=None):
|
|
"""Build a domain for the specified list of configuration symbols.
|
|
Each job in the domain disables one of the specified symbols.
|
|
Each job runs the specified commands."""
|
|
super().__init__(symbols, commands, exclude)
|
|
for symbol in symbols:
|
|
description = '!' + symbol
|
|
if exclude and re.match(exclude, description):
|
|
continue
|
|
config_settings = {symbol: False}
|
|
turn_off_dependencies(config_settings)
|
|
job = Job(description, config_settings, commands)
|
|
self.jobs.append(job)
|
|
|
|
class DualDomain(ExclusiveDomain, ComplementaryDomain): # pylint: disable=too-few-public-methods
|
|
"""A domain that contains both the ExclusiveDomain and BaseDomain tests.
|
|
Both parent class __init__ calls are performed in any order and
|
|
each call adds respective jobs. The job array initialization is done once in
|
|
BaseDomain, before the parent __init__ calls."""
|
|
|
|
class DomainData:
|
|
"""A container for domains and jobs, used to structurize testing."""
|
|
def config_symbols_matching(self, regexp):
|
|
"""List the mbedtls_config.h settings matching regexp."""
|
|
return [symbol for symbol in self.all_config_symbols
|
|
if re.match(regexp, symbol)]
|
|
|
|
# pylint: disable=too-many-locals
|
|
def __init__(self, options, conf):
|
|
"""Gather data about the library and establish a list of domains to test."""
|
|
build_command = [options.make_command, 'CFLAGS=-Werror -O2']
|
|
build_and_test = [build_command, [options.make_command, 'test']]
|
|
self.all_config_symbols = set(conf.settings.keys())
|
|
psa_info = psa_information.Information().constructors
|
|
algs = {crypto_knowledge.Algorithm(alg): symbol
|
|
for alg, symbol in ((alg, psa_information.psa_want_symbol(alg))
|
|
for alg in psa_info.algorithms)
|
|
if symbol in self.all_config_symbols}
|
|
cipher_algs = {alg
|
|
for alg in algs
|
|
if alg.can_do(crypto_knowledge.AlgorithmCategory.CIPHER)}
|
|
key_types = {crypto_knowledge.KeyType(expr): symbol
|
|
for key_type in psa_info.key_types
|
|
for expr, symbol in ((expr, psa_information.psa_want_symbol(key_type))
|
|
for expr in psa_info.generate_expressions([key_type]))
|
|
if symbol in self.all_config_symbols}
|
|
|
|
# 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')
|
|
|
|
# Find cipher key types
|
|
cipher_key_types = {symbol
|
|
for key_type, symbol in key_types.items()
|
|
for alg in cipher_algs
|
|
if key_type.can_do(alg)}
|
|
|
|
# Get cipher modes
|
|
cipher_chaining_symbols = {algs[cipher_alg] for cipher_alg in cipher_algs}
|
|
|
|
# Find block padding mode enabling macros by name.
|
|
cipher_padding_symbols = self.config_symbols_matching(r'MBEDTLS_CIPHER_PADDING_\w+\Z')
|
|
|
|
self.domains = {
|
|
# Cipher key types
|
|
'cipher_id': ExclusiveDomain(cipher_key_types, build_and_test),
|
|
|
|
# XTS is not yet supported via the PSA API.
|
|
# See https://github.com/Mbed-TLS/mbedtls/issues/6384
|
|
'cipher_chaining': ExclusiveDomain(cipher_chaining_symbols,
|
|
build_and_test,
|
|
exclude=r'PSA_WANT_ALG_XTS'),
|
|
|
|
'cipher_padding': ExclusiveDomain(cipher_padding_symbols,
|
|
build_and_test),
|
|
# Elliptic curves. Run the test suites.
|
|
'curves': ExclusiveDomain(curve_symbols, build_and_test),
|
|
# Hash algorithms. Excluding exclusive domains of MD, RIPEMD, SHA1,
|
|
# SHA224 and SHA384 because MBEDTLS_ENTROPY_C is extensively used
|
|
# across various modules, but it depends on either SHA256 or SHA512.
|
|
# As a consequence an "exclusive" test of anything other than SHA256
|
|
# or SHA512 with MBEDTLS_ENTROPY_C enabled is not possible.
|
|
'hashes': DualDomain(hash_symbols, build_and_test,
|
|
exclude=r'MBEDTLS_(MD|RIPEMD|SHA1_)' \
|
|
'|MBEDTLS_SHA224_' \
|
|
'|MBEDTLS_SHA384_' \
|
|
'|MBEDTLS_SHA3_'),
|
|
# Key exchange types.
|
|
'kex': ExclusiveDomain(key_exchange_symbols, build_and_test),
|
|
'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, conf, colors=NO_COLORS):
|
|
"""Run the specified job (a Job instance)."""
|
|
subprocess.check_call([options.make_command, 'clean'])
|
|
job.announce(colors, None)
|
|
if not job.configure(conf, colors):
|
|
job.announce(colors, False)
|
|
return False
|
|
conf.write()
|
|
success = job.test(options)
|
|
job.announce(colors, success)
|
|
return success
|
|
|
|
def run_tests(options, domain_data, conf):
|
|
"""Run the desired jobs.
|
|
domain_data should be a DomainData instance that describes the available
|
|
domains and jobs.
|
|
Run the jobs listed in options.tasks."""
|
|
colors = Colors(options)
|
|
jobs = []
|
|
failures = []
|
|
successes = []
|
|
for name in options.tasks:
|
|
jobs += domain_data.get_jobs(name)
|
|
conf.backup()
|
|
try:
|
|
for job in jobs:
|
|
success = run(options, job, conf, colors=colors)
|
|
if not success:
|
|
if options.keep_going:
|
|
failures.append(job.name)
|
|
else:
|
|
return False
|
|
else:
|
|
successes.append(job.name)
|
|
conf.restore()
|
|
except:
|
|
# Restore the configuration, except in stop-on-error mode if there
|
|
# was an error, where we leave the failing configuration up for
|
|
# developer convenience.
|
|
if options.keep_going:
|
|
conf.restore()
|
|
raise
|
|
if successes:
|
|
log_line('{} passed'.format(' '.join(successes)), color=colors.bold_green)
|
|
if failures:
|
|
log_line('{} FAILED'.format(' '.join(failures)), color=colors.bold_red)
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def main():
|
|
try:
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
description=
|
|
"Test Mbed TLS with a subset of algorithms.\n\n"
|
|
"Example usage:\n"
|
|
r"./tests/scripts/depends.py \!MBEDTLS_SHA1_C MBEDTLS_SHA256_C""\n"
|
|
"./tests/scripts/depends.py MBEDTLS_AES_C hashes\n"
|
|
"./tests/scripts/depends.py cipher_id cipher_chaining\n")
|
|
parser.add_argument('--color', metavar='WHEN',
|
|
help='Colorize the output (always/auto/never)',
|
|
choices=['always', 'auto', 'never'], default='auto')
|
|
parser.add_argument('-c', '--config', metavar='FILE',
|
|
help='Configuration file to modify',
|
|
default=config.MbedTLSConfigFile.default_path[0])
|
|
parser.add_argument('-r', '--crypto-config', metavar='FILE',
|
|
help='Crypto configuration file to modify',
|
|
default=config.CryptoConfigFile.default_path[0])
|
|
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('tasks', metavar='TASKS', nargs='*',
|
|
help='The domain(s) or job(s) to test (default: all).',
|
|
default=True)
|
|
options = parser.parse_args()
|
|
os.chdir(options.directory)
|
|
conf = config.CombinedConfig(config.MbedTLSConfigFile(options.config),
|
|
config.CryptoConfigFile(options.crypto_config))
|
|
domain_data = DomainData(options, conf)
|
|
|
|
if options.tasks is True:
|
|
options.tasks = sorted(domain_data.domains.keys())
|
|
if options.list:
|
|
for arg in options.list:
|
|
for domain_name in sorted(getattr(domain_data, arg).keys()):
|
|
print(domain_name)
|
|
sys.exit(0)
|
|
else:
|
|
sys.exit(0 if run_tests(options, domain_data, conf) else 1)
|
|
except Exception: # pylint: disable=broad-except
|
|
traceback.print_exc()
|
|
sys.exit(3)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|