Skip to content
Merged
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
8 changes: 8 additions & 0 deletions include/envoy/ssl/context_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ class ClientContextConfig : public virtual ContextConfig {
* @return The maximum number of session keys to store.
*/
virtual size_t maxSessionKeys() const PURE;

/**
* @return const std::string& with the signature algorithms for the context.
Copy link
Contributor

Choose a reason for hiding this comment

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

There is no standard string representation of a signature algorithm list. This should document what format is being used.

* This is a :-delimited list of algorithms, see
* https://tools.ietf.org/id/draft-ietf-tls-tls13-21.html#rfc.section.4.2.3
* for names.
*/
virtual const std::string& signingAlgorithmsForTest() const PURE;
};

typedef std::unique_ptr<ClientContextConfig> ClientContextConfigPtr;
Expand Down
5 changes: 3 additions & 2 deletions source/common/ssl/context_config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -252,11 +252,12 @@ unsigned ContextConfigImpl::tlsVersionFromProto(
}

ClientContextConfigImpl::ClientContextConfigImpl(
const envoy::api::v2::auth::UpstreamTlsContext& config,
const envoy::api::v2::auth::UpstreamTlsContext& config, absl::string_view sigalgs,
Server::Configuration::TransportSocketFactoryContext& factory_context)
: ContextConfigImpl(config.common_tls_context(), factory_context),
server_name_indication_(config.sni()), allow_renegotiation_(config.allow_renegotiation()),
max_session_keys_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, max_session_keys, 1)) {
max_session_keys_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, max_session_keys, 1)),
sigalgs_(sigalgs) {
// BoringSSL treats this as a C string, so embedded NULL characters will not
// be handled correctly.
if (server_name_indication_.find('\0') != std::string::npos) {
Expand Down
16 changes: 11 additions & 5 deletions source/common/ssl/context_config_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,30 +91,36 @@ class ContextConfigImpl : public virtual Ssl::ContextConfig {

class ClientContextConfigImpl : public ContextConfigImpl, public ClientContextConfig {
public:
explicit ClientContextConfigImpl(
const envoy::api::v2::auth::UpstreamTlsContext& config,
ClientContextConfigImpl(
const envoy::api::v2::auth::UpstreamTlsContext& config, absl::string_view sigalgs,
Server::Configuration::TransportSocketFactoryContext& secret_provider_context);
explicit ClientContextConfigImpl(
ClientContextConfigImpl(
const envoy::api::v2::auth::UpstreamTlsContext& config,
Server::Configuration::TransportSocketFactoryContext& secret_provider_context)
: ClientContextConfigImpl(config, "", secret_provider_context) {}
ClientContextConfigImpl(
const Json::Object& config,
Server::Configuration::TransportSocketFactoryContext& secret_provider_context);

// Ssl::ClientContextConfig
const std::string& serverNameIndication() const override { return server_name_indication_; }
bool allowRenegotiation() const override { return allow_renegotiation_; }
size_t maxSessionKeys() const override { return max_session_keys_; }
const std::string& signingAlgorithmsForTest() const override { return sigalgs_; }

private:
const std::string server_name_indication_;
const bool allow_renegotiation_;
const size_t max_session_keys_;
const std::string sigalgs_;
};

class ServerContextConfigImpl : public ContextConfigImpl, public ServerContextConfig {
public:
explicit ServerContextConfigImpl(
ServerContextConfigImpl(
const envoy::api::v2::auth::DownstreamTlsContext& config,
Server::Configuration::TransportSocketFactoryContext& secret_provider_context);
explicit ServerContextConfigImpl(
ServerContextConfigImpl(
const Json::Object& config,
Server::Configuration::TransportSocketFactoryContext& secret_provider_context);

Expand Down
132 changes: 128 additions & 4 deletions source/common/ssl/context_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,27 @@
namespace Envoy {
namespace Ssl {

namespace {

bool cbsContainsU16(CBS& cbs, uint16_t n) {
while (CBS_len(&cbs) > 0) {
uint16_t v;
if (!CBS_get_u16(&cbs, &v)) {
return false;
}
if (v == n) {
return true;
}
}

return false;
}

} // namespace

ContextImpl::ContextImpl(Stats::Scope& scope, const ContextConfig& config, TimeSource& time_source)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would make sense to verify that you're adding at most one RSA and one ECDSA certificate, since SSL_IDENTITY might reject configs like that. cc @davidben

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't have particular plans to reject it though, at least with the PR as-is, I don't think anything but the first ECDSA certificate and the first RSA certificate will ever be used.

Copy link
Member Author

Choose a reason for hiding this comment

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

Based on what @davidben writes, I think we should be more permissive today. In general, considering that this code is throw away when SSL_IDENTITY arrives, we should aim to minimize the number of additional branches and testing complexity.

Copy link
Contributor

Choose a reason for hiding this comment

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

@davidben yeah, that's my point... I'd like to prevent people from accidentally configuring two ECDSA certificates, where the second one will be always ignored, etc.

@htuch fair enough.

Copy link
Contributor

Choose a reason for hiding this comment

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

@htuch I think you are confusing two related decisions.

If you only look at one curve, then obviously only one ECDSA certificate will be used and multiple are pointless. If you look at multiple curves, having both a P-256 and a P-384 certificate is potentially useful. However, P-384 and P-521 are themselves pointless, so that informs the first decision.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I'm going to stick to P-256 only for simplicity and validate the certificate at config ingestion time to ensure it meets this requirement.

Copy link
Member Author

Choose a reason for hiding this comment

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

See #5224

: scope_(scope), stats_(generateStats(scope)), time_source_(time_source) {
: scope_(scope), stats_(generateStats(scope)), time_source_(time_source),
tls_max_version_(config.maxProtocolVersion()) {
const auto tls_certificates = config.tlsCertificates();
tls_contexts_.resize(std::max(1UL, tls_certificates.size()));

Expand Down Expand Up @@ -539,6 +558,14 @@ ClientContextImpl::ClientContextImpl(Stats::Scope& scope, const ClientContextCon
}
}

if (!config.signingAlgorithmsForTest().empty()) {
for (auto& ctx : tls_contexts_) {
int rc =
SSL_CTX_set1_sigalgs_list(ctx.ssl_ctx_.get(), config.signingAlgorithmsForTest().c_str());
RELEASE_ASSERT(rc == 1, "");
Copy link
Contributor

Choose a reason for hiding this comment

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

(Note this will fail on syntax error. Is that okay for your purposes here?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, this is just for tests; I've done the rename suggested by @PiotrSikora, so we are good here.

}
}

if (max_session_keys_ > 0) {
SSL_CTX_set_session_cache_mode(tls_contexts_[0].ssl_ctx_.get(), SSL_SESS_CACHE_CLIENT);
SSL_CTX_sess_set_new_cb(
Expand Down Expand Up @@ -824,11 +851,88 @@ int ServerContextImpl::sessionTicketProcess(SSL*, uint8_t* key_name, uint8_t* iv
}
}

bool ServerContextImpl::isClientEcdsaCapable(const SSL_CLIENT_HELLO* ssl_client_hello) {
CBS client_hello;
CBS_init(&client_hello, ssl_client_hello->client_hello, ssl_client_hello->client_hello_len);

// This is the TLSv1.3 case (TLSv1.2 on the wire and the supported_versions extensions present).
// We just need to loook at signature algorithms.
const uint16_t client_version = ssl_client_hello->version;
if (client_version == TLS1_2_VERSION && tls_max_version_ == TLS1_3_VERSION) {
// If the supported_versions extension is found then we assume that the client is competent
// enough that just checking the signature_algorithms is sufficient.
const uint8_t* supported_versions_data;
size_t supported_versions_len;
if (SSL_early_callback_ctx_extension_get(ssl_client_hello, TLSEXT_TYPE_supported_versions,
&supported_versions_data, &supported_versions_len)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In theory, we should verify that client announced TLS 1.3 in supported_versions.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm going to follow the reference implementation here and leave as is, we can discuss offline.

const uint8_t* signature_algorithms_data;
size_t signature_algorithms_len;
if (SSL_early_callback_ctx_extension_get(ssl_client_hello, TLSEXT_TYPE_signature_algorithms,
&signature_algorithms_data,
&signature_algorithms_len)) {
CBS signature_algorithms_ext, signature_algorithms;
CBS_init(&signature_algorithms_ext, signature_algorithms_data, signature_algorithms_len);
if (!CBS_get_u16_length_prefixed(&signature_algorithms_ext, &signature_algorithms) ||
CBS_len(&signature_algorithms_ext) != 0) {
return false;
}
if (cbsContainsU16(signature_algorithms, SSL_SIGN_ECDSA_SECP256R1_SHA256)) {
return true;
}
}

return false;
}
}

// Otherwise we are < TLSv1.3 and need to look at both the curves in the supported_groups for
// ECDSA and also for a compatible cipher suite. https://tools.ietf.org/html/rfc4492#section-5.1.1
const uint8_t* curvelist_data;
size_t curvelist_len;
if (!SSL_early_callback_ctx_extension_get(ssl_client_hello, TLSEXT_TYPE_supported_groups,
&curvelist_data, &curvelist_len)) {
return false;
}

CBS curvelist;
CBS_init(&curvelist, curvelist_data, curvelist_len);

// We only support P256 ECDSA curves today.
if (!cbsContainsU16(curvelist, SSL_CURVE_SECP256R1)) {
return false;
}

// The client must have offered an ECDSA ciphersuite that we like.
CBS cipher_suites;
CBS_init(&cipher_suites, ssl_client_hello->cipher_suites, ssl_client_hello->cipher_suites_len);

while (CBS_len(&cipher_suites) > 0) {
uint16_t cipher_id;
if (!CBS_get_u16(&cipher_suites, &cipher_id)) {
return false;
}
// All tls_context_ share the same set of enabled ciphers, so we can just look at the base
// context.
if (tls_contexts_[0].isCipherEnabled(cipher_id, client_version)) {
return true;
}
}

return false;
}

enum ssl_select_cert_result_t
ServerContextImpl::selectTlsContext(const SSL_CLIENT_HELLO* ssl_client_hello) {
// This is currently a nop, since we only have a single cert, but this is where we will implement
// the certificate selection logic in #1319.
RELEASE_ASSERT(SSL_set_SSL_CTX(ssl_client_hello->ssl, tls_contexts_[0].ssl_ctx_.get()) != nullptr,
const bool client_ecdsa_capable = isClientEcdsaCapable(ssl_client_hello);
// Fallback on first certificate.
const TlsContext* selected_ctx = &tls_contexts_[0];
for (const auto& ctx : tls_contexts_) {
if (client_ecdsa_capable == ctx.is_ecdsa_) {
selected_ctx = &ctx;
break;
}
}
RELEASE_ASSERT(SSL_set_SSL_CTX(ssl_client_hello->ssl, selected_ctx->ssl_ctx_.get()) != nullptr,
"");
return ssl_select_cert_success;
}
Expand Down Expand Up @@ -878,5 +982,25 @@ void ServerContextImpl::TlsContext::addClientValidationContext(
}
}

bool ServerContextImpl::TlsContext::isCipherEnabled(uint16_t cipher_id, uint16_t client_version) {
const SSL_CIPHER* c = SSL_get_cipher_by_value(cipher_id);
if (c == nullptr) {
return false;
}
// Skip TLS 1.2 only ciphersuites unless the client supports it.
if (SSL_CIPHER_get_min_version(c) > client_version) {
return false;
}
if (SSL_CIPHER_get_auth_nid(c) != NID_auth_ecdsa) {
return false;
}
for (const SSL_CIPHER* our_c : SSL_CTX_get_ciphers(ssl_ctx_.get())) {
if (SSL_CIPHER_get_id(our_c) == SSL_CIPHER_get_id(c)) {
return true;
}
}
return false;
}

} // namespace Ssl
} // namespace Envoy
3 changes: 3 additions & 0 deletions source/common/ssl/context_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ class ContextImpl : public virtual Context {
std::string getCertChainFileName() const { return cert_chain_file_path_; };
void addClientValidationContext(const CertificateValidationContextConfig& config,
bool require_client_cert);
bool isCipherEnabled(uint16_t cipher_id, uint16_t client_version);
};

// This is always non-empty, with the first context used for all new SSL
Expand All @@ -154,6 +155,7 @@ class ContextImpl : public virtual Context {
std::string ca_file_path_;
std::string cert_chain_file_path_;
TimeSource& time_source_;
const unsigned tls_max_version_;
};

typedef std::shared_ptr<ContextImpl> ContextImplSharedPtr;
Expand Down Expand Up @@ -186,6 +188,7 @@ class ServerContextImpl : public ContextImpl, public ServerContext {
unsigned int inlen);
int sessionTicketProcess(SSL* ssl, uint8_t* key_name, uint8_t* iv, EVP_CIPHER_CTX* ctx,
HMAC_CTX* hmac_ctx, int encrypt);
bool isClientEcdsaCapable(const SSL_CLIENT_HELLO* ssl_client_hello);
// Select the TLS certificate context in SSL_CTX_set_select_certificate_cb() callback with
// ClientHello details.
enum ssl_select_cert_result_t selectTlsContext(const SSL_CLIENT_HELLO* ssl_client_hello);
Expand Down
14 changes: 11 additions & 3 deletions test/config/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -382,15 +382,16 @@ void ConfigHelper::setClientCodec(
}
}

void ConfigHelper::addSslConfig(bool ecdsa_cert) {
void ConfigHelper::addSslConfig(bool ecdsa_cert, bool tlsv1_3) {
RELEASE_ASSERT(!finalized_, "");

auto* filter_chain =
bootstrap_.mutable_static_resources()->mutable_listeners(0)->mutable_filter_chains(0);
initializeTls(ecdsa_cert, *filter_chain->mutable_tls_context()->mutable_common_tls_context());
initializeTls(ecdsa_cert, tlsv1_3,
*filter_chain->mutable_tls_context()->mutable_common_tls_context());
}

void ConfigHelper::initializeTls(bool ecdsa_cert,
void ConfigHelper::initializeTls(bool ecdsa_cert, bool tlsv1_3,
envoy::api::v2::auth::CommonTlsContext& common_tls_context) {
common_tls_context.add_alpn_protocols("h2");
common_tls_context.add_alpn_protocols("http/1.1");
Expand All @@ -400,6 +401,13 @@ void ConfigHelper::initializeTls(bool ecdsa_cert,
TestEnvironment::runfilesPath("test/config/integration/certs/cacert.pem"));
validation_context->add_verify_certificate_hash(TEST_CLIENT_CERT_HASH);

// We'll negotiate up to TLSv1.3 for the tests that care, but it really
// depends on what the client sets.
if (tlsv1_3) {
common_tls_context.mutable_tls_params()->set_tls_maximum_protocol_version(
envoy::api::v2::auth::TlsParameters::TLSv1_3);
}

auto* tls_certificate = common_tls_context.add_tls_certificates();
if (ecdsa_cert) {
tls_certificate->mutable_certificate_chain()->set_filename(
Expand Down
4 changes: 2 additions & 2 deletions test/config/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ConfigHelper {
ConfigHelper(const Network::Address::IpVersion version,
const std::string& config = HTTP_PROXY_CONFIG);

static void initializeTls(bool ecdsa_cert,
static void initializeTls(bool ecdsa_cert, bool tlsv1_3,
envoy::api::v2::auth::CommonTlsContext& common_context);

typedef std::function<void(envoy::config::bootstrap::v2::Bootstrap&)> ConfigModifierFunction;
Expand Down Expand Up @@ -94,7 +94,7 @@ class ConfigHelper {
type);

// Add the default SSL configuration.
void addSslConfig(bool ecdsa_cert = false);
void addSslConfig(bool ecdsa_cert = false, bool tlsv1_3 = false);

// Renames the first listener to the name specified.
void renameListener(const std::string& name);
Expand Down
Loading