Copilot commented on code in PR #13088: URL: https://github.com/apache/trafficserver/pull/13088#discussion_r3089478700
########## src/iocore/net/TLSCertCompression.cc: ########## @@ -0,0 +1,125 @@ +/** @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, 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); + } + } Review Comment: Unknown algorithm names in specified_algs are silently ignored (info remains nullptr and the function still returns success). This can leave cert compression disabled even though the config is set, with no operator-visible indication. Consider treating unknown values as a config error (return 0 / log a clear error) or at least emitting a warning when a token doesn't match any supported algorithm. ########## tests/gold_tests/tls/tls_cert_comp.test.py: ########## @@ -0,0 +1,169 @@ +''' +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 + + +TestCertCompression('zlib').run() Review Comment: The gold test only runs the zlib path, but this PR also adds brotli and zstd support. This leaves the new brotli/zstd compression/decompression implementations and their metrics untested when those libraries are available. Consider running TestCertCompression for each algorithm gated by the corresponding ATS feature flags (e.g., TS_HAS_BROTLI / TS_HAS_ZSTD) so all supported algorithms get coverage on capable builds. ```suggestion 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() ``` ########## src/iocore/net/SSLUtils.cc: ########## @@ -435,6 +436,36 @@ 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) { + std::string_view algs_sv = this->_params->server_cert_compression_algorithms; + size_t pos = 0; + + while (pos < algs_sv.size()) { + auto comma = algs_sv.find(',', pos); + if (comma == std::string_view::npos) { + comma = algs_sv.size(); + } + auto alg = algs_sv.substr(pos, comma - pos); + if (!alg.empty()) { + algs.emplace_back(alg); + } + pos = comma + 1; Review Comment: The algorithm list parsing doesn't trim whitespace around comma-separated values. A config like "zlib, brotli" will produce " brotli" and fail to match the supported algorithm names. Trim leading/trailing ASCII whitespace for each token before adding it to the algs vector (and consider sharing this parsing helper with the identical logic in SSLClientUtils.cc to avoid drift). ########## 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 + Review Comment: PR description lists a metric prefix "roxy.process..." (missing the leading 'p'), but the implementation and documentation use "proxy.process...". Please correct the PR description (or any external references) to avoid confusion about the actual metric names. ########## 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_zlib_failure); Review Comment: In the CBB_reserve failure path, the code increments the zlib failure counter, but this is the brotli compressor. This misreports failures and makes the brotli_failure metric inaccurate. Increment the brotli failure metric here instead. ```suggestion Metrics::Counter::increment(ssl_rsb.cert_compress_brotli_failure); ``` ########## src/iocore/net/SSLClientUtils.cc: ########## @@ -247,6 +248,25 @@ SSLInitClientContext(const SSLConfigParams *params) } #endif + if (params->client_cert_compression_algorithms) { + std::vector<std::string> algs; + std::string_view algs_sv = params->client_cert_compression_algorithms; + size_t pos = 0; + + while (pos < algs_sv.size()) { + auto comma = algs_sv.find(',', pos); + if (comma == std::string_view::npos) { + comma = algs_sv.size(); + } + auto alg = algs_sv.substr(pos, comma - pos); + if (!alg.empty()) { + algs.emplace_back(alg); + } + pos = comma + 1; + } + register_certificate_compression_preference(client_ctx, algs); Review Comment: The return value from register_certificate_compression_preference() is ignored. If the SSL library rejects the preference list (or the config list is invalid/too long), this will silently disable compression and make debugging difficult. Check the return value and either log an error (at minimum) or fail client context initialization consistently with other SSL config validation in this function. ```suggestion if (!register_certificate_compression_preference(client_ctx, algs)) { SSLError("invalid client certificate compression algorithm list in %s", ts::filename::RECORDS); goto fail; } ``` ########## doc/admin-guide/files/records.yaml.en.rst: ########## @@ -4268,6 +4268,39 @@ 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``. Review Comment: The client-side record description does not mention that an empty value disables certificate compression (which the server-side record does). For consistency and to avoid confusion, document the default/disable behavior here as well (and, if applicable, whether order affects client advertisement). ```suggestion Supported values: ``zlib``, ``brotli``, ``zstd``. An empty value (the default) disables certificate compression. ``` ########## 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_zlib_failure); Review Comment: In the CBB_reserve failure path, the code increments the zlib failure counter, but this is the zstd compressor. This will skew failure metrics for both zstd and zlib. Increment the zstd failure metric here instead. ```suggestion Metrics::Counter::increment(ssl_rsb.cert_compress_zstd_failure); ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
