Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tls: add enable_client_cipher_preference option #35106

Merged
merged 12 commits into from
Jul 23, 2024
7 changes: 6 additions & 1 deletion api/envoy/extensions/transport_sockets/tls/v3/common.proto
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;

// [#protodoc-title: Common TLS configuration]

// [#next-free-field: 6]
// [#next-free-field: 7]
message TlsParameters {
option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.auth.TlsParameters";

Expand Down Expand Up @@ -157,6 +157,11 @@ message TlsParameters {
// rsa_pkcs1_sha1
// ecdsa_sha1
repeated string signature_algorithms = 5;

// By default, Envoy as a server uses its preferred cipher during the handshake.
// Setting this to true would allow the downstream client's preferred cipher to be used instead.
// Has no effect when using TLSv1_3.
bool prefer_client_ciphers = 6;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this field be in DownstreamTlsContext instead of here? If I'm understanding the intent here correctly, this seems to apply to the downstream connection only, not to upstream connections. But the TlsParameters message is used in CommonTlsContext, which is used in both UpstreamTlsContext and DownstreamTlsContext.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good catch! Yes, agree this should be in the downstream context

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, Thanks for pointing that out. I've moved it to downstream context now.

}

// BoringSSL private key method configuration. The private key methods are used for external
Expand Down
5 changes: 5 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,11 @@ new_features:
Added :ref:`auto_sni_from_upstream <envoy_v3_api_field_config.core.v3.UpstreamHttpProtocolOptions.auto_sni_from_upstream>`
to use value of :ref:`hostnames <envoy_v3_api_field_config.endpoint.v3.Endpoint.hostname>` of
upstream cluster's endpoints as the value for SNI.
- area: tls
change: |
Added :ref:`prefer_client_ciphers
<envoy_v3_api_field_extensions.transport_sockets.tls.v3.TlsParameters.prefer_client_ciphers>`
to support enabling client cipher preference instead of server's for TLS handshakes.

deprecated:
- area: tracing
Expand Down
5 changes: 5 additions & 0 deletions envoy/ssl/context_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ class ContextConfig {
*/
virtual const std::string& signatureAlgorithms() const PURE;

/**
* @return true if the client cipher preference is enabled, false otherwise.
*/
virtual bool preferClientCiphers() const PURE;

/**
* @return std::vector<std::reference_wrapper<const TlsCertificateConfig>> TLS
* certificate configs.
Expand Down
1 change: 1 addition & 0 deletions source/common/tls/context_config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ ContextConfigImpl::ContextConfigImpl(
ecdh_curves_(StringUtil::nonEmptyStringOrDefault(
RepeatedPtrUtil::join(config.tls_params().ecdh_curves(), ":"), default_curves)),
signature_algorithms_(RepeatedPtrUtil::join(config.tls_params().signature_algorithms(), ":")),
prefer_client_ciphers_(config.tls_params().prefer_client_ciphers()),
tls_certificate_providers_(
getTlsCertificateConfigProviders(config, factory_context, creation_status)),
certificate_validation_context_provider_(getCertificateValidationContextConfigProvider(
Expand Down
2 changes: 2 additions & 0 deletions source/common/tls/context_config_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class ContextConfigImpl : public virtual Ssl::ContextConfig {
const std::string& cipherSuites() const override { return cipher_suites_; }
const std::string& ecdhCurves() const override { return ecdh_curves_; }
const std::string& signatureAlgorithms() const override { return signature_algorithms_; }
bool preferClientCiphers() const override { return prefer_client_ciphers_; }
// TODO(htuch): This needs to be made const again and/or zero copy and/or callers fixed.
std::vector<std::reference_wrapper<const Envoy::Ssl::TlsCertificateConfig>>
tlsCertificates() const override {
Expand Down Expand Up @@ -89,6 +90,7 @@ class ContextConfigImpl : public virtual Ssl::ContextConfig {
const std::string cipher_suites_;
const std::string ecdh_curves_;
const std::string signature_algorithms_;
const bool prefer_client_ciphers_;

std::vector<std::unique_ptr<Ssl::TlsCertificateConfigImpl>> tls_certificate_configs_;
Ssl::CertificateValidationContextConfigPtr validation_context_config_;
Expand Down
4 changes: 3 additions & 1 deletion source/common/tls/context_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,9 @@ ContextImpl::ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& c

// use the server's cipher list preferences
for (auto& ctx : tls_contexts_) {
SSL_CTX_set_options(ctx.ssl_ctx_.get(), SSL_OP_CIPHER_SERVER_PREFERENCE);
if (!config.preferClientCiphers()) {
SSL_CTX_set_options(ctx.ssl_ctx_.get(), SSL_OP_CIPHER_SERVER_PREFERENCE);
}
}

parsed_alpn_protocols_ = parseAlpnProtocols(config.alpnProtocols(), creation_status);
Expand Down
2 changes: 2 additions & 0 deletions test/common/tls/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ envoy_cc_test_library(
"ssl_test_utility.h",
],
deps = [
"//source/common/tls:ssl_socket_lib",
"//source/common/tls:utility_lib",
"//test/test_common:environment_lib",
],
Expand Down Expand Up @@ -197,6 +198,7 @@ envoy_cc_test(
name = "handshaker_factory_test",
srcs = ["handshaker_factory_test.cc"],
deps = [
":ssl_test_utils",
"//source/common/stream_info:stream_info_lib",
"//source/common/tls:context_lib",
"//source/common/tls:server_context_config_lib",
Expand Down
52 changes: 52 additions & 0 deletions test/common/tls/context_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "source/common/tls/context_config_impl.h"
#include "source/common/tls/context_impl.h"
#include "source/common/tls/server_context_config_impl.h"
#include "source/common/tls/server_ssl_socket.h"
#include "source/common/tls/utility.h"

#include "test/common/tls/ssl_certs_test.h"
Expand Down Expand Up @@ -141,6 +142,57 @@ TEST_F(SslContextImplTest, TestCipherSuites) {
"ciphers were rejected when tried individually: BOGUS1-SHA256, BOGUS2-SHA");
}

// Envoy's default cipher preference is server's.
TEST_F(SslContextImplTest, TestServerCipherPreference) {
const std::string yaml = R"EOF(
common_tls_context:
tls_certificates:
certificate_chain:
filename: "{{ test_rundir }}/test/common/tls/test_data/unittest_cert.pem"
private_key:
filename: "{{ test_rundir }}/test/common/tls/test_data/unittest_key.pem"
)EOF";

envoy::extensions::transport_sockets::tls::v3::DownstreamTlsContext tls_context;
TestUtility::loadFromYaml(TestEnvironment::substitute(yaml), tls_context);
auto cfg = ServerContextConfigImpl::create(tls_context, factory_context_).value();
ASSERT_FALSE(cfg.get()->preferClientCiphers());

auto socket_factory = *Extensions::TransportSockets::Tls::ServerSslSocketFactory::create(
std::move(cfg), manager_, *store_.rootScope(), {});
std::unique_ptr<Network::TransportSocket> socket =
socket_factory->createDownstreamTransportSocket();
SSL_CTX* ssl_ctx = extractSslCtx(socket.get());

EXPECT_TRUE(SSL_CTX_get_options(ssl_ctx) & SSL_OP_CIPHER_SERVER_PREFERENCE);
}

TEST_F(SslContextImplTest, TestPreferClientCiphers) {
const std::string yaml = R"EOF(
common_tls_context:
tls_certificates:
certificate_chain:
filename: "{{ test_rundir }}/test/common/tls/test_data/unittest_cert.pem"
private_key:
filename: "{{ test_rundir }}/test/common/tls/test_data/unittest_key.pem"
tls_params:
prefer_client_ciphers: true
)EOF";

envoy::extensions::transport_sockets::tls::v3::DownstreamTlsContext tls_context;
TestUtility::loadFromYaml(TestEnvironment::substitute(yaml), tls_context);
auto cfg = ServerContextConfigImpl::create(tls_context, factory_context_).value();
ASSERT_TRUE(cfg.get()->preferClientCiphers());

auto socket_factory = *Extensions::TransportSockets::Tls::ServerSslSocketFactory::create(
std::move(cfg), manager_, *store_.rootScope(), {});
std::unique_ptr<Network::TransportSocket> socket =
socket_factory->createDownstreamTransportSocket();
SSL_CTX* ssl_ctx = extractSslCtx(socket.get());

EXPECT_FALSE(SSL_CTX_get_options(ssl_ctx) & SSL_OP_CIPHER_SERVER_PREFERENCE);
}

TEST_F(SslContextImplTest, TestExpiringCert) {
const std::string yaml = R"EOF(
common_tls_context:
Expand Down
9 changes: 1 addition & 8 deletions test/common/tls/handshaker_factory_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "source/common/tls/ssl_handshaker.h"
#include "source/server/process_context_impl.h"

#include "test/common/tls/ssl_test_utility.h"
#include "test/mocks/network/connection.h"
#include "test/mocks/server/transport_socket_factory_context.h"
#include "test/test_common/registry.h"
Expand Down Expand Up @@ -105,14 +106,6 @@ class HandshakerFactoryTest : public testing::Test {
custom_handshaker->mutable_typed_config()->PackFrom(ProtobufWkt::StringValue());
}

// Helper for downcasting a socket to a test socket so we can examine its
// SSL_CTX.
SSL_CTX* extractSslCtx(Network::TransportSocket* socket) {
SslSocket* ssl_socket = dynamic_cast<SslSocket*>(socket);
SSL* ssl = ssl_socket->rawSslForTest();
return SSL_get_SSL_CTX(ssl);
}

NiceMock<Server::Configuration::MockServerFactoryContext> server_factory_context_;
Stats::IsolatedStoreImpl stats_store_;
std::unique_ptr<Extensions::TransportSockets::Tls::ContextManagerImpl> context_manager_;
Expand Down
38 changes: 38 additions & 0 deletions test/common/tls/integration/ssl_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,44 @@ TEST_P(SslIntegrationTest, RouterDownstreamDisconnectBeforeResponseComplete) {
checkStats();
}

// Test server preference of cipher suites. Server order is ECDHE-RSA-AES128-GCM-SHA256
// followed by ECDHE-RSA-AES256-GCM-SHA384. "ECDHE-RSA-AES128-GCM-SHA256" should be used based on
// server preference.
TEST_P(SslIntegrationTest, TestServerCipherPreference) {
server_ciphers_.push_back("ECDHE-RSA-AES128-GCM-SHA256");
server_ciphers_.push_back("ECDHE-RSA-AES256-GCM-SHA384");
initialize();
codec_client_ = makeHttpConnection(makeSslClientConnection(
ClientSslTransportOptions{}
.setTlsVersion(envoy::extensions::transport_sockets::tls::v3::TlsParameters::TLSv1_2)
.setCipherSuites({"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256"})));
auto response =
sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 0);

const std::string counter_name = listenerStatPrefix("ssl.ciphers.ECDHE-RSA-AES128-GCM-SHA256");
Stats::CounterSharedPtr counter = test_server_->counter(counter_name);
EXPECT_EQ(1, test_server_->counter(counter_name)->value());
}

// Test client preference of cipher suites. Same server preference is followed as in the previous.
// "ECDHE-RSA-AES256-GCM-SHA384" should be used based on client preference.
TEST_P(SslIntegrationTest, ClientCipherPreference) {
prefer_client_ciphers_ = true;
server_ciphers_.push_back("ECDHE-RSA-AES128-GCM-SHA256");
server_ciphers_.push_back("ECDHE-RSA-AES256-GCM-SHA384");
initialize();
codec_client_ = makeHttpConnection(makeSslClientConnection(
ClientSslTransportOptions{}
.setTlsVersion(envoy::extensions::transport_sockets::tls::v3::TlsParameters::TLSv1_2)
.setCipherSuites({"ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256"})));
auto response =
sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 0);

const std::string counter_name = listenerStatPrefix("ssl.ciphers.ECDHE-RSA-AES256-GCM-SHA384");
Stats::CounterSharedPtr counter = test_server_->counter(counter_name);
EXPECT_EQ(1, test_server_->counter(counter_name)->value());
}

// This test must be here vs integration_admin_test so that it tests a server with loaded certs.
TEST_P(SslIntegrationTest, AdminCertEndpoint) {
DISABLE_IF_ADMIN_DISABLED; // Admin functionality.
Expand Down
2 changes: 2 additions & 0 deletions test/common/tls/integration/ssl_integration_test_base.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ void SslIntegrationTestBase::initialize() {
.setEcdsaCert(server_ecdsa_cert_)
.setEcdsaCertOcspStaple(server_ecdsa_cert_ocsp_staple_)
.setOcspStapleRequired(ocsp_staple_required_)
.setPreferClientCiphers(prefer_client_ciphers_)
.setTlsV13(server_tlsv1_3_)
.setCurves(server_curves_)
.setCiphers(server_ciphers_)
.setExpectClientEcdsaCert(client_ecdsa_cert_)
.setTlsKeyLogFilter(keylog_local_, keylog_remote_,
keylog_local_negative_,
Expand Down
2 changes: 2 additions & 0 deletions test/common/tls/integration/ssl_integration_test_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ class SslIntegrationTestBase : public HttpIntegrationTest {
protected:
bool server_tlsv1_3_{false};
std::vector<std::string> server_curves_;
std::vector<std::string> server_ciphers_;
bool server_rsa_cert_{true};
bool server_rsa_cert_ocsp_staple_{false};
bool server_ecdsa_cert_{false};
bool server_ecdsa_cert_ocsp_staple_{false};
bool ocsp_staple_required_{false};
bool prefer_client_ciphers_{false};
bool client_ecdsa_cert_{false};
// Set this true to debug SSL handshake issues with openssl s_client. The
// verbose trace will be in the logs, openssl must be installed separately.
Expand Down
10 changes: 10 additions & 0 deletions test/common/tls/ssl_test_utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <string>
#include <vector>

#include "source/common/tls/ssl_socket.h"

#include "test/test_common/environment.h"

#include "gtest/gtest.h"
Expand Down Expand Up @@ -36,6 +38,14 @@ inline bssl::UniquePtr<STACK_OF(X509)> readCertChainFromFile(const std::string&
return certChain;
}

// Helper for downcasting a socket to a test socket so we can examine its
// SSL_CTX.
SSL_CTX* extractSslCtx(Network::TransportSocket* socket) {
SslSocket* ssl_socket = dynamic_cast<SslSocket*>(socket);
SSL* ssl = ssl_socket->rawSslForTest();
return SSL_get_SSL_CTX(ssl);
}

} // namespace Tls
} // namespace TransportSockets
} // namespace Extensions
Expand Down
5 changes: 5 additions & 0 deletions test/config/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,9 @@ void ConfigHelper::initializeTls(
for (const auto& curve : options.curves_) {
common_tls_context.mutable_tls_params()->add_ecdh_curves(curve);
}
for (const auto& cipher : options.ciphers_) {
common_tls_context.mutable_tls_params()->add_cipher_suites(cipher);
}
if (options.rsa_cert_) {
auto* tls_certificate = common_tls_context.add_tls_certificates();
tls_certificate->mutable_certificate_chain()->set_filename(
Expand All @@ -1528,6 +1531,8 @@ void ConfigHelper::initializeTls(
"test/config/integration/certs/server_ecdsa_ocsp_resp.der"));
}
}
common_tls_context.mutable_tls_params()->set_prefer_client_ciphers(
options.prefer_client_ciphers_);
if (!options.san_matchers_.empty()) {
*validation_context->mutable_match_typed_subject_alt_names() = {options.san_matchers_.begin(),
options.san_matchers_.end()};
Expand Down
12 changes: 12 additions & 0 deletions test/config/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ class ConfigHelper {
return *this;
}

ServerSslOptions& setPreferClientCiphers(bool prefer_client_ciphers) {
prefer_client_ciphers_ = prefer_client_ciphers;
return *this;
}

ServerSslOptions& setCiphers(const std::vector<std::string>& ciphers) {
ciphers_ = ciphers;
return *this;
}

ServerSslOptions& setCurves(const std::vector<std::string>& curves) {
curves_ = curves;
return *this;
Expand Down Expand Up @@ -127,9 +137,11 @@ class ConfigHelper {
bool rsa_cert_ocsp_staple_{true};
bool ecdsa_cert_{false};
bool ecdsa_cert_ocsp_staple_{false};
bool prefer_client_ciphers_{false};
bool ocsp_staple_required_{false};
bool tlsv1_3_{false};
std::vector<std::string> curves_;
std::vector<std::string> ciphers_;
bool expect_client_ecdsa_cert_{false};
bool keylog_local_filter_{false};
bool keylog_remote_filter_{false};
Expand Down
1 change: 1 addition & 0 deletions test/mocks/ssl/mocks.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class MockServerContextConfig : public ServerContextConfig {
MOCK_METHOD(const std::string&, cipherSuites, (), (const));
MOCK_METHOD(const std::string&, ecdhCurves, (), (const));
MOCK_METHOD(const std::string&, signatureAlgorithms, (), (const));
MOCK_METHOD(bool, preferClientCiphers, (), (const));
MOCK_METHOD(std::vector<std::reference_wrapper<const TlsCertificateConfig>>, tlsCertificates, (),
(const));
MOCK_METHOD(const CertificateValidationContextConfig*, certificateValidationContext, (), (const));
Expand Down