Skip to content

Commit

Permalink
ssl: add support for SNI.
Browse files Browse the repository at this point in the history
This version serves different TLS certificates based on SNI,
but it doesn't select alternative filter chains.

Partially fixes envoyproxy#95.

Signed-off-by: Piotr Sikora <piotrsikora@google.com>
  • Loading branch information
PiotrSikora committed Nov 1, 2017
1 parent cedea0e commit 76709a0
Show file tree
Hide file tree
Showing 16 changed files with 234 additions and 59 deletions.
11 changes: 10 additions & 1 deletion include/envoy/ssl/context_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,18 @@ class ContextManager {
/**
* Builds a ServerContext from a ServerContextConfig.
*/
virtual ServerContextPtr createSslServerContext(Stats::Scope& scope,
virtual ServerContextPtr createSslServerContext(const std::string& listener_name,
const std::vector<std::string>& server_names,
Stats::Scope& scope,
ServerContextConfig& config) PURE;

/**
* Find ServerContext for a given listener and server_name.
* @return ServerContext or nullptr in case there is no match.
*/
virtual ServerContext* findSslServerContext(const std::string& listener_name,
const std::string& server_name) PURE;

/**
* @return the number of days until the next certificate being managed will expire.
*/
Expand Down
1 change: 1 addition & 0 deletions source/common/ssl/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ envoy_cc_library(
"//include/envoy/stats:stats_interface",
"//include/envoy/stats:stats_macros",
"//source/common/common:assert_lib",
"//source/common/common:empty_string",
"//source/common/common:hex_lib",
],
)
66 changes: 63 additions & 3 deletions source/common/ssl/context_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "envoy/runtime/runtime.h"

#include "common/common/assert.h"
#include "common/common/empty_string.h"
#include "common/common/hex.h"

#include "fmt/format.h"
Expand Down Expand Up @@ -355,10 +356,20 @@ bssl::UniquePtr<SSL> ClientContextImpl::newSsl() const {
return ssl_con;
}

ServerContextImpl::ServerContextImpl(ContextManagerImpl& parent, Stats::Scope& scope,
ServerContextConfig& config, Runtime::Loader& runtime)
: ContextImpl(parent, scope, config), runtime_(runtime),
ServerContextImpl::ServerContextImpl(ContextManagerImpl& parent, const std::string& listener_name,
const std::vector<std::string>& server_names,
Stats::Scope& scope, ServerContextConfig& config,
Runtime::Loader& runtime)
: ContextImpl(parent, scope, config), listener_name_(listener_name),
server_names_(server_names), runtime_(runtime),
session_ticket_keys_(config.sessionTicketKeys()) {
SSL_CTX_set_select_certificate_cb(
ctx_.get(), [](const SSL_CLIENT_HELLO* client_hello) -> ssl_select_cert_result_t {
ContextImpl* context_impl = static_cast<ContextImpl*>(
SSL_CTX_get_ex_data(SSL_get_SSL_CTX(client_hello->ssl), sslContextIndex()));
return dynamic_cast<ServerContextImpl*>(context_impl)->processClientHello(client_hello);
});

if (!config.caCertFile().empty()) {
bssl::UniquePtr<STACK_OF(X509_NAME)> list(SSL_load_client_CA_file(config.caCertFile().c_str()));
if (nullptr == list) {
Expand Down Expand Up @@ -473,6 +484,55 @@ ServerContextImpl::ServerContextImpl(ContextManagerImpl& parent, Stats::Scope& s
RELEASE_ASSERT(rc == 1);
}

ssl_select_cert_result_t
ServerContextImpl::processClientHello(const SSL_CLIENT_HELLO* client_hello) {
std::string server_name = EMPTY_STRING;
const uint8_t* data;
size_t len;

if (SSL_early_callback_ctx_extension_get(client_hello, TLSEXT_TYPE_server_name, &data, &len)) {
/* Based on BoringSSL's ext_sni_parse_clienthello(). Match on empty SNI if we encounter any
* errors. */
CBS extension;
CBS_init(&extension, data, len);
CBS server_name_list, host_name;
uint8_t name_type;
if (CBS_get_u16_length_prefixed(&extension, &server_name_list) &&
CBS_get_u8(&server_name_list, &name_type) &&
CBS_get_u16_length_prefixed(&server_name_list, &host_name) &&
CBS_len(&server_name_list) == 0 && CBS_len(&extension) == 0 &&
name_type == TLSEXT_NAMETYPE_host_name && CBS_len(&host_name) != 0 &&
CBS_len(&host_name) <= TLSEXT_MAXLEN_host_name && !CBS_contains_zero_byte(&host_name)) {
server_name =
std::string(reinterpret_cast<const char*>(CBS_data(&host_name)), CBS_len(&host_name));
}
}

ServerContext* new_ctx = parent_.findSslServerContext(listener_name_, server_name);

// Reject connection if we didn't find a match.
if (new_ctx == nullptr) {
return ssl_select_cert_error;
}

// Update context if it changed.
if (new_ctx != this) {
ServerContextImpl* new_impl = dynamic_cast<ServerContextImpl*>(new_ctx);
new_impl->updateConnection(client_hello->ssl);
}

return ssl_select_cert_success;
}

void ServerContextImpl::updateConnection(SSL* ssl) {
RELEASE_ASSERT(ctx_);

// Note: this updates only served certificates.
SSL_set_SSL_CTX(ssl, ctx_.get());

// TODO(PiotrSikora): update other settings.
}

int ServerContextImpl::sessionTicketProcess(SSL*, uint8_t* key_name, uint8_t* iv,
EVP_CIPHER_CTX* ctx, HMAC_CTX* hmac_ctx, int encrypt) {
const EVP_MD* hmac = EVP_sha256();
Expand Down
18 changes: 12 additions & 6 deletions source/common/ssl/context_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ struct SslStats {

class ContextImpl : public virtual Context {
public:
~ContextImpl() { parent_.releaseContext(this); }

virtual bssl::UniquePtr<SSL> newSsl() const;

/**
Expand Down Expand Up @@ -124,6 +122,7 @@ class ContextImpl : public virtual Context {
class ClientContextImpl : public ContextImpl, public ClientContext {
public:
ClientContextImpl(ContextManagerImpl& parent, Stats::Scope& scope, ClientContextConfig& config);
~ClientContextImpl() { parent_.releaseClientContext(this); }

bssl::UniquePtr<SSL> newSsl() const override;

Expand All @@ -133,19 +132,26 @@ class ClientContextImpl : public ContextImpl, public ClientContext {

class ServerContextImpl : public ContextImpl, public ServerContext {
public:
ServerContextImpl(ContextManagerImpl& parent, Stats::Scope& scope, ServerContextConfig& config,
Runtime::Loader& runtime);
ServerContextImpl(ContextManagerImpl& parent, const std::string& listener_name,
const std::vector<std::string>& server_names, Stats::Scope& scope,
ServerContextConfig& config, Runtime::Loader& runtime);
~ServerContextImpl() { parent_.releaseServerContext(this, listener_name_, server_names_); }

private:
ssl_select_cert_result_t processClientHello(const SSL_CLIENT_HELLO* client_hello);
void updateConnection(SSL* ssl);

int alpnSelectCallback(const unsigned char** out, unsigned char* outlen, const unsigned char* in,
unsigned int inlen);
int sessionTicketProcess(SSL* ssl, uint8_t* key_name, uint8_t* iv, EVP_CIPHER_CTX* ctx,
HMAC_CTX* hmac_ctx, int encrypt);

const std::string listener_name_;
const std::vector<std::string> server_names_;
Runtime::Loader& runtime_;
std::vector<uint8_t> parsed_alt_alpn_protocols_;
const std::vector<ServerContextConfig::SessionTicketKey> session_ticket_keys_;
};

} // Ssl
} // Envoy
} // namespace Ssl
} // namespace Envoy
81 changes: 76 additions & 5 deletions source/common/ssl/context_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
#include <mutex>

#include "common/common/assert.h"
#include "common/common/empty_string.h"
#include "common/ssl/context_impl.h"

namespace Envoy {
namespace Ssl {

ContextManagerImpl::~ContextManagerImpl() { ASSERT(contexts_.empty()); }

void ContextManagerImpl::releaseContext(Context* context) {
void ContextManagerImpl::releaseClientContext(ClientContext* context) {
std::unique_lock<std::mutex> lock(contexts_lock_);

// context may not be found, in the case that a subclass of Context throws
Expand All @@ -20,23 +21,93 @@ void ContextManagerImpl::releaseContext(Context* context) {
contexts_.remove(context);
}

void ContextManagerImpl::releaseServerContext(ServerContext* context,
const std::string& listener_name,
const std::vector<std::string>& server_names) {
std::unique_lock<std::mutex> lock(contexts_lock_);

// Remove mappings.
if (server_names.empty()) {
if (map_exact_[listener_name][EMPTY_STRING] == context) {
map_exact_[listener_name][EMPTY_STRING] = nullptr;
}
} else {
for (const auto& name : server_names) {
if (name.size() > 2 && name[0] == '*' && name[1] == '.') {
if (map_wildcard_[listener_name][name] == context) {
map_wildcard_[listener_name][name] = nullptr;
}
} else {
if (map_exact_[listener_name][name] == context) {
map_exact_[listener_name][name] = nullptr;
}
}
}
}

// context may not be found, in the case that a subclass of Context throws
// in it's constructor. In that case the context did not get added, but
// the destructor of Context will run and call releaseContext().
contexts_.remove(context);
}

ClientContextPtr ContextManagerImpl::createSslClientContext(Stats::Scope& scope,
ClientContextConfig& config) {

ClientContextPtr context(new ClientContextImpl(*this, scope, config));
std::unique_lock<std::mutex> lock(contexts_lock_);
contexts_.emplace_back(context.get());
return context;
}

ServerContextPtr ContextManagerImpl::createSslServerContext(Stats::Scope& scope,
ServerContextConfig& config) {
ServerContextPtr context(new ServerContextImpl(*this, scope, config, runtime_));
ServerContextPtr
ContextManagerImpl::createSslServerContext(const std::string& listener_name,
const std::vector<std::string>& server_names,
Stats::Scope& scope, ServerContextConfig& config) {
ServerContextPtr context(
new ServerContextImpl(*this, listener_name, server_names, scope, config, runtime_));
std::unique_lock<std::mutex> lock(contexts_lock_);
contexts_.emplace_back(context.get());

// Save mappings.
if (server_names.empty()) {
map_exact_[listener_name][EMPTY_STRING] = context.get();
} else {
for (const auto& name : server_names) {
if (name.size() > 2 && name[0] == '*' && name[1] == '.') {
map_wildcard_[listener_name][name] = context.get();
} else {
map_exact_[listener_name][name] = context.get();
}
}
}

return context;
}

ServerContext* ContextManagerImpl::findSslServerContext(const std::string& listener_name,
const std::string& server_name) {
std::unique_lock<std::mutex> lock(contexts_lock_);
if (map_exact_[listener_name][server_name] != nullptr) {
return map_exact_[listener_name][server_name];
}

// Try to construct and match wildcard domain.
if (server_name.size() >= 5) {
size_t pos = server_name.find('.');
if (pos > 0) {
size_t rpos = server_name.rfind('.');
if (rpos > pos + 1 && rpos != server_name.size() - 1) {
std::string wildcard = '*' + server_name.substr(pos);
if (map_wildcard_[listener_name][wildcard] != nullptr) {
return map_wildcard_[listener_name][wildcard];
}
}
}
}

return map_exact_[listener_name][EMPTY_STRING];
}

size_t ContextManagerImpl::daysUntilFirstCertExpires() {
std::unique_lock<std::mutex> lock(contexts_lock_);
size_t ret = std::numeric_limits<int>::max();
Expand Down
13 changes: 11 additions & 2 deletions source/common/ssl/context_manager_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <functional>
#include <list>
#include <mutex>
#include <unordered_map>

#include "envoy/runtime/runtime.h"
#include "envoy/ssl/context_manager.h"
Expand All @@ -27,20 +28,28 @@ class ContextManagerImpl final : public ContextManager {
* admin purposes. When a caller frees a context it will tell us to release it also from the list
* of contexts.
*/
void releaseContext(Context* context);
void releaseClientContext(ClientContext* context);
void releaseServerContext(ServerContext* context, const std::string& listener_name,
const std::vector<std::string>& server_names);

// Ssl::ContextManager
Ssl::ClientContextPtr createSslClientContext(Stats::Scope& scope,
ClientContextConfig& config) override;
Ssl::ServerContextPtr createSslServerContext(Stats::Scope& scope,
Ssl::ServerContextPtr createSslServerContext(const std::string& listener_name,
const std::vector<std::string>& server_names,
Stats::Scope& scope,
ServerContextConfig& config) override;
Ssl::ServerContext* findSslServerContext(const std::string& listener_name,
const std::string& server_name) override;
size_t daysUntilFirstCertExpires() override;
void iterateContexts(std::function<void(Context&)> callback) override;

private:
Runtime::Loader& runtime_;
std::list<Context*> contexts_;
std::mutex contexts_lock_;
std::unordered_map<std::string, std::unordered_map<std::string, ServerContext*>> map_exact_;
std::unordered_map<std::string, std::unordered_map<std::string, ServerContext*>> map_wildcard_;
};

} // namespace Ssl
Expand Down
29 changes: 17 additions & 12 deletions source/server/listener_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ ListenerImpl::ListenerImpl(const envoy::api::v2::Listener& config, ListenerManag
Network::Utility::parseInternetAddress(config.address().socket_address().address(),
config.address().socket_address().port_value())),
global_scope_(parent_.server_.stats().createScope("")),
listener_scope_(
parent_.server_.stats().createScope(fmt::format("listener.{}.", address_->asString()))),
bind_to_port_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.deprecated_v1(), bind_to_port, true)),
use_proxy_proto_(
PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.filter_chains()[0], use_proxy_proto, false)),
Expand All @@ -88,19 +90,22 @@ ListenerImpl::ListenerImpl(const envoy::api::v2::Listener& config, ListenerManag
local_drain_manager_(parent.factory_.createDrainManager()) {
// TODO(htuch): Support multiple filter chains #1280, add constraint to ensure we have at least on
// filter chain #1308.
ASSERT(config.filter_chains().size() == 1);
const auto& filter_chain = config.filter_chains()[0];

listener_scope_ =
parent_.server_.stats().createScope(fmt::format("listener.{}.", address_->asString()));

if (filter_chain.has_tls_context()) {
Ssl::ServerContextConfigImpl context_config(filter_chain.tls_context());
ssl_context_ = parent_.server_.sslContextManager().createSslServerContext(*listener_scope_,
context_config);
ASSERT(config.filter_chains().size() >= 1);

for (const auto& filter_chain : config.filter_chains()) {
std::vector<std::string> sni_domains(filter_chain.filter_chain_match().sni_domains().begin(),
filter_chain.filter_chain_match().sni_domains().end());
if (filter_factories_.empty() || !filter_chain.filters().empty()) {
// Only one filter chain can have filters until multiple filter chains are properly supported.
RELEASE_ASSERT(filter_factories_.empty());
filter_factories_ = parent_.factory_.createFilterFactoryList(filter_chain.filters(), *this);
}
if (filter_chain.has_tls_context()) {
Ssl::ServerContextConfigImpl context_config(filter_chain.tls_context());
tls_contexts_.emplace_back(parent_.server_.sslContextManager().createSslServerContext(
name_, sni_domains, *listener_scope_, context_config));
}
}

filter_factories_ = parent_.factory_.createFilterFactoryList(filter_chain.filters(), *this);
}

ListenerImpl::~ListenerImpl() {
Expand Down
6 changes: 4 additions & 2 deletions source/server/listener_manager_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ class ListenerImpl : public Listener,
Network::FilterChainFactory& filterChainFactory() override { return *this; }
Network::ListenSocket& socket() override { return *socket_; }
bool bindToPort() override { return bind_to_port_; }
Ssl::ServerContext* sslContext() override { return ssl_context_.get(); }
Ssl::ServerContext* sslContext() override {
return tls_contexts_.empty() ? nullptr : tls_contexts_[0].get();
}
bool useProxyProto() override { return use_proxy_proto_; }
bool useOriginalDst() override { return use_original_dst_; }
uint32_t perConnectionBufferLimitBytes() override { return per_connection_buffer_limit_bytes_; }
Expand Down Expand Up @@ -236,7 +238,7 @@ class ListenerImpl : public Listener,
Network::ListenSocketSharedPtr socket_;
Stats::ScopePtr global_scope_; // Stats with global named scope, but needed for LDS cleanup.
Stats::ScopePtr listener_scope_; // Stats with listener named scope.
Ssl::ServerContextPtr ssl_context_;
std::vector<Ssl::ServerContextPtr> tls_contexts_;
const bool bind_to_port_;
const bool use_proxy_proto_;
const bool use_original_dst_;
Expand Down
Loading

0 comments on commit 76709a0

Please sign in to comment.