shrpx: Verify backend server's certificate in client mode
The -k, --insecure option is added to skip this verification. The system wide trusted CA certificates will be loaded at startup. The --cacert option is added to specify the trusted CA certificate file.
This commit is contained in:
parent
8a5db1751e
commit
d589f4c74c
23
src/shrpx.cc
23
src/shrpx.cc
|
@ -356,6 +356,9 @@ void fill_default_config()
|
|||
mod_config()->client_proxy = false;
|
||||
mod_config()->client = false;
|
||||
mod_config()->client_mode = false;
|
||||
|
||||
mod_config()->insecure = false;
|
||||
mod_config()->cacert = 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
@ -465,6 +468,15 @@ void print_help(std::ostream& out)
|
|||
<< get_config()->backlog << "\n"
|
||||
<< " --ciphers=<SUITE> Set allowed cipher list. The format of the\n"
|
||||
<< " string is described in OpenSSL ciphers(1).\n"
|
||||
<< " -k, --insecure When used with -p or --client, don't verify\n"
|
||||
<< " backend server's certificate.\n"
|
||||
<< " --cacert=<PATH> When used with -p or --client, set path to\n"
|
||||
<< " trusted CA certificate file.\n"
|
||||
<< " The file must be in PEM format. It can\n"
|
||||
<< " contain multiple certificates. If the\n"
|
||||
<< " linked OpenSSL is configured to load system\n"
|
||||
<< " wide certificates, they are loaded\n"
|
||||
<< " at startup regardless of this option.\n"
|
||||
<< " -h, --help Print this help.\n"
|
||||
<< std::endl;
|
||||
}
|
||||
|
@ -482,6 +494,7 @@ int main(int argc, char **argv)
|
|||
static option long_options[] = {
|
||||
{"backend", required_argument, 0, 'b' },
|
||||
{"frontend", required_argument, 0, 'f' },
|
||||
{"insecure", no_argument, 0, 'k' },
|
||||
{"workers", required_argument, 0, 'n' },
|
||||
{"spdy-max-concurrent-streams", required_argument, 0, 'c' },
|
||||
{"log-level", required_argument, 0, 'L' },
|
||||
|
@ -506,11 +519,12 @@ int main(int argc, char **argv)
|
|||
{"ciphers", required_argument, &flag, 16 },
|
||||
{"client", no_argument, &flag, 17 },
|
||||
{"backend-spdy-window-bits", required_argument, &flag, 18 },
|
||||
{"cacert", required_argument, &flag, 19 },
|
||||
{"help", no_argument, 0, 'h' },
|
||||
{0, 0, 0, 0 }
|
||||
};
|
||||
int option_index = 0;
|
||||
int c = getopt_long(argc, argv, "DL:sb:c:f:n:hp", long_options,
|
||||
int c = getopt_long(argc, argv, "DL:ksb:c:f:n:hp", long_options,
|
||||
&option_index);
|
||||
if(c == -1) {
|
||||
break;
|
||||
|
@ -531,6 +545,9 @@ int main(int argc, char **argv)
|
|||
case 'f':
|
||||
cmdcfgs.push_back(std::make_pair(SHRPX_OPT_FRONTEND, optarg));
|
||||
break;
|
||||
case 'k':
|
||||
cmdcfgs.push_back(std::make_pair(SHRPX_OPT_INSECURE, "yes"));
|
||||
break;
|
||||
case 'n':
|
||||
cmdcfgs.push_back(std::make_pair(SHRPX_OPT_WORKERS, optarg));
|
||||
break;
|
||||
|
@ -626,6 +643,10 @@ int main(int argc, char **argv)
|
|||
cmdcfgs.push_back(std::make_pair(SHRPX_OPT_BACKEND_SPDY_WINDOW_BITS,
|
||||
optarg));
|
||||
break;
|
||||
case 19:
|
||||
// --cacert
|
||||
cmdcfgs.push_back(std::make_pair(SHRPX_OPT_CACERT, optarg));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -71,6 +71,8 @@ const char SHRPX_OPT_SYSLOG_FACILITY[] = "syslog-facility";
|
|||
const char SHRPX_OPT_BACKLOG[] = "backlog";
|
||||
const char SHRPX_OPT_CIPHERS[] = "ciphers";
|
||||
const char SHRPX_OPT_CLIENT[] = "client";
|
||||
const char SHRPX_OPT_INSECURE[] = "insecure";
|
||||
const char SHRPX_OPT_CACERT[] = "cacert";
|
||||
|
||||
Config::Config()
|
||||
: verbose(false),
|
||||
|
@ -103,7 +105,9 @@ Config::Config()
|
|||
backlog(0),
|
||||
ciphers(0),
|
||||
client(false),
|
||||
client_mode(false)
|
||||
client_mode(false),
|
||||
insecure(false),
|
||||
cacert(0)
|
||||
{}
|
||||
|
||||
namespace {
|
||||
|
@ -268,6 +272,10 @@ int parse_config(const char *opt, const char *optarg)
|
|||
set_config_str(&mod_config()->ciphers, optarg);
|
||||
} else if(util::strieq(opt, SHRPX_OPT_CLIENT)) {
|
||||
mod_config()->client = util::strieq(optarg, "yes");
|
||||
} else if(util::strieq(opt, SHRPX_OPT_INSECURE)) {
|
||||
mod_config()->insecure = util::strieq(optarg, "yes");
|
||||
} else if(util::strieq(opt, SHRPX_OPT_CACERT)) {
|
||||
set_config_str(&mod_config()->cacert, optarg);
|
||||
} else if(util::strieq(opt, "conf")) {
|
||||
LOG(WARNING) << "conf is ignored";
|
||||
} else {
|
||||
|
|
|
@ -63,6 +63,8 @@ extern const char SHRPX_OPT_SYSLOG_FACILITY[];
|
|||
extern const char SHRPX_OPT_BACKLOG[];
|
||||
extern const char SHRPX_OPT_CIPHERS[];
|
||||
extern const char SHRPX_OPT_CLIENT[];
|
||||
extern const char SHRPX_OPT_INSECURE[];
|
||||
extern const char SHRPX_OPT_CACERT[];
|
||||
|
||||
union sockaddr_union {
|
||||
sockaddr sa;
|
||||
|
@ -112,6 +114,8 @@ struct Config {
|
|||
bool client;
|
||||
// true if --client or --client-proxy are enabled.
|
||||
bool client_mode;
|
||||
bool insecure;
|
||||
char *cacert;
|
||||
Config();
|
||||
};
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
#include "shrpx_error.h"
|
||||
#include "shrpx_spdy_downstream_connection.h"
|
||||
#include "shrpx_client_handler.h"
|
||||
#include "shrpx_ssl.h"
|
||||
#include "util.h"
|
||||
|
||||
using namespace spdylay;
|
||||
|
@ -215,7 +216,8 @@ void eventcb(bufferevent *bev, short events, void *ptr)
|
|||
LOG(INFO) << "Downstream spdy connection established. " << spdy;
|
||||
}
|
||||
spdy->connected();
|
||||
if(spdy->on_connect() != 0) {
|
||||
if((!get_config()->insecure && spdy->check_cert() != 0) ||
|
||||
spdy->on_connect() != 0) {
|
||||
spdy->disconnect();
|
||||
return;
|
||||
}
|
||||
|
@ -233,6 +235,11 @@ void eventcb(bufferevent *bev, short events, void *ptr)
|
|||
}
|
||||
} // namespace
|
||||
|
||||
int SpdySession::check_cert()
|
||||
{
|
||||
return ssl::check_cert(ssl_);
|
||||
}
|
||||
|
||||
int SpdySession::initiate_connection()
|
||||
{
|
||||
int rv;
|
||||
|
|
|
@ -51,6 +51,8 @@ public:
|
|||
|
||||
int init_notification();
|
||||
|
||||
int check_cert();
|
||||
|
||||
int disconnect();
|
||||
int initiate_connection();
|
||||
void connected();
|
||||
|
|
200
src/shrpx_ssl.cc
200
src/shrpx_ssl.cc
|
@ -29,7 +29,12 @@
|
|||
#include <netinet/tcp.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include <openssl/crypto.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/x509v3.h>
|
||||
|
||||
#include <event2/bufferevent.h>
|
||||
#include <event2/bufferevent_ssl.h>
|
||||
|
@ -40,6 +45,9 @@
|
|||
#include "shrpx_client_handler.h"
|
||||
#include "shrpx_config.h"
|
||||
#include "shrpx_accesslog.h"
|
||||
#include "util.h"
|
||||
|
||||
using namespace spdylay;
|
||||
|
||||
namespace shrpx {
|
||||
|
||||
|
@ -182,6 +190,20 @@ SSL_CTX* create_ssl_client_context()
|
|||
SSL_CTX_set_mode(ssl_ctx, SSL_MODE_AUTO_RETRY);
|
||||
SSL_CTX_set_mode(ssl_ctx, SSL_MODE_RELEASE_BUFFERS);
|
||||
|
||||
if(SSL_CTX_set_default_verify_paths(ssl_ctx) != 1) {
|
||||
LOG(WARNING) << "Could not load system trusted ca certificates: "
|
||||
<< ERR_error_string(ERR_get_error(), NULL);
|
||||
}
|
||||
|
||||
if(get_config()->cacert) {
|
||||
if(SSL_CTX_load_verify_locations(ssl_ctx, get_config()->cacert, 0) != 1) {
|
||||
LOG(FATAL) << "Could not load trusted ca certificates from "
|
||||
<< get_config()->cacert << ": "
|
||||
<< ERR_error_string(ERR_get_error(), NULL);
|
||||
DIE();
|
||||
}
|
||||
}
|
||||
|
||||
SSL_CTX_set_next_proto_select_cb(ssl_ctx, select_next_proto_cb, 0);
|
||||
return ssl_ctx;
|
||||
}
|
||||
|
@ -228,6 +250,184 @@ ClientHandler* accept_ssl_connection(event_base *evbase, SSL_CTX *ssl_ctx,
|
|||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool numeric_host(const char *hostname)
|
||||
{
|
||||
struct addrinfo hints;
|
||||
struct addrinfo* res;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_flags = AI_NUMERICHOST;
|
||||
if(getaddrinfo(hostname, 0, &hints, &res)) {
|
||||
return false;
|
||||
}
|
||||
freeaddrinfo(res);
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
bool tls_hostname_match(const char *pattern, const char *hostname)
|
||||
{
|
||||
const char *ptWildcard = strchr(pattern, '*');
|
||||
if(ptWildcard == 0) {
|
||||
return util::strieq(pattern, hostname);
|
||||
}
|
||||
const char *ptLeftLabelEnd = strchr(pattern, '.');
|
||||
bool wildcardEnabled = true;
|
||||
// Do case-insensitive match. At least 2 dots are required to enable
|
||||
// wildcard match. Also wildcard must be in the left-most label.
|
||||
// Don't attempt to match a presented identifier where the wildcard
|
||||
// character is embedded within an A-label.
|
||||
if(ptLeftLabelEnd == 0 || strchr(ptLeftLabelEnd+1, '.') == 0 ||
|
||||
ptLeftLabelEnd < ptWildcard || util::istartsWith(pattern, "xn--")) {
|
||||
wildcardEnabled = false;
|
||||
}
|
||||
if(!wildcardEnabled) {
|
||||
return util::strieq(pattern, hostname);
|
||||
}
|
||||
const char *hnLeftLabelEnd = strchr(hostname, '.');
|
||||
if(hnLeftLabelEnd == 0 || !util::strieq(ptLeftLabelEnd, hnLeftLabelEnd)) {
|
||||
return false;
|
||||
}
|
||||
// Perform wildcard match. Here '*' must match at least one
|
||||
// character.
|
||||
if(hnLeftLabelEnd - hostname < ptLeftLabelEnd - pattern) {
|
||||
return false;
|
||||
}
|
||||
return util::istartsWith(hostname, hnLeftLabelEnd, pattern, ptWildcard) &&
|
||||
util::iendsWith(hostname, hnLeftLabelEnd, ptWildcard+1, ptLeftLabelEnd);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
int verify_hostname(const char *hostname,
|
||||
const sockaddr_union *su,
|
||||
size_t salen,
|
||||
const std::vector<std::string>& dns_names,
|
||||
const std::vector<std::string>& ip_addrs,
|
||||
const std::string& common_name)
|
||||
{
|
||||
if(numeric_host(hostname)) {
|
||||
if(ip_addrs.empty()) {
|
||||
return util::strieq(common_name.c_str(), hostname) ? 0 : -1;
|
||||
}
|
||||
const void *saddr;
|
||||
switch(su->storage.ss_family) {
|
||||
case AF_INET:
|
||||
saddr = &su->in.sin_addr;
|
||||
break;
|
||||
case AF_INET6:
|
||||
saddr = &su->in6.sin6_addr;
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
for(size_t i = 0; i < ip_addrs.size(); ++i) {
|
||||
if(salen == ip_addrs[i].size() &&
|
||||
memcmp(saddr, ip_addrs[i].c_str(), salen) == 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(dns_names.empty()) {
|
||||
return tls_hostname_match(common_name.c_str(), hostname) ? 0 : -1;
|
||||
}
|
||||
for(size_t i = 0; i < dns_names.size(); ++i) {
|
||||
if(tls_hostname_match(dns_names[i].c_str(), hostname)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int check_cert(SSL *ssl)
|
||||
{
|
||||
X509 *cert = SSL_get_peer_certificate(ssl);
|
||||
if(!cert) {
|
||||
LOG(ERROR) << "No certificate found";
|
||||
return -1;
|
||||
}
|
||||
util::auto_delete<X509*> cert_deleter(cert, X509_free);
|
||||
long verify_res = SSL_get_verify_result(ssl);
|
||||
if(verify_res != X509_V_OK) {
|
||||
LOG(ERROR) << "Certificate verification failed: "
|
||||
<< X509_verify_cert_error_string(verify_res);
|
||||
return -1;
|
||||
}
|
||||
std::string common_name;
|
||||
std::vector<std::string> dns_names;
|
||||
std::vector<std::string> ip_addrs;
|
||||
GENERAL_NAMES* altnames;
|
||||
altnames = reinterpret_cast<GENERAL_NAMES*>
|
||||
(X509_get_ext_d2i(cert, NID_subject_alt_name, 0, 0));
|
||||
if(altnames) {
|
||||
util::auto_delete<GENERAL_NAMES*> altnames_deleter(altnames,
|
||||
GENERAL_NAMES_free);
|
||||
size_t n = sk_GENERAL_NAME_num(altnames);
|
||||
for(size_t i = 0; i < n; ++i) {
|
||||
const GENERAL_NAME *altname = sk_GENERAL_NAME_value(altnames, i);
|
||||
if(altname->type == GEN_DNS) {
|
||||
const char *name;
|
||||
name = reinterpret_cast<char*>(ASN1_STRING_data(altname->d.ia5));
|
||||
if(!name) {
|
||||
continue;
|
||||
}
|
||||
size_t len = ASN1_STRING_length(altname->d.ia5);
|
||||
if(std::find(name, name+len, '\0') != name+len) {
|
||||
// Embedded NULL is not permitted.
|
||||
continue;
|
||||
}
|
||||
dns_names.push_back(std::string(name, len));
|
||||
} else if(altname->type == GEN_IPADD) {
|
||||
const unsigned char *ip_addr = altname->d.iPAddress->data;
|
||||
if(!ip_addr) {
|
||||
continue;
|
||||
}
|
||||
size_t len = altname->d.iPAddress->length;
|
||||
ip_addrs.push_back(std::string(reinterpret_cast<const char*>(ip_addr),
|
||||
len));
|
||||
}
|
||||
}
|
||||
}
|
||||
X509_NAME *subjectname = X509_get_subject_name(cert);
|
||||
if(!subjectname) {
|
||||
LOG(ERROR) << "Could not get X509 name object from the certificate.";
|
||||
return -1;
|
||||
}
|
||||
int lastpos = -1;
|
||||
while(1) {
|
||||
lastpos = X509_NAME_get_index_by_NID(subjectname, NID_commonName,
|
||||
lastpos);
|
||||
if(lastpos == -1) {
|
||||
break;
|
||||
}
|
||||
X509_NAME_ENTRY *entry = X509_NAME_get_entry(subjectname, lastpos);
|
||||
unsigned char *out;
|
||||
int outlen = ASN1_STRING_to_UTF8(&out, X509_NAME_ENTRY_get_data(entry));
|
||||
if(outlen < 0) {
|
||||
continue;
|
||||
}
|
||||
if(std::find(out, out+outlen, '\0') != out+outlen) {
|
||||
// Embedded NULL is not permitted.
|
||||
continue;
|
||||
}
|
||||
common_name.assign(&out[0], &out[outlen]);
|
||||
OPENSSL_free(out);
|
||||
break;
|
||||
}
|
||||
if(verify_hostname(get_config()->downstream_host,
|
||||
&get_config()->downstream_addr,
|
||||
get_config()->downstream_addrlen,
|
||||
dns_names, ip_addrs, common_name) != 0) {
|
||||
LOG(ERROR) << "Certificate verification failed: hostname does not match";
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
pthread_mutex_t *ssl_locks;
|
||||
} // namespace
|
||||
|
|
|
@ -46,6 +46,8 @@ ClientHandler* accept_ssl_connection(event_base *evbase, SSL_CTX *ssl_ctx,
|
|||
evutil_socket_t fd,
|
||||
sockaddr *addr, int addrlen);
|
||||
|
||||
int check_cert(SSL *ssl);
|
||||
|
||||
void setup_ssl_lock();
|
||||
|
||||
void teardown_ssl_lock();
|
||||
|
|
56
src/util.h
56
src/util.h
|
@ -36,6 +36,47 @@ namespace spdylay {
|
|||
|
||||
namespace util {
|
||||
|
||||
template<typename T>
|
||||
class auto_delete {
|
||||
private:
|
||||
T obj_;
|
||||
void (*deleter_)(T);
|
||||
public:
|
||||
auto_delete(T obj, void (*deleter)(T)):obj_(obj), deleter_(deleter) {}
|
||||
|
||||
~auto_delete()
|
||||
{
|
||||
deleter_(obj_);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class auto_delete_d {
|
||||
private:
|
||||
T obj_;
|
||||
public:
|
||||
auto_delete_d(T obj):obj_(obj) {}
|
||||
|
||||
~auto_delete_d()
|
||||
{
|
||||
delete obj_;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, typename R>
|
||||
class auto_delete_r {
|
||||
private:
|
||||
T obj_;
|
||||
R (*deleter_)(T);
|
||||
public:
|
||||
auto_delete_r(T obj, R (*deleter)(T)):obj_(obj), deleter_(deleter) {}
|
||||
|
||||
~auto_delete_r()
|
||||
{
|
||||
(void)deleter_(obj_);
|
||||
}
|
||||
};
|
||||
|
||||
extern const std::string DEFAULT_STRIP_CHARSET;
|
||||
|
||||
template<typename InputIterator>
|
||||
|
@ -233,8 +274,23 @@ bool endsWith
|
|||
return std::equal(first2, last2, last1-(last2-first2));
|
||||
}
|
||||
|
||||
template<typename InputIterator1, typename InputIterator2>
|
||||
bool iendsWith
|
||||
(InputIterator1 first1,
|
||||
InputIterator1 last1,
|
||||
InputIterator2 first2,
|
||||
InputIterator2 last2)
|
||||
{
|
||||
if(last1-first1 < last2-first2) {
|
||||
return false;
|
||||
}
|
||||
return std::equal(first2, last2, last1-(last2-first2), CaseCmp());
|
||||
}
|
||||
|
||||
bool endsWith(const std::string& a, const std::string& b);
|
||||
|
||||
bool strieq(const std::string& a, const std::string& b);
|
||||
|
||||
bool strieq(const char *a, const char *b);
|
||||
|
||||
bool strifind(const char *a, const char *b);
|
||||
|
|
Loading…
Reference in New Issue