Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update the hashes domain to use PSA macros in depends.py #9634

Open
wants to merge 7 commits into
base: development
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 169 additions & 76 deletions tests/scripts/depends.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import subprocess
import sys
import traceback
from collections import OrderedDict
from typing import Union

# Add the Mbed TLS Python library directory to the module search path
Expand Down Expand Up @@ -134,19 +135,26 @@ def set_reference_config(conf, colors):

class Job:
"""A job builds the library in a specific configuration and runs some tests."""
def __init__(self, name, config_settings, commands):
def __init__(self, name, config_settings, commands, alter_names=None):
"""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."""

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.

The alter_names can be set for complex jobs which handle multiple symbols
within one job and thus each symbol be referenced separately.
"""

self.name = name
self.config_settings = config_settings
self.commands = commands
self.alter_names = alter_names if isinstance(alter_names, set) else set()

def announce(self, colors, what):
'''Announce the start or completion of a job.
Expand Down Expand Up @@ -328,38 +336,38 @@ def test(self, options):
'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'],
'PSA_WANT_ALG_MD5': ['MBEDTLS_MD5_C'],
'PSA_WANT_ALG_RIPEMD160': ['MBEDTLS_RIPEMD160_C'],
'PSA_WANT_ALG_SHA_1': ['MBEDTLS_SHA1_C'],
'PSA_WANT_ALG_SHA_224': ['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_SHA224_C'],
'PSA_WANT_ALG_SHA_256': ['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',
'MBEDTLS_SHA256_C',
'PSA_WANT_ALG_TLS12_ECJPAKE_TO_PMS'],
'PSA_WANT_ALG_SHA_384': ['MBEDTLS_SHA384_C'],
'PSA_WANT_ALG_SHA_512': ['MBEDTLS_SHA512_USE_A64_CRYPTO_IF_PRESENT',
'MBEDTLS_SHA512_USE_A64_CRYPTO_ONLY',
'MBEDTLS_SHA512_C'],
'PSA_WANT_ALG_SHA3_224': ['MBEDTLS_SHA3_C'],
'PSA_WANT_ALG_SHA3_256': ['MBEDTLS_SHA3_C'],
'PSA_WANT_ALG_SHA3_384': ['MBEDTLS_SHA3_C'],
'PSA_WANT_ALG_SHA3_512': ['MBEDTLS_SHA3_C'],
}

# 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'],
'PSA_WANT_ALG_SHA_512': ['-MBEDTLS_SSL_COOKIE_C',
'-MBEDTLS_SSL_TLS_C'],
'PSA_WANT_ECC_MONTGOMERY_448': ['-MBEDTLS_ECDSA_C',
'-MBEDTLS_ECDSA_DETERMINISTIC',
'-MBEDTLS_ECJPAKE_C',],
Expand Down Expand Up @@ -389,7 +397,7 @@ def handle_exclusive_groups(config_settings, symbol):
dep = dep[1:]
config_settings[dep] = not unset

def turn_off_dependencies(config_settings, exclude=None):
def turn_off_dependencies(config_settings, *exclude):
"""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.
Expand All @@ -402,7 +410,9 @@ def turn_off_dependencies(config_settings, exclude=None):
# 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, []))
revdep = {dep
for e in exclude
for dep in REVERSE_DEPENDENCIES.get(e, [])}
while revdep:
dep = revdep.pop()
excludes.add(dep)
Expand All @@ -425,51 +435,124 @@ def turn_off_dependencies(config_settings, exclude=None):

class BaseDomain: # pylint: disable=too-few-public-methods, unused-argument
"""A base class for all domains."""
def __init__(self, symbols, commands, exclude):
def __init__(self, symbols, commands, exclude, mutual_exclusion):
"""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):

Establish a list of configuration symbols. For each symbol, run a test job
with this symbol set and the others unset.
"""

# pylint: disable=too-many-locals
def __init__(self, symbols, commands, exclude=None, mutual_exclusion=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)

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.

If `mutual_exclusion` contains regular expressions, create only one job
for the matching symbols.
"""

super().__init__(symbols, commands, exclude, mutual_exclusion)
base_config_settings = {}
single_symbols = set()
grouped_symbols = {}
for symbol in symbols:
base_config_settings[symbol] = False
for symbol in symbols:
description = symbol
if exclude and re.match(exclude, description):

if exclude and re.match(exclude, symbol):
continue

# Determine mutually exlusive symbol groups
matched = False
if mutual_exclusion:
for group in mutual_exclusion:
if re.match(group, symbol):
matched = True
grouped_symbols.get(group, set()).add(symbol)
if not matched:
single_symbols.add(symbol)

# Individual symbol handling
for symbol in single_symbols:
config_settings = base_config_settings.copy()
config_settings[symbol] = True
description = symbol

handle_exclusive_groups(config_settings, symbol)
turn_off_dependencies(config_settings, symbol)
job = Job(description, config_settings, commands)
self.jobs.append(job)

# Handle mutually exclusive symbols
for gsymbols in grouped_symbols.values():
config_settings = base_config_settings.copy()
config_settings.update({symbol: True for symbol in gsymbols})
description = '(' + ' '.join(gsymbols) + ')'
alter_names = grouped_symbols

for symbol in gsymbols:
handle_exclusive_groups(config_settings, symbol)

turn_off_dependencies(config_settings, *gsymbols)
job = Job(description, config_settings, commands, alter_names)
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):

Establish a list of configuration symbols. For each symbol, run a test job
with this symbol unset.
"""

def __init__(self, symbols, commands, exclude=None, mutual_exclusion=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:

Each job in the domain disables one of the specified symbols or
group of symbols if mutual_exclusion is used.
Each job runs the specified commands.

If `exclude` is a regular expression, skip generated jobs whose description
would match this regular expression.

If `mutual_exclusion` contains regular expressions, create one job for all the
symbols matching a regular expression. Thus these symbols can be handled as
one entity.
"""

super().__init__(symbols, commands, exclude, mutual_exclusion)

# Filter out excluded symbols
valid_symbols = {symbol
for symbol in symbols
if not (exclude and re.match(exclude, '!' + symbol))}

# Handle mutually exclusive symbols.
# These symbols have its own job and excluded from the individual symbol handling.
grouped_symbols = set()
if mutual_exclusion:
for group in mutual_exclusion:
config_settings = {symbol: False
for symbol in valid_symbols
if re.match(group, symbol)}
description = '!(' + ' '.join(sorted(config_settings.keys())) + ')'
turn_off_dependencies(config_settings)
alter_names = {'!' + symbol for symbol in config_settings.keys()}
job = Job(description, config_settings, commands, alter_names)
self.jobs.append(job)
grouped_symbols.update(config_settings.keys())

# Individual symbol handling
for symbol in valid_symbols - grouped_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)
Expand All @@ -485,10 +568,10 @@ 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)]
return {symbol for symbol in self.all_config_symbols
if re.match(regexp, symbol)}

# pylint: disable=too-many-locals
# pylint: disable=too-many-locals,fixme
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']
Expand All @@ -508,10 +591,12 @@ def __init__(self, options, conf):
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 hash modules by category.
hash_symbols = {symbol
for alg, symbol in algs.items()
if alg.can_do(crypto_knowledge.AlgorithmCategory.HASH)}

# Find elliptic curve enabling macros
# Find elliptic curve enabling macros by name.
# MBEDTLS_ECP_DP_SECP224K1_ENABLED added to disable it for all curves
curve_symbols = self.config_symbols_matching(r'PSA_WANT_ECC_\w+\Z|'
r'MBEDTLS_ECP_DP_SECP224K1_ENABLED')
Expand Down Expand Up @@ -545,19 +630,19 @@ def __init__(self, options, conf):
build_and_test),

# Elliptic curves. Run the test suites.
'curves': ExclusiveDomain(curve_symbols, build_and_test,
exclude=r'MBEDTLS_ECP_DP_SECP224K1_ENABLED'),
'curves': ExclusiveDomain(curve_symbols, build_and_test),

# Hash algorithms. Excluding exclusive domains of MD, RIPEMD, SHA1,
# Hash algorithms. Excluding exclusive domains of MD, RIPEMD, SHA1, SHA3*,
# 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.
# TODO: when MBEDTLS_SHA3_C is removed the mutual_exclusion
# argument must be removed.
'hashes': DualDomain(hash_symbols, build_and_test,
exclude=r'MBEDTLS_(MD|RIPEMD|SHA1_)' \
'|MBEDTLS_SHA224_' \
'|MBEDTLS_SHA384_' \
'|MBEDTLS_SHA3_'),
exclude=r'PSA_WANT_ALG_(?!SHA_(256|512))',
mutual_exclusion=[r'PSA_WANT_ALG_SHA3_']),

# Key exchange types.
'kex': ExclusiveDomain(key_exchange_symbols, build_and_test),
'pkalgs': ComplementaryDomain(['MBEDTLS_ECDSA_C',
Expand All @@ -578,8 +663,15 @@ def get_jobs(self, 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:
elif name in self.jobs:
return [self.jobs[name]]
else:
# Use the altarnative names of the complex jobs
for job in self.jobs.values():
if name in job.alter_names:
return [job]

raise ValueError(f'Invalid job name: \'{name}\'')

def run(options, job, conf, colors=NO_COLORS):
"""Run the specified job (a Job instance)."""
Expand All @@ -606,7 +698,8 @@ def run_tests(options, domain_data, conf):
jobs += domain_data.get_jobs(name)
conf.backup()
try:
for job in jobs:
# Run the jobs, without duplication
for job in OrderedDict.fromkeys(jobs).keys():
success = run(options, job, conf, colors=colors)
if not success:
if options.keep_going:
Expand Down