diff --git a/changelogs/current.yaml b/changelogs/current.yaml index fcf4a1c034e4..d46e53b6071e 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -230,6 +230,11 @@ new_features: Added socket ``type`` field for specifying a socket type to apply the socket option to under :ref:`SocketOption `. If not specified, the socket option will be applied to all socket types. +- area: quic + change: | + QUIC server and client support certificate compression, which can in some cases reduce the number of round trips + required to setup a connection. This change temporarily disabled by setting the runtime flag + ``envoy.reloadable_features.quic_support_certificate_compression`` to ``false``. - area: tls change: | Added an extension point :ref:`custom_tls_certificate_selector diff --git a/source/common/quic/BUILD b/source/common/quic/BUILD index 884ce2714010..8a91e57052e4 100644 --- a/source/common/quic/BUILD +++ b/source/common/quic/BUILD @@ -116,7 +116,9 @@ envoy_cc_library( name = "envoy_quic_proof_source_lib", srcs = ["envoy_quic_proof_source.cc"], hdrs = ["envoy_quic_proof_source.h"], - external_deps = ["ssl"], + external_deps = [ + "ssl", + ], tags = ["nofips"], deps = [ ":envoy_quic_proof_source_base_lib", @@ -124,6 +126,7 @@ envoy_cc_library( ":quic_io_handle_wrapper_lib", ":quic_transport_socket_factory_lib", "//envoy/ssl:tls_certificate_config_interface", + "//source/common/quic:cert_compression_lib", "//source/common/quic:quic_server_transport_socket_factory_lib", "//source/common/stream_info:stream_info_lib", "//source/server:listener_stats", @@ -500,6 +503,7 @@ envoy_cc_library( "//envoy/ssl:context_config_interface", "//source/common/common:assert_lib", "//source/common/network:transport_socket_options_lib", + "//source/common/quic:cert_compression_lib", "//source/common/tls:client_ssl_socket_lib", "//source/common/tls:context_config_lib", "@com_github_google_quiche//:quic_core_crypto_crypto_handshake_lib", @@ -714,3 +718,18 @@ envoy_cc_library( "@com_github_google_quiche//:quic_core_types_lib", ], ) + +envoy_cc_library( + name = "cert_compression_lib", + srcs = ["cert_compression.cc"], + hdrs = ["cert_compression.h"], + external_deps = [ + "ssl", + "zlib", + ], + deps = [ + "//source/common/common:assert_lib", + "//source/common/common:logger_lib", + "//source/common/runtime:runtime_lib", + ], +) diff --git a/source/common/quic/cert_compression.cc b/source/common/quic/cert_compression.cc new file mode 100644 index 000000000000..90acde1e2694 --- /dev/null +++ b/source/common/quic/cert_compression.cc @@ -0,0 +1,122 @@ +#include "source/common/quic/cert_compression.h" + +#include "source/common/common/assert.h" +#include "source/common/runtime/runtime_features.h" + +#include "openssl/tls1.h" + +#define ZLIB_CONST +#include "zlib.h" + +namespace Envoy { +namespace Quic { + +namespace { + +class ScopedZStream { +public: + using CleanupFunc = int (*)(z_stream*); + + ScopedZStream(z_stream& z, CleanupFunc cleanup) : z_(z), cleanup_(cleanup) {} + ~ScopedZStream() { cleanup_(&z_); } + +private: + z_stream& z_; + CleanupFunc cleanup_; +}; + +} // namespace + +void CertCompression::registerSslContext(SSL_CTX* ssl_ctx) { + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.quic_support_certificate_compression")) { + auto ret = SSL_CTX_add_cert_compression_alg(ssl_ctx, TLSEXT_cert_compression_zlib, compressZlib, + decompressZlib); + ASSERT(ret == 1); + } +} + +int CertCompression::compressZlib(SSL*, CBB* out, const uint8_t* in, size_t in_len) { + + z_stream z = {}; + int rv = deflateInit(&z, Z_DEFAULT_COMPRESSION); + if (rv != Z_OK) { + IS_ENVOY_BUG(fmt::format("Cert compression failure in deflateInit: {}", rv)); + return FAILURE; + } + + ScopedZStream deleter(z, deflateEnd); + + const auto upper_bound = deflateBound(&z, in_len); + + uint8_t* out_buf = nullptr; + if (!CBB_reserve(out, &out_buf, upper_bound)) { + IS_ENVOY_BUG(fmt::format("Cert compression failure in allocating output CBB buffer of size {}", + upper_bound)); + return FAILURE; + } + + z.next_in = in; + z.avail_in = in_len; + z.next_out = out_buf; + z.avail_out = upper_bound; + + rv = deflate(&z, Z_FINISH); + if (rv != Z_STREAM_END) { + IS_ENVOY_BUG(fmt::format( + "Cert compression failure in deflate: {}, z.total_out {}, in_len {}, z.avail_in {}", rv, + z.avail_in, in_len, z.avail_in)); + return FAILURE; + } + + if (!CBB_did_write(out, z.total_out)) { + IS_ENVOY_BUG("CBB_did_write failed"); + return FAILURE; + } + + ENVOY_LOG(trace, "Cert compression successful"); + + return SUCCESS; +} + +int CertCompression::decompressZlib(SSL*, CRYPTO_BUFFER** out, size_t uncompressed_len, + const uint8_t* in, size_t in_len) { + z_stream z = {}; + int rv = inflateInit(&z); + if (rv != Z_OK) { + IS_ENVOY_BUG(fmt::format("Cert decompression failure in inflateInit: {}", rv)); + return FAILURE; + } + + ScopedZStream deleter(z, inflateEnd); + + z.next_in = in; + z.avail_in = in_len; + bssl::UniquePtr decompressed_data( + CRYPTO_BUFFER_alloc(&z.next_out, uncompressed_len)); + z.avail_out = uncompressed_len; + + rv = inflate(&z, Z_FINISH); + if (rv != Z_STREAM_END) { + ENVOY_LOG_PERIODIC(error, std::chrono::seconds(10), + "Cert decompression failure in inflate, possibly caused by invalid " + "compressed cert from peer: {}, z.total_out {}, uncompressed_len {}", + rv, z.total_out, uncompressed_len); + return FAILURE; + } + + if (z.total_out != uncompressed_len) { + ENVOY_LOG_PERIODIC(error, std::chrono::seconds(10), + "Decompression length did not match peer provided uncompressed length, " + "caused by either invalid peer handshake data or decompression error."); + return FAILURE; + } + + ENVOY_LOG(trace, "Cert decompression successful"); + + *out = decompressed_data.release(); + return SUCCESS; +} + +} // namespace Quic +} // namespace Envoy diff --git a/source/common/quic/cert_compression.h b/source/common/quic/cert_compression.h new file mode 100644 index 000000000000..65be63cb8fde --- /dev/null +++ b/source/common/quic/cert_compression.h @@ -0,0 +1,31 @@ +#pragma once + +#include "source/common/common/logger.h" + +#include "openssl/ssl.h" + +namespace Envoy { +namespace Quic { + +/** + * Support for certificate compression and decompression in QUIC TLS handshakes. This often + * needed for the ServerHello to fit in the initial response and not need an additional round trip + * between client and server. + */ +class CertCompression : protected Logger::Loggable { +public: + // Registers compression and decompression functions on `ssl_ctx` if enabled. + static void registerSslContext(SSL_CTX* ssl_ctx); + + // Callbacks for `SSL_CTX_add_cert_compression_alg`. + static int compressZlib(SSL* ssl, CBB* out, const uint8_t* in, size_t in_len); + static int decompressZlib(SSL*, CRYPTO_BUFFER** out, size_t uncompressed_len, const uint8_t* in, + size_t in_len); + + // Defined return values for callbacks from `SSL_CTX_add_cert_compression_alg`. + static constexpr int SUCCESS = 1; + static constexpr int FAILURE = 0; +}; + +} // namespace Quic +} // namespace Envoy diff --git a/source/common/quic/envoy_quic_proof_source.cc b/source/common/quic/envoy_quic_proof_source.cc index e5457e953ad9..4d696753e013 100644 --- a/source/common/quic/envoy_quic_proof_source.cc +++ b/source/common/quic/envoy_quic_proof_source.cc @@ -4,6 +4,7 @@ #include "envoy/ssl/tls_certificate_config.h" +#include "source/common/quic/cert_compression.h" #include "source/common/quic/envoy_quic_utils.h" #include "source/common/quic/quic_io_handle_wrapper.h" #include "source/common/runtime/runtime_features.h" @@ -211,5 +212,9 @@ void EnvoyQuicProofSource::updateFilterChainManager( filter_chain_manager_ = &filter_chain_manager; } +void EnvoyQuicProofSource::OnNewSslCtx(SSL_CTX* ssl_ctx) { + CertCompression::registerSslContext(ssl_ctx); +} + } // namespace Quic } // namespace Envoy diff --git a/source/common/quic/envoy_quic_proof_source.h b/source/common/quic/envoy_quic_proof_source.h index e950b982445b..1d8bf461f948 100644 --- a/source/common/quic/envoy_quic_proof_source.h +++ b/source/common/quic/envoy_quic_proof_source.h @@ -19,6 +19,7 @@ class EnvoyQuicProofSource : public EnvoyQuicProofSourceBase { ~EnvoyQuicProofSource() override = default; // quic::ProofSource + void OnNewSslCtx(SSL_CTX* ssl_ctx) override; quiche::QuicheReferenceCountedPointer GetCertChain(const quic::QuicSocketAddress& server_address, const quic::QuicSocketAddress& client_address, const std::string& hostname, diff --git a/source/common/quic/quic_client_transport_socket_factory.cc b/source/common/quic/quic_client_transport_socket_factory.cc index e7b2febc8f75..0ec580f830b0 100644 --- a/source/common/quic/quic_client_transport_socket_factory.cc +++ b/source/common/quic/quic_client_transport_socket_factory.cc @@ -4,6 +4,7 @@ #include "envoy/extensions/transport_sockets/quic/v3/quic_transport.pb.validate.h" +#include "source/common/quic/cert_compression.h" #include "source/common/quic/envoy_quic_proof_verifier.h" #include "source/common/runtime/runtime_features.h" #include "source/common/tls/context_config_impl.h" @@ -86,6 +87,8 @@ std::shared_ptr QuicClientTransportSocketFactory:: tls_config.crypto_config_ = std::make_shared( std::make_unique(std::move(context), accept_untrusted), std::make_unique()); + + CertCompression::registerSslContext(tls_config.crypto_config_->ssl_ctx()); } // Return the latest crypto config. return tls_config.crypto_config_; diff --git a/source/common/runtime/runtime_features.cc b/source/common/runtime/runtime_features.cc index 7f0378342a27..1411ac47f991 100644 --- a/source/common/runtime/runtime_features.cc +++ b/source/common/runtime/runtime_features.cc @@ -80,6 +80,7 @@ RUNTIME_GUARD(envoy_reloadable_features_quic_receive_ecn); // Ignore the automated "remove this flag" issue: we should keep this for 1 year. Confirm with // @danzh2010 or @RyanTheOptimist before removing. RUNTIME_GUARD(envoy_reloadable_features_quic_send_server_preferred_address_to_all_clients); +RUNTIME_GUARD(envoy_reloadable_features_quic_support_certificate_compression); RUNTIME_GUARD(envoy_reloadable_features_quic_upstream_reads_fixed_number_packets); RUNTIME_GUARD(envoy_reloadable_features_quic_upstream_socket_use_address_cache_for_read); RUNTIME_GUARD(envoy_reloadable_features_reject_invalid_yaml); diff --git a/test/common/quic/BUILD b/test/common/quic/BUILD index dc2df66c3174..938238aafdf4 100644 --- a/test/common/quic/BUILD +++ b/test/common/quic/BUILD @@ -395,6 +395,15 @@ envoy_cc_test( ]), ) +envoy_cc_test( + name = "cert_compression_test", + srcs = ["cert_compression_test.cc"], + deps = [ + "//source/common/quic:cert_compression_lib", + "//test/test_common:logging_lib", + ], +) + envoy_proto_library( name = "envoy_quic_h3_fuzz_proto", srcs = ["envoy_quic_h3_fuzz.proto"], diff --git a/test/common/quic/cert_compression_test.cc b/test/common/quic/cert_compression_test.cc new file mode 100644 index 000000000000..767b13df1bf3 --- /dev/null +++ b/test/common/quic/cert_compression_test.cc @@ -0,0 +1,46 @@ +#include "source/common/quic/cert_compression.h" + +#include "test/test_common/logging.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Quic { + +TEST(CertCompressionZlibTest, DecompressBadData) { + EXPECT_LOG_CONTAINS( + "error", + "Cert decompression failure in inflate, possibly caused by invalid compressed cert from peer", + { + CRYPTO_BUFFER* out = nullptr; + const uint8_t bad_compressed_data = 1; + EXPECT_EQ(CertCompression::FAILURE, + CertCompression::decompressZlib(nullptr, &out, 100, &bad_compressed_data, + sizeof(bad_compressed_data))); + }); +} + +TEST(CertCompressionZlibTest, DecompressBadLength) { + constexpr uint8_t the_data[] = {1, 2, 3, 4, 5, 6}; + constexpr size_t uncompressed_len = 6; + bssl::ScopedCBB compressed; + ASSERT_EQ(1, CBB_init(compressed.get(), 0)); + ASSERT_EQ(CertCompression::SUCCESS, + CertCompression::compressZlib(nullptr, compressed.get(), the_data, uncompressed_len)); + const auto compressed_len = CBB_len(compressed.get()); + EXPECT_NE(0, compressed_len); + + EXPECT_LOG_CONTAINS("error", + "Decompression length did not match peer provided uncompressed length, " + "caused by either invalid peer handshake data or decompression error.", + { + CRYPTO_BUFFER* out = nullptr; + EXPECT_EQ(CertCompression::FAILURE, + CertCompression::decompressZlib( + nullptr, &out, + uncompressed_len + 1 /* intentionally incorrect */, + CBB_data(compressed.get()), compressed_len)); + }); +} +} // namespace Quic +} // namespace Envoy diff --git a/test/integration/quic_http_integration_test.cc b/test/integration/quic_http_integration_test.cc index e431aa45828e..b2dc7b4fa18a 100644 --- a/test/integration/quic_http_integration_test.cc +++ b/test/integration/quic_http_integration_test.cc @@ -671,6 +671,26 @@ TEST_P(QuicHttpIntegrationTest, RuntimeEnableDraft29) { test_server_->waitForCounterEq("http3.quic_version_h3_29", 1u); } +TEST_P(QuicHttpIntegrationTest, CertCompressionEnabled) { + config_helper_.addRuntimeOverride( + "envoy.reloadable_features.quic_support_certificate_compression", "true"); + initialize(); + + EXPECT_LOG_CONTAINS_ALL_OF( + Envoy::ExpectedLogMessages( + {{"trace", "Cert compression successful"}, {"trace", "Cert decompression successful"}}), + { testRouterHeaderOnlyRequestAndResponse(); }); +} + +TEST_P(QuicHttpIntegrationTest, CertCompressionDisabled) { + config_helper_.addRuntimeOverride( + "envoy.reloadable_features.quic_support_certificate_compression", "false"); + initialize(); + + EXPECT_LOG_NOT_CONTAINS("trace", "Cert compression successful", + { testRouterHeaderOnlyRequestAndResponse(); }); +} + TEST_P(QuicHttpIntegrationTest, ZeroRtt) { // Make sure all connections use the same PersistentQuicInfoImpl. concurrency_ = 1; diff --git a/test/per_file_coverage.sh b/test/per_file_coverage.sh index 8138b1e56ebc..5b5f6d472a7a 100755 --- a/test/per_file_coverage.sh +++ b/test/per_file_coverage.sh @@ -16,7 +16,7 @@ declare -a KNOWN_LOW_COVERAGE=( "source/common/memory:74.5" # tcmalloc code path is not enabled in coverage build, only gperf tcmalloc, see PR#32589 "source/common/network:94.4" # Flaky, `activateFileEvents`, `startSecureTransport` and `ioctl`, listener_socket do not always report LCOV "source/common/network/dns_resolver:91.4" # A few lines of MacOS code not tested in linux scripts. Tested in MacOS scripts -"source/common/quic:93.7" +"source/common/quic:93.5" "source/common/secret:95.4" "source/common/signal:87.2" # Death tests don't report LCOV "source/common/thread:0.0" # Death tests don't report LCOV