nghttpx: Add OCSP stapling feature

This commit is contained in:
Tatsuhiro Tsujikawa 2015-03-30 23:20:40 +09:00
parent ccea4d42b5
commit 4bc9afe20a
13 changed files with 570 additions and 13 deletions

View File

@ -28,6 +28,7 @@ TESTS =
AM_CFLAGS = $(WARNCFLAGS) AM_CFLAGS = $(WARNCFLAGS)
AM_CPPFLAGS = \ AM_CPPFLAGS = \
-DPKGDATADIR='"$(pkgdatadir)"' \
-Wall \ -Wall \
-I$(top_srcdir)/lib/includes \ -I$(top_srcdir)/lib/includes \
-I$(top_builddir)/lib/includes \ -I$(top_builddir)/lib/includes \

View File

@ -575,6 +575,8 @@ void refresh_cb(struct ev_loop *loop, ev_timer *w, int revents) {
(!worker || worker->get_worker_stat()->num_connections == 0)) { (!worker || worker->get_worker_stat()->num_connections == 0)) {
ev_break(loop); ev_break(loop);
} }
conn_handler->handle_ocsp_completion();
} }
} // namespace } // namespace
@ -740,6 +742,10 @@ int event_loop() {
refresh_timer.data = conn_handler.get(); refresh_timer.data = conn_handler.get();
ev_timer_again(loop, &refresh_timer); 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)) { if (LOG_ENABLED(INFO)) {
LOG(INFO) << "Entering event loop"; LOG(INFO) << "Entering event loop";
} }
@ -747,6 +753,7 @@ int event_loop() {
ev_run(loop, 0); ev_run(loop, 0);
conn_handler->join_worker(); conn_handler->join_worker();
conn_handler->join_ocsp_thread();
return 0; return 0;
} }
@ -918,6 +925,11 @@ void fill_default_config() {
mod_config()->no_server_push = false; mod_config()->no_server_push = false;
mod_config()->host_unix = false; mod_config()->host_unix = false;
mod_config()->http2_downstream_connections_per_worker = 0; 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 } // namespace
@ -942,7 +954,8 @@ void print_help(std::ostream &out) {
Set path to server's private key. Required unless -p, Set path to server's private key. Required unless -p,
--client or --frontend-no-tls are given. --client or --frontend-no-tls are given.
<CERT> Set path to server's certificate. Required unless -p, <CERT> 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: Options:
The options are categorized into several groups. The options are categorized into several groups.
@ -1130,7 +1143,8 @@ SSL/TLS:
Specify additional certificate and private key file. Specify additional certificate and private key file.
nghttpx will choose certificates based on the hostname nghttpx will choose certificates based on the hostname
indicated by client using TLS SNI extension. This 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, <CERTPATH> must be absolute path.
--backend-tls-sni-field=<HOST> --backend-tls-sni-field=<HOST>
Explicitly set the content of the TLS SNI extension. Explicitly set the content of the TLS SNI extension.
This will default to the backend HOST name. This will default to the backend HOST name.
@ -1191,6 +1205,15 @@ SSL/TLS:
objects, which means session ID generated by one worker objects, which means session ID generated by one worker
is not acceptable by another worker. On the other hand, is not acceptable by another worker. On the other hand,
session ticket key is shared across all worker threads. session ticket key is shared across all worker threads.
--fetch-ocsp-response-file=<PATH>
Path to fetch-ocsp-response script file. It should be
absolute path.
Default: )" << get_config()->fetch_ocsp_response_file.get() << R"(
--ocsp-update-interval=<DURATION>
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: HTTP/2 and SPDY:
-c, --http2-max-concurrent-streams=<N> -c, --http2-max-concurrent-streams=<N>
@ -1379,8 +1402,9 @@ Misc:
10 * 1024). Units are K, M and G (powers of 1024). 10 * 1024). Units are K, M and G (powers of 1024).
The <DURATION> argument is an integer and an optional unit (e.g., 1s The <DURATION> 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 is 1 second and 500ms is 500 milliseconds). Units are h, m, s or ms
a unit is omitted, a second is used as unit.)" << std::endl; (hours, minutes, seconds and milliseconds, respectively). If a unit
is omitted, a second is used as unit.)" << std::endl;
} }
} // namespace } // namespace
@ -1492,6 +1516,9 @@ int main(int argc, char **argv) {
{"no-host-rewrite", no_argument, &flag, 73}, {"no-host-rewrite", no_argument, &flag, 73},
{"no-server-push", no_argument, &flag, 74}, {"no-server-push", no_argument, &flag, 74},
{"backend-http2-connections-per-worker", required_argument, &flag, 76}, {"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}}; {nullptr, 0, nullptr, 0}};
int option_index = 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, cmdcfgs.emplace_back(SHRPX_OPT_BACKEND_HTTP2_CONNECTIONS_PER_WORKER,
optarg); optarg);
break; 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: default:
break; break;
} }
@ -2000,6 +2039,23 @@ int main(int argc, char **argv) {
exit(EXIT_FAILURE); 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()) { if (get_config()->downstream_addrs.empty()) {
DownstreamAddr addr; DownstreamAddr addr;
addr.host = strcopy(DEFAULT_DOWNSTREAM_HOST); addr.host = strcopy(DEFAULT_DOWNSTREAM_HOST);

View File

@ -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_NO_SERVER_PUSH[] = "no-server-push";
const char SHRPX_OPT_BACKEND_HTTP2_CONNECTIONS_PER_WORKER[] = const char SHRPX_OPT_BACKEND_HTTP2_CONNECTIONS_PER_WORKER[] =
"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 { namespace {
Config *config = nullptr; Config *config = nullptr;
@ -1200,6 +1203,22 @@ int parse_config(const char *opt, const char *optarg) {
opt, 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")) { if (util::strieq(opt, "conf")) {
LOG(WARN) << "conf: ignored"; LOG(WARN) << "conf: ignored";

View File

@ -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_BACKEND_RESPONSE_BUFFER[];
extern const char SHRPX_OPT_NO_SERVER_PUSH[]; extern const char SHRPX_OPT_NO_SERVER_PUSH[];
extern const char SHRPX_OPT_BACKEND_HTTP2_CONNECTIONS_PER_WORKER[]; 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 { union sockaddr_union {
sockaddr_storage storage; sockaddr_storage storage;
@ -209,6 +212,7 @@ struct Config {
ev_tstamp stream_write_timeout; ev_tstamp stream_write_timeout;
ev_tstamp downstream_idle_read_timeout; ev_tstamp downstream_idle_read_timeout;
ev_tstamp listener_disable_timeout; ev_tstamp listener_disable_timeout;
ev_tstamp ocsp_update_interval;
// address of frontend connection. This could be a path to UNIX // address of frontend connection. This could be a path to UNIX
// domain socket. In this case, |host_unix| must be true. // domain socket. In this case, |host_unix| must be true.
std::unique_ptr<char[]> host; std::unique_ptr<char[]> host;
@ -246,6 +250,7 @@ struct Config {
std::unique_ptr<char[]> client_cert_file; std::unique_ptr<char[]> client_cert_file;
std::unique_ptr<char[]> accesslog_file; std::unique_ptr<char[]> accesslog_file;
std::unique_ptr<char[]> errorlog_file; std::unique_ptr<char[]> errorlog_file;
std::unique_ptr<char[]> fetch_ocsp_response_file;
FILE *http2_upstream_dump_request_header; FILE *http2_upstream_dump_request_header;
FILE *http2_upstream_dump_response_header; FILE *http2_upstream_dump_response_header;
nghttp2_session_callbacks *http2_upstream_callbacks; nghttp2_session_callbacks *http2_upstream_callbacks;
@ -324,6 +329,7 @@ struct Config {
bool no_server_push; bool no_server_push;
// true if host contains UNIX domain socket path // true if host contains UNIX domain socket path
bool host_unix; bool host_unix;
bool no_ocsp;
}; };
const Config *get_config(); const Config *get_config();

View File

@ -58,15 +58,32 @@ void acceptor_disable_cb(struct ev_loop *loop, ev_timer *w, int revent) {
} }
} // namespace } // namespace
namespace {
void ocsp_cb(struct ev_loop *loop, ev_timer *w, int revent) {
auto h = static_cast<ConnectionHandler *>(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) ConnectionHandler::ConnectionHandler(struct ev_loop *loop)
: single_worker_(nullptr), loop_(loop), worker_round_robin_cnt_(0), : single_worker_(nullptr), loop_(loop), worker_round_robin_cnt_(0),
graceful_shutdown_(false) { graceful_shutdown_(false) {
ev_timer_init(&disable_acceptor_timer_, acceptor_disable_cb, 0., 0.); ev_timer_init(&disable_acceptor_timer_, acceptor_disable_cb, 0., 0.);
disable_acceptor_timer_.data = this; disable_acceptor_timer_.data = this;
ev_timer_init(&ocsp_timer_, ocsp_cb, 0., 0.);
ocsp_timer_.data = this;
} }
ConnectionHandler::~ConnectionHandler() { ConnectionHandler::~ConnectionHandler() {
ev_timer_stop(loop_, &disable_acceptor_timer_); ev_timer_stop(loop_, &disable_acceptor_timer_);
ev_timer_stop(loop_, &ocsp_timer_);
} }
void ConnectionHandler::worker_reopen_log_files() { void ConnectionHandler::worker_reopen_log_files() {
@ -95,7 +112,7 @@ void ConnectionHandler::worker_renew_ticket_keys(
void ConnectionHandler::create_single_worker() { void ConnectionHandler::create_single_worker() {
auto cert_tree = ssl::create_cert_lookup_tree(); 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(); auto cl_ssl_ctx = ssl::setup_client_ssl_context();
single_worker_ = make_unique<Worker>(loop_, sv_ssl_ctx, cl_ssl_ctx, cert_tree, single_worker_ = make_unique<Worker>(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) { if (!get_config()->tls_ctx_per_worker) {
cert_tree = ssl::create_cert_lookup_tree(); 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(); 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) { if (get_config()->tls_ctx_per_worker) {
cert_tree = ssl::create_cert_lookup_tree(); cert_tree = ssl::create_cert_lookup_tree();
sv_ssl_ctx = ssl::setup_server_ssl_context(cert_tree); std::vector<SSL_CTX *> all_ssl_ctx;
sv_ssl_ctx = ssl::setup_server_ssl_context(all_ssl_ctx, cert_tree);
cl_ssl_ctx = ssl::setup_client_ssl_context(); cl_ssl_ctx = ssl::setup_client_ssl_context();
} }
@ -309,4 +327,80 @@ bool ConnectionHandler::get_graceful_shutdown() const {
return graceful_shutdown_; return graceful_shutdown_;
} }
namespace {
void update_ocsp_ssl_ctx(SSL_CTX *ssl_ctx) {
auto tls_ctx_data =
static_cast<ssl::TLSContextData *>(SSL_CTX_get_app_data(ssl_ctx));
auto cert_file = tls_ctx_data->cert_file;
std::vector<uint8_t> 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<std::mutex> 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 } // namespace shrpx

View File

@ -32,6 +32,9 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#ifndef NOTHREADS
#include <future>
#endif // !NOTHREADS
#include <openssl/ssl.h> #include <openssl/ssl.h>
@ -48,7 +51,6 @@ class Worker;
struct WorkerStat; struct WorkerStat;
struct TicketKeys; struct TicketKeys;
// TODO should be renamed as ConnectionHandler
class ConnectionHandler { class ConnectionHandler {
public: public:
ConnectionHandler(struct ev_loop *loop); ConnectionHandler(struct ev_loop *loop);
@ -77,8 +79,23 @@ public:
void set_graceful_shutdown(bool f); void set_graceful_shutdown(bool f);
bool get_graceful_shutdown() const; bool get_graceful_shutdown() const;
void join_worker(); 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: private:
#ifndef NOTHREADS
std::future<void> ocsp_result_;
#endif // !NOTHREADS
std::vector<SSL_CTX *> all_ssl_ctx_;
// Worker instances when multi threaded mode (-nN, N >= 2) is used. // Worker instances when multi threaded mode (-nN, N >= 2) is used.
std::vector<std::unique_ptr<Worker>> workers_; std::vector<std::unique_ptr<Worker>> workers_;
// Worker instance used when single threaded mode (-n1) is used. // Worker instance used when single threaded mode (-n1) is used.
@ -94,6 +111,7 @@ private:
// acceptor for IPv6 address // acceptor for IPv6 address
std::unique_ptr<AcceptHandler> acceptor6_; std::unique_ptr<AcceptHandler> acceptor6_;
ev_timer disable_acceptor_timer_; ev_timer disable_acceptor_timer_;
ev_timer ocsp_timer_;
unsigned int worker_round_robin_cnt_; unsigned int worker_round_robin_cnt_;
bool graceful_shutdown_; bool graceful_shutdown_;
}; };

View File

@ -28,6 +28,11 @@
#include <netdb.h> #include <netdb.h>
#include <netinet/tcp.h> #include <netinet/tcp.h>
#include <pthread.h> #include <pthread.h>
#include <sys/types.h>
#include <sys/wait.h>
#ifndef NOTHREADS
#include <spawn.h>
#endif // !NOTHREADS
#include <vector> #include <vector>
#include <string> #include <string>
@ -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<TLSContextData *>(SSL_CTX_get_app_data(ssl_ctx));
{
std::lock_guard<std::mutex> g(tls_ctx_data->mu);
auto &data = tls_ctx_data->ocsp_data;
if (!data.empty()) {
auto buf = static_cast<uint8_t *>(
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; return SSL_TLSEXT_ERR_OK;
} }
} // namespace } // 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_servername_callback(ssl_ctx, servername_callback);
SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx, ticket_key_cb); 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); SSL_CTX_set_info_callback(ssl_ctx, info_callback);
// NPN advertisement // NPN advertisement
@ -426,6 +457,12 @@ SSL_CTX *create_ssl_context(const char *private_key_file,
// ALPN selection callback // ALPN selection callback
SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_proto_cb, nullptr); SSL_CTX_set_alpn_select_cb(ssl_ctx, alpn_select_proto_cb, nullptr);
#endif // OPENSSL_VERSION_NUMBER >= 0x10002000L #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; return ssl_ctx;
} }
@ -929,7 +966,8 @@ bool check_http2_requirement(SSL *ssl) {
return true; return true;
} }
SSL_CTX *setup_server_ssl_context(CertLookupTree *cert_tree) { SSL_CTX *setup_server_ssl_context(std::vector<SSL_CTX *> &all_ssl_ctx,
CertLookupTree *cert_tree) {
if (get_config()->upstream_no_tls) { if (get_config()->upstream_no_tls) {
return nullptr; 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(), auto ssl_ctx = ssl::create_ssl_context(get_config()->private_key_file.get(),
get_config()->cert_file.get()); get_config()->cert_file.get());
all_ssl_ctx.push_back(ssl_ctx);
if (get_config()->subcerts.empty()) { if (get_config()->subcerts.empty()) {
return ssl_ctx; return ssl_ctx;
} }
@ -950,6 +990,7 @@ SSL_CTX *setup_server_ssl_context(CertLookupTree *cert_tree) {
for (auto &keycert : get_config()->subcerts) { for (auto &keycert : get_config()->subcerts) {
auto ssl_ctx = auto ssl_ctx =
ssl::create_ssl_context(keycert.first.c_str(), keycert.second.c_str()); 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( if (ssl::cert_lookup_tree_add_cert_from_file(
cert_tree, ssl_ctx, keycert.second.c_str()) == -1) { cert_tree, ssl_ctx, keycert.second.c_str()) == -1) {
LOG(FATAL) << "Failed to add sub certificate."; LOG(FATAL) << "Failed to add sub certificate.";
@ -984,6 +1025,114 @@ CertLookupTree *create_cert_lookup_tree() {
return new ssl::CertLookupTree(); 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<uint8_t> &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<uint8_t, 4096> 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<uint8_t> &out, const char *cert_file) {
char *const argv[] = {
const_cast<char *>(get_config()->fetch_ocsp_response_file.get()),
const_cast<char *>(cert_file), nullptr};
char *const envp[] = {nullptr};
if (exec_read_stdout(out, argv, envp) != 0 || out.empty()) {
return -1;
}
return 0;
}
} // namespace ssl } // namespace ssl
} // namespace shrpx } // namespace shrpx

View File

@ -28,6 +28,7 @@
#include "shrpx.h" #include "shrpx.h"
#include <vector> #include <vector>
#include <mutex>
#include <openssl/ssl.h> #include <openssl/ssl.h>
#include <openssl/err.h> #include <openssl/err.h>
@ -42,6 +43,18 @@ class DownstreamConnectionPool;
namespace ssl { 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<uint8_t> ocsp_data;
// Path to certificate file
const char *cert_file;
};
// Create server side SSL_CTX // Create server side SSL_CTX
SSL_CTX *create_ssl_context(const char *private_key_file, SSL_CTX *create_ssl_context(const char *private_key_file,
const char *cert_file); const char *cert_file);
@ -143,8 +156,10 @@ std::vector<unsigned char> set_alpn_prefs(const std::vector<char *> &protos);
// and if upstream_no_tls is true, returns nullptr. Otherwise // and if upstream_no_tls is true, returns nullptr. Otherwise
// construct default SSL_CTX. If subcerts are available // construct default SSL_CTX. If subcerts are available
// (get_config()->subcerts), caller should provide CertLookupTree // (get_config()->subcerts), caller should provide CertLookupTree
// object as |cert_tree| parameter, otherwise SNI does not work. // object as |cert_tree| parameter, otherwise SNI does not work. All
SSL_CTX *setup_server_ssl_context(CertLookupTree *cert_tree); // the created SSL_CTX is stored into |all_ssl_ctx|.
SSL_CTX *setup_server_ssl_context(std::vector<SSL_CTX *> &all_ssl_ctx,
CertLookupTree *cert_tree);
// Setups client side SSL_CTX. This function inspects get_config() // Setups client side SSL_CTX. This function inspects get_config()
// and if downstream_no_tls is true, returns nullptr. Otherwise, only // 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. // this function returns nullptr.
CertLookupTree *create_cert_lookup_tree(); CertLookupTree *create_cert_lookup_tree();
int get_ocsp_response(std::vector<uint8_t> &out, const char *cert_file);
} // namespace ssl } // namespace ssl
} // namespace shrpx } // namespace shrpx

View File

@ -963,6 +963,7 @@ int64_t parse_uint(const uint8_t *s, size_t len) {
} }
double parse_duration_with_unit(const char *s) { double parse_duration_with_unit(const char *s) {
constexpr auto max = std::numeric_limits<int64_t>::max();
int64_t n; int64_t n;
size_t i; size_t i;
auto len = strlen(s); auto len = strlen(s);
@ -976,17 +977,36 @@ double parse_duration_with_unit(const char *s) {
switch (s[i]) { switch (s[i]) {
case 'S': case 'S':
case 's': case 's':
// seconds
if (i + 1 != len) { if (i + 1 != len) {
goto fail; goto fail;
} }
return static_cast<double>(n); return static_cast<double>(n);
break;
case 'M': case 'M':
case 'm': case 'm':
if (i + 1 == len) {
// minutes
if (n > max / 60) {
goto fail;
}
return static_cast<double>(n) * 60;
}
if (i + 2 != len || (s[i + 1] != 's' && s[i + 1] != 'S')) { if (i + 2 != len || (s[i + 1] != 's' && s[i + 1] != 'S')) {
goto fail; goto fail;
} }
// milliseconds
return static_cast<double>(n) / 1000.; return static_cast<double>(n) / 1000.;
case 'H':
case 'h':
// hours
if (i + 1 != len) {
goto fail;
}
if (n > max / 3600) {
goto fail;
}
return static_cast<double>(n) * 3600;
} }
fail: fail:
return std::numeric_limits<double>::infinity(); return std::numeric_limits<double>::infinity();
@ -1000,7 +1020,16 @@ std::string duration_str(double t) {
if (frac > 0) { if (frac > 0) {
return utos(static_cast<int64_t>(t * 1000)) + "ms"; return utos(static_cast<int64_t>(t * 1000)) + "ms";
} }
return utos(static_cast<int64_t>(t)) + "s"; auto v = static_cast<int64_t>(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) { std::string format_duration(const std::chrono::microseconds &u) {

View File

@ -303,6 +303,8 @@ void test_util_parse_duration_with_unit(void) {
CU_ASSERT(0.500 == util::parse_duration_with_unit("500ms")); CU_ASSERT(0.500 == util::parse_duration_with_unit("500ms"));
CU_ASSERT(123. == util::parse_duration_with_unit("123S")); CU_ASSERT(123. == util::parse_duration_with_unit("123S"));
CU_ASSERT(0.500 == util::parse_duration_with_unit("500MS")); 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<double>::infinity(); auto err = std::numeric_limits<double>::infinity();
// check overflow case // check overflow case
@ -321,6 +323,9 @@ void test_util_duration_str(void) {
CU_ASSERT("1s" == util::duration_str(1.)); CU_ASSERT("1s" == util::duration_str(1.));
CU_ASSERT("500ms" == util::duration_str(0.5)); CU_ASSERT("500ms" == util::duration_str(0.5));
CU_ASSERT("1500ms" == util::duration_str(1.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) { void test_util_format_duration(void) {

View File

@ -27,3 +27,6 @@ noinst_LTLIBRARIES = libhttp-parser.la
libhttp_parser_la_SOURCES = \ libhttp_parser_la_SOURCES = \
http-parser/http_parser.c \ http-parser/http_parser.c \
http-parser/http_parser.h http-parser/http_parser.h
dist_pkgdata_SCRIPTS = h2o/fetch-ocsp-response

10
third-party/h2o/README.rst vendored Normal file
View File

@ -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.

150
third-party/h2o/fetch-ocsp-response vendored Executable file
View File

@ -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>] <certificate-file>
Options:
--issuer <file> issuer certificate (if omitted, is extracted from the
certificate chain)
--openssl <cmd> 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;
}