Copilot commented on code in PR #13006: URL: https://github.com/apache/trafficserver/pull/13006#discussion_r2967112710
########## tests/gold_tests/tls/tls_sni_ticket.test.py: ########## @@ -0,0 +1,238 @@ +''' +Test sni.yaml session ticket overrides. +''' +# 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 +import re +from typing import Any + +Test.Summary = ''' +Test sni.yaml session ticket overrides +''' + +Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) +Test.Setup.Copy('file.ticket') + + +class TlsSniTicketTest: + + def __init__(self) -> None: + """ + Initialize shared test state and configure the ATS processes. + """ + self.ticket_file = os.path.join(Test.RunDirectory, 'file.ticket') + self.setupOriginServer() + self.setupEnabledTS() + self.setupDisabledTS() + + def setupOriginServer(self) -> None: + """ + Configure the origin server with a simple response for all requests. + """ + request_header = { + 'headers': 'GET / HTTP/1.1\r\nHost: tickets.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': 'ticket test' + } + self.server = Test.MakeOriginServer('server') + self.server.addResponse('sessionlog.json', request_header, response_header) + + def setupTS( + self, + name: str, + sni_name: str, + global_ticket_enabled: int, + global_ticket_number: int, + sni_ticket_enabled: int, + sni_ticket_number: int | None = None) -> Any: + """ + Configure an ATS process for one SNI ticket override scenario. + + :param name: ATS process name. + :param sni_name: SNI hostname matched in sni.yaml. + :param global_ticket_enabled: Process-wide session ticket enable setting. + :param global_ticket_number: Process-wide TLSv1.3 ticket count. + :param sni_ticket_enabled: Per-SNI session ticket enable override. + :param sni_ticket_number: Per-SNI TLSv1.3 ticket count override. + :return: Configured ATS process. + """ + ts = Test.MakeATSProcess(name, enable_tls=True) + + ts.addSSLfile('ssl/server.pem') + ts.addSSLfile('ssl/server.key') + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.Port}') + ts.Disk.ssl_multicert_yaml.AddLines( + """ +ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key +""".split("\n")) + + 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': global_ticket_enabled, + 'proxy.config.ssl.server.session_ticket.number': global_ticket_number, + 'proxy.config.ssl.server.ticket_key.filename': self.ticket_file, + }) + + sni_lines = [ + 'sni:', + f'- fqdn: {sni_name}', + f' ssl_ticket_enabled: {sni_ticket_enabled}', + ] + if sni_ticket_number is not None: + sni_lines.append(f' ssl_ticket_number: {sni_ticket_number}') + ts.Disk.sni_yaml.AddLines(sni_lines) + + return ts + + def setupEnabledTS(self) -> None: + """ + Create the ATS process whose SNI rule enables tickets. + """ + self.ts_on = self.setupTS('ts_on', 'tickets-on.com', 0, 0, 1, 3) + + def setupDisabledTS(self) -> None: + """ + Create the ATS process whose SNI rule disables tickets. + """ + self.ts_off = self.setupTS('ts_off', 'tickets-off.com', 1, 2, 0) + + @staticmethod + def check_regex_count(output_path: str, pattern: str, expected_count: int, description: str) -> tuple[bool, str, str]: + """ + Count regex matches in a process output file. + + :param output_path: Path to the output file to inspect. + :param pattern: Regex pattern to count. + :param expected_count: Expected number of matches. + :param description: Description reported by the tester. + :return: AuTest lambda result tuple. + """ + with open(output_path, 'r') as f: + content = f.read() + + matches = re.findall(pattern, content) + if len(matches) == expected_count: + return (True, description, f'Found {len(matches)} matches for {pattern}') + return (False, description, f'Expected {expected_count} matches for {pattern}, found {len(matches)}') + + @staticmethod + def session_reuse_command(port: int, servername: str) -> str: + """ + Build a TLSv1.2 resumption command for a specific SNI name. + + :param port: ATS TLS listening port. + :param servername: SNI hostname to send with the connection. + :return: Shell command for repeated TLSv1.2 session reuse attempts. + """ + return ( + f'session_path=`mktemp` && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_out "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2') Review Comment: In `session_reuse_command`, the command uses `"$$session_path"` for `-sess_out`/`-sess_in`. In POSIX shells `$$` expands to the PID, so this won’t reference the `session_path` variable created by `mktemp` and will break session reuse. Use `$session_path` (or `${session_path}`) instead. ########## src/iocore/net/SSLUtils.cc: ########## @@ -493,6 +491,69 @@ ssl_context_enable_dhe(const char *dhparams_file, SSL_CTX *ctx) return ctx; } +#if TS_HAS_TLS_SESSION_TICKET +static bool +ssl_context_enable_ticket_callback(SSL_CTX *ctx) +{ +#ifdef HAVE_SSL_CTX_SET_TLSEXT_TICKET_KEY_EVP_CB + if (SSL_CTX_set_tlsext_ticket_key_evp_cb(ctx, ssl_callback_session_ticket) == 0) { +#else + if (SSL_CTX_set_tlsext_ticket_key_cb(ctx, ssl_callback_session_ticket) == 0) { +#endif + Error("failed to set session ticket callback"); + return false; + } + return true; +} + +static bool +ssl_apply_sni_session_ticket_properties(SSL *ssl) +{ + auto snis = TLSSNISupport::getInstance(ssl); + if (snis == nullptr) { + return true; + } + + auto const &hints = snis->hints_from_sni; + if (!hints.ssl_ticket_enabled.has_value() && !hints.ssl_ticket_number.has_value()) { + return true; + } + + SSL_CTX *ctx = SSL_get_SSL_CTX(ssl); + if (ctx == nullptr) { + return false; + } + + if (hints.ssl_ticket_enabled.has_value()) { + if (hints.ssl_ticket_enabled.value() != 0) { + if (!ssl_context_enable_ticket_callback(ctx)) { + return false; + } Review Comment: `ssl_apply_sni_session_ticket_properties()` calls `SSL_CTX_set_tlsext_ticket_key_*_cb()` via `ssl_context_enable_ticket_callback(ctx)` during `ssl_cert_callback()` (handshake time). This mutates the shared `SSL_CTX` and can be hit concurrently across threads, creating a potential data race; it’s also redundant now that `init_server_ssl_ctx()` sets the ticket callback on the context. Prefer to set the callback only during context initialization and have this function only adjust per-connection `SSL` options / ticket count. ```suggestion ``` ########## tests/gold_tests/tls/tls_sni_ticket.test.py: ########## @@ -0,0 +1,238 @@ +''' +Test sni.yaml session ticket overrides. +''' +# 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 +import re +from typing import Any + +Test.Summary = ''' +Test sni.yaml session ticket overrides +''' + +Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) +Test.Setup.Copy('file.ticket') + + +class TlsSniTicketTest: + + def __init__(self) -> None: + """ + Initialize shared test state and configure the ATS processes. + """ + self.ticket_file = os.path.join(Test.RunDirectory, 'file.ticket') + self.setupOriginServer() + self.setupEnabledTS() + self.setupDisabledTS() + + def setupOriginServer(self) -> None: + """ + Configure the origin server with a simple response for all requests. + """ + request_header = { + 'headers': 'GET / HTTP/1.1\r\nHost: tickets.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': 'ticket test' + } + self.server = Test.MakeOriginServer('server') + self.server.addResponse('sessionlog.json', request_header, response_header) + + def setupTS( + self, + name: str, + sni_name: str, + global_ticket_enabled: int, + global_ticket_number: int, + sni_ticket_enabled: int, + sni_ticket_number: int | None = None) -> Any: + """ + Configure an ATS process for one SNI ticket override scenario. + + :param name: ATS process name. + :param sni_name: SNI hostname matched in sni.yaml. + :param global_ticket_enabled: Process-wide session ticket enable setting. + :param global_ticket_number: Process-wide TLSv1.3 ticket count. + :param sni_ticket_enabled: Per-SNI session ticket enable override. + :param sni_ticket_number: Per-SNI TLSv1.3 ticket count override. + :return: Configured ATS process. + """ + ts = Test.MakeATSProcess(name, enable_tls=True) + + ts.addSSLfile('ssl/server.pem') + ts.addSSLfile('ssl/server.key') + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.Port}') + ts.Disk.ssl_multicert_yaml.AddLines( + """ +ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key +""".split("\n")) + + 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': global_ticket_enabled, + 'proxy.config.ssl.server.session_ticket.number': global_ticket_number, + 'proxy.config.ssl.server.ticket_key.filename': self.ticket_file, + }) + + sni_lines = [ + 'sni:', + f'- fqdn: {sni_name}', + f' ssl_ticket_enabled: {sni_ticket_enabled}', + ] + if sni_ticket_number is not None: + sni_lines.append(f' ssl_ticket_number: {sni_ticket_number}') + ts.Disk.sni_yaml.AddLines(sni_lines) + + return ts + + def setupEnabledTS(self) -> None: + """ + Create the ATS process whose SNI rule enables tickets. + """ + self.ts_on = self.setupTS('ts_on', 'tickets-on.com', 0, 0, 1, 3) + + def setupDisabledTS(self) -> None: + """ + Create the ATS process whose SNI rule disables tickets. + """ + self.ts_off = self.setupTS('ts_off', 'tickets-off.com', 1, 2, 0) + + @staticmethod + def check_regex_count(output_path: str, pattern: str, expected_count: int, description: str) -> tuple[bool, str, str]: + """ + Count regex matches in a process output file. + + :param output_path: Path to the output file to inspect. + :param pattern: Regex pattern to count. + :param expected_count: Expected number of matches. + :param description: Description reported by the tester. + :return: AuTest lambda result tuple. + """ + with open(output_path, 'r') as f: + content = f.read() + + matches = re.findall(pattern, content) + if len(matches) == expected_count: + return (True, description, f'Found {len(matches)} matches for {pattern}') + return (False, description, f'Expected {expected_count} matches for {pattern}, found {len(matches)}') + + @staticmethod + def session_reuse_command(port: int, servername: str) -> str: + """ + Build a TLSv1.2 resumption command for a specific SNI name. + + :param port: ATS TLS listening port. + :param servername: SNI hostname to send with the connection. + :return: Shell command for repeated TLSv1.2 session reuse attempts. + """ + return ( + f'session_path=`mktemp` && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_out "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2') + + def add_tls12_enabled_run(self) -> None: + """ + Register the TLSv1.2 resumption test for the enabled SNI case. + """ + tr = Test.AddTestRun('sni.yaml enables TLSv1.2 ticket resumption') + tr.Command = TlsSniTicketTest.session_reuse_command(self.ts_on.Variables.ssl_port, 'tickets-on.com') + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts_on) + tr.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: TlsSniTicketTest.check_regex_count( + tr.Processes.Default.Streams.All.AbsPath, r'Reused, TLSv1\.2', 5, + 'Check that tickets-on.com reuses TLSv1.2 sessions')) + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_on + + def add_tls13_enabled_run(self) -> None: + """ + Register the TLSv1.3 ticket count test for the enabled SNI case. + """ + tr = Test.AddTestRun('sni.yaml sets TLSv1.3 ticket count') + tr.Command = ( + f'echo -e "GET / HTTP/1.1\\r\\nHost: tickets-on.com\\r\\nConnection: close\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{self.ts_on.Variables.ssl_port} -servername tickets-on.com -tls1_3 -msg -ign_eof') + tr.ReturnCode = 0 + tr.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: TlsSniTicketTest.check_regex_count( + tr.Processes.Default.Streams.All.AbsPath, r'NewSessionTicket', 3, + 'Check that tickets-on.com receives three TLSv1.3 tickets')) + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_on + + def add_tls12_disabled_run(self) -> None: + """ + Register the TLSv1.2 non-resumption test for the disabled SNI case. + """ + tr = Test.AddTestRun('sni.yaml disables TLSv1.2 ticket resumption') + tr.Command = TlsSniTicketTest.session_reuse_command(self.ts_off.Variables.ssl_port, 'tickets-off.com') + tr.Processes.Default.StartBefore(self.ts_off) + tr.Processes.Default.Streams.All = Testers.ExcludesExpression('Reused', 'tickets-off.com should not reuse TLSv1.2 sessions') + tr.Processes.Default.Streams.All += Testers.ContainsExpression('TLSv1.2', 'tickets-off.com should still negotiate TLSv1.2') + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_off Review Comment: This TLSv1.2 disabled run starts `ts_off` but never starts the origin server, even though the remap points to it. Add `tr.Processes.Default.StartBefore(self.server)` (and keep it running after) to avoid connection failures when ATS forwards the request. ########## tests/gold_tests/tls/tls_sni_ticket.test.py: ########## @@ -0,0 +1,238 @@ +''' +Test sni.yaml session ticket overrides. +''' +# 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 +import re +from typing import Any + +Test.Summary = ''' +Test sni.yaml session ticket overrides +''' + +Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) +Test.Setup.Copy('file.ticket') + + +class TlsSniTicketTest: + + def __init__(self) -> None: + """ + Initialize shared test state and configure the ATS processes. + """ + self.ticket_file = os.path.join(Test.RunDirectory, 'file.ticket') + self.setupOriginServer() + self.setupEnabledTS() + self.setupDisabledTS() + + def setupOriginServer(self) -> None: + """ + Configure the origin server with a simple response for all requests. + """ + request_header = { + 'headers': 'GET / HTTP/1.1\r\nHost: tickets.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': 'ticket test' + } + self.server = Test.MakeOriginServer('server') + self.server.addResponse('sessionlog.json', request_header, response_header) + + def setupTS( + self, + name: str, + sni_name: str, + global_ticket_enabled: int, + global_ticket_number: int, + sni_ticket_enabled: int, + sni_ticket_number: int | None = None) -> Any: + """ + Configure an ATS process for one SNI ticket override scenario. + + :param name: ATS process name. + :param sni_name: SNI hostname matched in sni.yaml. + :param global_ticket_enabled: Process-wide session ticket enable setting. + :param global_ticket_number: Process-wide TLSv1.3 ticket count. + :param sni_ticket_enabled: Per-SNI session ticket enable override. + :param sni_ticket_number: Per-SNI TLSv1.3 ticket count override. + :return: Configured ATS process. + """ + ts = Test.MakeATSProcess(name, enable_tls=True) + + ts.addSSLfile('ssl/server.pem') + ts.addSSLfile('ssl/server.key') + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.Port}') + ts.Disk.ssl_multicert_yaml.AddLines( + """ +ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key +""".split("\n")) + + 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': global_ticket_enabled, + 'proxy.config.ssl.server.session_ticket.number': global_ticket_number, + 'proxy.config.ssl.server.ticket_key.filename': self.ticket_file, + }) + + sni_lines = [ + 'sni:', + f'- fqdn: {sni_name}', + f' ssl_ticket_enabled: {sni_ticket_enabled}', + ] + if sni_ticket_number is not None: + sni_lines.append(f' ssl_ticket_number: {sni_ticket_number}') + ts.Disk.sni_yaml.AddLines(sni_lines) + + return ts + + def setupEnabledTS(self) -> None: + """ + Create the ATS process whose SNI rule enables tickets. + """ + self.ts_on = self.setupTS('ts_on', 'tickets-on.com', 0, 0, 1, 3) + + def setupDisabledTS(self) -> None: + """ + Create the ATS process whose SNI rule disables tickets. + """ + self.ts_off = self.setupTS('ts_off', 'tickets-off.com', 1, 2, 0) + + @staticmethod + def check_regex_count(output_path: str, pattern: str, expected_count: int, description: str) -> tuple[bool, str, str]: + """ + Count regex matches in a process output file. + + :param output_path: Path to the output file to inspect. + :param pattern: Regex pattern to count. + :param expected_count: Expected number of matches. + :param description: Description reported by the tester. + :return: AuTest lambda result tuple. + """ + with open(output_path, 'r') as f: + content = f.read() + + matches = re.findall(pattern, content) + if len(matches) == expected_count: + return (True, description, f'Found {len(matches)} matches for {pattern}') + return (False, description, f'Expected {expected_count} matches for {pattern}, found {len(matches)}') + + @staticmethod + def session_reuse_command(port: int, servername: str) -> str: + """ + Build a TLSv1.2 resumption command for a specific SNI name. + + :param port: ATS TLS listening port. + :param servername: SNI hostname to send with the connection. + :return: Shell command for repeated TLSv1.2 session reuse attempts. + """ + return ( + f'session_path=`mktemp` && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_out "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2') + + def add_tls12_enabled_run(self) -> None: + """ + Register the TLSv1.2 resumption test for the enabled SNI case. + """ + tr = Test.AddTestRun('sni.yaml enables TLSv1.2 ticket resumption') + tr.Command = TlsSniTicketTest.session_reuse_command(self.ts_on.Variables.ssl_port, 'tickets-on.com') + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts_on) + tr.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: TlsSniTicketTest.check_regex_count( + tr.Processes.Default.Streams.All.AbsPath, r'Reused, TLSv1\.2', 5, + 'Check that tickets-on.com reuses TLSv1.2 sessions')) + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_on + + def add_tls13_enabled_run(self) -> None: + """ + Register the TLSv1.3 ticket count test for the enabled SNI case. + """ + tr = Test.AddTestRun('sni.yaml sets TLSv1.3 ticket count') + tr.Command = ( + f'echo -e "GET / HTTP/1.1\\r\\nHost: tickets-on.com\\r\\nConnection: close\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{self.ts_on.Variables.ssl_port} -servername tickets-on.com -tls1_3 -msg -ign_eof') + tr.ReturnCode = 0 + tr.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: TlsSniTicketTest.check_regex_count( + tr.Processes.Default.Streams.All.AbsPath, r'NewSessionTicket', 3, + 'Check that tickets-on.com receives three TLSv1.3 tickets')) + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_on + + def add_tls12_disabled_run(self) -> None: + """ + Register the TLSv1.2 non-resumption test for the disabled SNI case. + """ + tr = Test.AddTestRun('sni.yaml disables TLSv1.2 ticket resumption') + tr.Command = TlsSniTicketTest.session_reuse_command(self.ts_off.Variables.ssl_port, 'tickets-off.com') + tr.Processes.Default.StartBefore(self.ts_off) + tr.Processes.Default.Streams.All = Testers.ExcludesExpression('Reused', 'tickets-off.com should not reuse TLSv1.2 sessions') + tr.Processes.Default.Streams.All += Testers.ContainsExpression('TLSv1.2', 'tickets-off.com should still negotiate TLSv1.2') + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_off + + def add_tls13_disabled_run(self) -> None: + """ + Register the TLSv1.3 no-ticket test for the disabled SNI case. + """ + tr = Test.AddTestRun('sni.yaml disables TLSv1.3 ticket issuance') + tr.Command = ( + f'echo -e "GET / HTTP/1.1\\r\\nHost: tickets-off.com\\r\\nConnection: close\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{self.ts_off.Variables.ssl_port} -servername tickets-off.com -tls1_3 -msg -ign_eof' + ) + tr.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: TlsSniTicketTest.check_regex_count( + tr.Processes.Default.Streams.All.AbsPath, r'NewSessionTicket', 0, + 'Check that tickets-off.com receives no TLSv1.3 tickets')) + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_off + Review Comment: This TLSv1.3 disabled run also doesn’t start the origin server or `ts_off`. Add `StartBefore(self.server)` / `StartBefore(self.ts_off)` like the other runs so the `openssl s_client` command has a listening endpoint. ########## tests/gold_tests/tls/tls_sni_ticket.test.py: ########## @@ -0,0 +1,238 @@ +''' +Test sni.yaml session ticket overrides. +''' +# 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 +import re +from typing import Any + +Test.Summary = ''' +Test sni.yaml session ticket overrides +''' + +Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1')) +Test.Setup.Copy('file.ticket') + + +class TlsSniTicketTest: + + def __init__(self) -> None: + """ + Initialize shared test state and configure the ATS processes. + """ + self.ticket_file = os.path.join(Test.RunDirectory, 'file.ticket') + self.setupOriginServer() + self.setupEnabledTS() + self.setupDisabledTS() + + def setupOriginServer(self) -> None: + """ + Configure the origin server with a simple response for all requests. + """ + request_header = { + 'headers': 'GET / HTTP/1.1\r\nHost: tickets.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': 'ticket test' + } + self.server = Test.MakeOriginServer('server') + self.server.addResponse('sessionlog.json', request_header, response_header) + + def setupTS( + self, + name: str, + sni_name: str, + global_ticket_enabled: int, + global_ticket_number: int, + sni_ticket_enabled: int, + sni_ticket_number: int | None = None) -> Any: + """ + Configure an ATS process for one SNI ticket override scenario. + + :param name: ATS process name. + :param sni_name: SNI hostname matched in sni.yaml. + :param global_ticket_enabled: Process-wide session ticket enable setting. + :param global_ticket_number: Process-wide TLSv1.3 ticket count. + :param sni_ticket_enabled: Per-SNI session ticket enable override. + :param sni_ticket_number: Per-SNI TLSv1.3 ticket count override. + :return: Configured ATS process. + """ + ts = Test.MakeATSProcess(name, enable_tls=True) + + ts.addSSLfile('ssl/server.pem') + ts.addSSLfile('ssl/server.key') + ts.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{self.server.Variables.Port}') + ts.Disk.ssl_multicert_yaml.AddLines( + """ +ssl_multicert: + - dest_ip: "*" + ssl_cert_name: server.pem + ssl_key_name: server.key +""".split("\n")) + + 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': global_ticket_enabled, + 'proxy.config.ssl.server.session_ticket.number': global_ticket_number, + 'proxy.config.ssl.server.ticket_key.filename': self.ticket_file, + }) + + sni_lines = [ + 'sni:', + f'- fqdn: {sni_name}', + f' ssl_ticket_enabled: {sni_ticket_enabled}', + ] + if sni_ticket_number is not None: + sni_lines.append(f' ssl_ticket_number: {sni_ticket_number}') + ts.Disk.sni_yaml.AddLines(sni_lines) + + return ts + + def setupEnabledTS(self) -> None: + """ + Create the ATS process whose SNI rule enables tickets. + """ + self.ts_on = self.setupTS('ts_on', 'tickets-on.com', 0, 0, 1, 3) + + def setupDisabledTS(self) -> None: + """ + Create the ATS process whose SNI rule disables tickets. + """ + self.ts_off = self.setupTS('ts_off', 'tickets-off.com', 1, 2, 0) + + @staticmethod + def check_regex_count(output_path: str, pattern: str, expected_count: int, description: str) -> tuple[bool, str, str]: + """ + Count regex matches in a process output file. + + :param output_path: Path to the output file to inspect. + :param pattern: Regex pattern to count. + :param expected_count: Expected number of matches. + :param description: Description reported by the tester. + :return: AuTest lambda result tuple. + """ + with open(output_path, 'r') as f: + content = f.read() + + matches = re.findall(pattern, content) + if len(matches) == expected_count: + return (True, description, f'Found {len(matches)} matches for {pattern}') + return (False, description, f'Expected {expected_count} matches for {pattern}, found {len(matches)}') + + @staticmethod + def session_reuse_command(port: int, servername: str) -> str: + """ + Build a TLSv1.2 resumption command for a specific SNI name. + + :param port: ATS TLS listening port. + :param servername: SNI hostname to send with the connection. + :return: Shell command for repeated TLSv1.2 session reuse attempts. + """ + return ( + f'session_path=`mktemp` && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_out "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2 && ' + f'echo -e "GET / HTTP/1.1\\r\\nHost: {servername}\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{port} -servername {servername} -sess_in "$$session_path" -tls1_2') + + def add_tls12_enabled_run(self) -> None: + """ + Register the TLSv1.2 resumption test for the enabled SNI case. + """ + tr = Test.AddTestRun('sni.yaml enables TLSv1.2 ticket resumption') + tr.Command = TlsSniTicketTest.session_reuse_command(self.ts_on.Variables.ssl_port, 'tickets-on.com') + tr.ReturnCode = 0 + tr.Processes.Default.StartBefore(self.server) + tr.Processes.Default.StartBefore(self.ts_on) + tr.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: TlsSniTicketTest.check_regex_count( + tr.Processes.Default.Streams.All.AbsPath, r'Reused, TLSv1\.2', 5, + 'Check that tickets-on.com reuses TLSv1.2 sessions')) + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_on + + def add_tls13_enabled_run(self) -> None: + """ + Register the TLSv1.3 ticket count test for the enabled SNI case. + """ + tr = Test.AddTestRun('sni.yaml sets TLSv1.3 ticket count') + tr.Command = ( + f'echo -e "GET / HTTP/1.1\\r\\nHost: tickets-on.com\\r\\nConnection: close\\r\\n\\r\\n" | ' + f'openssl s_client -connect 127.0.0.1:{self.ts_on.Variables.ssl_port} -servername tickets-on.com -tls1_3 -msg -ign_eof') + tr.ReturnCode = 0 + tr.Processes.Default.Streams.All.Content = Testers.Lambda( + lambda info, tester: TlsSniTicketTest.check_regex_count( + tr.Processes.Default.Streams.All.AbsPath, r'NewSessionTicket', 3, + 'Check that tickets-on.com receives three TLSv1.3 tickets')) + tr.StillRunningAfter += self.server + tr.StillRunningAfter += self.ts_on + Review Comment: This TLSv1.3 enabled run never starts the origin server or `ts_on` (`StartBefore(...)` is missing), but the `openssl s_client` command depends on them being up. Add the same `StartBefore(self.server)` / `StartBefore(self.ts_on)` setup used in the TLSv1.2 run so the test is deterministic. -- 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]
