bneradt commented on code in PR #13736: URL: https://github.com/apache/trafficserver/pull/13736#discussion_r4108986703
########## tests/gold_tests/tls/tls_resume_cert_partition.test.py: ########## @@ -0,0 +1,188 @@ +''' +Test that a TLS session is not resumed against a different server certificate when no SNI is sent. +''' +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +Test.Summary = ''' +Test that a session issued on one dest_ip-selected certificate is not resumed +on a different one. Such connections send no SNI, so the server name in the +session id context cannot distinguish them. +''' + +Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) + + +class TlsResumeCertPartition: + ''' + Test that resumption is partitioned by server certificate for no-SNI connections. + + A client that sends no SNI has its certificate chosen by destination + address, so the server name cannot tell two such connections apart. + Session tickets for every certificate are protected by the same globally + configured ticket keys, so a ticket carries nothing tying it to the + certificate that issued it unless the keys used for it depend on that + certificate. Without that, a ticket issued on one address resumes on another + address serving a different certificate, and the client skips the + certificate it would otherwise have been shown. + + The second address is the IPv6 loopback because it needs no interface + alias on any platform, unlike 127.0.0.2. + ''' + + _first_ip = '127.0.0.1' + _second_ip = '[::1]' + + def __init__(self) -> None: + '''Configure the origin server, ATS process, and test runs.''' + Test.Setup.Copy('file.ticket') + self.ticket_file = os.path.join(Test.RunDirectory, 'file.ticket') + self._configure_server() + self.ts = self._configure_ts('ts') + # A second instance sharing the ticket key file stands in for another server in a fleet. + self.ts2 = self._configure_ts('ts2') + self._add_same_cert_run() + self._add_cross_cert_run() + self._add_shared_key_run() + + def _configure_server(self) -> None: + '''Configure the origin server with a simple response.''' + server = Test.MakeOriginServer('server') + request_header = { + 'headers': 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': '' + } + response_header = { + 'headers': 'HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n', + 'timestamp': '1469733493.993', + 'body': 'hello' + } + server.addResponse('sessionlog.json', request_header, response_header) + self.server = server + + def _configure_ts(self, name: str) -> 'Process': + ''' + Configure an ATS process with a different certificate per destination address. + + :param name: Name of the ATS process. + :return: The configured ATS process. + ''' + ts = Test.MakeATSProcess(name, enable_tls=True) + ts.addSSLfile('ssl/server.pem') + ts.addSSLfile('ssl/server.key') + ts.addSSLfile('ssl/signed-foo.pem') + ts.addSSLfile('ssl/signed-foo.key') + + # Two certificates on one listener, chosen by destination address rather + # than by SNI. Neither entry sets ssl_ca_name, so the only thing that + # differs between the two connections is which certificate is served. + ts.Disk.ssl_multicert_yaml.AddLines( + f""" +ssl_multicert: + - dest_ip: "{self._first_ip}" + ssl_cert_name: server.pem + ssl_key_name: server.key + - dest_ip: "{self._second_ip}" + ssl_cert_name: signed-foo.pem + ssl_key_name: signed-foo.key + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key +""".split("\n")) + + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.Port}') + + ts.Disk.records_config.update( + { + 'proxy.config.ssl.server.cert.path': f'{ts.Variables.SSLDir}', + 'proxy.config.ssl.server.private_key.path': f'{ts.Variables.SSLDir}', + 'proxy.config.exec_thread.autoconfig.scale': 1.0, + 'proxy.config.ssl.server.session_ticket.enable': 1, + 'proxy.config.ssl.server.ticket_key.filename': self.ticket_file, + }) + return ts + + def _client_command(self, ip: str, session_arg: str, ts: 'Process' = None) -> str: + ''' + Build a shell command that connects to the given address without sending an SNI. + + :param ip: Destination address to connect to, which selects the certificate. + :param session_arg: The s_client argument saving or offering a session. + :param ts: The ATS process to connect to, by default the first one. + :return: Shell command performing the connection. + ''' + ts = ts or self.ts + # The IPv6 loopback is on lo0 everywhere, whereas 127.0.0.2 needs an alias on macOS. + port = ts.Variables.ssl_portv6 if ip.startswith('[') else ts.Variables.ssl_port + request = 'printf "GET / HTTP/1.1\\r\\nHost: example.com\\r\\nConnection: close\\r\\n\\r\\n"' + # No -servername, so no SNI extension is sent and dest_ip selects the cert. + return (f'{request} | openssl s_client -connect {ip}:{port} -noservername ' + f'{session_arg} -tls1_3 -ign_eof') Review Comment: Both addressed. `s_client` is replaced by a small Python client, `tls_resume_cert_partition_client.py`. It sends no SNI, verifies against the test CAs so it can report the served certificate's CN, optionally sends a PROXY v1 header, and runs both legs in one process. Since it uses Python's `ssl` module rather than whatever `openssl` binary is first on `PATH`, the LibreSSL problem goes away. The same-certificate and cross-certificate runs now exist for both `-tls 1.2` and `1.3`, alongside the PROXY and shared-key runs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
