bneradt commented on code in PR #13736: URL: https://github.com/apache/trafficserver/pull/13736#discussion_r4109300720
########## tests/gold_tests/tls/tls_resume_cert_partition.test.py: ########## @@ -0,0 +1,252 @@ +''' +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 + +import os +import re +import sys + +Test.Summary = ''' +Test that a session ticket issued on one dest_ip-selected certificate is not +resumed on a different one, while servers sharing the ticket keys and the +certificate still resume each other's tickets. +''' + +Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) + + +class TlsResumeCertPartition: + ''' + Test that ticket resumption is partitioned by the certificate selected by destination address. + + 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. + + Each run checks what the client observes, which certificate each leg is + served and whether it resumed, and the log checks that ATS records the + resumption the same way. The second address is the IPv6 loopback because it + needs no interface alias on any platform, unlike 127.0.0.2. For the PROXY + protocol runs the destination comes from the PROXY header, so documentation + addresses stand in for two load balancer VIPs. + ''' + + _first_ip = '127.0.0.1' + _second_ip = '::1' + _first_vip = '192.0.2.1' + _second_vip = '192.0.2.2' + _first_cn = 'random.server.com' + _second_cn = 'foo.com' + _client = 'tls_resume_cert_partition_client.py' + + def __init__(self) -> None: + '''Configure the origin server, ATS processes, and test runs.''' + Test.Setup.Copy('file.ticket') + Test.Setup.Copy(self._client) + 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._started = False + self._second_legs: list[tuple['Process', str, bool]] = [] + + for tls in ('1.3', '1.2'): + self._add_run( + f'A no-SNI TLS {tls} session resumes on the certificate that issued it', + tls, + self._leg(self.ts, self._first_ip, f'/same-{tls}-first'), + self._leg(self.ts, self._first_ip, f'/same-{tls}-second'), + resumed=True, + cns=(self._first_cn, self._first_cn)) + self._add_run( + f'A no-SNI TLS {tls} session does not resume on a different certificate', + tls, + self._leg(self.ts, self._first_ip, f'/cross-{tls}-first'), + self._leg(self.ts, self._second_ip, f'/cross-{tls}-second'), + resumed=False, + cns=(self._first_cn, self._second_cn)) + self._add_run( + 'A PROXY protocol session resumes on the VIP whose certificate issued it', + '1.3', + self._leg(self.ts, self._first_ip, '/proxy-same-first', self._first_vip), + self._leg(self.ts, self._first_ip, '/proxy-same-second', self._first_vip), + resumed=True, + cns=(self._first_cn, self._first_cn)) + self._add_run( + 'A PROXY protocol session does not resume on a VIP with a different certificate', + '1.3', + self._leg(self.ts, self._first_ip, '/proxy-cross-first', self._first_vip), + self._leg(self.ts, self._first_ip, '/proxy-cross-second', self._second_vip), + resumed=False, + cns=(self._first_cn, self._second_cn)) + self._add_run( + 'A no-SNI session resumes on another server sharing the ticket keys and certificate', + '1.3', + self._leg(self.ts, self._first_ip, '/shared-first'), + self._leg(self.ts2, self._first_ip, '/shared-second'), + resumed=True, + cns=(self._first_cn, self._first_cn)) + self._add_log_checks() + + 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\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, enable_proxy_protocol=True) + ts.addSSLfile('ssl/server.pem') + ts.addSSLfile('ssl/server.key') + ts.addSSLfile('ssl/signed-foo.pem') + ts.addSSLfile('ssl/signed-foo.key') + # Only for the client, which verifies so that it can report which certificate it was served. + ts.addSSLfile('ssl/signer.pem') + + # Certificates chosen by destination address rather than by SNI. Neither + # certificate is chosen by name, so the only thing that differs between 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: "{self._first_vip}" + ssl_cert_name: server.pem + ssl_key_name: server.key + - dest_ip: "{self._second_vip}" + 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}') Review Comment: Dropped the origin server and the remap line. The class docstring now says ATS answers each request itself and only the handshake and its logged resumption matter. 🤖 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]
