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

maskit 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 02af45126d Add support for TLS Certificate Compression (RFC 8879) 
(#13088)
02af45126d is described below

commit 02af45126d976d6eadaf6c09de9bcb8570b4a62e
Author: Masakazu Kitajo <[email protected]>
AuthorDate: Wed Apr 22 14:46:07 2026 -0600

    Add support for TLS Certificate Compression (RFC 8879) (#13088)
    
    New settings:
    proxy.config.ssl.server.cert_compression.algorithms
    proxy.config.ssl.client.cert_compression.algorithms
    proxy.config.ssl.server.cert_compression.cache is going to be added on next 
PR.
    
    New metrics:
    proxy.process.ssl.cert_compress.<alg>
    proxy.process.ssl.cert_compress.<alg>_failure
    proxy.process.ssl.cert_decompress.<alg>
    proxy.process.ssl.cert_decompress.<alg>_failure
---
 CMakeLists.txt                                     |   2 +
 cmake/Findbrotli.cmake                             |  15 +-
 doc/admin-guide/files/records.yaml.en.rst          |  34 ++++
 .../monitoring/statistics/core/ssl.en.rst          |  66 ++++++++
 include/iocore/net/SSLMultiCertConfigLoader.h      |   1 +
 include/tscore/ink_config.h.cmake.in               |   2 +
 src/iocore/net/CMakeLists.txt                      |  16 ++
 src/iocore/net/P_SSLConfig.h                       |   3 +
 src/iocore/net/SSLClientUtils.cc                   |  14 ++
 src/iocore/net/SSLConfig.cc                        |  21 ++-
 src/iocore/net/SSLStats.cc                         |  12 ++
 src/iocore/net/SSLStats.h                          |  12 ++
 src/iocore/net/SSLUtils.cc                         |  25 +++
 src/iocore/net/TLSCertCompression.cc               | 128 +++++++++++++++
 src/iocore/net/TLSCertCompression.h                |  37 +++++
 src/iocore/net/TLSCertCompression_brotli.cc        |  77 +++++++++
 src/iocore/net/TLSCertCompression_brotli.h         |  30 ++++
 src/iocore/net/TLSCertCompression_zlib.cc          |  75 +++++++++
 src/iocore/net/TLSCertCompression_zlib.h           |  30 ++++
 src/iocore/net/TLSCertCompression_zstd.cc          |  82 ++++++++++
 src/iocore/net/TLSCertCompression_zstd.h           |  30 ++++
 src/records/RecordsConfig.cc                       |   4 +
 src/traffic_layout/info.cc                         |   5 +
 .../tls/replay/tls_cert_compression.replay.yaml    |  41 +++++
 tests/gold_tests/tls/tls_cert_comp.test.py         | 175 +++++++++++++++++++++
 25 files changed, 930 insertions(+), 7 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 923651059f..49831bfebf 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -541,6 +541,8 @@ check_symbol_exists(SSL_error_description "openssl/ssl.h" 
HAVE_SSL_ERROR_DESCRIP
 check_symbol_exists(SSL_CTX_set_ciphersuites "openssl/ssl.h" 
TS_USE_TLS_SET_CIPHERSUITES)
 check_symbol_exists(SSL_CTX_set_keylog_callback "openssl/ssl.h" 
TS_HAS_TLS_KEYLOGGING)
 check_symbol_exists(SSL_CTX_set_tlsext_ticket_key_cb "openssl/ssl.h" 
HAVE_SSL_CTX_SET_TLSEXT_TICKET_KEY_CB)
+check_symbol_exists(SSL_CTX_add_cert_compression_alg "openssl/ssl.h" 
HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG)
+check_symbol_exists(SSL_CTX_set1_cert_comp_preference "openssl/ssl.h" 
HAVE_SSL_CTX_SET1_CERT_COMP_PREFERENCE)
 check_symbol_exists(SSL_get_all_async_fds openssl/ssl.h TS_USE_TLS_ASYNC)
 check_symbol_exists(OSSL_PARAM_construct_end "openssl/params.h" 
HAVE_OSSL_PARAM_CONSTRUCT_END)
 check_symbol_exists(TLS1_3_VERSION "openssl/ssl.h" TS_USE_TLS13)
diff --git a/cmake/Findbrotli.cmake b/cmake/Findbrotli.cmake
index 8a2c63b4a8..bf12a0ab7e 100644
--- a/cmake/Findbrotli.cmake
+++ b/cmake/Findbrotli.cmake
@@ -21,23 +21,28 @@
 #
 #     brotli_FOUND
 #     brotlicommon_LIBRARY
+#     brotlidec_LIBRARY
 #     brotlienc_LIBRARY
 #     brotli_INCLUDE_DIRS
 #
 # and the following imported targets
 #
 #     brotli::brotlicommon
+#     brotli::brotlidec
 #     brotli::brotlienc
 #
 
 find_library(brotlicommon_LIBRARY NAMES brotlicommon)
+find_library(brotlidec_LIBRARY NAMES brotlidec)
 find_library(brotlienc_LIBRARY NAMES brotlienc)
 find_path(brotli_INCLUDE_DIR NAMES brotli/encode.h)
 
-mark_as_advanced(brotli_FOUND brotlicommon_LIBRARY brotlienc_LIBRARY 
brotli_INCLUDE_DIR)
+mark_as_advanced(brotli_FOUND brotlicommon_LIBRARY brotlidec_LIBRARY 
brotlienc_LIBRARY brotli_INCLUDE_DIR)
 
 include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(brotli REQUIRED_VARS brotlicommon_LIBRARY 
brotlienc_LIBRARY brotli_INCLUDE_DIR)
+find_package_handle_standard_args(
+  brotli REQUIRED_VARS brotlicommon_LIBRARY brotlidec_LIBRARY 
brotlienc_LIBRARY brotli_INCLUDE_DIR
+)
 
 if(brotli_FOUND)
   set(brotli_INCLUDE_DIRS "${brotli_INCLUDE_DIR}")
@@ -49,6 +54,12 @@ if(brotli_FOUND AND NOT TARGET brotli::brotlicommon)
   target_link_libraries(brotli::brotlicommon INTERFACE 
"${brotlicommon_LIBRARY}")
 endif()
 
+if(brotli_FOUND AND NOT TARGET brotli::brotlidec)
+  add_library(brotli::brotlidec INTERFACE IMPORTED)
+  target_include_directories(brotli::brotlidec INTERFACE 
${brotli_INCLUDE_DIRS})
+  target_link_libraries(brotli::brotlidec INTERFACE brotli::brotlicommon 
"${brotlidec_LIBRARY}")
+endif()
+
 if(brotli_FOUND AND NOT TARGET brotli::brotlienc)
   add_library(brotli::brotlienc INTERFACE IMPORTED)
   target_include_directories(brotli::brotlienc INTERFACE 
${brotli_INCLUDE_DIRS})
diff --git a/doc/admin-guide/files/records.yaml.en.rst 
b/doc/admin-guide/files/records.yaml.en.rst
index 6fdc524275..30b8d5c255 100644
--- a/doc/admin-guide/files/records.yaml.en.rst
+++ b/doc/admin-guide/files/records.yaml.en.rst
@@ -4280,6 +4280,40 @@ SSL Termination
    ``1`` Enables the use of Kernel TLS..
    ===== ======================================================================
 
+.. ts:cv:: CONFIG proxy.config.ssl.server.cert_compression.algorithms STRING
+   :reloadable:
+
+   A comma-separated list of compression algorithms that |TS| is willing to
+   use for TLS Certificate Compression
+   (`RFC 8879 <https://datatracker.ietf.org/doc/html/rfc8879>`_) when |TS|
+   acts as a TLS server (i.e. accepting connections from clients).  When a
+   connecting client advertises support for one of these algorithms, |TS| will
+   send its certificate in compressed form, reducing handshake size.
+
+   Supported values: ``zlib``, ``brotli``, ``zstd``.  The order determines the
+   server's preference.  An empty value (the default) disables certificate
+   compression.
+
+   ``brotli`` and ``zstd`` are only available when |TS| is compiled with the
+   corresponding libraries.
+
+   Example::
+
+      proxy.config.ssl.server.cert_compression.algorithms: zlib,brotli
+
+.. ts:cv:: CONFIG proxy.config.ssl.client.cert_compression.algorithms STRING
+   :reloadable:
+
+   A comma-separated list of compression algorithms that |TS| advertises for
+   TLS Certificate Compression
+   (`RFC 8879 <https://datatracker.ietf.org/doc/html/rfc8879>`_) when |TS|
+   acts as a TLS client (i.e. connecting to origin servers).  When the origin
+   supports one of these algorithms, |TS| will accept and decompress the
+   certificate.
+
+   Supported values: ``zlib``, ``brotli``, ``zstd``. An empty value (the
+   default) disables certificate compression.
+
 Client-Related Configuration
 ----------------------------
 
diff --git a/doc/admin-guide/monitoring/statistics/core/ssl.en.rst 
b/doc/admin-guide/monitoring/statistics/core/ssl.en.rst
index efef309c22..c3dc1bb799 100644
--- a/doc/admin-guide/monitoring/statistics/core/ssl.en.rst
+++ b/doc/admin-guide/monitoring/statistics/core/ssl.en.rst
@@ -389,3 +389,69 @@ Stats for Pre-warming TLS Tunnel is registered 
dynamically. The ``POOL`` in belo
    :type: counter
 
    Represents the total number of pre-warming retry.
+
+.. ts:stat:: global proxy.process.ssl.cert_compress.zlib integer
+   :type: counter
+
+   The number of times a server certificate was compressed with zlib during a
+   TLS handshake.
+
+.. ts:stat:: global proxy.process.ssl.cert_compress.zlib_failure integer
+   :type: counter
+
+   The number of times zlib compression of a server certificate failed.
+
+.. ts:stat:: global proxy.process.ssl.cert_decompress.zlib integer
+   :type: counter
+
+   The number of times a certificate received from an origin server was
+   decompressed with zlib.
+
+.. ts:stat:: global proxy.process.ssl.cert_decompress.zlib_failure integer
+   :type: counter
+
+   The number of times zlib decompression of a certificate failed.
+
+.. ts:stat:: global proxy.process.ssl.cert_compress.brotli integer
+   :type: counter
+
+   The number of times a server certificate was compressed with Brotli during a
+   TLS handshake.
+
+.. ts:stat:: global proxy.process.ssl.cert_compress.brotli_failure integer
+   :type: counter
+
+   The number of times Brotli compression of a server certificate failed.
+
+.. ts:stat:: global proxy.process.ssl.cert_decompress.brotli integer
+   :type: counter
+
+   The number of times a certificate received from an origin server was
+   decompressed with Brotli.
+
+.. ts:stat:: global proxy.process.ssl.cert_decompress.brotli_failure integer
+   :type: counter
+
+   The number of times Brotli decompression of a certificate failed.
+
+.. ts:stat:: global proxy.process.ssl.cert_compress.zstd integer
+   :type: counter
+
+   The number of times a server certificate was compressed with zstd during a
+   TLS handshake.
+
+.. ts:stat:: global proxy.process.ssl.cert_compress.zstd_failure integer
+   :type: counter
+
+   The number of times zstd compression of a server certificate failed.
+
+.. ts:stat:: global proxy.process.ssl.cert_decompress.zstd integer
+   :type: counter
+
+   The number of times a certificate received from an origin server was
+   decompressed with zstd.
+
+.. ts:stat:: global proxy.process.ssl.cert_decompress.zstd_failure integer
+   :type: counter
+
+   The number of times zstd decompression of a certificate failed.
diff --git a/include/iocore/net/SSLMultiCertConfigLoader.h 
b/include/iocore/net/SSLMultiCertConfigLoader.h
index d0f68469ce..c12594cb0f 100644
--- a/include/iocore/net/SSLMultiCertConfigLoader.h
+++ b/include/iocore/net/SSLMultiCertConfigLoader.h
@@ -109,6 +109,7 @@ private:
   virtual bool _set_npn_callback(SSL_CTX *ctx);
   virtual bool _set_alpn_callback(SSL_CTX *ctx);
   virtual bool _set_keylog_callback(SSL_CTX *ctx);
+  virtual bool _enable_cert_compression(SSL_CTX *ctx);
   virtual bool _enable_ktls(SSL_CTX *ctx);
   virtual bool _enable_early_data(SSL_CTX *ctx);
 };
diff --git a/include/tscore/ink_config.h.cmake.in 
b/include/tscore/ink_config.h.cmake.in
index bf012cefec..73c8b860fb 100644
--- a/include/tscore/ink_config.h.cmake.in
+++ b/include/tscore/ink_config.h.cmake.in
@@ -186,6 +186,8 @@ const int DEFAULT_STACKSIZE = @DEFAULT_STACK_SIZE@;
 #cmakedefine01 HAVE_SSL_ERROR_DESCRIPTION
 #cmakedefine01 HAVE_OSSL_PARAM_CONSTRUCT_END
 #cmakedefine01 TS_USE_TLS_SET_CIPHERSUITES
+#cmakedefine01 HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG
+#cmakedefine01 HAVE_SSL_CTX_SET1_CERT_COMP_PREFERENCE
 
 #define TS_BUILD_CANONICAL_HOST "@CMAKE_HOST@"
 
diff --git a/src/iocore/net/CMakeLists.txt b/src/iocore/net/CMakeLists.txt
index 16563e10ab..c5b66d8468 100644
--- a/src/iocore/net/CMakeLists.txt
+++ b/src/iocore/net/CMakeLists.txt
@@ -61,6 +61,7 @@ add_library(
   TLSSessionResumptionSupport.cc
   TLSSNISupport.cc
   TLSTunnelSupport.cc
+  TLSCertCompression.cc
   UDPEventIO.cc
   UDPIOEvent.cc
   UnixConnection.cc
@@ -116,6 +117,21 @@ if(TS_USE_LINUX_IO_URING)
   target_link_libraries(inknet PUBLIC ts::inkuring)
 endif()
 
+# Link cert compression libraries after OpenSSL so that OpenSSL include
+# directories appear first in the search order, preventing broad system
+# include paths (e.g. from Homebrew's zstd) from shadowing them.
+if(HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG)
+  target_sources(inknet PRIVATE TLSCertCompression_zlib.cc)
+  if(HAVE_BROTLI_ENCODE_H)
+    target_sources(inknet PRIVATE TLSCertCompression_brotli.cc)
+    target_link_libraries(inknet PRIVATE brotli::brotlienc brotli::brotlidec)
+  endif()
+  if(HAVE_ZSTD_H)
+    target_sources(inknet PRIVATE TLSCertCompression_zstd.cc)
+    target_link_libraries(inknet PRIVATE zstd::zstd)
+  endif()
+endif()
+
 if(BUILD_TESTING)
   # libinknet_stub.cc is need because GNU ld is sensitive to the order of 
static libraries on the command line, and we have a cyclic dependency between 
inknet and proxy
   add_executable(
diff --git a/src/iocore/net/P_SSLConfig.h b/src/iocore/net/P_SSLConfig.h
index 0d6ee6b14e..6a6d5a24e4 100644
--- a/src/iocore/net/P_SSLConfig.h
+++ b/src/iocore/net/P_SSLConfig.h
@@ -88,6 +88,9 @@ struct SSLConfigParams : public ConfigInfo {
   unsigned char alpn_protocols_array[MAX_ALPN_STRING];
   int           alpn_protocols_array_size = 0;
 
+  char *server_cert_compression_algorithms;
+  char *client_cert_compression_algorithms;
+
   char *server_tls13_cipher_suites;
   char *client_tls13_cipher_suites;
   char *server_groups_list;
diff --git a/src/iocore/net/SSLClientUtils.cc b/src/iocore/net/SSLClientUtils.cc
index fe08b43a4f..453b971dd7 100644
--- a/src/iocore/net/SSLClientUtils.cc
+++ b/src/iocore/net/SSLClientUtils.cc
@@ -24,9 +24,11 @@
 #include "P_SSLNetVConnection.h"
 #include "P_TLSKeyLogger.h"
 #include "SSLSessionCache.h"
+#include "TLSCertCompression.h"
 #include "iocore/net/YamlSNIConfig.h"
 #include "iocore/net/SSLDiags.h"
 #include "tscore/ink_config.h"
+#include "tscore/SimpleTokenizer.h"
 #include "tscore/Filenames.h"
 #include "tscore/X509HostnameValidator.h"
 
@@ -247,6 +249,18 @@ SSLInitClientContext(const SSLConfigParams *params)
   }
 #endif
 
+  if (params->client_cert_compression_algorithms) {
+    std::vector<std::string> algs;
+    SimpleTokenizer          tok(params->client_cert_compression_algorithms, 
',');
+    for (const char *token = tok.getNext(); token; token = tok.getNext()) {
+      algs.emplace_back(token);
+    }
+    if (register_certificate_compression_preference(client_ctx, algs) != 1) {
+      SSLError("invalid client certificate compression algorithm list in %s", 
ts::filename::RECORDS);
+      goto fail;
+    }
+  }
+
   SSL_CTX_set_verify_depth(client_ctx, params->client_verify_depth);
   if (SSLConfigParams::init_ssl_ctx_cb) {
     SSLConfigParams::init_ssl_ctx_cb(client_ctx, false);
diff --git a/src/iocore/net/SSLConfig.cc b/src/iocore/net/SSLConfig.cc
index c6b5f42232..4480acef51 100644
--- a/src/iocore/net/SSLConfig.cc
+++ b/src/iocore/net/SSLConfig.cc
@@ -115,6 +115,8 @@ SSLConfigParams::reset()
     clientKeyPath = clientCACertFilename = clientCACertPath = cipherSuite = 
client_cipherSuite = dhparamsFile = serverKeyPathOnly =
       clientKeyPathOnly = clientCertPathOnly = nullptr;
   ssl_ocsp_response_path_only                = nullptr;
+  server_cert_compression_algorithms         = nullptr;
+  client_cert_compression_algorithms         = nullptr;
   server_tls13_cipher_suites                 = nullptr;
   client_tls13_cipher_suites                 = nullptr;
   server_groups_list                         = nullptr;
@@ -151,11 +153,13 @@ SSLConfigParams::cleanup()
 
   ssl_ocsp_response_path_only = static_cast<char 
*>(ats_free_null(ssl_ocsp_response_path_only));
 
-  server_tls13_cipher_suites = static_cast<char 
*>(ats_free_null(server_tls13_cipher_suites));
-  client_tls13_cipher_suites = static_cast<char 
*>(ats_free_null(client_tls13_cipher_suites));
-  server_groups_list         = static_cast<char 
*>(ats_free_null(server_groups_list));
-  client_groups_list         = static_cast<char 
*>(ats_free_null(client_groups_list));
-  keylog_file                = static_cast<char *>(ats_free_null(keylog_file));
+  server_cert_compression_algorithms = static_cast<char 
*>(ats_free_null(server_cert_compression_algorithms));
+  client_cert_compression_algorithms = static_cast<char 
*>(ats_free_null(client_cert_compression_algorithms));
+  server_tls13_cipher_suites         = static_cast<char 
*>(ats_free_null(server_tls13_cipher_suites));
+  client_tls13_cipher_suites         = static_cast<char 
*>(ats_free_null(client_tls13_cipher_suites));
+  server_groups_list                 = static_cast<char 
*>(ats_free_null(server_groups_list));
+  client_groups_list                 = static_cast<char 
*>(ats_free_null(client_groups_list));
+  keylog_file                        = static_cast<char 
*>(ats_free_null(keylog_file));
 
   cleanupCTXTable();
   reset();
@@ -494,6 +498,13 @@ SSLConfigParams::initialize()
     server_groups_list = ats_stringdup(rec_str);
   }
 
+  if (auto 
rec_str{RecGetRecordStringAlloc("proxy.config.ssl.server.cert_compression.algorithms")};
 rec_str) {
+    server_cert_compression_algorithms = ats_stringdup(rec_str);
+  }
+  if (auto 
rec_str{RecGetRecordStringAlloc("proxy.config.ssl.client.cert_compression.algorithms")};
 rec_str) {
+    client_cert_compression_algorithms = ats_stringdup(rec_str);
+  }
+
   // ++++++++++++++++++++++++ Client part ++++++++++++++++++++
   client_verify_depth = 7;
 
diff --git a/src/iocore/net/SSLStats.cc b/src/iocore/net/SSLStats.cc
index e6bfdcbdf7..60e3b41d77 100644
--- a/src/iocore/net/SSLStats.cc
+++ b/src/iocore/net/SSLStats.cc
@@ -175,6 +175,18 @@ SSLInitializeStatistics()
   // For now, register with the librecords global sync.
   RecRegNewSyncStatSync(SSLPeriodicMetricsUpdate);
 
+  ssl_rsb.cert_compress_zlib                 = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.zlib");
+  ssl_rsb.cert_compress_zlib_failure         = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.zlib_failure");
+  ssl_rsb.cert_decompress_zlib               = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.zlib");
+  ssl_rsb.cert_decompress_zlib_failure       = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.zlib_failure");
+  ssl_rsb.cert_compress_brotli               = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.brotli");
+  ssl_rsb.cert_compress_brotli_failure       = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.brotli_failure");
+  ssl_rsb.cert_decompress_brotli             = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.brotli");
+  ssl_rsb.cert_decompress_brotli_failure     = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.brotli_failure");
+  ssl_rsb.cert_compress_zstd                 = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.zstd");
+  ssl_rsb.cert_compress_zstd_failure         = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_compress.zstd_failure");
+  ssl_rsb.cert_decompress_zstd               = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.zstd");
+  ssl_rsb.cert_decompress_zstd_failure       = 
Metrics::Counter::createPtr("proxy.process.ssl.cert_decompress.zstd_failure");
   ssl_rsb.early_data_received_count          = 
Metrics::Counter::createPtr("proxy.process.ssl.early_data_received");
   ssl_rsb.error_async                        = 
Metrics::Counter::createPtr("proxy.process.ssl.ssl_error_async");
   ssl_rsb.error_ssl                          = 
Metrics::Counter::createPtr("proxy.process.ssl.ssl_error_ssl");
diff --git a/src/iocore/net/SSLStats.h b/src/iocore/net/SSLStats.h
index 152abf3acb..50ae6f89de 100644
--- a/src/iocore/net/SSLStats.h
+++ b/src/iocore/net/SSLStats.h
@@ -37,6 +37,18 @@ using ts::Metrics;
 // for ssl_rsb.total_ticket_keys_renewed needs this initialization, but lets be
 // consistent at least.
 struct SSLStatsBlock {
+  Metrics::Counter::AtomicType *cert_compress_zlib                             
= nullptr;
+  Metrics::Counter::AtomicType *cert_compress_zlib_failure                     
= nullptr;
+  Metrics::Counter::AtomicType *cert_decompress_zlib                           
= nullptr;
+  Metrics::Counter::AtomicType *cert_decompress_zlib_failure                   
= nullptr;
+  Metrics::Counter::AtomicType *cert_compress_brotli                           
= nullptr;
+  Metrics::Counter::AtomicType *cert_compress_brotli_failure                   
= nullptr;
+  Metrics::Counter::AtomicType *cert_decompress_brotli                         
= nullptr;
+  Metrics::Counter::AtomicType *cert_decompress_brotli_failure                 
= nullptr;
+  Metrics::Counter::AtomicType *cert_compress_zstd                             
= nullptr;
+  Metrics::Counter::AtomicType *cert_compress_zstd_failure                     
= nullptr;
+  Metrics::Counter::AtomicType *cert_decompress_zstd                           
= nullptr;
+  Metrics::Counter::AtomicType *cert_decompress_zstd_failure                   
= nullptr;
   Metrics::Counter::AtomicType *early_data_received_count                      
= nullptr;
   Metrics::Counter::AtomicType *error_async                                    
= nullptr;
   Metrics::Counter::AtomicType *error_ssl                                      
= nullptr;
diff --git a/src/iocore/net/SSLUtils.cc b/src/iocore/net/SSLUtils.cc
index ee2231615a..6f230334e2 100644
--- a/src/iocore/net/SSLUtils.cc
+++ b/src/iocore/net/SSLUtils.cc
@@ -30,6 +30,7 @@
 #include "SSLSessionCache.h"
 #include "SSLSessionTicket.h"
 #include "SSLDynlock.h" // IWYU pragma: keep - for ssl_dyn_*
+#include "TLSCertCompression.h"
 
 #include "iocore/net/SSLMultiCertConfigLoader.h"
 #include "config/ssl_multicert.h"
@@ -435,6 +436,26 @@ DH_get_2048_256()
 }
 #endif
 
+bool
+SSLMultiCertConfigLoader::_enable_cert_compression(SSL_CTX *ctx)
+{
+  std::vector<std::string> algs;
+
+  if (this->_params->server_cert_compression_algorithms) {
+    SimpleTokenizer tok(this->_params->server_cert_compression_algorithms, 
',');
+    for (const char *token = tok.getNext(); token; token = tok.getNext()) {
+      algs.emplace_back(token);
+    }
+  }
+
+  if (register_certificate_compression_preference(ctx, algs) == 1) {
+    return true;
+  } else {
+    SSLError("Failed to enable certificate compression");
+    return false;
+  }
+}
+
 bool
 SSLMultiCertConfigLoader::_enable_ktls([[maybe_unused]] SSL_CTX *ctx)
 {
@@ -1272,6 +1293,10 @@ 
SSLMultiCertConfigLoader::init_server_ssl_ctx(CertLoadData const &data, const SS
       goto fail;
     }
 
+    if (!this->_enable_cert_compression(ctx)) {
+      goto fail;
+    }
+
     if (!this->_enable_ktls(ctx)) {
       goto fail;
     }
diff --git a/src/iocore/net/TLSCertCompression.cc 
b/src/iocore/net/TLSCertCompression.cc
new file mode 100644
index 0000000000..3b31d3e1be
--- /dev/null
+++ b/src/iocore/net/TLSCertCompression.cc
@@ -0,0 +1,128 @@
+/** @file
+
+  Functions for Certificate Compression
+
+  @section license License
+
+  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.
+ */
+
+#include <openssl/ssl.h>
+
+#include "tscore/Diags.h"
+#include "TLSCertCompression.h"
+
+namespace
+{
+DbgCtl dbg_ctl_ssl_cert_compress{"ssl_cert_compress"};
+}
+
+constexpr unsigned int N_ALGORITHMS = 3;
+
+#if HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG
+#include "TLSCertCompression_zlib.h"
+
+#if HAVE_BROTLI_ENCODE_H
+#include "TLSCertCompression_brotli.h"
+#endif
+
+#if HAVE_ZSTD_H
+#include "TLSCertCompression_zstd.h"
+#endif
+#endif
+
+struct alg_info {
+  const char *name;
+  int32_t     number;
+#if HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG
+  ssl_cert_compression_func_t   compress_func;
+  ssl_cert_decompression_func_t decompress_func;
+#endif
+} supported_algs[] = {
+  {"zlib",   1,
+#if HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG
+   compression_func_zlib,   decompression_func_zlib
+#endif
+  },
+#if HAVE_BROTLI_ENCODE_H
+  {"brotli", 2,
+#if HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG
+   compression_func_brotli, decompression_func_brotli
+#endif
+  },
+#endif
+#if HAVE_ZSTD_H
+  {"zstd",   3,
+#if HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG
+   compression_func_zstd,   decompression_func_zstd
+#endif
+  },
+#endif
+};
+
+int
+register_certificate_compression_preference(SSL_CTX *ctx, const 
std::vector<std::string> &specified_algs)
+{
+  ink_assert(ctx != nullptr);
+  if (specified_algs.size() > N_ALGORITHMS) {
+    return 0;
+  }
+
+  if (specified_algs.empty()) {
+    return 1;
+  }
+
+#if HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG
+  for (auto &&alg : specified_algs) {
+    struct alg_info *info = nullptr;
+
+    for (unsigned int i = 0; i < countof(supported_algs); ++i) {
+      if (strcmp(alg.c_str(), supported_algs[i].name) == 0) {
+        info = &supported_algs[i];
+      }
+    }
+    if (info != nullptr) {
+      if (SSL_CTX_add_cert_compression_alg(ctx, info->number, 
info->compress_func, info->decompress_func) == 0) {
+        return 0;
+      }
+      Dbg(dbg_ctl_ssl_cert_compress, "Enabled %s", info->name);
+    } else {
+      Dbg(dbg_ctl_ssl_cert_compress, "Unrecognized algorithm: %s", 
alg.c_str());
+      return 0;
+    }
+  }
+  return 1;
+#elif HAVE_SSL_CTX_SET1_CERT_COMP_PREFERENCE
+  int algs[N_ALGORITHMS];
+  int n = 0;
+
+  for (unsigned int i = 0; i < specified_algs.size(); ++i) {
+    for (unsigned int j = 0; j < countof(supported_algs); ++j) {
+      if (strcmp(specified_algs[i].c_str(), supported_algs[j].name) == 0) {
+        algs[n++] = supported_algs[j].number;
+        Dbg(dbg_ctl_ssl_cert_compress, "Enabled %s", supported_algs[j].name);
+      }
+    }
+  }
+  return SSL_CTX_set1_cert_comp_preference(ctx, algs, n);
+#else
+  // If Certificate Compression is unsupported there's nothing to do.
+  // No need to raise an error since handshake would be done successfully 
without compression.
+  Dbg(dbg_ctl_ssl_cert_compress, "Certificate Compression is unsupported");
+  return 1;
+#endif
+}
diff --git a/src/iocore/net/TLSCertCompression.h 
b/src/iocore/net/TLSCertCompression.h
new file mode 100644
index 0000000000..891c2c5951
--- /dev/null
+++ b/src/iocore/net/TLSCertCompression.h
@@ -0,0 +1,37 @@
+/** @file
+
+  Functions for Certificate Compression
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include <openssl/ssl.h>
+#include <vector>
+#include <string>
+
+/**
+ * Common function to set certificate compression preference
+ *
+ * @param[in] ctx SSL_CTX
+ * @param[in] algs A vector that contains compression algorithm names ("zlib", 
"brotli", or "zstd")
+ * @return 1 on success
+ */
+int register_certificate_compression_preference(SSL_CTX *ctx, const 
std::vector<std::string> &algs);
diff --git a/src/iocore/net/TLSCertCompression_brotli.cc 
b/src/iocore/net/TLSCertCompression_brotli.cc
new file mode 100644
index 0000000000..b758e476e0
--- /dev/null
+++ b/src/iocore/net/TLSCertCompression_brotli.cc
@@ -0,0 +1,77 @@
+/** @file
+
+  Functions for brotli compression/decompression
+
+  @section license License
+
+  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.
+ */
+
+#include "TLSCertCompression_brotli.h"
+#include "SSLStats.h"
+#include <openssl/ssl.h>
+#include <brotli/decode.h>
+#include <brotli/encode.h>
+
+int
+compression_func_brotli(SSL * /* ssl */, CBB *out, const uint8_t *in, size_t 
in_len)
+{
+  // TODO Need a cache mechanism inside this function for better performance.
+
+  uint8_t      *buf;
+  unsigned long buf_len = BrotliEncoderMaxCompressedSize(in_len);
+
+  if (CBB_reserve(out, &buf, buf_len) != 1) {
+    Metrics::Counter::increment(ssl_rsb.cert_compress_brotli_failure);
+    return 0;
+  }
+
+  if (BrotliEncoderCompress(BROTLI_DEFAULT_QUALITY, BROTLI_DEFAULT_WINDOW, 
BROTLI_DEFAULT_MODE, in_len, in, &buf_len, buf) ==
+      BROTLI_TRUE) {
+    CBB_did_write(out, buf_len);
+    Metrics::Counter::increment(ssl_rsb.cert_compress_brotli);
+    return 1;
+  } else {
+    CBB_did_write(out, 0);
+    Metrics::Counter::increment(ssl_rsb.cert_compress_brotli_failure);
+    return 0;
+  }
+}
+
+int
+decompression_func_brotli(SSL * /* ssl */, CRYPTO_BUFFER **out, size_t 
uncompressed_len, const uint8_t *in, size_t in_len)
+{
+  uint8_t *buf;
+
+  *out = CRYPTO_BUFFER_alloc(&buf, uncompressed_len);
+  if (*out == nullptr) {
+    Metrics::Counter::increment(ssl_rsb.cert_decompress_brotli_failure);
+    return 0;
+  }
+
+  size_t dest_len = uncompressed_len;
+
+  if (BrotliDecoderDecompress(in_len, in, &dest_len, buf) != 
BROTLI_DECODER_RESULT_SUCCESS || dest_len != uncompressed_len) {
+    CRYPTO_BUFFER_free(*out);
+    *out = nullptr;
+    Metrics::Counter::increment(ssl_rsb.cert_decompress_brotli_failure);
+    return 0;
+  }
+
+  Metrics::Counter::increment(ssl_rsb.cert_decompress_brotli);
+  return 1;
+}
diff --git a/src/iocore/net/TLSCertCompression_brotli.h 
b/src/iocore/net/TLSCertCompression_brotli.h
new file mode 100644
index 0000000000..7026f4ff70
--- /dev/null
+++ b/src/iocore/net/TLSCertCompression_brotli.h
@@ -0,0 +1,30 @@
+/** @file
+
+  Functions for brotli compression/decompression
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include <openssl/ssl.h>
+#include <openssl/bytestring.h>
+
+int compression_func_brotli(SSL *ssl, CBB *out, const uint8_t *in, size_t 
in_len);
+int decompression_func_brotli(SSL *ssl, CRYPTO_BUFFER **out, size_t 
uncompressed_len, const uint8_t *in, size_t in_len);
diff --git a/src/iocore/net/TLSCertCompression_zlib.cc 
b/src/iocore/net/TLSCertCompression_zlib.cc
new file mode 100644
index 0000000000..5416dc6986
--- /dev/null
+++ b/src/iocore/net/TLSCertCompression_zlib.cc
@@ -0,0 +1,75 @@
+/** @file
+
+  Functions for zlib compression/decompression
+
+  @section license License
+
+  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.
+ */
+
+#include "TLSCertCompression_zlib.h"
+#include "SSLStats.h"
+#include <openssl/ssl.h>
+#include <zlib.h>
+
+int
+compression_func_zlib(SSL * /* ssl */, CBB *out, const uint8_t *in, size_t 
in_len)
+{
+  // TODO Need a cache mechanism inside this function for better performance.
+
+  uint8_t      *buf;
+  unsigned long buf_len = compressBound(in_len);
+
+  if (CBB_reserve(out, &buf, buf_len) != 1) {
+    Metrics::Counter::increment(ssl_rsb.cert_compress_zlib_failure);
+    return 0;
+  }
+
+  if (compress(buf, &buf_len, in, in_len) == Z_OK) {
+    CBB_did_write(out, buf_len);
+    Metrics::Counter::increment(ssl_rsb.cert_compress_zlib);
+    return 1;
+  } else {
+    CBB_did_write(out, 0);
+    Metrics::Counter::increment(ssl_rsb.cert_compress_zlib_failure);
+    return 0;
+  }
+}
+
+int
+decompression_func_zlib(SSL * /* ssl */, CRYPTO_BUFFER **out, size_t 
uncompressed_len, const uint8_t *in, size_t in_len)
+{
+  uint8_t *buf;
+
+  *out = CRYPTO_BUFFER_alloc(&buf, uncompressed_len);
+  if (*out == nullptr) {
+    Metrics::Counter::increment(ssl_rsb.cert_decompress_zlib_failure);
+    return 0;
+  }
+
+  unsigned long dest_len = uncompressed_len;
+
+  if (uncompress(buf, &dest_len, in, in_len) != Z_OK || dest_len != 
uncompressed_len) {
+    CRYPTO_BUFFER_free(*out);
+    *out = nullptr;
+    Metrics::Counter::increment(ssl_rsb.cert_decompress_zlib_failure);
+    return 0;
+  }
+
+  Metrics::Counter::increment(ssl_rsb.cert_decompress_zlib);
+  return 1;
+}
diff --git a/src/iocore/net/TLSCertCompression_zlib.h 
b/src/iocore/net/TLSCertCompression_zlib.h
new file mode 100644
index 0000000000..622f8efb5a
--- /dev/null
+++ b/src/iocore/net/TLSCertCompression_zlib.h
@@ -0,0 +1,30 @@
+/** @file
+
+  Functions for zlib compression/decompression
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include <openssl/ssl.h>
+#include <openssl/bytestring.h>
+
+int compression_func_zlib(SSL *ssl, CBB *out, const uint8_t *in, size_t 
in_len);
+int decompression_func_zlib(SSL *ssl, CRYPTO_BUFFER **out, size_t 
uncompressed_len, const uint8_t *in, size_t in_len);
diff --git a/src/iocore/net/TLSCertCompression_zstd.cc 
b/src/iocore/net/TLSCertCompression_zstd.cc
new file mode 100644
index 0000000000..ab63633d0f
--- /dev/null
+++ b/src/iocore/net/TLSCertCompression_zstd.cc
@@ -0,0 +1,82 @@
+/** @file
+
+  Functions for zstd compression/decompression
+
+  @section license License
+
+  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.
+ */
+
+#include "TLSCertCompression_zstd.h"
+#include "SSLStats.h"
+#include <openssl/ssl.h>
+#include <zstd.h>
+
+int
+compression_func_zstd(SSL * /* ssl */, CBB *out, const uint8_t *in, size_t 
in_len)
+{
+  // TODO Need a cache mechanism inside this function for better performance.
+
+  uint8_t      *buf;
+  unsigned long buf_len = ZSTD_compressBound(in_len);
+
+  if (ZSTD_isError(buf_len) == 1) {
+    Metrics::Counter::increment(ssl_rsb.cert_compress_zstd_failure);
+    return 0;
+  }
+
+  if (CBB_reserve(out, &buf, buf_len) != 1) {
+    Metrics::Counter::increment(ssl_rsb.cert_compress_zstd_failure);
+    return 0;
+  }
+
+  // For better performance ZSTD_compressCCtx, which reuses a context object, 
should be used.
+  // One context object need to be made for each thread.
+  size_t ret = ZSTD_compress(buf, buf_len, in, in_len, ZSTD_CLEVEL_DEFAULT);
+  if (ZSTD_isError(ret) == 1) {
+    Metrics::Counter::increment(ssl_rsb.cert_compress_zstd_failure);
+    return 0;
+  } else {
+    CBB_did_write(out, ret);
+    Metrics::Counter::increment(ssl_rsb.cert_compress_zstd);
+    return 1;
+  }
+}
+
+int
+decompression_func_zstd(SSL * /* ssl */, CRYPTO_BUFFER **out, size_t 
uncompressed_len, const uint8_t *in, size_t in_len)
+{
+  uint8_t *buf;
+
+  *out = CRYPTO_BUFFER_alloc(&buf, uncompressed_len);
+  if (*out == nullptr) {
+    Metrics::Counter::increment(ssl_rsb.cert_decompress_zstd_failure);
+    return 0;
+  }
+
+  size_t ret = ZSTD_decompress(buf, uncompressed_len, in, in_len);
+
+  if (ZSTD_isError(ret) || ret != uncompressed_len) {
+    CRYPTO_BUFFER_free(*out);
+    *out = nullptr;
+    Metrics::Counter::increment(ssl_rsb.cert_decompress_zstd_failure);
+    return 0;
+  }
+
+  Metrics::Counter::increment(ssl_rsb.cert_decompress_zstd);
+  return 1;
+}
diff --git a/src/iocore/net/TLSCertCompression_zstd.h 
b/src/iocore/net/TLSCertCompression_zstd.h
new file mode 100644
index 0000000000..bde6ef6d7b
--- /dev/null
+++ b/src/iocore/net/TLSCertCompression_zstd.h
@@ -0,0 +1,30 @@
+/** @file
+
+  Functions for zstd compression/decompression
+
+  @section license License
+
+  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.
+ */
+
+#pragma once
+
+#include <openssl/ssl.h>
+#include <openssl/bytestring.h>
+
+int compression_func_zstd(SSL *ssl, CBB *out, const uint8_t *in, size_t 
in_len);
+int decompression_func_zstd(SSL *ssl, CRYPTO_BUFFER **out, size_t 
uncompressed_len, const uint8_t *in, size_t in_len);
diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc
index 8c887a2e0f..7c8ab553d4 100644
--- a/src/records/RecordsConfig.cc
+++ b/src/records/RecordsConfig.cc
@@ -1245,6 +1245,10 @@ static constexpr RecordElement RecordsConfig[] =
   ,
   {RECT_CONFIG, "proxy.config.ssl.ktls.enabled", RECD_INT, "0", 
RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
   ,
+  {RECT_CONFIG, "proxy.config.ssl.server.cert_compression.algorithms", 
RECD_STRING, nullptr, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+  ,
+  {RECT_CONFIG, "proxy.config.ssl.client.cert_compression.algorithms", 
RECD_STRING, nullptr, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
+  ,
   
//##############################################################################
   //#
   //# OCSP (Online Certificate Status Protocol) Stapling Configuration
diff --git a/src/traffic_layout/info.cc b/src/traffic_layout/info.cc
index 39df7e303b..dfdbc6915a 100644
--- a/src/traffic_layout/info.cc
+++ b/src/traffic_layout/info.cc
@@ -100,6 +100,11 @@ produce_features(bool json)
 #else
   print_feature("TS_HAS_ZSTD", 0, json);
 #endif
+#if HAVE_SSL_CTX_ADD_CERT_COMPRESSION_ALG || 
HAVE_SSL_CTX_SET1_CERT_COMP_PREFERENCE
+  print_feature("TS_HAS_CERT_COMPRESSION", 1, json);
+#else
+  print_feature("TS_HAS_CERT_COMPRESSION", 0, json);
+#endif
 #ifdef F_GETPIPE_SZ
   print_feature("TS_HAS_PIPE_BUFFER_SIZE_CONFIG", 1, json);
 #else
diff --git a/tests/gold_tests/tls/replay/tls_cert_compression.replay.yaml 
b/tests/gold_tests/tls/replay/tls_cert_compression.replay.yaml
new file mode 100644
index 0000000000..bc05706ec2
--- /dev/null
+++ b/tests/gold_tests/tls/replay/tls_cert_compression.replay.yaml
@@ -0,0 +1,41 @@
+#  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.
+
+meta:
+  version: "1.0"
+
+sessions:
+- transactions:
+
+  - client-request:
+      method: GET
+      url: /cert-compression-test
+      version: '1.1'
+      headers:
+        fields:
+        - [Host, example.com]
+        - [uuid, cert-compression-request]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [Content-Length, "0"]
+        - [X-Response, cert-compression-response]
+
+    proxy-response:
+      status: 200
diff --git a/tests/gold_tests/tls/tls_cert_comp.test.py 
b/tests/gold_tests/tls/tls_cert_comp.test.py
new file mode 100644
index 0000000000..53d87c670b
--- /dev/null
+++ b/tests/gold_tests/tls/tls_cert_comp.test.py
@@ -0,0 +1,175 @@
+'''
+Verify TLS Certificate Compression (RFC 8879) between two ATS processes.
+'''
+#  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.
+
+Test.Summary = '''
+Verify TLS Certificate Compression (RFC 8879) works between two ATS
+instances. An edge ATS (client) connects via HTTPS to a mid ATS (server)
+with cert compression enabled. The test verifies compression and
+decompression succeed by checking the ssl cert compression metrics.
+'''
+
+Test.SkipUnless(Condition.HasATSFeature('TS_HAS_CERT_COMPRESSION'))
+
+REPLAY_FILE = 'replay/tls_cert_compression.replay.yaml'
+
+
+class TestCertCompression:
+    server_counter: int = 0
+    ts_counter: int = 0
+    client_counter: int = 0
+
+    def __init__(self, algorithm: str) -> None:
+        self._algorithm = algorithm
+        self._server = self._configure_server()
+        self._ts_mid = self._configure_ts_mid()
+        self._ts_edge = self._configure_ts_edge()
+
+    def _configure_server(self) -> 'Process':
+        name = f'server-{TestCertCompression.server_counter}'
+        TestCertCompression.server_counter += 1
+        server = Test.MakeVerifierServerProcess(name, REPLAY_FILE)
+        return server
+
+    def _configure_ts_mid(self) -> 'Process':
+        """Mid-tier ATS that terminates TLS and forwards to origin."""
+        name = f'm{TestCertCompression.ts_counter}'
+        TestCertCompression.ts_counter += 1
+        ts = Test.MakeATSProcess(name, enable_tls=True, enable_cache=False)
+
+        ts.addDefaultSSLFiles()
+        ts.Disk.ssl_multicert_yaml.AddLines(
+            """
+ssl_multicert:
+  - dest_ip: "*"
+    ssl_cert_name: server.pem
+    ssl_key_name: server.key
+""".split("\n"))
+
+        ts.Disk.remap_config.AddLine(f'map / 
http://127.0.0.1:{self._server.Variables.http_port}/')
+
+        ts.Disk.records_config.update(
+            {
+                'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir,
+                'proxy.config.ssl.server.private_key.path': 
ts.Variables.SSLDir,
+                'proxy.config.ssl.server.cert_compression.algorithms': 
self._algorithm,
+                'proxy.config.diags.debug.enabled': 1,
+                'proxy.config.diags.debug.tags': 'ssl_cert_compress',
+            })
+
+        return ts
+
+    def _configure_ts_edge(self) -> 'Process':
+        """Edge ATS that connects to mid-tier via HTTPS."""
+        name = f'e{TestCertCompression.ts_counter}'
+        TestCertCompression.ts_counter += 1
+        ts = Test.MakeATSProcess(name, enable_tls=True, enable_cache=False)
+
+        ts.addDefaultSSLFiles()
+        ts.Disk.ssl_multicert_yaml.AddLines(
+            """
+ssl_multicert:
+  - dest_ip: "*"
+    ssl_cert_name: server.pem
+    ssl_key_name: server.key
+""".split("\n"))
+
+        ts.Disk.remap_config.AddLine(f'map / 
https://127.0.0.1:{self._ts_mid.Variables.ssl_port}/')
+
+        ts.Disk.records_config.update(
+            {
+                'proxy.config.ssl.server.cert.path': ts.Variables.SSLDir,
+                'proxy.config.ssl.server.private_key.path': 
ts.Variables.SSLDir,
+                'proxy.config.ssl.client.verify.server.policy': 'PERMISSIVE',
+                'proxy.config.ssl.client.cert_compression.algorithms': 
self._algorithm,
+                'proxy.config.diags.debug.enabled': 1,
+                'proxy.config.diags.debug.tags': 'ssl_cert_compress',
+            })
+
+        return ts
+
+    def run(self) -> None:
+        # Test run 1: Send traffic through the proxy chain.
+        tr = Test.AddTestRun(f'Send request through edge->mid with 
{self._algorithm} cert compression')
+        tr.Processes.Default.StartBefore(self._server)
+        tr.Processes.Default.StartBefore(self._ts_mid)
+        tr.Processes.Default.StartBefore(self._ts_edge)
+
+        name = f'client-{TestCertCompression.client_counter}'
+        TestCertCompression.client_counter += 1
+        tr.AddVerifierClientProcess(name, REPLAY_FILE, 
http_ports=[self._ts_edge.Variables.port])
+
+        # Test run 2: Check compression metric on the mid-tier (server side).
+        tr = Test.AddTestRun(f'Verify {self._algorithm} compression metric on 
mid-tier')
+        tr.Processes.Default.Command = (f'traffic_ctl metric get'
+                                        f' 
proxy.process.ssl.cert_compress.{self._algorithm}')
+        tr.Processes.Default.Env = self._ts_mid.Env
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.All = Testers.ContainsExpression(
+            f'proxy.process.ssl.cert_compress.{self._algorithm} 1',
+            f'Certificate should have been compressed with {self._algorithm}')
+        tr.StillRunningAfter = self._ts_mid
+        tr.StillRunningAfter = self._ts_edge
+        tr.StillRunningAfter = self._server
+
+        # Test run 3: Check decompression metric on the edge (client side).
+        tr = Test.AddTestRun(f'Verify {self._algorithm} decompression metric 
on edge')
+        tr.Processes.Default.Command = (f'traffic_ctl metric get'
+                                        f' 
proxy.process.ssl.cert_decompress.{self._algorithm}')
+        tr.Processes.Default.Env = self._ts_edge.Env
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.All = Testers.ContainsExpression(
+            f'proxy.process.ssl.cert_decompress.{self._algorithm} 1',
+            f'Certificate should have been decompressed with 
{self._algorithm}')
+        tr.StillRunningAfter = self._ts_mid
+        tr.StillRunningAfter = self._ts_edge
+        tr.StillRunningAfter = self._server
+
+        # Test run 4: Verify no failures on either side.
+        tr = Test.AddTestRun(f'Verify no {self._algorithm} compression 
failures on mid-tier')
+        tr.Processes.Default.Command = (f'traffic_ctl metric get'
+                                        f' 
proxy.process.ssl.cert_compress.{self._algorithm}_failure')
+        tr.Processes.Default.Env = self._ts_mid.Env
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.All = Testers.ContainsExpression(
+            f'proxy.process.ssl.cert_compress.{self._algorithm}_failure 0',
+            f'There should be no {self._algorithm} compression failures')
+        tr.StillRunningAfter = self._ts_mid
+        tr.StillRunningAfter = self._ts_edge
+        tr.StillRunningAfter = self._server
+
+        tr = Test.AddTestRun(f'Verify no {self._algorithm} decompression 
failures on edge')
+        tr.Processes.Default.Command = (f'traffic_ctl metric get'
+                                        f' 
proxy.process.ssl.cert_decompress.{self._algorithm}_failure')
+        tr.Processes.Default.Env = self._ts_edge.Env
+        tr.Processes.Default.ReturnCode = 0
+        tr.Processes.Default.Streams.All = Testers.ContainsExpression(
+            f'proxy.process.ssl.cert_decompress.{self._algorithm}_failure 0',
+            f'There should be no {self._algorithm} decompression failures')
+        tr.StillRunningAfter = self._ts_mid
+        tr.StillRunningAfter = self._ts_edge
+        tr.StillRunningAfter = self._server
+
+
+algorithms = ['zlib']
+if Condition.HasATSFeature('TS_HAS_BROTLI'):
+    algorithms.append('brotli')
+if Condition.HasATSFeature('TS_HAS_ZSTD'):
+    algorithms.append('zstd')
+for algorithm in algorithms:
+    TestCertCompression(algorithm).run()

Reply via email to