From 412c8f9e679455209ce9e8bcc8ba1966e92cf128 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Sat, 8 Oct 2016 19:03:21 +0900 Subject: [PATCH] nghttpx: Add TLS signed_certificate_timestamp extension support --- doc/nghttpx.h2r | 18 ++++ gennghttpxfun.py | 1 + src/shrpx.cc | 25 +++++- src/shrpx_config.cc | 204 ++++++++++++++++++++++++++++++++++++++++++-- src/shrpx_config.h | 19 ++++- src/shrpx_ssl.cc | 83 ++++++++++++++---- src/shrpx_ssl.h | 7 +- 7 files changed, 330 insertions(+), 27 deletions(-) diff --git a/doc/nghttpx.h2r b/doc/nghttpx.h2r index 04ca05fd..70cbeb88 100644 --- a/doc/nghttpx.h2r +++ b/doc/nghttpx.h2r @@ -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 `_). 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 +`_. This format is +the same one used by `nginx-ct +`_ and `mod_ssl_ct +`_. +`ct-submit `_ can be +used to submit certificates to log servers, and obtain the +``SignedCertificateTimestamp`` struct which can be used with nghttpx. + MRUBY SCRIPTING --------------- diff --git a/gennghttpxfun.py b/gennghttpxfun.py index 0d29e790..467a6853 100755 --- a/gennghttpxfun.py +++ b/gennghttpxfun.py @@ -147,6 +147,7 @@ OPTIONS = [ "backend-http2-encoder-dynamic-table-size", "backend-http2-decoder-dynamic-table-size", "ecdh-curves", + "tls-sct-dir", ] LOGVARS = [ diff --git a/src/shrpx.cc b/src/shrpx.cc index c4215db0..a6a1aa6f 100644 --- a/src/shrpx.cc +++ b/src/shrpx.cc @@ -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=: + --subcert=:[[;]...] 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, must be absolute path. + + Additional parameter can be specified in . The + available is "sct-dir=". + + "sct-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 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= + Specifies the directory where *.sct files exist. All + *.sct files in 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 , 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= @@ -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; } diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc index e41e35d4..b43db96f 100644 --- a/src/shrpx_config.cc +++ b/src/shrpx_config.cc @@ -41,6 +41,7 @@ #ifdef HAVE_UNISTD_H #include #endif // HAVE_UNISTD_H +#include #include #include @@ -1016,6 +1017,162 @@ int parse_error_page(std::vector &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 &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 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 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"; diff --git a/src/shrpx_config.h b/src/shrpx_config.h index f05f6de5..cf2e6705 100644 --- a/src/shrpx_config.h +++ b/src/shrpx_config.h @@ -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 keys; }; +struct TLSCertificate { + TLSCertificate(StringRef private_key_file, StringRef cert_file, + std::vector 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 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> subcerts; + // The list of additional TLS certificate pair + std::vector subcerts; std::vector alpn_prefs; // list of supported NPN/ALPN protocol strings in the order of // preference. std::vector npn_list; // list of supported SSL/TLS protocol strings. std::vector tls_proto_list; + std::vector 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, diff --git a/src/shrpx_ssl.cc b/src/shrpx_ssl.cc index ddc01a5e..2b9d0f6b 100644 --- a/src/shrpx_ssl.cc +++ b/src/shrpx_ssl.cc @@ -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(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 &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 &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,13 +1425,14 @@ SSL_CTX *setup_server_ssl_context(std::vector &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 + , + nb #endif // HAVE_NEVERBLEED - ); + ); all_ssl_ctx.push_back(ssl_ctx); @@ -1407,24 +1461,21 @@ SSL_CTX *setup_server_ssl_context(std::vector &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 + , + nb #endif // HAVE_NEVERBLEED - ); + ); all_ssl_ctx.push_back(ssl_ctx); #if !defined(LIBRESSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER >= 0x10002000L 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 diff --git a/src/shrpx_ssl.h b/src/shrpx_ssl.h index d7f4f8b1..6f44a058 100644 --- a/src/shrpx_ssl.h +++ b/src/shrpx_ssl.h @@ -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 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 &sct_data + #ifdef HAVE_NEVERBLEED , neverbleed_t *nb