0
0
mirror of https://github.com/OpenVPN/openvpn3.git synced 2024-09-20 12:12:15 +02:00

OpenSSL: added SNI implementation

On the server side, we add the abstract base class
SNIHandlerBase to provide a hook (sni_hello) where
servers can inspect the SNI name given in the client
hello message and possibly return a different SSLFactoryAPI.

In other changes, we rename the ENABLE_SNI flag to
ENABLE_CLIENT_SNI to be clear that this flag only affects
the client-side SNI implementation.

We also add the NO_VERIFY_HOSTNAME flag on the client side
to allow the SNI name to be transmitted to the server
without requiring a match between the SNI name and the
common name or subject alternative name in the server
certificate.

Signed-off-by: James Yonan <james@openvpn.net>
This commit is contained in:
James Yonan 2019-05-22 00:23:06 -06:00 committed by Antonio Quartulli
parent 5def1d23ab
commit bbae814864
No known key found for this signature in database
GPG Key ID: F4556C5945830E6D
7 changed files with 308 additions and 24 deletions

View File

@ -58,6 +58,7 @@ namespace openvpn {
OTHER,
BAD_CERT_TYPE,
EXPIRED,
SNI_ERROR,
N
};
@ -124,6 +125,8 @@ namespace openvpn {
return "BAD_CERT_TYPE";
case EXPIRED:
return "EXPIRED";
case SNI_ERROR:
return "SNI_ERROR";
}
}

View File

@ -248,6 +248,12 @@ namespace openvpn {
throw MbedTLSException("set_client_session_tickets not implemented");
}
virtual void set_sni_handler(SNIHandlerBase* sni_handler)
{
// fixme -- this method should be implemented on the server-side for SNI
throw MbedTLSException("set_sni_handler not implemented");
}
virtual void set_private_key_password(const std::string& pwd)
{
priv_key_pwd = pwd;
@ -543,6 +549,13 @@ namespace openvpn {
}
}
#ifdef HAVE_JSON
virtual SSLConfigAPI::Ptr json_override(const Json::Value& root) const
{
throw MbedTLSException("json_override not implemented");
}
#endif
bool name_constraints_allowed() const
{
return allow_name_constraints;
@ -825,7 +838,7 @@ namespace openvpn {
// In pre-mbedtls-2.x the hostname for the CA chain was set in ssl_set_ca_chain().
// From mbedtls-2.x, the hostname must be set via mbedtls_ssl_set_hostname()
// https://tls.mbed.org/kb/how-to/upgrade-2.0
if (hostname && ((c.flags & SSLConst::ENABLE_SNI) || c.ca_chain))
if (hostname && ((c.flags & SSLConst::ENABLE_CLIENT_SNI) || c.ca_chain))
{
if (mbedtls_ssl_set_hostname(ssl, hostname))
throw MbedTLSException("mbedtls_ssl_set_hostname failed");

View File

@ -44,6 +44,7 @@
#include <openvpn/common/uniqueptr.hpp>
#include <openvpn/common/hexstr.hpp>
#include <openvpn/common/to_string.hpp>
#include <openvpn/common/unicode.hpp>
#include <openvpn/frame/frame.hpp>
#include <openvpn/buffer/buffer.hpp>
#include <openvpn/pki/cclist.hpp>
@ -55,6 +56,7 @@
#include <openvpn/ssl/sslconsts.hpp>
#include <openvpn/ssl/sslapi.hpp>
#include <openvpn/ssl/ssllog.hpp>
#include <openvpn/ssl/sni_handler.hpp>
#include <openvpn/openssl/util/error.hpp>
#include <openvpn/openssl/pki/x509.hpp>
#include <openvpn/openssl/pki/crl.hpp>
@ -64,6 +66,10 @@
#include <openvpn/openssl/bio/bio_memq_stream.hpp>
#include <openvpn/openssl/ssl/sess_cache.hpp>
#ifdef HAVE_JSON
#include <openvpn/common/jsonhelper.hpp>
#endif
// An SSL Context is essentially a configuration that can be used
// to generate an arbitrary number of actual SSL connections objects.
@ -125,6 +131,12 @@ namespace openvpn {
client_session_tickets = v;
}
// server side
virtual void set_sni_handler(SNIHandlerBase* sni_handler_arg)
{
sni_handler = sni_handler_arg;
}
virtual void set_private_key_password(const std::string& pwd)
{
pkey.set_private_key_password(pwd);
@ -400,6 +412,61 @@ namespace openvpn {
}
}
#ifdef HAVE_JSON
virtual SSLConfigAPI::Ptr json_override(const Json::Value& root) const override
{
static const char title[] = "json_override";
Config::Ptr ret(new Config);
// inherit from self
ret->mode = mode;
ret->extra_certs = extra_certs;
ret->dh = dh;
ret->frame = frame;
ret->ssl_debug_level = ssl_debug_level;
ret->flags = flags;
ret->ns_cert_type = ns_cert_type;
ret->ku = ku;
ret->eku = eku;
ret->tls_version_min = tls_version_min;
ret->tls_cert_profile = tls_cert_profile;
ret->local_cert_enabled = local_cert_enabled;
// ca
{
const std::string& ca_txt = json::get_string_ref(root, "ca", title);
ret->load_ca(ca_txt, true);
}
// CRL
{
const std::string crl_txt = json::get_string_optional(root, "crl_verify", std::string(), title);
if (!crl_txt.empty())
ret->load_crl(crl_txt);
}
// local cert/key
if (local_cert_enabled)
{
// cert
{
const std::string& cert_txt = json::get_string_ref(root, "cert", title);
ret->load_cert(cert_txt);
}
// private key
if (!external_pki)
{
const std::string& key_txt = json::get_string_ref(root, "key", title);
ret->load_private_key(key_txt);
}
}
return ret;
}
#endif
private:
Mode mode;
CertCRLList ca; // from OpenVPN "ca" and "crl-verify" option
@ -409,6 +476,7 @@ namespace openvpn {
OpenSSLPKI::DH dh; // diffie-hellman parameters (only needed in server mode)
ExternalPKIBase* external_pki = nullptr;
TLSSessionTicketBase* session_ticket_handler = nullptr; // server side only
SNIHandlerBase* sni_handler = nullptr; // server side only
Frame::Ptr frame;
int ssl_debug_level = 0;
unsigned int flags = 0; // defined in sslconsts.hpp
@ -549,7 +617,7 @@ namespace openvpn {
{
bmq_stream::init_static();
mydata_index = SSL_get_ex_new_index(0, (char *)"OpenSSLContext::SSL", nullptr, nullptr, nullptr);
ssl_data_index = SSL_get_ex_new_index(0, (char *)"OpenSSLContext::SSL", nullptr, nullptr, nullptr);
context_data_index = SSL_get_ex_new_index(0, (char *)"OpenSSLContext", nullptr, nullptr, nullptr);
/*
@ -592,7 +660,7 @@ namespace openvpn {
SSL_set_mode(ssl, SSL_MODE_RELEASE_BUFFERS);
// verify hostname
if (hostname)
if (hostname && !(ctx.config->flags & SSLConst::NO_VERIFY_HOSTNAME))
{
X509_VERIFY_PARAM *param = SSL_get0_param(ssl);
X509_VERIFY_PARAM_set_hostflags(param, 0);
@ -628,8 +696,10 @@ namespace openvpn {
sess_cache_key.reset(new OpenSSLSessionCache::Key(*cache_key, ctx.sess_cache));
}
SSL_set_connect_state(ssl);
if (ctx.config->flags & SSLConst::ENABLE_SNI)
if (SSL_set_tlsext_host_name(ssl, hostname) != 1)
// client-side SNI
if ((ctx.config->flags & SSLConst::ENABLE_CLIENT_SNI) && hostname)
if (SSL_set_tlsext_host_name(ssl, hostname->c_str()) != 1)
throw OpenSSLException("OpenSSLContext::SSL: SSL_set_tlsext_host_name failed");
}
else
@ -640,12 +710,10 @@ namespace openvpn {
SSL_set_bio (ssl, ct_in, ct_out);
BIO_set_ssl (ssl_bio, ssl, BIO_NOCLOSE);
if (mydata_index < 0)
throw ssl_context_error("OpenSSLContext::SSL: mydata_index is uninitialized");
if (context_data_index < 0)
throw ssl_context_error("OpenSSLContext::SSL: context_data_index is uninitialized");
SSL_set_ex_data (ssl, mydata_index, this);
SSL_set_ex_data (ssl, context_data_index, (void*) &ctx);
if (ssl_data_index < 0)
throw ssl_context_error("OpenSSLContext::SSL: ssl_data_index is uninitialized");
SSL_set_ex_data (ssl, ssl_data_index, this);
set_parent(&ctx);
}
catch (...)
{
@ -654,6 +722,13 @@ namespace openvpn {
}
}
void set_parent(const OpenSSLContext* ctx)
{
if (context_data_index < 0)
throw ssl_context_error("OpenSSLContext::SSL: context_data_index is uninitialized");
SSL_set_ex_data(ssl, context_data_index, (void *)ctx);
}
void rebuild_authcert()
{
authcert.reset(new AuthCert());
@ -796,12 +871,13 @@ namespace openvpn {
BIO *ct_out; // read ciphertext from here
AuthCert::Ptr authcert;
OpenSSLSessionCache::Key::UPtr sess_cache_key; // client-side only
OpenSSLContext::Ptr sni_ctx;
bool ssl_bio_linkage;
bool overflow;
bool called_did_full_handshake;
// Helps us to store pointer to self in ::SSL object
static int mydata_index;
static int ssl_data_index;
static int context_data_index;
#if OPENSSL_VERSION_NUMBER < 0x10100000L
@ -1007,6 +1083,16 @@ namespace openvpn {
throw OpenSSLException("OpenSSLContext: SSL_CTX_set_tmp_dh failed");
if (config->flags & SSLConst::SERVER_TO_SERVER)
SSL_CTX_set_purpose(ctx, X509_PURPOSE_SSL_SERVER);
// server-side SNI
if (config->sni_handler)
{
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
SSL_CTX_set_client_hello_cb(ctx, client_hello_callback, nullptr);
#else
OPENVPN_THROW(ssl_context_error, "OpenSSLContext: ENABLE_SERVER_SNI requires OpenSSL 1.1 or higher");
#endif
}
}
else if (config->mode.is_client())
{
@ -1599,7 +1685,7 @@ namespace openvpn {
const OpenSSLContext* self = (OpenSSLContext*) SSL_get_ex_data (ssl, SSL::context_data_index);
// get OpenSSLContext::SSL
SSL* self_ssl = (SSL *) SSL_get_ex_data (ssl, SSL::mydata_index);
SSL* self_ssl = (SSL *) SSL_get_ex_data (ssl, SSL::ssl_data_index);
// get error code
const int err = X509_STORE_CTX_get_error(ctx);
@ -1676,9 +1762,7 @@ namespace openvpn {
self->config->x509_track_config,
*self_ssl->authcert->x509_track);
return preverify_ok || ((self->config->flags & SSLConst::DEFERRED_CERT_VERIFY)
&& self_ssl->authcert // failsafe: don't defer error unless
&& self_ssl->authcert->is_fail()); // authcert has recorded it
return preverify_ok || self->deferred_cert_verify_failsafe(*self_ssl);
}
// Print debugging information on SSL/TLS session negotiation.
@ -1804,6 +1888,132 @@ namespace openvpn {
return true;
}
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
static int client_hello_callback(::SSL *s, int *al, void *)
{
std::string sni_name;
// get OpenSSLContext
OpenSSLContext* self = (OpenSSLContext*) SSL_get_ex_data(s, SSL::context_data_index);
// get OpenSSLContext::SSL
SSL* self_ssl = (SSL *) SSL_get_ex_data(s, SSL::ssl_data_index);
try {
// get the SNI from the client hello
sni_name = client_hello_get_sni(s);
// process the SNI name, if provided
if (!sni_name.empty())
{
// ignore the SNI if no handler was provided
if (self->config->sni_handler)
{
// get an alternative SSLFactoryAPI from the sni_handler
SSLFactoryAPI::Ptr fapi;
try {
fapi = self->config->sni_handler->sni_hello(sni_name, self->config);
}
catch (const std::exception& e)
{
OPENVPN_LOG("SNI HANDLER ERROR: " << e.what());
return sni_error(e.what(), SSL_AD_INTERNAL_ERROR, self, self_ssl, al);
}
if (!fapi)
return sni_error("SNI name not found", SSL_AD_UNRECOGNIZED_NAME, self, self_ssl, al);
// make sure that returned SSLFactoryAPI is an OpenSSLContext
self_ssl->sni_ctx = fapi.dynamic_pointer_cast<OpenSSLContext>();
if (!self_ssl->sni_ctx)
throw Exception("sni_handler returned wrong kind of SSLFactoryAPI");
// don't modify SSL CTX if the returned SSLFactoryAPI is ourself
if (fapi.get() != self)
{
SSL_set_SSL_CTX(s, self_ssl->sni_ctx->ctx);
self_ssl->set_parent(self_ssl->sni_ctx.get());
}
}
}
return SSL_CLIENT_HELLO_SUCCESS;
}
catch (const std::exception& e)
{
OPENVPN_LOG("SNI exception in OpenSSLContext, SNI=" << sni_name << " : " << e.what());
*al = SSL_AD_INTERNAL_ERROR;
return SSL_CLIENT_HELLO_ERROR;
}
}
static int sni_error(std::string err,
const int ssl_ad_error,
OpenSSLContext* self,
SSL* self_ssl,
int* al)
{
if (self_ssl->authcert)
self_ssl->authcert->add_fail(0, AuthCert::Fail::SNI_ERROR, std::move(err));
if (self->deferred_cert_verify_failsafe(*self_ssl))
return SSL_CLIENT_HELLO_SUCCESS;
*al = ssl_ad_error;
return SSL_CLIENT_HELLO_ERROR;
}
static size_t sni_get_len(ConstBuffer& buf)
{
size_t ret = buf.pop_front() << 8;
ret += buf.pop_front();
return ret;
}
static std::string client_hello_get_sni(::SSL* s)
{
const unsigned char *p;
size_t remaining;
if (!SSL_client_hello_get0_ext(s, TLSEXT_TYPE_server_name, &p, &remaining))
return std::string();
// For safety, map a ConstBuffer onto returned OpenSSL TLSEXT_TYPE_server_name data.
ConstBuffer buf(p, remaining, true);
// Extract the length of the supplied list of names,
// and check that it matches size of remaining data
// in buf.
{
const size_t len = sni_get_len(buf);
if (len != buf.size())
throw Exception("bad name list size");
}
// Next byte must be TLSEXT_NAMETYPE_host_name.
if (buf.pop_front() != TLSEXT_NAMETYPE_host_name)
throw Exception("expecting TLSEXT_NAMETYPE_host_name");
// Now try to extract the SNI name.
{
const size_t len = sni_get_len(buf);
if (len > buf.size())
throw Exception("bad name size");
if (!Unicode::is_valid_utf8_uchar_buf(buf.c_data(), len, 1024 | Unicode::UTF8_NO_CTRL))
throw Exception("invalid UTF-8");
return std::string((const char *)buf.c_data(), len);
}
}
#endif
// Return true if we should continue with authentication
// even though there was an error, because the user has
// enabled SSLConst::DEFERRED_CERT_VERIFY and wants the
// error to be logged in authcert so that it can be handled
// by a higher layer.
bool deferred_cert_verify_failsafe(const SSL& ssl) const
{
return (config->flags & SSLConst::DEFERRED_CERT_VERIFY)
&& ssl.authcert // failsafe: don't defer error unless
&& ssl.authcert->is_fail(); // authcert has recorded it
}
void erase()
{
if (epki)
@ -1824,7 +2034,7 @@ namespace openvpn {
OpenSSLSessionCache::Ptr sess_cache; // client-side only
};
int OpenSSLContext::SSL::mydata_index = -1;
int OpenSSLContext::SSL::ssl_data_index = -1;
int OpenSSLContext::SSL::context_data_index = -1;
#if OPENSSL_VERSION_NUMBER < 0x10100000L

View File

@ -0,0 +1,45 @@
// OpenVPN -- An application to securely tunnel IP networks
// over a single port, with support for SSL/TLS-based
// session authentication and key exchange,
// packet encryption, packet authentication, and
// packet compression.
//
// Copyright (C) 2012-2019 OpenVPN Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License Version 3
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program in the COPYING file.
// If not, see <http://www.gnu.org/licenses/>.
#pragma once
#include <string>
#include <memory>
#include <openvpn/ssl/sslapi.hpp>
namespace openvpn {
// Abstract base class used to provide an SNI handler
//
class SNIHandlerBase
{
public:
// Return a new SSLFactoryAPI for this SNI name.
// Return SSLFactoryAPI::Ptr() if sni_name is not recognized.
// The caller guarantees that sni_name is valid UTF-8 and
// doesn't contain any control characters.
virtual SSLFactoryAPI::Ptr sni_hello(const std::string& sni_name, SSLConfigAPI::Ptr default_factory) const = 0;
virtual ~SNIHandlerBase() {}
};
}

View File

@ -32,6 +32,7 @@
#include <openvpn/common/rc.hpp>
#include <openvpn/common/options.hpp>
#include <openvpn/common/mode.hpp>
#include <openvpn/common/jsonlib.hpp>
#include <openvpn/buffer/buffer.hpp>
#include <openvpn/frame/frame.hpp>
#include <openvpn/auth/authcert.hpp>
@ -46,6 +47,8 @@
namespace openvpn {
class SNIHandlerBase;
class SSLAPI : public RC<thread_unsafe_refcount>
{
public:
@ -146,6 +149,7 @@ namespace openvpn {
virtual void set_external_pki_callback(ExternalPKIBase* external_pki_arg) = 0; // private key alternative
virtual void set_session_ticket_handler(TLSSessionTicketBase* session_ticket_handler) = 0; // server side
virtual void set_client_session_tickets(const bool v) = 0; // client side
virtual void set_sni_handler(SNIHandlerBase* sni_handler) = 0; // server side
virtual void set_private_key_password(const std::string& pwd) = 0;
virtual void load_ca(const std::string& ca_txt, bool strict) = 0;
virtual void load_crl(const std::string& crl_txt) = 0;
@ -177,6 +181,10 @@ namespace openvpn {
virtual void set_rng(const RandomAPI::Ptr& rng_arg) = 0;
virtual void load(const OptionList& opt, const unsigned int lflags) = 0;
#ifdef HAVE_JSON
virtual SSLConfigAPI::Ptr json_override(const Json::Value& root) const = 0;
#endif
virtual std::string validate_cert(const std::string& cert_txt) const = 0;
virtual std::string validate_cert_list(const std::string& certs_txt) const = 0;
virtual std::string validate_crl(const std::string& crl_txt) const = 0;

View File

@ -44,25 +44,30 @@ namespace openvpn {
// Disable peer verification
NO_VERIFY_PEER=(1<<1),
// Enable SNI (Server Name Indication) when hostname is provided
ENABLE_SNI=(1<<2),
// [client only] Enable client-side SNI (Server Name Indication)
// when hostname is provided
ENABLE_CLIENT_SNI=(1<<2),
// [client only] Don't require that the hostname matches
// the common name in the certificate.
NO_VERIFY_HOSTNAME=(1<<3),
// [server only] Don't automatically fail connections on
// bad peer cert. Succeed the connection, but pass the
// fail status data via AuthCert so the higher layers
// can handle it.
DEFERRED_CERT_VERIFY=(1<<3),
DEFERRED_CERT_VERIFY=(1<<4),
// [server only] When running as a server, require that
// clients that connect to us have their certificate
// purpose set to server.
SERVER_TO_SERVER=(1<<4),
SERVER_TO_SERVER=(1<<5),
// Peer certificate is optional
PEER_CERT_OPTIONAL=(1<<5),
PEER_CERT_OPTIONAL=(1<<6),
// last flag marker
LAST=(1<<6),
LAST=(1<<7)
};
// filter all but SSL flags

View File

@ -315,7 +315,7 @@ namespace openvpn {
ssl_conf.reset(new SSLLib::SSLAPI::Config());
ssl_conf->set_mode(Mode(Mode::CLIENT));
ssl_conf->set_flags(SSLConst::LOG_VERIFY_STATUS|SSLConst::NO_VERIFY_PEER|
SSLConst::ENABLE_SNI);
SSLConst::ENABLE_CLIENT_SNI);
ssl_conf->set_local_cert_enabled(false);
ssl_conf->set_frame(config->frame);
ssl_conf->set_rng(new SSLLib::RandomAPI(false));