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