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. """)