This is an automated email from the ASF dual-hosted git repository.

bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 8d75849e0e Add sni.yaml session ticket overrides (#13006)
8d75849e0e is described below

commit 8d75849e0e3574748f25fd682001e0566d75bd18
Author: Brian Neradt <[email protected]>
AuthorDate: Sat Mar 21 10:32:00 2026 -0500

    Add sni.yaml session ticket overrides (#13006)
    
    Add ssl_ticket_enabled and ssl_ticket_number as sni.yaml overrides, apply 
them during SNI handling so they affect TLS 1.2 resumption and TLS 1.3 ticket 
issuance, and add unit and AuTest coverage plus docs and sample config updates.
    
    Fixes #12953
---
 configs/sni.yaml.default                        |   6 +
 doc/admin-guide/files/records.yaml.en.rst       |   5 +
 doc/admin-guide/files/sni.yaml.en.rst           |  15 ++
 include/iocore/net/TLSSNISupport.h              |   2 +
 include/iocore/net/YamlSNIConfig.h              |   4 +
 src/iocore/net/SNIActionPerformer.cc            |  34 +++
 src/iocore/net/SNIActionPerformer.h             |  30 +++
 src/iocore/net/SSLUtils.cc                      | 101 +++++++--
 src/iocore/net/TLSSNISupport.cc                 |   1 +
 src/iocore/net/YamlSNIConfig.cc                 |  20 ++
 src/iocore/net/unit_tests/sni_conf_test.yaml    |   5 +
 src/iocore/net/unit_tests/test_SSLSNIConfig.cc  |   7 +
 src/iocore/net/unit_tests/test_YamlSNIConfig.cc |  11 +-
 tests/gold_tests/tls/tls_sni_ticket.test.py     | 264 ++++++++++++++++++++++++
 14 files changed, 489 insertions(+), 16 deletions(-)

diff --git a/configs/sni.yaml.default b/configs/sni.yaml.default
index c39363a6ef..57a1ed24ed 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 46651a5d66..492dc9849f 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 9595894363..404f7ea62b 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 
<https://docs.openssl.org/3.5/man3/SSL_CTX_set1_curves/>`_
                                                    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 22f3d751a8..346c2c1c0a 100644
--- a/include/iocore/net/TLSSNISupport.h
+++ b/include/iocore/net/TLSSNISupport.h
@@ -140,6 +140,8 @@ public:
     std::optional<int32_t>          http2_max_priority_frames_per_minute;
     std::optional<int32_t>          http2_max_rst_stream_frames_per_minute;
     std::optional<int32_t>          http2_max_continuation_frames_per_minute;
+    std::optional<int32_t>          ssl_ticket_enabled;
+    std::optional<int32_t>          ssl_ticket_number;
     std::optional<std::string_view> outbound_sni_policy;
   } hints_from_sni;
 
diff --git a/include/iocore/net/YamlSNIConfig.h 
b/include/iocore/net/YamlSNIConfig.h
index c8396a76e7..91d0683f8f 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<int>  ssl_ticket_enabled;
+    std::optional<int>  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 d0ea144f18..a5c04710eb 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 c173caacaa..6f0e328333 100644
--- a/src/iocore/net/SNIActionPerformer.h
+++ b/src/iocore/net/SNIActionPerformer.h
@@ -313,6 +313,36 @@ private:
 #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 16b984cd73..9bca4c3086 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<SSLMultiCertConfigParams>();
-    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<size_t> 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<size_t>(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 9b56c9129b..b6acd202b2 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 bbc0eb4ace..ed1cd22916 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<ServerGroupsList>(server_groups_list));
   }
+  if (ssl_ticket_enabled.has_value()) {
+    
actions.push_back(std::make_unique<ServerSessionTicketEnabled>(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<ServerSessionTicketNumber>(ssl_ticket_number.value()));
+  }
   if (http2_buffer_water_mark.has_value()) {
     
actions.push_back(std::make_unique<HTTP2BufferWaterMark>(http2_buffer_water_mark.value()));
   }
@@ -230,6 +242,8 @@ std::set<std::string> 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<YamlSNIConfig::Item> {
     if (node[TS_server_groups_list]) {
       item.server_groups_list = node[TS_server_groups_list].as<std::string>();
     }
+    if (node[TS_ssl_ticket_enabled]) {
+      item.ssl_ticket_enabled = node[TS_ssl_ticket_enabled].as<int>();
+    }
+    if (node[TS_ssl_ticket_number]) {
+      item.ssl_ticket_number = node[TS_ssl_ticket_number].as<int>();
+    }
     if (node[TS_ip_allow]) {
       item.ip_allow = node[TS_ip_allow].as<std::string>();
     }
diff --git a/src/iocore/net/unit_tests/sni_conf_test.yaml 
b/src/iocore/net/unit_tests/sni_conf_test.yaml
index 2f7a1dedc3..487daf8070 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 19fbb31c3b..382c6aab7f 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 14327bb484..7f22739e6b 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 0000000000..25f7473dc3
--- /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()

Reply via email to