diff --git a/CMakeLists.txt b/CMakeLists.txt index 923651059f5..49831bfebf9 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 8a2c63b4a80..bf12a0ab7ed 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 9849d6e5de9..4162163b049 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -4268,6 +4268,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 `_) 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 `_) 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 efef309c222..c3dc1bb7994 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 d0f68469ce9..c12594cb0f6 100644 --- a/include/iocore/net/SSLMultiCertConfigLoader.h +++ b/include/iocore/net/SSLMultiCertConfigLoader.h @@ -109,6 +109,7 @@ class SSLMultiCertConfigLoader 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 bf012cefecd..73c8b860fb9 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 16563e10ab1..c5b66d8468c 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 0d6ee6b14e9..6a6d5a24e43 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 fe08b43a4f5..453b971dd79 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 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 c6b5f422322..4480acef518 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(ats_free_null(ssl_ocsp_response_path_only)); - server_tls13_cipher_suites = static_cast(ats_free_null(server_tls13_cipher_suites)); - client_tls13_cipher_suites = static_cast(ats_free_null(client_tls13_cipher_suites)); - server_groups_list = static_cast(ats_free_null(server_groups_list)); - client_groups_list = static_cast(ats_free_null(client_groups_list)); - keylog_file = static_cast(ats_free_null(keylog_file)); + server_cert_compression_algorithms = static_cast(ats_free_null(server_cert_compression_algorithms)); + client_cert_compression_algorithms = static_cast(ats_free_null(client_cert_compression_algorithms)); + server_tls13_cipher_suites = static_cast(ats_free_null(server_tls13_cipher_suites)); + client_tls13_cipher_suites = static_cast(ats_free_null(client_tls13_cipher_suites)); + server_groups_list = static_cast(ats_free_null(server_groups_list)); + client_groups_list = static_cast(ats_free_null(client_groups_list)); + keylog_file = static_cast(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 e6bfdcbdf73..60e3b41d77e 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 152abf3acb6..50ae6f89dee 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 ee2231615a1..6f230334e28 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 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 00000000000..c95c78fa963 --- /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 + +#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, std::vector 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 00000000000..80aa6961c94 --- /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 +#include +#include + +/** + * Common function to set certificate compression preference + * + * @param ctx SSL_CTX + * @param algs A vector that contains compression algorithm names ("zlib", "brotli", or "zstd") + * @return 1 on success + */ +int register_certificate_compression_preference(SSL_CTX *ctx, std::vector algs); diff --git a/src/iocore/net/TLSCertCompression_brotli.cc b/src/iocore/net/TLSCertCompression_brotli.cc new file mode 100644 index 00000000000..b758e476e0b --- /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 +#include +#include + +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 00000000000..7026f4ff70f --- /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 +#include + +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 00000000000..5416dc69869 --- /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 +#include + +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 00000000000..622f8efb5a4 --- /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 +#include + +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 00000000000..ab63633d0f1 --- /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 +#include + +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 00000000000..bde6ef6d7b3 --- /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 +#include + +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 8c887a2e0f3..7c8ab553d43 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 39df7e303bc..dfdbc6915a0 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 00000000000..bc05706ec25 --- /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 00000000000..53d87c670b1 --- /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()