Skip to content

Commit 2380347

Browse files
committed
tls: Add certificate compression algorithm configuration (RFC 8879)
This change adds TLS certificate compression support with brotli, zstd, and zlib algorithms per RFC 8879. Certificate compression reduces TLS handshake size, especially beneficial for QUIC where the ServerHello needs to fit in the initial response. Features: - Support for brotli (ID 2), zstd (ID 3), and zlib (ID 1) compression algorithms - Individual registration functions for each algorithm - Compression stats: certificate_compression.<algo>.{compressed,total_uncompressed_bytes,total_compressed_bytes} - Runtime feature flag: envoy.reloadable_features.tls_support_certificate_compression (default: false) Testing: - Unit tests for compression/decompression round-trips - Registration tests with real SSL_CTX - Integration tests for TLS handshake with compression enabled/disabled Documentation: - Changelog entry for the new feature - Stats documentation in ssl_stats.rst Part of certificate compression implementation (RFC 8879).
1 parent bcc8ef6 commit 2380347

File tree

17 files changed

+244
-130
lines changed

17 files changed

+244
-130
lines changed

changelogs/current.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,16 @@ new_features:
440440
``verify cert failed: cert hash and spki``, or the OpenSSL verification error string (e.g., certificate
441441
has expired, unable to get local issuer certificate). This provides better visibility into TLS handshake
442442
failures without requiring debug-level logging.
443+
- area: tls
444+
change: |
445+
Added TLS certificate compression support (RFC 8879) with brotli, zstd, and zlib algorithms.
446+
Compression reduces TLS handshake size, especially beneficial for QUIC where the ServerHello
447+
needs to fit in the initial response. Controlled by runtime flag
448+
``envoy.reloadable_features.tls_support_certificate_compression`` (defaults to ``false``).
449+
New stats: ``ssl.certificate_compression.<algo>.compressed``,
450+
``ssl.certificate_compression.<algo>.total_uncompressed_bytes``,
451+
``ssl.certificate_compression.<algo>.total_compressed_bytes`` where ``<algo>`` is one of
452+
brotli, zstd, or zlib.
443453
- area: ext_proc
444454
change: |
445455
Added support for forwarding cluster metadata to ext_proc server.

docs/root/_include/ssl_stats.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@
1919
sigalgs.<sigalg>, Counter, Total successful TLS connections that used signature algorithm <sigalg>
2020
versions.<version>, Counter, Total successful TLS connections that used protocol version <version>
2121
was_key_usage_invalid, Counter, Total successful TLS connections that used an `invalid keyUsage extension <https://github.com/google/boringssl/blob/6f13380d27835e70ec7caf807da7a1f239b10da6/ssl/internal.h#L3117>`_. (This is not available in BoringSSL FIPS yet due to `issue #28246 <https://github.com/envoyproxy/envoy/issues/28246>`_)
22+
certificate_compression.<algo>.compressed, Counter, Total certificates compressed using algorithm <algo> (brotli/zstd/zlib). Requires runtime flag ``envoy.reloadable_features.tls_support_certificate_compression``.
23+
certificate_compression.<algo>.total_uncompressed_bytes, Counter, Total bytes of certificates before compression using algorithm <algo>
24+
certificate_compression.<algo>.total_compressed_bytes, Counter, Total bytes of certificates after compression using algorithm <algo>

source/common/quic/cert_compression.h

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include "source/common/runtime/runtime_features.h"
34
#include "source/common/tls/cert_compression.h"
45

56
namespace Envoy {
@@ -12,11 +13,20 @@ namespace Quic {
1213
*/
1314
class CertCompression : protected Logger::Loggable<Logger::Id::quic> {
1415
public:
15-
// Registers all compression and decompression functions on `ssl_ctx`.
16-
// This is a wrapper that calls the TLS implementation.
17-
// Registers brotli, zstd, and zlib in priority order (RFC 8879).
16+
// Registers compression and decompression functions on `ssl_ctx`.
17+
// When runtime feature is enabled: registers all algorithms (brotli, zstd, zlib)
18+
// When runtime feature is disabled: registers zlib only (backward compat)
1819
static void registerSslContext(SSL_CTX* ssl_ctx) {
19-
Extensions::TransportSockets::Tls::CertCompression::registerAll(ssl_ctx);
20+
if (Runtime::runtimeFeatureEnabled(
21+
"envoy.reloadable_features.tls_support_certificate_compression")) {
22+
// Priority: brotli > zstd > zlib (brotli generally provides best compression for certs)
23+
Extensions::TransportSockets::Tls::CertCompression::registerBrotli(ssl_ctx);
24+
Extensions::TransportSockets::Tls::CertCompression::registerZstd(ssl_ctx);
25+
Extensions::TransportSockets::Tls::CertCompression::registerZlib(ssl_ctx);
26+
} else {
27+
// Backward compatibility: register zlib only
28+
Extensions::TransportSockets::Tls::CertCompression::registerZlib(ssl_ctx);
29+
}
2030
}
2131

2232
// Callbacks for `SSL_CTX_add_cert_compression_alg`.

source/common/runtime/runtime_features.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ FALSE_RUNTIME_GUARD(envoy_reloadable_features_always_use_v6);
117117
FALSE_RUNTIME_GUARD(envoy_restart_features_upstream_http_filters_with_tcp_proxy);
118118
// TODO(danzh) false deprecate it once QUICHE has its own enable/disable flag.
119119
FALSE_RUNTIME_GUARD(envoy_reloadable_features_quic_reject_all);
120+
// TODO: flip to true after sufficient testing of TLS certificate compression.
121+
FALSE_RUNTIME_GUARD(envoy_reloadable_features_tls_support_certificate_compression);
120122
// TODO(#10646) change to true when UHV is sufficiently tested
121123
// For more information about Universal Header Validation, please see
122124
// https://github.com/envoyproxy/envoy/issues/10646

source/common/tls/BUILD

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ envoy_cc_library(
183183
visibility = ["//visibility:public"],
184184
deps = [
185185
":cert_compression_lib",
186+
":ssl_ctx_stats_provider_lib",
186187
":stats_lib",
187188
":utility_lib",
188189
"//envoy/ssl:context_config_interface",
@@ -261,12 +262,22 @@ envoy_cc_library(
261262
],
262263
)
263264

265+
envoy_cc_library(
266+
name = "ssl_ctx_stats_provider_lib",
267+
hdrs = ["ssl_ctx_stats_provider.h"],
268+
deps = [
269+
":stats_lib",
270+
],
271+
)
272+
264273
envoy_cc_library(
265274
name = "cert_compression_lib",
266275
srcs = ["cert_compression.cc"],
267276
hdrs = ["cert_compression.h"],
268277
external_deps = ["ssl"],
269278
deps = [
279+
":ssl_ctx_stats_provider_lib",
280+
"//envoy/ssl:context_config_interface",
270281
"//source/common/common:assert_lib",
271282
"//source/common/common:logger_lib",
272283
"@org_brotli//:brotlidec",

source/common/tls/cert_compression.cc

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#include "source/common/tls/cert_compression.h"
22

33
#include "source/common/common/assert.h"
4+
#include "source/common/tls/ssl_ctx_stats_provider.h"
5+
#include "source/common/tls/stats.h"
46

57
#include "brotli/decode.h"
68
#include "brotli/encode.h"
@@ -29,17 +31,25 @@ class ScopedZStream {
2931
CleanupFunc cleanup_;
3032
};
3133

32-
} // namespace
33-
34-
void CertCompression::registerAll(SSL_CTX* ssl_ctx) {
35-
// Register all algorithms in priority order.
36-
// The TLS handshake will negotiate the best mutually supported algorithm.
37-
// Priority: brotli > zstd > zlib (brotli generally provides best compression for certs)
38-
registerBrotli(ssl_ctx);
39-
registerZstd(ssl_ctx);
40-
registerZlib(ssl_ctx);
34+
// Record certificate compression stats per algorithm for monitoring compression effectiveness.
35+
void recordCompressedCertSize(SSL* ssl, size_t uncompressed_size, size_t compressed_size,
36+
const std::string& algo) {
37+
SSL_CTX* ssl_ctx = SSL_get_SSL_CTX(ssl);
38+
if (ssl_ctx == nullptr) {
39+
return;
40+
}
41+
auto* provider = static_cast<SslCtxStatsProvider*>(SSL_CTX_get_app_data(ssl_ctx));
42+
if (provider != nullptr) {
43+
const std::string prefix = "ssl.certificate_compression." + algo + ".";
44+
CertCompressionStats stats = generateCertCompressionStats(provider->statsScope(), prefix);
45+
stats.compressed_.inc();
46+
stats.total_uncompressed_bytes_.add(uncompressed_size);
47+
stats.total_compressed_bytes_.add(compressed_size);
48+
}
4149
}
4250

51+
} // namespace
52+
4353
void CertCompression::registerBrotli(SSL_CTX* ssl_ctx) {
4454
auto ret = SSL_CTX_add_cert_compression_alg(ssl_ctx, TLSEXT_cert_compression_brotli,
4555
compressBrotli, decompressBrotli);
@@ -59,7 +69,7 @@ void CertCompression::registerZlib(SSL_CTX* ssl_ctx) {
5969
}
6070

6171
// Brotli compression implementation
62-
int CertCompression::compressBrotli(SSL*, CBB* out, const uint8_t* in, size_t in_len) {
72+
int CertCompression::compressBrotli(SSL* ssl, CBB* out, const uint8_t* in, size_t in_len) {
6373
size_t encoded_size = BrotliEncoderMaxCompressedSize(in_len);
6474
if (encoded_size == 0) {
6575
IS_ENVOY_BUG("BrotliEncoderMaxCompressedSize returned 0");
@@ -84,6 +94,7 @@ int CertCompression::compressBrotli(SSL*, CBB* out, const uint8_t* in, size_t in
8494
return FAILURE;
8595
}
8696

97+
recordCompressedCertSize(ssl, in_len, encoded_size, "brotli");
8798
ENVOY_LOG(trace, "Cert brotli compression successful");
8899
return SUCCESS;
89100
}
@@ -124,7 +135,7 @@ int CertCompression::decompressBrotli(SSL*, CRYPTO_BUFFER** out, size_t uncompre
124135
}
125136

126137
// Zstd compression implementation
127-
int CertCompression::compressZstd(SSL*, CBB* out, const uint8_t* in, size_t in_len) {
138+
int CertCompression::compressZstd(SSL* ssl, CBB* out, const uint8_t* in, size_t in_len) {
128139
size_t const max_size = ZSTD_compressBound(in_len);
129140
if (max_size == 0) {
130141
IS_ENVOY_BUG("ZSTD_compressBound returned 0");
@@ -149,6 +160,7 @@ int CertCompression::compressZstd(SSL*, CBB* out, const uint8_t* in, size_t in_l
149160
return FAILURE;
150161
}
151162

163+
recordCompressedCertSize(ssl, in_len, compressed_size, "zstd");
152164
ENVOY_LOG(trace, "Cert zstd compression successful");
153165
return SUCCESS;
154166
}
@@ -186,7 +198,7 @@ int CertCompression::decompressZstd(SSL*, CRYPTO_BUFFER** out, size_t uncompress
186198
}
187199

188200
// Zlib compression implementation
189-
int CertCompression::compressZlib(SSL*, CBB* out, const uint8_t* in, size_t in_len) {
201+
int CertCompression::compressZlib(SSL* ssl, CBB* out, const uint8_t* in, size_t in_len) {
190202
z_stream z = {};
191203
// The deflateInit macro from zlib.h contains an old-style cast, so we need to suppress the
192204
// warning for this call.
@@ -228,6 +240,7 @@ int CertCompression::compressZlib(SSL*, CBB* out, const uint8_t* in, size_t in_l
228240
return FAILURE;
229241
}
230242

243+
recordCompressedCertSize(ssl, in_len, z.total_out, "zlib");
231244
ENVOY_LOG(trace, "Cert zlib compression successful");
232245
return SUCCESS;
233246
}

source/common/tls/cert_compression.h

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ namespace Tls {
3232
*/
3333
class CertCompression : protected Logger::Loggable<Logger::Id::connection> {
3434
public:
35-
// Register all supported compression algorithms in priority order.
36-
// Priority: brotli > zstd > zlib (brotli generally provides best compression for certs)
37-
static void registerAll(SSL_CTX* ssl_ctx);
38-
3935
// Individual registration functions for each algorithm.
4036
static void registerBrotli(SSL_CTX* ssl_ctx);
4137
static void registerZstd(SSL_CTX* ssl_ctx);

source/common/tls/context_impl.cc

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,15 @@ ContextImpl::ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& c
164164
}
165165
}
166166

167-
// Register certificate compression algorithms to reduce TLS handshake size.
168-
// This registers brotli, zstd, and zlib in priority order (RFC 8879).
169-
CertCompression::registerAll(ctx.ssl_ctx_.get());
167+
// Register certificate compression algorithms to reduce TLS handshake size (RFC 8879).
168+
// Only register if the runtime feature flag is enabled (default: disabled).
169+
if (Runtime::runtimeFeatureEnabled(
170+
"envoy.reloadable_features.tls_support_certificate_compression")) {
171+
// Priority: brotli > zstd > zlib (brotli generally provides best compression for certs)
172+
CertCompression::registerBrotli(ctx.ssl_ctx_.get());
173+
CertCompression::registerZstd(ctx.ssl_ctx_.get());
174+
CertCompression::registerZlib(ctx.ssl_ctx_.get());
175+
}
170176
}
171177

172178
auto verify_mode_or_error = cert_validator_->initializeSslContexts(
@@ -574,16 +580,6 @@ void ContextImpl::logHandshake(SSL* ssl) const {
574580
stats_.was_key_usage_invalid_.inc();
575581
}
576582

577-
// Record certificate chain sizes (DER-encoded bytes) for TLS overhead visibility.
578-
const size_t peer_cert_size = Utility::getPeerCertificateChainDerSize(ssl);
579-
if (peer_cert_size > 0) {
580-
stats_.peer_certificate_chain_bytes_.recordValue(peer_cert_size);
581-
}
582-
583-
const size_t local_cert_size = Utility::getLocalCertificateChainDerSize(ssl);
584-
if (local_cert_size > 0) {
585-
stats_.local_certificate_chain_bytes_.recordValue(local_cert_size);
586-
}
587583
}
588584

589585
std::vector<Ssl::PrivateKeyMethodProviderSharedPtr> ContextImpl::getPrivateKeyMethodProviders() {

source/common/tls/context_impl.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "source/common/stats/symbol_table.h"
2222
#include "source/common/tls/cert_validator/cert_validator.h"
2323
#include "source/common/tls/context_manager_impl.h"
24+
#include "source/common/tls/ssl_ctx_stats_provider.h"
2425
#include "source/common/tls/stats.h"
2526

2627
#include "absl/synchronization/mutex.h"
@@ -80,6 +81,7 @@ namespace TransportSockets {
8081
namespace Tls {
8182

8283
class ContextImpl : public virtual Envoy::Ssl::Context,
84+
public SslCtxStatsProvider,
8385
protected Logger::Loggable<Logger::Id::config> {
8486
public:
8587
virtual absl::StatusOr<bssl::UniquePtr<SSL>>
@@ -92,7 +94,8 @@ class ContextImpl : public virtual Envoy::Ssl::Context,
9294
*/
9395
void logHandshake(SSL* ssl) const;
9496

95-
SslStats& stats() { return stats_; }
97+
SslStats& stats() override { return stats_; }
98+
Stats::Scope& statsScope() override { return scope_; }
9699

97100
/**
98101
* The global SSL-library index used for storing a pointer to the SslExtendedSocketInfo
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#pragma once
2+
3+
#include "envoy/stats/scope.h"
4+
5+
#include "source/common/tls/stats.h"
6+
7+
namespace Envoy {
8+
namespace Extensions {
9+
namespace TransportSockets {
10+
namespace Tls {
11+
12+
// Interface for providing SSL stats from SSL_CTX app_data.
13+
// This avoids circular dependency between cert_compression_lib and context_lib.
14+
class SslCtxStatsProvider {
15+
public:
16+
virtual ~SslCtxStatsProvider() = default;
17+
virtual SslStats& stats() = 0;
18+
virtual Stats::Scope& statsScope() = 0;
19+
};
20+
21+
} // namespace Tls
22+
} // namespace TransportSockets
23+
} // namespace Extensions
24+
} // namespace Envoy

0 commit comments

Comments
 (0)