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