From 634103c9f9b17e9f8f0e0ba978825f80b5c81fac Mon Sep 17 00:00:00 2001 From: Gabor Mezei Date: Wed, 11 Sep 2024 13:08:21 +0200 Subject: [PATCH] Update `config.py` to use `config_common.py` from the framework Signed-off-by: Gabor Mezei --- scripts/config.py | 592 +++++++++++++++++++--------------------------- 1 file changed, 240 insertions(+), 352 deletions(-) diff --git a/scripts/config.py b/scripts/config.py index 8704bdb51e..41dc8c5eb5 100755 --- a/scripts/config.py +++ b/scripts/config.py @@ -1,158 +1,31 @@ #!/usr/bin/env python3 -"""Mbed TLS configuration file manipulation library and tool +"""Mbed TLS and PSA configuration file manipulation library and tool Basic usage, to read the Mbed TLS configuration: - config = ConfigFile() + config = CombinedConfigFile() if 'MBEDTLS_RSA_C' in config: print('RSA is enabled') """ -# Note that as long as Mbed TLS 2.28 LTS is maintained, the version of -# this script in the mbedtls-2.28 branch must remain compatible with -# Python 3.4. The version in development may only use more recent features -# in parts that are not backported to 2.28. - ## Copyright The Mbed TLS Contributors ## SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later ## import os import re +import sys -class Setting: - """Representation of one Mbed TLS mbedtls_config.h setting. +import framework_scripts_path # pylint: disable=unused-import +from mbedtls_framework import config_common - Fields: - * name: the symbol name ('MBEDTLS_xxx'). - * value: the value of the macro. The empty string for a plain #define - with no value. - * active: True if name is defined, False if a #define for name is - present in mbedtls_config.h but commented out. - * section: the name of the section that contains this symbol. - """ - # pylint: disable=too-few-public-methods - def __init__(self, active, name, value='', section=None): - self.active = active - self.name = name - self.value = value - self.section = section - -class Config: - """Representation of the Mbed TLS configuration. - - In the documentation of this class, a symbol is said to be *active* - if there is a #define for it that is not commented out, and *known* - if there is a #define for it whether commented out or not. - - This class supports the following protocols: - * `name in config` is `True` if the symbol `name` is active, `False` - otherwise (whether `name` is inactive or not known). - * `config[name]` is the value of the macro `name`. If `name` is inactive, - raise `KeyError` (even if `name` is known). - * `config[name] = value` sets the value associated to `name`. `name` - must be known, but does not need to be set. This does not cause - name to become set. - """ - - def __init__(self): - self.settings = {} - - def __contains__(self, name): - """True if the given symbol is active (i.e. set). - - False if the given symbol is not set, even if a definition - is present but commented out. - """ - return name in self.settings and self.settings[name].active - - def all(self, *names): - """True if all the elements of names are active (i.e. set).""" - return all(self.__contains__(name) for name in names) - - def any(self, *names): - """True if at least one symbol in names are active (i.e. set).""" - return any(self.__contains__(name) for name in names) - - def known(self, name): - """True if a #define for name is present, whether it's commented out or not.""" - return name in self.settings - - def __getitem__(self, name): - """Get the value of name, i.e. what the preprocessor symbol expands to. - - If name is not known, raise KeyError. name does not need to be active. - """ - return self.settings[name].value - - def get(self, name, default=None): - """Get the value of name. If name is inactive (not set), return default. - - If a #define for name is present and not commented out, return - its expansion, even if this is the empty string. - - If a #define for name is present but commented out, return default. - """ - if name in self.settings: - return self.settings[name].value - else: - return default - - def __setitem__(self, name, value): - """If name is known, set its value. - - If name is not known, raise KeyError. - """ - self.settings[name].value = value - - def set(self, name, value=None): - """Set name to the given value and make it active. - - If value is None and name is already known, don't change its value. - If value is None and name is not known, set its value to the empty - string. - """ - if name in self.settings: - if value is not None: - self.settings[name].value = value - self.settings[name].active = True - else: - self.settings[name] = Setting(True, name, value=value) - - def unset(self, name): - """Make name unset (inactive). - - name remains known if it was known before. - """ - if name not in self.settings: - return - self.settings[name].active = False - - def adapt(self, adapter): - """Run adapter on each known symbol and (de)activate it accordingly. - - `adapter` must be a function that returns a boolean. It is called as - `adapter(name, active, section)` for each setting, where `active` is - `True` if `name` is set and `False` if `name` is known but unset, - and `section` is the name of the section containing `name`. If - `adapter` returns `True`, then set `name` (i.e. make it active), - otherwise unset `name` (i.e. make it known but inactive). - """ - for setting in self.settings.values(): - setting.active = adapter(setting.name, setting.active, - setting.section) - - def change_matching(self, regexs, enable): - """Change all symbols matching one of the regexs to the desired state.""" - if not regexs: - return - regex = re.compile('|'.join(regexs)) - for setting in self.settings.values(): - if regex.search(setting.name): - setting.active = enable def is_full_section(section): - """Is this section affected by "config.py full" and friends?""" - return section.endswith('support') or section.endswith('modules') + """Is this section affected by "config.py full" and friends? + + In a config file where the sections are not used the whole config file + is an empty section (with value None) and the whole file is affected. + """ + return section is None or section.endswith('support') or section.endswith('modules') def realfull_adapter(_name, active, section): """Activate all symbols found in the global and boolean feature sections. @@ -168,6 +41,26 @@ def realfull_adapter(_name, active, section): return active return True +PSA_UNSUPPORTED_FEATURE = frozenset([ + 'PSA_WANT_ALG_CBC_MAC', + 'PSA_WANT_ALG_XTS', + 'PSA_WANT_KEY_TYPE_RSA_KEY_PAIR_DERIVE', + 'PSA_WANT_KEY_TYPE_DH_KEY_PAIR_DERIVE' +]) + +PSA_DEPRECATED_FEATURE = frozenset([ + 'PSA_WANT_KEY_TYPE_ECC_KEY_PAIR', + 'PSA_WANT_KEY_TYPE_RSA_KEY_PAIR' +]) + +PSA_UNSTABLE_FEATURE = frozenset([ + 'PSA_WANT_ECC_SECP_K1_224' +]) + +EXCLUDE_FROM_CRYPTO = PSA_UNSUPPORTED_FEATURE | \ + PSA_DEPRECATED_FEATURE | \ + PSA_UNSTABLE_FEATURE + # The goal of the full configuration is to have everything that can be tested # together. This includes deprecated or insecure options. It excludes: # * Options that require additional build dependencies or unusual hardware. @@ -211,6 +104,9 @@ EXCLUDE_FROM_FULL = frozenset([ 'MBEDTLS_TEST_CONSTANT_FLOW_MEMSAN', # build dependency (clang+memsan) 'MBEDTLS_TEST_CONSTANT_FLOW_VALGRIND', # build dependency (valgrind headers) 'MBEDTLS_X509_REMOVE_INFO', # removes a feature + *PSA_UNSUPPORTED_FEATURE, + *PSA_DEPRECATED_FEATURE, + *PSA_UNSTABLE_FEATURE ]) def is_seamless_alt(name): @@ -317,6 +213,8 @@ def include_in_crypto(name): 'MBEDTLS_PKCS7_C', # part of libmbedx509 ]: return False + if name in EXCLUDE_FROM_CRYPTO: + return False return True def crypto_adapter(adapter): @@ -335,6 +233,7 @@ def crypto_adapter(adapter): DEPRECATED = frozenset([ 'MBEDTLS_PSA_CRYPTO_SE_C', + *PSA_DEPRECATED_FEATURE ]) def no_deprecated_adapter(adapter): """Modify an adapter to disable deprecated symbols. @@ -369,12 +268,9 @@ def no_platform_adapter(adapter): return adapter(name, active, section) return continuation -class ConfigFile(Config): - """Representation of the Mbed TLS configuration read for a file. - See the documentation of the `Config` class for methods to query - and modify the configuration. - """ +class MbedTLSConfigFile(config_common.ConfigFile): + """Representation of an MbedTLS configuration file.""" _path_in_tree = 'include/mbedtls/mbedtls_config.h' default_path = [_path_in_tree, @@ -385,228 +281,220 @@ class ConfigFile(Config): _path_in_tree)] def __init__(self, filename=None): - """Read the Mbed TLS configuration file.""" - if filename is None: - for candidate in self.default_path: - if os.path.lexists(candidate): - filename = candidate - break - else: - raise Exception('Mbed TLS configuration file not found', - self.default_path) - super().__init__() - self.filename = filename - self.inclusion_guard = None + super().__init__(self.default_path, 'Mbed TLS', filename) self.current_section = 'header' - with open(filename, 'r', encoding='utf-8') as file: - self.templates = [self._parse_line(line) for line in file] - self.current_section = None + + +class CryptoConfigFile(config_common.ConfigFile): + """Representation of a Crypto configuration file.""" + + # Temporary, while Mbed TLS does not just rely on the TF-PSA-Crypto + # build system to build its crypto library. When it does, the + # condition can just be removed. + _path_in_tree = ('include/psa/crypto_config.h' + if not os.path.isdir(os.path.join(os.path.dirname(__file__), + os.pardir, + 'tf-psa-crypto')) else + 'tf-psa-crypto/include/psa/crypto_config.h') + default_path = [_path_in_tree, + os.path.join(os.path.dirname(__file__), + os.pardir, + _path_in_tree), + os.path.join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))), + _path_in_tree)] + + def __init__(self, filename=None): + super().__init__(self.default_path, 'Crypto', filename) + + +class MbedTLSConfig(config_common.Config): + """Representation of the Mbed TLS configuration. + + See the documentation of the `Config` class for methods to query + and modify the configuration. + """ + + def __init__(self, filename=None): + """Read the Mbed TLS configuration file.""" + + super().__init__() + configfile = MbedTLSConfigFile(filename) + self.configfiles.append(configfile) + self.settings.update({name: config_common.Setting(configfile, active, name, value, section) + for (active, name, value, section) + in configfile.parse_file()}) def set(self, name, value=None): + """Set name to the given value and make it active.""" + if name not in self.settings: - self.templates.append((name, '', '#define ' + name + ' ')) + self._get_configfile().templates.append((name, '', '#define ' + name + ' ')) + super().set(name, value) - _define_line_regexp = (r'(?P\s*)' + - r'(?P(//\s*)?)' + - r'(?P#\s*define\s+)' + - r'(?P\w+)' + - r'(?P(?:\((?:\w|\s|,)*\))?)' + - r'(?P\s*)' + - r'(?P.*)') - _ifndef_line_regexp = r'#ifndef (?P\w+)' - _section_line_regexp = (r'\s*/?\*+\s*[\\@]name\s+SECTION:\s*' + - r'(?P
.*)[ */]*') - _config_line_regexp = re.compile(r'|'.join([_define_line_regexp, - _ifndef_line_regexp, - _section_line_regexp])) - def _parse_line(self, line): - """Parse a line in mbedtls_config.h and return the corresponding template.""" - line = line.rstrip('\r\n') - m = re.match(self._config_line_regexp, line) - if m is None: - return line - elif m.group('section'): - self.current_section = m.group('section') - return line - elif m.group('inclusion_guard') and self.inclusion_guard is None: - self.inclusion_guard = m.group('inclusion_guard') - return line - else: - active = not m.group('commented_out') - name = m.group('name') - value = m.group('value') - if name == self.inclusion_guard and value == '': - # The file double-inclusion guard is not an option. - return line - template = (name, - m.group('indentation'), - m.group('define') + name + - m.group('arguments') + m.group('separator')) - self.settings[name] = Setting(active, name, value, - self.current_section) - return template - def _format_template(self, name, indent, middle): - """Build a line for mbedtls_config.h for the given setting. +class CryptoConfig(config_common.Config): + """Representation of the PSA crypto configuration. - The line has the form "#define " - where is "#define ". - """ - setting = self.settings[name] - value = setting.value - if value is None: - value = '' - # Normally the whitespace to separate the symbol name from the - # value is part of middle, and there's no whitespace for a symbol - # with no value. But if a symbol has been changed from having a - # value to not having one, the whitespace is wrong, so fix it. - if value: - if middle[-1] not in '\t ': - middle += ' ' - else: - middle = middle.rstrip() - return ''.join([indent, - '' if setting.active else '//', - middle, - value]).rstrip() + See the documentation of the `Config` class for methods to query + and modify the configuration. + """ - def write_to_stream(self, output): - """Write the whole configuration to output.""" - for template in self.templates: - if isinstance(template, str): - line = template + def __init__(self, filename=None): + """Read the PSA crypto configuration file.""" + + super().__init__() + configfile = CryptoConfigFile(filename) + self.configfiles.append(configfile) + self.settings.update({name: config_common.Setting(configfile, active, name, value, section) + for (active, name, value, section) + in configfile.parse_file()}) + + def set(self, name, value='1'): + """Set name to the given value and make it active.""" + + if name in PSA_UNSUPPORTED_FEATURE: + raise ValueError(f'Feature is unsupported: \'{name}\'') + if name in PSA_UNSTABLE_FEATURE: + raise ValueError(f'Feature is unstable: \'{name}\'') + + if name not in self.settings: + self._get_configfile().templates.append((name, '', '#define ' + name + ' ')) + + super().set(name, value) + + +class CombinedConfig(config_common.Config): + """Representation of MbedTLS and PSA crypto configuration + + See the documentation of the `Config` class for methods to query + and modify the configuration. + """ + + def __init__(self, *configs): + super().__init__() + for config in configs: + if isinstance(config, MbedTLSConfigFile): + self.mbedtls_configfile = config + elif isinstance(config, CryptoConfigFile): + self.crypto_configfile = config else: - line = self._format_template(*template) - output.write(line + '\n') + raise ValueError(f'Invalid configfile: {config}') + self.configfiles.append(config) - def write(self, filename=None): + self.settings.update({name: config_common.Setting(configfile, active, name, value, section) + for configfile in [self.mbedtls_configfile, self.crypto_configfile] + for (active, name, value, section) in configfile.parse_file()}) + + _crypto_regexp = re.compile(r'$PSA_.*') + def _get_configfile(self, name=None): + """Find a config type for a setting name""" + + if name in self.settings: + return self.settings[name].configfile + elif re.match(self._crypto_regexp, name): + return self.crypto_configfile + else: + return self.mbedtls_configfile + + def set(self, name, value=None): + """Set name to the given value and make it active.""" + + configfile = self._get_configfile(name) + + if configfile == self.crypto_configfile: + if name in PSA_UNSUPPORTED_FEATURE: + raise ValueError(f'Feature is unsupported: \'{name}\'') + if name in PSA_UNSTABLE_FEATURE: + raise ValueError(f'Feature is unstable: \'{name}\'') + + # The default value in the crypto config is '1' + if not value: + value = '1' + + if name not in self.settings: + configfile.templates.append((name, '', '#define ' + name + ' ')) + + super().set(name, value) + + #pylint: disable=arguments-differ + def write(self, mbedtls_file=None, crypto_file=None): """Write the whole configuration to the file it was read from. - If filename is specified, write to this file instead. + If mbedtls_file or crypto_file is specified, write the specific configuration + to the corresponding file instead. """ - if filename is None: - filename = self.filename - with open(filename, 'w', encoding='utf-8') as output: - self.write_to_stream(output) + + self.mbedtls_configfile.write(self.settings, mbedtls_file) + self.crypto_configfile.write(self.settings, crypto_file) + + def filename(self, name=None): + """Get the names of the config files. + + If 'name' is specified return the name of the config file where it is defined. + """ + + if not name: + return [config.filename for config in [self.mbedtls_configfile, self.crypto_configfile]] + + return self._get_configfile(name).filename + + +class MbedTLSConfigTool(config_common.ConfigTool): + """Command line mbedtls_config.h and crypto_config.h manipulation tool.""" + + def __init__(self): + super().__init__(MbedTLSConfigFile) + self.config = CombinedConfig(MbedTLSConfigFile(self.parser_args.file), + CryptoConfigFile(self.parser_args.cryptofile)) + + def custom_parser_options(self): + """Adds MbedTLS specific options for the parser.""" + + self.parser.add_argument( + '--cryptofile', '-c', + help="""Crypto file to read (and modify if requested). Default: {}.""" + .format(CryptoConfigFile.default_path)) + + self.add_adapter( + 'baremetal', baremetal_adapter, + """Like full, but exclude features that require platform features + such as file input-output. + """) + self.add_adapter( + 'baremetal_size', baremetal_size_adapter, + """Like baremetal, but exclude debugging features. Useful for code size measurements. + """) + self.add_adapter( + 'full', full_adapter, + """Uncomment most features. + Exclude alternative implementations and platform support options, as well as + some options that are awkward to test. + """) + self.add_adapter( + 'full_no_deprecated', no_deprecated_adapter(full_adapter), + """Uncomment most non-deprecated features. + Like "full", but without deprecated features. + """) + self.add_adapter( + 'full_no_platform', no_platform_adapter(full_adapter), + """Uncomment most non-platform features. Like "full", but without platform features. + """) + self.add_adapter( + 'realfull', realfull_adapter, + """Uncomment all boolean #defines. + Suitable for generating documentation, but not for building. + """) + self.add_adapter( + 'crypto', crypto_adapter(None), + """Only include crypto features. Exclude X.509 and TLS.""") + self.add_adapter( + 'crypto_baremetal', crypto_adapter(baremetal_adapter), + """Like baremetal, but with only crypto features, excluding X.509 and TLS.""") + self.add_adapter( + 'crypto_full', crypto_adapter(full_adapter), + """Like full, but with only crypto features, excluding X.509 and TLS.""") + if __name__ == '__main__': - def main(): - """Command line mbedtls_config.h manipulation tool.""" - parser = argparse.ArgumentParser(description=""" - Mbed TLS configuration file manipulation tool. - """) - parser.add_argument('--file', '-f', - help="""File to read (and modify if requested). - Default: {}. - """.format(ConfigFile.default_path)) - parser.add_argument('--force', '-o', - action='store_true', - help="""For the set command, if SYMBOL is not - present, add a definition for it.""") - parser.add_argument('--write', '-w', metavar='FILE', - help="""File to write to instead of the input file.""") - subparsers = parser.add_subparsers(dest='command', - title='Commands') - parser_get = subparsers.add_parser('get', - help="""Find the value of SYMBOL - and print it. Exit with - status 0 if a #define for SYMBOL is - found, 1 otherwise. - """) - parser_get.add_argument('symbol', metavar='SYMBOL') - parser_set = subparsers.add_parser('set', - help="""Set SYMBOL to VALUE. - If VALUE is omitted, just uncomment - the #define for SYMBOL. - Error out of a line defining - SYMBOL (commented or not) is not - found, unless --force is passed. - """) - parser_set.add_argument('symbol', metavar='SYMBOL') - parser_set.add_argument('value', metavar='VALUE', nargs='?', - default='') - parser_set_all = subparsers.add_parser('set-all', - help="""Uncomment all #define - whose name contains a match for - REGEX.""") - parser_set_all.add_argument('regexs', metavar='REGEX', nargs='*') - parser_unset = subparsers.add_parser('unset', - help="""Comment out the #define - for SYMBOL. Do nothing if none - is present.""") - parser_unset.add_argument('symbol', metavar='SYMBOL') - parser_unset_all = subparsers.add_parser('unset-all', - help="""Comment out all #define - whose name contains a match for - REGEX.""") - parser_unset_all.add_argument('regexs', metavar='REGEX', nargs='*') - - def add_adapter(name, function, description): - subparser = subparsers.add_parser(name, help=description) - subparser.set_defaults(adapter=function) - add_adapter('baremetal', baremetal_adapter, - """Like full, but exclude features that require platform - features such as file input-output.""") - add_adapter('baremetal_size', baremetal_size_adapter, - """Like baremetal, but exclude debugging features. - Useful for code size measurements.""") - add_adapter('full', full_adapter, - """Uncomment most features. - Exclude alternative implementations and platform support - options, as well as some options that are awkward to test. - """) - add_adapter('full_no_deprecated', no_deprecated_adapter(full_adapter), - """Uncomment most non-deprecated features. - Like "full", but without deprecated features. - """) - add_adapter('full_no_platform', no_platform_adapter(full_adapter), - """Uncomment most non-platform features. - Like "full", but without platform features. - """) - add_adapter('realfull', realfull_adapter, - """Uncomment all boolean #defines. - Suitable for generating documentation, but not for building.""") - add_adapter('crypto', crypto_adapter(None), - """Only include crypto features. Exclude X.509 and TLS.""") - add_adapter('crypto_baremetal', crypto_adapter(baremetal_adapter), - """Like baremetal, but with only crypto features, - excluding X.509 and TLS.""") - add_adapter('crypto_full', crypto_adapter(full_adapter), - """Like full, but with only crypto features, - excluding X.509 and TLS.""") - - args = parser.parse_args() - config = ConfigFile(args.file) - if args.command is None: - parser.print_help() - return 1 - elif args.command == 'get': - if args.symbol in config: - value = config[args.symbol] - if value: - sys.stdout.write(value + '\n') - return 0 if args.symbol in config else 1 - elif args.command == 'set': - if not args.force and args.symbol not in config.settings: - sys.stderr.write("A #define for the symbol {} " - "was not found in {}\n" - .format(args.symbol, config.filename)) - return 1 - config.set(args.symbol, value=args.value) - elif args.command == 'set-all': - config.change_matching(args.regexs, True) - elif args.command == 'unset': - config.unset(args.symbol) - elif args.command == 'unset-all': - config.change_matching(args.regexs, False) - else: - config.adapt(args.adapter) - config.write(args.write) - return 0 - - # Import modules only used by main only if main is defined and called. - # pylint: disable=wrong-import-position - import argparse - import sys - sys.exit(main()) + sys.exit(MbedTLSConfigTool().main())