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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed upstream QUIC connections to present the client certificate configured in the cluster's
:ref:`upstream TLS context <envoy_v3_api_msg_extensions.transport_sockets.quic.v3.QuicUpstreamTransport>`
when the upstream server requests one. Previously configured client certificates were silently
not sent over HTTP/3. Client certificates using a private key provider are not supported over
QUIC and are skipped with a warning. This behavior change can be reverted by setting the runtime
guard ``envoy.reloadable_features.quic_upstream_client_certificates`` to ``false``.
2 changes: 2 additions & 0 deletions source/common/quic/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -557,8 +557,10 @@ envoy_cc_library(
"//envoy/ssl:context_config_interface",
"//source/common/common:assert_lib",
"//source/common/network:transport_socket_options_lib",
"//source/common/runtime:runtime_lib",
"//source/common/tls:client_ssl_socket_lib",
"//source/common/tls:context_config_lib",
"//source/common/tls:context_lib",
"@quiche//:quic_core_crypto_crypto_handshake_lib",
"@envoy_api//envoy/extensions/transport_sockets/quic/v3:pkg_cc_proto",
]),
Expand Down
75 changes: 75 additions & 0 deletions source/common/quic/quic_client_transport_socket_factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,70 @@

#include "source/common/quic/envoy_quic_proof_verifier.h"
#include "source/common/quic/envoy_quic_utils.h"
#include "source/common/runtime/runtime_features.h"
#include "source/common/tls/client_context_impl.h"
#include "source/common/tls/context_config_impl.h"

#include "quiche/quic/core/crypto/quic_client_session_cache.h"

namespace Envoy {
namespace Quic {

namespace {

// Installs the configured client certificate chain and private key on the QUICHE client SSL
// context so that upstream QUIC connections present a certificate when the peer requests one.
// QUICHE's SSL context uses the CRYPTO_BUFFER-based method, so the chain is installed via
// SSL_CTX_set_chain_and_key rather than the X509-based APIs.
absl::Status configureQuicClientCertChain(SSL_CTX* quic_ssl_ctx,
const Ssl::TlsContext& tls_context) {
if (tls_context.cert_chain_ == nullptr) {
// No client certificate configured.
return absl::OkStatus();
}
EVP_PKEY* private_key = SSL_CTX_get0_privatekey(tls_context.ssl_ctx_.get());
if (private_key == nullptr) {
// The private key is not directly accessible when a private key provider is configured.
return absl::UnimplementedError(
"client certificates with a private key provider are not supported on QUIC");
}

std::vector<bssl::UniquePtr<CRYPTO_BUFFER>> chain;
auto append_cert = [&chain](X509* cert) {
uint8_t* der = nullptr;
const int len = i2d_X509(cert, &der);
if (len <= 0) {
return false;
}
bssl::UniquePtr<uint8_t> free_der(der);
chain.emplace_back(CRYPTO_BUFFER_new(der, len, nullptr));
return chain.back() != nullptr;
};
if (!append_cert(tls_context.cert_chain_.get())) {
return absl::InvalidArgumentError("failed to convert client certificate for QUIC");
}
STACK_OF(X509)* intermediates = nullptr;
SSL_CTX_get0_chain_certs(tls_context.ssl_ctx_.get(), &intermediates);
for (size_t i = 0; intermediates != nullptr && i < sk_X509_num(intermediates); i++) {
if (!append_cert(sk_X509_value(intermediates, i))) {
return absl::InvalidArgumentError("failed to convert client certificate chain for QUIC");
}
}

std::vector<CRYPTO_BUFFER*> raw_chain;
raw_chain.reserve(chain.size());
for (const auto& cert : chain) {
raw_chain.push_back(cert.get());
}
if (SSL_CTX_set_chain_and_key(quic_ssl_ctx, raw_chain.data(), raw_chain.size(), private_key,
nullptr) != 1) {
return absl::InvalidArgumentError("failed to install client certificate chain for QUIC");
}
return absl::OkStatus();
}

} // namespace

absl::StatusOr<std::unique_ptr<QuicClientTransportSocketFactory>>
QuicClientTransportSocketFactory::create(
Ssl::ClientContextConfigPtr config,
Expand Down Expand Up @@ -92,6 +149,24 @@ std::shared_ptr<quic::QuicCryptoClientConfig> QuicClientTransportSocketFactory::
std::make_unique<quic::QuicClientSessionCache>());

registerCertCompression(tls_config.crypto_config_->ssl_ctx());

if (Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.quic_upstream_client_certificates")) {
// The cert selector is rejected at config load time for QUIC, so a production context is
// always a plain ClientContextImpl with at most one TLS certificate. The downcast can only
// fail for mock contexts in tests.
auto* client_context_impl =
dynamic_cast<Extensions::TransportSockets::Tls::ClientContextImpl*>(
tls_config.client_context_.get());
if (client_context_impl != nullptr && !client_context_impl->getTlsContexts().empty()) {
absl::Status status = configureQuicClientCertChain(
tls_config.crypto_config_->ssl_ctx(), client_context_impl->getTlsContexts()[0]);
if (!status.ok()) {
ENVOY_LOG(warn, "Not sending client certificates on QUIC connections: {}",
status.message());
}
}
}
}
// 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 @@ -109,6 +109,7 @@ RUNTIME_GUARD(envoy_reloadable_features_quic_fix_defer_logging_miss_for_half_clo
// @danzh2010 or @RyanTheOptimist before removing.
RUNTIME_GUARD(envoy_reloadable_features_quic_send_server_preferred_address_to_all_clients);
RUNTIME_GUARD(envoy_reloadable_features_quic_signal_headers_only_to_http1_backend);
RUNTIME_GUARD(envoy_reloadable_features_quic_upstream_client_certificates);
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_quic_validate_headers_only_content_length);
Expand Down
3 changes: 3 additions & 0 deletions test/common/quic/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -397,10 +397,13 @@ envoy_cc_test(
"//source/common/quic:quic_server_transport_socket_factory_lib",
"//source/common/quic:quic_transport_socket_factory_lib",
"//source/common/tls:context_config_lib",
"//source/common/tls:context_lib",
"//test/mocks/server:factory_context_mocks",
"//test/mocks/ssl:ssl_mocks",
"//test/test_common:environment_lib",
"//test/test_common:test_runtime_lib",
"//test/test_common:utility_lib",
"@quiche//:quic_test_tools_test_certificates_lib",
]),
)

Expand Down
89 changes: 89 additions & 0 deletions test/common/quic/quic_transport_socket_factory_test.cc
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
#include "source/common/quic/quic_client_transport_socket_factory.h"
#include "source/common/quic/quic_server_transport_socket_factory.h"
#include "source/common/tls/client_context_impl.h"
#include "source/common/tls/context_config_impl.h"

#include "test/mocks/server/server_factory_context.h"
#include "test/mocks/ssl/mocks.h"
#include "test/test_common/environment.h"
#include "test/test_common/test_runtime.h"
#include "test/test_common/utility.h"

#include "quiche/quic/test_tools/test_certificates.h"

using testing::NiceMock;
using testing::Return;
using testing::ReturnRef;
Expand Down Expand Up @@ -249,6 +254,50 @@ class QuicClientTransportSocketFactoryTest : public testing::Test {
std::unique_ptr<Envoy::Ssl::ClientContextConfig>(context_config_), context_);
}

// Builds a real ClientContextImpl, optionally with a TLS certificate, to exercise the client
// certificate installation on the QUICHE SSL context.
Ssl::ClientContextSharedPtr makeRealClientContext(bool with_cert) {
ON_CALL(real_context_config_, cipherSuites())
.WillByDefault(ReturnRef(
Extensions::TransportSockets::Tls::ClientContextConfigImpl::DEFAULT_CIPHER_SUITES));
ON_CALL(real_context_config_, ecdhCurves())
.WillByDefault(
ReturnRef(Extensions::TransportSockets::Tls::ClientContextConfigImpl::DEFAULT_CURVES));
ON_CALL(real_context_config_, alpnProtocols()).WillByDefault(ReturnRef(alpn_));
ON_CALL(real_context_config_, serverNameIndication()).WillByDefault(ReturnRef(empty_string_));
ON_CALL(real_context_config_, signatureAlgorithms()).WillByDefault(ReturnRef(sig_algs_));
if (with_cert) {
ON_CALL(tls_cert_config_, pkcs12()).WillByDefault(ReturnRef(empty_string_));
ON_CALL(tls_cert_config_, certificateChainPath()).WillByDefault(ReturnRef(empty_string_));
ON_CALL(tls_cert_config_, certificateName()).WillByDefault(ReturnRef(empty_string_));
ON_CALL(tls_cert_config_, privateKeyMethod()).WillByDefault(Return(nullptr));
ON_CALL(tls_cert_config_, privateKeyPath()).WillByDefault(ReturnRef(empty_string_));
ON_CALL(tls_cert_config_, password()).WillByDefault(ReturnRef(empty_string_));
ON_CALL(tls_cert_config_, ocspStaple()).WillByDefault(ReturnRef(ocsp_staple_));
ON_CALL(tls_cert_config_, certificateChain()).WillByDefault(ReturnRef(test_cert_chain_));
ON_CALL(tls_cert_config_, privateKey()).WillByDefault(ReturnRef(test_private_key_));
tls_cert_configs_.emplace_back(tls_cert_config_);
ON_CALL(real_context_config_, tlsCertificates()).WillByDefault(Return(tls_cert_configs_));
}
auto context_or_error = Extensions::TransportSockets::Tls::ClientContextImpl::create(
*store_.rootScope(), real_context_config_, context_.server_context_);
THROW_IF_NOT_OK_REF(context_or_error.status());
return Ssl::ClientContextSharedPtr(std::move(*context_or_error));
}

// Declared before factory_ so the store (and its symbol table) outlives the contexts created
// from it, which the factory retains until destruction.
Stats::IsolatedStoreImpl store_;
NiceMock<Ssl::MockClientContextConfig> real_context_config_;
NiceMock<Ssl::MockTlsCertificateConfig> tls_cert_config_;
std::vector<std::reference_wrapper<const Envoy::Ssl::TlsCertificateConfig>> tls_cert_configs_;
const std::string empty_string_;
const std::string alpn_{"h3"};
const std::string sig_algs_{"rsa_pss_rsae_sha256"};
const std::vector<uint8_t> ocsp_staple_;
const std::string test_cert_chain_{quic::test::kTestCertificateChainPem};
const std::string test_private_key_{quic::test::kTestCertificatePrivateKeyPem};

testing::NiceMock<ThreadLocal::MockInstance> thread_local_;
NiceMock<Server::Configuration::MockTransportSocketFactoryContext> context_;
std::unique_ptr<Quic::QuicClientTransportSocketFactory> factory_;
Expand Down Expand Up @@ -303,5 +352,45 @@ TEST_F(QuicClientTransportSocketFactoryTest, GetCryptoConfig) {
EXPECT_NE(crypto_config2, crypto_config1);
}

// A configured client certificate is installed on the QUICHE SSL context so it is presented when
// the upstream requests one.
TEST_F(QuicClientTransportSocketFactoryTest, ClientCertificateConfigured) {
initialize();
Ssl::ClientContextSharedPtr context = makeRealClientContext(/*with_cert=*/true);
EXPECT_CALL(context_.server_context_.ssl_context_manager_, createSslClientContext(_, _))
.WillOnce(Return(context));
update_callback_();
std::shared_ptr<quic::QuicCryptoClientConfig> crypto_config = factory_->getCryptoConfig();
ASSERT_NE(nullptr, crypto_config);
EXPECT_NE(nullptr, SSL_CTX_get0_privatekey(crypto_config->ssl_ctx()));
}

TEST_F(QuicClientTransportSocketFactoryTest, NoClientCertificate) {
initialize();
Ssl::ClientContextSharedPtr context = makeRealClientContext(/*with_cert=*/false);
EXPECT_CALL(context_.server_context_.ssl_context_manager_, createSslClientContext(_, _))
.WillOnce(Return(context));
update_callback_();
std::shared_ptr<quic::QuicCryptoClientConfig> crypto_config = factory_->getCryptoConfig();
ASSERT_NE(nullptr, crypto_config);
EXPECT_EQ(nullptr, SSL_CTX_get0_privatekey(crypto_config->ssl_ctx()));
}

// With the runtime guard disabled, client certificates are not installed (pre-existing behavior).
TEST_F(QuicClientTransportSocketFactoryTest, ClientCertificateRuntimeDisabled) {
TestScopedRuntime scoped_runtime;
scoped_runtime.mergeValues(
{{"envoy.reloadable_features.quic_upstream_client_certificates", "false"}});

initialize();
Ssl::ClientContextSharedPtr context = makeRealClientContext(/*with_cert=*/true);
EXPECT_CALL(context_.server_context_.ssl_context_manager_, createSslClientContext(_, _))
.WillOnce(Return(context));
update_callback_();
std::shared_ptr<quic::QuicCryptoClientConfig> crypto_config = factory_->getCryptoConfig();
ASSERT_NE(nullptr, crypto_config);
EXPECT_EQ(nullptr, SSL_CTX_get0_privatekey(crypto_config->ssl_ctx()));
}

} // namespace Quic
} // namespace Envoy