1#!/usr/bin/env python3
2
3"""Mbed TLS configuration file manipulation library and tool
4
5Basic usage, to read the Mbed TLS configuration:
6    config = ConfigFile()
7    if 'MBEDTLS_RSA_C' in config: print('RSA is enabled')
8"""
9
10# Note that as long as Mbed TLS 2.28 LTS is maintained, the version of
11# this script in the mbedtls-2.28 branch must remain compatible with
12# Python 3.4. The version in development may only use more recent features
13# in parts that are not backported to 2.28.
14
15## Copyright The Mbed TLS Contributors
16## SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
17##
18
19import os
20import re
21
22class Setting:
23    """Representation of one Mbed TLS mbedtls_config.h setting.
24
25    Fields:
26    * name: the symbol name ('MBEDTLS_xxx').
27    * value: the value of the macro. The empty string for a plain #define
28      with no value.
29    * active: True if name is defined, False if a #define for name is
30      present in mbedtls_config.h but commented out.
31    * section: the name of the section that contains this symbol.
32    """
33    # pylint: disable=too-few-public-methods
34    def __init__(self, active, name, value='', section=None):
35        self.active = active
36        self.name = name
37        self.value = value
38        self.section = section
39
40class Config:
41    """Representation of the Mbed TLS configuration.
42
43    In the documentation of this class, a symbol is said to be *active*
44    if there is a #define for it that is not commented out, and *known*
45    if there is a #define for it whether commented out or not.
46
47    This class supports the following protocols:
48    * `name in config` is `True` if the symbol `name` is active, `False`
49      otherwise (whether `name` is inactive or not known).
50    * `config[name]` is the value of the macro `name`. If `name` is inactive,
51      raise `KeyError` (even if `name` is known).
52    * `config[name] = value` sets the value associated to `name`. `name`
53      must be known, but does not need to be set. This does not cause
54      name to become set.
55    """
56
57    def __init__(self):
58        self.settings = {}
59
60    def __contains__(self, name):
61        """True if the given symbol is active (i.e. set).
62
63        False if the given symbol is not set, even if a definition
64        is present but commented out.
65        """
66        return name in self.settings and self.settings[name].active
67
68    def all(self, *names):
69        """True if all the elements of names are active (i.e. set)."""
70        return all(self.__contains__(name) for name in names)
71
72    def any(self, *names):
73        """True if at least one symbol in names are active (i.e. set)."""
74        return any(self.__contains__(name) for name in names)
75
76    def known(self, name):
77        """True if a #define for name is present, whether it's commented out or not."""
78        return name in self.settings
79
80    def __getitem__(self, name):
81        """Get the value of name, i.e. what the preprocessor symbol expands to.
82
83        If name is not known, raise KeyError. name does not need to be active.
84        """
85        return self.settings[name].value
86
87    def get(self, name, default=None):
88        """Get the value of name. If name is inactive (not set), return default.
89
90        If a #define for name is present and not commented out, return
91        its expansion, even if this is the empty string.
92
93        If a #define for name is present but commented out, return default.
94        """
95        if name in self.settings:
96            return self.settings[name].value
97        else:
98            return default
99
100    def __setitem__(self, name, value):
101        """If name is known, set its value.
102
103        If name is not known, raise KeyError.
104        """
105        self.settings[name].value = value
106
107    def set(self, name, value=None):
108        """Set name to the given value and make it active.
109
110        If value is None and name is already known, don't change its value.
111        If value is None and name is not known, set its value to the empty
112        string.
113        """
114        if name in self.settings:
115            if value is not None:
116                self.settings[name].value = value
117            self.settings[name].active = True
118        else:
119            self.settings[name] = Setting(True, name, value=value)
120
121    def unset(self, name):
122        """Make name unset (inactive).
123
124        name remains known if it was known before.
125        """
126        if name not in self.settings:
127            return
128        self.settings[name].active = False
129
130    def adapt(self, adapter):
131        """Run adapter on each known symbol and (de)activate it accordingly.
132
133        `adapter` must be a function that returns a boolean. It is called as
134        `adapter(name, active, section)` for each setting, where `active` is
135        `True` if `name` is set and `False` if `name` is known but unset,
136        and `section` is the name of the section containing `name`. If
137        `adapter` returns `True`, then set `name` (i.e. make it active),
138        otherwise unset `name` (i.e. make it known but inactive).
139        """
140        for setting in self.settings.values():
141            setting.active = adapter(setting.name, setting.active,
142                                     setting.section)
143
144    def change_matching(self, regexs, enable):
145        """Change all symbols matching one of the regexs to the desired state."""
146        if not regexs:
147            return
148        regex = re.compile('|'.join(regexs))
149        for setting in self.settings.values():
150            if regex.search(setting.name):
151                setting.active = enable
152
153def is_full_section(section):
154    """Is this section affected by "config.py full" and friends?"""
155    return section.endswith('support') or section.endswith('modules')
156
157def realfull_adapter(_name, active, section):
158    """Activate all symbols found in the global and boolean feature sections.
159
160    This is intended for building the documentation, including the
161    documentation of settings that are activated by defining an optional
162    preprocessor macro.
163
164    Do not activate definitions in the section containing symbols that are
165    supposed to be defined and documented in their own module.
166    """
167    if section == 'Module configuration options':
168        return active
169    return True
170
171# The goal of the full configuration is to have everything that can be tested
172# together. This includes deprecated or insecure options. It excludes:
173# * Options that require additional build dependencies or unusual hardware.
174# * Options that make testing less effective.
175# * Options that are incompatible with other options, or more generally that
176#   interact with other parts of the code in such a way that a bulk enabling
177#   is not a good way to test them.
178# * Options that remove features.
179EXCLUDE_FROM_FULL = frozenset([
180    #pylint: disable=line-too-long
181    'MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH', # interacts with CTR_DRBG_128_BIT_KEY
182    'MBEDTLS_AES_USE_HARDWARE_ONLY', # hardware dependency
183    'MBEDTLS_CTR_DRBG_USE_128_BIT_KEY', # interacts with ENTROPY_FORCE_SHA256
184    'MBEDTLS_DEPRECATED_REMOVED', # conflicts with deprecated options
185    'MBEDTLS_DEPRECATED_WARNING', # conflicts with deprecated options
186    'MBEDTLS_ECDH_VARIANT_EVEREST_ENABLED', # influences the use of ECDH in TLS
187    'MBEDTLS_ECP_NO_FALLBACK', # removes internal ECP implementation
188    'MBEDTLS_ECP_WITH_MPI_UINT', # disables the default ECP and is experimental
189    'MBEDTLS_ENTROPY_FORCE_SHA256', # interacts with CTR_DRBG_128_BIT_KEY
190    'MBEDTLS_HAVE_SSE2', # hardware dependency
191    'MBEDTLS_MEMORY_BACKTRACE', # depends on MEMORY_BUFFER_ALLOC_C
192    'MBEDTLS_MEMORY_BUFFER_ALLOC_C', # makes sanitizers (e.g. ASan) less effective
193    'MBEDTLS_MEMORY_DEBUG', # depends on MEMORY_BUFFER_ALLOC_C
194    'MBEDTLS_NO_64BIT_MULTIPLICATION', # influences anything that uses bignum
195    'MBEDTLS_NO_DEFAULT_ENTROPY_SOURCES', # removes a feature
196    'MBEDTLS_NO_PLATFORM_ENTROPY', # removes a feature
197    'MBEDTLS_NO_UDBL_DIVISION', # influences anything that uses bignum
198    'MBEDTLS_PSA_P256M_DRIVER_ENABLED', # influences SECP256R1 KeyGen/ECDH/ECDSA
199    'MBEDTLS_PLATFORM_NO_STD_FUNCTIONS', # removes a feature
200    'MBEDTLS_PSA_CRYPTO_EXTERNAL_RNG', # behavior change + build dependency
201    'MBEDTLS_PSA_CRYPTO_KEY_ID_ENCODES_OWNER', # incompatible with USE_PSA_CRYPTO
202    'MBEDTLS_PSA_CRYPTO_SPM', # platform dependency (PSA SPM)
203    'MBEDTLS_PSA_INJECT_ENTROPY', # conflicts with platform entropy sources
204    'MBEDTLS_RSA_NO_CRT', # influences the use of RSA in X.509 and TLS
205    'MBEDTLS_SHA256_USE_A64_CRYPTO_ONLY', # interacts with *_USE_A64_CRYPTO_IF_PRESENT
206    'MBEDTLS_SHA512_USE_A64_CRYPTO_ONLY', # interacts with *_USE_A64_CRYPTO_IF_PRESENT
207    'MBEDTLS_SSL_RECORD_SIZE_LIMIT', # in development, currently breaks other tests
208    'MBEDTLS_TEST_CONSTANT_FLOW_MEMSAN', # build dependency (clang+memsan)
209    'MBEDTLS_TEST_CONSTANT_FLOW_VALGRIND', # build dependency (valgrind headers)
210    'MBEDTLS_X509_REMOVE_INFO', # removes a feature
211])
212
213def is_seamless_alt(name):
214    """Whether the xxx_ALT symbol should be included in the full configuration.
215
216    Include alternative implementations of platform functions, which are
217    configurable function pointers that default to the built-in function.
218    This way we test that the function pointers exist and build correctly
219    without changing the behavior, and tests can verify that the function
220    pointers are used by modifying those pointers.
221
222    Exclude alternative implementations of library functions since they require
223    an implementation of the relevant functions and an xxx_alt.h header.
224    """
225    if name in (
226            'MBEDTLS_PLATFORM_GMTIME_R_ALT',
227            'MBEDTLS_PLATFORM_SETUP_TEARDOWN_ALT',
228            'MBEDTLS_PLATFORM_MS_TIME_ALT',
229            'MBEDTLS_PLATFORM_ZEROIZE_ALT',
230    ):
231        # Similar to non-platform xxx_ALT, requires platform_alt.h
232        return False
233    return name.startswith('MBEDTLS_PLATFORM_')
234
235def include_in_full(name):
236    """Rules for symbols in the "full" configuration."""
237    if name in EXCLUDE_FROM_FULL:
238        return False
239    if name.endswith('_ALT'):
240        return is_seamless_alt(name)
241    return True
242
243def full_adapter(name, active, section):
244    """Config adapter for "full"."""
245    if not is_full_section(section):
246        return active
247    return include_in_full(name)
248
249# The baremetal configuration excludes options that require a library or
250# operating system feature that is typically not present on bare metal
251# systems. Features that are excluded from "full" won't be in "baremetal"
252# either (unless explicitly turned on in baremetal_adapter) so they don't
253# need to be repeated here.
254EXCLUDE_FROM_BAREMETAL = frozenset([
255    #pylint: disable=line-too-long
256    'MBEDTLS_ENTROPY_NV_SEED', # requires a filesystem and FS_IO or alternate NV seed hooks
257    'MBEDTLS_FS_IO', # requires a filesystem
258    'MBEDTLS_HAVE_TIME', # requires a clock
259    'MBEDTLS_HAVE_TIME_DATE', # requires a clock
260    'MBEDTLS_NET_C', # requires POSIX-like networking
261    'MBEDTLS_PLATFORM_FPRINTF_ALT', # requires FILE* from stdio.h
262    'MBEDTLS_PLATFORM_NV_SEED_ALT', # requires a filesystem and ENTROPY_NV_SEED
263    'MBEDTLS_PLATFORM_TIME_ALT', # requires a clock and HAVE_TIME
264    'MBEDTLS_PSA_CRYPTO_SE_C', # requires a filesystem and PSA_CRYPTO_STORAGE_C
265    'MBEDTLS_PSA_CRYPTO_STORAGE_C', # requires a filesystem
266    'MBEDTLS_PSA_ITS_FILE_C', # requires a filesystem
267    'MBEDTLS_THREADING_C', # requires a threading interface
268    'MBEDTLS_THREADING_PTHREAD', # requires pthread
269    'MBEDTLS_TIMING_C', # requires a clock
270])
271
272def keep_in_baremetal(name):
273    """Rules for symbols in the "baremetal" configuration."""
274    if name in EXCLUDE_FROM_BAREMETAL:
275        return False
276    return True
277
278def baremetal_adapter(name, active, section):
279    """Config adapter for "baremetal"."""
280    if not is_full_section(section):
281        return active
282    if name == 'MBEDTLS_NO_PLATFORM_ENTROPY':
283        # No OS-provided entropy source
284        return True
285    return include_in_full(name) and keep_in_baremetal(name)
286
287# This set contains options that are mostly for debugging or test purposes,
288# and therefore should be excluded when doing code size measurements.
289# Options that are their own module (such as MBEDTLS_ERROR_C) are not listed
290# and therefore will be included when doing code size measurements.
291EXCLUDE_FOR_SIZE = frozenset([
292    'MBEDTLS_DEBUG_C', # large code size increase in TLS
293    'MBEDTLS_SELF_TEST', # increases the size of many modules
294    'MBEDTLS_TEST_HOOKS', # only useful with the hosted test framework, increases code size
295])
296
297def baremetal_size_adapter(name, active, section):
298    if name in EXCLUDE_FOR_SIZE:
299        return False
300    return baremetal_adapter(name, active, section)
301
302def include_in_crypto(name):
303    """Rules for symbols in a crypto configuration."""
304    if name.startswith('MBEDTLS_X509_') or \
305       name.startswith('MBEDTLS_SSL_') or \
306       name.startswith('MBEDTLS_KEY_EXCHANGE_'):
307        return False
308    if name in [
309            'MBEDTLS_DEBUG_C', # part of libmbedtls
310            'MBEDTLS_NET_C', # part of libmbedtls
311            'MBEDTLS_PKCS7_C', # part of libmbedx509
312    ]:
313        return False
314    return True
315
316def crypto_adapter(adapter):
317    """Modify an adapter to disable non-crypto symbols.
318
319    ``crypto_adapter(adapter)(name, active, section)`` is like
320    ``adapter(name, active, section)``, but unsets all X.509 and TLS symbols.
321    """
322    def continuation(name, active, section):
323        if not include_in_crypto(name):
324            return False
325        if adapter is None:
326            return active
327        return adapter(name, active, section)
328    return continuation
329
330DEPRECATED = frozenset([
331    'MBEDTLS_PSA_CRYPTO_SE_C',
332])
333def no_deprecated_adapter(adapter):
334    """Modify an adapter to disable deprecated symbols.
335
336    ``no_deprecated_adapter(adapter)(name, active, section)`` is like
337    ``adapter(name, active, section)``, but unsets all deprecated symbols
338    and sets ``MBEDTLS_DEPRECATED_REMOVED``.
339    """
340    def continuation(name, active, section):
341        if name == 'MBEDTLS_DEPRECATED_REMOVED':
342            return True
343        if name in DEPRECATED:
344            return False
345        if adapter is None:
346            return active
347        return adapter(name, active, section)
348    return continuation
349
350class ConfigFile(Config):
351    """Representation of the Mbed TLS configuration read for a file.
352
353    See the documentation of the `Config` class for methods to query
354    and modify the configuration.
355    """
356
357    _path_in_tree = 'include/mbedtls/mbedtls_config.h'
358    default_path = [_path_in_tree,
359                    os.path.join(os.path.dirname(__file__),
360                                 os.pardir,
361                                 _path_in_tree),
362                    os.path.join(os.path.dirname(os.path.abspath(os.path.dirname(__file__))),
363                                 _path_in_tree)]
364
365    def __init__(self, filename=None):
366        """Read the Mbed TLS configuration file."""
367        if filename is None:
368            for candidate in self.default_path:
369                if os.path.lexists(candidate):
370                    filename = candidate
371                    break
372            else:
373                raise Exception('Mbed TLS configuration file not found',
374                                self.default_path)
375        super().__init__()
376        self.filename = filename
377        self.current_section = 'header'
378        with open(filename, 'r', encoding='utf-8') as file:
379            self.templates = [self._parse_line(line) for line in file]
380        self.current_section = None
381
382    def set(self, name, value=None):
383        if name not in self.settings:
384            self.templates.append((name, '', '#define ' + name + ' '))
385        super().set(name, value)
386
387    _define_line_regexp = (r'(?P<indentation>\s*)' +
388                           r'(?P<commented_out>(//\s*)?)' +
389                           r'(?P<define>#\s*define\s+)' +
390                           r'(?P<name>\w+)' +
391                           r'(?P<arguments>(?:\((?:\w|\s|,)*\))?)' +
392                           r'(?P<separator>\s*)' +
393                           r'(?P<value>.*)')
394    _section_line_regexp = (r'\s*/?\*+\s*[\\@]name\s+SECTION:\s*' +
395                            r'(?P<section>.*)[ */]*')
396    _config_line_regexp = re.compile(r'|'.join([_define_line_regexp,
397                                                _section_line_regexp]))
398    def _parse_line(self, line):
399        """Parse a line in mbedtls_config.h and return the corresponding template."""
400        line = line.rstrip('\r\n')
401        m = re.match(self._config_line_regexp, line)
402        if m is None:
403            return line
404        elif m.group('section'):
405            self.current_section = m.group('section')
406            return line
407        else:
408            active = not m.group('commented_out')
409            name = m.group('name')
410            value = m.group('value')
411            template = (name,
412                        m.group('indentation'),
413                        m.group('define') + name +
414                        m.group('arguments') + m.group('separator'))
415            self.settings[name] = Setting(active, name, value,
416                                          self.current_section)
417            return template
418
419    def _format_template(self, name, indent, middle):
420        """Build a line for mbedtls_config.h for the given setting.
421
422        The line has the form "<indent>#define <name> <value>"
423        where <middle> is "#define <name> ".
424        """
425        setting = self.settings[name]
426        value = setting.value
427        if value is None:
428            value = ''
429        # Normally the whitespace to separate the symbol name from the
430        # value is part of middle, and there's no whitespace for a symbol
431        # with no value. But if a symbol has been changed from having a
432        # value to not having one, the whitespace is wrong, so fix it.
433        if value:
434            if middle[-1] not in '\t ':
435                middle += ' '
436        else:
437            middle = middle.rstrip()
438        return ''.join([indent,
439                        '' if setting.active else '//',
440                        middle,
441                        value]).rstrip()
442
443    def write_to_stream(self, output):
444        """Write the whole configuration to output."""
445        for template in self.templates:
446            if isinstance(template, str):
447                line = template
448            else:
449                line = self._format_template(*template)
450            output.write(line + '\n')
451
452    def write(self, filename=None):
453        """Write the whole configuration to the file it was read from.
454
455        If filename is specified, write to this file instead.
456        """
457        if filename is None:
458            filename = self.filename
459        with open(filename, 'w', encoding='utf-8') as output:
460            self.write_to_stream(output)
461
462if __name__ == '__main__':
463    def main():
464        """Command line mbedtls_config.h manipulation tool."""
465        parser = argparse.ArgumentParser(description="""
466        Mbed TLS configuration file manipulation tool.
467        """)
468        parser.add_argument('--file', '-f',
469                            help="""File to read (and modify if requested).
470                            Default: {}.
471                            """.format(ConfigFile.default_path))
472        parser.add_argument('--force', '-o',
473                            action='store_true',
474                            help="""For the set command, if SYMBOL is not
475                            present, add a definition for it.""")
476        parser.add_argument('--write', '-w', metavar='FILE',
477                            help="""File to write to instead of the input file.""")
478        subparsers = parser.add_subparsers(dest='command',
479                                           title='Commands')
480        parser_get = subparsers.add_parser('get',
481                                           help="""Find the value of SYMBOL
482                                           and print it. Exit with
483                                           status 0 if a #define for SYMBOL is
484                                           found, 1 otherwise.
485                                           """)
486        parser_get.add_argument('symbol', metavar='SYMBOL')
487        parser_set = subparsers.add_parser('set',
488                                           help="""Set SYMBOL to VALUE.
489                                           If VALUE is omitted, just uncomment
490                                           the #define for SYMBOL.
491                                           Error out of a line defining
492                                           SYMBOL (commented or not) is not
493                                           found, unless --force is passed.
494                                           """)
495        parser_set.add_argument('symbol', metavar='SYMBOL')
496        parser_set.add_argument('value', metavar='VALUE', nargs='?',
497                                default='')
498        parser_set_all = subparsers.add_parser('set-all',
499                                               help="""Uncomment all #define
500                                               whose name contains a match for
501                                               REGEX.""")
502        parser_set_all.add_argument('regexs', metavar='REGEX', nargs='*')
503        parser_unset = subparsers.add_parser('unset',
504                                             help="""Comment out the #define
505                                             for SYMBOL. Do nothing if none
506                                             is present.""")
507        parser_unset.add_argument('symbol', metavar='SYMBOL')
508        parser_unset_all = subparsers.add_parser('unset-all',
509                                                 help="""Comment out all #define
510                                                 whose name contains a match for
511                                                 REGEX.""")
512        parser_unset_all.add_argument('regexs', metavar='REGEX', nargs='*')
513
514        def add_adapter(name, function, description):
515            subparser = subparsers.add_parser(name, help=description)
516            subparser.set_defaults(adapter=function)
517        add_adapter('baremetal', baremetal_adapter,
518                    """Like full, but exclude features that require platform
519                    features such as file input-output.""")
520        add_adapter('baremetal_size', baremetal_size_adapter,
521                    """Like baremetal, but exclude debugging features.
522                    Useful for code size measurements.""")
523        add_adapter('full', full_adapter,
524                    """Uncomment most features.
525                    Exclude alternative implementations and platform support
526                    options, as well as some options that are awkward to test.
527                    """)
528        add_adapter('full_no_deprecated', no_deprecated_adapter(full_adapter),
529                    """Uncomment most non-deprecated features.
530                    Like "full", but without deprecated features.
531                    """)
532        add_adapter('realfull', realfull_adapter,
533                    """Uncomment all boolean #defines.
534                    Suitable for generating documentation, but not for building.""")
535        add_adapter('crypto', crypto_adapter(None),
536                    """Only include crypto features. Exclude X.509 and TLS.""")
537        add_adapter('crypto_baremetal', crypto_adapter(baremetal_adapter),
538                    """Like baremetal, but with only crypto features,
539                    excluding X.509 and TLS.""")
540        add_adapter('crypto_full', crypto_adapter(full_adapter),
541                    """Like full, but with only crypto features,
542                    excluding X.509 and TLS.""")
543
544        args = parser.parse_args()
545        config = ConfigFile(args.file)
546        if args.command is None:
547            parser.print_help()
548            return 1
549        elif args.command == 'get':
550            if args.symbol in config:
551                value = config[args.symbol]
552                if value:
553                    sys.stdout.write(value + '\n')
554            return 0 if args.symbol in config else 1
555        elif args.command == 'set':
556            if not args.force and args.symbol not in config.settings:
557                sys.stderr.write("A #define for the symbol {} "
558                                 "was not found in {}\n"
559                                 .format(args.symbol, config.filename))
560                return 1
561            config.set(args.symbol, value=args.value)
562        elif args.command == 'set-all':
563            config.change_matching(args.regexs, True)
564        elif args.command == 'unset':
565            config.unset(args.symbol)
566        elif args.command == 'unset-all':
567            config.change_matching(args.regexs, False)
568        else:
569            config.adapt(args.adapter)
570        config.write(args.write)
571        return 0
572
573    # Import modules only used by main only if main is defined and called.
574    # pylint: disable=wrong-import-position
575    import argparse
576    import sys
577    sys.exit(main())
578