Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion source/common/quic/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions source/common/quic/envoy_quic_proof_source.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion source/common/quic/envoy_quic_server_session.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
19 changes: 19 additions & 0 deletions source/common/quic/envoy_tls_server_handshaker.cc
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<EnvoyTlsServerHandshaker*>(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<EnvoyQuicServerSession*>(handshaker->session())->connectionInfoProvider();
handshaker->pinnedServerContext()->maybeWriteKeyLog(line, info.localAddress().get(),
info.remoteAddress().get());
}

} // namespace Quic
} // namespace Envoy
7 changes: 7 additions & 0 deletions source/common/quic/envoy_tls_server_handshaker.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
20 changes: 13 additions & 7 deletions source/common/tls/context_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -405,14 +405,20 @@ void ContextImpl::keylogCallback(const SSL* ssl, const char* line) {
auto ctx = static_cast<ContextImpl*>(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"));
}
}

Expand Down
7 changes: 7 additions & 0 deletions source/common/tls/context_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
9 changes: 9 additions & 0 deletions test/common/quic/envoy_tls_server_handshaker_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ TEST(EnvoyTlsServerHandshakerTest, TicketKeyCallbackNullHandshaker) {
nullptr, 0));
}

TEST(EnvoyTlsServerHandshakerTest, KeylogCallbackNullHandshaker) {
bssl::UniquePtr<SSL_CTX> ssl_ctx(SSL_CTX_new(TLS_method()));
ASSERT_NE(ssl_ctx, nullptr);
bssl::UniquePtr<SSL> 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
126 changes: 126 additions & 0 deletions test/integration/quic_http_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> 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
Loading