Skip to content

Commit

Permalink
quic: enable certificate compression/decompression (#35999)
Browse files Browse the repository at this point in the history
QUIC servers are limited in how much data they can send to clients in
the ServerHello before the client is validated (see
https://www.rfc-editor.org/rfc/rfc9000.html#section-8). If too much data
needs to be sent, an extra network round trip is needed.

One way to reduce the size of the ServerHello data is to compress the
certificates. This can, in some situations, remove an extra round trip.

Risk Level: Low
Testing: Added unit and integration tests
Docs Changes:
Release Notes: Added
Platform Specific Features:
Runtime guard:
`envoy.reloadable_features.quic_support_certificate_compression`

---------

Signed-off-by: Greg Greenway <[email protected]>
  • Loading branch information
ggreenway authored Sep 10, 2024
1 parent 97b2408 commit 627187b
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 2 deletions.
5 changes: 5 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ new_features:
Added socket ``type`` field for specifying a socket type to apply the socket option to under :ref:`SocketOption
<envoy_v3_api_msg_config.core.v3.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
Expand Down
21 changes: 20 additions & 1 deletion source/common/quic/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,17 @@ 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",
":envoy_quic_utils_lib",
":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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
],
)
122 changes: 122 additions & 0 deletions source/common/quic/cert_compression.cc
Original file line number Diff line number Diff line change
@@ -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<CRYPTO_BUFFER> 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
31 changes: 31 additions & 0 deletions source/common/quic/cert_compression.h
Original file line number Diff line number Diff line change
@@ -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<Logger::Id::quic> {
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
5 changes: 5 additions & 0 deletions source/common/quic/envoy_quic_proof_source.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions source/common/quic/envoy_quic_proof_source.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class EnvoyQuicProofSource : public EnvoyQuicProofSourceBase {
~EnvoyQuicProofSource() override = default;

// quic::ProofSource
void OnNewSslCtx(SSL_CTX* ssl_ctx) override;
quiche::QuicheReferenceCountedPointer<quic::ProofSource::Chain>
GetCertChain(const quic::QuicSocketAddress& server_address,
const quic::QuicSocketAddress& client_address, const std::string& hostname,
Expand Down
3 changes: 3 additions & 0 deletions source/common/quic/quic_client_transport_socket_factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -86,6 +87,8 @@ std::shared_ptr<quic::QuicCryptoClientConfig> QuicClientTransportSocketFactory::
tls_config.crypto_config_ = std::make_shared<quic::QuicCryptoClientConfig>(
std::make_unique<Quic::EnvoyQuicProofVerifier>(std::move(context), accept_untrusted),
std::make_unique<quic::QuicClientSessionCache>());

CertCompression::registerSslContext(tls_config.crypto_config_->ssl_ctx());
}
// Return the latest crypto config.
return tls_config.crypto_config_;
Expand Down
1 change: 1 addition & 0 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions test/common/quic/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
46 changes: 46 additions & 0 deletions test/common/quic/cert_compression_test.cc
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions test/integration/quic_http_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion test/per_file_coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 627187b

Please sign in to comment.