Skip to content

Commit

Permalink
ssl: add support for SNI. (#1984)
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 #95.

Signed-off-by: Piotr Sikora <piotrsikora@google.com>
  • Loading branch information
PiotrSikora authored and mattklein123 committed Nov 28, 2017
1 parent 210c5ae commit c26b25e
Show file tree
Hide file tree
Showing 25 changed files with 1,059 additions and 108 deletions.
4 changes: 2 additions & 2 deletions include/envoy/server/listener_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ class Listener {
virtual Network::ListenSocket& socket() PURE;

/**
* @return Ssl::ServerContext* the SSL context
* @return Ssl::ServerContext* the default SSL context.
*/
virtual Ssl::ServerContext* sslContext() PURE;
virtual Ssl::ServerContext* defaultSslContext() PURE;

/**
* @return bool whether to use the PROXY Protocol V1
Expand Down
6 changes: 3 additions & 3 deletions include/envoy/ssl/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ class Context {
/**
* @return the number of days in this context until the next certificate will expire
*/
virtual size_t daysUntilFirstCertExpires() PURE;
virtual size_t daysUntilFirstCertExpires() const PURE;

/**
* @return a string of ca certificate path, certificate serial number and days until certificate
* expiration
*/
virtual std::string getCaCertInformation() PURE;
virtual std::string getCaCertInformation() const PURE;

/**
* @return a string of cert chain certificate path, certificate serial number and days until
* certificate expiration
*/
virtual std::string getCertChainInformation() PURE;
virtual std::string getCertChainInformation() const PURE;
};

class ClientContext : public virtual Context {};
Expand Down
19 changes: 15 additions & 4 deletions include/envoy/ssl/context_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,30 @@ class ContextManager {

/**
* Builds a ServerContext from a ServerContextConfig.
* The skip_context_update parameter is used for fast-path (avoiding lock & context lookup)
* on listeners with a single filter chain and no SNI restrictions.
*/
virtual ServerContextPtr createSslServerContext(Stats::Scope& scope,
ServerContextConfig& config) PURE;
virtual ServerContextPtr createSslServerContext(const std::string& listener_name,
const std::vector<std::string>& server_names,
Stats::Scope& scope, ServerContextConfig& config,
bool skip_context_update) 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) const PURE;

/**
* @return the number of days until the next certificate being managed will expire.
*/
virtual size_t daysUntilFirstCertExpires() PURE;
virtual size_t daysUntilFirstCertExpires() const PURE;

/**
* Iterate through all currently allocated contexts.
*/
virtual void iterateContexts(std::function<void(Context&)> callback) PURE;
virtual void iterateContexts(std::function<void(const Context&)> callback) PURE;
};

} // namespace Ssl
Expand Down
19 changes: 19 additions & 0 deletions source/common/protobuf/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,25 @@ class RepeatedPtrUtil {
}) +
"]";
}

// Based on MessageUtil::hash() defined below.
template <class ProtoType>
static std::size_t hash(const google::protobuf::RepeatedPtrField<ProtoType>& source) {
// Use Protobuf::io::CodedOutputStream to force deterministic serialization, so that the same
// message doesn't hash to different values.
ProtobufTypes::String text;
{
// For memory safety, the StringOutputStream needs to be destroyed before
// we read the string.
Protobuf::io::StringOutputStream string_stream(&text);
Protobuf::io::CodedOutputStream coded_stream(&string_stream);
coded_stream.SetSerializationDeterministic(true);
for (const auto& message : source) {
message.SerializeToCodedStream(&coded_stream);
}
}
return HashUtil::xxHash64(text);
}
};

class MessageUtil {
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",
],
)
104 changes: 90 additions & 14 deletions source/common/ssl/context_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ int ContextImpl::sslContextIndex() {
}

ContextImpl::ContextImpl(ContextManagerImpl& parent, Stats::Scope& scope, ContextConfig& config)
: parent_(parent), ctx_(SSL_CTX_new(TLS_method())), scope_(scope),
stats_(generateStats(scope)) {
: parent_(parent), ctx_(SSL_CTX_new(TLS_method())), scope_(scope), stats_(generateStats(scope)),
ecdh_curves_(config.ecdhCurves()) {
RELEASE_ASSERT(ctx_);

int rc = SSL_CTX_set_ex_data(ctx_.get(), sslContextIndex(), this);
Expand All @@ -40,8 +40,8 @@ ContextImpl::ContextImpl(ContextManagerImpl& parent, Stats::Scope& scope, Contex
fmt::format("Failed to initialize cipher suites {}", config.cipherSuites()));
}

if (!SSL_CTX_set1_curves_list(ctx_.get(), config.ecdhCurves().c_str())) {
throw EnvoyException(fmt::format("Failed to initialize ECDH curves {}", config.ecdhCurves()));
if (!SSL_CTX_set1_curves_list(ctx_.get(), ecdh_curves_.c_str())) {
throw EnvoyException(fmt::format("Failed to initialize ECDH curves {}", ecdh_curves_));
}

int verify_mode = SSL_VERIFY_NONE;
Expand Down Expand Up @@ -266,7 +266,7 @@ SslStats ContextImpl::generateStats(Stats::Scope& store) {
POOL_HISTOGRAM_PREFIX(store, prefix))};
}

size_t ContextImpl::daysUntilFirstCertExpires() {
size_t ContextImpl::daysUntilFirstCertExpires() const {
int daysUntilExpiration = getDaysUntilExpiration(ca_cert_.get());
daysUntilExpiration =
std::min<int>(getDaysUntilExpiration(cert_chain_.get()), daysUntilExpiration);
Expand All @@ -276,7 +276,7 @@ size_t ContextImpl::daysUntilFirstCertExpires() {
return daysUntilExpiration;
}

int32_t ContextImpl::getDaysUntilExpiration(const X509* cert) {
int32_t ContextImpl::getDaysUntilExpiration(const X509* cert) const {
if (cert == nullptr) {
return std::numeric_limits<int>::max();
}
Expand All @@ -287,7 +287,7 @@ int32_t ContextImpl::getDaysUntilExpiration(const X509* cert) {
return 0;
}

std::string ContextImpl::getCaCertInformation() {
std::string ContextImpl::getCaCertInformation() const {
if (ca_cert_ == nullptr) {
return "";
}
Expand All @@ -296,7 +296,7 @@ std::string ContextImpl::getCaCertInformation() {
getDaysUntilExpiration(ca_cert_.get()));
}

std::string ContextImpl::getCertChainInformation() {
std::string ContextImpl::getCertChainInformation() const {
if (cert_chain_ == nullptr) {
return "";
}
Expand All @@ -305,9 +305,9 @@ std::string ContextImpl::getCertChainInformation() {
getDaysUntilExpiration(cert_chain_.get()));
}

std::string ContextImpl::getSerialNumber(X509* cert) {
std::string ContextImpl::getSerialNumber(const X509* cert) {
ASSERT(cert);
ASN1_INTEGER* serial_number = X509_get_serialNumber(cert);
ASN1_INTEGER* serial_number = X509_get_serialNumber(const_cast<X509*>(cert));
BIGNUM num_bn;
BN_init(&num_bn);
ASN1_INTEGER_to_BN(serial_number, &num_bn);
Expand Down Expand Up @@ -355,10 +355,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,
bool skip_context_update, Runtime::Loader& runtime)
: ContextImpl(parent, scope, config), listener_name_(listener_name),
server_names_(server_names), skip_context_update_(skip_context_update), 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 @@ -404,7 +414,7 @@ ServerContextImpl::ServerContextImpl(ContextManagerImpl& parent, Stats::Scope& s
int rc = EVP_DigestInit(&md, EVP_sha256());
RELEASE_ASSERT(rc == 1);

// Hash the CommonName/SANs in the server certificate. This makes sure that
// Hash the CommonName/SANs of the server certificate. This makes sure that
// sessions can only be resumed to a certificate for the same name, but allows
// resuming to unique certs in the case that different Envoy instances each have
// their own certs.
Expand Down Expand Up @@ -469,12 +479,78 @@ ServerContextImpl::ServerContextImpl(ContextManagerImpl& parent, Stats::Scope& s
RELEASE_ASSERT(rc == 1);
}

// Hash configured SNIs for this context, so that sessions cannot be resumed across different
// filter chains, even when using the same server certificate.
for (const auto& name : server_names_) {
rc = EVP_DigestUpdate(&md, name.data(), name.size());
RELEASE_ASSERT(rc == 1);
}

rc = EVP_DigestFinal(&md, session_context_buf, &session_context_len);
RELEASE_ASSERT(rc == 1);
rc = SSL_CTX_set_session_id_context(ctx_.get(), session_context_buf, session_context_len);
RELEASE_ASSERT(rc == 1);
}

ssl_select_cert_result_t
ServerContextImpl::processClientHello(const SSL_CLIENT_HELLO* client_hello) {
if (skip_context_update_) {
return ssl_select_cert_success;
}

std::string server_name;
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 instead of rejecting connection in case we cannot process the extension.
// TODO(PiotrSikora): figure out if we can upstream this to BoringSSL.
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.assign(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) {
stats_.fail_no_sni_match_.inc();
return ssl_select_cert_error;
}

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

return ssl_select_cert_success;
}

void ServerContextImpl::updateConnectionContext(SSL* ssl) {
ASSERT(ctx_);

SSL_set_SSL_CTX(ssl, ctx_.get());
ASSERT(SSL_CTX_get_ex_data(ctx_.get(), sslContextIndex()) == this);

// Update SSL-level settings and parameters that are inherited from SSL_CTX during SSL_new().
SSL_set_verify(ssl, SSL_CTX_get_verify_mode(ctx_.get()), SSL_CTX_get_verify_callback(ctx_.get()));

int rc = SSL_set1_curves_list(ssl, ecdh_curves_.c_str());
ASSERT(rc == 1);
UNREFERENCED_PARAMETER(rc);
}

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
34 changes: 22 additions & 12 deletions source/common/ssl/context_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace Ssl {
COUNTER(handshake) \
COUNTER(session_reused) \
COUNTER(no_certificate) \
COUNTER(fail_no_sni_match) \
COUNTER(fail_verify_no_cert) \
COUNTER(fail_verify_error) \
COUNTER(fail_verify_san) \
Expand All @@ -42,8 +43,6 @@ struct SslStats {

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

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

/**
Expand Down Expand Up @@ -72,9 +71,9 @@ class ContextImpl : public virtual Context {
SslStats& stats() { return stats_; }

// Ssl::Context
size_t daysUntilFirstCertExpires() override;
std::string getCaCertInformation() override;
std::string getCertChainInformation() override;
size_t daysUntilFirstCertExpires() const override;
std::string getCaCertInformation() const override;
std::string getCertChainInformation() const override;

protected:
ContextImpl(ContextManagerImpl& parent, Stats::Scope& scope, ContextConfig& config);
Expand Down Expand Up @@ -102,11 +101,11 @@ class ContextImpl : public virtual Context {

std::vector<uint8_t> parseAlpnProtocols(const std::string& alpn_protocols);
static SslStats generateStats(Stats::Scope& scope);
int32_t getDaysUntilExpiration(const X509* cert);
int32_t getDaysUntilExpiration(const X509* cert) const;
bssl::UniquePtr<X509> loadCert(const std::string& cert_file);
static std::string getSerialNumber(X509* cert);
std::string getCaFileName() { return ca_file_path_; };
std::string getCertChainFileName() { return cert_chain_file_path_; };
static std::string getSerialNumber(const X509* cert);
std::string getCaFileName() const { return ca_file_path_; };
std::string getCertChainFileName() const { return cert_chain_file_path_; };

ContextManagerImpl& parent_;
bssl::UniquePtr<SSL_CTX> ctx_;
Expand All @@ -119,11 +118,13 @@ class ContextImpl : public virtual Context {
bssl::UniquePtr<X509> cert_chain_;
std::string ca_file_path_;
std::string cert_chain_file_path_;
const std::string ecdh_curves_;
};

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 +134,28 @@ class ClientContextImpl : public ContextImpl, public ClientContext {

class ServerContextImpl : public ContextImpl, public ServerContext {
public:
ServerContextImpl(ContextManagerImpl& parent, Stats::Scope& scope, ServerContextConfig& config,
ServerContextImpl(ContextManagerImpl& parent, const std::string& listener_name,
const std::vector<std::string>& server_names, Stats::Scope& scope,
ServerContextConfig& config, bool skip_context_update,
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 updateConnectionContext(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_;
const bool skip_context_update_;
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
Loading

0 comments on commit c26b25e

Please sign in to comment.