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()