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]

Reply via email to