diff --git a/source/common/quic/BUILD b/source/common/quic/BUILD index 52997bcf1de32..31873b4917a0a 100644 --- a/source/common/quic/BUILD +++ b/source/common/quic/BUILD @@ -128,6 +128,7 @@ envoy_cc_library( hdrs = envoy_select_enable_http3(["envoy_tls_server_handshaker.h"]), external_deps = ["ssl"], deps = envoy_select_enable_http3([ + ":envoy_quic_server_session_lib", "//source/common/common:assert_lib", "//source/common/common:macros", "//source/common/tls:server_context_lib", @@ -303,12 +304,12 @@ envoy_cc_library( ]), deps = envoy_select_enable_http3([ ":envoy_quic_connection_debug_visitor_factory_interface", - ":envoy_quic_proof_source_lib", ":envoy_quic_server_connection_lib", ":envoy_quic_server_crypto_stream_factory_lib", ":envoy_quic_stream_lib", ":envoy_quic_utils_lib", ":quic_filter_manager_connection_lib", + ":quic_server_transport_socket_factory_lib", ":quic_stat_names_lib", ":quic_stats_gatherer", "//source/common/buffer:buffer_lib", diff --git a/source/common/quic/envoy_quic_proof_source.cc b/source/common/quic/envoy_quic_proof_source.cc index da99b88981b65..de14eb480fe65 100644 --- a/source/common/quic/envoy_quic_proof_source.cc +++ b/source/common/quic/envoy_quic_proof_source.cc @@ -119,6 +119,7 @@ void EnvoyQuicProofSource::OnNewSslCtx(SSL_CTX* ssl_ctx) { registerCertCompression(ssl_ctx); if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.quic_session_ticket_support")) { SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx, EnvoyTlsServerHandshaker::ticketKeyCallback); + SSL_CTX_set_keylog_callback(ssl_ctx, EnvoyTlsServerHandshaker::keylogCallback); } } diff --git a/source/common/quic/envoy_quic_server_session.cc b/source/common/quic/envoy_quic_server_session.cc index 09f6b1630a5d8..2ac7e27910f87 100644 --- a/source/common/quic/envoy_quic_server_session.cc +++ b/source/common/quic/envoy_quic_server_session.cc @@ -12,10 +12,10 @@ #include "source/common/common/scope_tracker.h" #include "source/common/http/session_idle_list_interface.h" #include "source/common/quic/envoy_quic_connection_debug_visitor_factory_interface.h" -#include "source/common/quic/envoy_quic_proof_source.h" #include "source/common/quic/envoy_quic_server_connection.h" #include "source/common/quic/envoy_quic_server_stream.h" #include "source/common/quic/quic_filter_manager_connection_impl.h" +#include "source/common/quic/quic_server_transport_socket_factory.h" #include "absl/types/optional.h" #include "quiche/quic/core/quic_config.h" diff --git a/source/common/quic/envoy_tls_server_handshaker.cc b/source/common/quic/envoy_tls_server_handshaker.cc index 262f5453f1a33..1c16892b68821 100644 --- a/source/common/quic/envoy_tls_server_handshaker.cc +++ b/source/common/quic/envoy_tls_server_handshaker.cc @@ -1,6 +1,7 @@ #include "source/common/quic/envoy_tls_server_handshaker.h" #include "source/common/common/macros.h" +#include "source/common/quic/envoy_quic_server_session.h" namespace Envoy { namespace Quic { @@ -41,5 +42,23 @@ int EnvoyTlsServerHandshaker::ticketKeyCallback(SSL* ssl, uint8_t* key_name, uin encrypt); } +void EnvoyTlsServerHandshaker::keylogCallback(const SSL* ssl, const char* line) { + auto* handshaker = + static_cast(SSL_get_ex_data(ssl, handshakerExDataIndex())); + if (handshaker == nullptr || handshaker->pinnedServerContext() == nullptr) { + // Same gating rationale as ticketKeyCallback: when EnvoyTlsServerHandshaker is not + // installed (vanilla quic::TlsServerHandshaker path), there is no pinned context + // to write through, so silently skip. + return; + } + // EnvoyQuicServerSession is-a Network::Connection, so reuse the cached + // envoy address objects from its connection info provider rather than + // re-converting QUICHE addresses on every key log line. + const auto& info = + static_cast(handshaker->session())->connectionInfoProvider(); + handshaker->pinnedServerContext()->maybeWriteKeyLog(line, info.localAddress().get(), + info.remoteAddress().get()); +} + } // namespace Quic } // namespace Envoy diff --git a/source/common/quic/envoy_tls_server_handshaker.h b/source/common/quic/envoy_tls_server_handshaker.h index 33bd6d836e42d..00e48d2dc9087 100644 --- a/source/common/quic/envoy_tls_server_handshaker.h +++ b/source/common/quic/envoy_tls_server_handshaker.h @@ -32,6 +32,13 @@ class EnvoyTlsServerHandshaker : public quic::TlsServerHandshaker { static int ticketKeyCallback(SSL* ssl, uint8_t* key_name, uint8_t* iv, EVP_CIPHER_CTX* ctx, HMAC_CTX* hmac_ctx, int encrypt); + // Key log callback installed on the QUICHE ssl context. Retrieves the + // handshaker from ssl ex_data and writes an NSS Key Log line via the + // pinned ServerContextImpl, applying the same local/remote IP-list + // filtering as TCP TLS key log. Connection addresses are read from the + // QUIC session at callback time. + static void keylogCallback(const SSL* ssl, const char* line); + // SSL ex_data index for storing the handshaker pointer per-connection. static int handshakerExDataIndex(); diff --git a/source/common/tls/context_impl.cc b/source/common/tls/context_impl.cc index 39888edb8e8a9..5a562a08d957d 100644 --- a/source/common/tls/context_impl.cc +++ b/source/common/tls/context_impl.cc @@ -405,14 +405,20 @@ void ContextImpl::keylogCallback(const SSL* ssl, const char* line) { auto ctx = static_cast(SSL_CTX_get_app_data(SSL_get_SSL_CTX(ssl))); ASSERT(callbacks != nullptr); ASSERT(ctx != nullptr); + ctx->maybeWriteKeyLog(line, callbacks->connection().connectionInfoProvider().localAddress().get(), + callbacks->connection().connectionInfoProvider().remoteAddress().get()); +} - if ((ctx->tls_keylog_local_.getIpListSize() == 0 || - ctx->tls_keylog_local_.contains( - *(callbacks->connection().connectionInfoProvider().localAddress()))) && - (ctx->tls_keylog_remote_.getIpListSize() == 0 || - ctx->tls_keylog_remote_.contains( - *(callbacks->connection().connectionInfoProvider().remoteAddress())))) { - ctx->tls_keylog_file_->write(absl::StrCat(line, "\n")); +void ContextImpl::maybeWriteKeyLog(const char* line, const Network::Address::Instance* local_addr, + const Network::Address::Instance* remote_addr) const { + if (tls_keylog_file_ == nullptr) { + return; + } + if ((tls_keylog_local_.getIpListSize() == 0 || + (local_addr != nullptr && tls_keylog_local_.contains(*local_addr))) && + (tls_keylog_remote_.getIpListSize() == 0 || + (remote_addr != nullptr && tls_keylog_remote_.contains(*remote_addr)))) { + tls_keylog_file_->write(absl::StrCat(line, "\n")); } } diff --git a/source/common/tls/context_impl.h b/source/common/tls/context_impl.h index 2db772e212d9d..b6a97919b8e90 100644 --- a/source/common/tls/context_impl.h +++ b/source/common/tls/context_impl.h @@ -118,6 +118,13 @@ class ContextImpl : public virtual Envoy::Ssl::Context, static void keylogCallback(const SSL* ssl, const char* line); + // Apply the configured local/remote IP-list filters and, if they match, + // write a single NSS Key Log line. Shared by the TCP TLS key log callback + // and by the QUIC TLS key log callback in EnvoyTlsServerHandshaker. The + // call is a no-op when no key log file has been opened. + void maybeWriteKeyLog(const char* line, const Network::Address::Instance* local_addr, + const Network::Address::Instance* remote_addr) const; + protected: friend class ContextImplPeer; diff --git a/test/common/quic/envoy_tls_server_handshaker_test.cc b/test/common/quic/envoy_tls_server_handshaker_test.cc index de13613391064..b1b5f42c646cc 100644 --- a/test/common/quic/envoy_tls_server_handshaker_test.cc +++ b/test/common/quic/envoy_tls_server_handshaker_test.cc @@ -25,6 +25,15 @@ TEST(EnvoyTlsServerHandshakerTest, TicketKeyCallbackNullHandshaker) { nullptr, 0)); } +TEST(EnvoyTlsServerHandshakerTest, KeylogCallbackNullHandshaker) { + bssl::UniquePtr ssl_ctx(SSL_CTX_new(TLS_method())); + ASSERT_NE(ssl_ctx, nullptr); + bssl::UniquePtr ssl(SSL_new(ssl_ctx.get())); + ASSERT_NE(ssl, nullptr); + // No ex_data set → silently no-ops (no crash, no ENVOY_BUG, no file write). + EnvoyTlsServerHandshaker::keylogCallback(ssl.get(), "CLIENT_RANDOM 00 11"); +} + } // namespace } // namespace Quic } // namespace Envoy diff --git a/test/integration/quic_http_integration_test.cc b/test/integration/quic_http_integration_test.cc index bea58ca4ad299..8d0264e45407a 100644 --- a/test/integration/quic_http_integration_test.cc +++ b/test/integration/quic_http_integration_test.cc @@ -2109,5 +2109,131 @@ TEST_P(QuicHttpIntegrationTest, NoSessionTicketResumptionWithoutKeys) { codec_client_->close(); } +class QuicKeylogIntegrationTest : public QuicHttpIntegrationTest { +public: + // Sets the runtime flag, allocates a temp key log file path, configures + // key_log on the listener (if requested), and calls initialize(). + std::string setUpKeylog(bool configure, bool local_filter = false, bool remote_filter = false, + bool local_negative = false, bool remote_negative = false) { + concurrency_ = 1; + config_helper_.addRuntimeOverride("envoy.reloadable_features.quic_session_ticket_support", + "true"); + const std::string path = TestEnvironment::temporaryPath(TestUtility::uniqueFilename()); + if (configure) { + configureKeylog(path, local_filter, remote_filter, local_negative, remote_negative); + } + initialize(); + return path; + } + + // Configures key_log on the listener's QuicDownstreamTransport via the + // shared TCP TLS helper, so any future change to the key_log proto layout + // flows through here automatically. + void configureKeylog(const std::string& path, bool local_filter, bool remote_filter, + bool local_negative, bool remote_negative) { + ConfigHelper::ServerSslOptions options; + options.setTlsKeyLogFilter(local_filter, remote_filter, local_negative, remote_negative, path, + /*multiple_ips=*/false, version_); + config_helper_.addConfigModifier([options](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* ts = bootstrap.mutable_static_resources() + ->mutable_listeners(0) + ->mutable_filter_chains(0) + ->mutable_transport_socket(); + auto quic_transport = MessageUtil::anyConvert< + envoy::extensions::transport_sockets::quic::v3::QuicDownstreamTransport>( + *ts->mutable_typed_config()); + auto* common_tls = + quic_transport.mutable_downstream_tls_context()->mutable_common_tls_context(); + ConfigHelper::initializeTlsKeyLog(*common_tls, options); + ts->mutable_typed_config()->PackFrom(quic_transport); + }); + } + + // Drive one QUIC handshake + request so the key log callback fires. + void runOneRequest() { + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + waitForNextUpstreamRequest(0); + upstream_request_->encodeHeaders(default_response_headers_, true); + ASSERT_TRUE(response->waitForEndStream()); + codec_client_->close(); + } + + // QUIC always uses TLS 1.3, so all handshakes derive these five secrets. + // Wait for all of them to be flushed to disk, then assert their presence. + void assertKeylogPopulated(const std::string& path) { + EXPECT_TRUE(api_->fileSystem().fileExists(path)); + constexpr uint32_t kExpectedSecrets = 5; + std::vector entries = + waitForAccessLogEntries(path, /*client_connection=*/nullptr, kExpectedSecrets); + const std::string log = absl::StrJoin(entries, "\n"); + EXPECT_THAT(log, testing::HasSubstr("CLIENT_HANDSHAKE_TRAFFIC_SECRET")); + EXPECT_THAT(log, testing::HasSubstr("SERVER_HANDSHAKE_TRAFFIC_SECRET")); + EXPECT_THAT(log, testing::HasSubstr("CLIENT_TRAFFIC_SECRET")); + EXPECT_THAT(log, testing::HasSubstr("SERVER_TRAFFIC_SECRET")); + EXPECT_THAT(log, testing::HasSubstr("EXPORTER_SECRET")); + } + + void assertKeylogEmpty(const std::string& path) { + EXPECT_TRUE(api_->fileSystem().fileExists(path)); + EXPECT_EQ(0, api_->fileSystem().fileSize(path)); + } +}; + +INSTANTIATE_TEST_SUITE_P(QuicHttpIntegrationTests, QuicKeylogIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(QuicKeylogIntegrationTest, KeylogNoFilter) { + const std::string path = setUpKeylog(/*configure=*/true); + runOneRequest(); + assertKeylogPopulated(path); +} + +TEST_P(QuicKeylogIntegrationTest, KeylogLocalFilterMatches) { + const std::string path = setUpKeylog(/*configure=*/true, /*local_filter=*/true); + runOneRequest(); + assertKeylogPopulated(path); +} + +TEST_P(QuicKeylogIntegrationTest, KeylogRemoteFilterMatches) { + const std::string path = setUpKeylog(/*configure=*/true, /*local_filter=*/false, + /*remote_filter=*/true); + runOneRequest(); + assertKeylogPopulated(path); +} + +TEST_P(QuicKeylogIntegrationTest, KeylogLocalAndRemoteFilterMatch) { + const std::string path = setUpKeylog(/*configure=*/true, /*local_filter=*/true, + /*remote_filter=*/true); + runOneRequest(); + assertKeylogPopulated(path); +} + +TEST_P(QuicKeylogIntegrationTest, KeylogLocalFilterNoMatch) { + const std::string path = setUpKeylog(/*configure=*/true, /*local_filter=*/true, + /*remote_filter=*/false, /*local_negative=*/true); + runOneRequest(); + assertKeylogEmpty(path); +} + +TEST_P(QuicKeylogIntegrationTest, KeylogRemoteFilterNoMatch) { + const std::string path = + setUpKeylog(/*configure=*/true, /*local_filter=*/false, /*remote_filter=*/true, + /*local_negative=*/false, /*remote_negative=*/true); + runOneRequest(); + assertKeylogEmpty(path); +} + +// When no key_log is configured, no key log file should be created — the +// runtime flag is on (so EnvoyTlsServerHandshaker is in play and the +// SSL_CTX key log callback is installed) but ServerContextImpl::writeKeyLog +// short-circuits because no key log file was opened, so nothing is written. +TEST_P(QuicKeylogIntegrationTest, KeylogNotConfigured) { + const std::string path = setUpKeylog(/*configure=*/false); + runOneRequest(); + EXPECT_FALSE(api_->fileSystem().fileExists(path)); +} + } // namespace Quic } // namespace Envoy