nghttpx: Add TLS signed_certificate_timestamp extension support

This commit is contained in:
Tatsuhiro Tsujikawa 2016-10-08 19:03:21 +09:00
parent 2795da840c
commit 412c8f9e67
7 changed files with 330 additions and 27 deletions

View File

@ -209,6 +209,24 @@ from the given file. In this case, nghttpx does not rotate key
automatically. To rotate key, one has to restart nghttpx (see
SIGNALS).
CERTIFICATE TRANSPARENCY
------------------------
nghttpx supports TLS ``signed_certificate_timestamp`` extension (`RFC
6962 <https://tools.ietf.org/html/rfc6962>`_). The relevant options
are :option:`--tls-sct-dir` and ``sct-dir`` parameter in
:option:`--subcert`. They takes a directory, and nghttpx reads all
files whose extension is ``.sct`` under the directory. The ``*.sct``
files are encoded as ``SignedCertificateTimestamp`` struct described
in `section 3.2 of RFC 69662
<https://tools.ietf.org/html/rfc6962#section-3.2>`_. This format is
the same one used by `nginx-ct
<https://github.com/grahamedgecombe/nginx-ct>`_ and `mod_ssl_ct
<https://httpd.apache.org/docs/trunk/mod/mod_ssl_ct.html>`_.
`ct-submit <https://github.com/grahamedgecombe/ct-submit>`_ can be
used to submit certificates to log servers, and obtain the
``SignedCertificateTimestamp`` struct which can be used with nghttpx.
MRUBY SCRIPTING
---------------

View File

@ -147,6 +147,7 @@ OPTIONS = [
"backend-http2-encoder-dynamic-table-size",
"backend-http2-decoder-dynamic-table-size",
"ecdh-curves",
"tls-sct-dir",
]
LOGVARS = [

View File

@ -1843,12 +1843,21 @@ SSL/TLS:
Path to file that contains password for the server's
private key. If none is given and the private key is
password protected it'll be requested interactively.
--subcert=<KEYPATH>:<CERTPATH>
--subcert=<KEYPATH>:<CERTPATH>[[;<PARAM>]...]
Specify additional certificate and private key file.
nghttpx will choose certificates based on the hostname
indicated by client using TLS SNI extension. This
option can be used multiple times. To make OCSP
stapling work, <CERTPATH> must be absolute path.
Additional parameter can be specified in <PARAM>. The
available <PARAM> is "sct-dir=<DIR>".
"sct-dir=<DIR>" specifies the path to directory which
contains *.sct files for TLS
signed_certificate_timestamp extension (RFC 6962). This
feature requires OpenSSL >= 1.0.2. See also
--tls-sct-dir option.
--dh-param-file=<PATH>
Path to file that contains DH parameters in PEM format.
Without this option, DHE cipher suites are not
@ -2004,6 +2013,15 @@ SSL/TLS:
Allow black listed cipher suite on HTTP/2 connection.
See https://tools.ietf.org/html/rfc7540#appendix-A for
the complete HTTP/2 cipher suites black list.
--tls-sct-dir=<DIR>
Specifies the directory where *.sct files exist. All
*.sct files in <DIR> are read, and sent as
extension_data of TLS signed_certificate_timestamp (RFC
6962) to client. These *.sct files are for the
certificate specified in positional command-line
argument <CERT>, or certificate option in configuration
file. For additional certificates, use --subcert
option. This option requires OpenSSL >= 1.0.2.
HTTP/2 and SPDY:
-c, --frontend-http2-max-concurrent-streams=<N>
@ -2937,6 +2955,7 @@ int main(int argc, char **argv) {
{SHRPX_OPT_BACKEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE.c_str(),
required_argument, &flag, 139},
{SHRPX_OPT_ECDH_CURVES.c_str(), required_argument, &flag, 140},
{SHRPX_OPT_TLS_SCT_DIR.c_str(), required_argument, &flag, 141},
{nullptr, 0, nullptr, 0}};
int option_index = 0;
@ -3601,6 +3620,10 @@ int main(int argc, char **argv) {
// --ecdh-curves
cmdcfgs.emplace_back(SHRPX_OPT_ECDH_CURVES, StringRef{optarg});
break;
case 141:
// --tls-sct-dir
cmdcfgs.emplace_back(SHRPX_OPT_TLS_SCT_DIR, StringRef{optarg});
break;
default:
break;
}

View File

@ -41,6 +41,7 @@
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif // HAVE_UNISTD_H
#include <dirent.h>
#include <cstring>
#include <cerrno>
@ -1016,6 +1017,162 @@ int parse_error_page(std::vector<ErrorPage> &error_pages, const StringRef &opt,
}
} // namespace
namespace {
// Maximum size of SCT extension payload length.
constexpr size_t MAX_SCT_EXT_LEN = 16_k;
} // namespace
struct SubcertParams {
StringRef sct_dir;
};
namespace {
// Parses subcert parameter |src_params|, and stores parsed results
// into |out|. This function returns 0 if it succeeds, or -1.
int parse_subcert_params(SubcertParams &out, const StringRef &src_params) {
auto last = std::end(src_params);
for (auto first = std::begin(src_params); first != last;) {
auto end = std::find(first, last, ';');
auto param = StringRef{first, end};
if (util::istarts_with_l(param, "sct-dir=")) {
#if !LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L
auto sct_dir =
StringRef{std::begin(param) + str_size("sct-dir="), std::end(param)};
if (sct_dir.empty()) {
LOG(ERROR) << "subcert: " << param << ": empty sct-dir";
return -1;
}
out.sct_dir = sct_dir;
#else // !(!LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L)
LOG(WARN) << "subcert: sct-dir requires OpenSSL >= 1.0.2";
#endif // !(!LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L)
} else if (!param.empty()) {
LOG(ERROR) << "subcert: " << param << ": unknown keyword";
return -1;
}
if (end == last) {
break;
}
first = end + 1;
}
return 0;
}
} // namespace
namespace {
// Reads *.sct files from directory denoted by |dir_path|. |dir_path|
// must be NULL-terminated string.
int read_tls_sct_from_dir(std::vector<uint8_t> &dst, const StringRef &opt,
const StringRef &dir_path) {
auto dir = opendir(dir_path.c_str());
if (dir == nullptr) {
auto error = errno;
LOG(ERROR) << opt << ": " << dir_path << ": " << strerror(error);
return -1;
}
auto closer = defer(closedir, dir);
// 2 bytes total length field
auto len_idx = std::distance(std::begin(dst), std::end(dst));
dst.insert(std::end(dst), 2, 0);
for (;;) {
errno = 0;
auto ent = readdir(dir);
if (ent == nullptr) {
if (errno != 0) {
auto error = errno;
LOG(ERROR) << opt << ": failed to read directory " << dir_path << ": "
<< strerror(error);
return -1;
}
break;
}
auto name = StringRef{ent->d_name};
if (name[0] == '.' || !util::iends_with_l(name, ".sct")) {
continue;
}
std::string path;
path.resize(dir_path.size() + 1 + name.size());
{
auto p = std::begin(path);
p = std::copy(std::begin(dir_path), std::end(dir_path), p);
*p++ = '/';
std::copy(std::begin(name), std::end(name), p);
}
auto fd = open(path.c_str(), O_RDONLY);
if (fd == -1) {
auto error = errno;
LOG(ERROR) << opt << ": failed to read SCT from " << path << ": "
<< strerror(error);
return -1;
}
// 2 bytes length field for this SCT.
auto len_idx = std::distance(std::begin(dst), std::end(dst));
dst.insert(std::end(dst), 2, 0);
// *.sct file tends to be small; around 110+ bytes.
std::array<char, 256> buf;
for (;;) {
ssize_t nread;
while ((nread = read(fd, buf.data(), buf.size())) == -1 && errno == EINTR)
;
if (nread == -1) {
auto error = errno;
LOG(ERROR) << opt << ": failed to read SCT data from " << path << ": "
<< strerror(error);
return -1;
}
if (nread == 0) {
break;
}
dst.insert(std::end(dst), std::begin(buf), std::begin(buf) + nread);
if (dst.size() > MAX_SCT_EXT_LEN) {
LOG(ERROR) << opt << ": the concatenated SCT data from " << dir_path
<< " is too large. Max " << MAX_SCT_EXT_LEN;
return -1;
}
}
auto len = dst.size() - len_idx - 2;
if (len == 0) {
dst.resize(dst.size() - 2);
continue;
}
dst[len_idx] = len >> 8;
dst[len_idx + 1] = len;
}
auto len = dst.size() - len_idx - 2;
if (len == 0) {
dst.resize(dst.size() - 2);
return 0;
}
dst[len_idx] = len >> 8;
dst[len_idx + 1] = len;
return 0;
}
} // namespace
// generated by gennghttpxfun.py
int option_lookup_token(const char *name, size_t namelen) {
switch (namelen) {
@ -1171,6 +1328,11 @@ int option_lookup_token(const char *name, size_t namelen) {
return SHRPX_OPTID_SERVER_NAME;
}
break;
case 'r':
if (util::strieq_l("tls-sct-di", name, 10)) {
return SHRPX_OPTID_TLS_SCT_DIR;
}
break;
case 's':
if (util::strieq_l("backend-tl", name, 10)) {
return SHRPX_OPTID_BACKEND_TLS;
@ -2165,30 +2327,51 @@ int parse_config(Config *config, int optid, const StringRef &opt,
return 0;
case SHRPX_OPTID_SUBCERT: {
auto end_keys = std::find(std::begin(optarg), std::end(optarg), ';');
auto src_params = StringRef{end_keys, std::end(optarg)};
SubcertParams params;
if (parse_subcert_params(params, src_params) != 0) {
return -1;
}
std::vector<uint8_t> sct_data;
if (!params.sct_dir.empty()) {
// Make sure that dir_path is NULL terminated string.
if (read_tls_sct_from_dir(sct_data, opt,
StringRef{params.sct_dir.str()}) != 0) {
return -1;
}
}
// Private Key file and certificate file separated by ':'.
auto sp = std::find(std::begin(optarg), std::end(optarg), ':');
if (sp == std::end(optarg)) {
LOG(ERROR) << opt << ": missing ':' in " << optarg;
auto sp = std::find(std::begin(optarg), end_keys, ':');
if (sp == end_keys) {
LOG(ERROR) << opt << ": missing ':' in "
<< StringRef{std::begin(optarg), end_keys};
return -1;
}
auto private_key_file = StringRef{std::begin(optarg), sp};
if (private_key_file.empty()) {
LOG(ERROR) << opt << ": missing private key file: " << optarg;
LOG(ERROR) << opt << ": missing private key file: "
<< StringRef{std::begin(optarg), end_keys};
return -1;
}
auto cert_file = StringRef{sp + 1, std::end(optarg)};
auto cert_file = StringRef{sp + 1, end_keys};
if (cert_file.empty()) {
LOG(ERROR) << opt << ": missing certificate file: " << optarg;
LOG(ERROR) << opt << ": missing certificate file: "
<< StringRef{std::begin(optarg), end_keys};
return -1;
}
config->tls.subcerts.emplace_back(
make_string_ref(config->balloc, private_key_file),
make_string_ref(config->balloc, cert_file));
make_string_ref(config->balloc, cert_file), std::move(sct_data));
return 0;
}
@ -2875,6 +3058,13 @@ int parse_config(Config *config, int optid, const StringRef &opt,
LOG(WARN) << opt << ": This option requires OpenSSL >= 1.0.2";
#endif // !(!LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L)
return 0;
case SHRPX_OPTID_TLS_SCT_DIR:
#if !LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L
return read_tls_sct_from_dir(config->tls.sct_data, opt, optarg);
#else // !(!LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L)
LOG(WARN) << opt << ": This option requires OpenSSL >= 1.0.2";
return 0;
#endif // !(!LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L)
case SHRPX_OPTID_CONF:
LOG(WARN) << "conf: ignored";

View File

@ -309,6 +309,7 @@ constexpr auto SHRPX_OPT_BACKEND_HTTP2_ENCODER_DYNAMIC_TABLE_SIZE =
constexpr auto SHRPX_OPT_BACKEND_HTTP2_DECODER_DYNAMIC_TABLE_SIZE =
StringRef::from_lit("backend-http2-decoder-dynamic-table-size");
constexpr auto SHRPX_OPT_ECDH_CURVES = StringRef::from_lit("ecdh-curves");
constexpr auto SHRPX_OPT_TLS_SCT_DIR = StringRef::from_lit("tls-sct-dir");
constexpr size_t SHRPX_OBFUSCATED_NODE_LENGTH = 8;
@ -437,6 +438,18 @@ struct TicketKeys {
std::vector<TicketKey> keys;
};
struct TLSCertificate {
TLSCertificate(StringRef private_key_file, StringRef cert_file,
std::vector<uint8_t> sct_data)
: private_key_file(std::move(private_key_file)),
cert_file(std::move(cert_file)),
sct_data(std::move(sct_data)) {}
StringRef private_key_file;
StringRef cert_file;
std::vector<uint8_t> sct_data;
};
struct HttpProxy {
Address addr;
// host in http proxy URI
@ -522,14 +535,15 @@ struct TLSConfig {
StringRef cert_file;
} client;
// The list of (private key file, certificate file) pair
std::vector<std::pair<StringRef, StringRef>> subcerts;
// The list of additional TLS certificate pair
std::vector<TLSCertificate> subcerts;
std::vector<unsigned char> alpn_prefs;
// list of supported NPN/ALPN protocol strings in the order of
// preference.
std::vector<StringRef> npn_list;
// list of supported SSL/TLS protocol strings.
std::vector<StringRef> tls_proto_list;
std::vector<uint8_t> sct_data;
BIO_METHOD *bio_method;
// Bit mask to disable SSL/TLS protocol versions. This will be
// passed to SSL_CTX_set_options().
@ -942,6 +956,7 @@ enum {
SHRPX_OPTID_TLS_DYN_REC_IDLE_TIMEOUT,
SHRPX_OPTID_TLS_DYN_REC_WARMUP_THRESHOLD,
SHRPX_OPTID_TLS_PROTO_LIST,
SHRPX_OPTID_TLS_SCT_DIR,
SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED,
SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_ADDRESS_FAMILY,
SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_CERT_FILE,

View File

@ -485,6 +485,44 @@ int alpn_select_proto_cb(SSL *ssl, const unsigned char **out,
} // namespace
#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L
namespace {
// https://tools.ietf.org/html/rfc6962#section-6
constexpr unsigned int TLS_EXT_SIGNED_CERTIFICATE_TIMESTAMP = 18;
} // namespace
namespace {
int sct_add_cb(SSL *ssl, unsigned int ext_type, const unsigned char **out,
size_t *outlen, int *al, void *add_arg) {
assert(ext_type == TLS_EXT_SIGNED_CERTIFICATE_TIMESTAMP);
auto ssl_ctx = SSL_get_SSL_CTX(ssl);
auto tls_ctx_data =
static_cast<TLSContextData *>(SSL_CTX_get_app_data(ssl_ctx));
*out = tls_ctx_data->sct_data.data();
*outlen = tls_ctx_data->sct_data.size();
return 1;
}
} // namespace
namespace {
void sct_free_cb(SSL *ssl, unsigned int ext_type, const unsigned char *out,
void *add_arg) {
assert(ext_type == TLS_EXT_SIGNED_CERTIFICATE_TIMESTAMP);
}
} // namespace
namespace {
int sct_parse_cb(SSL *ssl, unsigned int ext_type, const unsigned char *in,
size_t inlen, int *al, void *parse_arg) {
assert(ext_type == TLS_EXT_SIGNED_CERTIFICATE_TIMESTAMP);
// client SHOULD send 0 length extension_data, but it is still
// SHOULD, and not MUST.
return 1;
}
} // namespace
struct TLSProtocol {
StringRef name;
long int mask;
@ -513,7 +551,8 @@ long int create_tls_proto_mask(const std::vector<StringRef> &tls_proto_list) {
return res;
}
SSL_CTX *create_ssl_context(const char *private_key_file, const char *cert_file
SSL_CTX *create_ssl_context(const char *private_key_file, const char *cert_file,
const std::vector<uint8_t> &sct_data
#ifdef HAVE_NEVERBLEED
,
neverbleed_t *nb
@ -678,8 +717,22 @@ SSL_CTX *create_ssl_context(const char *private_key_file, const char *cert_file
SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_proto_cb, nullptr);
#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L
#if !LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L
if (!sct_data.empty() &&
SSL_extension_supported(TLS_EXT_SIGNED_CERTIFICATE_TIMESTAMP) == 0) {
if (SSL_CTX_add_server_custom_ext(
ssl_ctx, TLS_EXT_SIGNED_CERTIFICATE_TIMESTAMP, sct_add_cb,
sct_free_cb, nullptr, sct_parse_cb, nullptr) != 1) {
LOG(FATAL) << "SSL_CTX_add_server_custom_ext failed: "
<< ERR_error_string(ERR_get_error(), nullptr);
DIE();
}
}
#endif // !LIBRESSL_IN_USE && OPENSSL_VERSION_NUMBER >= 0x10002000L
auto tls_ctx_data = new TLSContextData();
tls_ctx_data->cert_file = cert_file;
tls_ctx_data->sct_data = sct_data;
SSL_CTX_set_app_data(ssl_ctx, tls_ctx_data);
@ -1372,8 +1425,9 @@ SSL_CTX *setup_server_ssl_context(std::vector<SSL_CTX *> &all_ssl_ctx,
auto &tlsconf = get_config()->tls;
auto ssl_ctx = ssl::create_ssl_context(tlsconf.private_key_file.c_str(),
tlsconf.cert_file.c_str()
auto ssl_ctx =
ssl::create_ssl_context(tlsconf.private_key_file.c_str(),
tlsconf.cert_file.c_str(), tlsconf.sct_data
#ifdef HAVE_NEVERBLEED
,
nb
@ -1407,12 +1461,9 @@ SSL_CTX *setup_server_ssl_context(std::vector<SSL_CTX *> &all_ssl_ctx,
DIE();
}
for (auto &keycert : tlsconf.subcerts) {
auto &priv_key_file = keycert.first;
auto &cert_file = keycert.second;
auto ssl_ctx =
ssl::create_ssl_context(priv_key_file.c_str(), cert_file.c_str()
for (auto &c : tlsconf.subcerts) {
auto ssl_ctx = ssl::create_ssl_context(c.private_key_file.c_str(),
c.cert_file.c_str(), c.sct_data
#ifdef HAVE_NEVERBLEED
,
nb
@ -1424,7 +1475,7 @@ SSL_CTX *setup_server_ssl_context(std::vector<SSL_CTX *> &all_ssl_ctx,
auto cert = SSL_CTX_get0_certificate(ssl_ctx);
#else // defined(LIBRESSL_VERSION_NUMBER) || OPENSSL_VERSION_NUMBER <
// 0x10002000L
auto cert = load_certificate(cert_file.c_str());
auto cert = load_certificate(c.cert_file.c_str());
auto cert_deleter = defer(X509_free, cert);
#endif // defined(LIBRESSL_VERSION_NUMBER) || OPENSSL_VERSION_NUMBER <
// 0x10002000L

View File

@ -63,6 +63,9 @@ struct TLSSessionCache {
// This struct stores the additional information per SSL_CTX. This is
// attached to SSL_CTX using SSL_CTX_set_app_data().
struct TLSContextData {
// SCT data formatted so that this can be directly sent as
// extension_data of signed_certificate_timestamp.
std::vector<uint8_t> sct_data;
#ifndef HAVE_ATOMIC_STD_SHARED_PTR
// Protects ocsp_data;
std::mutex mu;
@ -75,7 +78,9 @@ struct TLSContextData {
};
// Create server side SSL_CTX
SSL_CTX *create_ssl_context(const char *private_key_file, const char *cert_file
SSL_CTX *create_ssl_context(const char *private_key_file, const char *cert_file,
const std::vector<uint8_t> &sct_data
#ifdef HAVE_NEVERBLEED
,
neverbleed_t *nb