diff --git a/configs/sni.yaml.default b/configs/sni.yaml.default
index c39363a6ef2..57a1ed24ed9 100644
--- a/configs/sni.yaml.default
+++ b/configs/sni.yaml.default
@@ -28,6 +28,12 @@
# The location of the certificate file is relative to proxy.config.ssl.server.cert.path directory.
# client_key - sets the file containing the client private key that corresponds to the certificate for the outbound connection.
# client_sni_policy - policy of SNI on outbound connection.
+# ssl_ticket_enabled - enables or disables session tickets for matched inbound TLS connections; parameters = 1 or 0.
+# This overrides proxy.config.ssl.server.session_ticket.enable.
+# ssl_ticket_number - sets the number of TLSv1.3 session tickets issued for matched inbound TLS connections;
+# parameters = INTEGER. This overrides proxy.config.ssl.server.session_ticket.number.
+# BoringSSL does not support setting the ticket number on a per-SNI basis,
+# so this configuration is ignored when ATS is linked against BoringSSL.
# http2 - adds or removes HTTP/2 (H2) from the protocol list advertised by ATS; parameter required = None, parameters = on or off
# tunnel_route - sets the e2e tunnel route
# forward_route - destination as an FQDN and port, separated by a colon :.
diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst
index 43bf2ab7bba..b3877eff3ff 100644
--- a/doc/admin-guide/files/records.yaml.en.rst
+++ b/doc/admin-guide/files/records.yaml.en.rst
@@ -4190,6 +4190,11 @@ SSL Termination
Increasing the number of tickets could be potentially beneficial for clients performing
multiple requests over concurrent TLS connections as per RFC 8446 clients SHOULDN'T reuse TLS Tickets.
+ This setting is applied at the SSL context level. BoringSSL does not support setting the
+ ticket number on a per-SNI basis, so the :file:`sni.yaml` :code:`ssl_ticket_number`
+ configuration does not apply when ATS is linked against BoringSSL and this context-level
+ value remains in effect.
+
For more information see https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_num_tickets.html
.. ts:cv:: CONFIG proxy.config.ssl.hsts_max_age INT -1
diff --git a/doc/admin-guide/files/sni.yaml.en.rst b/doc/admin-guide/files/sni.yaml.en.rst
index 95958943630..404f7ea62b1 100644
--- a/doc/admin-guide/files/sni.yaml.en.rst
+++ b/doc/admin-guide/files/sni.yaml.en.rst
@@ -172,6 +172,21 @@ server_groups_list Inbound Specifies an override to the
`OpenSSL SSL_CTX_set_groups_list `_
documentation.
+ssl_ticket_enabled Inbound Specifies an override to the global
+ :ts:cv:`proxy.config.ssl.server.session_ticket.enable`
+ :file:`records.yaml` configuration. Set this to :code:`1` to enable
+ session tickets or :code:`0` to disable them for matching inbound TLS
+ connections.
+
+ssl_ticket_number Inbound Specifies an override to the global
+ :ts:cv:`proxy.config.ssl.server.session_ticket.number`
+ :file:`records.yaml` configuration. This controls how many TLSv1.3
+ session tickets are issued for matching inbound TLS connections.
+ BoringSSL does not support setting the ticket number on a
+ per-SNI basis, so this configuration does not apply when ATS is
+ linked against BoringSSL. The configured ticket count from the
+ selected SSL context remains in effect.
+
host_sni_policy Inbound One of the values :code:`DISABLED`, :code:`PERMISSIVE`, or :code:`ENFORCED`.
If not specified, the value of :ts:cv:`proxy.config.http.host_sni_policy` is used.
diff --git a/include/iocore/net/TLSSNISupport.h b/include/iocore/net/TLSSNISupport.h
index 22f3d751a8b..346c2c1c0af 100644
--- a/include/iocore/net/TLSSNISupport.h
+++ b/include/iocore/net/TLSSNISupport.h
@@ -140,6 +140,8 @@ class TLSSNISupport
std::optional http2_max_priority_frames_per_minute;
std::optional http2_max_rst_stream_frames_per_minute;
std::optional http2_max_continuation_frames_per_minute;
+ std::optional ssl_ticket_enabled;
+ std::optional ssl_ticket_number;
std::optional outbound_sni_policy;
} hints_from_sni;
diff --git a/include/iocore/net/YamlSNIConfig.h b/include/iocore/net/YamlSNIConfig.h
index c8396a76e74..91d0683f8f3 100644
--- a/include/iocore/net/YamlSNIConfig.h
+++ b/include/iocore/net/YamlSNIConfig.h
@@ -59,6 +59,8 @@ TSDECL(client_sni_policy);
TSDECL(server_cipher_suite);
TSDECL(server_TLSv1_3_cipher_suites);
TSDECL(server_groups_list);
+TSDECL(ssl_ticket_enabled);
+TSDECL(ssl_ticket_number);
TSDECL(ip_allow);
TSDECL(valid_tls_versions_in);
TSDECL(valid_tls_version_min_in);
@@ -107,6 +109,8 @@ struct YamlSNIConfig {
std::string server_cipher_suite;
std::string server_TLSv1_3_cipher_suites;
std::string server_groups_list;
+ std::optional ssl_ticket_enabled;
+ std::optional ssl_ticket_number;
std::string ip_allow;
bool protocol_unset = true;
unsigned long protocol_mask;
diff --git a/src/iocore/net/SNIActionPerformer.cc b/src/iocore/net/SNIActionPerformer.cc
index d0ea144f18a..a5c04710eb1 100644
--- a/src/iocore/net/SNIActionPerformer.cc
+++ b/src/iocore/net/SNIActionPerformer.cc
@@ -475,6 +475,40 @@ ServerMaxEarlyData::SNIAction([[maybe_unused]] SSL &ssl, const Context & /* ctx
return SSL_TLSEXT_ERR_OK;
}
+int
+ServerSessionTicketEnabled::SNIAction(SSL &ssl, const Context & /* ctx ATS_UNUSED */) const
+{
+#if TS_HAS_TLS_SESSION_TICKET
+ if (auto snis = TLSSNISupport::getInstance(&ssl)) {
+ const char *servername = snis->get_sni_server_name();
+ Dbg(dbg_ctl_ssl_sni, "Setting session ticket support from sni.yaml to %d for fqdn [%s]", session_ticket_enabled, servername);
+ snis->hints_from_sni.ssl_ticket_enabled = session_ticket_enabled;
+ }
+
+ // Apply the ticket enable/disable flag immediately so the current handshake
+ // sees the per-SNI override before TLS session ticket processing kicks in.
+ if (session_ticket_enabled != 0) {
+ SSL_clear_options(&ssl, SSL_OP_NO_TICKET);
+ } else {
+ SSL_set_options(&ssl, SSL_OP_NO_TICKET);
+ }
+#endif
+ return SSL_TLSEXT_ERR_OK;
+}
+
+int
+ServerSessionTicketNumber::SNIAction(SSL &ssl, const Context & /* ctx ATS_UNUSED */) const
+{
+#if TS_HAS_TLS_SESSION_TICKET
+ if (auto snis = TLSSNISupport::getInstance(&ssl)) {
+ const char *servername = snis->get_sni_server_name();
+ Dbg(dbg_ctl_ssl_sni, "Setting session ticket count from sni.yaml to %d for fqdn [%s]", session_ticket_number, servername);
+ snis->hints_from_sni.ssl_ticket_number = session_ticket_number;
+ }
+#endif
+ return SSL_TLSEXT_ERR_OK;
+}
+
int
ServerCipherSuite::SNIAction(SSL &ssl, const Context & /* ctx ATS_UNUSED */) const
{
diff --git a/src/iocore/net/SNIActionPerformer.h b/src/iocore/net/SNIActionPerformer.h
index c173caacaad..6f0e328333c 100644
--- a/src/iocore/net/SNIActionPerformer.h
+++ b/src/iocore/net/SNIActionPerformer.h
@@ -313,6 +313,36 @@ class ServerMaxEarlyData : public ActionItem
#endif
};
+/**
+ Override session ticket support by ssl_ticket_enabled in sni.yaml
+ */
+class ServerSessionTicketEnabled : public ActionItem
+{
+public:
+ ServerSessionTicketEnabled(int value) : session_ticket_enabled(value) {}
+ ~ServerSessionTicketEnabled() override {}
+
+ int SNIAction(SSL &ssl, const Context &ctx) const override;
+
+private:
+ int session_ticket_enabled = 0;
+};
+
+/**
+ Override the number of issued TLSv1.3 session tickets by ssl_ticket_number in sni.yaml
+ */
+class ServerSessionTicketNumber : public ActionItem
+{
+public:
+ ServerSessionTicketNumber(int value) : session_ticket_number(value) {}
+ ~ServerSessionTicketNumber() override {}
+
+ int SNIAction(SSL &ssl, const Context &ctx) const override;
+
+private:
+ int session_ticket_number = 0;
+};
+
/**
Override proxy.config.ssl.server.cipher_suite by server_cipher_suite in sni.yaml
*/
diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc
index 16b984cd737..9bca4c30869 100644
--- a/src/iocore/net/SSLUtils.cc
+++ b/src/iocore/net/SSLUtils.cc
@@ -97,6 +97,12 @@ static DbgCtl dbg_ctl_ssl_session_cache{"ssl.session_cache"};
static DbgCtl dbg_ctl_ssl_error{"ssl.error"};
static DbgCtl dbg_ctl_ssl_verify{"ssl_verify"};
+#if TS_HAS_TLS_SESSION_TICKET
+static bool ssl_context_enable_ticket_callback(SSL_CTX *ctx);
+static bool ssl_apply_sni_session_ticket_properties(SSL *ssl);
+static bool ssl_set_session_ticket_number(SSL *ssl, size_t num_tickets);
+#endif
+
/* Using pthread thread ID and mutex functions directly, instead of
* ATS this_ethread / ProxyMutex, so that other linked libraries
* may use pthreads and openssl without confusing us here. (TS-2271).
@@ -304,15 +310,8 @@ ssl_cert_callback(SSL *ssl, [[maybe_unused]] void *arg)
setClientCertCACerts(ssl, sslnetvc->get_ca_cert_file(), sslnetvc->get_ca_cert_dir());
}
- // Reset the ticket callback if needed
- SSL_CTX *ctx = SSL_get_SSL_CTX(ssl);
- shared_SSLMultiCertConfigParams sslMultiCertSettings = std::make_shared();
- if (sslMultiCertSettings->session_ticket_enabled != 0) {
-#ifdef HAVE_SSL_CTX_SET_TLSEXT_TICKET_KEY_EVP_CB
- SSL_CTX_set_tlsext_ticket_key_evp_cb(ctx, ssl_callback_session_ticket);
-#else
- SSL_CTX_set_tlsext_ticket_key_cb(ctx, ssl_callback_session_ticket);
-#endif
+ if (!ssl_apply_sni_session_ticket_properties(ssl)) {
+ retval = 0;
}
}
#endif
@@ -493,6 +492,77 @@ 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_set_session_ticket_number(SSL *ssl, size_t num_tickets)
+{
+#if defined(OPENSSL_IS_BORINGSSL)
+ // BoringSSL only exposes SSL_CTX_set_num_tickets(), so the per-connection
+ // sni.yaml override is not available here.
+ (void)ssl;
+ (void)num_tickets;
+ return true;
+#else
+ return SSL_set_num_tickets(ssl, num_tickets) == 1;
+#endif
+}
+
+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;
+ }
+
+ std::optional num_tickets;
+
+ if (hints.ssl_ticket_enabled.has_value()) {
+ if (hints.ssl_ticket_enabled.value() != 0) {
+ SSL_clear_options(ssl, SSL_OP_NO_TICKET);
+ Dbg(dbg_ctl_ssl_load, "Enabled session tickets due to sni.yaml override");
+ } else {
+ SSL_set_options(ssl, SSL_OP_NO_TICKET);
+ num_tickets = 0;
+ Dbg(dbg_ctl_ssl_load, "Disabled session tickets due to sni.yaml override");
+ }
+ }
+
+ if ((!hints.ssl_ticket_enabled.has_value() || hints.ssl_ticket_enabled.value() != 0) && hints.ssl_ticket_number.has_value()) {
+ num_tickets = hints.ssl_ticket_number.value() > 0 ? static_cast(hints.ssl_ticket_number.value()) : 0;
+ }
+
+ if (num_tickets.has_value()) {
+ if (!ssl_set_session_ticket_number(ssl, num_tickets.value())) {
+ Error("failed to set session ticket number from sni.yaml");
+ return false;
+ }
+ Dbg(dbg_ctl_ssl_load, "Set session ticket number from sni.yaml to %zu", num_tickets.value());
+ }
+
+ return true;
+}
+#endif
+
static ssl_ticket_key_block *
ssl_context_enable_tickets(SSL_CTX *ctx, const char *ticket_key_path)
{
@@ -509,12 +579,7 @@ ssl_context_enable_tickets(SSL_CTX *ctx, const char *ticket_key_path)
// Setting the callback can only fail if OpenSSL does not recognize the
// SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB constant. we set the callback first
// so that we don't leave a ticket_key pointer attached if it fails.
-#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");
+ if (!ssl_context_enable_ticket_callback(ctx)) {
ticket_block_free(keyblock);
return nullptr;
}
@@ -1179,6 +1244,12 @@ SSLMultiCertConfigLoader::init_server_ssl_ctx(CertLoadData const &data, const SS
}
}
+#if TS_HAS_TLS_SESSION_TICKET
+ if (!ssl_context_enable_ticket_callback(ctx)) {
+ goto fail;
+ }
+#endif
+
if (!this->_setup_client_cert_verification(ctx)) {
goto fail;
}
diff --git a/src/iocore/net/TLSSNISupport.cc b/src/iocore/net/TLSSNISupport.cc
index 9b56c9129be..b6acd202b2b 100644
--- a/src/iocore/net/TLSSNISupport.cc
+++ b/src/iocore/net/TLSSNISupport.cc
@@ -165,6 +165,7 @@ TLSSNISupport::set_sni_server_name(SSL *ssl, char const *name)
void
TLSSNISupport::_clear()
{
+ hints_from_sni = {};
_sni_server_name.reset();
}
diff --git a/src/iocore/net/YamlSNIConfig.cc b/src/iocore/net/YamlSNIConfig.cc
index bbc0eb4ace0..ed1cd229162 100644
--- a/src/iocore/net/YamlSNIConfig.cc
+++ b/src/iocore/net/YamlSNIConfig.cc
@@ -164,6 +164,18 @@ YamlSNIConfig::Item::populate_sni_actions(action_vector_t &actions)
if (!server_groups_list.empty()) {
actions.push_back(std::make_unique(server_groups_list));
}
+ if (ssl_ticket_enabled.has_value()) {
+ actions.push_back(std::make_unique(ssl_ticket_enabled.value()));
+ }
+ if (ssl_ticket_number.has_value()) {
+#if defined(OPENSSL_IS_BORINGSSL)
+ const char *servername = fqdn.empty() ? "*" : fqdn.c_str();
+ Warning(
+ "sni.yaml: BoringSSL does not support setting the session ticket number, so ssl_ticket_number does not apply for fqdn '%s'",
+ servername);
+#endif
+ actions.push_back(std::make_unique(ssl_ticket_number.value()));
+ }
if (http2_buffer_water_mark.has_value()) {
actions.push_back(std::make_unique(http2_buffer_water_mark.value()));
}
@@ -230,6 +242,8 @@ std::set valid_sni_config_keys = {TS_fqdn,
TS_server_TLSv1_3_cipher_suites,
#endif
TS_server_groups_list,
+ TS_ssl_ticket_enabled,
+ TS_ssl_ticket_number,
TS_http2,
TS_http2_buffer_water_mark,
TS_http2_initial_window_size_in,
@@ -465,6 +479,12 @@ template <> struct convert {
if (node[TS_server_groups_list]) {
item.server_groups_list = node[TS_server_groups_list].as();
}
+ if (node[TS_ssl_ticket_enabled]) {
+ item.ssl_ticket_enabled = node[TS_ssl_ticket_enabled].as();
+ }
+ if (node[TS_ssl_ticket_number]) {
+ item.ssl_ticket_number = node[TS_ssl_ticket_number].as();
+ }
if (node[TS_ip_allow]) {
item.ip_allow = node[TS_ip_allow].as();
}
diff --git a/src/iocore/net/unit_tests/sni_conf_test.yaml b/src/iocore/net/unit_tests/sni_conf_test.yaml
index 2f7a1dedc39..487daf8070c 100644
--- a/src/iocore/net/unit_tests/sni_conf_test.yaml
+++ b/src/iocore/net/unit_tests/sni_conf_test.yaml
@@ -49,3 +49,8 @@ sni:
# test glob in the middle, this will be an exact match
- fqdn: "cat.*.com"
+
+# test session ticket overrides
+- fqdn: tickets.com
+ ssl_ticket_enabled: 1
+ ssl_ticket_number: 3
diff --git a/src/iocore/net/unit_tests/test_SSLSNIConfig.cc b/src/iocore/net/unit_tests/test_SSLSNIConfig.cc
index 19fbb31c3b9..382c6aab7f7 100644
--- a/src/iocore/net/unit_tests/test_SSLSNIConfig.cc
+++ b/src/iocore/net/unit_tests/test_SSLSNIConfig.cc
@@ -107,6 +107,13 @@ TEST_CASE("Test SSLSNIConfig")
REQUIRE(actions.first->size() == 3);
}
+ SECTION("The config matches an SNI for tickets.com")
+ {
+ auto const &actions{params.get("tickets.com", 443)};
+ REQUIRE(actions.first);
+ REQUIRE(actions.first->size() == 4); ///< ticket enabled + ticket number + early data + fqdn
+ }
+
SECTION("Matching order")
{
auto const &actions{params.get("foo.bar.com", 443)};
diff --git a/src/iocore/net/unit_tests/test_YamlSNIConfig.cc b/src/iocore/net/unit_tests/test_YamlSNIConfig.cc
index 14327bb484b..7f22739e6b6 100644
--- a/src/iocore/net/unit_tests/test_YamlSNIConfig.cc
+++ b/src/iocore/net/unit_tests/test_YamlSNIConfig.cc
@@ -56,7 +56,7 @@ TEST_CASE("YamlSNIConfig sets port ranges appropriately")
FAIL(errorstream.str());
}
REQUIRE(zret.is_ok());
- REQUIRE(conf.items.size() == 10);
+ REQUIRE(conf.items.size() == 11);
SECTION("If no ports were specified, port range should contain all ports.")
{
@@ -103,6 +103,15 @@ TEST_CASE("YamlSNIConfig sets port ranges appropriately")
{
CHECK(conf.items[2].inbound_port_ranges.size() == 1);
}
+
+ SECTION("Session ticket overrides are parsed.")
+ {
+ auto const &item{conf.items[10]};
+ REQUIRE(item.ssl_ticket_enabled.has_value());
+ CHECK(item.ssl_ticket_enabled.value() == 1);
+ REQUIRE(item.ssl_ticket_number.has_value());
+ CHECK(item.ssl_ticket_number.value() == 3);
+ }
}
TEST_CASE("YamlConfig handles bad ports appropriately.")
diff --git a/tests/gold_tests/tls/tls_sni_ticket.test.py b/tests/gold_tests/tls/tls_sni_ticket.test.py
new file mode 100644
index 00000000000..25f7473dc33
--- /dev/null
+++ b/tests/gold_tests/tls/tls_sni_ticket.test.py
@@ -0,0 +1,264 @@
+'''
+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:
+ _server_is_started = False
+ _ts_on_started = False
+ _ts_off_started = False
+
+ 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)
+
+ def start_processes_if_needed(
+ self, tr: Any, start_server: bool = False, start_ts_on: bool = False, start_ts_off: bool = False) -> None:
+ """
+ Register one-time StartBefore hooks for the processes needed by a test run.
+
+ :param tr: The AuTest run definition being configured.
+ :param start_server: Whether the origin server should be started for this run.
+ :param start_ts_on: Whether the tickets-enabled ATS process should be started for this run.
+ :param start_ts_off: Whether the tickets-disabled ATS process should be started for this run.
+ """
+ if start_server and not TlsSniTicketTest._server_is_started:
+ tr.Processes.Default.StartBefore(self.server)
+ TlsSniTicketTest._server_is_started = True
+
+ if start_ts_on and not TlsSniTicketTest._ts_on_started:
+ tr.Processes.Default.StartBefore(self.ts_on)
+ TlsSniTicketTest._ts_on_started = True
+
+ if start_ts_off and not TlsSniTicketTest._ts_off_started:
+ tr.Processes.Default.StartBefore(self.ts_off)
+ TlsSniTicketTest._ts_off_started = True
+
+ @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
+ self.start_processes_if_needed(tr, start_server=True, start_ts_on=True)
+ 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
+ self.start_processes_if_needed(tr, start_server=True, start_ts_on=True)
+ 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')
+ self.start_processes_if_needed(tr, start_server=True, start_ts_off=True)
+ 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'
+ )
+ self.start_processes_if_needed(tr, start_server=True, start_ts_off=True)
+ 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
+
+ def run(self) -> None:
+ """
+ Register all AuTest runs for the SNI ticket override coverage.
+ """
+ self.add_tls12_enabled_run()
+ self.add_tls13_enabled_run()
+ self.add_tls12_disabled_run()
+ self.add_tls13_disabled_run()
+
+
+TlsSniTicketTest().run()