From 4bc9afe20a8565d03c83cb3272a002192fd384d3 Mon Sep 17 00:00:00 2001 From: Tatsuhiro Tsujikawa Date: Mon, 30 Mar 2015 23:20:40 +0900 Subject: [PATCH] nghttpx: Add OCSP stapling feature --- src/Makefile.am | 1 + src/shrpx.cc | 64 +++++++++++- src/shrpx_config.cc | 19 ++++ src/shrpx_config.h | 6 ++ src/shrpx_connection_handler.cc | 100 +++++++++++++++++- src/shrpx_connection_handler.h | 20 +++- src/shrpx_ssl.cc | 151 +++++++++++++++++++++++++++- src/shrpx_ssl.h | 21 +++- src/util.cc | 33 +++++- src/util_test.cc | 5 + third-party/Makefile.am | 3 + third-party/h2o/README.rst | 10 ++ third-party/h2o/fetch-ocsp-response | 150 +++++++++++++++++++++++++++ 13 files changed, 570 insertions(+), 13 deletions(-) create mode 100644 third-party/h2o/README.rst create mode 100755 third-party/h2o/fetch-ocsp-response diff --git a/src/Makefile.am b/src/Makefile.am index 8eac2b1f..b1336ae8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -28,6 +28,7 @@ TESTS = AM_CFLAGS = $(WARNCFLAGS) AM_CPPFLAGS = \ + -DPKGDATADIR='"$(pkgdatadir)"' \ -Wall \ -I$(top_srcdir)/lib/includes \ -I$(top_builddir)/lib/includes \ diff --git a/src/shrpx.cc b/src/shrpx.cc index b67043ce..bc2aec31 100644 --- a/src/shrpx.cc +++ b/src/shrpx.cc @@ -575,6 +575,8 @@ void refresh_cb(struct ev_loop *loop, ev_timer *w, int revents) { (!worker || worker->get_worker_stat()->num_connections == 0)) { ev_break(loop); } + + conn_handler->handle_ocsp_completion(); } } // namespace @@ -740,6 +742,10 @@ int event_loop() { refresh_timer.data = conn_handler.get(); ev_timer_again(loop, &refresh_timer); + if (!get_config()->upstream_no_tls && !get_config()->no_ocsp) { + conn_handler->update_ocsp_async(); + } + if (LOG_ENABLED(INFO)) { LOG(INFO) << "Entering event loop"; } @@ -747,6 +753,7 @@ int event_loop() { ev_run(loop, 0); conn_handler->join_worker(); + conn_handler->join_ocsp_thread(); return 0; } @@ -918,6 +925,11 @@ void fill_default_config() { mod_config()->no_server_push = false; mod_config()->host_unix = false; mod_config()->http2_downstream_connections_per_worker = 0; + // ocsp update interval = 14400 secs = 4 hours, borrowed from h2o + mod_config()->ocsp_update_interval = 14400.; + mod_config()->fetch_ocsp_response_file = + strcopy(PKGDATADIR "/fetch-ocsp-response"); + mod_config()->no_ocsp = false; } } // namespace @@ -942,7 +954,8 @@ void print_help(std::ostream &out) { Set path to server's private key. Required unless -p, --client or --frontend-no-tls are given. Set path to server's certificate. Required unless -p, - --client or --frontend-no-tls are given. + --client or --frontend-no-tls are given. To make OCSP + stapling work, this must be absolute path. Options: The options are categorized into several groups. @@ -1130,7 +1143,8 @@ SSL/TLS: 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. + option can be used multiple times. To make OCSP + stapling work, must be absolute path. --backend-tls-sni-field= Explicitly set the content of the TLS SNI extension. This will default to the backend HOST name. @@ -1191,6 +1205,15 @@ SSL/TLS: objects, which means session ID generated by one worker is not acceptable by another worker. On the other hand, session ticket key is shared across all worker threads. + --fetch-ocsp-response-file= + Path to fetch-ocsp-response script file. It should be + absolute path. + Default: )" << get_config()->fetch_ocsp_response_file.get() << R"( + --ocsp-update-interval= + Set interval to update OCSP response cache. + Default: )" + << util::duration_str(get_config()->ocsp_update_interval) << R"( + --no-ocsp Disable OCSP stapling. HTTP/2 and SPDY: -c, --http2-max-concurrent-streams= @@ -1379,8 +1402,9 @@ Misc: 10 * 1024). Units are K, M and G (powers of 1024). The argument is an integer and an optional unit (e.g., 1s - is 1 second and 500ms is 500 milliseconds). Units are s or ms. If - a unit is omitted, a second is used as unit.)" << std::endl; + is 1 second and 500ms is 500 milliseconds). Units are h, m, s or ms + (hours, minutes, seconds and milliseconds, respectively). If a unit + is omitted, a second is used as unit.)" << std::endl; } } // namespace @@ -1492,6 +1516,9 @@ int main(int argc, char **argv) { {"no-host-rewrite", no_argument, &flag, 73}, {"no-server-push", no_argument, &flag, 74}, {"backend-http2-connections-per-worker", required_argument, &flag, 76}, + {"fetch-ocsp-response-file", required_argument, &flag, 77}, + {"ocsp-update-interval", required_argument, &flag, 78}, + {"no-ocsp", no_argument, &flag, 79}, {nullptr, 0, nullptr, 0}}; int option_index = 0; @@ -1834,6 +1861,18 @@ int main(int argc, char **argv) { cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_CONNECTIONS_PER_WORKER, optarg); break; + case 77: + // --fetch-ocsp-response-file + cmdcfgs.emplace_back(SHRPX_OPT_FETCH_OCSP_RESPONSE_FILE, optarg); + break; + case 78: + // --ocsp-update-interval + cmdcfgs.emplace_back(SHRPX_OPT_OCSP_UPDATE_INTERVAL, optarg); + break; + case 79: + // --no-ocsp + cmdcfgs.emplace_back(SHRPX_OPT_NO_OCSP, "yes"); + break; default: break; } @@ -2000,6 +2039,23 @@ int main(int argc, char **argv) { exit(EXIT_FAILURE); } + if (!get_config()->upstream_no_tls && !get_config()->no_ocsp) { +#ifdef NOTHREADS + mod_config()->no_ocsp = true; + LOG(WARN) + << "OCSP stapling has been disabled since it requires threading but" + "threading disabled at build time."; +#else // !NOTHREADS + struct stat buf; + if (stat(get_config()->fetch_ocsp_response_file.get(), &buf) != 0) { + mod_config()->no_ocsp = true; + LOG(WARN) << "--fetch-ocsp-response-file: " + << get_config()->fetch_ocsp_response_file.get() + << " not found. OCSP stapling has been disabled."; + } +#endif // !NOTHREADS + } + if (get_config()->downstream_addrs.empty()) { DownstreamAddr addr; addr.host = strcopy(DEFAULT_DOWNSTREAM_HOST); diff --git a/src/shrpx_config.cc b/src/shrpx_config.cc index 182613ca..fb2ad994 100644 --- a/src/shrpx_config.cc +++ b/src/shrpx_config.cc @@ -148,6 +148,9 @@ const char SHRPX_OPT_BACKEND_RESPONSE_BUFFER[] = "backend-response-buffer"; const char SHRPX_OPT_NO_SERVER_PUSH[] = "no-server-push"; const char SHRPX_OPT_BACKEND_HTTP2_CONNECTIONS_PER_WORKER[] = "backend-http2-connections-per-worker"; +const char SHRPX_OPT_FETCH_OCSP_RESPONSE_FILE[] = "fetch-ocsp-response-file"; +const char SHRPX_OPT_OCSP_UPDATE_INTERVAL[] = "ocsp-update-interval"; +const char SHRPX_OPT_NO_OCSP[] = "no-ocsp"; namespace { Config *config = nullptr; @@ -1200,6 +1203,22 @@ int parse_config(const char *opt, const char *optarg) { opt, optarg); } + if (util::strieq(opt, SHRPX_OPT_FETCH_OCSP_RESPONSE_FILE)) { + mod_config()->fetch_ocsp_response_file = strcopy(optarg); + + return 0; + } + + if (util::strieq(opt, SHRPX_OPT_OCSP_UPDATE_INTERVAL)) { + return parse_duration(&mod_config()->ocsp_update_interval, opt, optarg); + } + + if (util::strieq(opt, SHRPX_OPT_NO_OCSP)) { + mod_config()->no_ocsp = util::strieq(optarg, "yes"); + + return 0; + } + if (util::strieq(opt, "conf")) { LOG(WARN) << "conf: ignored"; diff --git a/src/shrpx_config.h b/src/shrpx_config.h index 9ffaea1e..8a6a2649 100644 --- a/src/shrpx_config.h +++ b/src/shrpx_config.h @@ -137,6 +137,9 @@ extern const char SHRPX_OPT_BACKEND_REQUEST_BUFFER[]; extern const char SHRPX_OPT_BACKEND_RESPONSE_BUFFER[]; extern const char SHRPX_OPT_NO_SERVER_PUSH[]; extern const char SHRPX_OPT_BACKEND_HTTP2_CONNECTIONS_PER_WORKER[]; +extern const char SHRPX_OPT_FETCH_OCSP_RESPONSE_FILE[]; +extern const char SHRPX_OPT_OCSP_UPDATE_INTERVAL[]; +extern const char SHRPX_OPT_NO_OCSP[]; union sockaddr_union { sockaddr_storage storage; @@ -209,6 +212,7 @@ struct Config { ev_tstamp stream_write_timeout; ev_tstamp downstream_idle_read_timeout; ev_tstamp listener_disable_timeout; + ev_tstamp ocsp_update_interval; // address of frontend connection. This could be a path to UNIX // domain socket. In this case, |host_unix| must be true. std::unique_ptr host; @@ -246,6 +250,7 @@ struct Config { std::unique_ptr client_cert_file; std::unique_ptr accesslog_file; std::unique_ptr errorlog_file; + std::unique_ptr fetch_ocsp_response_file; FILE *http2_upstream_dump_request_header; FILE *http2_upstream_dump_response_header; nghttp2_session_callbacks *http2_upstream_callbacks; @@ -324,6 +329,7 @@ struct Config { bool no_server_push; // true if host contains UNIX domain socket path bool host_unix; + bool no_ocsp; }; const Config *get_config(); diff --git a/src/shrpx_connection_handler.cc b/src/shrpx_connection_handler.cc index 91873ff4..0adac5a7 100644 --- a/src/shrpx_connection_handler.cc +++ b/src/shrpx_connection_handler.cc @@ -58,15 +58,32 @@ void acceptor_disable_cb(struct ev_loop *loop, ev_timer *w, int revent) { } } // namespace +namespace { +void ocsp_cb(struct ev_loop *loop, ev_timer *w, int revent) { + auto h = static_cast(w->data); + + // If we are in graceful shutdown period, we won't do ocsp query. + if (h->get_graceful_shutdown()) { + return; + } + + h->update_ocsp_async(); +} +} // namespace + ConnectionHandler::ConnectionHandler(struct ev_loop *loop) : single_worker_(nullptr), loop_(loop), worker_round_robin_cnt_(0), graceful_shutdown_(false) { ev_timer_init(&disable_acceptor_timer_, acceptor_disable_cb, 0., 0.); disable_acceptor_timer_.data = this; + + ev_timer_init(&ocsp_timer_, ocsp_cb, 0., 0.); + ocsp_timer_.data = this; } ConnectionHandler::~ConnectionHandler() { ev_timer_stop(loop_, &disable_acceptor_timer_); + ev_timer_stop(loop_, &ocsp_timer_); } void ConnectionHandler::worker_reopen_log_files() { @@ -95,7 +112,7 @@ void ConnectionHandler::worker_renew_ticket_keys( void ConnectionHandler::create_single_worker() { auto cert_tree = ssl::create_cert_lookup_tree(); - auto sv_ssl_ctx = ssl::setup_server_ssl_context(cert_tree); + auto sv_ssl_ctx = ssl::setup_server_ssl_context(all_ssl_ctx_, cert_tree); auto cl_ssl_ctx = ssl::setup_client_ssl_context(); single_worker_ = make_unique(loop_, sv_ssl_ctx, cl_ssl_ctx, cert_tree, @@ -111,7 +128,7 @@ void ConnectionHandler::create_worker_thread(size_t num) { if (!get_config()->tls_ctx_per_worker) { cert_tree = ssl::create_cert_lookup_tree(); - sv_ssl_ctx = ssl::setup_server_ssl_context(cert_tree); + sv_ssl_ctx = ssl::setup_server_ssl_context(all_ssl_ctx_, cert_tree); cl_ssl_ctx = ssl::setup_client_ssl_context(); } @@ -120,7 +137,8 @@ void ConnectionHandler::create_worker_thread(size_t num) { if (get_config()->tls_ctx_per_worker) { cert_tree = ssl::create_cert_lookup_tree(); - sv_ssl_ctx = ssl::setup_server_ssl_context(cert_tree); + std::vector all_ssl_ctx; + sv_ssl_ctx = ssl::setup_server_ssl_context(all_ssl_ctx, cert_tree); cl_ssl_ctx = ssl::setup_client_ssl_context(); } @@ -309,4 +327,80 @@ bool ConnectionHandler::get_graceful_shutdown() const { return graceful_shutdown_; } +namespace { +void update_ocsp_ssl_ctx(SSL_CTX *ssl_ctx) { + auto tls_ctx_data = + static_cast(SSL_CTX_get_app_data(ssl_ctx)); + auto cert_file = tls_ctx_data->cert_file; + + std::vector out; + if (ssl::get_ocsp_response(out, cert_file) != 0) { + LOG(WARN) << "ocsp update for " << cert_file << " failed"; + return; + } + + if (LOG_ENABLED(INFO)) { + LOG(INFO) << "ocsp update for " << cert_file << " finished successfully"; + } + + std::lock_guard g(tls_ctx_data->mu); + tls_ctx_data->ocsp_data = std::move(out); +} +} // namespace + +void ConnectionHandler::update_ocsp() { + for (auto ssl_ctx : all_ssl_ctx_) { + update_ocsp_ssl_ctx(ssl_ctx); + } +} + +void ConnectionHandler::update_ocsp_async() { +#ifndef NOTHREADS + ocsp_result_ = std::async(std::launch::async, [this]() { + // Log files are managed per thread. We have to open log files + // for this thread. We don't reopen log files in this thread when + // signal is received. This is future TODO. + reopen_log_files(); + auto closer = defer([]() { + auto lgconf = log_config(); + if (lgconf->accesslog_fd != -1) { + close(lgconf->accesslog_fd); + } + if (lgconf->errorlog_fd != -1) { + close(lgconf->errorlog_fd); + } + }); + + update_ocsp(); + }); +#endif // !NOTHREADS +} + +void ConnectionHandler::handle_ocsp_completion() { +#ifndef NOTHREADS + if (!ocsp_result_.valid()) { + return; + } + + if (ocsp_result_.wait_for(std::chrono::seconds(0)) != + std::future_status::ready) { + return; + } + + ocsp_result_.get(); + + ev_timer_set(&ocsp_timer_, get_config()->ocsp_update_interval, 0.); + ev_timer_start(loop_, &ocsp_timer_); +#endif // !NOTHREADS +} + +void ConnectionHandler::join_ocsp_thread() { +#ifndef NOTHREADS + if (!ocsp_result_.valid()) { + return; + } + ocsp_result_.get(); +#endif // !NOTHREADS +} + } // namespace shrpx diff --git a/src/shrpx_connection_handler.h b/src/shrpx_connection_handler.h index 16847090..c5f6e80b 100644 --- a/src/shrpx_connection_handler.h +++ b/src/shrpx_connection_handler.h @@ -32,6 +32,9 @@ #include #include +#ifndef NOTHREADS +#include +#endif // !NOTHREADS #include @@ -48,7 +51,6 @@ class Worker; struct WorkerStat; struct TicketKeys; -// TODO should be renamed as ConnectionHandler class ConnectionHandler { public: ConnectionHandler(struct ev_loop *loop); @@ -77,8 +79,23 @@ public: void set_graceful_shutdown(bool f); bool get_graceful_shutdown() const; void join_worker(); + // Updates OCSP response cache for all server side SSL_CTX object + void update_ocsp(); + // Just like update_ocsp(), but performed in new thread. Call + // handle_ocsp_completion() to handle its completion and scheduling + // next update. + void update_ocsp_async(); + // Handles asynchronous OCSP update completion and schedules next + // update. + void handle_ocsp_completion(); + // Waits for OCSP thread finishes if it is still running. + void join_ocsp_thread(); private: +#ifndef NOTHREADS + std::future ocsp_result_; +#endif // !NOTHREADS + std::vector all_ssl_ctx_; // Worker instances when multi threaded mode (-nN, N >= 2) is used. std::vector> workers_; // Worker instance used when single threaded mode (-n1) is used. @@ -94,6 +111,7 @@ private: // acceptor for IPv6 address std::unique_ptr acceptor6_; ev_timer disable_acceptor_timer_; + ev_timer ocsp_timer_; unsigned int worker_round_robin_cnt_; bool graceful_shutdown_; }; diff --git a/src/shrpx_ssl.cc b/src/shrpx_ssl.cc index 07e6081f..d7a55a97 100644 --- a/src/shrpx_ssl.cc +++ b/src/shrpx_ssl.cc @@ -28,6 +28,11 @@ #include #include #include +#include +#include +#ifndef NOTHREADS +#include +#endif // !NOTHREADS #include #include @@ -141,7 +146,32 @@ int servername_callback(SSL *ssl, int *al, void *arg) { } } } + return SSL_TLSEXT_ERR_OK; +} +} // namespace +namespace { +int ocsp_resp_cb(SSL *ssl, void *arg) { + auto ssl_ctx = SSL_get_SSL_CTX(ssl); + auto tls_ctx_data = + static_cast(SSL_CTX_get_app_data(ssl_ctx)); + { + std::lock_guard g(tls_ctx_data->mu); + auto &data = tls_ctx_data->ocsp_data; + + if (!data.empty()) { + auto buf = static_cast( + CRYPTO_malloc(data.size(), __FILE__, __LINE__)); + + if (!buf) { + return SSL_TLSEXT_ERR_OK; + } + + std::copy(std::begin(data), std::end(data), buf); + + SSL_set_tlsext_status_ocsp_resp(ssl, buf, data.size()); + } + } return SSL_TLSEXT_ERR_OK; } } // namespace @@ -418,6 +448,7 @@ SSL_CTX *create_ssl_context(const char *private_key_file, } SSL_CTX_set_tlsext_servername_callback(ssl_ctx, servername_callback); SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx, ticket_key_cb); + SSL_CTX_set_tlsext_status_cb(ssl_ctx, ocsp_resp_cb); SSL_CTX_set_info_callback(ssl_ctx, info_callback); // NPN advertisement @@ -426,6 +457,12 @@ SSL_CTX *create_ssl_context(const char *private_key_file, // ALPN selection callback SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_proto_cb, nullptr); #endif // OPENSSL_VERSION_NUMBER >= 0x10002000L + + auto tls_ctx_data = new TLSContextData(); + tls_ctx_data->cert_file = cert_file; + + SSL_CTX_set_app_data(ssl_ctx, tls_ctx_data); + return ssl_ctx; } @@ -929,7 +966,8 @@ bool check_http2_requirement(SSL *ssl) { return true; } -SSL_CTX *setup_server_ssl_context(CertLookupTree *cert_tree) { +SSL_CTX *setup_server_ssl_context(std::vector &all_ssl_ctx, + CertLookupTree *cert_tree) { if (get_config()->upstream_no_tls) { return nullptr; } @@ -937,6 +975,8 @@ SSL_CTX *setup_server_ssl_context(CertLookupTree *cert_tree) { auto ssl_ctx = ssl::create_ssl_context(get_config()->private_key_file.get(), get_config()->cert_file.get()); + all_ssl_ctx.push_back(ssl_ctx); + if (get_config()->subcerts.empty()) { return ssl_ctx; } @@ -950,6 +990,7 @@ SSL_CTX *setup_server_ssl_context(CertLookupTree *cert_tree) { for (auto &keycert : get_config()->subcerts) { auto ssl_ctx = ssl::create_ssl_context(keycert.first.c_str(), keycert.second.c_str()); + all_ssl_ctx.push_back(ssl_ctx); if (ssl::cert_lookup_tree_add_cert_from_file( cert_tree, ssl_ctx, keycert.second.c_str()) == -1) { LOG(FATAL) << "Failed to add sub certificate."; @@ -984,6 +1025,114 @@ CertLookupTree *create_cert_lookup_tree() { return new ssl::CertLookupTree(); } +namespace { +// inspired by h2o_read_command function from h2o project: +// https://github.com/h2o/h2o +int exec_read_stdout(std::vector &out, char *const *argv, + char *const *envp) { +#ifndef NOTHREADS + int rv; + int pfd[2]; + +#ifdef O_CLOEXEC + if (pipe2(pfd, O_CLOEXEC) == -1) { + return -1; + } +#else // !O_CLOEXEC + if (pipe(pfd) == -1) { + return -1; + } + util::make_socket_closeonexec(pfd[0]); + util::make_socket_closeonexec(pfd[1]); +#endif // !O_CLOEXEC + + auto closer = defer([pfd]() { + close(pfd[0]); + + if (pfd[1] != -1) { + close(pfd[1]); + } + }); + + // posix_spawn family functions are really interesting. They makes + // fork + dup2 + execve pattern easier. + + posix_spawn_file_actions_t file_actions; + + if (posix_spawn_file_actions_init(&file_actions) != 0) { + return -1; + } + + auto file_actions_del = + defer(posix_spawn_file_actions_destroy, &file_actions); + + if (posix_spawn_file_actions_adddup2(&file_actions, pfd[1], 1) != 0) { + return -1; + } + + if (posix_spawn_file_actions_addclose(&file_actions, pfd[0]) != 0) { + return -1; + } + + pid_t pid; + rv = posix_spawn(&pid, argv[0], &file_actions, nullptr, argv, envp); + if (rv != 0) { + LOG(WARN) << "Cannot execute ocsp query command: " << argv[0] + << ", errno=" << rv; + return -1; + } + + close(pfd[1]); + pfd[1] = -1; + + std::array buf; + for (;;) { + ssize_t n; + while ((n = read(pfd[0], buf.data(), buf.size())) == -1 && errno == EINTR) + ; + + if (n == -1) { + auto error = errno; + LOG(WARN) << "Reading from ocsp query command failed: errno=" << error; + return -1; + } + + if (n == 0) { + break; + } + + std::copy_n(std::begin(buf), n, std::back_inserter(out)); + } + + int status; + if (waitpid(pid, &status, 0) == -1) { + auto error = errno; + LOG(WARN) << "waitpid for ocsp query command failed: errno=" << error; + return -1; + } + + if (!WIFEXITED(status)) { + LOG(WARN) << "ocsp query command did not exit normally: " << status; + return -1; + } +#endif // !NOTHREADS + return 0; +} +} // namespace + +int get_ocsp_response(std::vector &out, const char *cert_file) { + char *const argv[] = { + const_cast(get_config()->fetch_ocsp_response_file.get()), + const_cast(cert_file), nullptr}; + char *const envp[] = {nullptr}; + + if (exec_read_stdout(out, argv, envp) != 0 || out.empty()) { + return -1; + } + + return 0; +} + } // namespace ssl } // namespace shrpx diff --git a/src/shrpx_ssl.h b/src/shrpx_ssl.h index 710a999e..8888723c 100644 --- a/src/shrpx_ssl.h +++ b/src/shrpx_ssl.h @@ -28,6 +28,7 @@ #include "shrpx.h" #include +#include #include #include @@ -42,6 +43,18 @@ class DownstreamConnectionPool; namespace ssl { +// This struct stores the additional information per SSL_CTX. This is +// attached to SSL_CTX using SSL_CTX_set_app_data(). +struct TLSContextData { + // Protects ocsp_data; + std::mutex mu; + // OCSP resonse + std::vector ocsp_data; + + // Path to certificate file + const char *cert_file; +}; + // Create server side SSL_CTX SSL_CTX *create_ssl_context(const char *private_key_file, const char *cert_file); @@ -143,8 +156,10 @@ std::vector set_alpn_prefs(const std::vector &protos); // and if upstream_no_tls is true, returns nullptr. Otherwise // construct default SSL_CTX. If subcerts are available // (get_config()->subcerts), caller should provide CertLookupTree -// object as |cert_tree| parameter, otherwise SNI does not work. -SSL_CTX *setup_server_ssl_context(CertLookupTree *cert_tree); +// object as |cert_tree| parameter, otherwise SNI does not work. All +// the created SSL_CTX is stored into |all_ssl_ctx|. +SSL_CTX *setup_server_ssl_context(std::vector &all_ssl_ctx, + CertLookupTree *cert_tree); // Setups client side SSL_CTX. This function inspects get_config() // and if downstream_no_tls is true, returns nullptr. Otherwise, only @@ -155,6 +170,8 @@ SSL_CTX *setup_client_ssl_context(); // this function returns nullptr. CertLookupTree *create_cert_lookup_tree(); +int get_ocsp_response(std::vector &out, const char *cert_file); + } // namespace ssl } // namespace shrpx diff --git a/src/util.cc b/src/util.cc index 5cc6b25a..d08d8b5b 100644 --- a/src/util.cc +++ b/src/util.cc @@ -963,6 +963,7 @@ int64_t parse_uint(const uint8_t *s, size_t len) { } double parse_duration_with_unit(const char *s) { + constexpr auto max = std::numeric_limits::max(); int64_t n; size_t i; auto len = strlen(s); @@ -976,17 +977,36 @@ double parse_duration_with_unit(const char *s) { switch (s[i]) { case 'S': case 's': + // seconds if (i + 1 != len) { goto fail; } return static_cast(n); - break; case 'M': case 'm': + if (i + 1 == len) { + // minutes + if (n > max / 60) { + goto fail; + } + return static_cast(n) * 60; + } + if (i + 2 != len || (s[i + 1] != 's' && s[i + 1] != 'S')) { goto fail; } + // milliseconds return static_cast(n) / 1000.; + case 'H': + case 'h': + // hours + if (i + 1 != len) { + goto fail; + } + if (n > max / 3600) { + goto fail; + } + return static_cast(n) * 3600; } fail: return std::numeric_limits::infinity(); @@ -1000,7 +1020,16 @@ std::string duration_str(double t) { if (frac > 0) { return utos(static_cast(t * 1000)) + "ms"; } - return utos(static_cast(t)) + "s"; + auto v = static_cast(t); + if (v % 60) { + return utos(v) + "s"; + } + v /= 60; + if (v % 60) { + return utos(v) + "m"; + } + v /= 60; + return utos(v) + "h"; } std::string format_duration(const std::chrono::microseconds &u) { diff --git a/src/util_test.cc b/src/util_test.cc index 3e1b4028..21d992d8 100644 --- a/src/util_test.cc +++ b/src/util_test.cc @@ -303,6 +303,8 @@ void test_util_parse_duration_with_unit(void) { CU_ASSERT(0.500 == util::parse_duration_with_unit("500ms")); CU_ASSERT(123. == util::parse_duration_with_unit("123S")); CU_ASSERT(0.500 == util::parse_duration_with_unit("500MS")); + CU_ASSERT(180 == util::parse_duration_with_unit("3m")); + CU_ASSERT(3600 * 5 == util::parse_duration_with_unit("5h")); auto err = std::numeric_limits::infinity(); // check overflow case @@ -321,6 +323,9 @@ void test_util_duration_str(void) { CU_ASSERT("1s" == util::duration_str(1.)); CU_ASSERT("500ms" == util::duration_str(0.5)); CU_ASSERT("1500ms" == util::duration_str(1.5)); + CU_ASSERT("2m" == util::duration_str(120.)); + CU_ASSERT("121s" == util::duration_str(121.)); + CU_ASSERT("1h" == util::duration_str(3600.)); } void test_util_format_duration(void) { diff --git a/third-party/Makefile.am b/third-party/Makefile.am index 7549cd6d..aa8832d3 100644 --- a/third-party/Makefile.am +++ b/third-party/Makefile.am @@ -27,3 +27,6 @@ noinst_LTLIBRARIES = libhttp-parser.la libhttp_parser_la_SOURCES = \ http-parser/http_parser.c \ http-parser/http_parser.h + + +dist_pkgdata_SCRIPTS = h2o/fetch-ocsp-response diff --git a/third-party/h2o/README.rst b/third-party/h2o/README.rst new file mode 100644 index 00000000..627d741f --- /dev/null +++ b/third-party/h2o/README.rst @@ -0,0 +1,10 @@ +fetch-ocsp-response is a Perl script to perform OCSP query and get +response. It uses openssl command under the hood. nghttpx uses it to +enable OCSP stapling feature. + +fetch-ocsp-response has been developed as part of h2o project +(https://github.com/h2o/h2o). The script file with the same name in +this directory was copied from their github repository. + +fetch-ocsp-response is usually installed under $(pkgdatadir), which is +$(prefix)/share/nghttp2. diff --git a/third-party/h2o/fetch-ocsp-response b/third-party/h2o/fetch-ocsp-response new file mode 100755 index 00000000..9272dae2 --- /dev/null +++ b/third-party/h2o/fetch-ocsp-response @@ -0,0 +1,150 @@ +#! /bin/sh +exec perl -x $0 "$@" +#! perl + +# Copyright (c) 2015 DeNA Co., Ltd. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +use strict; +use warnings; +use File::Temp qw(tempdir); +use Getopt::Long; + +# from sysexits.h +use constant EX_TEMPFAIL => 75; + +my ($issuer_fn, $opt_help); +my $openssl_cmd = 'openssl'; + +GetOptions( + "issuer=s" => \$issuer_fn, + "openssl=s", => \$openssl_cmd, + help => \$opt_help, +) or exit(1); +if ($opt_help) { + print << "EOT"; +Usage: $0 [] + +Options: + --issuer issuer certificate (if omitted, is extracted from the + certificate chain) + --openssl openssl command to use (default: "openssl") + --help prints this help + +The command issues an OCSP request for given server certificate, verifies the +response and prints the resulting DER. + +The command exits 0 if successful, or 75 (EX_TEMPFAIL) on temporary error. +Other exit codes may be returned in case of hard errors. + +EOT + exit(0); +} + +die "no certificate file\n" + if @ARGV == 0; +my $cert_fn = shift @ARGV; + +my $tempdir = tempdir(CLEANUP => 1); + +my $openssl_version = run_openssl("version"); +chomp $openssl_version; +print STDERR "fetch-ocsp-response (using $openssl_version)\n"; + +# obtain ocsp uri +my $ocsp_uri = run_openssl("x509 -in $cert_fn -noout -ocsp_uri"); +chomp $ocsp_uri; +die "failed to extract ocsp URI from $cert_fn\n" + if $ocsp_uri !~ m{^https?://}; +my($ocsp_host) = $ocsp_uri =~ m{^https?://([^/:]+)}; + +# save issuer certificate +if (! defined $issuer_fn) { + my $chain = read_file($cert_fn); + $chain =~ m{-----END CERTIFICATE-----.*?(-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)}s + or die "--issuer option was not used, and failed to extract issuer certificate from the certificate\n"; + $issuer_fn = "$tempdir/issuer.crt"; + write_file($issuer_fn, "$1\n"); +} + +# obtain response (without verification) +print STDERR "sending OCSP request to $ocsp_uri\n"; +my $resp = run_openssl( + "ocsp -issuer $issuer_fn -cert $cert_fn -url $ocsp_uri" + . ($openssl_version =~ /^OpenSSL 1\./is ? " -header Host $ocsp_host" : "") + . " -noverify -respout $tempdir/resp.der " . join(' ', @ARGV), + 1, +); +print STDERR $resp; + +# verify the response +print STDERR "verifying the response signature\n"; +my $success; +for my $args ( + # try from exotic options + "-VAfile $issuer_fn", # for comodo + "-partial_chain -trusted_first -CAfile $issuer_fn", # these options are only available in OpenSSL >= 1.0.2 + "-CAfile $issuer_fn", # for OpenSSL <= 1.0.1 +) { + if (system("$openssl_cmd ocsp -respin $tempdir/resp.der $args > $tempdir/verify.out 2>&1") == 0) { + print STDERR "verify OK (used: $args)\n"; + $success = 1; + last; + } +} +if (! $success) { + print STDERR read_file("$tempdir/verify.out"); + tempfail("failed to verify the response\n"); +} + +# success +print read_file("$tempdir/resp.der"); +exit 0; + +sub run_openssl { + my ($args, $tempfail) = @_; + open my $fh, "-|", "$openssl_cmd $args" + or die "failed to invoke $openssl_cmd:$!"; + my $resp = do { local $/; <$fh> }; + close $fh + or ($tempfail ? \&tempfail : \&die)->("OpenSSL exitted abnormally: $openssl_cmd $args:$!"); + $resp; +} + +sub read_file { + my $fn = shift; + open my $fh, "<", $fn + or die "failed to open file:$fn:$!"; + local $/; + <$fh>; +} + +sub write_file { + my ($fn, $data) = @_; + open my $fh, ">", $fn + or die "failed to open file:$fn:$!"; + print $fh $data; + close $fh; +} + +sub tempfail { + print STDERR @_; + exit EX_TEMPFAIL; +}