From 11e4f5ac1c71fe7d803fa5193236560b2e176cea Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Sat, 1 Mar 2025 16:09:27 +0100 Subject: [PATCH 1/7] New script to generate handshake tests for ssl-opt.sh Signed-off-by: Gilles Peskine --- scripts/generate_tls_handshake_tests.py | 42 ++++++++++++ scripts/mbedtls_framework/tls_test_case.py | 74 ++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100755 scripts/generate_tls_handshake_tests.py create mode 100644 scripts/mbedtls_framework/tls_test_case.py diff --git a/scripts/generate_tls_handshake_tests.py b/scripts/generate_tls_handshake_tests.py new file mode 100755 index 000000000..78c7a42c3 --- /dev/null +++ b/scripts/generate_tls_handshake_tests.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +""" +Generate miscellaneous TLS test cases relating to the handshake. +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +import argparse +import sys +from typing import Optional + +from mbedtls_framework import tls_test_case +from mbedtls_framework import typing_util +def write_handshake_tests(out: typing_util.Writable) -> None: + """Generate handshake tests.""" + out.write(f"""\ +# Miscellaneous tests related to the TLS handshake layer. +# +# Automatically generated by {sys.argv[0]}. Do not edit! + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +""") + out.write("""\ +# End of automatically generated file. +""") + +def main() -> None: + """Command line entry point.""" + parser = argparse.ArgumentParser() + parser.add_argument('-o', '--output', + default='tests/opt-testcases/handshake-generated.sh', + help='Output file') + args = parser.parse_args() + with open(args.output, 'w') as out: + write_handshake_tests(out) + +if __name__ == '__main__': + main() diff --git a/scripts/mbedtls_framework/tls_test_case.py b/scripts/mbedtls_framework/tls_test_case.py new file mode 100644 index 000000000..214a7ed46 --- /dev/null +++ b/scripts/mbedtls_framework/tls_test_case.py @@ -0,0 +1,74 @@ +"""Library for constructing an Mbed TLS ssl-opt test case. +""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later + +import enum +import re +from typing import List + +from . import typing_util + + +class TestCase: + """Data about an ssl-opt test case.""" + #pylint: disable=too-few-public-methods + + def __init__(self, description: str) -> None: + # List of shell snippets to call before run_test, typically + # calls to requires_xxx functions. + self.requirements = [] #type: List[str] + # Test case description (first argument to run_test). + self.description = description + # Client command line. + # This will be placed directly inside double quotes in the shell script. + self.client = '$P_CLI' + # Server command line. + # This will be placed directly inside double quotes in the shell script. + self.server = '$P_SRV' + # Expected client exit code. + self.exit_code = 0 + # BRE for text that must be present in the client log (run_test -c). + self.wanted_client_patterns = [] #type: List[str] + # BRE for text that must be present in the server log (run_test -s). + self.wanted_server_patterns = [] #type: List[str] + # BRE for text that must not be present in the client log (run_test -C). + self.forbidden_client_patterns = [] #type: List[str] + # BRE for text that must not be present in the server log (run_test -S). + self.forbidden_server_patterns = [] #type: List[str] + + @staticmethod + def _quote(raw: str) -> str: + """Quote the given string for sh. + + Use double quotes, because that's currently the norm in ssl-opt.sh. + """ + return '"' + re.sub(r'([$"\\`])', r'\\\1', raw) + '"' + + def write(self, out: typing_util.Writable) -> None: + """Write the test case to the specified file.""" + for req in self.requirements: + out.write(req + '\n') + out.write(f'run_test {self._quote(self.description)} \\\n') + out.write(f' "{self.server}" \\\n') + out.write(f' "{self.client}" \\\n') + out.write(f' {self.exit_code}') + for pat in self.wanted_server_patterns: + out.write(' \\\n -s ' + self._quote(pat)) + for pat in self.forbidden_server_patterns: + out.write(' \\\n -S ' + self._quote(pat)) + for pat in self.wanted_client_patterns: + out.write(' \\\n -c ' + self._quote(pat)) + for pat in self.forbidden_client_patterns: + out.write(' \\\n -C ' + self._quote(pat)) + out.write('\n\n') + + +class Side(enum.Enum): + CLIENT = 0 + SERVER = 1 + +class Version(enum.Enum): + TLS12 = 2 + TLS13 = 3 From e453777af159ba34b932fdbc9dc00c9d4673125c Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Sat, 1 Mar 2025 16:44:02 +0100 Subject: [PATCH 2/7] Generate handshake defragmentation test cases The output is identical to the manually written tests in `tests/opt-testcases/handshake-manual.sh`, except that the script doesn't generate explanatory comments (they're in the generator script instead). Signed-off-by: Gilles Peskine --- scripts/generate_tls_handshake_tests.py | 132 ++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/scripts/generate_tls_handshake_tests.py b/scripts/generate_tls_handshake_tests.py index 78c7a42c3..cc1325661 100755 --- a/scripts/generate_tls_handshake_tests.py +++ b/scripts/generate_tls_handshake_tests.py @@ -13,6 +13,137 @@ from typing import Optional from mbedtls_framework import tls_test_case from mbedtls_framework import typing_util + +from mbedtls_framework.tls_test_case import Side, Version + + +# Assume that a TLS 1.2 ClientHello used in these tests will be at most +# this many bytes long. +TLS12_CLIENT_HELLO_ASSUMED_MAX_LENGTH = 255 + +# Minimum handshake fragment length that Mbed TLS supports. +TLS_HANDSHAKE_FRAGMENT_MIN_LENGTH = 4 + +def write_tls_handshake_defragmentation_test( + out: typing_util.Writable, + side: Side, + length: Optional[int], + version: Optional[Version] = None +) -> None: + """Generate one TLS handshake defragmentation test. + + :param out: file to write to. + :param side: which side is Mbed TLS. + :param length: fragment length, or None to not fragment. + :param version: protocol version, if forced. + """ + #pylint: disable=chained-comparison,too-many-branches,too-many-statements + + our_args = '' + their_args = '' + + if length is None: + description = 'no fragmentation, for reference' + else: + description = 'len=' + str(length) + if version is not None: + description += ', TLS 1.' + str(version.value) + description = f'Handshake defragmentation on {side.name.lower()}: {description}' + tc = tls_test_case.TestCase(description) + + if version == Version.TLS12 and \ + length is not None and \ + length >= TLS_HANDSHAKE_FRAGMENT_MIN_LENGTH and \ + length < 16 and \ + side == side.CLIENT: + # Skip test cases where the Finished message is fragmented in TLS 1.2. + # This is currently buggy when the symmetric encryption used an + # explicit IV (CBC, GCM or CCM; Chachapoly and null work, as does + # TLS 1.3, because they use a purely implicit IV). + tc.requirements.append('skip_next_test') + + if version is not None: + their_args += ' -tls1_' + str(version.value) + # Emit a version requirement, because we're forcing the version via + # OpenSSL, not via Mbed TLS, and the automatic depdendencies in + # ssl-opt.sh only handle forcing the version via Mbed TLS. + tc.requirements.append('requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_' + + str(version.value)) + if side == Side.SERVER and version == Version.TLS12 and \ + length is not None and \ + length <= TLS12_CLIENT_HELLO_ASSUMED_MAX_LENGTH: + # Server-side ClientHello defragmentation is only supported in + # the TLS 1.3 message parser. When that parser sees an 1.2-only + # ClientHello, it forwards the reassembled record to the + # TLS 1.2 ClientHello parser so the ClientHello can be fragmented. + # When TLS 1.3 support is disabled in the server (at compile-time + # or at runtime), the TLS 1.2 ClientHello parser only sees + # the first fragment of the ClientHello. + tc.requirements.append('requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_3') + tc.description += ' TLS 1.3 ClientHello -> 1.2 Handshake' + + # To guarantee that the handhake messages are large enough and need to be + # split into fragments, the tests require certificate authentication. + # The party in control of the fragmentation operations is OpenSSL and + # will always use server5.crt (548 Bytes). + if length is not None and \ + length >= TLS_HANDSHAKE_FRAGMENT_MIN_LENGTH: + tc.requirements.append('requires_certificate_authentication') + if version == Version.TLS12 and side == Side.CLIENT: + #The server uses an ECDSA cert, so make sure we have a compatible key exchange + tc.requirements.append( + 'requires_config_enabled MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED') + + if length is None: + forbidden_patterns = [ + 'reassembled record', + 'waiting for more fragments', + ] + wanted_patterns = [] + elif length < TLS_HANDSHAKE_FRAGMENT_MIN_LENGTH: + their_args += ' -split_send_frag ' + str(length) + tc.exit_code = 1 + forbidden_patterns = [] + wanted_patterns = [ + 'handshake message too short: ' + str(length), + 'SSL - An invalid SSL record was received', + ] + if side == Side.SERVER: + wanted_patterns[0:0] = ['<= parse client hello'] + elif version == Version.TLS13: + wanted_patterns[0:0] = ['=> ssl_tls13_process_server_hello'] + else: + their_args += ' -split_send_frag ' + str(length) + forbidden_patterns = [] + wanted_patterns = [ + 'reassembled record', + fr'handshake fragment: 0 \.\. {length} of [0-9]\+ msglen {length}', + fr'waiting for more fragments ({length} of', + ] + + if side == Side.CLIENT: + tc.client = '$P_CLI debug_level=4' + our_args + tc.server = '$O_NEXT_SRV' + their_args + tc.wanted_client_patterns = wanted_patterns + tc.forbidden_client_patterns = forbidden_patterns + else: + their_args += ' -cert $DATA_FILES_PATH/server5.crt -key $DATA_FILES_PATH/server5.key' + our_args += ' auth_mode=required' + tc.client = '$O_NEXT_CLI' + their_args + tc.server = '$P_SRV debug_level=4' + our_args + tc.wanted_server_patterns = wanted_patterns + tc.forbidden_server_patterns = forbidden_patterns + tc.write(out) + +def write_tls_handshake_defragmentation_tests(out: typing_util.Writable) -> None: + """Generate TLS handshake defragmentation tests.""" + for side in Side.CLIENT, Side.SERVER: + write_tls_handshake_defragmentation_test(out, side, None) + for length in [512, 513, 256, 128, 64, 36, 32, 16, 13, 5, 4, 3]: + write_tls_handshake_defragmentation_test(out, side, length, Version.TLS13) + write_tls_handshake_defragmentation_test(out, side, length, Version.TLS12) + + def write_handshake_tests(out: typing_util.Writable) -> None: """Generate handshake tests.""" out.write(f"""\ @@ -24,6 +155,7 @@ def write_handshake_tests(out: typing_util.Writable) -> None: # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later """) + write_tls_handshake_defragmentation_tests(out) out.write("""\ # End of automatically generated file. """) From f88eb21ff11afe2c9ed553dcdba27166198f90d9 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Sat, 1 Mar 2025 18:32:06 +0100 Subject: [PATCH 3/7] Don't embed a path in the generated output Signed-off-by: Gilles Peskine --- scripts/generate_tls_handshake_tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/generate_tls_handshake_tests.py b/scripts/generate_tls_handshake_tests.py index cc1325661..7fdb5528c 100755 --- a/scripts/generate_tls_handshake_tests.py +++ b/scripts/generate_tls_handshake_tests.py @@ -8,6 +8,7 @@ Generate miscellaneous TLS test cases relating to the handshake. # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later import argparse +import os import sys from typing import Optional @@ -149,7 +150,7 @@ def write_handshake_tests(out: typing_util.Writable) -> None: out.write(f"""\ # Miscellaneous tests related to the TLS handshake layer. # -# Automatically generated by {sys.argv[0]}. Do not edit! +# Automatically generated by {os.path.basename(sys.argv[0])}. Do not edit! # Copyright The Mbed TLS Contributors # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later From 69385652d08eabd0756dff68719fd1809719b081 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 4 Mar 2025 18:29:21 +0100 Subject: [PATCH 4/7] Fix TLS 1.3 tests with OpenSSL failing in pure-PSK builds Signed-off-by: Gilles Peskine --- scripts/generate_tls_handshake_tests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/generate_tls_handshake_tests.py b/scripts/generate_tls_handshake_tests.py index 7fdb5528c..53846ba93 100755 --- a/scripts/generate_tls_handshake_tests.py +++ b/scripts/generate_tls_handshake_tests.py @@ -94,6 +94,10 @@ def write_tls_handshake_defragmentation_test( #The server uses an ECDSA cert, so make sure we have a compatible key exchange tc.requirements.append( 'requires_config_enabled MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED') + else: + # This test case may run in a pure-PSK configuration. OpenSSL doesn't + # allow this by default with TLS 1.3. + their_args += ' -allow_no_dhe_kex' if length is None: forbidden_patterns = [ From 6749a8dcf7607b6252e0d273d86bc9a246ca0192 Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 4 Mar 2025 18:45:34 +0100 Subject: [PATCH 5/7] Briefly explain BRE Signed-off-by: Gilles Peskine --- scripts/mbedtls_framework/tls_test_case.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/mbedtls_framework/tls_test_case.py b/scripts/mbedtls_framework/tls_test_case.py index 214a7ed46..47e356445 100644 --- a/scripts/mbedtls_framework/tls_test_case.py +++ b/scripts/mbedtls_framework/tls_test_case.py @@ -29,6 +29,16 @@ class TestCase: self.server = '$P_SRV' # Expected client exit code. self.exit_code = 0 + + # Note that all patterns matched in the logs are in BRE + # (Basic Regular Expression) syntax, more precisely in the BRE + # dialect that is the default for GNU grep. The main difference + # with Python regular expressions is that the operators for + # grouping `\(...\)`, alternation `x\|y`, option `x\?`, + # one-or-more `x\+` and repetition ranges `x\{M,N\}` must be + # preceded by a backslash. The characters `()|?+{}` stand for + # themselves. + # BRE for text that must be present in the client log (run_test -c). self.wanted_client_patterns = [] #type: List[str] # BRE for text that must be present in the server log (run_test -s). From c69a7f6c2613e5815f804049847efb88ef68d9bf Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 4 Mar 2025 18:49:29 +0100 Subject: [PATCH 6/7] Use more abstractions for protocol version formatting Signed-off-by: Gilles Peskine --- scripts/generate_tls_handshake_tests.py | 5 ++--- scripts/mbedtls_framework/tls_test_case.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/generate_tls_handshake_tests.py b/scripts/generate_tls_handshake_tests.py index 53846ba93..3c23154a5 100755 --- a/scripts/generate_tls_handshake_tests.py +++ b/scripts/generate_tls_handshake_tests.py @@ -64,12 +64,11 @@ def write_tls_handshake_defragmentation_test( tc.requirements.append('skip_next_test') if version is not None: - their_args += ' -tls1_' + str(version.value) + their_args += ' ' + version.openssl_option() # Emit a version requirement, because we're forcing the version via # OpenSSL, not via Mbed TLS, and the automatic depdendencies in # ssl-opt.sh only handle forcing the version via Mbed TLS. - tc.requirements.append('requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_' + - str(version.value)) + tc.requirements.append(version.requires_command()) if side == Side.SERVER and version == Version.TLS12 and \ length is not None and \ length <= TLS12_CLIENT_HELLO_ASSUMED_MAX_LENGTH: diff --git a/scripts/mbedtls_framework/tls_test_case.py b/scripts/mbedtls_framework/tls_test_case.py index 47e356445..73bb039a8 100644 --- a/scripts/mbedtls_framework/tls_test_case.py +++ b/scripts/mbedtls_framework/tls_test_case.py @@ -80,5 +80,22 @@ class Side(enum.Enum): SERVER = 1 class Version(enum.Enum): + """TLS protocol version. + + This class doesn't know about DTLS yet. + """ + TLS12 = 2 TLS13 = 3 + + def force_version(self) -> str: + """Argument to pass to ssl_client2 or ssl_server2 to force this version.""" + return f'force_version=tls1{self.value}' + + def openssl_option(self) -> str: + """Option to pass to openssl s_client or openssl s_server to select this version.""" + return f'-tls1_{self.value}' + + def requires_command(self) -> str: + """Command to require this protocol version in an ssl-opt.sh test case.""" + return 'requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_' + str(self.value) From 4a009d4b3cf6c55a558d90c92c1aa2d1ea2bb99b Mon Sep 17 00:00:00 2001 From: Gilles Peskine Date: Tue, 4 Mar 2025 18:50:33 +0100 Subject: [PATCH 7/7] Improve --help Signed-off-by: Gilles Peskine --- scripts/generate_tls_handshake_tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/generate_tls_handshake_tests.py b/scripts/generate_tls_handshake_tests.py index 3c23154a5..86bf9f5b0 100755 --- a/scripts/generate_tls_handshake_tests.py +++ b/scripts/generate_tls_handshake_tests.py @@ -167,9 +167,10 @@ def write_handshake_tests(out: typing_util.Writable) -> None: def main() -> None: """Command line entry point.""" parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('-o', '--output', default='tests/opt-testcases/handshake-generated.sh', - help='Output file') + help='Output file (default: tests/opt-testcases/handshake-generated.sh)') args = parser.parse_args() with open(args.output, 'w') as out: write_handshake_tests(out)