Skip to content

Commit 0a94d05

Browse files
Merge pull request #142 from gilles-peskine-arm/tls-defragment-generate-tests-framework
Generate TLS handshake defragmentation tests
2 parents 523a12d + 4a009d4 commit 0a94d05

File tree

2 files changed

+280
-0
lines changed

2 files changed

+280
-0
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Generate miscellaneous TLS test cases relating to the handshake.
5+
"""
6+
7+
# Copyright The Mbed TLS Contributors
8+
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
9+
10+
import argparse
11+
import os
12+
import sys
13+
from typing import Optional
14+
15+
from mbedtls_framework import tls_test_case
16+
from mbedtls_framework import typing_util
17+
18+
from mbedtls_framework.tls_test_case import Side, Version
19+
20+
21+
# Assume that a TLS 1.2 ClientHello used in these tests will be at most
22+
# this many bytes long.
23+
TLS12_CLIENT_HELLO_ASSUMED_MAX_LENGTH = 255
24+
25+
# Minimum handshake fragment length that Mbed TLS supports.
26+
TLS_HANDSHAKE_FRAGMENT_MIN_LENGTH = 4
27+
28+
def write_tls_handshake_defragmentation_test(
29+
out: typing_util.Writable,
30+
side: Side,
31+
length: Optional[int],
32+
version: Optional[Version] = None
33+
) -> None:
34+
"""Generate one TLS handshake defragmentation test.
35+
36+
:param out: file to write to.
37+
:param side: which side is Mbed TLS.
38+
:param length: fragment length, or None to not fragment.
39+
:param version: protocol version, if forced.
40+
"""
41+
#pylint: disable=chained-comparison,too-many-branches,too-many-statements
42+
43+
our_args = ''
44+
their_args = ''
45+
46+
if length is None:
47+
description = 'no fragmentation, for reference'
48+
else:
49+
description = 'len=' + str(length)
50+
if version is not None:
51+
description += ', TLS 1.' + str(version.value)
52+
description = f'Handshake defragmentation on {side.name.lower()}: {description}'
53+
tc = tls_test_case.TestCase(description)
54+
55+
if version == Version.TLS12 and \
56+
length is not None and \
57+
length >= TLS_HANDSHAKE_FRAGMENT_MIN_LENGTH and \
58+
length < 16 and \
59+
side == side.CLIENT:
60+
# Skip test cases where the Finished message is fragmented in TLS 1.2.
61+
# This is currently buggy when the symmetric encryption used an
62+
# explicit IV (CBC, GCM or CCM; Chachapoly and null work, as does
63+
# TLS 1.3, because they use a purely implicit IV).
64+
tc.requirements.append('skip_next_test')
65+
66+
if version is not None:
67+
their_args += ' ' + version.openssl_option()
68+
# Emit a version requirement, because we're forcing the version via
69+
# OpenSSL, not via Mbed TLS, and the automatic depdendencies in
70+
# ssl-opt.sh only handle forcing the version via Mbed TLS.
71+
tc.requirements.append(version.requires_command())
72+
if side == Side.SERVER and version == Version.TLS12 and \
73+
length is not None and \
74+
length <= TLS12_CLIENT_HELLO_ASSUMED_MAX_LENGTH:
75+
# Server-side ClientHello defragmentation is only supported in
76+
# the TLS 1.3 message parser. When that parser sees an 1.2-only
77+
# ClientHello, it forwards the reassembled record to the
78+
# TLS 1.2 ClientHello parser so the ClientHello can be fragmented.
79+
# When TLS 1.3 support is disabled in the server (at compile-time
80+
# or at runtime), the TLS 1.2 ClientHello parser only sees
81+
# the first fragment of the ClientHello.
82+
tc.requirements.append('requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_3')
83+
tc.description += ' TLS 1.3 ClientHello -> 1.2 Handshake'
84+
85+
# To guarantee that the handhake messages are large enough and need to be
86+
# split into fragments, the tests require certificate authentication.
87+
# The party in control of the fragmentation operations is OpenSSL and
88+
# will always use server5.crt (548 Bytes).
89+
if length is not None and \
90+
length >= TLS_HANDSHAKE_FRAGMENT_MIN_LENGTH:
91+
tc.requirements.append('requires_certificate_authentication')
92+
if version == Version.TLS12 and side == Side.CLIENT:
93+
#The server uses an ECDSA cert, so make sure we have a compatible key exchange
94+
tc.requirements.append(
95+
'requires_config_enabled MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED')
96+
else:
97+
# This test case may run in a pure-PSK configuration. OpenSSL doesn't
98+
# allow this by default with TLS 1.3.
99+
their_args += ' -allow_no_dhe_kex'
100+
101+
if length is None:
102+
forbidden_patterns = [
103+
'reassembled record',
104+
'waiting for more fragments',
105+
]
106+
wanted_patterns = []
107+
elif length < TLS_HANDSHAKE_FRAGMENT_MIN_LENGTH:
108+
their_args += ' -split_send_frag ' + str(length)
109+
tc.exit_code = 1
110+
forbidden_patterns = []
111+
wanted_patterns = [
112+
'handshake message too short: ' + str(length),
113+
'SSL - An invalid SSL record was received',
114+
]
115+
if side == Side.SERVER:
116+
wanted_patterns[0:0] = ['<= parse client hello']
117+
elif version == Version.TLS13:
118+
wanted_patterns[0:0] = ['=> ssl_tls13_process_server_hello']
119+
else:
120+
their_args += ' -split_send_frag ' + str(length)
121+
forbidden_patterns = []
122+
wanted_patterns = [
123+
'reassembled record',
124+
fr'handshake fragment: 0 \.\. {length} of [0-9]\+ msglen {length}',
125+
fr'waiting for more fragments ({length} of',
126+
]
127+
128+
if side == Side.CLIENT:
129+
tc.client = '$P_CLI debug_level=4' + our_args
130+
tc.server = '$O_NEXT_SRV' + their_args
131+
tc.wanted_client_patterns = wanted_patterns
132+
tc.forbidden_client_patterns = forbidden_patterns
133+
else:
134+
their_args += ' -cert $DATA_FILES_PATH/server5.crt -key $DATA_FILES_PATH/server5.key'
135+
our_args += ' auth_mode=required'
136+
tc.client = '$O_NEXT_CLI' + their_args
137+
tc.server = '$P_SRV debug_level=4' + our_args
138+
tc.wanted_server_patterns = wanted_patterns
139+
tc.forbidden_server_patterns = forbidden_patterns
140+
tc.write(out)
141+
142+
def write_tls_handshake_defragmentation_tests(out: typing_util.Writable) -> None:
143+
"""Generate TLS handshake defragmentation tests."""
144+
for side in Side.CLIENT, Side.SERVER:
145+
write_tls_handshake_defragmentation_test(out, side, None)
146+
for length in [512, 513, 256, 128, 64, 36, 32, 16, 13, 5, 4, 3]:
147+
write_tls_handshake_defragmentation_test(out, side, length, Version.TLS13)
148+
write_tls_handshake_defragmentation_test(out, side, length, Version.TLS12)
149+
150+
151+
def write_handshake_tests(out: typing_util.Writable) -> None:
152+
"""Generate handshake tests."""
153+
out.write(f"""\
154+
# Miscellaneous tests related to the TLS handshake layer.
155+
#
156+
# Automatically generated by {os.path.basename(sys.argv[0])}. Do not edit!
157+
158+
# Copyright The Mbed TLS Contributors
159+
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
160+
161+
""")
162+
write_tls_handshake_defragmentation_tests(out)
163+
out.write("""\
164+
# End of automatically generated file.
165+
""")
166+
167+
def main() -> None:
168+
"""Command line entry point."""
169+
parser = argparse.ArgumentParser()
170+
parser = argparse.ArgumentParser(description=__doc__)
171+
parser.add_argument('-o', '--output',
172+
default='tests/opt-testcases/handshake-generated.sh',
173+
help='Output file (default: tests/opt-testcases/handshake-generated.sh)')
174+
args = parser.parse_args()
175+
with open(args.output, 'w') as out:
176+
write_handshake_tests(out)
177+
178+
if __name__ == '__main__':
179+
main()
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""Library for constructing an Mbed TLS ssl-opt test case.
2+
"""
3+
4+
# Copyright The Mbed TLS Contributors
5+
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
6+
7+
import enum
8+
import re
9+
from typing import List
10+
11+
from . import typing_util
12+
13+
14+
class TestCase:
15+
"""Data about an ssl-opt test case."""
16+
#pylint: disable=too-few-public-methods
17+
18+
def __init__(self, description: str) -> None:
19+
# List of shell snippets to call before run_test, typically
20+
# calls to requires_xxx functions.
21+
self.requirements = [] #type: List[str]
22+
# Test case description (first argument to run_test).
23+
self.description = description
24+
# Client command line.
25+
# This will be placed directly inside double quotes in the shell script.
26+
self.client = '$P_CLI'
27+
# Server command line.
28+
# This will be placed directly inside double quotes in the shell script.
29+
self.server = '$P_SRV'
30+
# Expected client exit code.
31+
self.exit_code = 0
32+
33+
# Note that all patterns matched in the logs are in BRE
34+
# (Basic Regular Expression) syntax, more precisely in the BRE
35+
# dialect that is the default for GNU grep. The main difference
36+
# with Python regular expressions is that the operators for
37+
# grouping `\(...\)`, alternation `x\|y`, option `x\?`,
38+
# one-or-more `x\+` and repetition ranges `x\{M,N\}` must be
39+
# preceded by a backslash. The characters `()|?+{}` stand for
40+
# themselves.
41+
42+
# BRE for text that must be present in the client log (run_test -c).
43+
self.wanted_client_patterns = [] #type: List[str]
44+
# BRE for text that must be present in the server log (run_test -s).
45+
self.wanted_server_patterns = [] #type: List[str]
46+
# BRE for text that must not be present in the client log (run_test -C).
47+
self.forbidden_client_patterns = [] #type: List[str]
48+
# BRE for text that must not be present in the server log (run_test -S).
49+
self.forbidden_server_patterns = [] #type: List[str]
50+
51+
@staticmethod
52+
def _quote(raw: str) -> str:
53+
"""Quote the given string for sh.
54+
55+
Use double quotes, because that's currently the norm in ssl-opt.sh.
56+
"""
57+
return '"' + re.sub(r'([$"\\`])', r'\\\1', raw) + '"'
58+
59+
def write(self, out: typing_util.Writable) -> None:
60+
"""Write the test case to the specified file."""
61+
for req in self.requirements:
62+
out.write(req + '\n')
63+
out.write(f'run_test {self._quote(self.description)} \\\n')
64+
out.write(f' "{self.server}" \\\n')
65+
out.write(f' "{self.client}" \\\n')
66+
out.write(f' {self.exit_code}')
67+
for pat in self.wanted_server_patterns:
68+
out.write(' \\\n -s ' + self._quote(pat))
69+
for pat in self.forbidden_server_patterns:
70+
out.write(' \\\n -S ' + self._quote(pat))
71+
for pat in self.wanted_client_patterns:
72+
out.write(' \\\n -c ' + self._quote(pat))
73+
for pat in self.forbidden_client_patterns:
74+
out.write(' \\\n -C ' + self._quote(pat))
75+
out.write('\n\n')
76+
77+
78+
class Side(enum.Enum):
79+
CLIENT = 0
80+
SERVER = 1
81+
82+
class Version(enum.Enum):
83+
"""TLS protocol version.
84+
85+
This class doesn't know about DTLS yet.
86+
"""
87+
88+
TLS12 = 2
89+
TLS13 = 3
90+
91+
def force_version(self) -> str:
92+
"""Argument to pass to ssl_client2 or ssl_server2 to force this version."""
93+
return f'force_version=tls1{self.value}'
94+
95+
def openssl_option(self) -> str:
96+
"""Option to pass to openssl s_client or openssl s_server to select this version."""
97+
return f'-tls1_{self.value}'
98+
99+
def requires_command(self) -> str:
100+
"""Command to require this protocol version in an ssl-opt.sh test case."""
101+
return 'requires_config_enabled MBEDTLS_SSL_PROTO_TLS1_' + str(self.value)

0 commit comments

Comments
 (0)